在开始学习 rollup
之前,我们先来讲解一下虚拟模块,虚拟模块指的是不通过文件系统就可以访问的成员,举一个例子:
vue
项目的时候,我们可以引入 .vue
文件,这得益于 vite
工具提供的虚拟模块,而在其他环境中,引入 .vue
会报错一个虚拟模块的例子:
// index.js
import v from 'virtual-module'
console.log(v)
virtual-module
这个依赖我们并没有安装,如果你直接运行这个 js 文件,会报错,所以我们借助 rollup
打包实现:
// 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
,即可看到打包文件:
var v = "this is virtual";
console.log(v);
约定
rollup
有各种钩子,编写插件时需要掌握各个钩子执行的顺序
options
钩子是构建阶段的第一个钩子,它的类型如下:
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
会在每个 rollup.rollup
构建上调用,此钩子使用场景是需要访问 rollup.rollup
的选项时,它考虑了所有 options
钩子的转换,并且还包含未设置选项的正确默认值,类型:
declare buildStart: (options: InputOptions) => void
重头戏,让我们先看代码,看看打包之后会发生什么:
// 官方的示例:
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;
}
};
}
// test.js
export function a() {
console.log('hello a')
}
export default 'hello test'
打包后的文件:
console.log('polyfill');
function a() {
console.log('hello a');
}
var test = 'hello test';
export { a, test as default };
可以看出了开头多了一句打印信息,那么这期间发生了什么呢?
test.js
首先进入 resolveId
:由于 test.js
不含有 \0polyfill
的 importer,而它又是打包的入口文件,所以它会进入第二个 if
判断:
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}`;
}
load
由于上述代码最终返回了一个额外的 PROXY_SUFFIX
所以 load
API 也会进入第二个 if:
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;
}
code
代码再次进入 resolveId
由于上述代码包含 POLYFILL_ID
importer,所以会直接进入第一个 if
判断:
if (source === POLYFILL_ID) {
// 对于polyfill,必须始终考虑副作用
// 否则使用 "treeshake.moduleSideEffects: false"
// 这样可能会阻止包含polyfill
return { id: POLYFILL_ID, moduleSideEffects: true };
}
polyfill
简单来说就是打补丁,比如远古 ie 浏览器不支持 es6 语法时,就是通过 polyfill
的方式支持,而 roolup
规定,对于 polyfill
必须不设置 treeshake
,即将 moduleSideEffects
设置为 true,考虑副作用
load
上述代码返回的 id 正是 POLYFILL_ID
,所以正好进入第一个 if:
if (id === POLYFILL_ID) {
// 用实际的 polyfill 替换
return "console.log('polyfill');";
}
最后查看打包后的文件:
console.log('polyfill');
function a() {
console.log('hello a');
}
var test = 'hello test';
export { a, test as default };
官方对 load
的定义是定义自定义加载器,该钩子可以返回一个 { code, ast, map }
对象
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
可用于转换单个模块,为了避免额外的解析开销,例如钩子已经使用了 this.parse
生成了 AST,此钩子可以选择性地返回一个 { code, ast, map }
对象,在之前的 vite插件开发一节中
,我们利用了 transform
API 实现了按需引用 antd
功能