Skip to content

Rollup

第一章:概述

一、简介

1. 是什么

Rollup 是一个用于 JavaScript 的模块打包工具,它将小的代码片段编译成更大、更复杂的代码,例如库或应用程序。它使用 JavaScript 的 ES6 版本中包含的新标准化代码模块格式,而不是以前的 CommonJS 和 AMD 等特殊解决方案。ES 模块允许你自由无缝地组合你最喜欢的库中最有用的个别函数。这在未来将在所有场景原生支持,但 Rollup 让你今天就可以开始这样做。

参考文档

2. 快速入门

1)安装 rollup

bash
npm install --global rollup

2)编写业务代码

javascript
console.log("hello world")

3)使用 rollup 打包

bash
# 编译为一个 CommonJS 模块
rollup main.js --file bundle.js --format cjs

# 编译为多个 CommonJS 模块
rollup main.js back.js --dir dist --format cjs
rollup -i main.js -i back.js --dir dist  --format cjs

第二章:命令行使用

  • --input:入口文件(可以指定多个 -i 选项)。
  • --file:单个输出文件(如果不存在,则打印到 stdout)。
  • --dir:多个文件输出(如果不存在,则打印到 stdout)。
  • --format: 输出类型(amd、cjs、es、iife、umd)。
  • --name:UMD 导出的名称。
  • --config:传递自定义配置文件位置。
  • --configPlugin:指定要使用的 Rollup 插件。
  • --watch:监视产物文件并在更改时重新构建。

更多选项,可以查看命令行接口

第三章:配置文件

一、快速入门

1)rollup.config.jsrollup.config.mjs,并位于项目的根目录中。

注意:nodejs 环境下要运行 esm 模块化的内容,要么文件名后缀处理为 .mjs,要么 package.json 文件中配置 "type":"module",因为 Rollup 将遵循 Node ESM 语义

2) 配置的智能提示

由于 Rollup 随附了 TypeScript 类型定义,因此可以使用 JSDoc 类型提示来利用你的 IDE 的智能感知功能:

javascript
/**
 * @type {import('rollup').RollupOptions}
 * @description: rollup配置文件
 */
export default {
  input: 'src/main.js',
  output: {
    file: 'dist/bundle.cjs.js', // 输出的文件路径和文件名
    format: 'cjs', // 五种输出的格式 amd/es/iife/umd/cjs
    name: 'bundleName' // 当format格式为iife和umd的时候必须提供变量名
  }
}

或者使用 defineConfig 辅助函数:

javascript
import { defineConfig } from 'rollup'

export default defineConfig({
  input: 'src/index.js',
  output: {
    file: 'dist/bundle.js',
    format: 'umd',
    name: 'bundle'
  },
})

3)执行 rollup -c 命令。其中 -c 选项用于启用配置文件。

二、常用配置

1. 多产物配置

在打包 JavaScript 类库的场景中,我们通常需要对外暴露出不同格式的产物供他人使用,不仅包括 ESM,也需要包括诸如 CommonJS、UMD 等格式,保证良好的兼容性。那么,同一份入口文件,如何让 Rollup 给我们打包出不一样格式的产物呢?

javascript
const buildOptions = {
  input: ["src/index.js"],
  // 将 output 改造成一个数组
  output: [
    {
      dir: "dist/es",
      format: "esm",
    },
    {
      dir: "dist/cjs",
      format: "cjs",
    },
  ],
};

export default buildOptions;

2. 多入口配置

除了多产物配置,Rollup 中也支持多入口配置,而且通常情况下两者会被结合起来使用。

javascript
{
  input: ["src/index.js", "src/util.js"]
}
// 或者
{
  input: {
    index: "src/index.js",
    util: "src/util.js",
  },
}

如果不同入口对应的打包配置不一样,我们也可以默认导出一个配置数组,如下所示:

javascript
// rollup.config.js
const buildIndexOptions = {
  input: ["src/index.js"],
  output: [
    // output 配置
  ],
};


