Rollup 打包实战:踩坑记录与关键点总结,帮你少走弯路

1,670 阅读6分钟

前言

Rollup 作为一款高效的 JavaScript 打包工具,凭借其出色的 Tree Shaking 和对 ES Module 的支持,成为许多开发者的首选。然而,在实际使用中,Rollup 的配置和插件生态也带来了不少挑战。

本文将分享我在使用 Rollup 过程中的踩坑经历,总结关键问题和解决方案,帮助你更高效地完成项目构建,避开常见的“坑”。

Rollup 基础配置

  1. 安装 Rollup:

    npm install rollup --save-dev
    
  2. 创建配置文件 rollup.config.js

    export default {
      input: 'src/main.js', // 入口文件
      output: {
        file: 'dist/bundle.js', // 输出文件
        format: 'esm', // 输出格式(esm、cjs、iife 等)
      },
    };
    
  3. 运行打包命令:

    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 核心模块(如 fspathcrypto 等)的 Rollup 插件。它通过提供浏览器兼容的 polyfill,使得原本依赖 Node.js 环境的代码可以在浏览器中运行。

Rollup 参数与插件的功能与使用场景

包代码压缩 -- @rollup/plugin-terser

import { terser } from "rollup-plugin-terser"; // 压缩工具
  plugins: [
    terser(),
  ]

image.png

保留模块结构 -- 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 的项目中可以正常安装和使用,未出现任何问题。

打包目录以及内容

image.png

解决

  • 安装rollup-plugin-node-polyfills
import polyfillNode from 'rollup-plugin-node-polyfills';
  plugins: [
    polyfillNode(),  // 添加这个插件以模拟 Node.js 模块,
  ]

image.png 打包出来已没有'fs'等相关模块

案例 2:引入的外部依赖报错

image.png

image.png

解决

配置external,用于指定外部依赖,告诉 Rollup 哪些模块不应该被打包到输出文件中,而是在运行时从外部获取。这些模块不会被打包到输出文件中,而是在运行时从外部环境(如全局变量、CDN 或其他脚本)中获取。

{
    external: ["react",  "lodash", "lodash/fp"],
}

image.png

打包命令文件配置

在创建依赖包时,package.json 和 README.md 是两个非常重要的文件。package.json 用于定义包的元数据和依赖关系,而 README.md 则是包的文档,向用户介绍包的功能、使用方法和其他相关信息。以下是关于如何编写这两个文件的示例、以及构建命令。

image.png

"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!');