目录

---------

虚拟模块实例

在开始学习 rollup 之前,我们先来讲解一下虚拟模块,虚拟模块指的是不通过文件系统就可以访问的成员,举一个例子:

一个虚拟模块的例子:

Typscript
// index.js
import v from 'virtual-module'
console.log(v)

virtual-module 这个依赖我们并没有安装,如果你直接运行这个 js 文件,会报错,所以我们借助 rollup 打包实现:

Javascript
// rollup.config.js
function myExample() {
  return {
  	name: 'rollup-plugin-virtual-example', // rollup 插件规范,应该以 `rollup-plugin-` 作为前缀,
    // 处理 esm
  	resolveId(source) {
      // 如果 import 的名字是 'virtual-module'
      if (source === 'virtual-module') {
        return 'export default "this is virtual-module"'
      }
      // 返回 null 表示 rollup 不做任何额外处理
      return null
    }
  }
}

export default {
  input: './index.js',
  plugins: [myExample()],
  output:[{
    file: 'bundle.js',
    format: 'es'
  }]
}

运行 rollup --config,即可看到打包文件:

Javascript
var v = "this is virtual";

console.log(v);

约定

  • 插件应该有一个明确的名称,并以 rollup-plugin- 作为前缀。
  • package.json 中包含 rollup-plugin 关键字。
  • 插件应该被测试,我们推荐 mochaava,它们支持 Promise。
  • 可能的话,使用异步方法,例如 fs.readFile 而不是 fs.readFileSync
  • 用英文文档描述你的插件。
  • 确保如果适当,你的插件输出正确的源映射。
  • 如果插件使用“虚拟模块”(例如用于辅助函数),请使用\0前缀模块 ID。这可以防止其他插件尝试处理它。

生命钩子周期

rollup 有各种钩子,编写插件时需要掌握各个钩子执行的顺序

image-20230823223520895

options

options 钩子是构建阶段的第一个钩子,它的类型如下:

Typscript
delcare options: (options: InputOptions) => InputOptions | null

interface InputOptions {
	acorn?: Record<string, unknown>;
	acornInjectPlugins?: ((...arguments_: any[]) => unknown)[] | ((...arguments_: any[]) => unknown);
	cache?: boolean | RollupCache;
	context?: string;
	experimentalCacheExpiry?: number;
	experimentalLogSideEffects?: boolean;
	external?: ExternalOption;
	inlineDynamicImports?: boolean;
	input?: InputOption;
	logLevel?: LogLevelOption;
	makeAbsoluteExternalsRelative?: boolean | 'ifRelativeSource';
	manualChunks?: ManualChunksOption;
	maxParallelFileOps?: number;
	maxParallelFileReads?: number;
	moduleContext?: ((id: string) => string | NullValue) | { [id: string]: string };
	onLog?: LogHandlerWithDefault;
	onwarn?: WarningHandlerWithDefault;
	perf?: boolean;
	plugins?: InputPluginOption;
	preserveEntrySignatures?: PreserveEntrySignaturesOption;
	preserveModules?: boolean;
	preserveSymlinks?: boolean;
	shimMissingExports?: boolean;
	strictDeprecations?: boolean;
	treeshake?: boolean | TreeshakingPreset | TreeshakingOptions;
	watch?: WatcherOptions | false;
}

官方介绍是替换或操作传递给 rollup.rollup 配置选项

使用此钩子的应用场景是,只需要读取选项,因为该钩子可以访问所有 options 钩子的转换考虑后的选项

buildStart

buildStart 会在每个 rollup.rollup 构建上调用,此钩子使用场景是需要访问 rollup.rollup 的选项时,它考虑了所有 options 钩子的转换,并且还包含未设置选项的正确默认值,类型:

Typscript
declare buildStart: (options: InputOptions) => void

resolveId

重头戏,让我们先看代码,看看打包之后会发生什么:

Typscript
// 官方的示例:
const POLYFILL_ID = '\0polyfill';
const PROXY_SUFFIX = '?inject-polyfill-proxy';

