大家好,我是小雨小雨,致力于分享有趣的、实用的技术文章。

内容分为翻译和原创,如果有问题,欢迎随时评论或私信,希望和大家一起进步。

大家的支持是我创作的动力。

计划

rollup系列打算一章一章的放出,内容更精简更专一更易于理解

这是rollup系列的最后一篇文章,以下是所有文章链接。

TL;DR

rollup的插件和其他大型框架大同小异,都是提供统一的标准接口,通过约定大于配置定义公共配置,注入当前构建结果相关的属性与方法,供开发者进行增删改查操作。为稳定可持续增长提供了强而有力的铺垫!

但不想webpack区分loader和plugin,rollup的plugin既可以担任loader的角色,也可以胜任传统plugin的角色。rollup提供的钩子函数是核心,比如load、transform对chunk进行解析更改,resolveFileUrl可以对加载模块进行合法解析,options对配置进行动态更新等等~

注意点

所有的注释都在这里,可自行阅读

!!!提示 => 标有TODO为具体实现细节,会视情况分析。

!!!注意 => 每一个子标题都是父标题(函数)内部实现

!!!强调 => rollup中模块(文件)的id就是文件地址,所以类似resolveID这种就是解析文件地址的意思,我们可以返回我们想返回的文件id(也就是地址,相对路径、决定路径)来让rollup加载

rollup是一个核心,只做最基础的事情,比如提供默认模块(文件)加载机制, 比如打包成不同风格的内容,我们的插件中提供了加载文件路径,解析文件内容(处理ts,sass等)等操作,是一种插拔式的设计,和webpack类似

插拔式是一种非常灵活且可长期迭代更新的设计,这也是一个中大型框架的核心,人多力量大嘛~

主要通用模块以及含义

  1. Graph: 全局唯一的图,包含入口以及各种依赖的相互关系,操作方法,缓存等。是rollup的核心
  2. PathTracker: 引用(调用)追踪器
  3. PluginDriver: 插件驱动器,调用插件和提供插件环境上下文等
  4. FileEmitter: 资源操作器
  5. GlobalScope: 全局作用局,相对的还有局部的
  6. ModuleLoader: 模块加载器
  7. NodeBase: ast各语法(ArrayExpression、AwaitExpression等)的构造基类

插件机制分析

rollup的插件其实一个普通的函数,函数返回一个对象,该对象包含一些基础属性(如name),和不同阶段的钩子函数,像这个样子:

function plugin(options = {}) {
return {
name: 'rollup-plugin',
transform() {
return {
code: 'code',
map: { mappings: '' }
};
}
};
}

这里是官方建议遵守的约定.

我们平常书写rollup插件的时候,最关注的就是钩子函数部分了,钩子函数的调用时机有三类:

  1. const chunks = rollup.rollup执行期间的Build Hooks
  2. chunks.generator(write)执行期间的Output Generation Hooks
  3. 监听文件变化并重新执行构建的rollup.watch执行期间的watchChange钩子函数

除了类别不同,rollup也提供了几种钩子函数的执行方式,每种方式都又分为同步或异步,方便内部使用:

  1. async: 处理promise的异步钩子,也有同步版本
  2. first: 如果多个插件实现了相同的钩子函数,那么会串式执行,从头到尾,但是,如果其中某个的返回值不是null也不是undefined的话,会直接终止掉后续插件。
  3. sequential: 如果多个插件实现了相同的钩子函数,那么会串式执行,按照使用插件的顺序从头到尾执行,如果是异步的,会等待之前处理完毕,在执行下一个插件。
  4. parallel: 同上,不过如果某个插件是异步的,其后的插件不会等待,而是并行执行。

文字表达比较苍白,咱们看几个实现:

  • 钩子函数: hookFirst

    使用场景:resolveId、resolveAssetUrl等
