背景
从一月初回公司到五一又开始请长假这四个月实习时间以来,我估计得有三分之一的时间是在写组件库的,所以写一篇 Blog 简要谈谈我对组件库的认识,和在这方面的实践。
我对组件库的认识
三大框架有一个共同点,就是组件化思想,把一个页面划分为多个组件,从一个个组件写起,最后组装成一个页面,乃至整个项目。而在这一个个业务组件里,我们或多或少都会使用到基础组件,或者叫做 UI 组件,就是只负责 UI 和自身逻辑的一些组件。比如一个 Input 输入框组件,它负责了基本的 UI 展示,包括各种 hover、focus、disabled 态,输入内容校验、以及对输入框内容变化、按下回车、点击清除图标时的回调函数进行封装等自身逻辑。这样在我们开发时就可以只专注于业务逻辑代码,而不需要再去重新写输入框的样式和基本逻辑。
可以说,维护自己的一个组件库的好处有:
将 UI 和业务逻辑分离开来,使业务方只需关注业务逻辑。
组件复用。一个项目中可能有很多个页面都使用到了输入框,通过组件库可以很方便地复用这些基础组件,大大提升了开发效率。
样式统一。因为基础组件都是在组件库中维护的,所以可以保证不同页面的同一个基础组件样式是一样的,不用在各个使用到的地方都维护一份组件样式。
可维护性高。通过组件库引入基础组件,使得业务组件代码量大大缩短,降低了耦合性和复杂度。并且基础组件都统一在组件库中管理,修改起来也很方便。
为什么要维护自己的组件库
前端估计是最喜欢造轮子的了,不说各个框架,只说组件库就有数不胜数个开源的。有的直接从零开始搭建一个组件库,有的则在已有的组件库的基础上进行二度封装,形成自己的组件库。那么,为啥要再搞一个自己的组件库呢?直接套用 Antd 或 ElementUI 等稳定又好使的组件库不好吗?我觉得主要原因是:每个项目都有自己的设计规范,像前端开发,基本上都会由 UI 设计师先出设计稿,具体给出每个页面的布局结构、每个地方的宽高、间距、字体、颜色等信息,再由前端编码还原设计稿。而像 Antd 等组件库,它的组件样式都是按它的设计规范来的,很可能并不适合于我们自己的项目。而要修改它的样式的话,就得去写全局的样式,来覆盖掉 Antd 的样式,这样可维护性不高。
如何基于 Antd 再度封装组件库
以下提到的 Antd 特指 3.22 版本。
既然只是修改 Antd 组件的样式而已,所以可以先新写一个组件,在这个新组件内部引用 Antd 组件,从而复用它的逻辑。
import { Input as AntdInput } from 'antd';
import { InputProps as AntdInputProps } from 'antd/es/input';
function MyInput(props: AntdInputProps) {
const newProps = {
// prefixCls 用来标志组件的类名前缀,后续会说到
prefixCls: 'dangosky-input',
...props
};
return <AntdInput {...newProps} />;
}
// 还得根据组件判断是否需要转发 Ref
export default MyInput;
在上面这个例子中,我们就已经实现了再度封装 Antd 的 Input 组件。通过观察 Antd 组件的 DOM 结构可以发现,所有的 Antd 组件都有一个统一的规范,它的类名都有一个固定的前缀,也就是 ant-{组件名}
,以此来标志每一个组件。为了和 Antd 做区别以及维护方便,我们可以统一用 dangosky
(是团子的天空幻想的意思呐~)来标志我们这个新的组件库。具体表现是:
使用
dangosky
来替代ant
作为每个组件的类名前缀。通过配置后可以使用
import { Input } from 'dangosky'
来导入我们的dangosky
组件库,使得项目使用的组件库从 Antd 迁移到dangosky
只需要修改它的导入包名就行,实现低成本迁移。
下面再说说基于 Antd 再度封装的组件库实现原理。因为只是对 Antd 的样式修改而已,所以主要就是修改组件的 className
类名、修改 less 样式文件中的类名:
1. 修改组件的类名
Antd 为每个组件的 props 都提供了一个 prefixCls
属性,用来作为每个组件的类名前缀。所以可以在我们的新组件中指定 prefixCls
,将其替换成我们自己的 dangosky
。这样组件中的 className
类名就可以从 ant-input
变成 .dangosky-input
了。
Antd 有一个 getPrefixCls
函数来处理这个逻辑,它是由外层的 configProvider
组件利用 React 的 Context 特性传递到内部组件的,可以无视组件嵌套的深度。
以 input 组件为例,Antd 内部是这样调用的。如果我们在组件内指定了 prefixCls
这个 prop 的话(它重命名为了 customizePrefixCls
),getPrefixCls
就会返回我们指定的类名前缀,否则就默认返回 ant-{组件名}
。
2. 修改样式文件的类名
将 DOM 节点的类名前缀改为 dangosky
后,还需要将 less 中的类名前缀也改为 dangosky
才行,不然会匹配不到相应的样式。Antd 里有一个全局的样式文件,包含了一些全局的样式变量,它会在每个组件中都被引入进去。其中有一个变量是 @ant-prefix
,less 中的每个类名都会在前面加上 @ant-prefix
。所以我们可以定制新组件库的主题文件,在里面将 @ant-prefix
值修改成 dangosky
,之后在每个新组件的样式文件中导入这个主题文件覆盖原来的 Antd 值就可以了。
3. 在组件里自动引入样式文件
在使用 Antd 组件的时候可以不用手动 import
进样式文件,Antd 提供了一个插件 babel-plugin-import 来实现按需加载和自动引入样式文件。
我们在使用某个组件的时候,都是直接 import { xxx } from 'antd'
。如果不加以额外的插件辅助的话,这样的写法会导入 antd 中的所有模块,如果要只加载指定的组件的话,我们就需要使用下面这种写法了。
import xxx from 'antd/es/xxx';
import 'antd/es/xxx/style';
这种写法的问题在于比较繁琐,那么有没有更简洁的方式来实现按需引入呢?我们可以使用 babel-plugin-import 插件,使用这个插件后,我们依旧可以使用 import { xxx } from 'antd'
来导入我们想要的组件,这个插件背后会帮我们转换成 antd/es/xxx
。可见源码。
babel-plugin-import
还会再自动导入进它的样式文件。从它的源码里可以看到,当在 babel 配置里设置了 style: true
的话,它会自动引入该组件下的 style
文件夹,所以就会默认引入我们写好的 index.tsx
文件(再在这里引入具体的样式文件)。除了取值为 true 外,还可以是 false 和一个函数,具体可以看文档。