從 0 到 1 解讀 rollup Plugin
左琳,微醫(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)流程可以參考下面的流程圖理解。

為此,我們可以通過(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)的順序??。

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)景有?onwrite、generateBundle?等。
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)景有?closeWatcher、watchChange?等。
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)看這張圖,是不是就清晰多了。

最后再來(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é)到了什么:
- rollup 的插件本質(zhì)是一個(gè)處理函數(shù),返回一個(gè)對(duì)象。返回的對(duì)象包含一些屬性(如 name),和不同階段的鉤子函數(shù)(構(gòu)建 build 和輸出 output 階段),以實(shí)現(xiàn)插件內(nèi)部的功能;
- 關(guān)于返回的對(duì)象,在插件返回對(duì)象中的鉤子函數(shù)中,大多數(shù)的鉤子函數(shù)定義了 插件的調(diào)用時(shí)機(jī)和調(diào)用方式,只有 runHook(Sync)鉤子真正執(zhí)行了插件;
- 關(guān)于插件調(diào)用時(shí)機(jī)和調(diào)用方法的觸發(fā)取決于打包流程,在此我們通過(guò)圖 1 流程圖也梳理了一遍 rollup.rollup() 打包流程;
- 插件原理都講完了,插件調(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 插件啦!
