Rollup的插件機(jī)制及構(gòu)建流程深入解析
引言
上一節(jié)我們學(xué)會(huì)了 Rollup 構(gòu)建工具的使用,相信你已經(jīng)對(duì) Rollup 的基礎(chǔ)概念和使用有了基本的掌握。同時(shí)我們也知道,僅僅使用 Rollup 內(nèi)置的打包能力很難滿(mǎn)足項(xiàng)目日益復(fù)雜的構(gòu)建需求。對(duì)于一個(gè)真實(shí)的項(xiàng)目構(gòu)建場(chǎng)景來(lái)說(shuō),我們還需要考慮到模塊打包之外的問(wèn)題,比如路徑別名(alias) 、全局變量注入和代碼壓縮等等。
可要是把這些場(chǎng)景的處理邏輯與核心的打包邏輯都寫(xiě)到一起,一來(lái)打包器本身的代碼會(huì)變得十分臃腫,二來(lái)也會(huì)對(duì)原有的核心代碼產(chǎn)生一定的侵入性,混入很多與核心流程無(wú)關(guān)的代碼,不易于后期的維護(hù)。因此 ,Rollup 設(shè)計(jì)出了一套完整的插件機(jī)制,將自身的核心邏輯與插件邏輯分離,讓你能按需引入插件功能,提高了 Rollup 自身的可擴(kuò)展性。
那接下來(lái),我會(huì)帶你分析 Rollup 的插件機(jī)制,熟悉 Rollup 插件的完整構(gòu)建階段和工作流程,并且結(jié)合案例深入插件開(kāi)發(fā)細(xì)節(jié)。Rollup 的打包過(guò)程中,會(huì)定義一套完整的構(gòu)建生命周期,從開(kāi)始打包到產(chǎn)物輸出,中途會(huì)經(jīng)歷一些標(biāo)志性的階段,并且在不同階段會(huì)自動(dòng)執(zhí)行對(duì)應(yīng)的插件鉤子函數(shù)(Hook)。對(duì) Rollup 插件來(lái)講,最重要的部分是鉤子函數(shù),一方面它定義了插件的執(zhí)行邏輯,也就是"做什么";另一方面也聲明了插件的作用階段,即"什么時(shí)候做",這與 Rollup 本身的構(gòu)建生命周期息息相關(guān)。
一、Rollup構(gòu)建階段
在執(zhí)行 rollup 命令之后,在 cli 內(nèi)部的主要邏輯簡(jiǎn)化如下:
// Build 階段 const bundle = await rollup.rollup(inputOptions); // Output 階段 await Promise.all(outputOptions.map(bundle.write)); // 構(gòu)建結(jié)束 await bundle.close();
而Rollup 內(nèi)部主要經(jīng)歷了 Build 和 Output 兩大階段,如下圖所示。

