前端组件库构建那些事儿(一)

1,700 阅读6分钟

怎么使用组件库?

作为一个开发者,通常会用如下方式使用组件库。

1. 使用 <script> 标签

即直接在 html 页面中,使用既传统又新潮(module)的 <script> 标签,来引入组件库代码。

2. 借助构建工具

大部分情况下,前端项目还是基于 npm 安装组件库的包,之后再借助 webpack、vite 或其他构建工具使用组件库。 (当然你也可以不用 npm,直接复制粘贴一把梭~)

所以一个组件库应该构建出什么产物呢?

有什么构建产物?

构建产物当然就是 js + css,以及类型声明的 d.ts 文件,但是这些文件的具体格式和组合则有些讲究~

1. umd + css + css vars(变量)

适用于直接使用 <script> 标签的场景。

最基础最简单的产物格式,即产出一个 UMD (Universal Module Definition) 格式的 js 代码文件,平平无奇的样式文件。 由于这样使用不经过构建工具处理,所以还需要产出对应的压缩文件。 这里的样式文件产物可以有两种:

  • css files(钦定的 css)
  • css vars files(使用 css 变量的 css 文件,方便自定义主题)。

总结:

  • 优点:兼容性好,无论是 cdn 直接引用,还是作为 npm 包通过构建工具使用皆可。
  • 缺点:代码体积大,无论组件库中有没有被用到代码,通通被引入。可能引起页面性能问题,尤其是在移动端使用。

举个使用 umd 产物,在页面上输出 vue 版本号的例子:

<script src="https://unpkg.com/vue/dist/vue.js"></script><script>
  document.body.textContent = 'Vue version ' + window.Vue.version;
</script>

2. esm files

适用于使用构建工具的场景。一般不需要输出压缩代码。

在不使用插件的情况下,要想使用 tree-shaking 来减少打包代码的体积,首要前提必须是 esm 格式的源代码。

在这种情况下各个组件都会构建出 esm 格式的 js,并在入口文件中统一导出

这样在使用组件库时,可以直接 import 用到的组件,随后在打包时会自动将没有用到的组件代码“摇掉”(后文会详细说明)。

// 打包时只会引入 Button 相关代码
import { Button } from 'foo-ui';

样式构建产物

  • a. css files
  • b. css vars files
  • c. less(scss/stylus) files

输出 css 类型的样式,是为了方便直接使用。 若是有其他需求,那么 css vars 和 less 等预编译类型的样式文件可以用于自定义样式。

3. esm browser files

适用于直接使用 <script> 标签的场景。

样式构建产物

  • a. css files
  • b. css vars files

与前一种构建产物类似,但适用于直接在浏览器端使用 module 类型的 <script> 标签引入代码。

但与 esm 格式不同的地方在于,构建出的代码需要包含所有依赖包的代码,而 esm 格式的构建产物不用,那些组件库的依赖只需要直接 import 即可。

esm browser 模式有一个缺点是兼容性不好

lol