const buildUtilOptions = {
  input: ["src/util.js"],
  output: [
    // output 配置
  ],
};

export default [buildIndexOptions, buildUtilOptions];

3. 自定义 output 配置

刚才提到了 input 的使用,主要用来声明入口,可以配置成字符串、数组或者对象,使用比较简单。而 output 与之相对,用来配置输出的相关信息,常用的配置项如下:

javascript
output: {
  // 产物输出目录
  dir: path.resolve(__dirname, 'dist'),
  // 以下三个配置项都可以使用这些占位符:
  // 1. [name]: 去除文件后缀后的文件名
  // 2. [hash]: 根据文件名和文件内容生成的 hash 值
  // 3. [format]: 产物模块格式,如 es、cjs
  // 4. [extname]: 产物后缀名(带`.`)
  // 入口模块的输出文件名
  entryFileNames: `[name].js`,
  // 非入口模块 (如动态 import) 的输出文件名
  chunkFileNames: 'chunk-[hash].js',
  // 静态资源文件输出文件名
  assetFileNames: 'assets/[name]-[hash][extname]',
  // 产物输出格式,包括`amd`、`cjs`、`es`、`iife`、`umd`、`system`
  format: 'cjs',
  // 是否生成 sourcemap 文件
  sourcemap: true,
  // 如果是打包出 iife/umd 格式,需要对外暴露出一个全局变量,通过 name 配置变量名
  name: 'MyBundle',
  // 全局变量声明
  globals: {
    // 项目中可以直接用`$`代替`jquery`
    jquery: '$'
  }
}

4. 依赖 external

对于某些第三方包,有时候我们不想让 Rollup 进行打包,也可以通过 external 进行外部化:

javascript
{
  external: ['react', 'react-dom']
}

三、案例

1. 构建 react 应用

1)node_modules
shell
# react
pnpm add react react-dom

# @types/react
pnpm add @types/react @types/react-dom -D

# react预设
pnpm add @babel/preset-react -D

# rollup
pnpm add rollup -D 

# rollup常规插件
pnpm add @rollup/plugin-node-resolve @rollup/plugin-commonjs -D

# typescript相关
pnpm add typescript tslib @rollup/plugin-typescript -D

# @rollup/plugin-babel相关
pnpm add @rollup/plugin-babel @babel/core @babel/preset-env -D

# @babel/runtime相关
pnpm add @babel/plugin-transform-runtime @babel/runtime @babel/runtime-corejs3 -D

# html文件模板
pnpm add rollup-plugin-generate-html-template -D

# 替换字符串
pnpm add @rollup/plugin-replace -D 

# 开发服务器与live server
pnpm add rollup-plugin-serve rollup-plugin-livereload -D

# clear插件
pnpm add rollup-plugin-clear -D

# scss
pnpm add rollup-plugin-scss sass -D 

# postcss
pnpm add postcss rollup-plugin-postcss -D

# 图片处理
pnpm add @rollup/plugin-image -D

# nodejs typescript类型
pnpm add @types/node -D

# 别名插件
pnpm add @rollup/plugin-alias -D 

# terser
pnpm add @rollup/plugin-terser -D

