San CLI 的實現原理

導讀:上期咱們討論了 San CLI 的使用,這期咱們再深刻一點,來看看 San CLI 的實現原理。node

1、核心模塊和核心概念

爲了方便理解下文的 San CLI 的總體工做流程(主流程),咱們先來看下 San CLI 的核心模塊和核心概念。webpack

1. 核心模塊

San CLI 的核心模塊包括:git

  • san-cli:負責 San CLI 的主流程和實現核心功能;
  • san-cli-service:Service 層,負責 Service 流程;
  • san-cli-command-init:實現 san init 命令的 Command 插件;
  • san-cli-plugin-*:Service 插件;
  • san-cli-utils:工具庫,在插件中也能夠直接使用;
  • san-cli-webpack:webpack build 和 dev-server 的通用邏輯,webpack 自研插件等。

2. 核心概念

核心概念主要有流程插件。其中,流程又分爲主流程和 Service 流程,插件又分爲 Command 插件 和 Service 插件:github

  • 流程:San CLI 的流程分爲兩段,主流程和 Service 流程:
  • 主流程san-cli/index.js 的流程,是整個 San CLI 的工做流程。當咱們在命令行輸入 San CLI 相關的命令後,好比 san initsan serve(對應 npm start) 和 san build(對應 npm run build),就會進入主流程,主流程會執行對應命令的 handler。有的命令的 handler 不直接包含處理邏輯,而是引入 san-cli-service,好比 san servesan build,輸入這類命令時,就會從主流程進入 Service 流程。
  • Service 流程san-cli-service/Service.js 的流程,主要處理 Webpack 構建相關的邏輯。
  • 插件:用於擴展 San CLI 的功能:
  • Command 插件:用於主流程,執行 Command 插件定義的命令後主流程就會執行對應的 Command 插件的 hanlder
  • Service 插件:用於 Service 流程,處理 Webpack 構建相關的邏輯。

2、總體工做流程

San CLI 的總體工做流程,即主流程,在 san-cli/index.js 中,大體以下:web

  1. 檢查 node 版本;
  2. 檢查 san-cli 版本;
  3. 調用 san-cli/lib/Commander.js 建立命令實例:
  4. 添加全局 option;
  5. 添加中間件:
  6. 設置全局 logLevel;
  7. 設置 NODE_ENV 環境變量;
  8. argv 添加日誌等屬性和方法。
  9. 加載內置命令: initservebuildinspectui 等;
  10. 加載 package.jsonsanrc.json 裏聲明的預置命令(自定義命令);
  11. sanrc.json是配置San CLI的文件,和san.config.js不一樣,後者是配置項目的文件,詳見San CLI官方文檔。
  12. 觸發命令的 hanlder,開始 San CLI 的正式執行。

結合核心模塊的主流程以下圖所示:npm

3、san init

san init命令用於初始化項目,用法在上期已有所介紹,這期咱們來看該命令是怎麼作到初始化一個項目的。json

1. 流程

san init初始化項目的原理,簡單來講,就是經過 git 命令遠程拉取項目腳手架模板的代碼庫到本地,或者直接使用本地的項目腳手架模板的代碼庫,而後使用 vinyl-fs 將拉取到的代碼庫的文件依次處理,處理完成就獲得了一個初始化好的項目。gulp

vinyl-fs是gulp的核心。設計模式

san init主要由四步串行任務組成:數組

  1. 檢查目錄和離線包狀態:檢查項目腳手架模板的本地路徑和離線包是否可用;
  2. 下載項目腳手架模板:從 Github 等遠程倉庫下載項目腳手架模板到模板緩存目錄;
  3. 生成項目目錄結構:使用 vinyl-fs 把項目腳手架模板從緩存目錄遍歷處理到開發者指定的項目目錄;
  4. 安裝項目依賴:詢問開發者是否安裝 package.json 裏的依賴。

對應的流程圖以下圖所示:

其中,檢查目錄和離線包狀態的流程圖以下:

2. 設計思路

san init的具體實如今san-cli-command-init模塊中,san-cli-command-init 模塊是一個 Command 插件,其核心是一個TaskList類,san init的執行過程的本質就是:傳入上述4個任務組成的數組來實例化TaskList並調用實例的run方法。

咱們來看下 TaskList 的內部實現。

當調用 TaskList 實例的 run 方法時,首先會對執行實例化 TaskList 時傳入的任務列表的第一個任務進行處理。

由於這些任務本質上都是一個個函數,因此先給第一個任務加上一些方法,好比表示這個任務已完成的 complete 方法,加完方法後,就調用第一個任務函數。

調用第一個任務函數,就意味着第一個任務開始執行了,當第一個任務該作的事情都作完後,最後就會在這個任務函數中調用以前給這個任務函數加上的 complete 方法。

complete方法就作一件事情,讓 TaskList 實例開始處理下一個任務,處理方式和上面說的同樣,只是簡單地重複。

最後, TaskList 實例發現沒有下一個任務了,就收工了。

3. TaskList 源碼簡化版

咱們用簡化版的源碼來看下 TaskList 的使用和實現。

TaskList的使用:

checkStatus 爲例,看下任務函數的實現:

TaskList 的實現:

咱們能夠看出, san init 的設計模式仍是比較優秀的,舉個栗子,若是咱們想給 san init 添加多一個任務,那隻須要關注該任務自己的實現,實現好後放入實例化 TaskList 時傳入的任務列表中,就能夠了,可擴展性很是好。

4、插件機制

San CLI 的插件分爲 Command 插件和 Service 插件,在上期咱們以實際例子討論了具體怎麼開發一個 Command 插件或 Service 插件了,這期咱們就來看看 San CLI 的插件機制。

