淺談 vue-cli 擴充套件性和外掛設計

荒山發表於2019-05-28

這是一個新開的'實驗性'文章系列,如其名‘技術地圖’,這個系列計劃剖析一些前端開源專案,可能會探討這些專案的設計和組織、整理他們使用到技術棧。 首先拿vue-cli小試牛刀,再決定後續要不要繼續這個系列.


我一直在思考我們程式設計主要在做什麼?我們有一大部分工作就是選擇各種工具/庫/框架,來黏合業務. 工具和場景越匹配、原理了解越多,運用越嫻熟,我們效率可能就越高. 這種說法很有爭議,就像程式=演算法+資料結構不能完全表達現今的軟體工程一樣, 說我們的工作就是堆砌工具,黏合業務, 一定程度上有自貶的意思。 但這確實是大部分程式設計師的真實寫照。

這系列文章其實有點類似於 github 上面的Awesome專案. 這些 Awesome 專案就是一個生態展覽館, 裡面專案琳琅滿目. 因為數量太多了,而且缺少評分機制,大部分情況我們不可能一個個去檢視,很難從中選擇符合需求的專案(當然你帶著明確的目的,且目標範圍非常小,可能比較有用)。

淺談 vue-cli 擴充套件性和外掛設計

是否可以嘗試換個角度,選取一些有趣的開源專案,看看它是怎麼應用這些工具的, 有序的羅列出來? 對於有相同場景的專案, 參考或者模仿價值可能會更大一些. 這些開源專案就是巨人,站在巨人肩膀上顯然省事多了

只是技術棧羅列未免過於簡單,筆者還希望從這些專案中學點東西,比如他的設計和專案組織. 我會嘗試簡化和通俗解釋裡面的關鍵知識或亮點, 但是不求甚解。為了避免陷入細節泥潭,我會盡量使用圖形化方式展示他們程式流程,避免拘泥於細節。你也可以把這些文章作為深入閱讀這些專案原始碼的引導

2019.10.23 技術地圖系列失敗

我也希望讀者同我交流反饋,共同學習和進步。




vue-cli

說到 CLI, 不得不提Rails框架,它可能是框架提供 CLI 的先祖(具體歷史沒有深入考究). Rails 有一個重要的指導思想,即約定大於配置, 它為 Web 應用的大多數需求都提供了最好的解決方法,並且預設使用這些約定,而不是在長長的配置檔案中設定每個細節

CLI 也是這個指導思想下的產物, 例如通過它提供的 CLI,可以在15 分鐘內構建一個簡易的部落格, 可以通過 CLI 啟動伺服器和 REPL、生成專案腳手架、生成程式碼檔案、路由、資料庫遷移等等:

淺談 vue-cli 擴充套件性和外掛設計

Rails 的很多設計在那個年代就是就是一個明星(閃瞎 PHP、JSP、 ASP..., 想想要配置各種伺服器,各種 xml 檔案),它的很多設計模式深刻影響了後面的 web 框架,比如 Django、Laravel, 甚至很多模仿 Rails 命名的,如 Sails、Grails.

Rails 對於前端開發影響也很深遠,比如在 Nodejs 出來之前,Rails 社群就開始使用 coffeescript + sass預編譯語言進行前端開發了, Asset Pipeline可以說是最早的'前端工程化', 配合Turbolink可以讓傳統後端渲染頁面擁有不亞於單頁應用的使用者體驗...

當初 Rails 給我帶來的各種震撼還歷歷在目, Ruby China 社群也是國內最好社群之一. 但是目前 Rails 的關注度不如從前, 在前端社群像 Rails 這種集大成的框架也早已不吃香(參考 Ember, 某種程度上 Angular 也算吧?).

說實在話如果一生只學一門語言,我會選 Ruby,如果選一個 web 框架,那就是 Rails。

推薦大家閱讀The Rails Doctrine - Rails 信條 這篇文章裡面有一句話筆者非常喜歡: "只要放下了自負的個人喜好,便可以跳過無謂的世俗決定,專注在最重要的地方下更快的決定。"。為人寫程式,而不是為了機器寫程式.

