自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

從零到一解讀Rollup Plugin

開發(fā) 前端
rollup 的插件本質(zhì)是一個處理函數(shù),返回一個對象。返回的對象包含一些屬性(如 name),和不同階段的鉤子函數(shù)(構建 build 和輸出 output 階段),以實現(xiàn)插件內(nèi)部的功能.

[[431668]]

rollup plugin 這篇文章讀讀改改,終于和大家見面啦~~~

盡管對于 rollup 的插件編寫,官網(wǎng)上對于 rolup 插件的介紹幾乎都是英文,學習起來不是很友好, 例子也相對較少,但目前針對 rollup 插件的分析與開發(fā)指南的文章已經(jīng)不少見,以關于官方英文文檔的翻譯與函數(shù)鉤子的分析為主。

講道理,稀里糊涂直接看源碼分析只會分分鐘勸退我,而我只想分分鐘寫個 rollup 插件而已~~

rollup 為什么需要 Plugin

rollup -c 打包流程

在 rollup 的打包流程中,通過相對路徑,將一個入口文件和一個模塊創(chuàng)建成了一個簡單的 bundle。隨著構建更復雜的 bundle,通常需要更大的靈活性——引入 npm 安裝的模塊、通過 Babel 編譯代碼、和 JSON 文件打交道等。通過 rollup -c 打包的實現(xiàn)流程可以參考下面的流程圖理解。

為此,我們可以通過 插件(plugins) 在打包的關鍵過程中更改 Rollup 的行為。

這其實和 webpack 的插件相類似,不同的是,webpack 區(qū)分 loader 和 plugin,而 rollup 的 plugin 既可以擔任 loader 的角色,也可以勝任傳統(tǒng) plugin 的角色。

理解 rollup plugin

引用官網(wǎng)的解釋:Rollup 插件是一個具有下面描述的一個或多個屬性、構建鉤子和輸出生成鉤子的對象,它遵循我們的約定。一個插件應該作為一個包來分發(fā),該包導出一個可以用插件特定選項調(diào)用的函數(shù),并返回這樣一個對象。插件允許你定制 Rollup 的行為,例如,在捆綁之前編譯代碼,或者在你的 node_modules 文件夾中找到第三方模塊。

簡單來說,rollup 的插件是一個普通的函數(shù),函數(shù)返回一個對象,該對象包含一些屬性(如 name),和不同階段的鉤子函數(shù)(構建 build 和輸出 output 階段),此處應該回顧下上面的流程圖。

關于約定

  • 插件應該有一個帶有 rollup-plugin-前綴的明確名稱。
  • 在 package.json 中包含 rollup-plugin 關鍵字。
  • 插件應該支持測試,推薦 mocha 或者 ava 這類開箱支持 promises 的庫。
  • 盡可能使用異步方法。
  • 用英語記錄你的插件。
  • 確保你的插件輸出正確的 sourcemap。
  • 如果你的插件使用 'virtual modules'(比如幫助函數(shù)),給模塊名加上 \0 前綴。這可以阻止其他插件執(zhí)行它。

分分鐘寫個 rollup 插件

為了保持學習下去的熱情與動力,先舉個栗子壓壓驚,如果看到插件實現(xiàn)的各種源碼函數(shù)鉤子部分覺得腦子不清醒了,歡迎隨時回來重新看這一小節(jié),重拾勇氣與信心!

插件其實很簡單

可以打開rollup 插件列表,隨便找個你感興趣的插件,看下源代碼。

有不少插件都是幾十行,不超過 100 行的。比如圖片文件多格式支持插件@rollup/plugin-image 的代碼甚至不超過 50 行,而將 json 文件轉(zhuǎn)換為 ES6 模塊的插件@rollup/plugin-json 源代碼更少。

