开发一个 npm 组件, 你是否了解需要对外导出什么格式的代码?如何让 npm 组件体积尽可能小?
整篇文章按照如下目录进行讲解:
- 为何需要打包
- 组件打包输出格式
- 如何打包 esm 模式代码(感兴趣选读)
- 减少组件打包体积的最佳实践
为何需要打包
首先,这里的打包概念解释一下, 只要有输出到新目录,就称为打包(免得大家对打包理解概念不一致)。
- 一份代码,多种消费方式
- 使用新特性语法,由于一般项目中,会默认不对 node_module 中的库进行编译以提高整个项目的编译速度,所以作为 npm 包,要转换成 es5 ,免得消费方吐槽……
打包格式
按照目前主流的模块系统来区分,可以先看一张图片宏观了解一下:
esm
如果是用 npm 组件来使用, 都推荐使用这种导出模式。
产生方式:
- rollup 声明 target 为 esm 或者 babel 编译之后生成一个新的目录 (iceworks 的做法)
- package.json 中声明 module,指向 esm
使用方式:
- 浏览器通过
<script type="module" />
引入 - 作为 npm 使用
特性
由于是静态的,所以可以使用 tree-shaking
umd
使用方式
- 浏览器通过
<script />
引入 - 浏览器通过 requirejs 或 seajs 引入 【目前这个已经很少使用了】
如何产生
- rollup 或者 webpack 声明 target 为 umd
- package.json 中声明 unpkg,指向对应文件
commonjs
使用方式
- node 端, npm 方式
如何产生
- rollup 或者 webpack 声明 target 为 commonjs
- package.json 中声明 main,指向对应文件 。
package.json 中引用优先级如下:target 为 web 时, 依次查找 browser、module 和 main。其他 target , 依次找 module 和 main。因此如果声明了 module, 会优先读取 module 中的路径。
因此, 在导出的时候,同时设置好 main 和 module 字段,这样就可以二者兼具了,在node端,浏览器端都可以正常使用。
webpack 如何打包 esm 模式
这里不讲 rollup , 毕竟写一个 target 就可以解决了。
大家都知道,webpack 的 target 没有支持 esm 模式, 而 rollup 提供了, 为此很多人也在吐槽,为什么 webpack 不做……
我们这使用的是 iceworks , 源码地址[1],它默认支持导出 esm 模式, 那就一起看看它的源码是怎么做的。它是用 build-plugin-component 这个来实现的。【贴部分源码,感兴趣的可以看看】
- 如果不是 jsx 或 tsx 文件, 则直接 copy 到目标目录,否则经过 bable 处理, 并将后缀改成 js
jsx.png
- 使用 babel-plugin-import 处理第三方依赖的组件库,且兼容没有 es 模块的第三方组件
ba.png
- 将 ts 解析生成 d.ts 文件
d.png
- bable 7 (@babel/preset-env ),若为 esm 模块, 则关闭 module 选项
其实它实现的很简单, 如果是 es 模块, 只是用 babel 将对应的 es6 语法编译成 es5 语法(且不选择modlue), 然后 copy 到新目录 es 下, 对于里面使用到的第三方依赖组件, 用 babel-plugin-import 做一下兼容处理。
组件打包体积的最佳实践
首先,尽可能提供 esm 的格式, 因为它可以走 tree-shaking ,摇掉不必要的文件。【webpack 只要开启 production 模式,就默认有 tree-shaking 功能】
tree-shaking
tree.png
定义
如果被标记为无副作用的模块没有被直接导出使用,打包工具会跳过进行模块的副作用分析评估。由此安全地删除文件中未使用的部分。
在打包阶段,webpack无法准确判断某个文件是否有副作用,所以默认认为所有文件都是有副作用的。也就是说这里sideEffects默认是true。
副作用:一个函数会、或者可能会对函数外部变量产生影响的行为。
- 模块作用域
将package.json 中sideEffects 设置为 false ,则表示改模块全部忽略副作用
- 局部文件
package.json 中 sideEffects 数组写对应文件,比如常见的写上 css 文件, 如 antd 的配置
- 函数级别
/*@__PURE__*/
声明函数无副作用
只要我们基本保证这个组件包没有对外部对象产生影响,就能设置 sideEffects: false 了
举个栗子:设置了 sideEffects: false 和 未设置 sideEffects: false 的情况如下, 可以看到体积确实减少了不少
image.png
更深入的 sideEffect 可以看看这篇文章[2]:
peerDependency
对于消费方可能也用到的组件,写到 peerDependency 中。看下面一张图就可以理解。这样可以减少重复打包。
举一个栗子:
代码语言:javascript复制"peerDependencies": {
"react": ">=16.12.0",
"react-dom": ">=16.12.0"
}
如上的配置,可以让组件库下的 node_modules 不安装 react,同时指定组件库使用方需安装的 react/reactDOM 的版本。
external
对于打包成 umd 的文件,由于它无法分析是否存在 peerDependencies, 所以如果使用方已有 react、 react-dom 等库,需要在webpack打包时,将 external 剔除掉对应依赖。
wepack5 模块联邦
external 还是静态的,如果项目支持, 还可以使用 wepack5 Module Federation 方式,由使用方动态决定是否下载依赖。
总结
1. 对外提供组件时,同时提供 esm ,commonjs, umd 这3种方式,并且在package 中对应的字段进行声明,以确保这个包可以兼容多环境。
2. 尽可能提供 esm 模式,并且如果这个组件没有影响外部变量时,设置 sideEffect 为 false, 让使用方可以最大的 tree-shaking 。对于公用的依赖包,将其写入 peerDependencies 中。
3. 若要提供 umd 模式, 在打包时, 将对应公用依赖写入 external 剔除对应依赖。
参考资料
[1]
源码地址: https://github.com/ice-lab/iceworks