約定大於配置可以減少我們做決定的數量,減少無謂的爭論和考慮,讓我們可以專注於更重要的事情. 這個原則可以提高開發和團隊協作效率, 甚至可以凝聚一個社群.

以 Webpack 為例,噁心複雜的配置被人詬病,所以才需要 vue-cli 或者 create-react-app 這些工具.

沒有用 Ruby/Rails 工作過, 默默寫了個 Ruby China 小程式(微信搜Ruby CN),算是感恩回饋社群吧


Ok, 忍不住吹了一波 Rails, 回到正題.

筆者是使用 React 作為主力開發的,Vue 也是我非常喜歡的一個開源專案,不說別的,在開發者的'使用者體驗'方面 Vue 是我見過最好之一,主要體現在 API 的簡潔性和易用性、文件還有專案構建工具(今天的主角).

vue-cli-ui 是我想寫這系列文章的動機之一. 前陣子用了一下vue-cli-ui, 感覺很不錯, 支援視覺化配置和任務執行,比我在終端下一個專案一個專案跑 task 清爽多了. 很想在我們自家的構建工具上也搞一套,怎搞? 學習它的原始碼, 我覺得可以作為部落格記錄下來.

現在前端工程師也有‘webpack 配置工程師’的戲稱,這能說明 webpack 配置是費時費力的苦事(Angular 例外). 這不後來就有了parcel宣稱零配置的輪子, 還有 React 社群的create-react-app, vue-cli 前期是基於模板的建立專案, 不算此列。

後來 vue-cli 汲取著前者的很多優點,把這塊做大做優了(看來 vue 很擅長做這些事情). 我們可以來對比一下這些工具:


Vue CLI create-react-app parcel
快速原型開發 支援 - 支援
全域性模式 零配置原型開發就是全域性的 - 支援
外掛 支援 - 支援,擴充套件檔案型別和檔案輸出
擴充套件性 強,通過外掛擴充套件 wepack 配置 弱, 強約定, 無法配置 webpack,可以 eject, 然後手工配置;支援 babel-macro;(嚴格說可以通過react-app-rewired進行擴充套件) 中(可以配置 babel,postcss,Typescript); 提供了 Node API; 支援外掛擴充套件檔案型別
多頁面 支援 - 支援
適用範圍 Vue 元件的第一公民。通過擴充套件可以支援任意前端框架 針對 React 開發,不支援其他框架 parcel 是一個通用的打包工具,它的競爭對手是 webpack
編譯速度 cache-loader,thread-loader 來加速 JS 和 TS 編譯 babel-loader 開啟了 cache 編譯速度號稱是 webpack 的兩倍
可升級性 支援升級 cli-service, 外掛需要單獨升級, 外掛需要遵循語義化版本. 太多外掛存在升級風險 支援升級 react-script, 官方維護,且強約定基本可以保障向下相容 支援升級 parcel-bundler
UI 圖形化管理是 CLI 的特色之一 - -

通過上面的對比,可以看出 vue-cli 是一個擴充套件性非常強的構建工具,以致於它不僅限於 Vue,也可以用來構建 React 甚至其他前端框架

相比而言 create-react-app 就是一個非常 Opinionated(堅持己見) 的工具,強約定. 一個典型的例子就是它不內建開啟 babel 裝飾器轉譯,CRA 團隊認為已經廢棄(或者不成熟)的語言特性不應該帶到 CRA 中; 後面為了給‘優雅’地給 babel 擴充套件外掛,就搗鼓出來了babel-macro, 這是一種'免配置'的 babel 外掛規範.

這種強約定也是有好處的,比如不需要管理配置; 而且 CRA 團隊謹慎可靠地維護著 CRA,這使得開發者可以一般無痛地升級 CRA. 如果要擴充套件 webpack,一般只有 eject,這就走回了手動配置 webpack 的老路, 不可取.

vue-cli 也是一個'漸進式'的 cli,vue-cli 提供了預設的 preset,但不阻止你對其進行擴充套件. vue-cli 的擴充套件介面也非常簡潔(合理, 不多不少), 還有 UI 管理介面,視覺化管理專案的配置和外掛,使用者體驗很棒,計劃在下一篇文章介紹 vue ui. 唯一比較不舒服的是如果濫用這種擴充套件性,裝 N 多外掛,而且外掛之間還存在依賴關係時,也會成為升級維護的負擔.




