前言
從 9 月份開始,vuepress 源碼進行了重新設(shè)計和拆分。先是開了個 next 分支氏淑,后來又合并到 master 分支兵琳,為即將發(fā)布的 1.x 版本做準(zhǔn)備狂秘。
最主要的變化是:大部分的全局功能都被拆分成了插件的形式,以可插拔的方式來支撐 vuepress 的運作躯肌,這一點很像 webpack者春。
具體架構(gòu)如下:
從圖中我們可以看出,vuepress 被劃分成了兩個部分:前端部分和服務(wù)端(Node.js)部分清女。
- 前端部分
- 1.1 UI钱烟,也就是站點主題使用的代碼。包括導(dǎo)航欄、側(cè)邊菜單拴袭、搜索框組件等读第。
- 1.2 當(dāng)前 Vue 實例的擴展,提供了代碼注入(inject)拥刻、實例混入(mixin)怜瞒、組件拓展(components)、路由拓展(routes)方式般哼。
- 服務(wù)端部分
- 2.1 構(gòu)建流程盼砍,這部分暴露出了 webpack、webpack-dev-server逝她、markdown-it浇坐、動態(tài)模塊的配置。
- 2.2 用戶文件黔宛,包括配置文件和 markdown 文件(文檔)近刘,這些文件相當(dāng)于站點的元數(shù)據(jù)。
- 2.3 主題臀晃,這部分被劃分為配置文件和布局組件觉渴。vuepress 提供了一份默認(rèn)的主題。
在這個架構(gòu)中徽惋,主題即插件案淋。也就是說使用(開發(fā))一個主題和使用(開發(fā))一個插件的方式幾乎一致。
- 2.4 插件 API险绘,這是今天我們重點介紹的部分踢京,特別是插件機制的核心實現(xiàn)。
根據(jù)這個架構(gòu)宦棺,vuepress 的插件便可以做很多事情了瓣距。具體用法可以參考文檔。
內(nèi)部插件和官方插件
讓我們先來了解一下 vuepress 的內(nèi)部插件和官方插件都有些什么代咸,借助插件機制做了哪些事情蹈丸。
內(nèi)部插件
全局增強:默認(rèn)用來實現(xiàn)全局應(yīng)用增強的邏輯。
它使用 enhanceAppFiles 指定增強全局應(yīng)用和主題的文件路徑呐芥。憑著這個逻杖,vuepress 就能準(zhǔn)確地找到你全局增強或是主題的文件所在地。布局組件:默認(rèn)提供的布局組件思瘟。
它使用 clientDynamicModules 來實現(xiàn)動態(tài)引入布局相關(guān)的組件荸百。頁面組件:默認(rèn)提供的頁面組件(布局組件的子組件)。
它使用 clientDynamicModules 來實現(xiàn)動態(tài)引入頁面相關(guān)的組件潮太。根組件混入:默認(rèn)往根組件混入的邏輯管搪。
它使用 clientDynamicModules 來實現(xiàn)動態(tài)混入元信息。包括根組件的標(biāo)題铡买、語言等更鲁。路由:默認(rèn)的生成路由邏輯。
它使用 clientDynamicModules 來實現(xiàn)動態(tài)注冊路由奇钞。我們的 markdown 文件在轉(zhuǎn)換成 vue 組件后就是通過它自動注冊到 vue-router 的澡为。站點數(shù)據(jù):默認(rèn)的生成站點數(shù)據(jù)邏輯。
它使用 clientDynamicModules 來實現(xiàn)生成全局站點數(shù)據(jù)景埃。我們在頁面里拿到的全局計算屬性 $site 就是這樣來的媒至。模塊化轉(zhuǎn)化:將 cmd 代碼轉(zhuǎn)成 esm 代碼的邏輯。
還是用 clientDynamicModules 來實現(xiàn)將 cmd 代碼轉(zhuǎn)成 esm 代碼谷徙。主要是因為 ClientComputedMixin 這個類前后端代碼都要使用拒啰。樣式增強
全局樣式增強。使用 enhanceAppFiles 和 ready 鉤子來實現(xiàn)(主題樣式+用戶樣式+父主題樣式)完慧。樣式覆蓋
全局樣式覆蓋谋旦,使用 ready 鉤子來實現(xiàn),覆蓋 config.styl 和父主題的 palette屈尼。dataBlock數(shù)據(jù)注入
解析 blockType=data 的數(shù)據(jù)册着,使用 chainWebpack 和 enhanceAppFiles 來實現(xiàn),對 blockType=data 類型的數(shù)據(jù)注入到 markdown 生成的 vue 組件里去脾歧,每個組件可以訪問自己的 $dataBlock 屬性拿到甲捏。
官方插件
活動的標(biāo)題鏈接
它會在用戶滾動頁面時自動轉(zhuǎn)變側(cè)邊欄的高亮標(biāo)題。
它使用了 clientRootMixin 和 define 往根組件混入了滾動邏輯:監(jiān)聽 onScroll 事件鞭执,獲取所有錨點元素并根據(jù)滾動距離計算出高亮的錨點司顿。回到頂部
使用了 enhanceAppFiles 和 globalUIComponents 注冊了一個全局組件:點擊后可以滾動到頁面頂部。-
- 3.1 使用 extendPageData 創(chuàng)建標(biāo)簽頁和目錄頁
- 3.2 使用 ready兄纺、clientDynamicModules免猾、enhanceAppFile 創(chuàng)建頁面元數(shù)據(jù)。
ga
谷歌分析站點的庫囤热。使用了 define 和 enhanceAppFiles 初始化了 ga猎提。國際化(廢棄)
可以讓你的站點擁有切換語言的能力。使用了 enhanceAppFiles 和 additionalPages 注冊了個 I18n 布局組件旁蔼。文檔的最近更新時間
可以讓每個文檔頁下面顯示最近的 git 提交時間锨苏。使用 extendPageData 拓展了 $page 的 lastUpdated 屬性。圖片預(yù)覽
集成了 medium-zoom棺聊。使用了 define伞租、clientRootMixin 往根組件里混入了 zoom 的初始化和更新邏輯。分頁
讓共享側(cè)邊菜單欄的文檔擁有分頁切換的能力限佩。使用了 enhanceAppFiles 定義了所有頁面的索引和順序葵诈。ready 定義了分頁的規(guī)則如排序規(guī)則等裸弦、clientDynamicModules 生成動態(tài)模塊給前端代碼使用。pwa
集成 service-worker 功能
- 9.1. 使用 ready 開啟 serviceWorker 選項
- 9.2. 使用 alias 實現(xiàn)用 vue 當(dāng)事件通道
- 9.3. 使用 define作喘、globalUIComponents 注冊更新 PWA 應(yīng)用按鈕組件
- 9.4. 使用 enhanceAppFiles 注入 register-service-worker 的初始化和更新邏輯
- 9.5. 使用 generated 通過 workbox-build 完成 sw 功能注冊全局 Vue 組件
使用 enhanceAppFiles 把一個文件夾中的 vue 組件文件都注冊好理疙。搜索框
使用 alias 和 define 讓搜索框可以動態(tài)引入。進度條
使用 clientRootMixin 和 enhanceAppFiles 集成 nprogress泞坦。
lerna
項目管理上窖贤,插件機制也使得原來的一個大項目拆成了 1 + N 的形式,package.json
也變得多了起來贰锁,為了管理這種項目赃梧,vuepress 引入了 lerna。
關(guān)于 lerna 的知識豌熄,有興趣的讀者可以參考:lerna管理前端packages的最佳實踐授嘀。
核心實現(xiàn)
當(dāng)一系列插件要使用時,需要通過 PluginAPI 和組成它的各種 Option 來實現(xiàn)锣险。
整體流程大致如下:
這里我劃分成了兩個階段粤攒,用虛線分隔,一個是調(diào)用前階段囱持,一個是調(diào)用后階段夯接。插件們被調(diào)用前,是會被載入以及注冊的纷妆,之后化整為零盔几,映射成若干個 Option 實例。
源碼
- PluginAPI 類掩幢,這部分代碼包含了插件機制中的注冊和調(diào)用實現(xiàn)逊拍。
-
構(gòu)造(constructor):初始化選項、插件上下文际邻、插件隊列(可注冊插件列表)芯丧、日志插件、初始化標(biāo)志位世曾、插件解析器屬性缨恒,然后把選項們都裝載進來(initializeOptions)。這里會把一個插件映射成若干個 Option 實例轮听。
例如骗露,一個插件只有 ready、chainWebpack血巍、additionalPages 三個選項萧锉,則會得到三個 Option 實例。
- 使用(use)述寡,需要
_initialized
標(biāo)志為 false 才能調(diào)用柿隙,用于確認(rèn)哪些插件是可以被注冊的:- 對于非對象類型的插件叶洞,會調(diào)用
normalizePlugin
方法將之轉(zhuǎn)成對象
- 期間會調(diào)用
_pluginResolver(ModuleResolver 實例)
來解析模塊
- 用于解析模塊的 ModuleResolver 類,工作原理類似 webpack 的模塊解析禀崖。源碼
-
這里值得一提的是 resolve 方法衩辟,它支持從非字符串包、npm 包帆焕、絕對路徑、相對路徑中解析模塊不恭。
- 相對路徑的模塊先使用 node 的原生
path.resolve
方法解析得到絕對路徑叶雹,然后交給解析絕對路徑模塊的方法處理。
-
絕對路徑换吧、非字符串包和 npm 包會用通用模塊 CommonModule 表示折晦。
- 通用模塊有四個屬性:entry、shortcut沾瓦、name满着、fromDep。
- 還會調(diào)用 flattenPlugin 拍平插件贯莺,主要是獲取配置风喇。
-
如果傳入配置是函數(shù),則返回調(diào)用后的結(jié)果缕探,入?yún)椴寮x項魂莫、插件上下文、PluginAPI 實例爹耗。
-
傳入的配置是對象耙考,則返回一個拷貝后的對象。
-
- 期間會調(diào)用
-
非 multiple 的插件潭兽,會根據(jù)插件名字去重倦始。
- 標(biāo)準(zhǔn)化后的插件,會加入到插件隊列中去山卦。
-
最后鞋邑,存在插件中使用插件的情況時,會調(diào)用 useByPluginsConfig 來實現(xiàn)账蓉。
- 這里面的 normalizePluginsConfig 會將配置格式化成[[p1]炫狱、[p2]的形式]。
- 對于非對象類型的插件叶洞,會調(diào)用
- 初始化(initialize):先將
_initialized
標(biāo)志位置為 true剔猿,然后注冊所有可用的插件视译。
-
在初始化之前,內(nèi)部插件的使用归敬,會先于用戶的插件酷含。
-
注冊(applyPlugin):到這里鄙早,插件已經(jīng)被拆分成細(xì)化的選項,按照信息類(pluginName椅亚、shortcut)限番、鉤子類(ready、compiled 等)呀舔、其他類(chainWebpack弥虐、chainMarkdown、enhanceAppFiles 等)按順序鏈?zhǔn)阶裕╮egisterOption)媚赖。
此時霜瘪,一個 Option 實例中已經(jīng)承載了若干個插件的邏輯了。
-
- enabledPlugins 和 disabledPlugins 兩個只讀屬性可以取啟用(可注冊)或禁用(不可注冊)的插件列表惧磺。
- getOption 可以取具體的一個選項實例颖对,applyAsyncOption 和 applySyncOption 分別應(yīng)用異步選項和同步選項中的邏輯(回調(diào)函數(shù))。
-
選項和異步選項磨隘,插件的本體
-
Option 類
- 每個實例初始化 key(選項標(biāo)識) 和 items(這個選項所對應(yīng)的函數(shù)們) 屬性缤底。-
重要方法:syncApply(也叫 apply),對之前保存在實例中的 items 遍歷調(diào)用 add 方法番捂,如果 item 中的值是函數(shù)个唧,則執(zhí)行之取其返回值。
-
在插件應(yīng)用選項時如果匹配成功设预,會調(diào)用 add 方法將選項映射成 1-n 個對象推入 items 屬性里坑鱼。
- 除了 add 還有 delete 和 clear 方法,不做贅述絮缅。(增刪清)
- 另外有 values鲁沥、entries 和 appliedValues 三個只讀屬性,用于獲取值耕魄、實體画恰、已應(yīng)用的值。
-
管道方法(pipeline)吸奴,它將實例的 values 屬性柯里化成一個組合函數(shù)允扇,依次執(zhí)行。
-
重要方法:syncApply(也叫 apply),對之前保存在實例中的 items 遍歷調(diào)用 add 方法番捂,如果 item 中的值是函數(shù)个唧,則執(zhí)行之取其返回值。
-
AsyncOption 類
- asyncApply 異步版
syncApply
则奥,調(diào)用函數(shù)的時候使用了 await考润。
-
parallelApply 如果說 pipeline 是串行,它就是并行:使用了 Promise.all
- pipeline 同理读处,調(diào)用函數(shù)的時候使用了 await糊治。
- asyncApply 異步版
特殊選項
- EnhanceAppFilesOption、ClientDynamicModulesOption罚舱、GlobalUIComponentsOption井辜、DefineOption绎谦、AliasOption 類
- AliasOption
- 在創(chuàng)建 webpack 配置的時候調(diào)用
- 重寫 apply 方法:先調(diào)用 syncApply,然后將 appliedValues 取出粥脚,設(shè)置為 webpack 的 alias
- ClientDynamicModulesOption
- 在 prepare 階段調(diào)用
- 重寫 apply 方法:從 appliedItems 取出應(yīng)用的插件信息窃肠,遍歷寫入文件以待使用
- DefineOption
- 類似 AliasOption,只不過是設(shè)置 webpack 的全局變量
- 最后在 injections 插件(DefinePlugin)觸發(fā)時收集選項將 define 注入進去
- EnhanceAppFilesOption
- 在 prepare 階段調(diào)用
- 重寫 apply 方法:從 appliedItems 取出插件信息刷允,生成引入模塊或者注冊組件的代碼文件
- GlobalUIComponentsOption
- 類似 ClientDynamicModulesOption冤留,寫全局 ui 組件文件
- AliasOption
調(diào)用函數(shù)型 Option 時機
- extendCli
創(chuàng)建 cli 命令時 - chainMarkdown 和 extendMarkdown
創(chuàng)建 MarkdownIt 實例時 - additionalPages
解析完所有頁面后
3、extendPageData
additionalPages 執(zhí)行完之后树灶,依賴 additionalPages 執(zhí)行完的結(jié)果 - ready
緊跟 additionalPages 之后 - clientDynamicModules纤怒、enhanceAppFiles、globalUIComponents
緊跟 ready 之后 - define破托、alias
創(chuàng)建公共 webpack 配置后 - chainWebpack
創(chuàng)建 dev webpack 配置后肪跋、創(chuàng)建 build webpack 配置后 - beforeDevServer
webpack-dev-server 的 before 選項執(zhí)行后 - afterDevServer
webpack-dev-server 的 after 選項執(zhí)行后 - generated
build 完成后 - updated
文件更新后 - clientRootMixin
clientDynamicModules 選項執(zhí)行時
編寫一個 vuepress 插件
我也寫了一個小插件歧蒋,它可以將你的 vuepress 站點下載成一個 pdf 文件:vuepress-plugin-export-site
- 使用 ready 選項
- 借助 puppeteer 和 easy-pdf-merge 實現(xiàn):從上下文中拿到路由信息土砂,然后使用 puppeteer 遍歷訪問并下載,最后合并成一個大 PDF谜洽。
- 因為需要下載 chromium萝映,所以國內(nèi)網(wǎng)絡(luò)受限。我們換成了 puppeteer-cn阐虚。
- easy-pdf-merge 如果在 windows 下運行需要指定 jar 環(huán)境變量序臂。
后記
我們熟悉的 webpack、vue 也有插件系統(tǒng)实束,它們都有兩個共同的特點:
- 提供一個功能擴展點奥秆,讓插件能夠去擴展它。
- 提供一個功能注冊功能咸灿,讓插件注冊進來构订。
其實插件機制也可以看做設(shè)計模式的一種體現(xiàn):抽離出變化的部分,保留不變的部分避矢。這些變化的部分悼瘾,便可以稱之為插件。
在我們造輪子的時候审胸,如果輪子的功能越來越多亥宿,代碼越來越臃腫的話,引入插件機制會讓后續(xù)的開發(fā)更加靈活砂沛。
最后烫扼,幫插件機制的開發(fā)者真山同學(xué)宣傳一下,屆時會有更加精彩的 vuepress 分享: