利用uplugin對(duì)比Webpack和Rollup插件系統(tǒng)

本文由華為云云嶺團(tuán)隊(duì)松塔同學(xué)分享~
江湖上一直流傳一種說法:Rollup 的插件系統(tǒng)設(shè)計(jì),相比與 webpack,要更加科學(xué)順手。(網(wǎng)絡(luò)上對(duì) webpack 插件編寫的吐槽不計(jì)其數(shù))Talk is cheap,本文基于 unplugin 這個(gè)三方庫(kù)來對(duì)比研究一下二者的插件系統(tǒng)。Unplugin 是一個(gè)插件編寫工具,它可以讓開發(fā)者用一套代碼同時(shí)為主流 bundler 編寫插件,包括 webpack、Rollup、Vite、esbuild、Rspack。
Unplugin hooks
Unplugin 以 Rollup 的 hooks 為基礎(chǔ),總共有 9 個(gè)生命周期鉤子函數(shù),其中包含 6 個(gè) build hooks,1 個(gè) output generation hook,和 2 個(gè)獨(dú)立 hooks。借此我們可以大致了解不同 bundler 之間的通用能力。下面將簡(jiǎn)要介紹包含 unplugin 自身邏輯的鉤子函數(shù),其余請(qǐng)參考 Rollup 官方文檔。
buildStart 和 buildEnd
與 Rollup 的鉤子函數(shù)相同,分別代表一次 build 準(zhǔn)備開始和 build 結(jié)束。不同的是 unplugin 將函數(shù)的 this 指向了自身定義的UnpluginBuildContext
:
該上下文提供了四個(gè)方法,unplugin 為每個(gè) bundler 都實(shí)現(xiàn)了一遍,按需使用。
loadInclude 和 transformInclude
專門為 webpack 適配的鉤子函數(shù),用來過濾需要 load 或者 transform 的模塊。由于 webpack loader 和 plugin 分離的設(shè)計(jì),load 和 transform 的功能實(shí)際被 loader 所承載。如果沒有過濾函數(shù),會(huì)導(dǎo)致所有模塊都被插件加載,影響 webpack 性能。
Unplugin Webpack模塊實(shí)現(xiàn)
深入看 unplugin 對(duì) webpack 模塊的實(shí)現(xiàn),可以觀察到 Rollup 類的鉤子函數(shù)是如何轉(zhuǎn)換到 webpack 系統(tǒng)中的。
首先了解 webpack plugin 的設(shè)計(jì)。官方文檔中給出的示例比較傳統(tǒng):一個(gè)具有 apply 函數(shù)的 class,通過 constructor 接收用戶對(duì)插件的自定義設(shè)置。實(shí)際上,webpack 只需要一個(gè)帶有 apply 方法的對(duì)象就夠了。Unplugin 還額外包了一層生成函數(shù),將用戶配置傳遞到每個(gè) bundler 的插件定義函數(shù)中,此外還提供了meta
參數(shù)表明它要為哪個(gè) bunlder 生成插件:
代碼中factory
就是定義插件在各生命周期中執(zhí)行具體邏輯的函數(shù),例如:
在執(zhí)行鉤子函數(shù)之前,有一系列初始化工作。首先在 webpack compiler 中注入自身上下文。
接著調(diào)用factory
函數(shù)拿到插件定義:
unplugin 支持多個(gè)插件同時(shí)定義,所以這里統(tǒng)一用toArray
轉(zhuǎn)換成數(shù)組處理。然后遍歷數(shù)組,給插件增加公共屬性:
注意這里給 childCompiler 也同樣注入了上下文。這一系列注入上下文的動(dòng)作,是讓整個(gè) webpack 都能拿到插件的定義。這在 webpack loader 中拿到 plugin 的定義是有作用的,因?yàn)?loader 定義中,它只是一個(gè)接受 source code 的函數(shù),然后返回轉(zhuǎn)譯過的 source code。通過全局注入,我們就能在 loader 的定義函數(shù)中拿到 plugin 的load
函數(shù)和transform
函數(shù)。
接下來按照鉤子函數(shù)的執(zhí)行順序,逐一解析其源碼。
buildStart
buildStart
與watchChange
被放在一起處理,因?yàn)樗麄兌家玫缴舷挛?。具體看buildStart
,僅僅提供context
并執(zhí)行plugin.buildStart
。對(duì)應(yīng)到 webpack 插件生命周期是make
。查閱 webpack 文檔我們可以發(fā)現(xiàn),unplugin 略過了一系列 webpack 初始化的鉤子函數(shù),例如讀取 config,初始化 compiler,調(diào)用插件等等。因?yàn)檫@些是 webpack 的自有邏輯,和 Rollup 也無法兼容。make
會(huì)在一次 compliation 創(chuàng)建完后觸發(fā),即將開始從 entry 讀取文件。符合 Rollup 的buildStart
定義。
watchChange
watchChange
是獨(dú)立于執(zhí)行順序之外的鉤子函數(shù)。當(dāng) bundler 以 watch 模式運(yùn)行時(shí),當(dāng)被監(jiān)測(cè)的文件發(fā)生變化時(shí)觸發(fā)。在 webpack 中,unplugin 利用了 compiler 的modifiedFiles
和removedFiles
來獲取對(duì)應(yīng)的文件。由于每次文件變化 Webpack 都會(huì)重新執(zhí)行一次 compilation,因此modifiedFiles
和removedFiles
也對(duì)應(yīng)更新。
modifiedFiles
是 Webpack 5 新增的屬性。
resolveId
Rollup 的resolveId
存在三個(gè)入?yún)?code>source,?importer
,?options?
:
Webpack 中 resolve 相關(guān)概念位于 config 中的 resolve 對(duì)象,比較常見的設(shè)置如 alias。Webpack 對(duì) resolve 專門提供了一個(gè)插件的設(shè)置,它不同于普通的 plugin,屬于ResolvePluginInstance
,unplugin 利用這個(gè)設(shè)置傳入resolveId
函數(shù)。
可以看到id
和importer
都來自于resolve
這個(gè)鉤子函數(shù)傳入的參數(shù),可惜在 webpack 文檔中缺乏相關(guān)說明。options
參數(shù)中,只提供了isEntry
屬性。最后我們看到resolverPlugin
被手動(dòng)創(chuàng)建出來后,放進(jìn)了 compiler options 中??梢?webpack 插件的能力包括修改 config 文件,能力其實(shí)完全覆蓋了 loader,這在后續(xù)的load
和transform
函數(shù)中同樣能見到。
從源碼中我們會(huì)看到?virtual module?相關(guān)的代碼,本文為簡(jiǎn)化場(chǎng)景會(huì)略過。下同。
load
Webpack 中 loader 定義在 config 中,例如:
用正則表示文件類型,然后指定 loader。Unplugin 通過手動(dòng)實(shí)現(xiàn)一個(gè) loader,然后插入 rules 來實(shí)現(xiàn)load
的功能:
Loader 除了用test
正則匹配外,也支持用函數(shù)過濾,以被 import 的資源 path 為入?yún)?,這正是loadInclude
的設(shè)計(jì)來源。從上述代碼中我還會(huì)發(fā)現(xiàn)一個(gè)屬性enforce
,它是用來控制 loader?執(zhí)行時(shí)機(jī)的。這也是 unplugin 要用unshift
插入 rules 數(shù)組的原因。(默認(rèn)最后加載 unplugin 插件)
具體看下LOAD_LOADER
實(shí)現(xiàn):
通過 webpack 上下文,可以拿到資源 id、找到對(duì)應(yīng) unplugin 插件。接著就是提供 unplugin 自身上下文然后調(diào)用load
函數(shù)。根據(jù) Rollup 對(duì)load
返回結(jié)果的定義,調(diào)用callback
傳參。
transform
transform
?函數(shù)和load
函數(shù)類似,同樣是自定義一個(gè) loader,然后插入rules
。唯一的區(qū)別是處理 transform 邏輯時(shí),沒有用到 include 函數(shù),而是在 use 函數(shù)中再執(zhí)行transformInclude
進(jìn)行過濾。(這是令人困惑的地方,因?yàn)檫@和前文所述 unplugin 設(shè)計(jì)transformInclude
的理由矛盾。沒有 include 函數(shù)會(huì)導(dǎo)致所有模塊都被插件加載)
Unplugin會(huì)先處理 transform 邏輯,由于用 unshift 插入 rules,會(huì)導(dǎo)致load生成的 rule 在transform 之前,按照 webpack 默認(rèn)的加載 loader 順序,transform 會(huì)先于 load 被觸發(fā)。不知是 bug 還是 unplugin 的預(yù)期行為。
buildEnd
buildEnd
對(duì)應(yīng) webpack 的emit
鉤子函數(shù)。
writeBundle
writeBundle
對(duì)應(yīng) webpack 的afterEmit
鉤子函數(shù)。沒有任何傳參和上下文的調(diào)用,意味著拿不到所有 bundler 創(chuàng)建出的文件。
總結(jié)
我們可以發(fā)現(xiàn) unplugin 實(shí)際用到的 webpack hooks 只有三個(gè):make
,?emit
,afterEmit
。load
和transform
的功能由 webpack loader 所承載。make
對(duì) webpack 是一個(gè)很關(guān)鍵鉤子函數(shù),它表明了 webpack 一系列初始化的工作已完成,開始從入口文件出發(fā)編譯每一個(gè)模塊。
webpack 的插件系統(tǒng)對(duì)比 rollup 來說,一大特點(diǎn)是鉤子函數(shù)特別多。除了本文提到的 compiler 具有鉤子函數(shù)外,包括 compilation、ContextModuleFactory、 NormalModuleFactory、甚至 JavaScript 的 parser 都有一系列鉤子函數(shù)。同時(shí) plugin、loader、resolver 分離的設(shè)計(jì)也增加了系統(tǒng)的復(fù)雜度。因此網(wǎng)絡(luò)上的觀點(diǎn)更偏愛于 rollup 也不無道理。復(fù)雜的系統(tǒng)不代表不好,但是無疑增加了用戶的學(xué)習(xí)成本。
關(guān)于 OpenTiny
OpenTiny 是一套企業(yè)級(jí)組件庫(kù)解決方案,適配 PC 端 / 移動(dòng)端等多端,涵蓋 Vue2 / Vue3 / Angular 多技術(shù)棧,擁有主題配置系統(tǒng) / 中后臺(tái)模板 / CLI 命令行等效率提升工具,可幫助開發(fā)者高效開發(fā) Web 應(yīng)用。
核心亮點(diǎn):
跨端跨框架
:使用 Renderless 無渲染組件設(shè)計(jì)架構(gòu),實(shí)現(xiàn)了一套代碼同時(shí)支持 Vue2 / Vue3,PC / Mobile 端,并支持函數(shù)級(jí)別的邏輯定制和全模板替換,靈活性好、二次開發(fā)能力強(qiáng)。組件豐富
:PC 端有80+組件,移動(dòng)端有30+組件,包含高頻組件 Table、Tree、Select 等,內(nèi)置虛擬滾動(dòng),保證大數(shù)據(jù)場(chǎng)景下的流暢體驗(yàn),除了業(yè)界常見組件之外,我們還提供了一些獨(dú)有的特色組件,如:Split 面板分割器、IpAddress IP地址輸入框、Calendar 日歷、Crop 圖片裁切等配置式組件
:組件支持模板式和配置式兩種使用方式,適合低代碼平臺(tái),目前團(tuán)隊(duì)已經(jīng)將 OpenTiny 集成到內(nèi)部的低代碼平臺(tái),針對(duì)低碼平臺(tái)做了大量?jī)?yōu)化周邊生態(tài)齊全
:提供了基于 Angular + TypeScript 的 TinyNG 組件庫(kù),提供包含 10+ 實(shí)用功能、20+ 典型頁(yè)面的 TinyPro 中后臺(tái)模板,提供覆蓋前端開發(fā)全流程的 TinyCLI 工程化工具,提供強(qiáng)大的在線主題配置平臺(tái) TinyTheme
聯(lián)系我們:
官方公眾號(hào):
OpenTiny
OpenTiny 官網(wǎng):https://opentiny.design/
OpenTiny 代碼倉(cāng)庫(kù):https://github.com/opentiny/
Vue 組件庫(kù):https://github.com/opentiny/tiny-vue?(歡迎 Star)
Angluar組件庫(kù):https://github.com/opentiny/ng?(歡迎 Star)
CLI工具:https://github.com/opentiny/tiny-cli?(歡迎 Star)
更多視頻內(nèi)容也可以關(guān)注OpenTiny社區(qū),B站/抖音/小紅書/視頻號(hào)。