基本設計

注意,本文不是 vue-cli 的教程,最好的教程是官方文件.


目錄結構

下面是 vue-cli 的基本目錄結構. 大部分大型的前端專案都使用 lerna 實現 mono-repo 模式, 然後統一分發到 npm. 這種模式有利於專案模組組織

淺談 vue-cli 擴充套件性和外掛設計

分離 CLI 層和 Service 層

這個設計是借鑑create-react-app的, CLI 層只是一些基礎的命令一般不需要頻繁升級,而且是全域性安裝; 而 Service 層是多變的, 作為專案的區域性依賴,不應該硬編碼在 CLI 裡面. CLI 和 Service 的職責劃分如下:


  • CLI: 用於專案建立和管理

    • 全域性安裝
    • vue create 建立專案腳手架. 拉取最新的 Service,並選擇配置需要的外掛
    • vue ui. 啟動 UI 管理介面
    • 快速原型開發: vue serve | vue build, 直接伺服和編譯一個 Vue 檔案
    • 外掛管理: vue add | vue invoke 安裝外掛和呼叫外掛生成器
  • Service: 負責專案的實際構建

    • 區域性安裝
    • 整合 webpack 構建環境,Service 本身只有一個外掛機制, 所有構建相關邏輯都由內建外掛和外部外掛提供
    • 內建外掛(命令): serve, build, inspect


外掛系統

vue-cli 提供了類似 babel、eslint 的外掛機制。

淺談 vue-cli 擴充套件性和外掛設計

外掛

外掛機制是 vue-cli 的核心, 用於擴充套件 Service. Service 的命令和 webpack 配置都由外掛提供.

其實外掛機制本身並沒有什麼技術難度, 換句話說外掛其實就是一個協議的設計. vue-cli 外掛的協議如下:

  • 命名: @vue/cli-plugin-*vue-cli-plugin-*. package.json 中按著這個命名約定的依賴會被識別為 vue-cli 外掛,另外命名約定也有利於在 github 或 npm 上篩選

  • 生命週期: 一個外掛的生命週期可以分為安裝階段執行階段. vue create命令建立專案腳手架、vue add以及vue invoke外掛安裝命令都屬於安裝階段; 而 cli-service 命令執行時屬於執行階段.

  • 基本結構: 區分了生命週期後,外掛的結構就比較清晰了:

    .
    ├── README.md
    ├── generator.js  # generator (可選)
    ├── prompts.js    # prompt 檔案 (可選)
    ├── index.js      # service 外掛
    └── package.json
    複製程式碼
    • 安裝階段:
      • prompts: 收集使用者意見和配置
      • gernerator: 在安裝階段生成模板檔案
    • 執行時: index.js
      • 注入 service 命令
      • 擴充套件和修改 webpack 配置. vue-cli 通過webpack-chainwebpack-merge來實現 webpack 可配置化

一個簡單的外掛結構是這樣子的:

淺談 vue-cli 擴充套件性和外掛設計

preset

這個 preset 和 babel 的 preset 概念實際上是不一樣的:

vue-cli 的 preset 一個腳手架建立方案, 也就是說它只作用於vue create階段。比如vue create時預設使用的就是 babel+eslint preset. preset 可以簡化專案腳手架的建立。團隊可以共享一個 preset 來建立腳手架

而 babel 中的 preset 是一個外掛集合,他可以統一收納和管理一組外掛方案. 例如babel-preset-reactbabel-preset-env. 上文說到如果擴充套件性被濫用,裝 N 多外掛,而且外掛之間還存在依賴關係時,也會成為升級維護的負擔. 而 'babel 式'的 preset 可以讓外掛更方便維護和和一鍵式升級

儘管目前 vue 也提供了vue upgrade對外掛進行升級,這個是基於語義化版本約定的, 且當外掛之間存在依賴關係時, 不排除升級存在風險. 尤其對於團隊專案還是推薦有統一地管理這些外掛, 實現傻瓜化的升級。 實際上這種 'babel 式'的 preset 是可以通過 vue-plugin 實現和轉發的。