但它也有一个优点就是,正因为兼容性不好,所以构建时 babel 可以不用将代码转换地兼容性太好。(笑死

js-modules-via-script-tag

兼容性不好这个问题还好,例如一些用于展示 demo 类的页面,或者面向用户都是开发者,那么这个问题就可以绕过去。

4. cjs files

适用于不支持 esm 格式的服务端渲染场景。

但随着 Node.js 的版本升级,对于 esm 格式的支持越来越好,之后将渐渐淘汰 cjs 格式的构建产物。

2017-09-12,Node.js 8.5.0 发布了对ECMAScript模块的实验性支持。这种ECMAScript模块的支持是需要在后面加上 --experimental-modules 标识来运行。 Node.js CHANGELOG_V8

2019-11-21,Node.js 13.2.0 起开始正式支持 ES Modules 特性。移除了 --experimental-modules 启动参数。 Node.js CHANGELOG_V13

5. 类型声明文件 *.d.ts

手写编写

例如适配 Vue v2 版本的许多组件库都是手写的类型声明。

自动生成

自动生成的前提就是你的组件库源代码要是用 TypeScript 写的。 随着 ts 的生态越来越好,现在新版本的组件库基本都是用 ts 写的。

如何实现按需加载?

开发者一般会这么使用组件库,例如希望最后打包的时候只引入使用到的 Button 组件的代码。

import { Button } from 'foo-ui';

babel 插件

在构建工具对于 tree-shaking 支持不好的情况下,一般会安装 babel 插件。曲线救国,间接实现只引入用到的组件。

原理其实就是在 babel 处理源代码的时候,将以上写法转换成直接引用对应组件

import { Button } from 'antd';
ReactDOM.render(<Button>xxxx</Button>);
      ↓ ↓ ↓ ↓ ↓ ↓
var _button = require('antd/lib/button');
ReactDOM.render(<_button>xxxx</_button>);

甚至实现样式的按需加载

{
  "libraryName": "element-ui",
  "styleLibraryDirectory": "lib/theme-chalk",
}
​
import { Button } from 'element-ui';
      ↓ ↓ ↓ ↓ ↓ ↓
var _button = require('element-ui/lib/button');
require('element-ui/lib/theme-chalk/button');

但其实除非你用的还是 webpack v1 版本,不然现在构建工具基本都支持 tree-shaking。

js 部分

只要你用的是 import、export 而非 require 来编写组件库。

从 webpack v4 版本开始,通过 package.json 的 "sideEffects" 属性作为标记,向 compiler 提供提示,表明项目中的哪些文件是 "pure(纯正 ES2015 模块)",由此可以安全地删除文件中未使用的部分。

{
  "name": "your-project",
  "sideEffects": false
}

样式部分

纯 js 的 tree-shaking 思路还很明确清晰。但组件库中肯定会包含具有副作用的不纯的样式代码。

这时就需要在 "sideEffects" 属性中标记这些有副作用的样式代码。

{
  "name": "your-project",
  "sideEffects": ["./src/some-side-effectful-file.js", "*.css"]
}

想要了解更多可以看看 webpack 文档:webpack.docschina.org/guides/tree…

在加载样式方面,一般的组件库直接就打包成一个大 css 文件了,手动引入即可。 更进一步有的组件库将每个组件的样式单独打包,但这样就需要手动引用各个组件对应的样式文件,或者借助 babel 插件,走代码转换的路子。 但其实在构建出的 js 文件中直接 import 对应样式,就可以实现自动加载组件样式代码,并且在打包后也自动删除未引用到的组件样式代码。

但在实践中,发现 vite 对于 "sideEffects" 这个属性似乎支持的不太好,默认将样式都标记为有副作用,于是我就顺便提了个 issue 4389

用什么构建工具?rollup VS webpack?

先说结论:选 rollup

webpack 更适合对于应用的打包,rollup 更适合对于库的打包。

为什么不用 webpack?

webpack 无法构建出 esm 格式的 js 文件。 即使借助一些插件实现了(我没试过),产出代码也比不上 rollup 简洁、干净。

项目文件结构

├─ src
│   ├─ foo-component
│        ├─ __tests__/
│        ├─ demos/
│        ├─ style/
│        ├─ foo-component.tsx
│        ├─ index.ts
│        └─ README.md
├──── index.ts
├─ ...
├─ README.md
└─ package.json

foo-component/ 下导出文件说明

  • __tests__/: 组件单测文件夹
  • demos/: 组件文档示例文件夹
  • style/: 组件样式文件夹
  • foo-component.tsx: 组件本体
  • index.ts: 组件入口,可用于添加一些通用逻辑(例如为组件添加 install 方法)

package.json 相关字段

  • module: esm 格式的产物入口
  • main: cjs 格式的产物入口
  • typings: 类型声明的产物入口
  • sideEffects: 声明有副作用的代码,一般是样式

小结

本文从使用组件库的两种方式(script 标签和借助构建工具),说明有哪些构建产物类型以及如何实现 js 和样式的按需加载。

接着说明了为什么应该用 rollup 作为组件库的构建工具,并给出项目文件结构的示例和 package.json 中相关字段的说明。

希望本文能让读者对于组件库的构建有一个基本的概念和思路。下一部分将从实践方向详细介绍如何解决组件库构建过程中的各种问题和踩到的一些坑。

以上 to be continued...

参考资料