首先,Build 階段主要負(fù)責(zé)創(chuàng)建模塊依賴(lài)圖,初始化各個(gè)模塊的 AST 以及模塊之間的依賴(lài)關(guān)系。下面我們用一個(gè)簡(jiǎn)單的例子來(lái)感受一下:
// src/index.js
import { a } from './module-a';
console.log(a);
// src/module-a.js
export const a = 1;然后,執(zhí)行如下的構(gòu)建腳本:
const rollup = require('rollup');
const util = require('util');
async function build() {
const bundle = await rollup.rollup({
input: ['./src/index.js'],
});
console.log(util.inspect(bundle));
}
build();執(zhí)行上面的代碼,可以看到如下的 bundle對(duì)象信息。
{
cache: {
modules: [
{
ast: 'AST 節(jié)點(diǎn)信息,具體內(nèi)容省略',
code: 'export const a = 1;',
dependencies: [],
id: '/Users/code/rollup-demo/src/data.js',
// 其它屬性省略
},
{
ast: 'AST 節(jié)點(diǎn)信息,具體內(nèi)容省略',
code: "import { a } from './data';\n\nconsole.log(a);",
dependencies: [
'/Users/code/rollup-demo/src/data.js'
],
id: '/Users/code/rollup-demo/src/index.js',
// 其它屬性省略
}
],
plugins: {}
},
closed: false,
// 掛載后續(xù)階段會(huì)執(zhí)行的方法
close: [AsyncFunction: close],
generate: [AsyncFunction: generate],
write: [AsyncFunction: write]
}從上面的信息中可以看出,目前經(jīng)過(guò) Build 階段的 bundle 對(duì)象其實(shí)并沒(méi)有進(jìn)行模塊的打包,這個(gè)對(duì)象的作用在于存儲(chǔ)各個(gè)模塊的內(nèi)容及依賴(lài)關(guān)系,同時(shí)暴露generate和write方法,以進(jìn)入到后續(xù)的 Output 階段。
所以,真正進(jìn)行打包的過(guò)程會(huì)在 Output 階段進(jìn)行,即在bundle對(duì)象的 generate或者write方法中進(jìn)行。還是以上面的 demo 為例,我們稍稍改動(dòng)一下構(gòu)建邏輯:
const rollup = require('rollup');
async function build() {
const bundle = await rollup.rollup({
input: ['./src/index.js'],
});
const result = await bundle.generate({
format: 'es',
});
console.log('result:', result);
}
build();重新執(zhí)行項(xiàng)目后可以得到如下的輸出:
{
output: [
{
exports: [],
facadeModuleId: '/Users/code/rollup-demo/src/index.js',
isEntry: true,
isImplicitEntry: false,
type: 'chunk',
code: 'const a = 1;\n\nconsole.log(a);\n',
dynamicImports: [],
fileName: 'index.js',
// 其余屬性省略
}
]
}這里可以看到所有的輸出信息,生成的output數(shù)組即為打包完成的結(jié)果。當(dāng)然,如果使用 bundle.write 會(huì)根據(jù)配置將最后的產(chǎn)物寫(xiě)入到指定的磁盤(pán)目錄中。
因此,對(duì)于一次完整的構(gòu)建過(guò)程而言, Rollup 會(huì)先進(jìn)入到 Build 階段,解析各模塊的內(nèi)容及依賴(lài)關(guān)系,然后進(jìn)入Output階段,完成打包及輸出的過(guò)程。對(duì)于不同的階段,Rollup 插件會(huì)有不同的插件工作流程,接下來(lái)我們就來(lái)拆解一下 Rollup 插件在 Build 和 Output 兩個(gè)階段的詳細(xì)工作流程。
二、拆解插件工作流
2.1 插件 Hook 類(lèi)型
在具體講述 Rollup 插件工作流之前,我想先給大家介紹一下不同插件 Hook 的類(lèi)型,這些類(lèi)型代表了不同插件的執(zhí)行特點(diǎn),是我們理解 Rollup 插件工作流的基礎(chǔ),因此有必要跟大家好好拆解一下。
通過(guò)上文的例子,相信你可以直觀地理解 Rollup 兩大構(gòu)建階段(Build和Output)各自的原理。可能你會(huì)有疑問(wèn),這兩個(gè)階段到底跟插件機(jī)制有什么關(guān)系呢?實(shí)際上,插件的各種 Hook 可以根據(jù)這兩個(gè)構(gòu)建階段分為兩類(lèi): Build Hook 與 Output Hook。
- Build Hook:在Build階段執(zhí)行的鉤子函數(shù),在這個(gè)階段主要進(jìn)行模塊代碼的轉(zhuǎn)換、AST 解析以及模塊依賴(lài)的解析,那么這個(gè)階段的 Hook 對(duì)于代碼的操作粒度一般為模塊級(jí)別,也就是單文件級(jí)別。
- Ouput Hook:(官方稱(chēng)為Output Generation Hook),則主要進(jìn)行代碼的打包,對(duì)于代碼而言,操作粒度一般為 chunk級(jí)別,而一個(gè) chunk通常指很多文件打包到一起的產(chǎn)物。
除了根據(jù)構(gòu)建階段可以將 Rollup 插件進(jìn)行分類(lèi),根據(jù)不同的 Hook 執(zhí)行方式也會(huì)有不同的分類(lèi),主要包括Async、Sync、Parallel、Squential、First這五種。在實(shí)際的開(kāi)發(fā)過(guò)程中,我們將接觸各種各樣的插件 Hook,但無(wú)論哪個(gè) Hook 都離不開(kāi)這五種執(zhí)行方式。接下來(lái),讓我們具體來(lái)認(rèn)識(shí)下這五個(gè)函數(shù)鉤子。
Async & Sync
首先是Async和Sync鉤子函數(shù),兩者其實(shí)是相對(duì)的,分別代表異步和同步的鉤子函數(shù),兩者最大的區(qū)別在于同步鉤子里面不能有異步邏輯,而異步鉤子可以有。
Parallel
Parallel用來(lái)代表并行鉤子函數(shù)。如果有多個(gè)插件實(shí)現(xiàn)了這個(gè)鉤子的邏輯,一旦有鉤子函數(shù)是異步邏輯,則并發(fā)執(zhí)行鉤子函數(shù),不會(huì)等待當(dāng)前鉤子完成(底層使用 Promise.all)。
比如對(duì)于Build階段的buildStart鉤子,它的執(zhí)行時(shí)機(jī)其實(shí)是在構(gòu)建剛開(kāi)始的時(shí)候,各個(gè)插件可以在這個(gè)鉤子當(dāng)中做一些狀態(tài)的初始化操作,但其實(shí)插件之間的操作并不是相互依賴(lài)的,也就是可以并發(fā)執(zhí)行,從而提升構(gòu)建性能。反之,對(duì)于需要依賴(lài)其他插件處理結(jié)果的情況就不適合用 Parallel 鉤子了,比如 transform。
Sequential
Sequential 指串行的鉤子函數(shù)。這種 Hook 往往適用于插件間處理結(jié)果相互依賴(lài)的情況,前一個(gè)插件 Hook 的返回值作為后續(xù)插件的入?yún)ⅲ@種情況就需要等待前一個(gè)插件執(zhí)行完 Hook,獲得其執(zhí)行結(jié)果,然后才能進(jìn)行下一個(gè)插件相應(yīng) Hook 的調(diào)用,如transform。
First
如果有多個(gè)插件實(shí)現(xiàn)了這個(gè) Hook,那么 Hook 將依次運(yùn)行,直到返回一個(gè)非 null 或非 undefined 的值為止。比較典型的 Hook 是 resolveId,一旦有插件的 resolveId 返回了一個(gè)路徑,將停止執(zhí)行后續(xù)插件的 resolveId 邏輯。
剛剛我們介紹了 Rollup 當(dāng)中不同插件 Hook 的類(lèi)型,實(shí)際上不同的類(lèi)型是可以疊加的,Async/Sync 可以搭配后面三種類(lèi)型中的任意一種,比如一個(gè) Hook既可以是 Async 也可以是 First 類(lèi)型,接著我們來(lái)具體分析一下 Rollup 當(dāng)中的插件工作流程。
2.2 Build 階段工作流
首先,我們來(lái)分析 Build 階段的插件工作流程。對(duì)于 Build 階段,插件 Hook 的調(diào)用流程如下圖所示。