# visualizer
pnpm add rollup-plugin-visualizer -D
2)tsconfig.json
javascript
{
  "compilerOptions": {
    "module": "esnext",
    "target": "es5",
    "lib": ["esnext", "dom", "dom.iterable"],
    "skipLibCheck": true,

    "moduleResolution": "bundler",
    "noEmit": true,
    "allowImportingTsExtensions":true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",

    "baseUrl": "./",
    "paths": {
      "@/*": ["src/*"]
    },
  },
  "include": ["src/**/*","rollup.config.ts", "global.d.ts"],
}
.babelrc.json
javascript
{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": "> 0.25%, not dead",
        "useBuiltIns": "usage",
        "corejs": 3
      }
    ],
    ["@babel/preset-react"]
  ],
  "plugins": [
    [
      "@babel/plugin-transform-runtime",
      {
        "corejs": 3
      }
    ]
  ]
}
rollup.config.ts
javascript
import { RollupOptions } from "rollup";
import { nodeResolve } from "@rollup/plugin-node-resolve";
import commonjs from "@rollup/plugin-commonjs";
import babel from "@rollup/plugin-babel";
import typescript from "@rollup/plugin-typescript";
import htmlTemplate from "rollup-plugin-generate-html-template";
import serve from "rollup-plugin-serve";
import livereload from "rollup-plugin-livereload";
import replace from "@rollup/plugin-replace";
import postcss from "rollup-plugin-postcss";
import alias from "@rollup/plugin-alias";
import clear from "rollup-plugin-clear";
import image from "@rollup/plugin-image"
import terser from '@rollup/plugin-terser';
import { fileURLToPath } from "node:url";
import { visualizer } from "rollup-plugin-visualizer";

const config: RollupOptions = {
  input: "src/main.tsx",
  output: {
    dir: "dist/",
    format: "esm",
    name: "rollupDemo",
    sourcemap: true,
    plugins: [terser()],
    entryFileNames: "[name].[hash:6].js",
    chunkFileNames: "chunks/chunk-[name]-[hash].js",
    // 代码分割
    // manualChunks: { 
    //   react: ["react", "react-dom"]
    // },
    globals: {
      react: "React",
      "react-dom": "ReactDOM",
    },
    paths: {
      react: "https://cdn.jsdelivr.net/npm/react@18.2.0/+esm",
      "react-dom": "https://cdn.jsdelivr.net/npm/react-dom@18.2.0/+esm",
    }
  },
  external: ["react", "react-dom"],
  plugins: [
    visualizer(),
    nodeResolve({
      extensions: [".js", "jsx", "ts", "tsx"],
    }),
    commonjs(),
    typescript(),
    babel({
      babelHelpers: "runtime",
      include: "src/**",
      exclude: "node_modules/**",
      extensions: [".js", ".ts", "jsx", "tsx"],
    }),
    alias({
      entries: [
        {
          find: "@",
          replacement: fileURLToPath(new URL("src", import.meta.url)),
        },
      ],
    }),
    postcss({
      extensions: [".css"], // 将scss解析成css
      extract: true,
      modules: true,
    }),
    replace({
      preventAssignment: true,
      "process.env.NODE_ENV": JSON.stringify("production"), // 否则会报:process is not defined的错
    }),
    clear({
      targets: ["dist"],
    }),
    htmlTemplate({
      template: "public/index.html",
      target: "dist/index.html",
      attrs: ['type="module"'],
    }),
    image(),
    serve("dist"),
    livereload("src"),
  ],
};
export default config;

第四章:JavaScript API

一、rollup.rollup

对于一次完整的构建过程而言,Rollup 会先进入到 Build 阶段,解析各模块的内容及依赖关系,然后进入 Output 阶段,完成打包及输出的过程。

javascript
import { rollup } from 'rollup';
import rollupOptions from './rollup.config.js';

/**
 * rollup的执行分成三个阶段
 */
(async function () {
  // 1.打包阶段: build hooks是在构建阶段,也就是打包阶段触发
  const bundle = await rollup(rollupOptions);
  // 在generate之前调用buildend钩子
  // 2.生成阶段
  await bundle.generate(rollupOptions.output);
  // 3.写入阶段
  await bundle.write(rollupOptions.output);
  // 4.关闭阶段
  await bundle.close();
})();

Build 阶段

rollup.rollup 函数接收一个输入选项对象作为参数,并返回一个 Promise,该 Promise 解析为一个 bundle 对象。

主要负责创建模块依赖图,初始化各个模块的 AST 以及模块之间的依赖关系。以及除屑优化,不会生成任何输出。

