你們好,我是小雨小雨,致力於分享有趣的、實用的技術文章。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 -> 字符串
若是改系列能對你一絲絲幫忙,還請動動手指,鼓勵一下~
拜了個拜~