1. Command 插件

Command 插件相對 Service 插件來講,機制比較簡單。

Command 插件實際是 yargs 的插件系統的擴展,yargs 是一個 npm 包,用它咱們能夠定義咱們本身的命令行命令。

回顧下咱們在上期建立的 Command 插件:

之因此要這麼寫,是由於這是 yargs 對定義一個命令的要求。定義好命令後,就在項目的 package.json 裏聲明這個命令。

當咱們執行任何一個 san 的命令時——注意,是任何一個——在真正執行這個命令以前,San CLI 會先去讀取 package.json 裏聲明的命令,而後找到命令的定義並傳入 yargs,此時,yargs 就知道了都有些什麼命令,在此以後,San CLI 才把咱們執行的命令的名字和參數傳如 yargs,yargs 拿到命令的名字和參數後,就回去執行對應命令的 hanlder。

Command 插件的機制就是這樣。

另外還值得注意的是,在 san-cli/lib/commander.js 裏定義了一個名爲 Command 的類,這個類對 yargs 插件作了一些定製,好比經過中間件機制添加了經常使用的方法和屬性到 argv 對象中,方便下游 handler 直接使用。

2. Service 插件

San CLI 的 Service 插件機制借鑑了 Vue CLI 的 Service 插件機制,但有一些不一樣之處:

  • Vue CLI 註冊一個新命令是經過 Service 插件來完成的,具體是使用 Service 插件的 registerCommandAPI 方法實現;而 San CLI 把註冊一個新命令的邏輯從 Service 插件裏分離了出來,成爲了一個獨立的部分,也就是前面介紹過的 Command 插件。
  • Vue CLI 的一個命令對應一個或多個 Service 插件,也就是說,一個命令的實現由一個或多個 Service 插件來完成;而 San CLI 的一個命令對應零個或全部 Service 插件(引入的),一個命令對應零個的狀況是這個命令是一個純的 Command 插件,一個命令對應全部 Service 插件的狀況是這個命令在它對應的 Command 插件的邏輯裏觸發了 Service 流程,而 Service 流程會依次註冊並執行全部的 Service 插件。

下面咱們會以 san serve 命令爲例,分別看下 Service 流程、Service 插件的設計思路和  Service  類的簡化版源碼。

1)Service 流程

Service 流程,即 Service 的整個工做流程:

  1. San CLI 在主流程解析輸入命令行的 san serve 命令,進入 san-cli/commands/servehandler
  2. san serve 命令的 handler 主要是實例化 Service,實例化會將配置項和 Service 插件進行處理;
  3. 執行 service.run(callback),進入 Service 流程,Service 流程的實現主要在 service.run 中:
  4. loadEnv:加載 env 文件;
  5. loadProjectOptions:加載 san.config.js
  6. init:service 啓動:
  7. 初始化插件,即依次執行插件;
  8. 依次執行 webpackChain 回調棧;
  9. 依次執行 webpackConfig 回調棧;
  10. 執行 callback

對應的流程圖以下:

咱們自定義的 Service 插件的具體執行時機是在 3-1-1 「初始化插件,即依次執行插件」 這一步,對應上圖中的 「初始化插件(插件.apply)」 這一步。

上圖中的 「初始化 plugin 變量並加載傳入的 plugin」 這一步和 「加載 config 中 plugins 裏設置的插件」 這一步,其實都是在加載 Service 插件,只不過前者是在加載內置插件和 sanrc.json 裏預設的插件,然後者主要是在加載 san.config.json 裏的插件。

加載 Service 插件的流程圖以下:

2)設計思路

輸入 san serve 命令,觸發對應的 handlerhandler 主要就作兩件事情:一是實例化 Service,二是調用 Service 實例的 run 方法。

實例化 Service 時,會加載內置 Service 插件和 sanrc.json 裏預設的 Service 插件。若是咱們自定義的 Service 插件預設在了 sanrc.json 裏,好比上期的 san-cli-plugin-get-entry,這個時候就會被加載了。

實例化完 Service,就調用 Service 實例的 run 方法。

調用 run 方法時,首先會加載 san.config.js 裏的 Service 插件,固然也包括咱們放在 san.config.js 裏的自定義 Service 插件;而後,該加載的 Service 插件都加載完了,這時就準備依次執行它們了。

在執行每一個 Service 插件以前,會先實例化 PluginAPIPluginAPI 實例給 Service 插件提供了用於處理 Webpack 構建相關邏輯的方法,好比 configWebpack,經過這個方法咱們能夠在 Service 插件裏獲取和修改 Webpack 配置,好比在上期咱們寫的 Service 插件示例裏,就用這個方法獲取了網站的入口文件名。

最後,把 PluginAPI 實例做爲入參來調用 Service 插件定義的 apply 函數,就正式開始了 Service 插件的執行。

3)Service 源碼簡化版

Service的使用:

Service 的實現:

PluginAPI 的實現:

5、最後

感謝你閱讀到了這裏,以上即是《San CLI 的實現原理》的所有內容。若是你都看懂了,請收下個人膝蓋:

2021年,San-CLI還會有持續的開發和優化,好比eject功能、CLI和Service要不要分離,想了解後續的更新,能夠關注San CLI的GitHub,歡迎star、歡迎issues、歡迎pr。

地址:https://github.com/ecomfe/san-cli

原文連接:https://mp.weixin.qq.com/s/-yhs_86CAMnsCxIYwrmMeQ


百度架構師

百度官方技術公衆號上線啦!

技術乾貨 · 行業資訊 · 線上沙龍 · 行業大會

招聘信息 · 內推信息 · 技術書籍 · 百度周邊

歡迎各位同窗關注!

相關文章
相關標籤/搜索