流程圖的最上面聲明了不同 Hook 的類(lèi)型,也就是我們?cè)谏厦婵偨Y(jié)的 5 種 Hook 分類(lèi),每個(gè)方塊代表了一個(gè) Hook,邊框的顏色可以表示Async和Sync 類(lèi)型,方塊的填充顏色可以表示Parallel、Sequential 和First 類(lèi)型。
接下來(lái),我們一步步來(lái)分析 Build Hooks 的工作流程,你可以對(duì)照著圖一起看。
- 首先經(jīng)歷 options 鉤子進(jìn)行配置的轉(zhuǎn)換,得到處理后的配置對(duì)象。
- 隨之 Rollup 會(huì)調(diào)用buildStart鉤子,正式開(kāi)始構(gòu)建流程。
- Rollup 先進(jìn)入到 resolveId 鉤子中解析文件路徑。(從 input 配置指定的入口文件開(kāi)始)。
- Rollup 通過(guò)調(diào)用load鉤子加載模塊內(nèi)容。
- 緊接著 Rollup 執(zhí)行所有的 transform 鉤子來(lái)對(duì)模塊內(nèi)容進(jìn)行進(jìn)行自定義的轉(zhuǎn)換,比如 babel 轉(zhuǎn)譯。
- 現(xiàn)在 Rollup 拿到了模塊內(nèi)容,接下來(lái)就是進(jìn)行 AST 分析,得到所有的 import 內(nèi)容,調(diào)用 moduleParsed 鉤子。如果是普通的 import,則執(zhí)行 resolveId 鉤子,繼續(xù)回到步驟3;如果是動(dòng)態(tài) import,則執(zhí)行 resolveDynamicImport 鉤子解析路徑,如果解析成功,則回到步驟4加載模塊,否則回到步驟3通過(guò) resolveId 解析路徑。
- 直到所有的 import 都解析完畢,Rollup 執(zhí)行buildEnd鉤子,Build 階段結(jié)束。
當(dāng)然,在 Rollup 解析路徑的時(shí)候,即執(zhí)行resolveId或者resolveDynamicImport的時(shí)候,有些路徑可能會(huì)被標(biāo)記為external(翻譯為排除),也就是說(shuō)不參加 Rollup 打包過(guò)程,這個(gè)時(shí)候就不會(huì)進(jìn)行l(wèi)oad、transform等等后續(xù)的處理了。
在流程圖最上面,不知道大家有沒(méi)有注意到watchChange和closeWatcher這兩個(gè) Hook,這里其實(shí)是對(duì)應(yīng)了 rollup 的watch模式。當(dāng)你使用 rollup --watch 指令或者在配置文件配有watch: true的屬性時(shí),代表開(kāi)啟了 Rollup 的watch打包模式,這個(gè)時(shí)候 Rollup 內(nèi)部會(huì)初始化一個(gè) watcher 對(duì)象,當(dāng)文件內(nèi)容發(fā)生變化時(shí),watcher 對(duì)象會(huì)自動(dòng)觸發(fā)watchChange鉤子執(zhí)行并對(duì)項(xiàng)目進(jìn)行重新構(gòu)建。在當(dāng)前打包過(guò)程結(jié)束時(shí),Rollup 會(huì)自動(dòng)清除 watcher 對(duì)象調(diào)用closeWacher鉤子。
2.3 Output 階段工作流
好,接著我們來(lái)看看 Output 階段的插件到底是如何來(lái)進(jìn)行工作的。這個(gè)階段的 Hook 相比于 Build 階段稍微多一些,流程上也更加復(fù)雜。需要注意的是,其中會(huì)涉及的 Hook 函數(shù)比較多,可能會(huì)給你理解整個(gè)流程帶來(lái)一些困擾,因此我會(huì)在 Hook 執(zhí)行的階段解釋其大致的作用和意義,關(guān)于具體的使用大家可以去 Rollup 的官網(wǎng)自行查閱,畢竟這里的主線還是分析插件的執(zhí)行流程,摻雜太多的使用細(xì)節(jié)反而不易于理解。下面我結(jié)合一張完整的插件流程圖和你具體分析一下。