function hookFirst<H extends keyof PluginHooks, R = ReturnType<PluginHooks[H]>>(
hookName: H,
args: Args<PluginHooks[H]>,
replaceContext?: ReplaceContext | null,
skip?: number | null
): EnsurePromise<R> {
// 初始化promise
let promise: Promise<any> = Promise.resolve();
// this.plugins在初始化Graph的时候,进行了初始化
for (let i = 0; i < this.plugins.length; i++) {
if (skip === i) continue;
// 覆盖之前的promise,换言之就是串行执行钩子函数
promise = promise.then((result: any) => {
// 返回非null或undefined的时候,停止运行,返回结果
if (result != null) return result;
// 执行钩子函数
return this.runHook(hookName, args as any[], i, false, replaceContext);
});
}
// 最后一个promise执行的结果
return promise;
}
  • 钩子函数: hookFirstSync

    使用场景:resolveFileUrl、resolveImportMeta等
// hookFirst的同步版本,也就是并行执行
function hookFirstSync<H extends keyof PluginHooks, R = ReturnType<PluginHooks[H]>>(
hookName: H,
args: Args<PluginHooks[H]>,
replaceContext?: ReplaceContext
): R {
for (let i = 0; i < this.plugins.length; i++) {
// runHook的同步版本
const result = this.runHookSync(hookName, args, i, replaceContext);
// 返回非null或undefined的时候,停止运行,返回结果
if (result != null) return result as any;
}
// 否则返回null
return null as any;
}
  • 钩子函数: hookSeq

    使用场景:onwrite、generateBundle等
// 和hookFirst的区别就是不能中断
async function hookSeq<H extends keyof PluginHooks>(
hookName: H,
args: Args<PluginHooks[H]>,
replaceContext?: ReplaceContext
): Promise<void> {
let promise: Promise<void> = Promise.resolve();
for (let i = 0; i < this.plugins.length; i++)
promise = promise.then(() =>
this.runHook<void>(hookName, args as any[], i, false, replaceContext)
);
return promise;
}
  • 钩子函数: hookParallel

    使用场景:buildStart、buildEnd、renderStart等
// 同步进行,利用的Promise.all
function hookParallel<H extends keyof PluginHooks>(
hookName: H,
args: Args<PluginHooks[H]>,
replaceContext?: ReplaceContext
): Promise<void> {
// 创建promise.all容器
const promises: Promise<void>[] = [];
// 遍历每一个plugin
for (let i = 0; i < this.plugins.length; i++) {
// 执行hook返回promise
const hookPromise = this.runHook<void>(hookName, args as any[], i, false, replaceContext);
// 如果没有那么不push
if (!hookPromise) continue;
promises.push(hookPromise);
}
// 返回promise
return Promise.all(promises).then(() => {});
}
  • 钩子函数: hookReduceArg0

    使用场景: outputOptions、renderChunk等
// 对arg第一项进行reduce操作
function hookReduceArg0<H extends keyof PluginHooks, V, R = ReturnType<PluginHooks[H]>>(
hookName: H,
[arg0, ...args]: any[], // 取出传入的数组的第一个参数,将剩余的置于一个数组中
reduce: Reduce<V, R>,
replaceContext?: ReplaceContext // 替换当前plugin调用时候的上下文环境
) {
let promise = Promise.resolve(arg0); // 默认返回source.code
for (let i = 0; i < this.plugins.length; i++) {
// 第一个promise的时候只会接收到上面传递的arg0
// 之后每一次promise接受的都是上一个插件处理过后的source.code值
promise = promise.then(arg0 => {
const hookPromise = this.runHook(hookName, [arg0, ...args], i, false, replaceContext);
// 如果没有返回promise,那么直接返回arg0
if (!hookPromise) return arg0;
// result代表插件执行完成的返回值
return hookPromise.then((result: any) =>
reduce.call(this.pluginContexts[i], arg0, result, this.plugins[i])
);
});
}
return promise;
}

通过观察上面几种钩子函数的调用方式,我们可以发现,其内部有一个调用钩子函数的方法: runHook(Sync),该函数执行插件中提供的钩子函数。

实现很简单:

