讀 VuePress(四)插件機制的設計

前言

從 9 月份開始,vuepress 源碼進行了從新設計和拆分。先是開了個 next 分支,後來又合併到 master 分支,爲即將發佈的 1.x 版本作準備。html

最主要的變化是:大部分的全局功能都被拆分紅了插件的形式,以可插拔的方式來支撐 vuepress 的運做,這一點很像 webpack。前端

具體架構以下: vue

架構

從圖中咱們能夠看出,vuepress 被劃分紅了兩個部分:前端部分和服務端(Node.js)部分。node

  1. 前端部分
  1. 服務端部分
  • 2.1 構建流程,這部分暴露出了 webpackwebpack-dev-servermarkdown-it動態模塊的配置。
  • 2.2 用戶文件,包括配置文件和 markdown 文件(文檔),這些文件至關於站點的元數據。
  • 2.3 主題,這部分被劃分爲配置文件和佈局組件。vuepress 提供了一份默認的主題。

在這個架構中,主題即插件。也就是說使用(開發)一個主題和使用(開發)一個插件的方式幾乎一致。webpack

  • 2.4 插件 API,這是今天咱們重點介紹的部分,特別是插件機制的核心實現。

根據這個架構,vuepress 的插件即可以作不少事情了。具體用法能夠參考文檔git

內部插件和官方插件

讓咱們先來了解一下 vuepress 的內部插件和官方插件都有些什麼,藉助插件機制作了哪些事情。github

內部插件

  1. 全局加強:默認用來實現全局應用加強的邏輯。 它使用 enhanceAppFiles 指定加強全局應用和主題的文件路徑。憑着這個,vuepress 就能準確地找到你全局加強或是主題的文件所在地。web

  2. 佈局組件:默認提供的佈局組件。 它使用 clientDynamicModules 來實現動態引入佈局相關的組件。vue-router

  3. 頁面組件:默認提供的頁面組件(佈局組件的子組件)。 它使用 clientDynamicModules 來實現動態引入頁面相關的組件。npm

  4. 根組件混入:默認往根組件混入的邏輯。 它使用 clientDynamicModules 來實現動態混入元信息。包括根組件的標題、語言等。

  5. 路由:默認的生成路由邏輯。 它使用 clientDynamicModules 來實現動態註冊路由。咱們的 markdown 文件在轉換成 vue 組件後就是經過它自動註冊到 vue-router 的。

  6. 站點數據:默認的生成站點數據邏輯。 它使用 clientDynamicModules 來實現生成全局站點數據。咱們在頁面裏拿到的全局計算屬性 $site 就是這樣來的。

  7. 模塊化轉化:將 cmd 代碼轉成 esm 代碼的邏輯。 仍是用 clientDynamicModules 來實現將 cmd 代碼轉成 esm 代碼。主要是由於 ClientComputedMixin 這個類先後端代碼都要使用。

  8. 樣式加強 全局樣式加強。使用 enhanceAppFiles 和 ready 鉤子來實現(主題樣式+用戶樣式+父主題樣式)。

  9. 樣式覆蓋 全局樣式覆蓋,使用 ready 鉤子來實現,覆蓋 config.styl 和父主題的 palette。

  10. dataBlock數據注入 解析 blockType=data 的數據,使用 chainWebpack 和 enhanceAppFiles 來實現,對 blockType=data 類型的數據注入到 markdown 生成的 vue 組件裏去,每一個組件能夠訪問本身的 $dataBlock 屬性拿到。