Output 阶段

真正进行打包的过程会在 Output 阶段进行,即在 bundle 对象的 generate 或者 write 方法中进行(writegenerate 方法唯一的区别在于前者打包完产物会写入磁盘,而后者不会)。

bundle 对象上,可以多次调用 bundle.generate 并使用不同的输出选项对象来生成不同的产物到内存中。如果想直接将它们写入磁盘,使用 bundle.write

关闭阶段

完成 bundle 对象后,应调用 bundle.close(),这将通过 closeBundle 钩子让插件清理它们的外部进程或服务。

二、rollup.watch

Rollup 还提供了一个 rollup.watch 函数,当检测到磁盘上的某个模块已更改时,它将重新打包。

使用 js api 时,需要在响应 BUNDLE_END 事件时调用 event.result.close(),以允许插件在 closeBundle 钩子中清理资源。

第五章:插件的使用

Rollup 插件市场:rollup/awesome

注意:在 Rollup 配置文件中,plugins 除了可以与 output 配置在同一级,也可以配置在 output 参数里面。output.plugins 中配置的插件只有使用 Output 阶段相关钩子的插件才能够放到这个配置中。

一、打包模块

1. 引入第三方模块

Rollup 打包的 js 文件中,若有导入第三方模块,会原样输出。因为 Rollup 不会去 node_modules 文件夹里找你通过 npm install 下载的第三方库。怎么办?

使用 @rollup/plugin-node-resolve 插件。

1)安装

bash
npm install --save-dev @rollup/plugin-node-resolve

2)配置

javascript
import { nodeResolve } from '@rollup/plugin-node-resolve';

export default {
  input: 'src/index.js',
  output: {
    dir: 'output',
    format: 'cjs'
  },
  plugins: [
    nodeResolve()
  ]
};

2. 打包 cjs 文件

Rollup 本身只支持打包 ESM 文件,不支持打包 cjs 模块。怎么打包 cjs 文件?

@rollup/plugin-commonjs 插件将 CommonJS 模块转换为 ES6,以便它们可以包含在 bundle 中。

1)安装

bash
npm install --save-dev @rollup/plugin-commonjs

2)配置

javascript
import commonjs from '@rollup/plugin-commonjs';

export default {
  input: 'src/index.js',
  output: {
    dir: 'output',
    format: 'cjs'
  },
  plugins: [
    commonjs()
  ]
};

注意:

  • 此插件必须要跟 @rollup/plugin-node-resolve 插件结合使用。

  • plugins 中,要把 node-resolve 放在 commonjs 的前面。因为必须先找到文件,才能对其进行代码格式转换。

3. 打包 TS 文件

@rollup/plugin-typescript 是集成 Rollup 和 Typescript 的 Rollup 插件。

1)安装

bash
npm install --save-dev @rollup/plugin-commonjs

2)配置

javascript
// rollup.config.js
import typescript from '@rollup/plugin-typescript';

export default {
  input: 'src/index.ts',
  output: {
    dir: 'output',
    format: 'cjs'
  },
  plugins: [
    typescript()
  ]
};

二、常见工具集成

1. babel

参考文档:Babel 中文文档

1)安装依赖

bash
npm install --save-dev @rollup/plugin-babel @babel/core

2)与 rollup 集成

javascript
import babel from '@rollup/plugin-babel';

const config = {
  input: 'src/index.js',
  output: {
    dir: 'output',
    format: 'esm'
  },
  plugins: [
    babel({
      babelHelpers: 'bundled',
      // include:
      exclude: /node_modules/
    })
  ]
};

export default config;

注意:在使用 Rollup 打包时,如果同时使用了 @rollup/plugin-babel@rollup/plugin-commonjs 这两个插件,必须注意它们在 plugins 数组中的顺序:@rollup/plugin-commonjs 必须放在 @rollup/plugin-babel 的前面。

3)babel.config.json

json
{
  "presets": ["@babel/preset-env"]
}

4)安装 babel 预设

bash
npm install @babel/preset-env --save-dev

2. postcss

rollup-plugin-postcss 是 Rollup 和 PostCSS 的无缝集成。

1)安装

bash
yarn add --dev postcss rollup-plugin-postcss

2)配置

javascript
// rollup.config.js
import postcss from 'rollup-plugin-postcss'

export default {
  plugins: [
    postcss({
      plugins: [] // 配置 postcss 的插件, 推荐创建一个独立的 postcss 配置文件
    })
  ]
}

三、CDN

1)在 html 文件中引入 CDN 链接。

2)配置

javascript
export default {
  input: 'src/index.js',
  output: {
    dir: 'output',
    format: 'cjs',
    globals: {
      lodash: '_', // 告诉 Rollup, lodash 是外部依赖, 全局变量 _
      jquery: '$' // 告诉 Rollup, jquery 是外部依赖, 全局变量 $
    }
  },
  external: ['lodash', 'jquery'], // 不把模块内容打包到产物中
};

四、压缩

1. 压缩 JS

@rollup/plugin-terser 是使用 terser 生成最小化 bundle 的插件。

1)安装

bash
npm install --save-dev @rollup/plugin-terser

2)配置

javascript
import terser from '@rollup/plugin-terser';

export default {
  input: 'src/index.js',
  output: {
    dir: 'output',
    format: 'cjs'
  },
  plugins: [
    terser()
  ]
};

五、开发相关

1. 服务器

rollup-plugin-serve 是运行一个服务器。

1)安装

bash
npm install --save-dev rollup-plugin-serve

2)配置

javascript
import serve from 'rollup-plugin-serve'

export default {
  input: 'src/main.js',
  output: {
    file: 'dist/bundle.js',
    format: 'cjs'
  },
  plugins: [
    // serve('dist') // will be used as contentBase
    serve({
      open: true,
      port: 8080,
      contentBase: './dist'
    })
  ]
}

3)执行 rollup -c -w 就会在 dist 目录下找到 index.html 后以服务器运行它。

第六章:开发插件

官方文档:插件开发

一、概述

Rollup 插件是一个对象,具有属性、构建钩子和输出生成钩子中的一个或多个。插件文件应导出一个函数,该函数可以使用插件特定的选项进行调用并返回此类对象。

约定

  • 插件应该有一个明确的名称,并以 rollup-plugin- 作为前缀。
  • package.json 中包含 rollup-plugin 关键字。
  • 如果插件使用“虚拟模块”,请使用 \0 前缀模块 ID。这可以防止其他插件尝试处理它。

二、插件内容

1. 属性

name:插件的名称。

version:插件的版本。

2. 构建钩子

构建钩子在构建阶段运行,该阶段由 rollup.rollup(inputOptions) 触发。主要发生在 Rollup 处理输入文件之前进行定位、加载和转换输入文件。

1)初始化阶段
  • options: 这是第一个触发的 Hook。它接收你在 rollup.config.js 中写的配置对象,可以在这里读取并修改/替换配置。
  • buildStart: 构建正式开始时触发。此时所有的配置都已经确定。插件通常在这里进行一些初始化操作。
    • 流程流向: buildStart 之后,Rollup 会找到你的入口文件(each entry),把它们送入真正的模块解析循环中。
2)核心的模块解析循环(图中间的递归圈圈)

这是 Rollup 最核心的部分。它会从入口文件开始,顺藤摸瓜找到所有依赖。

  • resolveId (寻找模块位置)

    当代码里出现 import foo from './foo.js' 时,Rollup 会调用这个 Hook,问插件:“这个 ./foo.js 到底在硬盘上的什么具体绝对路径?”

    • 分支判断: 如果插件判断这个模块是外部的第三方库(external),那么图中对应的虚线会直接跳走(不参与接下来的打包过程);如果是内部需要打包的(non-external),则顺着箭头进入 load
  • load (加载文件内容)

    拿到了具体的路径(Id)后,调用 load 把这个文件里的代码读取成字符串。如果你做的是虚拟模块(比如在内存里动态生成的代码),也会在这里直接返回内容。

  • shouldTransformCachedModule

    如果开启了缓存,Rollup 会在这里问一句:“这个使用了缓存的模块,还需要再经过 transform 转换一次吗?”

  • transform (转换代码)

    插件开发最常用的 Hook。上一步拿到了代码字符串,这一步可以对代码进行随意篡改/编译。比如:把 TypeScript 编译成 JS、把 ES6 转 ES5、注入 CSS 等等。

  • moduleParsed (模块解析完成)

    此时,Rollup 已经把转换好的代码解析成了 AST(抽象语法树),它清楚地知道了这个文件里又 import 了哪些别的新文件。

    • 关键回调: 你会发现图从这里引出了好几条线。如果解析后发现了新的依赖(each import),箭头又指回了上方的 resolveId 或者 load,如此反复循环,直到所有的依赖文件都被找出来。
3)处理动态导入
  • resolveDynamicImport

    如果在解析过程中发现了 异步的动态导入(比如 import('./some-module.js')),会走这个专门的 Hook 来寻找模块路径,后续它也会视情况进入 load 等流程或者被标记为 external

4)结束阶段
  • buildEnd

    当代码里再也没有任何新的 import(图左下角的虚线 no imports),所有的依赖关系网都摸查完毕时,或者构建过程中发生错误时,都会触发这个 Hook。插件通常在这里做一些清理工作、输出打包分析报告等。

5)监听模式专用(左上角独立区块)

如果你运行了 rollup --watch(监听模式):

  • watchChange: 当你在编辑器里修改了被监听的文件并保存时触发。
  • closeWatcher: 当你退出监听模式(比如按下 Ctrl+C 终止进程)时触发,用于关闭相关资源。

简单总结这个流水线

先看配置 (options) -> 宣布干活 (buildStart) -> [ 找文件位置 (resolveId) -> 把代码读出来 (load) -> 翻译转换代码 (transform) -> 分析看看有没有引入新文件 (moduleParsed),有的话回到第一步继续找 ] -> 活干完了/报错了 (buildEnd)。

3. 输出生成钩子

上图展示了 Rollup 插件生命周期的第二个核心大阶段:输出生成阶段(Output Generation Phase)的流程流向。将在内存中处理好的代码图谱,真正生成目标文件(Chunk)并写到硬盘上的流程。

1)渲染准备阶段
  • outputOptions:最先执行。它接收配置中关于输出的选项(即 rollup.config.js 里的 output 字段),插件可以在这里读取或者修改具体的输出配置。
  • renderStart:这是一个信号枪,标志着构建图谱全部完毕,正式开始渲染并生成最终代码。
2)Chunk 渲染核心循环(图中的主循环圈)

在 Rollup 中,你的代码根据依赖和配置,可能会被打包成一个或多个独立的文件代码块,统称为 Chunk。此时 Rollup 会对每一个 Chunk 进行遍历(each chunk)处理:

  • renderDynamicImport:如果你的代码里有动态导入(如 import('./foo.js')),这个钩子用来定制最终在代码里应该怎么去呈现它(比如改变转译后的加载代码)。

  • resolveFileUrl / resolveImportMeta:用来处理代码块里用到的 import.meta.url 或其他 import.meta.* 等元数据属性的替换规则。

  • 四周包边 (banner, intro, outro, footer)

    专门用来在代码的顶部或底部注入一些额外的字符串。

    • banner / footer:在文件最顶部 / 最底部插入(常用于插入版权声明注释)。
    • intro / outro:在核心代码的最外层包裹范围内插入。
  • renderChunk (渲染 Chunk 代码)

    这也是插件开发中最核心、最常用的钩子之一。到这里,整个 Chunk 文件的长相已经基本定了。插件拿到这整块代码字符串后,可以进行最终的手脚,比如代码压缩(Terser),或者替换一些最终的环境变量。

  • augmentChunkHash:给这个 Chunk 算哈希值(生成类似 main.a1b2c3.js 的后缀),如果你的插件改变了模块的运作导致需要一个不同的哈希,可以在这里追加哈希逻辑。

(以上走到这,一个 Chunk 就处理完了,如果还有其他的 Chunk,会随着右侧虚线 next chunk 返回到主循环重新继续跑。)

3)生成与收尾阶段(循环结束以后)

当所有的 Chunk 都被成功渲染完成后:

  • generateBundle (极其重要)

    这会儿,所有的文件数据都已经搞定了,但它们还全都在内存里,没有写入硬盘。这里会拿到一个包含本次即将输出的所有文件信息的对象(Bundle)。你可以赶在写入之前,在这里最后修改/添加非代码类的静态资产(如自动生成 HTML 文件、拷贝图片等),或者生成 sourcemap。

  • writeBundle

    走到这里说明前面在内存里的模块们已经被正式写入到本地硬盘上了。如果你的插件想要等打包全部落地后再执行一些动作(比如把打好的包上传到服务器)。

  • renderError

    打包报错的分支。如果在上面任何一步渲染过程中抛出了异常,就会走到这里。

  • closeBundle

    打包结束。不论是成功走到了最后,还是中途走到了报错(renderError),最终都会殊途同归走到这里,用作最后清理资源、关闭进程等收尾工作。

三、实战

1. 构建钩子

1)自动引入 polyfill

需求:若文件是入口模块就自动引入 polyfill。

[1] 方案一
javascript
const PROXY_SUFFIX = '?inject-polyfill';
const POLYFILL_ID = '\0polyfill';

function polyfill() {
  return {
    name: 'inject-polyfill', // 插件的名字
    async resolveId(source, importer, options) {
      if (source === POLYFILL_ID) {
        return { id: POLYFILL_ID, moduleSideEffects: true };
      }
      if (options.isEntry) { // 说明这是一个入口点
        // this指PluginContext, resolve方法将导入解析为模块ID (即路径+文件名)
        // 查找模块的ID是什么? 模块文件名或者说此模块的文件的绝对路径
        const resolution = await this.resolve(source, importer, { skipSelf: true, ...options });
        // 如果此模块无法解析, 或者是外部模块, 要以直接返回, rollup 会报错或进行 external 提示
        if (!resolution || resolution.external) {
          return resolution;
        }
        // 加载模块内容
        // 1.读取模块内容 触发load钩子 2.转换模块内容 触发transform钩子 3.模块解析ast 分析ast, 触发moduleParsed
        const moduleInfo = await this.load(resolution)
        // 表示此模块有副作用, 不要tree shaking
        moduleInfo.moduleSideEffects = true;
        // 例子 C:\\aproject\\rollup详解\\rollup插件开发\\src\\index.js?inject-polyfill
        return `${resolution.id}${PROXY_SUFFIX}`;
      }
      return null; // 交给下一个插件处理
    }
    load(id) {
      if (id === POLYFILL_ID) {
        return `console.log('腻子代码')`;
      }
      // 如果是一个需要代理的入口, 特殊处理下, 生成一个中间的代理模块
      if (id.endsWith(PROXY_SUFFIX)) {
        // C:\\aproject\\rollup详解\\rollup插件开发\\src\\index.js
        const entryId = id.slice(0, -PROXY_SUFFIX.length);
        let code = `
          import ${JSON.stringify(POLYFILL_ID)};
          export * from ${JSON.stringify(entryId)}
        `;
        // 如果钩子有返回值了, 不去走后面的load钩子了, 也不会读硬盘上的文件了 类似webpack中loader的pitch
        return code;
      }
      return null;
    }
  }
}

export default polyfill;
[2] 方案二
javascript
function polyfill() {
  return {
    name: 'inject-polyfill2',
    async transform(code, id) {
      return `
        console.log('polyfill');
        ${code}
      `;
    }
  }
}

export default polyfill;
2)transform 钩子
javascript
import babel from '@babel/core';
import { createFilter } from 'rollup-pluginutils';

function babelPlugin(pluginOptions) {
  const { include, exclude, extensions = ['.js'] } = pluginOptions;
  // (js|jsx|ts)$
  const extensionsRegExp = new RegExp(`(${extensions.join('|')})$`);
  const userDefinedFilter = createFilter(include, exclude);
  const filter = id => extensionsRegExp.test(id) && userDefinedFilter(id)

  return {
    name: 'babel',
    async transform(code, id) { // 类似于webpack loader
      // 如果过滤没有通过,就不需要任何处理
      if (!filter(id)) return null;
      return await babel.transformAsync(code, pluginOptions);
    }
  }
}

export default babelPlugin;

2. 输出钩子

1)renderDynamicImport

手写一个动态导入的 polyfill。

javascript
// import.meta.url => http://127.0.0.1:8080/index.js
dynamicImportPolyfill('./msg-80f39f29.js', import.meta.url).then(res => {
  console.log(res.default);
});

function dynamicImportPolyfill(filename, url) {
  return new Promise((resolve) => {
    const script = document.createElement('script');
    script.type = 'module';
    script.onload = () => resolve(window.mod);

    // absURL http://127.0.0.1:8080/msg-80f39f29.js
    const absURL = new URL(filename, url).href;
    console.log('absURL', absURL);
    const blob = new Blob([
      `import * as mod from "${absURL}"; 
       window.mod = mod;`
    ], { type: 'text/javascript' });
    script.src = URL.createObjectURL(blob);
    document.head.appendChild(script);
  });
}
2)resolveFileUrl
javascript
resolveId(source) {
  // 逻辑是获取source对应的绝对路径,直接返回
  if (logger === 'logger') {
    return source;
  }
},
load(importee) {
  if (importee === 'logger') {
    // 发出一个包含在生成输出中的新文件,并返回一个referenceId,该ID可在不同位置用于引用发出的文件
    const referenceId = this.emitFile({
      type: 'asset', // 文件类型
      source: 'console.log("LOGGER")', // 文件内容
      fileName: 'logger.js' // 文件名
    });
    return `export default import.meta.ROLLUP_FILE_URL_${referenceId};`;
  }
}

export default import.meta.ROLLUP_FILE_URL_${referenceId}; 这个代码经过 rollup 处理后变为 new URL('logger.js', import.meta.url).href;

import.meta.url 怎么改?使用 resolveFileUrl 钩子。

javascript
resolveFileUrl({ chunkId, fileName }) {
  return `new URL('${fileName}', document.baseURI).href`;
}

意思是用 new URL('${fileName}', document.baseURI).href 替换 new URL('logger.js', import.meta.url).href

3)resolveImportMeta

比如代码里有 console.log(import.meta.age),经过该钩子会修改为 console.log(14)

javascript
resolveImportMeta(property) {
  console.log('resolveImportMeta', property);
  return '14';
},
4)generateBundle

向输出目录里写入一个 html 文件进行预览。

javascript
generateBundle(options, bundle, isWrite) {
  let entryNames = [];
  for (let fileName in bundle) {
    let assetOrChunkInfo = bundle[fileName];
    if (assetOrChunkInfo.isEntry) {
      entryNames.push(fileName);
    }
  }

  this.emitFile({
    type: 'asset',
    fileName: 'index.html',
    source: `
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>rollup 钩子学习</title>
</head>
<body>
  ${entryNames.map(entryName => `
    <script src="${entryName}" type="module"></script>
  `).join('')}
</body>
</html>
`
  });
}
preview
图片加载中
预览

Released under the MIT License.