前言
Rollup 作为一款高效的 JavaScript 打包工具,凭借其出色的 Tree Shaking 和对 ES Module 的支持,成为许多开发者的首选。然而,在实际使用中,Rollup 的配置和插件生态也带来了不少挑战。
本文将分享我在使用 Rollup 过程中的踩坑经历,总结关键问题和解决方案,帮助你更高效地完成项目构建,避开常见的“坑”。
Rollup 基础配置
-
安装 Rollup:
npm install rollup --save-dev -
创建配置文件
rollup.config.js:export default { input: 'src/main.js', // 入口文件 output: { file: 'dist/bundle.js', // 输出文件 format: 'esm', // 输出格式(esm、cjs、iife 等) }, }; -
运行打包命令:
npx rollup -c
rollup.config.js 配置代码分享
以下是一个基础的 rollup.config.js 配置示例,涵盖了入口文件、输出格式以及常用插件的配置:
import typescript from "rollup-plugin-typescript2"; // 打包 TS 文件,可生成 *.d.ts 文件
import { nodeResolve } from "@rollup/plugin-node-resolve"; // 打包模块化
import commonjs from "@rollup/plugin-commonjs"; // 用于打包 comjs
import babel from "@rollup/plugin-babel"; // babel 打包工具
import replace from "@rollup/plugin-replace"; // 代码替换的工具
import progress from "rollup-plugin-progress"; // 打包进度条
import postcss from "rollup-plugin-postcss"; // 打包 scss
import { terser } from "rollup-plugin-terser"; // 压缩工具
import url from "rollup-plugin-url";
import polyfillNode from 'rollup-plugin-node-polyfills';
const extensions = [".js", ".ts", ".tsx", ".less"];
const isProduction = process.env.NODE_ENV === "production";
export default {
treeshake: {
// 打包时将没有用到的代码移除
moduleSideEffects: false,
},
input: ["src/components/getPrintFormat/index.ts"],
output: [
{
dir: "dist",
format: "cjs",
preserveModules: true,
preserveModulesRoot: 'src/components',
entryFileNames: '[name]/index.js',
exports: 'named'
},
],
plugins: [
typescript(),
polyfillNode(), // 添加这个插件以模拟 Node.js 模块
replace({
values: {
"process.env.NODE_ENV": JSON.stringify(
process.env.NODE_ENV || "development"
),
},
preventAssignment: true,
}),
nodeResolve({
extensions,
}),
commonjs(),
babel({
extensions,
babelHelpers: "bundled",
include: "src/components/**",
exclude: "src/**",
}),
postcss({
// 把 css 输出为单独的文件
extract: true,
}),
isProduction && terser({ format: { comments: false } }),
progress(),
url({
limit: 10 * 1024, // inline files < 10k, copy files > 10k
include: ["**/*.png"], // defaults to .svg, .png, .jpg and .gif files
emitFiles: true, // defaults to true
}),
],
external: ["react", "dayjs", "moment", "qrcode", "bignumber.js", "lodash-es", "lodash", "lodash/fp"],
};
该配置文件已在多个项目中稳定运行,能够有效处理模块化开发中的常见需求。接下来,我将详细解析部分配置项的作用,并分享在配置中遇到的坑,帮助大家更好地掌握 Rollup 的配置方法。
配置文件的核心选项解析
-
input:指定入口文件。支持string、对象形式、数组形式等。
-
output:配置输出文件的路径、格式和名称。
format:支持esm(ES Module)、cjs(CommonJS)、iife(立即执行函数)等格式。file:输出文件的路径。
-
plugins:用于配置插件,处理非 JavaScript 文件或优化打包结果。
常用插件推荐
@rollup/plugin-node-resolve:解析node_modules中的模块。@rollup/plugin-commonjs:将 CommonJS 模块转换为 ES Module。@rollup/plugin-babel:使用 Babel 转换 JavaScript 代码。@rollup/plugin-terser:压缩打包后的代码。rollup-plugin-node-polyfills: 是一个用于在浏览器环境中模拟 Node.js 核心模块(如fs、path、crypto等)的 Rollup 插件。它通过提供浏览器兼容的 polyfill,使得原本依赖 Node.js 环境的代码可以在浏览器中运行。
Rollup 参数与插件的功能与使用场景
包代码压缩 -- @rollup/plugin-terser
import { terser } from "rollup-plugin-terser"; // 压缩工具
plugins: [
terser(),
]
保留模块结构 -- preserveModules & preserveModulesRoot
preserveModules是 Rollup 的一个输出选项(output.preserveModules),用于控制打包时是否保留模块的原始文件结构。默认情况下,Rollup 会将所有模块打包成一个或多个文件,而启用preserveModules后,Rollup 会尽可能保留每个模块的独立性,生成与源码结构相似的输出文件。- 作用: 1、保留模块结构:将每个模块单独输出,而不是合并成一个文件。 2、支持模块化开发:适用于需要保持模块独立性的场景,如库开发。 3、 便于调试:生成的输出文件与源码结构一致,便于定位问题。
output: [
{
dir: "dist",
format: "cjs",
preserveModules: true, // 保留模块结构
preserveModulesRoot: 'src/components', // **指定保留模块结构的根目录**
entryFileNames: '[name]/index.js',
exports: 'named'
},
],
假设源码结构如下:
src/
├── index.js
├── utils/
│ ├── math.js
│ └── string.js
└── components/
└── button.js
启用 preserveModules 后,输出结构如下:
dist/
├── index.js
├── utils/
│ ├── math.js
│ └── string.js
└── components/
└── button.js
未启用 preserveModules 时,输出可能是一个单独的文件:
dist/
└── bundle.js
preserveModulesRoot是 Rollup 的一个输出选项(output.preserveModulesRoot),通常与preserveModules: true一起使用。它的作用是指定保留模块结构的根目录,从而在输出文件中保留源码的相对路径结构
启用 preserveModulesRoot: 'src' 后,输出结构如下:
dist/
├── index.js
├── utils/
│ ├── math.js
│ └── string.js
└── components/
└── button.js
未启用 preserveModulesRoot 时,输出结构可能会扁平化:
dist/
├── index.js
├── math.js
├── string.js
└── button.js
实战案例:踩坑与解决
案例 1:Module not found: Error: Cannot resolve module 'fs'报错:Webpack 4 与 Webpack 5 的差异及 Rollup 的修复方法
在使用 Webpack 5 构建的项目中,安装并运行相同的 npm 包时,出现以下错误:
Module not found: Error: Cannot resolve module 'fs'
然而,该包在 Webpack 4 的项目中可以正常安装和使用,未出现任何问题。
打包目录以及内容
解决
- 安装
rollup-plugin-node-polyfills
import polyfillNode from 'rollup-plugin-node-polyfills';
plugins: [
polyfillNode(), // 添加这个插件以模拟 Node.js 模块,
]
打包出来已没有'fs'等相关模块
案例 2:引入的外部依赖报错
解决
配置external,用于指定外部依赖,告诉 Rollup 哪些模块不应该被打包到输出文件中,而是在运行时从外部获取。这些模块不会被打包到输出文件中,而是在运行时从外部环境(如全局变量、CDN 或其他脚本)中获取。
{
external: ["react", "lodash", "lodash/fp"],
}
打包命令文件配置
在创建依赖包时,package.json 和 README.md 是两个非常重要的文件。package.json 用于定义包的元数据和依赖关系,而 README.md 则是包的文档,向用户介绍包的功能、使用方法和其他相关信息。以下是关于如何编写这两个文件的示例、以及构建命令。
"scripts": {
"build": "node scripts/copyPackageJSON.js && cross-env NODE_ENV=production rollup -c",
}
scripts/copyPackageJSON.js
创建包的package.js文件
const fs = require('fs-extra');
const resolve = require('path').resolve;
const distPath = resolve(__dirname, '../dist');
const path = require('path');
if (fs.existsSync(distPath)) {
fs.removeSync(distPath);
}
fs.mkdirSync(distPath);
// 复制README.md文件
fs.copyFileSync(resolve(__dirname, '../README.md'), resolve(distPath, 'README.md'));
function copyFolderSync(src, dest) {
// 创建目标文件夹
if (!fs.existsSync(dest)) {
fs.mkdirSync(dest);
}
// 读取源文件夹中的文件和子文件夹
const files = fs.readdirSync(src);
files.forEach(file => {
const srcFile = path.join(src, file);
const destFile = path.join(dest, file);
if (fs.statSync(srcFile).isDirectory()) {
// 递归复制子文件夹
copyFolderSync(srcFile, destFile);
} else {
// 复制文件
fs.copyFileSync(srcFile, destFile);
}
});
}
// make package.json
// 根据DEP_LIB指定版本号的递增规则
const depComponentLibName = process.env.DEP_LIB || 'xxx';
function updateVersion(version, depComponentLibName) {
const versionList = version.split('.');
versionList[2] = Number(versionList[2]) + 1;
if (depComponentLibName === 'xxx') {
versionList[0] = 0;
} else if (depComponentLibName === 'yyy') {
versionList[0] = 1;
}
return versionList[0] + '.' + versionList[1] + '.' + versionList[2];
}
const packageJSON = require('../package.json');
// 创建版本号
packageJSON.version = updateVersion(packageJSON.version, depComponentLibName);
fs.writeJSONSync(resolve(__dirname, '../package.json'), packageJSON, { spaces: 2 });
// 规范的,定义程序入口文件
packageJSON.main = 'dist/index.js';
// npm publish 中约定可上传的文件夹
// packageJSON.files = ['src/components'];
// 删除部分无用配置
delete packageJSON.files;
delete packageJSON.scripts;
delete packageJSON.jest;
delete packageJSON.devDependencies;
delete packageJSON.private;
delete packageJSON.browserslist;
delete packageJSON.babel;
delete packageJSON['pre-commit'];
delete packageJSON['name'];
// 写入相关配置
packageJSON['name'] = "@ircloud/printFormat"
packageJSON['pre-commit'] = {};
const dependencies = {...packageJSON['dependencies']};
delete packageJSON['dependencies'];
packageJSON['dependencies'] = {
dayjs: dependencies.dayjs,
moment: dependencies.moment,
qrcode: dependencies.qrcode,
lodash: dependencies.lodash,
['bignumber.js']: dependencies['bignumber.js'],
['lodash-es']: dependencies['lodash-es'],
}
fs.writeJSONSync(resolve(distPath, 'package.json'), packageJSON, { spaces: 2 });
console.log('done!');