官方插件

  1. 活動的標題連接 它會在用戶滾動頁面時自動轉變側邊欄的高亮標題。 它使用了 clientRootMixin 和 define 往根組件混入了滾動邏輯:監聽 onScroll 事件,獲取全部錨點元素並根據滾動距離計算出高亮的錨點。

  2. 回到頂部 使用了 enhanceAppFiles 和 globalUIComponents 註冊了一個全局組件:點擊後能夠滾動到頁面頂部。

  3. 博客

    • 3.1 使用 extendPageData 建立標籤頁和目錄頁
    • 3.2 使用 ready、clientDynamicModules、enhanceAppFile 建立頁面元數據。
  4. ga 谷歌分析站點的庫。使用了 define 和 enhanceAppFiles 初始化了 ga。

  5. 國際化(廢棄) 可讓你的站點擁有切換語言的能力。使用了 enhanceAppFiles 和 additionalPages 註冊了個 I18n 佈局組件。

  6. 文檔的最近更新時間 可讓每一個文檔頁下面顯示最近的 git 提交時間。使用 extendPageData 拓展了 $page 的 lastUpdated 屬性。

  7. 圖片預覽 集成了 medium-zoom。使用了 define、clientRootMixin 往根組件裏混入了 zoom 的初始化和更新邏輯。

  8. 分頁 讓共享側邊菜單欄的文檔擁有分頁切換的能力。使用了 enhanceAppFiles 定義了全部頁面的索引和順序。ready 定義了分頁的規則如排序規則等、clientDynamicModules 生成動態模塊給前端代碼使用。

  9. pwa 集成 service-worker 功能 - 9.1. 使用 ready 開啓 serviceWorker 選項 - 9.2. 使用 alias 實現用 vue 當事件通道 - 9.3. 使用 define、globalUIComponents 註冊更新 PWA 應用按鈕組件 - 9.4. 使用 enhanceAppFiles 注入 register-service-worker 的初始化和更新邏輯 - 9.5. 使用 generated 經過 workbox-build 完成 sw 功能

  10. 註冊全局 Vue 組件 使用 enhanceAppFiles 把一個文件夾中的 vue 組件文件都註冊好。

  11. 搜索框 使用 alias 和 define 讓搜索框能夠動態引入。

  12. 進度條 使用 clientRootMixin 和 enhanceAppFiles 集成 nprogress。

lerna

項目管理上,插件機制也使得原來的一個大項目拆成了 1 + N 的形式,package.json 也變得多了起來,爲了管理這種項目,vuepress 引入了 lerna。

關於 lerna 的知識,有興趣的讀者能夠參考:lerna管理前端packages的最佳實踐

核心實現

當一系列插件要使用時,須要經過 PluginAPI 和組成它的各類 Option 來實現。

總體流程大體以下:

這裏我劃分紅了兩個階段,用虛線分隔,一個是調用前階段,一個是調用後階段。插件們被調用前,是會被載入以及註冊的,以後化整爲零,映射成若干個 Option 實例。

源碼

  1. PluginAPI 類,這部分代碼包含了插件機制中的註冊調用實現。
    • 構造(constructor):初始化選項、插件上下文、插件隊列(可註冊插件列表)、日誌插件、初始化標誌位、插件解析器屬性,而後把選項們都裝載進來(initializeOptions)。這裏會把一個插件映射成若干個 Option 實例。 例如,一個插件只有 ready、chainWebpack、additionalPages 三個選項,則會獲得三個 Option 實例。
    • 使用(use),須要 _initialized 標誌爲 false 才能調用,用於確認哪些插件是能夠被註冊的:
      • 對於非對象類型的插件,會調用 normalizePlugin 方法將之轉成對象
        • 期間會調用 _pluginResolver(ModuleResolver 實例) 來解析模塊
          • 用於解析模塊的 ModuleResolver 類,工做原理相似 webpack 的模塊解析。源碼
          • 這裏值得一提的是 resolve 方法,它支持從非字符串包、npm 包、絕對路徑、相對路徑中解析模塊。
          • 相對路徑的模塊先使用 node 的原生 path.resolve 方法解析獲得絕對路徑,而後交給解析絕對路徑模塊的方法處理。
          • 絕對路徑、非字符串包和 npm 包會用通用模塊 CommonModule 表示。
          • 通用模塊有四個屬性:entry、shortcut、name、fromDep。
        • 還會調用 flattenPlugin 拍平插件,主要是獲取配置。
          • 若是傳入配置是函數,則返回調用後的結果,入參爲插件選項、插件上下文、PluginAPI 實例。
          • 傳入的配置是對象,則返回一個拷貝後的對象。
      • 非 multiple 的插件,會根據插件名字去重。
      • 標準化後的插件,會加入到插件隊列中去。
      • 最後,存在插件中使用插件的狀況時,會調用 useByPluginsConfig 來實現。
        • 這裏面的 normalizePluginsConfig 會將配置格式化成[[p1]、[p2]的形式]。
    • 初始化(initialize):先將 _initialized 標誌位置爲 true,而後註冊全部可用的插件。
      • 在初始化以前,內部插件的使用,會先於用戶的插件。
      • 註冊(applyPlugin):到這裏,插件已經被拆分紅細化的選項,按照信息類(pluginName、shortcut)、鉤子類(ready、compiled 等)、其餘類(chainWebpack、chainMarkdown、enhanceAppFiles 等)按順序鏈式註冊(registerOption)。
        此時,一個 Option 實例中已經承載了若干個插件的邏輯了。
    • enabledPlugins 和 disabledPlugins 兩個只讀屬性能夠取啓用(可註冊)或禁用(不可註冊)的插件列表。
    • getOption 能夠取具體的一個選項實例,applyAsyncOption 和 applySyncOption 分別應用異步選項和同步選項中的邏輯(回調函數)。