export function injectPolyfillPlugin() {
	return {
		name: 'inject-polyfill',
		async resolveId(source, importer, options) {
			if (source === POLYFILL_ID) {
				return { id: POLYFILL_ID, moduleSideEffects: true };
			}
			if (options.isEntry) {
				const resolution = await this.resolve(source, importer, {
					skipSelf: true,
					...options
				});
				if (!resolution || resolution.external) return resolution;
				const moduleInfo = await this.load(resolution);
				moduleInfo.moduleSideEffects = true;
				return `${resolution.id}${PROXY_SUFFIX}`;
			}
			return null;
		},
		load(id) {
      if (id === POLYFILL_ID) {
				return "console.log('polyfill');";
			}
			if (id.endsWith(PROXY_SUFFIX)) {
        console.log({ id })
				const entryId = id.slice(0, -PROXY_SUFFIX.length);
				const { hasDefaultExport } = this.getModuleInfo(entryId);
				let code =
					`import ${JSON.stringify(POLYFILL_ID)};` +
					`export * from ${JSON.stringify(entryId)};`;
				if (hasDefaultExport) {
					code += `export { default } from ${JSON.stringify(entryId)};`;
				}
				return code;
			}
			return null;
		}
	};
}
Javascript
// test.js
export function a() {
  console.log('hello a')
}
export default 'hello test'

打包后的文件:

Javascript
console.log('polyfill');

function a() {
  console.log('hello a');
}
var test = 'hello test';

export { a, test as default };

可以看出了开头多了一句打印信息,那么这期间发生了什么呢?

  1. test.js 首先进入 resolveId:

由于 test.js 不含有 \0polyfill 的 importer,而它又是打包的入口文件,所以它会进入第二个 if 判断:

Javascript
if (options.isEntry) {
  // 
  const resolution = await this.resolve(source, importer, {
    // skipSelf 可以避免一直解析自己导致的死循环
    skipSelf: true,
    ...options
  });
  // 如果 resolution 不存在或者存在外部导入,那么直接返回
  if (!resolution || resolution.external) return resolution;
  const moduleInfo = await this.load(resolution);
  moduleInfo.moduleSideEffects = true;
  return `${resolution.id}${PROXY_SUFFIX}`;
}
  1. 上述代码进入 load

由于上述代码最终返回了一个额外的 PROXY_SUFFIX 所以 load API 也会进入第二个 if:

Javascript
if (id.endsWith(PROXY_SUFFIX)) {
  // 拿到入口文件的 Id
  const entryId = id.slice(0, -PROXY_SUFFIX.length);
  // 是否有默认导出
  const { hasDefaultExport } = this.getModuleInfo(entryId);
  // code 将 POLYFILL_ID 作为 importer,这个 importer 同样也会被 resolveId 识别
  let code =
      `import ${JSON.stringify(POLYFILL_ID)};` +
      `export * from ${JSON.stringify(entryId)};`;
  // 命名空间重新导出不会重新导出默认值
  // 因此我们需要特殊处理
  if (hasDefaultExport) {
    code += `export { default } from ${JSON.stringify(entryId)};`;
  }
  return code;
}
  1. 上述 code 代码再次进入 resolveId

由于上述代码包含 POLYFILL_ID importer,所以会直接进入第一个 if 判断:

Javascript
if (source === POLYFILL_ID) {
  // 对于polyfill,必须始终考虑副作用
  // 否则使用 "treeshake.moduleSideEffects: false"
  // 这样可能会阻止包含polyfill
  return { id: POLYFILL_ID, moduleSideEffects: true };
}

polyfill 简单来说就是打补丁,比如远古 ie 浏览器不支持 es6 语法时,就是通过 polyfill 的方式支持,而 roolup 规定,对于 polyfill 必须不设置 treeshake,即将 moduleSideEffects 设置为 true,考虑副作用

  1. 再次进入 load

上述代码返回的 id 正是 POLYFILL_ID,所以正好进入第一个 if:

Javascript
if (id === POLYFILL_ID) {
  // 用实际的 polyfill 替换
  return "console.log('polyfill');";
}

最后查看打包后的文件:

Typscript
console.log('polyfill');

function a() {
  console.log('hello a');
}
var test = 'hello test';

export { a, test as default };

load

官方对 load 的定义是定义自定义加载器,该钩子可以返回一个 { code, ast, map } 对象

Typscript
declare load: (id: string) => LoadResult

type LoadResult = string | null | SourceDescription;

interface SourceDescription {
	code: string;
	map?: string | SourceMap;
	ast?: ESTree.Program;
	assertions?: { [key: string]: string } | null;
	meta?: { [plugin: string]: any } | null;
	moduleSideEffects?: boolean | 'no-treeshake' | null;
	syntheticNamedExports?: boolean | string | null;
}

transform

transform 可用于转换单个模块,为了避免额外的解析开销,例如钩子已经使用了 this.parse 生成了 AST,此钩子可以选择性地返回一个 { code, ast, map } 对象,在之前的 vite插件开发一节中,我们利用了 transform API 实现了按需引用 antd 功能