function runHook<T>(
hookName: string,
args: any[],
pluginIndex: number,
permitValues: boolean,
hookContext?: ReplaceContext | null
): Promise<T> {
this.previousHooks.add(hookName);
// 找到当前plugin
const plugin = this.plugins[pluginIndex];
// 找到当前执行的在plugin中定义的hooks钩子函数
const hook = (plugin as any)[hookName];
if (!hook) return undefined as any; // pluginContexts在初始化plugin驱动器类的时候定义,是个数组,数组保存对应着每个插件的上下文环境
let context = this.pluginContexts[pluginIndex];
// 用于区分对待不同钩子函数的插件上下文
if (hookContext) {
context = hookContext(context, plugin);
}
return Promise.resolve()
.then(() => {
// permit values allows values to be returned instead of a functional hook
if (typeof hook !== 'function') {
if (permitValues) return hook;
return error({
code: 'INVALID_PLUGIN_HOOK',
message: `Error running plugin hook ${hookName} for ${plugin.name}, expected a function hook.`
});
}
// 传入插件上下文和参数,返回插件执行结果
return hook.apply(context, args);
})
.catch(err => throwPluginError(err, plugin.name, { hook: hookName }));
}

当然,并不是每个人刚开始都会使用插件,所以rollup本身也提供了几个必需的钩子函数供我们使用,在Graph实例化的时候与用户自定义插件进行concat操作:

import { getRollupDefaultPlugin } from './defaultPlugin';

this.plugins = userPlugins.concat(
// 采用内置默认插件或者graph的插件驱动器的插件,不管怎么样,内置默认插件是肯定有的
// basePluginDriver是上一个PluginDriver初始化的插件
// preserveSymlinks: 软连标志
basePluginDriver ? basePluginDriver.plugins : [getRollupDefaultPlugin(preserveSymlinks)]
);

那rollup提供了哪些必需的钩子函数呢:

export function getRollupDefaultPlugin(preserveSymlinks: boolean): Plugin {
return {
// 插件名
name: 'Rollup Core',
// 默认的模块(文件)加载机制,内部主要使用path.resolve
resolveId: createResolveId(preserveSymlinks) as ResolveIdHook,
// this.pluginDriver.hookFirst('load', [id])为异步调用,readFile内部用promise包装了fs.readFile,并返回该promise
load(id) {
return readFile(id);
},
// 用来处理通过emitFile添加的urls或文件
resolveFileUrl({ relativePath, format }) {
// 不同format会返回不同的文件解析地址
return relativeUrlMechanisms[format](relativePath);
},
// 处理import.meta.url,参考地址:https://nodejs.org/api/esm.html#esm_import_meta)
resolveImportMeta(prop, { chunkId, format }) {
// 改变 获取import.meta的信息 的行为
const mechanism = importMetaMechanisms[format] && importMetaMechanisms[format](prop, chunkId);
if (mechanism) {
return mechanism;
}
}
};
}

过一眼发现都是最基本处理路径解析内容的钩子函数。

不仅如此,rollup给钩子函数注入了context,也就是上下文环境,用来方便对chunks和其他构建信息进行增删改查。

文档中也写得很清楚,比如:

  • 使用this.parse,调用rollup内部中的acron实例解析出ast
  • 使用this.emitFile来增加产出的文件,看这个例子.

我们通过transform操作来简单看下,之前对ast进行transform的时候,调用了transform钩子:


graph.pluginDriver
.hookReduceArg0<any, string>(
'transform',
[curSource, id], // source.code 和 模块id
transformReducer,
// 第四个参数是一个函数,用来声明某些钩子上下文中需要的方法
(pluginContext, plugin) => {
// 这一大堆是插件利用的,通过this.xxx调用
curPlugin = plugin;
if (curPlugin.cacheKey) customTransformCache = true;
else trackedPluginCache = getTrackedPluginCache(pluginContext.cache);
return {
...pluginContext,
cache: trackedPluginCache ? trackedPluginCache.cache : pluginContext.cache,
warn(warning: RollupWarning | string, pos?: number | { column: number; line: number }) {
if (typeof warning === 'string') warning = { message: warning } as RollupWarning;
if (pos) augmentCodeLocation(warning, pos, curSource, id);
warning.id = id;
warning.hook = 'transform';
pluginContext.warn(warning);
},
error(err: RollupError | string, pos?: number | { column: number; line: number }): never {
if (typeof err === 'string') err = { message: err };
if (pos) augmentCodeLocation(err, pos, curSource, id);
err.id = id;
err.hook = 'transform';
return pluginContext.error(err);
},
emitAsset(name: string, source?: string | Buffer) {
const emittedFile = { type: 'asset' as const, name, source };
emittedFiles.push({ ...emittedFile });
return graph.pluginDriver.emitFile(emittedFile);
},
emitChunk(id, options) {
const emittedFile = { type: 'chunk' as const, id, name: options && options.name };
emittedFiles.push({ ...emittedFile });
return graph.pluginDriver.emitFile(emittedFile);
},
emitFile(emittedFile: EmittedFile) {
emittedFiles.push(emittedFile);
return graph.pluginDriver.emitFile(emittedFile);
},
addWatchFile(id: string) {
transformDependencies.push(id);
pluginContext.addWatchFile(id);
},
setAssetSource(assetReferenceId, source) {
pluginContext.setAssetSource(assetReferenceId, source);
if (!customTransformCache && !setAssetSourceErr) {
try {
return this.error({
code: 'INVALID_SETASSETSOURCE',
message: `setAssetSource cannot be called in transform for caching reasons. Use emitFile with a source, or call setAssetSource in another hook.`
});
} catch (err) {
setAssetSourceErr = err;
}
}
},
getCombinedSourcemap() {
const combinedMap = collapseSourcemap(
graph,
id,
originalCode,
originalSourcemap,
sourcemapChain
);
if (!combinedMap) {
const magicString = new MagicString(originalCode);
return magicString.generateMap({ includeContent: true, hires: true, source: id });
}
if (originalSourcemap !== combinedMap) {
originalSourcemap = combinedMap;
sourcemapChain.length = 0;
}
return new SourceMap({
...combinedMap,
file: null as any,
sourcesContent: combinedMap.sourcesContent!
});
}
};
}
)

runHook中有一句判断,就是对上下文环境的使用:

function runHook<T>(
hookName: string,
args: any[],
pluginIndex: number,
permitValues: boolean,
hookContext?: ReplaceContext | null
) {
// ...
const plugin = this.plugins[pluginIndex];
// 获取默认的上下文环境
let context = this.pluginContexts[pluginIndex];
// 如果提供了,就替换
if (hookContext) {
context = hookContext(context, plugin);
}
// ...
}

至于rollup是什么时机调用插件提供的钩子函数的,这里就不啰嗦了,代码中分布很清晰,一看便知.

还有 rollup 为了方便咱们变化插件,还提供了一个工具集,可以非常方便的进行模块的操作以及判断,有兴趣的自行查看。

插件的缓存

插件还提供缓存的能力,实现的非常巧妙:

export function createPluginCache(cache: SerializablePluginCache): PluginCache {
// 利用闭包将cache缓存
return {
has(id: string) {
const item = cache[id];
if (!item) return false;
item[0] = 0; // 如果访问了,那么重置访问过期次数,猜测:就是说明用户有意向主动去使用
return true;
},
get(id: string) {
const item = cache[id];
if (!item) return undefined;
item[0] = 0; // 如果访问了,那么重置访问过期次数
return item[1];
},
set(id: string, value: any) {
// 存储单位是数组,第一项用来标记访问次数
cache[id] = [0, value];
},
delete(id: string) {
return delete cache[id];
}
};
}

然后创建缓存后,会添加在插件上下文中:

import createPluginCache from 'createPluginCache';

const cacheInstance = createPluginCache(pluginCache[cacheKey] || (pluginCache[cacheKey] = Object.create(null)));