選項和異步選項,插件的本體

  1. Option 類 - 每一個實例初始化 key(選項標識) 和 items(這個選項所對應的函數們) 屬性。

    • 重要方法:syncApply(也叫 apply),對以前保存在實例中的 items 遍歷調用 add 方法,若是 item 中的值是函數,則執行之取其返回值。
    • 在插件應用選項時若是匹配成功,會調用 add 方法將選項映射成 1-n 個對象推入 items 屬性裏。
    • 除了 add 還有 delete 和 clear 方法,不作贅述。(增刪清)
    • 另外有 values、entries 和 appliedValues 三個只讀屬性,用於獲取值、實體、已應用的值。
    • 管道方法(pipeline),它將實例的 values 屬性柯里化成一個組合函數,依次執行。
  2. AsyncOption 類

    • asyncApply 異步版syncApply,調用函數的時候使用了 await。
    • parallelApply 若是說 pipeline 是串行,它就是並行:使用了 Promise.all
    • pipeline 同理,調用函數的時候使用了 await。

特殊選項

  1. EnhanceAppFilesOption、ClientDynamicModulesOption、GlobalUIComponentsOption、DefineOption、AliasOption 類
    • AliasOption
      • 在建立 webpack 配置的時候調用
      • 重寫 apply 方法:先調用 syncApply,而後將 appliedValues 取出,設置爲 webpack 的 alias
    • ClientDynamicModulesOption
      • 在 prepare 階段調用
      • 重寫 apply 方法:從 appliedItems 取出應用的插件信息,遍歷寫入文件以待使用
    • DefineOption
      • 相似 AliasOption,只不過是設置 webpack 的全局變量
      • 最後在 injections 插件(DefinePlugin)觸發時收集選項將 define 注入進去
    • EnhanceAppFilesOption
      • 在 prepare 階段調用
      • 重寫 apply 方法:從 appliedItems 取出插件信息,生成引入模塊或者註冊組件的代碼文件
    • GlobalUIComponentsOption
      • 相似 ClientDynamicModulesOption,寫全局 ui 組件文件

調用函數型 Option 時機

  1. extendCli 建立 cli 命令時
  2. chainMarkdown 和 extendMarkdown 建立 MarkdownIt 實例時
  3. additionalPages 解析完全部頁面後 三、extendPageData additionalPages 執行完以後,依賴 additionalPages 執行完的結果
  4. ready 緊跟 additionalPages 以後
  5. clientDynamicModules、enhanceAppFiles、globalUIComponents 緊跟 ready 以後
  6. define、alias
    建立公共 webpack 配置後
  7. chainWebpack
    建立 dev webpack 配置後、建立 build webpack 配置後
  8. beforeDevServer
    webpack-dev-server 的 before 選項執行後
  9. afterDevServer webpack-dev-server 的 after 選項執行後
  10. generated build 完成後
  11. updated 文件更新後
  12. clientRootMixin
    clientDynamicModules 選項執行時

編寫一個 vuepress 插件

我也寫了一個小插件,它能夠將你的 vuepress 站點下載成一個 pdf 文件:vuepress-plugin-export-site

源碼

  1. 使用 ready 選項
  2. 藉助 puppeteer 和 easy-pdf-merge 實現:從上下文中拿到路由信息,而後使用 puppeteer 遍歷訪問並下載,最後合併成一個大 PDF。
    • 由於須要下載 chromium,因此國內網絡受限。咱們換成了 puppeteer-cn。
    • easy-pdf-merge 若是在 windows 下運行須要指定 jar 環境變量。

後記

咱們熟悉的 webpack、vue 也有插件系統,它們都有兩個共同的特色:

  1. 提供一個功能擴展點,讓插件可以去擴展它。
  2. 提供一個功能註冊功能,讓插件註冊進來。

其實插件機制也能夠看作設計模式的一種體現:抽離出變化的部分,保留不變的部分。這些變化的部分,即可以稱之爲插件。

在咱們造輪子的時候,若是輪子的功能愈來愈多,代碼愈來愈臃腫的話,引入插件機制會讓後續的開發更加靈活。

最後,幫插件機制的開發者真山同窗宣傳一下,屆時會有更加精彩的 vuepress 分享:

相關文章
相關標籤/搜索