为什么组件库打包用 Rollup 而不是 Webpack?

2024-11-20 09:22   重庆  

点击关注公众号,“技术干货” 及时达!



Rolup 是一个打包工具,类似 Webpack。

组件库打包基本都是用 Rollup。

那 Webpack 和 Rollup 有什么区别呢?为什么组件库打包都用 Rollup 呢?

我们来试一下:

mkdir rollup-test
cd rollup-test
npm init -y

我们创建两个模块:

src/index.js

import { add } from './utils';
function main() { console.log(add(1, 2))}
export default main;

src/utils.js

function add(a, b) {    return a + b;}
export { add}

很简单的两个模块,我们分别用 rollup 和 webpack 来打包下:

安装 rollup:

npm install --save-dev rollup

创建 rollup.config.js

/** @type {import("rollup").RollupOptions} */export default {    input: 'src/index.js',    output: [        {            file: 'dist/esm.js',            format: 'esm'        },        {            file: 'dist/cjs.js',            format: "cjs"        },        {            file: 'dist/umd.js',            name: 'Guang',            format: "umd"        }    ]};

配置入口模块,打包产物的位置、模块规范。

在 webpack 里叫做 entry、output,而在 rollup 里叫做 input、output。

我们指定产物的模块规范有 es module、commonjs、umd 三种。

umd 是挂在全局变量上,还要指定一个全局变量的 name。

上面的 @type 是 jsdoc 的语法,也就是 ts 支持的在 js 里声明类型的方式。

效果就是写配置时会有类型提示:


不引入的话,啥提示都没有:


这里我们用了 export,把 rollup.config.js 改名为 rollup.config.mjs,告诉 node 这个模块是 es module 的。

配置好后,我们打包下:

npx rollup -c rollup.config.mjs

看下产物:

image.png


三种模块规范的产物都没问题。

那用 webpack 打包,产物是什么样呢?

我们试一下:

npm install --save-dev webpack-cli webpack

创建 webpack.config.mjs

import path from 'node:path';
/** @type {import("webpack").Configuration} */export default { entry: './src/index.js', mode: 'development', devtool: false, output: { path: path.resolve(import.meta.dirname, 'dist2'), filename: 'bundle.js', libraryTarget: 'commonjs2' }};

指定 libraryTarget 为 commonjs2

打包下:

npx webpack-cli -c webpack.config.mjs

可以看到,webpack 的打包产物有 100 行代码:


再来试试 umd 的:


umd 要指定全局变量的名字。

打包下:



也是 100 多行。

最后再试下 es module 的:


libraryTarget 为 module 的时候,还要指定 experiments.outputModule 为 true。

import path from 'node:path';
/** @type {import("webpack").Configuration} */export default { entry: './src/index.js', mode: 'development', devtool: false, experiments: { outputModule: true }, output: { path: path.resolve(import.meta.dirname, 'dist2'), filename: 'bundle.js', libraryTarget: 'module' }};

打包下:


产物也同样是 100 多行。

相比之下,rollup 的产物就非常干净,没任何 runtime 代码:


更重要的是 webpack 目前打包出 es module 产物还是实验性的,并不稳定


webpack 打 cjs 和 umd 的 library 还行。

但 js 库一般不都要提供 es module 版本么,支持的不好怎么行?

所以我们一般用 rollup 来做 js 库的打包,用 webpack 做浏览器环境的打包。

前面说组件库打包一般都用 rollup,我们来看下各大组件库的打包需求。

安装 antd:

npm install --no-save antd

在 node_modules 下可以看到它分了 dist、es、lib 三个目录:

分别看下这三个目录的组件代码:

lib 下的组件是 commonjs 的:

es 下的组件是 es module 的:

dist 下的组件是 umd 的:

然后在 package.json 里分别声明了 commonjs、esm、umd 还有类型的入口:

这样,当你用 require 引入的就是 lib 下的组件,用 import 引入的就是 es 下的组件。

而直接 script 标签引入的就是 unpkg 下的组件。

再来看一下 semi design 的:

npm install --no-save @douyinfe/semi-ui

也是一样:

只不过多了个 css 目录。

所以说,组件库的打包需求就是组件分别提供 esm、commonjs、umd 三种模块规范的代码,并且还有单独打包出的 css。

那 rollup 如何打包 css 呢?

我们试一下:

创建 src/index.css

.aaa {    background: blue;}

创建 src/utils.css

.bbb {    background: red;}

然后分别在 index.js 和 utils.js 里引入下:



安装 rollup 处理 css 的插件:

npm install --save-dev rollup-plugin-postcss

引入下:


import postcss from 'rollup-plugin-postcss';
/** @type {import("rollup").RollupOptions} */export default { input: 'src/index.js', output: [ { file: 'dist/esm.js', format: 'esm' }, { file: 'dist/cjs.js', format: "cjs" }, { file: 'dist/umd.js', name: 'Guang', format: "umd" } ], plugins: [ postcss({ extract: true, extract: 'index.css' }), ]};

然后跑一下:

npx rollup -c rollup.config.mjs

可以看到,产物多了 index.css


而 js 中没有引入 css 了:


被 tree shaking 掉了,rollup 默认开启 tree shaking。

这样我们就可以单独打包组件库的 js 和 css。

删掉 dist,我们试下不抽离是什么样的:


npx rollup -c rollup.config.mjs

可以看到,代码里多了 styleInject 的方法:


用于往 head 里注入 style


一般打包组件库产物,我们都会分离出来。

然后我们再用 webpack 打包试试:

安装用到的 loader:

npm install --save-dev css-loader style-loader