一個例子

  1. // 官網(wǎng)的一個例子 
  2. export default function myExample () { 
  3.   return { 
  4.     name'my-example', // 名字用來展示在警告和報錯中 
  5.     resolveId ( source ) { 
  6.       if (source === 'virtual-module') { 
  7.         return source; // rollup 不應該查詢其他插件或文件系統(tǒng) 
  8.       } 
  9.       return null; // other ids 正常處理 
  10.     }, 
  11.     load ( id ) { 
  12.       if (id === 'virtual-module') { 
  13.         return 'export default "This is virtual!"'; // source code for "virtual-module" 
  14.       } 
  15.       return null; // other ids 
  16.     } 
  17.   }; 
  18.  
  19. // rollup.config.js 
  20. import myExample from './rollup-plugin-my-example.js'
  21. export default ({ 
  22.   input: 'virtual-module', // 配置 virtual-module 作為入口文件滿足條件通過上述插件處理 
  23.   plugins: [myExample()], 
  24.   output: [{ 
  25.     file: 'bundle.js'
  26.     format: 'es' 
  27.   }] 
  28. }); 

光看不練假把式,模仿寫一個:

  1. // 自己編的一個例子 QAQ 
  2. export default function bundleReplace () { 
  3.   return { 
  4.     name'bundle-replace', // 名字用來展示在警告和報錯中 
  5.     transformBundle(bundle) { 
  6.       return bundle 
  7.         .replace('key_word''replace_word'
  8.         .replace(/正則/, '替換內(nèi)容'); 
  9.     }, 
  10.   }; 
  11.  
  12. // rollup.config.js 
  13. import bundleReplace from './rollup-plugin-bundle-replace.js'
  14. export default ({ 
  15.   input: 'src/main.js', // 通用入口文件 
  16.   plugins: [bundleReplace()], 
  17.   output: [{ 
  18.     file: 'bundle.js'
  19.     format: 'es' 
  20.   }] 
  21. }); 

嘿!這也不難嘛~~~

rollup plugin 功能的實現(xiàn)

我們要講的 rollup plugin 也不可能就這么簡單啦~~~

接下來當然是結(jié)合例子分析實現(xiàn)原理~~

其實不難發(fā)現(xiàn),rollup 的插件配置與 webpack 等框架中的插件使用大同小異,都是提供配置選項,注入當前構建結(jié)果相關的屬性與方法,供開發(fā)者進行增刪改查操作。

那么插件寫好了,rollup 是如何在打包過程中調(diào)用它并實現(xiàn)它的功能的呢?

相關概念

首先還是要了解必備的前置知識,大致瀏覽下 rollup 中處理 plugin 的方法,基本可以定位到 PluginContext.ts(上下文相關)、PluginDriver.ts(驅(qū)動相關)、PluginCache.ts(緩存相關)和 PluginUtils.ts(警告錯誤異常處理)等文件,其中最關鍵的就在 PluginDriver.ts 中了。

首先要清楚插件驅(qū)動的概念,它是實現(xiàn)插件提供功能的的核心 -- PluginDriver,插件驅(qū)動器,調(diào)用插件和提供插件環(huán)境上下文等。

鉤子函數(shù)的調(diào)用時機

大家在研究 rollup 插件的時候,最關注的莫過于鉤子函數(shù)部分了,鉤子函數(shù)的調(diào)用時機有三類:

  • const chunks = rollup.rollup 執(zhí)行期間的構建鉤子函數(shù) - Build Hooks
  • chunks.generator(write)執(zhí)行期間的輸出鉤子函數(shù) - Output Generation Hooks
  • 監(jiān)聽文件變化并重新執(zhí)行構建的 rollup.watch 執(zhí)行期間的 watchChange 鉤子函數(shù)

鉤子函數(shù)處理方式分類

除了以調(diào)用時機來劃分鉤子函數(shù)以外,我們還可以以鉤子函數(shù)處理方式來劃分,這樣來看鉤子函數(shù)就主要有以下四種版本:

  • async: 處理 promise 的異步鉤子,即這類 hook 可以返回一個解析為相同類型值的 promise,同步版本 hook 將被標記為 sync。
  • first: 如果多個插件實現(xiàn)了相同的鉤子函數(shù),那么會串式執(zhí)行,從頭到尾,但是,如果其中某個的返回值不是 null 也不是 undefined 的話,會直接終止掉后續(xù)插件。
  • sequential: 如果多個插件實現(xiàn)了相同的鉤子函數(shù),那么會串式執(zhí)行,按照使用插件的順序從頭到尾執(zhí)行,如果是異步的,會等待之前處理完畢,在執(zhí)行下一個插件。
  • parallel: 同上,不過如果某個插件是異步的,其后的插件不會等待,而是并行執(zhí)行,這個也就是我們在 rollup.rollup() 階段看到的處理方式。

構建鉤子函數(shù)

為了與構建過程交互,你的插件對象需要包含一些構建鉤子函數(shù)。構建鉤子是構建的各個階段調(diào)用的函數(shù)。構建鉤子函數(shù)可以影響構建執(zhí)行方式、提供構建的信息或者在構建完成后修改構建。rollup 中有不同的構建鉤子函數(shù),在構建階段執(zhí)行時,它們被 [rollup.rollup(inputOptions)](https://github.com/rollup/rollup/blob/07b3a02069594147665daa95d3fa3e041a82b2d0/cli/run/build.ts#L34) 觸發(fā)。

構建鉤子函數(shù)主要關注在 Rollup 處理輸入文件之前定位、提供和轉(zhuǎn)換輸入文件。構建階段的第一個鉤子是 options,最后一個鉤子總是 buildEnd,除非有一個構建錯誤,在這種情況下 closeBundle 將在這之后被調(diào)用。

順便提一下,在觀察模式下,watchChange 鉤子可以在任何時候被觸發(fā),以通知新的運行將在當前運行產(chǎn)生其輸出后被觸發(fā)。當 watcher 關閉時,closeWatcher 鉤子函數(shù)將被觸發(fā)。

輸出鉤子函數(shù)

輸出生成鉤子函數(shù)可以提供關于生成的包的信息并在構建完成后立馬執(zhí)行。它們和構建鉤子函數(shù)擁有一樣的工作原理和相同的類型,但是不同的是它們分別被 ·[bundle.generate(output)](https://github.com/rollup/rollup/blob/07b3a02069594147665daa95d3fa3e041a82b2d0/cli/run/build.ts#L44) 或 [bundle.write(outputOptions)](https://github.com/rollup/rollup/blob/07b3a02069594147665daa95d3fa3e041a82b2d0/cli/run/build.ts#L64) 調(diào)用。只使用輸出生成鉤子的插件也可以通過輸出選項傳入,因為只對某些輸出運行。

輸出生成階段的第一個鉤子函數(shù)是 outputOptions,如果輸出通過 bundle.generate(...) 成功生成則第一個鉤子函數(shù)是 generateBundle,如果輸出通過 [bundle.write(...)](https://github.com/rollup/rollup/blob/07b3a02069594147665daa95d3fa3e041a82b2d0/src/watch/watch.ts#L200) 生成則最后一個鉤子函數(shù)是 [writeBundle](https://github.com/rollup/rollup/blob/master/src/rollup/rollup.ts#L176),另外如果輸出生成階段發(fā)生了錯誤的話,最后一個鉤子函數(shù)則是 renderError。

另外,closeBundle 可以作為最后一個鉤子被調(diào)用,但用戶有責任手動調(diào)用 bundle.close() 來觸發(fā)它。CLI 將始終確保這種情況發(fā)生。

以上就是必須要知道的概念了,讀到這里好像還是看不明白這些鉤子函數(shù)到底是干啥的!那么接下來進入正題!

鉤子函數(shù)加載實現(xiàn)

[PluginDriver](https://github.com/rollup/rollup/blob/07b3a02069594147665daa95d3fa3e041a82b2d0/src/utils/PluginDriver.ts#L124) 中有 9 個 hook 加載函數(shù)。主要是因為每種類別的 hook 都有同步和異步的版本。

接下來先康康 9 個 hook 加載函數(shù)及其應用場景(看完第一遍不知所以然,但是別人看了咱也得看,先看了再說,看不懂就多看幾遍 QAQ~)

排名不分先后,僅參考它們在 PluginDriver.ts 中出現(xiàn)的順序🌠。

1. hookFirst

加載 first 類型的鉤子函數(shù),場景有 resolveId、resolveAssetUrl 等,在實例化 Graph 的時候,初始化初始化 promise 和 this.plugins,并通過覆蓋之前的 promise,實現(xiàn)串行執(zhí)行鉤子函數(shù)。當多個插件實現(xiàn)了相同的鉤子函數(shù)時從頭到尾串式執(zhí)行,如果其中某個的返回值不是 null 也不是 undefined 的話,就會直接終止掉后續(xù)插件。

  1. function hookFirst<H extends keyof PluginHooks, R = ReturnType<PluginHooks[H]>>( 
  2.   hookName: H, 
  3.   args: Args<PluginHooks[H]>, 
  4.   replaceContext?: ReplaceContext | null
  5.   skip?: number | null 
  6. ): EnsurePromise<R> { 
  7.   // 初始化 promise 
  8.   let promise: Promise<any> = Promise.resolve(); 
  9.   // 實例化 Graph 的時候,初始化 this.plugins 
  10.   for (let i = 0; i < this.plugins.length; i++) { 
  11.     if (skip === i) continue
  12.     // 覆蓋之前的 promise,即串行執(zhí)行鉤子函數(shù) 
  13.     promise = promise.then((result: any) => { 
  14.       // 返回非 null 或 undefined 的時候,停止運行,返回結(jié)果 
  15.       if (result != nullreturn result; 
  16.       // 執(zhí)行鉤子函數(shù) 
  17.       return this.runHook(hookName, args as any[], i, false, replaceContext); 
  18.     }); 
  19.   } 
  20.   // 返回 hook 過的 promise 
  21.   return promise; 

2. hookFirstSync

hookFirst 的同步版本,使用場景有 resolveFileUrl、resolveImportMeta 等。

  1. function hookFirstSync<H extends keyof PluginHooks, R = ReturnType<PluginHooks[H]>>( 
  2.   hookName: H, 
  3.   args: Args<PluginHooks[H]>, 
  4.   replaceContext?: ReplaceContext 
  5. ): R { 
  6.   for (let i = 0; i < this.plugins.length; i++) { 
  7.     // runHook 的同步版本 
  8.     const result = this.runHookSync(hookName, args, i, replaceContext); 
  9.     // 返回非 null 或 undefined 的時候,停止運行,返回結(jié)果 
  10.     if (result != nullreturn result as any
  11.   } 
  12.   // 否則返回 null 
  13.   return null as any

3. hookParallel

并行執(zhí)行 hook,不會等待當前 hook 完成。也就是說如果某個插件是異步的,其后的插件不會等待,而是并行執(zhí)行。使用場景 buildEnd、buildStart、moduleParsed 等。

  1. hookParallel<H extends AsyncPluginHooks & ParallelPluginHooks>( 
  2.   hookName: H, 
  3.   args: Parameters<PluginHooks[H]>, 
  4.   replaceContext?: ReplaceContext 
  5. ): Promise<void> { 
  6.   const promises: Promise<void>[] = []; 
  7.   for (const plugin of this.plugins) { 
  8.     const hookPromise = this.runHook(hookName, args, plugin, false, replaceContext); 
  9.     if (!hookPromise) continue
  10.     promises.push(hookPromise); 
  11.   } 
  12.   return Promise.all(promises).then(() => {}); 

4.hookReduceArg0

對 arg 第一項進行 reduce 操作。使用場景: options、renderChunk 等。

  1. function hookReduceArg0<H extends keyof PluginHooks, V, R = ReturnType<PluginHooks[H]>>( 
  2.     hookName: H, 
  3.     [arg0, ...args]: any[], // 取出傳入的數(shù)組的第一個參數(shù),將剩余的置于一個數(shù)組中 
  4.     reduce: Reduce<V, R>, 
  5.     replaceContext?: ReplaceContext // 替換當前 plugin 調(diào)用時候的上下文環(huán)境 
  6. ) { 
  7.   let promise = Promise.resolve(arg0); // 默認返回 source.code 
  8.   for (let i = 0; i < this.plugins.length; i++) { 
  9.     // 第一個 promise 的時候只會接收到上面?zhèn)鬟f的 arg0 
  10.     // 之后每一次 promise 接受的都是上一個插件處理過后的 source.code 值 
  11.     promise = promise.then(arg0 => { 
  12.       const hookPromise = this.runHook(hookName, [arg0, ...args], i, false, replaceContext); 
  13.       // 如果沒有返回 promise,那么直接返回 arg0 
  14.       if (!hookPromise) return arg0; 
  15.       // result 代表插件執(zhí)行完成的返回值 
  16.       return hookPromise.then((result: any) => 
  17.         reduce.call(this.pluginContexts[i], arg0, result, this.plugins[i]) 
  18.       ); 
  19.     }); 
  20.   } 
  21.   return promise; 

5.hookReduceArg0Sync

hookReduceArg0 同步版本,使用場景 transform、generateBundle 等,不做贅述。

6. hookReduceValue

將返回值減少到類型 T,分別處理減少的值。允許鉤子作為值。

  1. hookReduceValue<H extends PluginValueHooks, T>( 
  2.   hookName: H, 
  3.   initialValue: T | Promise<T>, 
  4.   args: Parameters<AddonHookFunction>, 
  5.   reduce: ( 
  6.    reduction: T, 
  7.    result: ResolveValue<ReturnType<AddonHookFunction>>, 
  8.    plugin: Plugin 
  9.   ) => T, 
  10.   replaceContext?: ReplaceContext 
  11.  ): Promise<T> { 
  12.   let promise = Promise.resolve(initialValue); 
  13.   for (const plugin of this.plugins) { 
  14.    promise = promise.then(value => { 
  15.     const hookPromise = this.runHook(hookName, args, plugin, true, replaceContext); 
  16.     if (!hookPromise) return value; 
  17.     return hookPromise.then(result => 
  18.      reduce.call(this.pluginContexts.get(plugin), value, result, plugin) 
  19.     ); 
  20.    }); 
  21.   } 
  22.   return promise; 
  23.  } 

7. hookReduceValueSync

hookReduceValue 的同步版本。

8. hookSeq

加載 sequential 類型的鉤子函數(shù),和 hookFirst 的區(qū)別就是不能中斷,使用場景有 onwrite、generateBundle 等。

  1. async function hookSeq<H extends keyof PluginHooks>( 
  2.   hookName: H, 
  3.   args: Args<PluginHooks[H]>, 
  4.   replaceContext?: ReplaceContext, 
  5.   // hookFirst 通過 skip 參數(shù)決定是否跳過某個鉤子函數(shù) 
  6. ): Promise<void> { 
  7.   let promise: Promise<void> = Promise.resolve(); 
  8.   for (let i = 0; i < this.plugins.length; i++) 
  9.     promise = promise.then(() => 
  10.       this.runHook<void>(hookName, args as any[], i, false, replaceContext), 
  11.     ); 
  12.   return promise; 

9.hookSeqSync

hookSeq 同步版本,不需要構造 promise,而是直接使用 runHookSync 執(zhí)行鉤子函數(shù)。使用場景有 closeWatcher、watchChange 等。

  1. hookSeqSync<H extends SyncPluginHooks & SequentialPluginHooks>( 
  2.   hookName: H, 
  3.   args: Parameters<PluginHooks[H]>, 
  4.   replaceContext?: ReplaceContext 
  5. ): void { 
  6.   for (const plugin of this.plugins) { 
  7.     this.runHookSync(hookName, args, plugin, replaceContext); 
  8.   } 

通過觀察上面幾種鉤子函數(shù)的調(diào)用方式,我們可以發(fā)現(xiàn),其內(nèi)部有一個調(diào)用鉤子函數(shù)的方法: runHook(Sync)(當然也分同步和異步版本),該函數(shù)真正執(zhí)行插件中提供的鉤子函數(shù)。

也就是說,之前介紹了那么多的鉤子函數(shù),僅僅決定了我們插件的調(diào)用時機和調(diào)用方式(比如同步/異步),而真正調(diào)用并執(zhí)行插件函數(shù)(前面提到插件本身是個「函數(shù)」)的鉤子其實是 runHook 。

runHook(Sync)

真正執(zhí)行插件的鉤子函數(shù),同步版本和異步版本的區(qū)別是有無 permitValues 許可標識允許返回值而不是只允許返回函數(shù)。

  1. function runHook<T>( 
  2.   hookName: string, 
  3.   args: any[], 
  4.   pluginIndex: number, 
  5.   permitValues: boolean, 
  6.   hookContext?: ReplaceContext | null
  7. ): Promise<T> { 
  8.   this.previousHooks.add(hookName); 
  9.   // 找到當前 plugin 
  10.   const plugin = this.plugins[pluginIndex]; 
  11.   // 找到當前執(zhí)行的在 plugin 中定義的 hooks 鉤子函數(shù) 
  12.   const hook = (plugin as any)[hookName]; 
  13.   if (!hook) return undefined as any
  14.  
  15.   // pluginContexts 在初始化 plugin 驅(qū)動器類的時候定義,是個數(shù)組,數(shù)組保存對應著每個插件的上下文環(huán)境 
  16.   let context = this.pluginContexts[pluginIndex]; 
  17.   // 用于區(qū)分對待不同鉤子函數(shù)的插件上下文 
  18.   if (hookContext) { 
  19.     context = hookContext(context, plugin); 
  20.   } 
  21.   return Promise.resolve() 
  22.     .then(() => { 
  23.       // 允許返回值,而不是一個函數(shù)鉤子,使用 hookReduceValue 或 hookReduceValueSync 加載。 
  24.       // 在 sync 同步版本鉤子函數(shù)中,則沒有 permitValues 許可標識允許返回值 
  25.       if (typeof hook !== 'function') { 
  26.         if (permitValues) return hook; 
  27.         return error({ 
  28.           code: 'INVALID_PLUGIN_HOOK'
  29.           message: `Error running plugin hook ${hookName} for ${plugin.name}, expected a function hook.`, 
  30.         }); 
  31.       } 
  32.       // 傳入插件上下文和參數(shù),返回插件執(zhí)行結(jié)果 
  33.       return hook.apply(context, args); 
  34.     }) 
  35.     .catch(err => throwPluginError(err, plugin.name, { hook: hookName })); 

看完這些鉤子函數(shù)介紹,我們清楚了插件的調(diào)用時機、調(diào)用方式以及執(zhí)行輸出鉤子函數(shù)。但你以為這就結(jié)束了??當然沒有結(jié)束我們還要把這些鉤子再帶回 rollup 打包流程康康一下調(diào)用時機和調(diào)用方式的實例~~

rollup.rollup()

又回到最初的起點~~~

前面提到過,構建鉤子函數(shù)在 Rollup 處理輸入文件之前定位、提供和轉(zhuǎn)換輸入文件。那么當然要先從輸入開始看起咯~

build 階段

處理 inputOptions

  1. // 從處理 inputOptions 開始,你的插件鉤子函數(shù)已到達! 
  2. const { options: inputOptions, unsetOptions: unsetInputOptions } = await getInputOptions( 
  3.   rawInputOptions, 
  4.   watcher !== null 
  5. ); 

朋友們,把 async、first、sequential 和 parallel 以及 9 個鉤子函數(shù)帶上開搞!

  1. // 處理 inputOptions 的應用場景下調(diào)用了 options 鉤子 
  2. function applyOptionHook(watchMode: boolean) { 
  3.  return async ( // 異步串行執(zhí)行 
  4.   inputOptions: Promise<GenericConfigObject>, 
  5.   plugin: Plugin 
  6.  ): Promise<GenericConfigObject> => { 
  7.   if (plugin.options) { // plugin 配置存在 
  8.    return ( 
  9.     ((await plugin.options.call( 
  10.      { meta: { rollupVersion, watchMode } }, // 上下文 
  11.      await inputOptions 
  12.     )) as GenericConfigObject) || inputOptions 
  13.    ); 
  14.   } 
  15.  
  16.   return inputOptions; 
  17.  }; 

接著標準化插件

  1. // 標準化插件 
  2. function normalizePlugins(plugins: Plugin[], anonymousPrefix: string): void { 
  3.  for (let pluginIndex = 0; pluginIndex < plugins.length; pluginIndex++) { 
  4.   const plugin = plugins[pluginIndex]; 
  5.   if (!plugin.name) { 
  6.    plugin.name = `${anonymousPrefix}${pluginIndex + 1}`; 
  7.   } 
  8.  } 

生成 graph 對象處理

重點來了!const graph = new Graph(inputOptions, watcher);里面就調(diào)用了我們上面介紹的一些關鍵鉤子函數(shù)了~

  1. // 不止處理緩存 
  2. this.pluginCache = options.cache?.plugins || Object.create(null); 
  3.  
  4. // 還有 WatchChangeHook 鉤子 
  5. if (watcher) { 
  6.   this.watchMode = true
  7.   const handleChange: WatchChangeHook = (...args) => this.pluginDriver.hookSeqSync('watchChange', args); // hookSeq 同步版本,watchChange 使用場景下 
  8.   const handleClose = () => this.pluginDriver.hookSeqSync('closeWatcher', []); // hookSeq 同步版本, closeWatcher 使用場景下 
  9.   watcher.on('change', handleChange); 
  10.   watcher.on('close', handleClose); 
  11.   watcher.once('restart', () => { 
  12.     watcher.removeListener('change', handleChange); 
  13.     watcher.removeListener('close', handleClose); 
  14.   }); 
  15.  
  16. this.pluginDriver = new PluginDriver(this, options, options.plugins, this.pluginCache); // 生成一個插件驅(qū)動對象 
  17. ... 
  18. this.moduleLoader = new ModuleLoader(this, this.modulesById, this.options, this.pluginDriver); // 初始化模塊加載對象 

到目前為止,處理inputOptions生成了graph對象,還記不記得!我們前面講過_graph 包含入口以及各種依賴的相互關系,操作方法,緩存等,在實例內(nèi)部實現(xiàn) AST 轉(zhuǎn)換,是 rollup 的核心。

我們還講過!在解析入口文件路徑階段,為了從入口文件的絕對路徑出發(fā)找到它的模塊定義,并獲取這個入口模塊所有的依賴語句,我們要先通過 resolveId()方法解析文件地址,拿到文件絕對路徑。這個過程就是通過在 ModuleLoader 中調(diào)用 resolveId 完成的。resolveId() 我們在 tree-shaking 時講到基本構建流程時已經(jīng)介紹過的,下面看調(diào)用了鉤子函數(shù)的具體方法~

  1. export function resolveIdViaPlugins( 
  2.  source: string, 
  3.  importer: string | undefined, 
  4.  pluginDriver: PluginDriver, 
  5.  moduleLoaderResolveId: ( 
  6.   source: string, 
  7.   importer: string | undefined, 
  8.   customOptions: CustomPluginOptions | undefined, 
  9.   skip: { importer: string | undefined; plugin: Plugin; source: string }[] | null 
  10.  ) => Promise<ResolvedId | null>, 
  11.  skip: { importer: string | undefined; plugin: Plugin; source: string }[] | null
  12.  customOptions: CustomPluginOptions | undefined 
  13. ) { 
  14.  let skipped: Set<Plugin> | null = null
  15.  let replaceContext: ReplaceContext | null = null
  16.  if (skip) { 
  17.   skipped = new Set(); 
  18.   for (const skippedCall of skip) { 
  19.    if (source === skippedCall.source && importer === skippedCall.importer) { 
  20.     skipped.add(skippedCall.plugin); 
  21.    } 
  22.   } 
  23.   replaceContext = (pluginContext, plugin): PluginContext => ({ 
  24.    ...pluginContext, 
  25.    resolve: (source, importer, { custom, skipSelf } = BLANK) => { 
  26.     return moduleLoaderResolveId( 
  27.      source, 
  28.      importer, 
  29.      custom, 
  30.      skipSelf ? [...skip, { importer, plugin, source }] : skip 
  31.     ); 
  32.    } 
  33.   }); 
  34.  } 
  35.  return pluginDriver.hookFirst( // hookFirst 被調(diào)用,通過插件處理獲取就絕對路徑,first 類型,如果有插件返回了值,那么后續(xù)所有插件的 resolveId 都不會被執(zhí)行。 
  36.   'resolveId'
  37.   [source, importer, { custom: customOptions }], 
  38.   replaceContext, 
  39.   skipped 
  40.  ); 

拿到resolveId hook處理過返回的絕對路徑后,就要從入口文件的絕對路徑出發(fā)找到它的模塊定義,并獲取這個入口模塊所有的依賴語句并返回所有內(nèi)容。在這里,我們收集配置并標準化、分析文件并編譯源碼生成 AST、生成模塊并解析依賴,最后生成 chunks,總而言之就是讀取并修改文件!要注意的是,每個文件只會被一個插件的load Hook處理,因為它是以hookFirst來執(zhí)行的。另外,如果你沒有返回值,rollup 會自動讀取文件。接下來進入 fetchModule 階段~

  1. const module: Module = new Module(...) 
  2. ... 
  3. await this.pluginDriver.hookParallel('moduleParsed', [module.info]); // 并行執(zhí)行 hook,moduleParsed 場景 
  4. ... 
  5. await this.addModuleSource(id, importer, module); 
  6. ...// addModuleSource 
  7. source = (await this.pluginDriver.hookFirst('load', [id])) ?? (await readFile(id)); // 在 load 階段對代碼進行轉(zhuǎn)換、生成等操作 
  8. ...// resolveDynamicImport 
  9. const resolution = await this.pluginDriver.hookFirst('resolveDynamicImport', [ 
  10.   specifier, 
  11.   importer 
  12. ]); 

bundle 處理代碼

生成的 graph 對象準備進入 build 階段~~build 開始與結(jié)束中的插件函數(shù)鉤子

  1. await graph.pluginDriver.hookParallel('buildStart', [inputOptions]); // 并行執(zhí)行 hook,buildStart 場景 
  2. ... 
  3. await graph.build(); 
  4. ... 
  5. await graph.pluginDriver.hookParallel('buildEnd', []); // 并行執(zhí)行 hook,buildEnd 場景 

如果在 buildStart 和 build 階段出現(xiàn)異常,就會提前觸發(fā)處理 closeBundle 的 hookParallel 鉤子函數(shù):

  1. await graph.pluginDriver.hookParallel('closeBundle', []); 

generate 階段

outputOptions

在 handleGenerateWrite() 階段,獲取處理后的 outputOptions。

  1. outputPluginDriver.hookReduceArg0Sync( 
  2.   'outputOptions'
  3.   [rawOutputOptions.output || rawOutputOptions] as [OutputOptions], 
  4.   (outputOptions, result) => result || outputOptions, 
  5.     pluginContext => { 
  6.     const emitError = () => pluginContext.error(errCannotEmitFromOptionsHook()); 
  7.     return { 
  8.       ...pluginContext, 
  9.       emitFile: emitError, 
  10.       setAssetSource: emitError 
  11.     }; 
  12.   } 

將處理后的 outputOptions 作為傳參生成 bundle 對象:

  1. const bundle = new Bundle(outputOptions, unsetOptions, inputOptions, outputPluginDriver, graph); 

生成代碼

在 const generated = await bundle.generate(isWrite); bundle 生成代碼階段,

  1. ... // render 開始 
  2. await this.pluginDriver.hookParallel('renderStart', [this.outputOptions, this.inputOptions]); 
  3. ... // 該鉤子函數(shù)執(zhí)行過程中不能中斷 
  4. await this.pluginDriver.hookSeq('generateBundle', [ 
  5.   this.outputOptions, 
  6.   outputBundle as OutputBundle, 
  7.   isWrite 
  8. ]); 

最后并行執(zhí)行處理生成的代碼~

  1. await outputPluginDriver.hookParallel('writeBundle', [outputOptions, generated]); 

小結(jié)

不難看出插件函數(shù)鉤子貫穿了整個 rollup 的打包過程,并扮演了不同角色,支撐起了相應功能實現(xiàn)。我們目前做的就是梳理并理解這個過程,再回過頭來看這張圖,是不是就清晰多了。

最后再來講講 rollup 插件的兩個周邊叭~

插件上下文

rollup 給鉤子函數(shù)注入了 context,也就是上下文環(huán)境,用來方便對 chunks 和其他構建信息進行增刪改查。也就是說,在插件中,可以在各個 hook 中直接通過 this.xxx 來調(diào)用上面的方法。

  1. const context: PluginContext = { 
  2.     addWatchFile(id) {}, 
  3.     cache: cacheInstance, 
  4.     emitAsset: getDeprecatedContextHandler(...), 
  5.     emitChunk: getDeprecatedContextHandler(...), 
  6.     emitFile: fileEmitter.emitFile, 
  7.     error(err) 
  8.     getAssetFileName: getDeprecatedContextHandler(...), 
  9.     getChunkFileName: getDeprecatedContextHandler(), 
  10.     getFileName: fileEmitter.getFileName, 
  11.     getModuleIds: () => graph.modulesById.keys(), 
  12.     getModuleInfo: graph.getModuleInfo, 
  13.     getWatchFiles: () => Object.keys(graph.watchFiles), 
  14.     isExternal: getDeprecatedContextHandler(...), 
  15.     meta: { // 綁定 graph.watchMode 
  16.         rollupVersion, 
  17.         watchMode: graph.watchMode 
  18.     }, 
  19.     get moduleIds() { // 綁定 graph.modulesById.keys(); 
  20.         const moduleIds = graph.modulesById.keys(); 
  21.         return wrappedModuleIds(); 
  22.     }, 
  23.     parse: graph.contextParse, // 綁定 graph.contextParse 
  24.     resolve(source, importer, { custom, skipSelf } = BLANK) { // 綁定 graph.moduleLoader 上方法 
  25.         return graph.moduleLoader.resolveId(source, importer, custom, skipSelf ? pidx : null); 
  26.     }, 
  27.     resolveId: getDeprecatedContextHandler(...), 
  28.     setAssetSource: fileEmitter.setAssetSource, 
  29.     warn(warning) {} 
  30. }; 

插件的緩存

插件還提供緩存的能力,利用了閉包實現(xiàn)的非常巧妙。

  1. export function createPluginCache(cache: SerializablePluginCache): PluginCache { 
  2.  // 利用閉包將 cache 緩存 
  3.  return { 
  4.   has(id: string) { 
  5.    const item = cache[id]; 
  6.    if (!item) return false
  7.    item[0] = 0; // 如果訪問了,那么重置訪問過期次數(shù),猜測:就是說明用戶有意向主動去使用 
  8.    return true
  9.   }, 
  10.   get(id: string) { 
  11.    const item = cache[id]; 
  12.    if (!item) return undefined; 
  13.    item[0] = 0; // 如果訪問了,那么重置訪問過期次數(shù) 
  14.    return item[1]; 
  15.   }, 
  16.   set(id: string, value: any) { 
  17.             // 存儲單位是數(shù)組,第一項用來標記訪問次數(shù) 
  18.    cache[id] = [0, value]; 
  19.   }, 
  20.   delete(id: string) { 
  21.    return delete cache[id]; 
  22.   } 
  23.  }; 

然后創(chuàng)建緩存后,會添加在插件上下文中:

  1. import createPluginCache from 'createPluginCache'
  2.  
  3. const cacheInstance = createPluginCache(pluginCache[cacheKey] || (pluginCache[cacheKey] = Object.create(null))); 
  4.  
  5. const context = { 
  6.  // ... 
  7.     cache: cacheInstance, 
  8.     // ... 

之后我們就可以在插件中就可以使用 cache 進行插件環(huán)境下的緩存,進一步提升打包效率:

  1. function testPlugin() { 
  2.   return { 
  3.     name"test-plugin"
  4.     buildStart() { 
  5.       if (!this.cache.has("prev")) { 
  6.         this.cache.set("prev""上一次插件執(zhí)行的結(jié)果"); 
  7.       } else { 
  8.         // 第二次執(zhí)行 rollup 的時候會執(zhí)行 
  9.         console.log(this.cache.get("prev")); 
  10.       } 
  11.     }, 
  12.   }; 
  13. let cache; 
  14. async function build() { 
  15.   const chunks = await rollup.rollup({ 
  16.     input: "src/main.js"
  17.     plugins: [testPlugin()], 
  18.     // 需要傳遞上次的打包結(jié)果 
  19.     cache, 
  20.   }); 
  21.   cache = chunks.cache; 
  22.  
  23. build().then(() => { 
  24.   build(); 
  25. }); 

總結(jié)

恭喜你,把 rollup 那么幾種鉤子函數(shù)都熬著看過來了,并且又梳理了一遍 rollup.rollup() 打包流程??偨Y(jié)幾點輸出,康康我們學到了什么:

rollup 的插件本質(zhì)是一個處理函數(shù),返回一個對象。返回的對象包含一些屬性(如 name),和不同階段的鉤子函數(shù)(構建 build 和輸出 output 階段),以實現(xiàn)插件內(nèi)部的功能;

關于返回的對象,在插件返回對象中的鉤子函數(shù)中,大多數(shù)的鉤子函數(shù)定義了 插件的調(diào)用時機和調(diào)用方式,只有 runHook(Sync)鉤子真正執(zhí)行了插件;

關于插件調(diào)用時機和調(diào)用方法的觸發(fā)取決于打包流程,在此我們通過圖 1 流程圖也梳理了一遍 rollup.rollup() 打包流程;

插件原理都講完了,插件調(diào)用當然 so easy,一個函數(shù)誰還不會用呢?而對于簡單插件函數(shù)的開發(fā)頁也不僅僅是單純模仿,也可以做到心中有數(shù)了!

在實際的插件開發(fā)中,我們會進一步用到這些知識并一一掌握,至少寫出 bug 的時候,梳理一遍插件原理,再進一步內(nèi)化吸收,就能更快的定位問題了。在開發(fā)中如果有想法,就可以著手編寫自己的 rollup 插件啦!

 

責任編輯:姜華 來源: 微醫(yī)大前端技術
相關推薦

2020-09-08 18:37:49

TypeScript開發(fā)前端

2015-04-07 11:05:15

VMwareOpenStack

2021-07-12 07:33:31

Nacos微服務管理

2019-09-01 21:15:51

思科安全零信任云安全

2021-02-05 09:00:00

開發(fā)IT事件管理

2011-03-22 16:55:53

LAMPWAMP

2024-06-12 09:06:48

2021-06-30 07:51:09

新項目領域建模

2023-04-06 08:01:30

RustMutex

2025-04-02 07:30:37

LLMDify應用

2025-01-16 10:46:31

2022-02-13 23:00:48

前端微前端qiankun

2024-11-25 09:10:03

2021-08-07 21:51:17

服務器網(wǎng)站部署

2023-01-12 22:00:48

2013-12-18 13:30:19

Linux運維Linux學習Linux入門

2022-10-28 08:14:44

rollup打包工具庫?

2025-02-24 13:46:40

2022-05-25 10:28:35

模型AI

2021-08-15 22:52:30

前端H5拼圖
點贊
收藏

51CTO技術棧公眾號