你的npm仓库也许不需要任何打包工具
为什么你的仓库不需要打包工具
如果你的仓库使用的是打包工具进行编译和打包,那么
- 优点是仓库对业务方没有影响
- 缺点是它可能会增加仓库的复杂性,并且有可能对业务方造成一些限制
举个实际性的例子,你的仓库可能会考虑通过异步的import()
来进行代码分割,如果你将仓库打包成一个单一的bundle
文件,然后业务方是无法通过异步的import()
来对该仓库的代码进行代码分割的。
打包仓库前先考虑以下问题
如果你想生成一个UMD
文件以至于能通过<script>
标签来引入到业务方的项目中,或许你需要一个打包工具。但是未来UMD
的形式将不再流行,你也许会更倾向于使用单一的ESM
模块。但这都是后话了,你仍然需要考虑
- 如果你不需要打包依赖项,那么使用打包工具的收益是什么?
- 如果你需要打包依赖项,那么你就不允许业务方通过
semver
语义化的方式更新你的子依赖项吗?
我的建议:别使用打包工具,
tsc
和babel
能解决大部分的组件库编译场景
如何正确使用 babel 和 tsc 编译我们的 npm 包
通过 babel 进行编译
.babelrc 关键配置示例如下
export default {
presets: [
[
'@babel/preset-env',
{
modules: false, // 保持ES modules,可根据需求修改
},
],
'@babel/preset-typescript', // 编译ts
],
plugins: [
[
'@babel/plugin-transform-runtime' // 将通用helper方法通过@babel/runtime的形式引入
],
],
}
modules:false
:保持 ES modules,我的是前端环境,可根据自身需求修改- 也许你会问我为什么不引入
usage
的 polyfill,个人建议不在 npm 包中引入 polyfill,有可能你引入的和业务方的有冲突,写好库的 README 比什么都强
通过 tsc 对类型进行检查并生成声明文件
tsconfig.json 配置示例如下
{
"include": ["src"],
"compilerOptions": {
"target": "es2018",
"outDir": "dist",
"lib": ["dom", "esnext"],
"declaration": true,
// "moduleResolution": "node",
"sourceMap": true,
"strict": true,
"esModuleInterop": true
}
}
moduleResolution:node
:如果你不使用 ES modules,那么你应该设置此属性
修改包配置启用编译
package.json 示例如下
{
"name": "hello-world",
"version": "1.0.0",
"main": "dist/index.js",
"type": "module",
"scripts": {
"prebuild": "rm -rf dist && tsc",
"build": "babel src --out-dir dist --extensions '.ts,.tsx' --delete-dir-on-start --source-maps"
},
"files": ["src", "dist"],
"peerDependencies": {
"antd": "^4.0.0"
}
}
- files 里面为啥要包括 src 目录? - 为了方便 sourcemap
-
antd 为什么要放在 peerDependencies 里? -
- 为了防止和业务方的 antd 仓库冲突;
- 为了防止 antd 仓库的多次安装
顺便提一嘴仓库在使用 dependencies 声明依赖库的特点,来表明仓库使用 peerDependencies 的重要性
- 业务方显示依赖的核心库,会忽略仓库的 peerDependency 声明
- 业务方没有显示依赖核心库,则按照插件的 peerDependency 中声明的版本将库安装到项目根目录中
- 当业务方依赖的版本和仓库依赖的版本不一致,会报错让用户修复
所以一般会有冲突的包,比如react-dom
、react
、antd
等都可通过放在 peerDependency 中引入,就不会和业务方的版本冲突啦!
那么问题来了,peerDependency 在开的时候很影响开发体验啊,npm install
不会安装它。解决方法如下:
- npm i -D install-peers-cli
- 在 npm scripts 中添加
"prepare": "install-peers",
,prepare 会在每次npm install
后执行,非常 nice~
当然,你也可以既兼容 cjs,也可兼容 esm。去除 type:module,添加 module: esm/index.js 为 ESM 的入口,main:dist/index.js 为 Commonjs 的入口,如果业务方不支持 esm 会进入到 main 入口哒~
处理样式文件
有以下四种方案:
- 告知业务方增加 less-loader。会导致业务方使用成本增加;
- 打包出一份完整的 css 文件,进行全量引入。无法进行按需引入;
- css in js 方案;
- 每个组自己提供一份 css.js 文件,引入该组件 css 样式依赖,而非 less 依赖,组件库底层抹平差异。
对于组件库来说,如果需要开放出去,最好考虑 3 和 4 方案,优缺点可看我参考的 React 组件库编译打包文章
由于我这里是公司内部使用,采用一方案了直接拷贝 less 文件:
在 babel 参数添加 --copy-files
,会自动将不符合编译条件(未编译)的文件拷贝到目标文件夹中,这样做的缺点很明显,若业务方没使用 less 预处理器,使用的是 sass 方案甚至原生 css 方案,那现有方案就搞不定了。
babel src --out-dir dist --extensions '.ts,.tsx' --delete-dir-on-start --source-maps --copy-files
如果需要在保持开发体验的同时,减少业务方使用的成本。那就最好采用 2,3,4 方案。如果还需要按需加载 css 样式的要求,那就只能考虑 3,4 方案了。4 方案是 antd 采用的方案,但是需要业务方自己手动管理依赖,但是也可以通过 babel-plugin-import 插件来自动帮我们管理依赖。看了以下示例你们就懂第四种的方式了。
假设我们有以下文件:
- index.js (引入 styles.less)
- Button.jsx
- styles.less
- css.js (引入 styles.css,由 index.js 生成,最后提供给业务方使用)
- styles.css (styles.less 编译生成,最后提供给业务方使用)
css.js
import "style.less"
Example.jsx 文件中不引入样式文件,如果业务方要使用我们的组件,他们需要
import { Button } from "ui"
import "ui/Button/css"
我们需要构建的时候就可以抹平这些差异,将styles.less
编译成styles.css
,再将css.js
中的内容替换成import styles.css
就可以了。这样不仅可以兼顾开发体验和使用成本,还能按需加载样式,但是增加了使用成本,下面会介绍 babel-plugin-import 插件。
按需加载
按需加载的原理是结合 ES module 和 tree-shaking 特性实现
说到按需加载,就不得聊聊 package.json 中的sideEffects
属性和 babel 的babel-plugin-import 插件。
- babel-plugin-import 插件是将类似
import {array} from lodash
转化为import array from lodash/array
,改变组件的引用路径实现按需加载,如果你的组件库有个统一的导出出口 index.js 就没必要用此插件了 - sideEffects 是告诉 webpack 等打包工具,哪些文件是有副作用的,没办法被安全 tree shaking
sideEffects
说一下它常用的两个类型
- false:标记哪些文件没有副作用,则表示组件库的所有 js 文件都没有副作用,可以用路径替换的方式放心实现 tree-shaking。
- 数组:数组内的文件都标记为有副作用的,打包工具不能将这些文件 shaking 掉。有个常见的 bug 就是样式丢失,如果不设置 sideEffects:[.less,.css],如果你在业务代码中
import styles.less
,会导致最后打包的文件没有你想要的样式,因为打包工具认为没有地方引用到了该文件的内容。
babel-plugin-import
该插件决定将哪个库通过改变路径来实现 tree-shaking,有个关于样式按需加载的属性是style
,当我们不想通过css in js
的方案进行样式加载的时候,我们需要告知业务方,你引入了我们的组件,但是你也要引入它的样式,也就是我们第四种方案。
import { Button } from "ui"
import "/ui/Button/style"
但是这样对于业务方来说,他们不可能引入一个组件就引入一个样式啊,这样的使用体验也太不好了。当然,组件库也可以导出一个完成的样式文件,只引入一次就够了,但是这又与我们的按需加载相去甚远。此时,告知业务方去使用 babel-plugin-import 的 style 属性就很重要。
通过插件源码可以发现:
else if (style === true) {
addSideEffect(file.path, `${path}/style`);
} else if (style === 'css') {
addSideEffect(file.path, `${path}/style/css`);
}
style
属性的作用是当你引入组件库的时候,自动在业务组件中插入import Button/style
或import Button/style/css
代码来按需引入样式的 js 文件
注意:两者的区别,style: true
在编译时引入源文件而style: “css”
在打包前按引入源文件。在编译时引入文件能有效利用 tree-shaking 减少 bundle 体积,而在打包前不能,这是 ES module 静态 import 的特性。
Q & A
-
为什么引入 Button 组件的样式不是直接引入 style.less,而是通过 css.js 文件做了个代理?
-
管理样式依赖,保障组件库开发者的开发体验 DX;
如果 Button 引用了 Icon 组件,那么使用方每次引入 Button 也需要去引入 Icon 的样式,如果我们拿 css.js 做一个统一的出口,那么只需要引入一次 css.js 就可以引入 Button 组件及其依赖组件的样式了。依赖复杂的情况下开发效率及其高效。
-
-
为什么不在 Button 中直接引入
import style.less
来引入 css 文件呢,这样外部就不用考虑 css 的按需加载了?-
减轻业务方的使用成本。
这样的话就需要业务方配置好 less-loader,如果业务方不使用 less-loader,在组件库内直接使用 import style.css 的开发体验岂不是直线下降?所以需要业务方引入 css.js 文件,css.js 文件引用的是编译好的 css 样式文件依赖,而不是 less 依赖,组件库底层抹平差异。
-