const context = {
// ...
cache: cacheInstance,
// ...
}

之后我们就可以在插件中就可以使用cache进行插件环境下的缓存,进一步提升打包效率:

function testPlugin() {
return {
name: "test-plugin",
buildStart() {
if (!this.cache.has("prev")) {
this.cache.set("prev", "上一次插件执行的结果");
} else {
// 第二次执行rollup的时候会执行
console.log(this.cache.get("prev"));
}
},
};
}
let cache;
async function build() {
const chunks = await rollup.rollup({
input: "src/main.js",
plugins: [testPlugin()],
// 需要传递上次的打包结果
cache,
});
cache = chunks.cache;
} build().then(() => {
build();
});

不过需要注意的一点是options钩子函数是没有注入上下文环境的,它的调用方式也和其他钩子不一样:

function applyOptionHook(inputOptions: InputOptions, plugin: Plugin) {
if (plugin.options){
// 指定this和经过处理的input配置,并未传入context
return plugin.options.call({ meta: { rollupVersion } }, inputOptions) || inputOptions;
} return inputOptions;
}

总结

rollup系列到此也就告一段落了,从开始阅读时的一脸懵逼,到读到依赖收集、各工具类的十脸懵逼,到现在的轻车熟路,真是一段难忘的经历~

学习大佬们的操作并取其精华,去其糟粕就像打怪升级一样,你品,你细品。哈哈

在这期间也是误导一些东西,看得多了,就会发现,其实套路都一样,摸索出它们的核心框架,再对功能缝缝补补,不断更新迭代,或许我们也可以成为开源大作的作者。

如果用几句话来描述rollup的话:

读取并合并配置 -> 创建依赖图 -> 读取入口模块内容 -> 借用开源estree规范解析器进行源码分析,获取依赖,递归此操作 -> 生成模块,挂载模块对应文件相关信息 -> 分析ast,构建各node实例 -> 生成chunks -> 调用各node重写的render -> 利用magic-string进行字符串拼接和wrap操作 -> 写入

精简一下就是:

字符串 -> AST -> 字符串

如果改系列能对你一丝丝帮忙,还请动动手指,鼓励一下~

拜了个拜~

原来rollup这么简单之插件篇的更多相关文章

  1. 原来rollup这么简单之 rollup.watch篇

    大家好,我是小雨小雨,致力于分享有趣的.实用的技术文章. 内容分为翻译和原创,如果有问题,欢迎随时评论或私信,希望和大家一起进步. 大家的支持是我创作的动力. 计划 rollup系列打算一章一章的放出 ...

  2. ASP.NET MVC 4 插件化架构简单实现-思路篇

    用过和做过插件的都会了解插件的好处,园子里也有很多和讨论,但大都只些简单的加载程序集什么的,这里主要讨论的就是使用 ASP.NET MVC 4 来实现每个插件都可以完全从主站点剥离出来,即使只是一个插 ...

  3. ASP.NET MVC 4 插件化架构简单实现-实例篇

    先回顾一下上篇决定的做法: 1.定义程序集搜索目录(临时目录). 2.将要使用的各种程序集(插件)复制到该目录. 3.加载临时目录中的程序集. 4.定义模板引擎的搜索路径. 5.在模板引擎的查找页面方 ...

  4. MVC 4 插件化架构简单实现实例篇

    ASP.NET MVC 4 插件化架构简单实现-实例篇   先回顾一下上篇决定的做法: 1.定义程序集搜索目录(临时目录). 2.将要使用的各种程序集(插件)复制到该目录. 3.加载临时目录中的程序集 ...

  5. 定制Eclipse IDE之插件篇(二)

    上文回顾:定制Eclipse IDE之插件篇(一) 延续上一篇的插件篇,这一篇将会讲到一个最关键的插件aptana. 一.aptana插件 官方的解释我就不说了,从下面图可以看到插件提供了什么功能,列 ...

  6. WordPress插件制作教程(二): 编写一个简单的插件

    上一篇说到了如何创建一个插件,我想大家看了之后一定会有所收获,这一篇简单给大家写一个插件样例,让大家有一个基本的印象.这个插件的样例就是当你激活这个插件后会在你的每篇文章中插入一段自己定义好的内容,比 ...

  7. Unity的NGUI插件篇——入场效果

    Unity的NGUI插件篇--入场效果 入场效果 入场效果须要借助于NGUI提供的TweenPosition类来完毕.为了说明此类的用法.本节将使会解说两个演示样例.本文选自  大学霸 <NGU ...

  8. VS简单注释插件——VS插件开发续

    VS简单注释插件——VS插件开发续 前些时候,我写过一篇<VS版权信息插件——初试VS插件开发小记>分享过一个用于添加注释信息的插件,但那个插件有几个问题: 不能添加带块注释(/**/), ...

  9. Oh My Zsh 插件篇 - 实用工具

    Oh My Zsh 除了为我们提供快捷的命令行操作之外,还提供了强大丰富的插件机制,每个社区贡献者都可以贡献自己的插件,让整个生态体系更加丰富完善.今天给大家介绍了一下它的实用工具类插件. 前面我们分 ...

