<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          從 0 到 1 解讀 rollup Plugin

          共 17874字,需瀏覽 36分鐘

           ·

          2021-10-30 23:27


          左琳,微醫(yī)前端技術(shù)部前端開(kāi)發(fā)工程師。超~能吃,喜歡游泳健身和跳舞,熱愛(ài)生活與技術(shù)。

          rollup plugin 這篇文章讀讀改改,終于和大家見(jiàn)面啦~~~

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

          講道理,稀里糊涂直接看源碼分析只會(huì)分分鐘勸退我,而我只想分分鐘寫(xiě)個(gè) rollup 插件而已~~

          rollup 為什么需要 Plugin

          rollup -c 打包流程

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

          5794df83046a91c989200809fed88304.webp

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

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

          理解 rollup plugin

          引用官網(wǎng)的解釋?zhuān)?/span>

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

          簡(jiǎn)單來(lái)說(shuō),rollup 的插件是一個(gè)普通的函數(shù),函數(shù)返回一個(gè)對(duì)象,該對(duì)象包含一些屬性(如 name),和不同階段的鉤子函數(shù)(構(gòu)建 build 和輸出 output 階段),此處應(yīng)該回顧下上面的流程圖。

          關(guān)于約定

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

          分分鐘寫(xiě)個(gè) rollup 插件

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

          插件其實(shí)很簡(jiǎn)單

          可以打開(kāi)rollup 插件列表,隨便找個(gè)你感興趣的插件,看下源代碼。

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

          一個(gè)例子

          //?官網(wǎng)的一個(gè)例子
          export?default?function?myExample?()?{
          ??return?{
          ????name:?'my-example',?//?名字用來(lái)展示在警告和報(bào)錯(cuò)中
          ????resolveId?(?source?)?{
          ??????if?(source?===?'virtual-module')?{
          ????????return?source;?//?rollup?不應(yīng)該查詢其他插件或文件系統(tǒng)
          ??????}
          ??????return?null;?//?other?ids?正常處理
          ????},
          ????load?(?id?)?{
          ??????if?(id?===?'virtual-module')?{
          ????????return?'export?default?"This?is?virtual!"';?//?source?code?for?"virtual-module"
          ??????}
          ??????return?null;?//?other?ids
          ????}
          ??};
          }

          //?rollup.config.js
          import?myExample?from?'./rollup-plugin-my-example.js';
          export?default?({
          ??input:?'virtual-module',?//?配置?virtual-module?作為入口文件滿足條件通過(guò)上述插件處理
          ??plugins:?[myExample()],
          ??output:?[{
          ????file:?'bundle.js',
          ????format:?'es'
          ??}]
          });

          光看不練假把式,模仿寫(xiě)一個(gè):

          //?自己編的一個(gè)例子?QAQ
          export?default?function?bundleReplace?()?{
          ??return?{
          ????name:?'bundle-replace',?//?名字用來(lái)展示在警告和報(bào)錯(cuò)中
          ????transformBundle(bundle)?{
          ??????return?bundle
          ????????.replace('key_word',?'replace_word')
          ????????.replace(/正則/,?'替換內(nèi)容');
          ????},
          ??};
          }

          //?rollup.config.js
          import?bundleReplace?from?'./rollup-plugin-bundle-replace.js';
          export?default?({
          ??input:?'src/main.js',?//?通用入口文件
          ??plugins:?[bundleReplace()],
          ??output:?[{
          ????file:?'bundle.js',
          ????format:?'es'
          ??}]
          });

          嘿!這也不難嘛~~~

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

          我們要講的 rollup?plugin 也不可能就這么簡(jiǎn)單啦~~~

          接下來(lái)當(dāng)然是結(jié)合例子分析實(shí)現(xiàn)原理~~

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

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

          相關(guān)概念

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

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

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

          大家在研究 rollup 插件的時(shí)候,最關(guān)注的莫過(guò)于鉤子函數(shù)部分了,鉤子函數(shù)的調(diào)用時(shí)機(jī)有三類(lèi):

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

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

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

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

          構(gòu)建鉤子函數(shù)

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

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

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

          輸出鉤子函數(shù)

          輸出生成鉤子函數(shù)可以提供關(guān)于生成的包的信息并在構(gòu)建完成后立馬執(zhí)行。它們和構(gòu)建鉤子函數(shù)擁有一樣的工作原理和相同的類(lèi)型,但是不同的是它們分別被 ·[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)用。只使用輸出生成鉤子的插件也可以通過(guò)輸出選項(xiàng)傳入,因?yàn)橹粚?duì)某些輸出運(yùn)行。

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

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

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

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

          [PluginDriver](https://github.com/rollup/rollup/blob/07b3a02069594147665daa95d3fa3e041a82b2d0/src/utils/PluginDriver.ts#L124)?中有 9 個(gè) hook 加載函數(shù)。主要是因?yàn)槊糠N類(lèi)別的 hook 都有同步和異步的版本。

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

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

          47c7087ed1248c5e60ca5e7c7b0c5051.webp

          1. hookFirst

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

          function?hookFirst<H?extends?keyof?PluginHooks,?R?=?ReturnType<PluginHooks[H]>>(
          ??hookName:?H,
          ??args:?Args,
          ??replaceContext?:?ReplaceContext?|?null,
          ??skip?:?number?|?null
          ):?EnsurePromise<R>?
          {
          ??//?初始化?promise
          ??let?promise:?Promise?=?Promise.resolve();
          ??//?實(shí)例化?Graph?的時(shí)候,初始化?this.plugins
          ??for?(let?i?=?0;?i?this.plugins.length;?i++)?{
          ????if?(skip?===?i)?continue;
          ????//?覆蓋之前的?promise,即串行執(zhí)行鉤子函數(shù)
          ????promise?=?promise.then((result:?any)?=>?{
          ??????//?返回非?null?或?undefined?的時(shí)候,停止運(yùn)行,返回結(jié)果
          ??????if?(result?!=?null)?return?result;
          ??????//?執(zhí)行鉤子函數(shù)
          ??????return?this.runHook(hookName,?args?as?any[],?i,?false,?replaceContext);
          ????});
          ??}
          ??//?返回?hook?過(guò)的?promise
          ??return?promise;
          }

          2. hookFirstSync

          hookFirst 的同步版本,使用場(chǎng)景有?resolveFileUrl、resolveImportMeta?等。

          function?hookFirstSync<H?extends?keyof?PluginHooks,?R?=?ReturnType<PluginHooks[H]>>(
          ??hookName:?H,
          ??args:?Args,
          ??replaceContext?:?ReplaceContext
          ):?R?
          {
          ??for?(let?i?=?0;?i?this.plugins.length;?i++)?{
          ????//?runHook?的同步版本
          ????const?result?=?this.runHookSync(hookName,?args,?i,?replaceContext);
          ????//?返回非?null?或?undefined?的時(shí)候,停止運(yùn)行,返回結(jié)果
          ????if?(result?!=?null)?return?result?as?any;
          ??}
          ??//?否則返回?null
          ??return?null?as?any;
          }

          3. hookParallel

          并行執(zhí)行 hook,不會(huì)等待當(dāng)前 hook 完成。也就是說(shuō)如果某個(gè)插件是異步的,其后的插件不會(huì)等待,而是并行執(zhí)行。使用場(chǎng)景?buildEnd、buildStart、moduleParsed?等。

          hookParallel(
          ??hookName:?H,
          ??args:?Parameters,
          ??replaceContext?:?ReplaceContext
          ):?Promise<void>?{
          ??const?promises:?Promise<void>[]?=?[];
          ??for?(const?plugin?of?this.plugins)?{
          ????const?hookPromise?=?this.runHook(hookName,?args,?plugin,?false,?replaceContext);
          ????if?(!hookPromise)?continue;
          ????promises.push(hookPromise);
          ??}
          ??return?Promise.all(promises).then(()?=>?{});
          }

          4.hookReduceArg0

          對(duì) arg 第一項(xiàng)進(jìn)行 reduce 操作。使用場(chǎng)景:?options、renderChunk?等。

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

          5.hookReduceArg0Sync

          hookReduceArg0?同步版本,使用場(chǎng)景?transform、generateBundle?等,不做贅述。

          6. hookReduceValue

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

          hookReduceValue(
          ??hookName:?H,
          ??initialValue:?T?|?Promise,
          ??args:?Parameters,
          ??reduce:?(
          ???reduction:?T,
          ???result:?ResolveValue>,
          ???plugin:?Plugin
          ??)?=>?T,
          ??replaceContext?:?ReplaceContext
          ?):?Promise?{
          ??let?promise?=?Promise.resolve(initialValue);
          ??for?(const?plugin?of?this.plugins)?{
          ???promise?=?promise.then(value?=>?{
          ????const?hookPromise?=?this.runHook(hookName,?args,?plugin,?true,?replaceContext);
          ????if?(!hookPromise)?return?value;
          ????return?hookPromise.then(result?=>
          ?????reduce.call(this.pluginContexts.get(plugin),?value,?result,?plugin)
          ????);
          ???});
          ??}
          ??return?promise;
          ?}

          7. hookReduceValueSync

          hookReduceValue 的同步版本。

          8. hookSeq

          加載?sequential?類(lèi)型的鉤子函數(shù),和 hookFirst 的區(qū)別就是不能中斷,使用場(chǎng)景有?onwritegenerateBundle?等。

          async?function?hookSeq<H?extends?keyof?PluginHooks>(
          ??hookName:?H,
          ??args:?Args,
          ??replaceContext?:?ReplaceContext,
          ??//?hookFirst?通過(guò)?skip?參數(shù)決定是否跳過(guò)某個(gè)鉤子函數(shù)
          ):?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;
          }

          9.hookSeqSync

          hookSeq 同步版本,不需要構(gòu)造 promise,而是直接使用?runHookSync?執(zhí)行鉤子函數(shù)。使用場(chǎng)景有?closeWatcherwatchChange?等。

          hookSeqSync(
          ??hookName:?H,
          ??args:?Parameters,
          ??replaceContext?:?ReplaceContext
          ):?void?{
          ??for?(const?plugin?of?this.plugins)?{
          ????this.runHookSync(hookName,?args,?plugin,?replaceContext);
          ??}
          }


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

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


          runHook(Sync)

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

          function?runHook<T>(
          ??hookName:?string,
          ??args:?any[],
          ??pluginIndex:?number,
          ??permitValues:?boolean,
          ??hookContext?:?ReplaceContext?|?null,
          ):?Promise<T>?
          {
          ??this.previousHooks.add(hookName);
          ??//?找到當(dāng)前?plugin
          ??const?plugin?=?this.plugins[pluginIndex];
          ??//?找到當(dāng)前執(zhí)行的在?plugin?中定義的?hooks?鉤子函數(shù)
          ??const?hook?=?(plugin?as?any)[hookName];
          ??if?(!hook)?return?undefined?as?any;

          ??//?pluginContexts?在初始化?plugin?驅(qū)動(dòng)器類(lèi)的時(shí)候定義,是個(gè)數(shù)組,數(shù)組保存對(duì)應(yīng)著每個(gè)插件的上下文環(huán)境
          ??let?context?=?this.pluginContexts[pluginIndex];
          ??//?用于區(qū)分對(duì)待不同鉤子函數(shù)的插件上下文
          ??if?(hookContext)?{
          ????context?=?hookContext(context,?plugin);
          ??}
          ??return?Promise.resolve()
          ????.then(()?=>?{
          ??????//?允許返回值,而不是一個(gè)函數(shù)鉤子,使用 hookReduceValue 或 hookReduceValueSync 加載。
          ??????//?在?sync?同步版本鉤子函數(shù)中,則沒(méi)有?permitValues?許可標(biāo)識(shí)允許返回值
          ??????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.`,
          ????????});
          ??????}
          ??????//?傳入插件上下文和參數(shù),返回插件執(zhí)行結(jié)果
          ??????return?hook.apply(context,?args);
          ????})
          ????.catch(err?=>?throwPluginError(err,?plugin.name,?{?hook:?hookName?}));
          }

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

          rollup.rollup()

          又回到最初的起點(diǎn)~~~

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

          build 階段

          處理 inputOptions

          //?從處理 inputOptions 開(kāi)始,你的插件鉤子函數(shù)已到達(dá)!
          const?{?options:?inputOptions,?unsetOptions:?unsetInputOptions?}?=?await?getInputOptions(
          ??rawInputOptions,
          ??watcher?!==?null
          );

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

          //?處理?inputOptions?的應(yīng)用場(chǎng)景下調(diào)用了?options?鉤子
          function?applyOptionHook(watchMode:?boolean)?{
          ?return?async?(?//?異步串行執(zhí)行
          ??inputOptions:?Promise,
          ??plugin:?Plugin
          ?):?Promise?=>?{
          ??if?(plugin.options)?{?//?plugin?配置存在
          ???return?(
          ????((await?plugin.options.call(
          ?????{?meta:?{?rollupVersion,?watchMode?}?},?//?上下文
          ?????await?inputOptions
          ????))?as?GenericConfigObject)?||?inputOptions
          ???);
          ??}

          ??return?inputOptions;
          ?};
          }

          接著標(biāo)準(zhǔn)化插件

          //?標(biāo)準(zhǔn)化插件
          function?normalizePlugins(plugins:?Plugin[],?anonymousPrefix:?string):?void?{
          ?for?(let?pluginIndex?=?0;?pluginIndex???const?plugin?=?plugins[pluginIndex];
          ??if?(!plugin.name)?{
          ???plugin.name?=?`${anonymousPrefix}${pluginIndex?+?1}`;
          ??}
          ?}
          }

          生成 graph 對(duì)象處理

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

          //?不止處理緩存
          this.pluginCache?=?options.cache?.plugins?||?Object.create(null);

          //?還有?WatchChangeHook?鉤子
          if?(watcher)?{
          ??this.watchMode?=?true;
          ??const?handleChange:?WatchChangeHook?=?(...args)?=>?this.pluginDriver.hookSeqSync('watchChange',?args);?//?hookSeq?同步版本,watchChange?使用場(chǎng)景下
          ??const?handleClose?=?()?=>?this.pluginDriver.hookSeqSync('closeWatcher',?[]);?//?hookSeq?同步版本,?closeWatcher?使用場(chǎng)景下
          ??watcher.on('change',?handleChange);
          ??watcher.on('close',?handleClose);
          ??watcher.once('restart',?()?=>?{
          ????watcher.removeListener('change',?handleChange);
          ????watcher.removeListener('close',?handleClose);
          ??});
          }

          this.pluginDriver?=?new?PluginDriver(this,?options,?options.plugins,?this.pluginCache);?//?生成一個(gè)插件驅(qū)動(dòng)對(duì)象
          ...
          this.moduleLoader?=?new?ModuleLoader(this,?this.modulesById,?this.options,?this.pluginDriver);?//?初始化模塊加載對(duì)象

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

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

          export?function?resolveIdViaPlugins(
          ?source:?string,
          ?importer:?string?|?undefined,
          ?pluginDriver:?PluginDriver,
          ?moduleLoaderResolveId:?(
          ??source:?string,
          ??importer:?string?|?undefined,
          ??customOptions:?CustomPluginOptions?|?undefined,
          ??skip:?{?importer:?string?|?undefined;?plugin:?Plugin;?source:?string?}[]?|?null
          ?
          )?=>?Promise<ResolvedId?|?null>,
          ?skip:?
          {?importer:?string?|?undefined;?plugin:?Plugin;?source:?string?}[]?|?null,
          ?customOptions:?CustomPluginOptions?|?undefined
          )?{
          ?let?skipped:?Set?|?null?=?null;
          ?let?replaceContext:?ReplaceContext?|?null?=?null;
          ?if?(skip)?{
          ??skipped?=?new?Set();
          ??for?(const?skippedCall?of?skip)?{
          ???if?(source?===?skippedCall.source?&&?importer?===?skippedCall.importer)?{
          ????skipped.add(skippedCall.plugin);
          ???}
          ??}
          ??replaceContext?=?(pluginContext,?plugin):?PluginContext?=>?({
          ???...pluginContext,
          ???resolve:?(source,?importer,?{?custom,?skipSelf?}?=?BLANK)?=>?{
          ????return?moduleLoaderResolveId(
          ?????source,
          ?????importer,
          ?????custom,
          ?????skipSelf???[...skip,?{?importer,?plugin,?source?}]?:?skip
          ????);
          ???}
          ??});
          ?}
          ?return?pluginDriver.hookFirst(?// hookFirst 被調(diào)用,通過(guò)插件處理獲取就絕對(duì)路徑,first 類(lèi)型,如果有插件返回了值,那么后續(xù)所有插件的 resolveId 都不會(huì)被執(zhí)行。
          ??'resolveId',
          ??[source,?importer,?{?custom:?customOptions?}],
          ??replaceContext,
          ??skipped
          ?);
          }

          拿到resolveId hook處理過(guò)返回的絕對(duì)路徑后,就要從入口文件的絕對(duì)路徑出發(fā)找到它的模塊定義,并獲取這個(gè)入口模塊所有的依賴語(yǔ)句并返回所有內(nèi)容。在這里,我們收集配置并標(biāo)準(zhǔn)化、分析文件并編譯源碼生成 AST、生成模塊并解析依賴,最后生成 chunks,總而言之就是讀取并修改文件!要注意的是,每個(gè)文件只會(huì)被一個(gè)插件的load Hook處理,因?yàn)樗且?code style="font-size:14px;background-color:rgba(27,31,35,.05);font-family:'Operator Mono', Consolas, Monaco, Menlo, monospace;color:rgb(244,138,0);">hookFirst來(lái)執(zhí)行的。另外,如果你沒(méi)有返回值,rollup 會(huì)自動(dòng)讀取文件。接下來(lái)進(jìn)入 fetchModule 階段~

          const?module:?Module?=?new?Module(...)
          ...
          await?this.pluginDriver.hookParallel('moduleParsed',?[module.info]);?//?并行執(zhí)行?hook,moduleParsed?場(chǎng)景
          ...
          await?this.addModuleSource(id,?importer,?module);
          ...//?addModuleSource
          source?=?(await?this.pluginDriver.hookFirst('load',?[id]))????(await?readFile(id));?//?在?load?階段對(duì)代碼進(jìn)行轉(zhuǎn)換、生成等操作
          ...//?resolveDynamicImport
          const?resolution?=?await?this.pluginDriver.hookFirst('resolveDynamicImport',?[
          ??specifier,
          ??importer
          ]);


          bundle 處理代碼

          生成的 graph 對(duì)象準(zhǔn)備進(jìn)入 build 階段~~build 開(kāi)始與結(jié)束中的插件函數(shù)鉤子

          await?graph.pluginDriver.hookParallel('buildStart',?[inputOptions]);?//?并行執(zhí)行?hook,buildStart?場(chǎng)景
          ...
          await?graph.build();
          ...
          await?graph.pluginDriver.hookParallel('buildEnd',?[]);?//?并行執(zhí)行?hook,buildEnd?場(chǎng)景

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

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

          generate 階段

          outputOptions

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

          outputPluginDriver.hookReduceArg0Sync(
          ??'outputOptions',
          ??[rawOutputOptions.output?||?rawOutputOptions]?as?[OutputOptions],
          ??(outputOptions,?result)?=>?result?||?outputOptions,
          ????pluginContext?=>?{
          ????const?emitError?=?()?=>?pluginContext.error(errCannotEmitFromOptionsHook());
          ????return?{
          ??????...pluginContext,
          ??????emitFile:?emitError,
          ??????setAssetSource:?emitError
          ????};
          ??}
          )

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

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


          生成代碼

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

          ...?//?render?開(kāi)始
          await?this.pluginDriver.hookParallel('renderStart',?[this.outputOptions,?this.inputOptions]);
          ...?//?該鉤子函數(shù)執(zhí)行過(guò)程中不能中斷
          await?this.pluginDriver.hookSeq('generateBundle',?[
          ??this.outputOptions,
          ??outputBundle?as?OutputBundle,
          ??isWrite
          ]);

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

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

          小結(jié)

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

          5794df83046a91c989200809fed88304.webp

          最后再來(lái)講講 rollup 插件的兩個(gè)周邊叭~

          插件上下文

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

          const?context:?PluginContext?=?{
          ????addWatchFile(id)?{},
          ????cache:?cacheInstance,
          ????emitAsset:?getDeprecatedContextHandler(...),
          ????emitChunk:?getDeprecatedContextHandler(...),
          ????emitFile:?fileEmitter.emitFile,
          ????error(err)
          ????getAssetFileName:?getDeprecatedContextHandler(...),
          ????getChunkFileName:?getDeprecatedContextHandler(),
          ????getFileName:?fileEmitter.getFileName,
          ????getModuleIds:?()?=>?graph.modulesById.keys(),
          ????getModuleInfo:?graph.getModuleInfo,
          ????getWatchFiles:?()?=>?Object.keys(graph.watchFiles),
          ????isExternal:?getDeprecatedContextHandler(...),
          ????meta:?{?//?綁定?graph.watchMode
          ????????rollupVersion,
          ????????watchMode:?graph.watchMode
          ????},
          ????get?moduleIds()?{?//?綁定?graph.modulesById.keys();
          ????????const?moduleIds?=?graph.modulesById.keys();
          ????????return?wrappedModuleIds();
          ????},
          ????parse:?graph.contextParse,?//?綁定?graph.contextParse
          ????resolve(source,?importer,?{?custom,?skipSelf?}?=?BLANK)?{?//?綁定?graph.moduleLoader?上方法
          ????????return?graph.moduleLoader.resolveId(source,?importer,?custom,?skipSelf???pidx?:?null);
          ????},
          ????resolveId:?getDeprecatedContextHandler(...),
          ????setAssetSource:?fileEmitter.setAssetSource,
          ????warn(warning)?{}
          };


          插件的緩存

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

          export?function?createPluginCache(cache:?SerializablePluginCache):?PluginCache?{
          ?//?利用閉包將?cache?緩存
          ?return?{
          ??has(id:?string)?{
          ???const?item?=?cache[id];
          ???if?(!item)?return?false;
          ???item[0]?=?0;?//?如果訪問(wèn)了,那么重置訪問(wèn)過(guò)期次數(shù),猜測(cè):就是說(shuō)明用戶有意向主動(dòng)去使用
          ???return?true;
          ??},
          ??get(id:?string)?{
          ???const?item?=?cache[id];
          ???if?(!item)?return?undefined;
          ???item[0]?=?0;?//?如果訪問(wèn)了,那么重置訪問(wèn)過(guò)期次數(shù)
          ???return?item[1];
          ??},
          ??set(id:?string,?value:?any)?{
          ????????????//?存儲(chǔ)單位是數(shù)組,第一項(xiàng)用來(lái)標(biāo)記訪問(wèn)次數(shù)
          ???cache[id]?=?[0,?value];
          ??},
          ??delete(id:?string)?{
          ???return?delete?cache[id];
          ??}
          ?};
          }

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

          import?createPluginCache?from?'createPluginCache';

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

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

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

          function?testPlugin()?{
          ??return?{
          ????name:?"test-plugin",
          ????buildStart()?{
          ??????if?(!this.cache.has("prev"))?{
          ????????this.cache.set("prev",?"上一次插件執(zhí)行的結(jié)果");
          ??????}?else?{
          ????????//?第二次執(zhí)行?rollup?的時(shí)候會(huì)執(zhí)行
          ????????console.log(this.cache.get("prev"));
          ??????}
          ????},
          ??};
          }
          let?cache;
          async?function?build()?{
          ??const?chunks?=?await?rollup.rollup({
          ????input:?"src/main.js",
          ????plugins:?[testPlugin()],
          ????//?需要傳遞上次的打包結(jié)果
          ????cache,
          ??});
          ??cache?=?chunks.cache;
          }

          build().then(()?=>?{
          ??build();
          });


          總結(jié)

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

          1. rollup 的插件本質(zhì)是一個(gè)處理函數(shù),返回一個(gè)對(duì)象。返回的對(duì)象包含一些屬性(如 name),和不同階段的鉤子函數(shù)(構(gòu)建 build 和輸出 output 階段),以實(shí)現(xiàn)插件內(nèi)部的功能;
          2. 關(guān)于返回的對(duì)象,在插件返回對(duì)象中的鉤子函數(shù)中,大多數(shù)的鉤子函數(shù)定義了 插件的調(diào)用時(shí)機(jī)和調(diào)用方式,只有 runHook(Sync)鉤子真正執(zhí)行了插件;
          3. 關(guān)于插件調(diào)用時(shí)機(jī)和調(diào)用方法的觸發(fā)取決于打包流程,在此我們通過(guò)圖 1 流程圖也梳理了一遍 rollup.rollup() 打包流程;
          4. 插件原理都講完了,插件調(diào)用當(dāng)然 so easy,一個(gè)函數(shù)誰(shuí)還不會(huì)用呢?而對(duì)于簡(jiǎn)單插件函數(shù)的開(kāi)發(fā)頁(yè)也不僅僅是單純模仿,也可以做到心中有數(shù)了!


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


          瀏覽 62
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  国产久视频 | 色五月婷婷激情 | 天天爽天天爽夜夜爽毛片 | 99热最新国产 | 成人AⅤ|