以下是關(guān)于Output 階段工作流的說(shuō)明:
- 執(zhí)行所有插件的 outputOptions 鉤子函數(shù),對(duì) output 配置進(jìn)行轉(zhuǎn)換。
- 執(zhí)行 renderStart,并發(fā)執(zhí)行 renderStart 鉤子,正式開(kāi)始打包。
- 并發(fā)執(zhí)行所有插件的banner、footer、intro、outro 鉤子(底層用 Promise.all 包裹所有的這四種鉤子函數(shù)),這四個(gè)鉤子功能很簡(jiǎn)單,就是往打包產(chǎn)物的固定位置(比如頭部和尾部)插入一些自定義的內(nèi)容,比如協(xié)議聲明內(nèi)容、項(xiàng)目介紹等等。
- 從入口模塊開(kāi)始掃描,針對(duì)動(dòng)態(tài) import 語(yǔ)句執(zhí)行 renderDynamicImport鉤子,來(lái)自定義動(dòng)態(tài) import 的內(nèi)容。
- 對(duì)每個(gè)即將生成的 chunk,執(zhí)行 augmentChunkHash鉤子,來(lái)決定是否更改 chunk 的哈希值,在 watch 模式下即可能會(huì)多次打包的場(chǎng)景下,這個(gè)鉤子會(huì)比較適用。
- 如果沒(méi)有遇到 import.meta 語(yǔ)句,則進(jìn)入下一步,否則執(zhí)行如下的Case。對(duì)于 import.meta.url 語(yǔ)句調(diào)用 resolveFileUrl 來(lái)自定義 url 解析邏輯;對(duì)于其他import.meta 屬性,則調(diào)用 resolveImportMeta 來(lái)進(jìn)行自定義的解析。
- 接著 Rollup 會(huì)生成所有 chunk 的內(nèi)容,針對(duì)每個(gè) chunk 會(huì)依次調(diào)用插件的renderChunk方法進(jìn)行自定義操作,也就是說(shuō),在這里時(shí)候可以直接操作打包產(chǎn)物了。
- 隨后會(huì)調(diào)用 generateBundle 鉤子,這個(gè)鉤子的入?yún)⒗锩鏁?huì)包含所有的打包產(chǎn)物信息,包括 chunk (打包后的代碼)、asset(最終的靜態(tài)資源文件)??梢栽谶@里刪除一些 chunk 或者 asset,最終這些內(nèi)容將不會(huì)作為產(chǎn)物輸出。
- 由于rollup.rollup方法會(huì)返回一個(gè)bundle對(duì)象,這個(gè)對(duì)象是包含generate和write兩個(gè)方法,兩個(gè)方法唯一的區(qū)別在于后者會(huì)將代碼寫(xiě)入到磁盤(pán)中,同時(shí)會(huì)觸發(fā)writeBundle鉤子,傳入所有的打包產(chǎn)物信息,包括 chunk 和 asset,和 generateBundle鉤子非常相似。不過(guò)值得注意的是,這個(gè)鉤子執(zhí)行的時(shí)候,產(chǎn)物已經(jīng)輸出了,而 generateBundle 執(zhí)行的時(shí)候產(chǎn)物還并沒(méi)有輸出。

