00后学习rollup plugin
1. 前言
rollup文档链接:www.rollupjs.com/guide/plugi…
学习第一步,阅读文档。
正如官方文档所说的,rollup plugin本质就是一个 包含 属性(properties), 构建钩子(build hook),
输出生成钩子(output generation hooks) 的对象(object)。
2. 举例
// rollup-plugin-my-example.js
export default function myExample () {
return {
name: 'my-example', // this name will show up in warnings and errors
resolveId ( source ) {
if (source === 'virtual-module') {
return source; // this signals that rollup should not ask other plugins or check the file system to find this id
}
return null; // other ids should be handled as usually
},
load ( id ) {
if (id === 'virtual-module') {
return 'export default "This is virtual!"'; // the source code for "virtual-module"
}
return null; // other ids should be handled as usually
}
};
}
3. 起因
与众多的构建工具一样,在一个真实的项目场景中,rollup本身提供的能力未必能满足复杂的构建需求。
如果把处理逻辑跟打包的逻辑混在一起,就会导致与rollup配置相关的核心代码显得特别臃肿,也不利于维护。
所以,Rollup 设计出了一套完整的插件机制,可根据不同的构建需求引入对应的插件。
4. 方向
在Rollup 的打包过程中,会定义一套完整的构建生命周期,从开始打包到产物输出,中间会经历多个阶段,
同时在不同的阶段会执行对应的插件钩子函数(Hook)。
所以,记忆并理解rollup的插件钩子函数是学习rollup plugin的重要方向。
5. 整体
从整体的角度来说,rollup一次完整的构建周期可分为两大阶段,build(构建)和output(输出)阶段。
build阶段: 解析各模块的内容,根据模块间的依赖关系构建依赖图。
output阶段: 代码的打包与输出
6. 分类
对于rollup插件钩子的类型,我们可以进行一个分类。
(1)分类标准:钩子执行时所处的阶段
build hook
主要事情:模块代码 -> 抽象语法树(AST),AST -> 目标代码,模块依赖图的构建与解析
操作粒度:单个模块,单个文件
output hook
主要事情:多个代码的打包
操作粒度: chunk级别,多个模块组成
(2)分类标准:钩子执行的方式,官方分类,可叠加
async: 异步的钩子 (return a promise resolving to the same type of value)
sync: 同步的钩子
first: 如果有多个插件实现了这个 Hook,那么 Hook 将依次运行,直到返回一个非 null 或非 undefined 的值为止。
sequential: 串行的钩子函数,如果有几个插件实现了这个钩子,它们都会按照指定的插件顺序运行。如果一个钩子是异步的,则此类后续钩子将等待当前挂钩被解析。这种 Hook 往往适用于插件间处理结果相互依赖的情况,即前一个插件 Hook 的返回值作为后续插件的入参。
parallel: 并行的钩子函数, 各走各的,互不干扰。
7. 解读
下面我将从rollup的钩子函数中提取一部分,简单介绍下 rollup 的完整工作流程。
(1)options:串行钩子,解析配置文件,得到新的配置对象。
(2)buildStart: 并行钩子,开始构建流程。
(3)resolveId:first钩子,从入口文件开始解析文件的路径,只有一个插件的resolveId返回了路径,其余插件的resolveId也就会停止执行。
(4)load:first钩子,加载模块内容。
(5)transform:串行钩子,根据模块依赖图确定指定顺序,执行所有的transform钩子,对于模块内容进行转换,例如把es6转换成es5。
(6)在解析模块路径时,如果是external(排除)属性,则跳过打包过程。
(7)resolveDynamicImport:first钩子,如果遇到动态import请求,解析路径成功后会跳到load钩子,加载模块内容。解析路径失败或者是普通的import请求,则会跳转resolveId继续解析路径。
(8)buildEnd:并行钩子,构建阶段结束。
(9)outputOptions:串行钩子,读取output配置。
(10)renderStart:并行钩子,开始打包。
(11)banner、footer、intro、outro钩子:并行钩子,在打包产物的固定位置(比如头部和尾部)插入一些自定义的内容,比如协议声明内容、项目介绍等等。
(12)renderChunk:串行钩子,可获取到chunk 代码内容和chunk 元信息。
(13)generateBundle:串行钩子,产物生成前的最后一步。可获取到output配置和打包产物的元信息,通过操作元信息可以删除一些无用的chunk,也可以输出自定义的新资源。
(14)writeBundle:并行钩子,产物已生成。
(15)closeBundle: 并行钩子,打包结束。
8. 实战
下面 是 rollup插件 @rollup/plugin-alias的源码,功能:设置路径的别名。
// 判断路径是否匹配
function matches(pattern, importee) {
if (pattern instanceof RegExp) {
return pattern.test(importee);
}
if (importee.length < pattern.length) {
return false;
}
if (importee === pattern) {
return true;
}
// eslint-disable-next-line prefer-template
return importee.startsWith(pattern + '/');
}
function getEntries({
entries,
customResolver // 自定义路径解析方法
}) {
if (!entries) {
return [];
}
const resolverFunctionFromOptions = resolveCustomResolver(customResolver);
if (Array.isArray(entries)) {
return entries.map((entry) => {
return {
find: entry.find,
replacement: entry.replacement,
resolverFunction: resolveCustomResolver(entry.customResolver) || resolverFunctionFromOptions
};
});
}
// 解析成一个对象,例如路径中’@‘->'./src'
return Object.entries(entries).map(([key, value]) => {
return {
find: key,
replacement: value,
resolverFunction: resolverFunctionFromOptions
};
});
}
function getHookFunction(hook) {
if (typeof hook === 'function') {
return hook;
}
if (hook && 'handler' in hook && typeof hook.handler === 'function') {
return hook.handler;
}
return null;
}
function resolveCustomResolver(customResolver) {
if (typeof customResolver === 'function') {
return customResolver;
}
if (customResolver) {
return getHookFunction(customResolver.resolveId);
}
return null;
}
function alias(options = {}) {
const entries = getEntries(options);
if (entries.length === 0) {
return {
name: 'alias',
resolveId: () => null
};
}
return {
name: 'alias',
async buildStart(inputOptions) {
await Promise.all([...(Array.isArray(options.entries) ? options.entries : []), options].map(({
customResolver
}) => {
var _a;
return customResolver && ((_a = getHookFunction(customResolver.buildStart)) === null || _a === void 0 ? void 0 : _a.call(this, inputOptions));
}));
},
resolveId(importee, importer, resolveOptions) {
/*
importee,当前资源路径(import ‘xx’语句中的xx原始字符串)
importer,被哪个模块导入的,父模块的路径(一般是绝对路径,如果importee是入口文件,则该值为undefined。表示没有父模块导入它)。
resolveOptions,可能用到的一些配置。比如isEntry: boolean,当前impotee文件是不是入口文件之类的。
*/
if (!importer) {
return null;
}
// First match is supposed to be the correct one
const matchedEntry = entries.find((entry) => matches(entry.find, importee));
if (!matchedEntry) {
return null;
}
//匹配成功,路径替换
const updatedId = importee.replace(matchedEntry.find, matchedEntry.replacement);
// resolverFunction对应选项中的customResolver,自定义的路径解析算法
if (matchedEntry.resolverFunction) {
return matchedEntry.resolverFunction.call(this, updatedId, importer, resolveOptions);
}
// this.resolve是rollup自带的路径解析算法,返回一个资源对象
return this.resolve(updatedId, importer, Object.assign({
skipSelf: true
}, resolveOptions)).then((resolved) => resolved || {
id: updatedId
});
}
};
}
export {
alias as
default
};
//# sourceMappingURL=index.js.map