随机推荐

  1. C++ 读取配置文件结束指定进程

    #define _CRT_SECURE_NO_WARNINGS #include <string> #include <windows.h> #include <stdi ...

  2. 结题报告--hih0CoderP1041

    题目:点此 描述 小Hi和小Ho准备国庆期间去A国旅游.A国的城际交通比较有特色:它共有n座城市(编号1-n):城市之间恰好有n-1条公路相连,形成一个树形公路网.小Hi计划从A国首都(1号城市)出发 ...

  3. Win2012+Nginx+IIS+xxfpm(服务版)

    这次做了一个项目部署在环境为win2012+nginx1.13.5+mysql5.6+php7的环境下,服务器是阿里云的 由于之前没有这种经验,遇到了点坑(据参考文章里说的这坑还有些年份了),最开始自 ...

  4. 在 macOS 下备份/还原/重置 LaunchPad 布局

    原文链接:https://billc.io/2019/07/launchpad-layout-backup/ 前几天升级到 Catalina Public Beta 后,LauchPad 会出现无法刷 ...

  5. MyBatis框架——动态SQL

    MyBatis 作为⼀个“半⾃动化”的 ORM 框架,需要开发者⼿动定义 SQL 语句. 在业务需求⽐较复杂的情 况下,⼿动拼接 SQL 语句的⼯作量会⾮常⼤,为了适⽤于不同的业务需求,往往需要做很多 ...

  6. MySQL:REPLACE函数的使用

    原文链接 REPLACE函数功能 REPLACE(columnName, search_str, replace_str) 查找columnName字段中所有search_str,并替换为replac ...

  7. beautifulsoup实现文章截取和脚本攻击

    引子:现在我们所写的所有高大上,炫酷的网页,在数据库中是如何存储的呢?其实数据库中存储的所有高大上的文章都是存的html,然后我们平时看的写的都是由于编辑器的原因,在编辑器内部做了转换,所以我们可以直 ...

  8. [Docker6] Docker compose多容器运行与管理

    六.Docker compose docker compose就是通过yml文件来定义和运行多个容器docker应用程序的工具,三步过程就能跑起一个compose: 定义应用程序的环境(yml中) 定 ...

  9. python中的抽象类

    今天给大家介绍一下python中一个强大的功能:抽象类 因为最近发现很多同事不理解抽象类,正好抽空写篇博客,给大家介绍一下抽象类 抽象类的作用就是控制子类的方法的名称,要求子类必须按照父类的要求的实现 ...

  10. OpenCV-Python 光流 | 四十八

    目标 在本章中, 我们将了解光流的概念及其使用Lucas-Kanade方法的估计. 我们将使用cv.calcOpticalFlowPyrLK()之类的函数来跟踪视频中的特征点. 我们将使用cv.cal ...