- 當(dāng)上述的bundle的close方法被調(diào)用時(shí),會(huì)觸發(fā)closeBundle鉤子,到這里 Output 階段正式結(jié)束。
到這里,我們終于梳理完了 Rollup 當(dāng)中完整的插件工作流程,從一開(kāi)始在構(gòu)建生命周期中對(duì)兩大構(gòu)建階段的感性認(rèn)識(shí),到現(xiàn)在插件工作流的具體分析,不禁感嘆 Rollup 看似簡(jiǎn)單,實(shí)則內(nèi)部細(xì)節(jié)繁雜。
三、常用 Hook
實(shí)際上開(kāi)發(fā) Rollup 插件就是在編寫(xiě)一個(gè)個(gè) Hook 函數(shù),你可以理解為一個(gè) Rollup 插件基本就是各種 Hook 函數(shù)的組合。
1,路徑解析: resolveId
resolveId 鉤子一般用來(lái)解析模塊路徑,為Async + First類(lèi)型即異步優(yōu)先的鉤子。這里我們拿官方的 alias 插件 來(lái)說(shuō)明,這個(gè)插件用法演示如下:
// rollup.config.js
import alias from '@rollup/plugin-alias';
module.exports = {
input: 'src/index.js',
output: {
dir: 'output',
format: 'cjs'
},
plugins: [
alias({
entries: [
// 將把 import xxx from 'module-a'
// 轉(zhuǎn)換為 import xxx from './module-a'
{ find: 'module-a', replacement: './module-a.js' },
]
})
]
};插件的代碼簡(jiǎn)化后如下:
export default alias(options) {
// 獲取 entries 配置
const entries = getEntries(options);
return {
// 傳入三個(gè)參數(shù),當(dāng)前模塊路徑、引用當(dāng)前模塊的模塊路徑、其余參數(shù)
resolveId(importee, importer, resolveOptions) {
// 先檢查能不能匹配別名規(guī)則
const matchedEntry = entries.find((entry) => matches(entry.find, importee));
// 如果不能匹配替換規(guī)則,或者當(dāng)前模塊是入口模塊,則不會(huì)繼續(xù)后面的別名替換流程
if (!matchedEntry || !importerId) {
// return null 后,當(dāng)前的模塊路徑會(huì)交給下一個(gè)插件處理
return null;
}
// 正式替換路徑
const updatedId = normalizeId(
importee.replace(matchedEntry.find, matchedEntry.replacement)
);
// 每個(gè)插件執(zhí)行時(shí)都會(huì)綁定一個(gè)上下文對(duì)象作為 this
// 這里的 this.resolve 會(huì)執(zhí)行所有插件(除當(dāng)前插件外)的 resolveId 鉤子
return this.resolve(
updatedId,
importer,
Object.assign({ skipSelf: true }, resolveOptions)
).then((resolved) => {
// 替換后的路徑即 updateId 會(huì)經(jīng)過(guò)別的插件進(jìn)行處理
let finalResult: PartialResolvedId | null = resolved;
if (!finalResult) {
// 如果其它插件沒(méi)有處理這個(gè)路徑,則直接返回 updateId
finalResult = { id: updatedId };
}
return finalResult;
});
}
}
}從這里你可以看到 resolveId 鉤子函數(shù)的一些常用使用方式,它的入?yún)⒎謩e是當(dāng)前模塊路徑、引用當(dāng)前模塊的模塊路徑、解析參數(shù),返回值可以是 null、string 或者一個(gè)對(duì)象,下面我們分情況討論一下。
- 返回值為 null 時(shí),會(huì)默認(rèn)交給下一個(gè)插件的 resolveId 鉤子處理。
- 返回值為 string 時(shí),則停止后續(xù)插件的處理。這里為了讓替換后的路徑能被其他插件處理,特意調(diào)用了 this.resolve 來(lái)交給其它插件處理,否則將不會(huì)進(jìn)入到其它插件的處理。
- 返回值為一個(gè)對(duì)象,也會(huì)停止后續(xù)插件的處理,不過(guò)這個(gè)對(duì)象就可以包含更多的信息了,包括解析后的路徑、是否被 enternal、是否需要 tree-shaking 等等,不過(guò)大部分情況下返回一個(gè) string 就夠用了。
2,load
load 為Async + First類(lèi)型,即異步優(yōu)先的鉤子,和resolveId類(lèi)似。它的作用是通過(guò) resolveId 解析后的路徑來(lái)加載模塊內(nèi)容。這里,我們以官方的 image 插件 為例來(lái)介紹一下 load 鉤子的使用。源碼簡(jiǎn)化后如下所示:
const mimeTypes = {
'.jpg': 'image/jpeg',
// 后面圖片類(lèi)型省略
};
export default function image(opts = {}) {
const options = Object.assign({}, defaults, opts);
return {
name: 'image',
load(id) {
const mime = mimeTypes[extname(id)];
if (!mime) {
// 如果不是圖片類(lèi)型,返回 null,交給下一個(gè)插件處理
return null;
}
// 加載圖片具體內(nèi)容
const isSvg = mime === mimeTypes['.svg'];
const format = isSvg ? 'utf-8' : 'base64';
const source = readFileSync(id, format).replace(/[\r\n]+/gm, '');
const dataUri = getDataUri({ format, isSvg, mime, source });
const code = options.dom ? domTemplate({ dataUri }) : constTemplate({ dataUri });
return code.trim();
}
};
}從中可以看到,load 鉤子的入?yún)⑹悄K id,返回值一般是 null、string 或者一個(gè)對(duì)象:
- 如果返回值為 null,則交給下一個(gè)插件處理;
- 如果返回值為 string 或者對(duì)象,則終止后續(xù)插件的處理,如果是對(duì)象可以包含 SourceMap、AST 等。
3,代碼轉(zhuǎn)換: transform
transform 鉤子也是非常常見(jiàn)的一個(gè)鉤子函數(shù),為Async + Sequential類(lèi)型,也就是異步串行鉤子,作用是對(duì)加載后的模塊內(nèi)容進(jìn)行自定義的轉(zhuǎn)換。我們以官方的 replace 插件為例,這個(gè)插件的使用方式如下:
// rollup.config.js
import replace from '@rollup/plugin-replace';
module.exports = {
input: 'src/index.js',
output: {
dir: 'output',
format: 'cjs'
},
plugins: [
// 將會(huì)把代碼中所有的 __TEST__ 替換為 1
replace({
__TEST__: 1
})
]
};事實(shí)上,transform的內(nèi)部實(shí)現(xiàn)也并不是很復(fù)雜,主要通過(guò)字符串替換來(lái)實(shí)現(xiàn),核心邏輯簡(jiǎn)化如下:
import MagicString from 'magic-string';
export default function replace(options = {}) {
return {
name: 'replace',
transform(code, id) {
// 省略一些邊界情況的處理
// 執(zhí)行代碼替換的邏輯,并生成最后的代碼和 SourceMap
return executeReplacement(code, id);
}
}
}
function executeReplacement(code, id) {
const magicString = new MagicString(code);
// 通過(guò) magicString.overwrite 方法實(shí)現(xiàn)字符串替換
if (!codeHasReplacements(code, id, magicString)) {
return null;
}
const result = { code: magicString.toString() };
if (isSourceMapEnabled()) {
result.map = magicString.generateMap({ hires: true });
}
// 返回一個(gè)帶有 code 和 map 屬性的對(duì)象
return result;
}transform 鉤子的入?yún)⒎謩e為模塊代碼、模塊 ID,返回一個(gè)包含 code(代碼內(nèi)容) 和 map(SourceMap 內(nèi)容) 屬性的對(duì)象,當(dāng)然也可以返回 null 來(lái)跳過(guò)當(dāng)前插件的 transform 處理。需要注意的是,當(dāng)前插件返回的代碼會(huì)作為下一個(gè)插件 transform 鉤子的第一個(gè)入?yún)ⅲ瑢?shí)現(xiàn)類(lèi)似于瀑布流的處理。
4,Chunk 級(jí)代碼修改: renderChunk
這里我們繼續(xù)以 replace插件舉例,在這個(gè)插件中,也同樣實(shí)現(xiàn)了 renderChunk 鉤子函數(shù):
export default function replace(options = {}) {
return {
name: 'replace',
transform(code, id) {
// transform 代碼省略
},
renderChunk(code, chunk) {
const id = chunk.fileName;
// 省略一些邊界情況的處理
// 拿到 chunk 的代碼及文件名,執(zhí)行替換邏輯
return executeReplacement(code, id);
},
}
}可以看到這里 replace 插件為了替換結(jié)果更加準(zhǔn)確,在 renderChunk 鉤子中又進(jìn)行了一次替換,因?yàn)楹罄m(xù)的插件仍然可能在 transform 中進(jìn)行模塊內(nèi)容轉(zhuǎn)換,進(jìn)而可能出現(xiàn)符合替換規(guī)則的字符串。
這里我們把關(guān)注點(diǎn)放到 renderChunk 函數(shù)本身,可以看到有兩個(gè)入?yún)?,分別為 chunk 代碼內(nèi)容、chunk 元信息,返回值跟 transform 鉤子類(lèi)似,既可以返回包含 code 和 map 屬性的對(duì)象,也可以通過(guò)返回 null 來(lái)跳過(guò)當(dāng)前鉤子的處理。
5,generateBundle
generateBundle 也是異步串行的鉤子,你可以在這個(gè)鉤子里面自定義刪除一些無(wú)用的 chunk 或者靜態(tài)資源,或者自己添加一些文件。這里我們以 Rollup 官方的html插件來(lái)具體說(shuō)明,這個(gè)插件的作用是通過(guò)拿到 Rollup 打包后的資源來(lái)生成包含這些資源的 HTML 文件,源碼簡(jiǎn)化后如下所示:
export default function html(opts: RollupHtmlOptions = {}): Plugin {
// 初始化配置
return {
name: 'html',
async generateBundle(output: NormalizedOutputOptions, bundle: OutputBundle) {
// 省略一些邊界情況的處理
// 1. 獲取打包后的文件
const files = getFiles(bundle);
// 2. 組裝 HTML,插入相應(yīng) meta、link 和 script 標(biāo)簽
const source = await template({ attributes, bundle, files, meta, publicPath, title});
// 3. 通過(guò)上下文對(duì)象的 emitFile 方法,輸出 html 文件
const htmlFile: EmittedAsset = {
type: 'asset',
source,
name: 'Rollup HTML Asset',
fileName
};
this.emitFile(htmlFile);
}
}
}相信從插件的具體實(shí)現(xiàn)中,你也能感受到這個(gè)鉤子的強(qiáng)大作用了。入?yún)⒎謩e為output 配置、所有打包產(chǎn)物的元信息對(duì)象,通過(guò)操作元信息對(duì)象你可以刪除一些不需要的 chunk 或者靜態(tài)資源,也可以通過(guò) 插件上下文對(duì)象的emitFile方法輸出自定義文件。
好,常用的 Rollup 鉤子我們就先介紹到這里,相信這些知識(shí)點(diǎn)已經(jīng)足夠你應(yīng)付大多數(shù)的構(gòu)建場(chǎng)景了。順便說(shuō)一句,大家在后面的章節(jié)可以了解到,Vite 的插件機(jī)制也是基于 Rollup 來(lái)實(shí)現(xiàn)的,像上面介紹的這些常用鉤子在 Vite 當(dāng)中也隨處可見(jiàn),因此,掌握了這些常用鉤子,也相當(dāng)于給 Vite 插件的學(xué)習(xí)做下了很好的鋪墊。
以上就是Rollup 的插件機(jī)制的詳細(xì)內(nèi)容,更多關(guān)于 Rollup 插件機(jī)制的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
微信公眾號(hào) 提示:Unauthorized API function 問(wèn)題解決方法
這篇文章主要介紹了微信公眾號(hào) 提示:Unauthorized API function 問(wèn)題解決方法的相關(guān)資料,這里提供了出現(xiàn)提示的解決方法,需要的朋友可以參考下2016-12-12
Javascript繼承(上)——對(duì)象構(gòu)建介紹
Javascript中除了基本數(shù)據(jù)(Undefined、Null、Boolean、Number、String),其他都是對(duì)象(Object)2012-11-11
ECharts transform數(shù)據(jù)轉(zhuǎn)換和dataZoom在項(xiàng)目中使用
這篇文章主要為大家介紹了ECharts transform數(shù)據(jù)轉(zhuǎn)換和dataZoom在項(xiàng)目中使用示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12
JS實(shí)現(xiàn)簡(jiǎn)單的操作桿旋轉(zhuǎn)示例詳解
這篇文章主要為大家介紹了JS實(shí)現(xiàn)簡(jiǎn)單的操作桿旋轉(zhuǎn)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-01-01
微信小程序 wxapp內(nèi)容組件 icon詳細(xì)介紹
這篇文章主要介紹了微信小程序 wxapp內(nèi)容組件 icon詳細(xì)介紹的相關(guān)資料,并附簡(jiǎn)單實(shí)例,需要的朋友可以參考下2016-10-10
js實(shí)現(xiàn)兔年轉(zhuǎn)圈圈動(dòng)畫(huà)示例
這篇文章主要為大家介紹了js實(shí)現(xiàn)兔年轉(zhuǎn)圈圈動(dòng)畫(huà)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-01-01

