你们好,我是小雨小雨,致力于分享有趣的、实用的技术文章。javascript
内容分为翻译和原创,若是有问题,欢迎随时评论或私信,但愿和你们一块儿进步。html
你们的支持是我创做的动力。java
rollup系列打算一章一章的放出,内容更精简更专注更易于理解node
这是rollup系列的最后一篇文章,如下是全部文章连接。webpack
rollup的插件和其余大型框架大同小异,都是提供统一的标准接口,经过约定大于配置定义公共配置,注入当前构建结果相关的属性与方法,供开发者进行增删改查操做。为稳定可持续增加提供了强而有力的铺垫!git
但不想webpack区分loader和plugin,rollup的plugin既能够担任loader的角色,也能够胜任传统plugin的角色。rollup提供的钩子函数是核心,好比load、transform对chunk进行解析更改,resolveFileUrl能够对加载模块进行合法解析,options对配置进行动态更新等等~github
全部的注释都在这里,可自行阅读web
!!!提示 => 标有TODO为具体实现细节,会视状况分析。api
!!!注意 => 每个子标题都是父标题(函数)内部实现数组
!!!强调 => rollup中模块(文件)的id就是文件地址,因此相似resolveID这种就是解析文件地址的意思,咱们能够返回咱们想返回的文件id(也就是地址,相对路径、决定路径)来让rollup加载
rollup是一个核心,只作最基础的事情,好比提供默认模块(文件)加载机制, 好比打包成不一样风格的内容,咱们的插件中提供了加载文件路径,解析文件内容(处理ts,sass等)等操做,是一种插拔式的设计,和webpack相似
插拔式是一种很是灵活且可长期迭代更新的设计,这也是一个中大型框架的核心,人多力量大嘛~
rollup的插件其实一个普通的函数,函数返回一个对象,该对象包含一些基础属性(如name),和不一样阶段的钩子函数,像这个样子:
function plugin(options = {}) { return { name: 'rollup-plugin', transform() { return { code: 'code', map: { mappings: '' } }; } }; }
这里是官方建议遵照的约定.
咱们日常书写rollup插件的时候,最关注的就是钩子函数部分了,钩子函数的调用时机有三类:
除了类别不一样,rollup也提供了几种钩子函数的执行方式,每种方式都又分为同步或异步,方便内部使用:
文字表达比较苍白,我们看几个实现:
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; }
// 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; }
// 和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; }
// 同步进行,利用的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(() => {}); }
// 对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和其余构建信息进行增删改查。
文档中也写得很清楚,好比:
咱们经过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 为了方便我们变化插件,还提供了一个工具集,能够很是方便的进行模块的操做以及判断,有兴趣的自行查看。
rollup系列到此也就告一段落了,从开始阅读时的一脸懵逼,到读到依赖收集、各工具类的十脸懵逼,到如今的轻车熟路,真是一段难忘的经历~
学习大佬们的操做并取其精华,去其糟粕就像打怪升级同样,你品,你细品。哈哈
在这期间也是误导一些东西,看得多了,就会发现,其实套路都同样,摸索出它们的核心框架
,再对功能缝缝补补,不断更新迭代,或许咱们也能够成为开源大做的做者。
若是用几句话来描述rollup的话:
读取并合并配置 -> 建立依赖图 -> 读取入口模块内容 -> 借用开源estree规范解析器进行源码分析,获取依赖,递归此操做 -> 生成模块,挂载模块对应文件相关信息 -> 分析ast,构建各node实例 -> 生成chunks -> 调用各node重写的render -> 利用magic-string进行字符串拼接和wrap操做 -> 写入
精简一下就是:
字符串 -> AST -> 字符串
若是改系列能对你一丝丝帮忙,还请动动手指,鼓励一下~
拜了个拜~