diff --git a/docs/blog/historical-debt.en-US.md b/docs/blog/historical-debt.en-US.md new file mode 100644 index 0000000000..c8c81c6260 --- /dev/null +++ b/docs/blog/historical-debt.en-US.md @@ -0,0 +1,83 @@ +--- +title: Historical Debt of API +date: 2023-10-11 +author: zombieJ +--- + +You may have received this warning when upgrading Ant Design: + +```text +Warning: [antd: XXX] `old prop` is deprecated. Please use `new prop` instead. +``` + +This is because antd has some historical debt in API design. For example, in antd v3 and before, the code of TreeSelect was directly copied from Select and extended on this basis. There are differences in their search styles: + +Select + +TreeSelect + +And in the maintenance process, developers want to control the content of the search box. Unfortunately, this requirement was PR by different developers at different times. So two different properties were added, one called `inputValue` and the other called `searchValue`: + +```tsx +// Select in combobox mode, the search box is the input box, `inputValue` looks reasonable + + +// TreeSelect 的搜索框在弹出层,`searchValue` 也很合理 + +``` + +在多选模式下,类 Select 组件在选择内容后会清除搜索框内容。但是有些场景下,开发者希望能够保留。因而 TreeSelect 和 Select 又添加了 `autoClearSearchValue` 属性。 + +等等,Select 明明叫 `inputValue`,为什么要叫 `autoClearSearchValue`?明显应该叫做 `autoClearInputValue` 呐。如果我们在现有的 API 上继续生长其他的同类 API 风格。你会发现组件的 prop 变得越来越分裂。这也会导致代码维护出现坏味道。例如上面这个例子,在之后我们对类 Select 组件抽成了统一的 UI 层并将其合并到 `rc-select` 组件中。`rc-tree-select` 只需要实现弹出层的内容,而输入框的结构和样式可以和 Select 完全复用。但是由于两者的 API 不一致,导致我们需要额外的处理,所以我们在迭代过程中需要对这些 API 债务进行重构并将其统一起来。(在 v4 中,我们将其合并为了 `searchValue` 并且对设计也进行了统一) + +然而世上没有银弹,我们无法在一开始就设计出完美的 API。有一些 API 在设计之初显得非常合理,而随着迭代又会发现或多或少不合时宜。比如说弹出层早期起名为 dropdown,这对应了 Dropdown 以及类 Select 组件的弹出内容。但是对于 Tooltip 而言,dropdown 显然是不适合的。从统一的角度看,popup 会更适合。 + +### 废弃警告 + +在维护过程中,我们逐渐统一了 API 命名规范([API Naming rules](https://github.com/ant-design/ant-design/wiki/API-Naming-rules))。在添加新的 feature 时,优先从现存的 API 中寻找接近。对于现存的 API,逐步添加废弃警告。为了保持兼容,我们的策略是每个版本提供的废弃警告会继续兼容一个大版本,而在下下个大版本中移除它。例如在 v4 中添加了废弃警告,那么在 v5 中仍然可以使用,但是在 v6 中将会被移除。以此确保开发者有足够的时间进行迁移。 + +但是从开发者角度看,这也并不合理。开发者本身只是对 antd 进行了升级,却要因为组件库 API 设计的失误而遭受 console 的侵扰。如果在废弃警告中混入几个使用警告,开发者往往很难发现它们。这种情况在大版本升级中尤为显著,业务可能并没有给你足够的时间去做升级迁移,因而不得不使用兼容包以及其他的一些技术手段让它先跑起来。而对于冗长的废弃警告,开发者不得不选择暂时(或者永远)无视它们。针对这种情况,使用警告会更为重要,因而我们提出了 [Warning Filter RFC](https://github.com/ant-design/ant-design/discussions/44551)。 + +#### 警告过滤 + +通过 ConfigProvider 的 `warning` 属性,可以将废弃信息进行聚合: + +```tsx + +``` + +聚合后,原本打平的废弃信息会合并为一个数组在 console 中展示。而对于使用警告则不会影响: + +![Merged Message](https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*MG-rQ4NSbbcAAAAAAAAAAAAADrJ8AQ/original) + +### 拓展问题 + +如上所述,API 设计不存在银弹。为了防止 breaking change,我们一般不会改动现有的 API 实现。但是对于一些约定的内容,这就会造成麻烦。比如说 `ref` 组件是很典型的约定,只要是 React 的开发者就能明白,通过 `ref` 可以获取 DOM 节点以及做一些诸如 `focus` 的基本操作。但是对于复合组件而言,调用方法和 DOM 不一定能够统一。比如说 Table 组件的 `ref` 显然应该是最外层的 div,但是对于 `scrollTo` 方法则应该对应到滚动容器上(如果是 VirtualTable 则应该交由内部的 `rc-virtual-list` 进行处理)。在 antd mobile 中 `ref` 被设计为复合结构,DOM 节点总是通过 `nativeElement` 返回: + +```tsx +export interface SampleRef { + nativeElement: HTMLElement; + focus(): void; + blur(): void; +} +``` + +而在 antd 中,由于我们早期没有对 `ref` 进行约定,导致在实现方法是就遇到了难题。不过好在有 Proxy 支持,我们可以通过 Proxy 对 `ref` 进行拦截并返回我们想要的结果: + +```tsx +useImperativeHandle( + ref, + () => + new Proxy(divRef.current, { + get(target, key) { + // ... + }, + }), +); +``` + +通过这种方式,我们可以继续兼容之前的使用。它仍然是一个 DOM 节点,但是同样也支持了 SampleRef 的定义调用。 + +## 总结 + +API 设计是个难题,随着技术栈以及组件本身的迭代。一些设计会逐渐腐朽,而 API 升级本身对于开发者也是痛苦的。我们希望通过这篇文章,让开发者能够理解我们的设计思路以及在升级过程中的一些问题。如果你有任何的建议或者想法,欢迎在 Github 中讨论。