承接上文,我们用 rollup 搭建了一个组件库 rollup-build
,但还有些需要优化的地方:
- 构建速度不够快
- 构建的产物只有 esm 模块,没有 umd 模块,iife 模块
本篇文章内容是基于上篇文章的内容,所以了解上篇文章的内容会对阅读更有帮助
构建产物准备了三种模块,每种模块都有它的作用:
- esm 用来支持 esmodule 环境的, 在大多数的开发环境,都是 esmodule,并且 esm 模式支持 tree shaking。所以 esm 是必要的
- umd 模块,支持三种模式的引入,一种是 commonjs,一种是 script,还有一种是 amd; 对于 commonjs 在一些 node 环境中是必要的。script 的场景就是在消费端将
rollup-build
external 的时候,就需要用 script 导入了。amd 的场景我还没遇到过
对于 commonjs,我还没有遇到要用 commojs 来开发前端页面的,但是很多库都支持,咱也不能落后不是。 可能一些构建工具在引入库的时候就是 commonjs? 不过对于 script,我倒认为需求场景比较多
- iife 模块,iife 模块也是 script 导入使用,但相较于 umd,我更倾向于认为 iife是适配低版本浏览器,只支持 es5,甚至更低的那种。在 script 引入组件库
rollup-build
的依赖(ps: react, reactd-dom),然后引入组件库rollup-build
,就可以直接使用。要做到这一点,就需要在 iife 的构建产物中包含所有的 es6 语法的 polyfill。
下面开始
准备
因为需要用到三种模块的构建,所以为了项目结构清晰,需要将不同模块的构建配置文件分开。
创建一个 config 文件夹,并且在里面创建三个 rollup 配置文件
由于不同配置文件的插件配置是高度重合的,所以单独列出一个getPlugins.js
文件处理插件,减少代码的重复性
getPlugin.js
javascript
// getPlugins.js
import commonjs from "@rollup/plugin-commonjs";
import image from "@rollup/plugin-image";
import resolve from "@rollup/plugin-node-resolve";
import nodeExternals from "rollup-plugin-node-externals";
import postcss from "rollup-plugin-postcss";
const basePlugins = [resolve(), commonjs(), postcss({ extract: "rollup-build.css" }), image()];
首先准备一个基础的 basePlugins,里面有各个模块都需要的基础功能,
resolve()
, 解析第三方依赖commonjs()
, 解析 commonjs 模块postcss({ extract: "rollup-build.css" })
,解析各种 css 文件格式,其中的参数表示将组件库的 css 样式都提取出来放到一个rollup-build.css
文件中image()
,解析各种图片格式,将图片转换为 base64 格式
准备 esm 的插件
javascript
// getPlugins.js
import esbuild from "rollup-plugin-esbuild";
import nodeExternals from "rollup-plugin-node-externals";
export const esmPlugins = [...basePlugins, nodeExternals(), esbuild()];
构建 esm 模块,编译插件需要处理 ts 和 react 代码,上篇文章中是用rollup-plugin-typescript
处理 ts 代码,用rollup-plugin-babel
处理 react 代码,输出的最后结果是 es6 代码。
这份工作完全可以交给 esbuild 来做,esbuild 可以同时处理 ts 和 react 代码,而且速度还一个数量级
在代码编译, esbuild 相较于 webpack,babel 会快很多
nodeExternals 是用来排除所有的所有的第三方依赖的,这样组件库的构建产物体积就会足够的小
准备 umd 模块
javascript
// getPlugins.js
export const umdPlugins = [
...basePlugins,
nodeExternals(),
esbuild({
tsconfigRaw: {
compilerOptions: {
jsx: "react",
},
},
}),
];
umd 模块相比于 esm 模块,有个不同的地方--esbuild 的参数。这个参数的作用是使构建之后的 react 组件生成 reactElement 函数是 React.createElement
,而不是 jsx/runtime
中的函数。
这是 umd 构建产物 -- Input 组件 的截图
这样做的目的是用 script 引入的时候,可以少引入一个jsx/runtime
的包
使用了这种设置,是不是需要在组件中显示引入 React
变量呢?答案是:不需要
准备 iife 模块
javascript
// getPlugins.js
import { babel } from "@rollup/plugin-babel";
import { DEFAULT_EXTENSIONS } from "@babel/core";
export const iifePlugins = [
...basePlugins,
esbuild({
tsconfigRaw: {
compilerOptions: {
jsx: "react",
},
},
}),
// 处理低版本浏览器的polyfill
babel({
presets: [
[
"@babel/preset-env",
{
targets: "> 0.25%, not dead, IE 10",
},
],
],
exclude: /node_modules/,
extensions: [...DEFAULT_EXTENSIONS, ".ts", ".tsx"],
babelHelpers: "inline", //将所有的polyfill代码打包进bundle中
}),
];
准备 iife 模块时,不需要将所有的第三方依赖都 external 掉,需要 external 的依赖会在 rollup.external 配置中单独列出来,这也是为了减少 script 引入产物的考量。所以这里拿掉了nodeExternals()
插件
因为 esbuild 并不能很好支持 es5 代码,所以,还单独加了一个 babel 插件,目的是将 es6 代码转译成 es5。其中的一些配置需要我们注意
- 一个 targets,表示我们需要支持的浏览器数量,
- 还有一个是 extensions,用来告诉 bable 插件,你需要它处理哪些文件。它默认是没有 ts、tsx 后缀的,所以我们额外添加。(这是坑,需要特别注意)
- babelHelpers 设置成 inline, 目的将 polyfill 代码都打包进构建产物中,而不是向 runtime 一样引入其中。虽然会导致构建产物增加,不过这是没有办法的事情
其实要实现这个目的,
babelHelpers: bundle
其实是个更好的选择,但我用了这个值,构建就会报错,这是为什么:他们的区别在于 inline 会在构建产物每个文件都放一份 polyfill,而 bundle 对构建产物的所有文件都只有一个 polyfill。所以inline 模式会比 bundle 的构建产物体积更大。
不过,其实对我也没有影响,因为我构建产物只有一个文件呀😄
到此,getPlugins.js
文件也就准备好了:
javascript
// getPlugins.js
import { DEFAULT_EXTENSIONS } from "@babel/core";
import { babel } from "@rollup/plugin-babel";
import commonjs from "@rollup/plugin-commonjs";
import image from "@rollup/plugin-image";
import resolve from "@rollup/plugin-node-resolve";
import esbuild from "rollup-plugin-esbuild";
import nodeExternals from "rollup-plugin-node-externals";
import postcss from "rollup-plugin-postcss";
const basePlugins = [resolve(), commonjs(), postcss({ extract: "rollup-build.css" }), image()];
export const esmPlugins = [...basePlugins, nodeExternals(), esbuild()];
export const umdPlugins = [
...basePlugins,
nodeExternals(),
esbuild({
tsconfigRaw: {
compilerOptions: {
jsx: "react",
},
},
})
];
// script的依赖过多是不友好的
export const iifePlugins = [
...basePlugins,
esbuild({
tsconfigRaw: {
compilerOptions: {
jsx: "react",
},
},
}),
// 处理低版本浏览器的polyfill
babel({
presets: [
[
"@babel/preset-env",
{
targets: "> 0.25%, not dead, IE 10",
},
],
],
exclude: /node_modules/,
extensions: [...DEFAULT_EXTENSIONS, ".ts", ".tsx"],
babelHelpers: "inline", //将所有的polyfill代码打包进bundle中
}),
];
rollup.config.es.mjs
这是 esm 模块的配置文件,内容很简单:
javascript
// rollup.config.es.mjs
import { esmPlugins } from "./getPlugins.js";
/**@type {import('rollup').RollupOptions} */
export default {
input: "./src/index.tsx",
output: {
dir: "./dist/es",
format: "esm",
sourcemap: true,
preserveModules: true,
},
plugins: esmPlugins,
};
入口时 src 中的 index.tsx
, 输出目录是dist
文件夹下面的es
文件夹。格式是 esm,生成 sourcemap,preserveModules: true
的意思是编译不打包,构建产物仍然是按照开发的目录结构
rollup.config.umd.mjs
这是 umd 模块的配置文件:
javascript
// rollup.config.umd.mjs
import { umdPlugins } from "./getPlugins.js";
/**@type {import('rollup').RollupOptions} */
export default {
input: "./src/index.tsx",
output: {
file: "./dist/umd/rollup-build.js",
format: "umd",
name: "RB",
sourcemap: true,
globals: {
react: "React",
"react-dom": "ReactDOM",
},
},
plugins: umdPlugins,
};
入口和 esm 模块一样,出口是 dist 文件夹下面的 umd 文件夹。umd 还需要给出 script 场景下的全局变量名RB
(取的是 rollup-build 首字母)
其中 globals 的配置目的是告诉 rollup,组件库中引入的 react,react-dom,在 script 场景中,它们的全局变量名是什么。这个变量名不能是自定义的,要看具体的 react umd 格式文件中是什么
那 react 举个例子:
先找到 ract 中 umd 格式的文件
红圈圈出来的位置,就是浏览器环境下的定义,给 global 上赋值了一个 React 属性,所以 react 库在 script 环境下的全局变量名称,就是 React
红圈上面两行,分别是 commonjs 环境,和 AMD 环境,这就是为什么 umd 可以支持三种环境的原因所在了
每 external 一个库,就要在 globals 中专门列出来一个,万一依赖库有十多个,那就太麻烦了。有没有一个库是专门维护这样一个映射关系的?
rollup.config.iife.mjs
这是 iife 模块的配置文件:
javascript
// rollup.config.iife.mjs
import { iifePlugins } from "./getPlugins.js";
/**@type {import('rollup').RollupOptions} */
export default {
input: "./src/index.tsx",
output: {
file: "./dist/iife/rollup-build.js",
format: "iife",
name: "RB",
sourcemap: true,
globals: {
react: "React",
"react-dom": "ReactDOM",
},
},
plugins: iifePlugins,
external: ["react", "react-dom"],
};
相同的地方就不讲了,重点关注和其他模块配置不同的地方
- format 要改成 iife
- 输出路径,是在
dist/iife
目录下面 - 要额外地写出要 external 哪些库。原因之前提到过,减少 script 场景下需要额外引入的依赖,所以只将消费端大概率也会 external 的库,external 出去
到此,所有模块的配置文件都准备好了,下面准备 package.json
package.json
json
{
"main": "./dist/umd/rollup-build.js",
"types": "./dist/types",
"module": "./dist/es/index.js",
}
这个配置是面向消费端的,配置了不同模块的入口。如果 commonjs 模块引入,node 就会返回 umd 下面的文件,如果是 esmodule 模块引入,node 就会返回 es 下面的文件。
下面来添加构建的 npm 脚本
json
{
"scripts": {
"prebuild": "rimraf dist && tsc",
"build": "npm run build:es & npm run build:umd & npm run build:iife",
"build:es": "rollup -c ./config/rollup.config.es.mjs",
"build:umd": "rollup -c ./config/rollup.config.umd.mjs",
"build:iife": "rollup -c ./config/rollup.config.iife.mjs",
},
}
当执行npm run build
,就会同时执行三种模块的构建,
&
与&&
不相同,&&
是串行执行,用来连接两个命令,如果前面的命令执行失败,就会阻塞后面的命令。&
是并行执行,也是连接两个命令,但前后命令不会相互影响
不同模块的构建并没有前后关系,所以这里用并行执行
其中,还有个 prebuild 的命令,其会在npm run build
之前执行。prebuild 内容中,首先是删除 dist 目录,然后执行 tsc,用来生成 ts 的类型文件,还兼有类型检查的功能。如果 ts 类型报错,就会停止构建功能。
当然,报错停止构建后,类型文件也没必要输出了,可以在 tsconfig 中加上"noEmitOnError": true
,就可以优化的实现这一点了。
构建脚本也准备好了,下面来执行下看看吧!
两个细节:
- 可以看到 log 是交替出现了,印证了构建过程是并行执行的
- iife 模块构建的时间是最长的,因为用到了 babel 对 es 代码降级
对比下 umd 和 iife 构建后的组件代码:
可以看到 iife 中函数被转换为了 function 形式,不是箭头函数
配置文件的再优化
rollup 支持导出数组类型的配置文件,也就是说可以同时对不同入口,不同模块进行构建
可以这么做:
在 config 文件夹下面创建一个 rollup.config.js
文件:
js
//rollup.config.js
import rollupConfigIife from "./rollup.config.iife.mjs";
import rollupConfigUmd from "./rollup.config.umd.mjs";
import rollupConfigEs from "./rollup.config.es.mjs";
export default [rollupConfigEs, rollupConfigUmd, rollupConfigIife];
文件中将不同模块的配置文件放在一数组中重新导出
package.json 中可以修改 npm run build
的脚本内容:
json
{
"scripts": {
"build": "rollup -c ./config/rollup-config.js",
},
}
其他的构建脚本可以不删除,类似与
npm run build:es
,npm run build:umd
,如果有特别需求,可以单独对某个模块进行构建
来执行一下:
可以看到, umd 构建时间显著小于 esm,也许是重用了 esm 模块的构建中间产物吧🤔
总结
这篇文章分享如何对 rollup 构建的组件库进行优化,优化主要两个方向,一个构建速度,构建速度用 esbuild 替代 bable;还有一个是不同模块的构建产物,构建产物分成了三种,分别是 esm, umd, iife。不同模块有其不同的特点,用来适应不同的使用场景
构建产物是准备好了,那怎么对产物进行测试呢?下篇文章来讲吧
rollup 构建相关的另外两篇文章: