Jacky's blog Jacky's blog
首页
  • 编码专题
  • 深入浅出 Vite
  • 深入浅出 babel
  • 快速上手API
  • 深入浅出 react
  • Node

    • code-notebook
  • 状态管理

    • redux
  • 前端工程化

    • Wepack
  • React源码

    • React源码
  • 组件库封装

    • 组件库
  • 开发工具

    • Vscode 插件
  • 项目展示
  • 案例中心 (opens new window)
  • First Project
  • 基础算法题
  • 链表题
  • 动态规划
  • 双指针
  • 递归
  • 数据结构
  • 前端学习计划 (opens new window)
  • 技术随笔
  • 转载文章
  • 包管理工具
  • 前端学习周报
  • VSCode插件
  • Promise 专题
  • 函数技巧
  • React 专题
  • 配置文件

    • TSCONFIG-配置 (opens new window)
    • NGINX-配置 (opens new window)
    • 正则规则查询手册 (opens new window)
    • Lint 配置 (opens new window)
  • 教程

    • GIT-教程
    • NPM SCRIPTS-工作流 (opens new window)
    • DOCKER-教程 (opens new window)
    • LERNA-教程 (opens new window)
    • GIT-常用操作整理 (opens new window)
  • VSCode

    • LAUNCH.JSON (opens new window)
  • 指令

    • NPM 指令 (opens new window)
    • NVM 指令 (opens new window)
    • Nginx 指令 (opens new window)
    • YARN 指令 (opens new window)
    • PNPM 指令 (opens new window)
  • 库

    • FS-EXTRA 库 (opens new window)
    • NODE 库-PATH (opens new window)
  • 永远的神

    • 魔法师卡颂-自顶向下学 React 源码 (opens new window)
    • 全栈潇晨 (opens new window)
    • 博客-程序员山月-Daily (opens new window)
    • 淘系前端:冴羽 (opens new window)
  • 系列文章

    • 《图解HTTP》 (opens new window)
    • 《ES6标准入门》 (opens new window)
    • 《现代JavaScript教程》 (opens new window)
    • 《深入浅出Webpack》 (opens new window)
    • VSCode 插件系列:小茗同学 (opens new window)
    • JEST 教程 (opens new window)
    • 前端精读周刊:各种精读系列 (opens new window)
    • 一文吃透系列 (opens new window)
    • 图解 REACT 原理 (opens new window)
  • 实用网站

    • MDN (opens new window)
    • CAN I USE (opens new window)
    • TYPESCRIPT-ESLint-RULES (opens new window)
    • ESLint-RULES (opens new window)
    • FRONT-END TREND (opens new window)
    • NPM TREND (opens new window)
    • 在线分析 Node 依赖 (opens new window)
    • FIND NPM (opens new window)
    • CODE PEN (opens new window)
    • 印记中文 (opens new window)
    • TOOL.LU (opens new window)
    • 阮一峰-网道 (opens new window)
    • DIGITAL OCEAN (opens new window)
    • DEVDOCS.IO (opens new window)
    • JOI (opens new window)
  • 算法

    • 小浩算法 (opens new window)
    • LABULADONG 的算法小抄 (opens new window)
    • 力扣 SOLUTION (opens new window)
    • HACKER RANK (opens new window)
    • 代码随想录 (opens new window)
  • 博客系列

    • 美团大佬 (opens new window)
    • 蜡笔小伟 (opens new window)
    • 优秀博客1 (opens new window)
    • 优秀博客2-umi (opens new window)
    • 优质博客 (opens new window)
  • CSS

    • CSS-EASING 库 (opens new window)
    • ROUGH.JS (opens new window)
    • CSS 网站收集
    • UNOCSS (opens new window)
  • 前端

    • PROMISE (opens new window)
    • UNDERSCORE.JS (opens new window)
    • study with BGM (opens new window)
    • nginx【B站视频】 (opens new window)
    • 机器学习
    • Js基础
  • 掘金已购课程

    • 前端自动化测试精讲 (opens new window)
    • 深入浅出 Vite (opens new window)
    • 现代 Web 布局 (opens new window)
    • 前端算法与数据结构 (opens new window)
    • 基于 Vite 的 SSG 框架开发实战 (opens new window)
    • SSR 实战:官网开发指南 (opens new window)
    • WebGL 入门与实践 (opens new window)
    • 玩转 CSS 的艺术之美 (opens new window)
    • 前端调试通关秘籍 (opens new window)
    • React 进阶实践指南 (opens new window)
    • TypeScript 全面进阶指南 (opens new window)
    • 前端缓存技术与方案解析 (opens new window)
    • npm scripts 前端工作流 (opens new window)
    • Webpack5 核心原理与应用实践 (opens new window)
  • 购物车

    • 张鑫旭-技术写作指南 (opens new window)
    • 深入剖析 Node.js 底层原理 (opens new window)
    • 前端开发者的现代 C++ 课 (opens new window)
    • 从前端到全栈 (opens new window)
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