配置

vue 支援在 package.json 的 vue 欄位或vue.config.js中進行配置。這裡可以對 Service 核心功能和外掛進行配置, 也可以直接修改 webpack 配置. 另外部分構建行為是通過環境變數進行影響的,這些可以通過.env.*檔案進行配置


基本流程

現在來看看一個 vue-cli 內部的基本流程, Service 的外掛實現是 vue-cli 比較有意思的點. 以vue serve為例:

淺談 vue-cli 擴充套件性和外掛設計

Service 物件是 vue-cli 的核心物件,負責管理和應用外掛,所有命令和 webpack 配置都是以外掛的形式存在:

淺談 vue-cli 擴充套件性和外掛設計

首先劃分為配置階段和執行階段。 配置階段 vue-cli 會載入配置檔案,並查詢和應用所有外掛。將 PluginAPI 例項和專案配置傳遞給外掛執行時, 外掛執行時通過 PluginAPI 注入命令(registerCommand)和 擴充套件 webpack 配置(chainWebpack, configureWebpack).

執行階段則根據使用者傳入的命令名呼叫外掛注入命令。在命令實現函式中,可以呼叫 resolveWebpackConfig()來生成最終的 webpack 配置。以 serve 命令為例,獲取到 webpackConfig 後會建立一個 webpack 編譯器,並開啟 webpack-dev-server 開發伺服器.


技術地圖

  • 組織
  • cli 命令列相關工具
    • chalk: 命令列字型顏色樣式
    • cli-highlight: 終端語法高亮輸出, 類似於 Highlight.js
    • cliui: 在終端中進行多列輸出
    • didyoumean: 根據單詞相似度,來對使用者輸入糾正提示
    • semver: 提供語義化版本號相關的工具函式。 例如比較,規範化
    • commander TJ 寫的命令列選項和引數解析器,支援子命令,選項校驗和型別轉換,幫組資訊生成等等. API 簡單優雅
    • minimist: 一個極簡的命令列引數解析器。如果只是簡單的選項解析,可以用這個庫
    • inquirer 命令列詢問
    • ora 命令列 spinner
    • launch-editor 開啟編輯器. 通過 node 開啟編輯器,前端可以 express 暴露介面呼叫開啟
    • open 開啟 URL、檔案、可執行檔案
    • execa 更好的 child_process,修復了原生 exec 的一些問題
    • validate-npm-package-name: 驗證 npm 包名稱,比如建立的專案名是否合法
    • dotenv & dotenv-expand: 從.env 檔案中載入配置,環境變數
  • 網路相關
    • portfinder: 獲取可用的埠
    • address: 獲取當前主機的 ip,MAC 和 DNS 伺服器
  • 檔案處理相關
    • slash 一致化處理路徑中的分隔符
    • fs-extra node fs 模組擴充套件
    • globby: glob 模式匹配
    • rimraf 跨平臺檔案刪除命令
    • memfs 相容 Node fs API 的記憶體檔案系統
  • 資料檢驗
  • 除錯
    • debug: 這是一個 debug 日誌利器, 支援通過環境變數或動態設定來確定是否需要輸出; 支援 printf 風格格式化
  • 演算法
    • hash-sum: 雜湊值計算
    • deepmerge 深合併
  • 其他
    • recast Javascript 語法樹轉換器,支援非破壞性的格式化輸出. 常用於擴充套件 js 程式碼
    • javascript-stringify: 類似於 JSON.stringify, 將物件字串化。
  • webpack
    • 配置定義
      • webpack-merge: 合併 webpack 配置物件
      • webpack-chain: 鏈式配置 webpack. 這兩個庫是 vue-cli 外掛的重要成員
    • webpack-dev-server: webpack 開發伺服器,支援程式碼熱過載,錯誤資訊展示,介面代理等等
    • webpack-bundle-analyzer: webpack 包分析器
  • 擴充套件(一些相關的技術棧)
    • http-server 快速伺服靜態檔案
    • plop 模板生成器
    • yeoman 專案腳手架工具

相關文章