css-loader 是读取 css 内容为 js

style-loader 是往页面 head 下添加 style 标签,填入 css

这俩结合起来和 rollup 那个插件功能一样。

配置 loader:


module: {    rules: [{        test: /\.css$/i,        use: ["style-loader", "css-loader"],    }],}

用 webpack 打包下:

npx webpack-cli -c webpack.config.mjs

可以看到 css 变成 js 模块引入了:


这是 css-loader 做的。

而插入到 style 标签的 injectStylesIntoStyleTag 方法则是 style-loader 做的:


然后再试下分离 css,这用到一个单独的插件:

npm install --save-dev mini-css-extract-plugin

配一下:


import path from 'node:path';import MiniCssExtractPlugin from "mini-css-extract-plugin";
/** @type {import("webpack").Configuration} */export default { entry: './src/index.js', mode: 'development', devtool: false, output: { path: path.resolve(import.meta.dirname, 'dist2'), filename: 'bundle.js', }, module: { rules: [{ test: /\.css$/i, use: [MiniCssExtractPlugin.loader, "css-loader"], }], }, plugins: [ new MiniCssExtractPlugin({ filename: 'index.css' }) ]};

指定抽离的 filename 为 index.css

抽离用的 loader 要紧放在 css-loader 之前。

样式抽离到了 css 中,这时候 style-loader 也就不需要了。

打包下:

npx webpack-cli -c webpack.config.mjs

样式抽离到了 css 中:


而 js 里的这个模块变为了空实现:


所以 webpack 的 style-loader + css-loader + mini-css-extract-plugin 就相当于 rollup 的 rollup-plugin-postcss 插件。

为什么 rollup 没有 loader 呢?

因为 rollup 的 plugin 有 transform 方法,也就相当于 loader 的功能了。

我们自己写一下抽离 css 的 rollup 插件:

创建 my-extract-css-rollup-plugin.mjs(注意这里用 es module 需要指定后缀为 .mjs):

const extractArr = [];
export default function myExtractCssRollupPlugin (opts) { return { name: 'my-extract-css-rollup-plugin', transform(code, id) { if(!id.endsWith('.css')) { return null; }
extractArr.push(code);
return { code: 'export default undefined', map: { mappings: '' } } }, generateBundle(options, bundle) {
this.emitFile({ fileName: opts.filename || 'guang.css', type: 'asset', source: extractArr.join('\n/*光光666*/\n') }) } }; }

在 transform 里对代码做转换,这就相当于 webpack 的 loader 了。

我们在 transform 里只处理 css 文件,保存 css 代码,返回一个空的 js 文件。

然后 generateBundle 里调用 emitFile 生成一个合并后的 css 文件。

用一下:


import myExtractCssRollupPlugin from './my-extract-css-rollup-plugin.mjs';
myExtractCssRollupPlugin({    filename: '666.css'})

删掉之前的 dist 目录,重新打包:

npx rollup -c rollup.config.mjs

看下产物:

可以看到,抽离出了 css,内容是合并后的所有 css。

而 cjs 也没有 css 的引入:


也是被 tree shaking 掉了。

我们把 tree shaking 关掉试试:


再次打包:


可以看到,两个 css 模块转换后的 js 模块依然被引入了:


我们改下插件 transform 的内容:


再次打包:


可以看到引入的也是我们转后后的 css 模块的内容:


因为没用到,同样会被 tree shaking 掉。

所以说 rollup 的插件的 transform 就相当于 webpack loader 的功能。

前面说 webpack 用来做浏览器的打包,而 rollup 一般做 js 库的打包。

这也不全对,vite 就是用 rollup 来做的生产环境的打包。

因为它开发环境下不打包,而是跑了一个开发服务器,对代码做了下转换,不需要 webpack 那些 dev server 的功能。

而生产环境又需要打包,所以 rollup 就很合适。


开发环境下,浏览器里用 type 为 module 的 script 引入,会请求 vite 的开发服务器。

vite 开发服务器会调用 rollup 插件的 transform 方法来做转换。

而生产环境下,用 rollup 打包,也是用同样的 rollup 插件。

当然,vite 还会用 esbuild 来做下依赖的与构建,比如把 cjs 转换成 esm、把小模块打包成一个大的模块。

用 esbuild 是因为它更快。

所以说,vite 是基于 rollup 来实现的,包括开发服务器的 transform,以及生产环境的打包。

但是为了性能考虑,又用了 esbuild 做依赖预构建。

现在 vite 团队在开发 rust 版 rollup 也就是 rolldown 了,有了它之后,就可以完全替代掉 rollup + esbuild 了。

综上,除了 webpack、vite 外,rollup 也是非常常用的一个打包工具。

案例代码上传了github

总结

这节我们学习了 rollup,虽然它不如 webpack、vite 提到的多,但也是一个常用的打包工具。

它打包产物没有 runtime 代码,更简洁纯粹,能打包出 esm、cjs、umd 的产物,常用来做 js 库、组件库的打包。相比之下,webpack 目前对 esm 产物的支持还是实验性的,不稳定。

rollup 只有 plugin,没有 loader,因为它的 transform 方法就相当于 webpack 插件的 loader。

vite 就是基于 rollup 来实现的,开发环境用 rollup 插件的 transform 来做代码转换,生产环境用 rollup 打包。

不管你是想做组件库、js 库的打包,还是想深入学习 vite,都离不开 rollup。

更多内容可以看我的小册《Node.js CLI 通关秘籍》


点击关注公众号,“技术干货” 及时达!



稀土掘金技术社区
掘金,一个帮助开发者成长的技术社区
 最新文章