Jacky Wang

行到水穷处,坐看云起时
首页
  • 编码专题
  • 深入浅出 Vite
  • 深入浅出 babel
  • 快速上手API
  • 深入浅出 react
  • Node

    • code-notebook
  • 状态管理

    • redux
  • 前端工程化

    • Wepack
  • React源码

    • React源码
  • 组件库封装

    • 组件库
  • 开发工具

    • Vscode 插件
  • 项目展示
  • 案例中心 (opens new window)
  • First Project
  • 基础算法题
  • 链表题
  • 动态规划
  • 双指针
  • 递归
  • 数据结构
  • 前端学习计划 (opens new window)
  • 技术随笔
  • 转载文章
  • 包管理工具
  • 前端学习周报
  • VSCode插件
  • Promise 专题
  • 函数技巧
  • React 专题
  • 配置文件

    • TSCONFIG-配置 (opens new window)
    • NGINX-配置 (opens new window)
    • 正则规则查询手册 (opens new window)
    • Lint 配置 (opens new window)
  • 教程

    • GIT-教程
    • NPM SCRIPTS-工作流 (opens new window)
    • DOCKER-教程 (opens new window)
    • LERNA-教程 (opens new window)
    • GIT-常用操作整理 (opens new window)
  • VSCode

    • LAUNCH.JSON (opens new window)
  • 指令

    • NPM 指令 (opens new window)
    • NVM 指令 (opens new window)
    • Nginx 指令 (opens new window)
    • YARN 指令 (opens new window)
    • PNPM 指令 (opens new window)
  • 库

    • FS-EXTRA 库 (opens new window)
    • NODE 库-PATH (opens new window)
  • 永远的神

    • 魔法师卡颂-自顶向下学 React 源码 (opens new window)
    • 全栈潇晨 (opens new window)
    • 博客-程序员山月-Daily (opens new window)
    • 淘系前端:冴羽 (opens new window)
  • 系列文章

    • 《图解HTTP》 (opens new window)
    • 《ES6标准入门》 (opens new window)
    • 《现代JavaScript教程》 (opens new window)
    • 《深入浅出Webpack》 (opens new window)
    • VSCode 插件系列:小茗同学 (opens new window)
    • JEST 教程 (opens new window)
    • 前端精读周刊:各种精读系列 (opens new window)
    • 一文吃透系列 (opens new window)
    • 图解 REACT 原理 (opens new window)
  • 实用网站

    • MDN (opens new window)
    • CAN I USE (opens new window)
    • TYPESCRIPT-ESLint-RULES (opens new window)
    • ESLint-RULES (opens new window)
    • FRONT-END TREND (opens new window)
    • NPM TREND (opens new window)
    • 在线分析 Node 依赖 (opens new window)
    • FIND NPM (opens new window)
    • CODE PEN (opens new window)
    • 印记中文 (opens new window)
    • TOOL.LU (opens new window)
    • 阮一峰-网道 (opens new window)
    • DIGITAL OCEAN (opens new window)
    • DEVDOCS.IO (opens new window)
    • JOI (opens new window)
  • 算法

    • 小浩算法 (opens new window)
    • LABULADONG 的算法小抄 (opens new window)
    • 力扣 SOLUTION (opens new window)
    • HACKER RANK (opens new window)
    • 代码随想录 (opens new window)
  • 博客系列

    • 美团大佬 (opens new window)
    • 蜡笔小伟 (opens new window)
    • 优秀博客1 (opens new window)
    • 优秀博客2-umi (opens new window)
    • 优质博客 (opens new window)
  • CSS

    • CSS-EASING 库 (opens new window)
    • ROUGH.JS (opens new window)
    • CSS 网站收集
    • UNOCSS (opens new window)
  • 前端

    • PROMISE (opens new window)
    • UNDERSCORE.JS (opens new window)
    • study with BGM (opens new window)
    • nginx【B站视频】 (opens new window)
    • 机器学习
    • Js基础
  • 掘金已购课程

    • 前端自动化测试精讲 (opens new window)
    • 深入浅出 Vite (opens new window)
    • 现代 Web 布局 (opens new window)
    • 前端算法与数据结构 (opens new window)
    • 基于 Vite 的 SSG 框架开发实战 (opens new window)
    • SSR 实战:官网开发指南 (opens new window)
    • WebGL 入门与实践 (opens new window)
    • 玩转 CSS 的艺术之美 (opens new window)
    • 前端调试通关秘籍 (opens new window)
    • React 进阶实践指南 (opens new window)
    • TypeScript 全面进阶指南 (opens new window)
    • 前端缓存技术与方案解析 (opens new window)
    • npm scripts 前端工作流 (opens new window)
    • Webpack5 核心原理与应用实践 (opens new window)
  • 购物车

    • 张鑫旭-技术写作指南 (opens new window)
    • 深入剖析 Node.js 底层原理 (opens new window)
    • 前端开发者的现代 C++ 课 (opens new window)
    • 从前端到全栈 (opens new window)
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • 编码专题

  • 深入浅出 Vite

    • 00-目录大纲
    • 07-Vite预构建上手
    • 09-Esbuild 插件开发实战
    • 10-Rollup 打包基本概念及使用
    • 11-Rollup打包机制及插件开发
      • 0.前言
      • 1.构建机制
        • 问题1:在Rollup一次完整的构建过程中,Rollup会经历哪两个阶段?每个阶段的作用是什么?
        • 问题2:rollup 中 Build Hook 和 Output Hook 的本质区别是什么?
        • 问题3:根据 Hook 执行方式可以把插件分成哪五类?
        • 问题4:请描述一下Rollup插件在build阶段的工作流程?
        • 问题5:请描述一下Rollup插件在Output阶段的工作流程?
      • 2. 官方插件源码解析
        • 问题6:如何实现别名替换 alias 插件?
        • 问题7:如何支持图片加载 image 插件?
        • 问题8:如何实现一个全局替换 replace 插件?
    • 12-Vite插件开发实战
    • 13-Vite热更新
    • 16-Vite搭建 SSR 框架
  • 快速上手 API

  • 深入浅出Babel

  • 深入浅出 React

  • 百问掘金
  • 深入浅出 Vite
