做开源项目和企业项目不同的地方在于,前者的目标是在封装的同时,尽量去保持灵活性和可扩展性。这是一种开闭原则——对实现封闭,对扩展开放。这使得开源库/组件可以适用所有场景。
然而在设计可复用的业务组件时,我们通常考虑的是,该组件在复用时能不能尽量少写代码?
这里所指的代码,就是通用的基础组件为了遵循开闭原则而留待业务方自己实现的逻辑。举个例子,我们在 ant design 基础组件的基础上,试图把 Form 和 Table 组合为一个解决「对数据进行条件查询、展示、操作」的高层业务组件。
在设计这个组件时,如果我们希望这个组件适合所有场景,我们需要使以下几点具有可扩展性:
- 点击搜索按钮时,执行请求的具体实现。例如把表单项传给某个 api 接口。
- 得到响应后,表格应该从响应的哪个字段作为数据进行渲染?
- 表格的分页组件从响应的什么地方得到数据总数?
- 点击分页组件中时,分页信息(page size, page)应该作为哪种形式传到后端?
如此一来,使用这个组件的时候,至少需要写这些逻辑来适配这个组件:
+const onSearch = (values, page, pageSize) => {
+ return api.search(values, page, pageSize)
+}
+const getTotal = (res) => {
+ return res.data.metadata.total
+ // or return res.headers['x-total-count']
+}
+const getDataSource = (res) => {
+ return res.data.data
+}
<DataTable
+ onSearch={onSearch}
+ getDataSource={getDataSource}
+ getTotal={getTotal}
>每个团队在使用这个组件时,都要写一遍这样的代码。这种做法并不高效。这时如果我们在协议层面对响应的数据结构进行直接的约定,这些代码都不需要编写,因为组件可以直接根据约定渲染数据:
+const onSearch = (values) => {
+ return api.search(values)
+}
-const getTotal = (res) => {
- return res.data.metadata.total
- // or return res.headers['x-total-count']
-}
-const getDataSource = (res) => {
- return res.data.data
-}
<DataTable
+ onSearch={onSearch}
- getDataSource={getDataSource}
- getTotal={getTotal}
>假设我们规定使用 rpc 进行通讯,组件还可以写得更精简——只要传 rpc 的方法名:
<DataTable
+ method='search'
>