wangjiasheng
2023-04-03
目录

11-Rollup打包机制及插件开发

# 0.前言

本节课程代码的仓库 (opens new window):13-vite-rollup

承接上文,本篇博客为《深入浅出 Vite 》掘金手册第十一章的总结。

# 1.构建机制

# 问题1:在Rollup一次完整的构建过程中,Rollup会经历哪两个阶段?每个阶段的作用是什么?

Rollup 内部会经历 build 和 output 两个大阶段

代码逻辑简化如下:

// Build 阶段
const bundle = await rollup.rollup(inputOptions);

// Output 阶段
await Promise.all(outputOptions.map(bundle.write));

// 构建结束
await bundle.close();
1
2
3
4
5
6
7
8

Build 阶段:通过 debug 断点后,简化 bundle 对象(以简化部分)如下:

{
  /* 缓存 ast 相关信息 */
  cache: {
    modules: [
      {
        assertions: {
        },
        ast: {
          .......
          sourceType: "module",
        },
        /* 构建后代码 */
        code: "export var add = function (a, b) { return a + b; };\r\nexport var multiple = function (a, b) { return a * b; };\r\n",
        /* 导入的模块 */
        id: "/Users/jiashengwang/Project/Learn-vite/examples/13-vite-rollup/src/basic/util.ts",
        moduleSideEffects: true,
        /* 源代码 */
        originalCode: "export const add = (a: number, b: number) => a + b;\n\nexport const multiple = (a: number, b: number) => a * b;\n",
      },
    ],
    /* 记录使用到的插件 */
    plugins: {},
    close: /* 关闭构建*/
    closed: /* 标识构建是否结束 */
    generate: /* 生成 chunk */
    write: /* 将 chunk 写入到磁盘 */
    watchFiles: [ /* 记录入口文件相关信息 */
    "/Users/../examples/13-vite-rollup/src/basic/index.ts",
    "/Users/../examples/13-vite-rollup/src/basic/util.ts",
  	],
  },
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

从上可以看出,Build 阶段主要完成的事情有:

  1. bundle 对象的 cache.modules 中实际存储各个模块的内容(源码及构建后代码),模块依赖关系,以及解析后的 ast 树。
  2. 暴露出三个函数:generate 、 write 和close 方法,用于进入到后的 Outup 阶段。

Output 阶段:通过打断点 const {output} = bundle.generate({}) 查看 output 对象。

分析如下:

// 入口源码 src/basic/index.ts
import { add, multiple } from "./util";

console.log(add(1, 2));

// ========== 构建结果 ==========
{
  output: [
    {
      exports: [], /* 当前无导出 */
      facadeModuleId: "/Users/jiashengwang/Project/Learn-vite/examples/13-vite-rollup/src/basic/index.ts",
      isEntry: true,
      isDynamicEntry: false, /* 是否为动态导入入口模块 */
      isImplicitEntry: false, /* 是否为隐式入口模块 */
      isEntry: true, /* 是否为入口 */
      type: 'chunk', /* 类型 */
      /* 打包后的代码 */
      code: "import { add } from './util.85d9f98d.js';\n\nconsole.log(add(1, 2));\r\n/* console.log(multiple(2, 3)); */\n//# sourceMappingURL=index.ce505c09.js.map\n", 
      dynamicImports: [],
      fileName: "index.ce505c09.js", /* 构建后名称 */
      imports: [
        "util.85d9f98d.js", /* 导入的模块 */
      ],
      // 其余属性省略
    }
  ]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

非入口文件分析:

// src/basic/util.ts
export const add = (a: number, b: number) => a + b;
export const multiple = (a: number, b: number) => a * b;

// ========== 构建结果 ==========
{
  output: [
    {
      /* 导出的方法 */
      exports: [
        "add",
        "multiple",
    	], 
      facadeModuleId: "/Users/../examples/13-vite-rollup/src/basic/util.ts",
      isDynamicEntry: false,
      isImplicitEntry: false, 
      isEntry: true,
      type: 'chunk', /* 类型 */
      /* 打包后的代码 */
      code: "var add = function (a, b) { return a + b; };\r\nvar multiple = function (a, b) { return a * b; };\n\nexport { add, multiple };\n//# sourceMappingURL=util.85d9f98d.js.map\n",
      dynamicImports: [],
      fileName: "util.85d9f98d.js", /* 构建后名称 */
      imports: [],
      // 其余属性省略
    }
  ]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

对应 map 文件结构:

{
  output: [
    {
      fileName: "index.ce505c09.js.map",
      source: "{\"version\":3,\"file\":\"index.ce505c09.js\",\"sources\":[],\"sourcesContent\":[],\"names\":[],\"mappings\":\";;;\"}",
      type: "asset", /* 类型为 assets */
    },
    {
      fileName: "util.85d9f98d.js.map",
    }
  ]
}
1
2
3
4
5
6
7
8
9
10
11
12

最终的输出结果:

image-20230403232858652

# 问题2:rollup 中 Build Hook 和 Output Hook 的本质区别是什么?

插件的各种 Hook 可以根据这两个构建阶段分为两类: Build Hook 与 Output Hook。

  • Build Hook:是以 module 作为处理边界。
  • Output Hook:则是以 Chunk 作为处理边界。

# 问题3:根据 Hook 执行方式可以把插件分成哪五类?

Rollup 中的钩子类型应该也是参考 tabpable 这个库。

hook钩子类型大致可以分为 5 类:

  1. 同步Sync or 非同步 Async。
  2. 并行Parallel or 串行Sequential :类比 Promise.all 和 async + await
  3. First:类比 Promise.race 仅处理第一个返回值。

上述分类太八股了点,背住即可,实际流程还是挺简单的。

# 问题4:请描述一下Rollup插件在build阶段的工作流程?

Build 的执行流程图如下:

主要记住以下几点即可:

  1. 对于多个插件的启动是并发的,因此构建开始阶段 buildStart为 parallel 模式。

  2. 而单个插件内部的执行流程是串行的。

    其中负责解析 module 的钩子,即 resolve + load 类属于 First 型。当某个模块被插件处理过后,其他模块无法处理了,如果此时仍需处理,可通过 this.resolve() 发起二次模块解析操作,这一点特性在rollup 插件阶段的 alias 插件时体现的很明显。

  3. 核心构建流程:resolve=> load => transform(字符串到字符串)=> moduleParse(字符串到 ast 树,这个阶段很耗时可以是并发模式)

# 问题5:请描述一下Rollup插件在Output阶段的工作流程?

Output 的执行流程图如下:

Build 和 Output 其实也挺像的,只是前者处理 moudle,或者处理 chunk,如何 emit 到磁盘:

  1. 在执行多个插件时基本都是并发的。如renderStart 中并发执行所有插件的 banner\footer\intro\outro 钩子。这四个钩子功能很简单,就是往打包产物的固定位置(比如头部和尾部)插入一些自定义的内容,比如协议声明内容、项目介绍等等。
  2. writeBundle 也是如此,构建结束后,output 结果为一个数组,根据数组中的 filename 去往磁盘中输出产物。
  3. 特殊的钩子解析:
    • augmentChunkHash:决定是否要以 hash 方式命名。
    • resolveFileUrl:之前在构建 __dirname 时有使用过,此阶段遇到 import.meta.url 语句时,可通过此函数解析。(路径解析类的都属于 First 型)
    • resolveImportMeta:对于 import.meta.属性 语句时,可通过此函数解析。(路径解析类的都属于 First 型)
  4. 构建的核心流程: renderChunk=>generateBundle=>writeBundle

# 2. 官方插件源码解析

# 问题6:如何实现别名替换 alias 插件?

官方 alias 插件 (opens new window) 功能很全,我在 仓库 (opens new window)中仅实现了一个简易版本。

使用方式:

// 官方插件
import alias from "@rollup/plugin-alias";
// 常用 inputOptions 配置
const inputOptions = {
  input: path.join(SRC_PATH, "plugin", "alias.js"),
  plugins: [
    otherPlugin(),
    myAlias({
      /* 将 util-a 这个虚假模块替换为 ./util.js 相对 */
      entries: [{ find: "util-a", replacement: "./util.js" }],
    }),
    /* 官方用法 */
    alias({
      entries: [{ find: "util-b", replacement: "./util.js" }],
    }),
  ],
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

其中,util-a 和 util-b 为两个虚拟模块,使用官方实践处理 util-b,使用myAlias 处理 util-a。

import { add } from "util-a";
import { multiple } from "util-b";
1
2

实践思路:

  1. 通过 resolveId 筛选出 util-a 模块,筛选后通过字符串的 replace 函数替换成目标模块./util.js。
  2. 特别注意时,由于 resolveId 属于First 型,当 myAlias 处理后,后续所有的插件将不再处理此模块,需考虑一个情况,就是转译后的 ./util.js 有可能还会被二次处理。因此需要通过 this 上下文,进行模块二次触发。
  3. 第二次触发时,myAlias 不需要再进行处理了,通过透传 { skipSelf: true } 可跳过当前插件。

简易版代码如下:

/* 简易版-官方插件,经支持 find(不支持正则) 和 replacement 两个参数 */
function myAlias(options) {
  // 获取 entries 配置
  const { entries } = options;
  return {
    name: "myAlias",
    // 传入三个参数,当前模块路径、引用当前模块的模块路径、其余参数
    resolveId(importee, importer, resolveOptions) {
      log(importee, importer, "alias-plugin");
      // 根据 find 过滤出模块
      const matchedEntry = entries.find(
        (entry) =>
          /* matches(entry.find, importee), */
          entry.find === importee,
      );
      /* 判断是否为入口模块 */
      const isEntry = !importer;

      // 如果不能匹配替换规则,或者当前模块是入口模块,则不会继续后面的别名替换流程
      if (!matchedEntry || isEntry) {
        return null;
      }

      // 执行替换操作
      const updatedId = importee.replace(
        matchedEntry.find,
        matchedEntry.replacement,
      );

      /* ===== END ===== */
      /* 理论上替换完成后,直接 return string 或对象 即可,但是仍需考虑一个问题,
      转译后的模块(本例中为 "./util.js")需不需要被其他模块所处理。*/
      /* 因此:需通过  this.resolve 会执行所有插件(除当前插件外)的 resolveId 钩子,重新发起一轮构建去处理 "./util.js" 依赖。 */
      /* 新一轮依赖处理,当前插件无需处理,则可以通过传入第三个参数 {skipSelf: true} 跳过 */
      console.log("\n触发第二轮依赖解析......\n");
      return this.resolve(
        updatedId,
        importer,
        Object.assign({ skipSelf: true }, resolveOptions),
      ).then((resolved) => {
        // 替换后的路径即 updateId 会经过别的插件进行处理
        /* 如果是个真实的地址,最终会被 rollup 替换为绝对路径,若为虚拟模块的话 */

        let finalResult = resolved;
        if (!finalResult) {
          // 如果其它插件没有处理这个路径,则直接返回 updateId
          finalResult = { id: updatedId };
        }
        return finalResult;
      });
    },
  };
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53

这边为了能更好的看出二次依赖解析的过程,封装 log 函数打印处理结果。

function log(importee, importer, pluginName) {
  const isEntry = !importer;
  if (isEntry) {
    console.log(`${pluginName}解析: 入口文件`);
  } else {
    console.log(`${pluginName}解析: ${importee}`);
  }
}
1
2
3
4
5
6
7
8

执行:npm run plugin:alias ,打印结果如下:

otherPlugin解析: 入口文件
myAlias解析: 入口文件 
otherPlugin解析: util-a
otherPlugin解析: util-b
myAlias解析: util-a

触发第二轮依赖解析......

myAlias解析: util-b
otherPlugin解析: ./util.js
otherPlugin 可以捕获到经 alias 插件 replace 后的模块
otherPlugin解析: ./util.js
otherPlugin 可以捕获到经 alias 插件 replace 后的模块
myAlias解析: ./util.js
🚀 Build Finished!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

可以发现如下:

  • 插件生效时为并发,因此两个插件 myAlias 和 otherPlugin 同时处理入口依赖。且整体执行次序按照plugins 的书写顺序。

    otherPlugin解析: 入口文件
    myAlias解析: 入口文件 
    
    1
    2
  • 单个插件属于串行,因此两个插件会依次触发解析 util-a 和 util-b 两个包。

    otherPlugin解析: util-a
    otherPlugin解析: util-b
    myAlias解析: util-a 
    // 调用 this.resolve 此为 async 模式。
    myAlias解析: util-b
    
    1
    2
    3
    4
    5

    因为 util-a 符合 alias 的 find 条件,会被转译为 ./util.js,触发二次依赖解析。会在 myAlias 触发结束后执行 this.resolve 函数。这就是 hook 钩子为 async 的体现。

  • 由于 { skipSelf: true } 二次构建时,只有 otherPlugin 和官方的 alias 参与解析。

    otherPlugin解析: ./util.js
    otherPlugin 可以捕获到经 alias 插件 replace 后的模块
    
    1
    2

    当 alias 模块解析到 util-b 时,也会触发二次构建,因此最后执行:

    otherPlugin解析: ./util.js
    otherPlugin 可以捕获到经 alias 插件 replace 后的模块
    
    1
    2

分析结束,感觉我这个例子设计的超级好,读懂这个例子就能完全明白 hook 5种类型的实际含义了。

# 问题7:如何支持图片加载 image 插件?

官方 image 插件 (opens new window) , 仓库 (opens new window)仅实现了一个简易版本。

相当于webpack 的 file-loader 插件去处理图片。

使用方式:

// 常用 inputOptions 配置
const inputOptions = {
  input: path.join(SRC_PATH, "plugin", "image.js"),
  plugins: [
    myImage({
      dom: true, 
    }),
  ],
};
1
2
3
4
5
6
7
8
9

myImage 插件支持一个 dom 参数,开启后可如下使用:

import logo from './rollup.png';
document.body.appendChild(logo);
1
2

涉及依赖处理流程:resolve => load

实现思路:

  1. 使用 resolveId 拦截.png 等后缀图片模块(由于太过简单,在load 钩子中也可直接完成)

  2. 在 load 钩子函数中,使用 fs.readFileSync(xxx,"base64") 以 base64 的方式读取图片资源。

    如果 dom:false,则直接返回 base64,构造 export default ${dataUri} 即可。

    如果 dom:true,通过 new Image 创建一个 <img> 标签后返回,如下:

    function domeTemplate({dataUri}){
      return `
      var img = new Image();
      img.src = "${dataUri}";
      export default img;
      `;
    }
    
    1
    2
    3
    4
    5
    6
    7

额外需要注意的是,对不同格式的图片处理逻辑有所不同:

  1. 对于 img 图片:直接 fs.readFileSync("xxx.png","base64") 获取 dataUri
  2. 对于 svg 图片:对于 svg 格式的图片,并不是直接以 base64 的方式读取,而是通过 fs.readFileySync("xxx.svg","utf-8") 的方式获取 svg 字符串,再通过mini-svg-data-uri 这个第三方库获取压缩后的 dataUri。

简化版的代码:

const defaultOption = {
  dom: false,
  exclude: null,
  include: null,
};

function myImage(opts = {}) {
  const options = { ...defaultOption, ...opts };
  return {
    name: "rollup:image",
    load(id) {
      /* 1. 获取文件后缀名 */
      const fileExtname = path.extname(id);
      const mime = mimeTypes[fileExtname];

      if (!mime) {
        /* 非图标格式文件 */
        return null;
      }

      const isSvg = fileExtname === ".svg";
      const format = isSvg ? "utf-8" : "base64";
      const source = fs.readFileSync(id, format).replace(/[\r\n]+/gm, "");
      const dataUri = isSvg
        ? svgToMiniDataURI(source)
        : `data:${mime};${format},${source}`;
      const code = options.dom
        ? domTemplate({ dataUri })
        : constTemplate({ dataUri });
      return code.trim();
    },
  };
}

function domTemplate({ dataUri }) {
  return `
  var img = new Image();
  img.src = "${dataUri}";
  export default img;
  `;
}

function constTemplate({ dataUri }) {
  return `
  var img = "${dataUri}";
  export default img;
  `;
}

async function build() {
  const bundle = await rollup.rollup(inputOptions);
  await bundle.write({
    dir: path.join(DIST_PATH, "plugin", "image"),
    format: "cjs",
  });
}

build()
  .then(() => {
    console.log("🚀 Build Finished!");
  })
  .catch((error) => {
    console.log("rollup failed", error);
  });
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64

# 问题8:如何实现一个全局替换 replace 插件?

官方 replace 插件 (opens new window) , 仓库 (opens new window)仅实现了一个简易版本。

使用方式:

// 常用 inputOptions 配置
const inputOptions = {
  input: path.join(SRC_PATH, "plugin", "replace.js"),
  external: [],
  plugins: [
    replace({
      "process.env.Jacky": "'Jack!!!'",
    }),
    myReplace({
      "process.env.Hello": "'Jack!!!'",
      /* 支持回调形式,且回调中可获取模块id */
      "process.env.World": () => "'Jack!!!'",
       delimiters: ["\\b", "\\b(?!\\.)"],
    }),
  ],
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

测试文本如下:

console.log("process.env.Jacky", process.env.Jacky);
console.log("process.env.Hello", process.env.Hello);
console.log("process.env.World", process.env.World);
1
2
3

输出结果如下:

'use strict';

console.log("'Jack!!!'", 'Jacky!!!');
console.log("'Jack!!!'", 'Jack!!!');
console.log("'Jack!!!'", 'Jack!!!');
1
2
3
4
5

实现思路:

  • 这个插件功能是典型的transform 功能,即 string => string
  • 核心思路是,通过 transform(code,id) 可以获取字符串,通过magic-string 第三方包工具完成字符串的替换后返回。

代码逻辑:

  1. 读取转化规则字段。由于myReplace 组件只接受一个对象,配置字段和替换规则混合在一起了,所以需要过滤规则,通过 getReplacements 删除 delimiters/include/exclude 等属性。

  2. 规则支持回调格式,如 {"process.env.World": () => "'Jack!!!'"},通过 mapToFunctions 将所有规则键值改为:Record<string,function> 格式。

  3. 对替换键值进行 escape 处理,是为了防止后续使用 magic string 包替换被正则匹配。

  4. 生成替换规则,如:/\b(process\.env\.Hello|process\.env\.World)\b(?!\.)/g

    带匹配的键值都通过 (规则1|规则2|规则3) 分割,前后通过 \b 和 \b(?!\.) 作为单词分隔。

  5. 以上准备做完,就直接通过 excuteReplacement 进行字符替换工作。

具体简易版实现代码如下:

function myReplace(opts = {}) {
  /* 此处 \\b xxxx \\b 用于单词边界区分 */
  const { delimiters = ["\\b", "\\b(?!\\.)"] } = opts as any;
  const replacements = getReplacements(opts);
  const functionValues = mapToFunctions(replacements);
  const keys = Object.keys(functionValues).map(escape);
  const pattern = new RegExp(
    `${delimiters[0]}(${keys.join("|")})${delimiters[1]}`,
    "g",
  );
  return {
    name: "rollup:replace",
    transform(code, id) {
      if (!keys.length) return null;
      debugger;
      return executeReplacement(code, id);
    },
  };
  /* 过滤额外属性 */
  function getReplacements(options) {
    const values = Object.assign({}, options);
    delete values.delimiters;
    return values;
  }

  /* 将对象转化为函数 */
  function mapToFunctions(object) {
    return Object.keys(object).reduce((pre, cur) => {
      const functions = { ...pre };
      functions[cur] = ensureFunction(object[cur]);
      return functions;
    }, {});
  }

  /* 将 value 转化为函数 */
  function ensureFunction(functionOrValue) {
    if (typeof functionOrValue === "function") return functionOrValue;
    return () => functionOrValue;
  }

  function executeReplacement(code, id) {
    const magicString = new MagicString(code);
    let match;

    while ((match = pattern.exec(code))) {
      const start = match.index;
      const end = start + match[0].length;
      const replacement = String(functionValues[match[1]](id));
      magicString.overwrite(start, end, replacement);
    }
    const result = { code: magicString.toString() };
    return result;
  }
}

/* []内的特殊字符都无需添加转义 */
function escape(str) {
  return str.replace(/[-[\]/{}()*+?.\\^$|]/g, "\\$&");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59

上述处理中一大段代码都是在做准备工作,最后触发 executeReplacement 函数执行替换,比较有意思的是正则规则,如:

  • escape 替换:str.replace(/[-[\]/{}()*+?.\\^$|]/g, "\\$&)" 为所有特殊字符添加反斜杠

    通过 [] 包裹特殊字符时,就无需对所有特殊字符添加 \ 反斜杠,除 ] 比较特殊。

    通过 $& 可以获取匹配值,测试结果如下:

    "[]{}".replace(/[-[\]/{}()*+?.\\^$|]/g, "\\$&")
    '\\[\\]\\{\\}'
    
    1
    2
  • 通过 \b 可以作为单词边界,如获取完整的cat 单词

    "cat catb".match(/\bcat\b/g)
    > ['cat']
    
    1
    2
编辑 (opens new window)
上次更新: 2023/04/05, 22:04:00
10-Rollup 打包基本概念及使用
12-Vite插件开发实战

← 10-Rollup 打包基本概念及使用 12-Vite插件开发实战→

最近更新
01
如何理解浏览器的 user agent 用户代理的含义?
11-05
02
浏览器事件循环机制
10-31
03
浏览器页面渲染机制【2023】
10-15
更多文章>
Theme by Vdoing | Copyright © 2020-2023
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式