這是一個新開的'實驗性'文章系列,如其名‘技術地圖’,這個系列計劃剖析一些前端開源專案,可能會探討這些專案的設計和組織、整理他們使用到技術棧。 首先拿vue-cli
小試牛刀,再決定後續要不要繼續這個系列.
我一直在思考我們程式設計主要在做什麼?我們有一大部分工作就是選擇各種工具/庫/框架,來黏合業務. 工具和場景越匹配、原理了解越多,運用越嫻熟,我們效率可能就越高. 這種說法很有爭議,就像程式=演算法+資料結構
不能完全表達現今的軟體工程一樣, 說我們的工作就是堆砌工具,黏合業務, 一定程度上有自貶的意思。 但這確實是大部分程式設計師的真實寫照。
這系列文章其實有點類似於 github 上面的Awesome
專案. 這些 Awesome 專案就是一個生態展覽館, 裡面專案琳琅滿目. 因為數量太多了,而且缺少評分機制,大部分情況我們不可能一個個去檢視,很難從中選擇符合需求的專案(當然你帶著明確的目的,且目標範圍非常小,可能比較有用)。
是否可以嘗試換個角度,選取一些有趣的開源專案,看看它是怎麼應用這些工具的, 有序的羅列出來? 對於有相同場景的專案, 參考或者模仿價值可能會更大一些. 這些開源專案就是巨人,站在巨人肩膀上顯然省事多了
只是技術棧羅列未免過於簡單,筆者還希望從這些專案中學點東西,比如他的設計和專案組織. 我會嘗試簡化和通俗解釋裡面的關鍵知識或亮點, 但是不求甚解。為了避免陷入細節泥潭,我會盡量使用圖形化方式展示他們程式流程,避免拘泥於細節。你也可以把這些文章作為深入閱讀這些專案原始碼的引導
2019.10.23 技術地圖系列失敗
我也希望讀者同我交流反饋,共同學習和進步。
vue-cli
說到 CLI, 不得不提Rails框架,它可能是框架提供 CLI 的先祖(具體歷史沒有深入考究). Rails 有一個重要的指導思想,即約定大於配置, 它為 Web 應用的大多數需求都提供了最好的解決方法,並且預設使用這些約定,而不是在長長的配置檔案中設定每個細節。
CLI 也是這個指導思想下的產物, 例如通過它提供的 CLI,可以在15 分鐘內構建一個簡易的部落格, 可以通過 CLI 啟動伺服器和 REPL、生成專案腳手架、生成程式碼檔案、路由、資料庫遷移等等:
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. 這種模式有利於專案模組組織
分離 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 的核心, 用於擴充套件 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-chain
和webpack-merge
來實現 webpack 可配置化
- 安裝階段:
一個簡單的外掛結構是這樣子的:
preset
這個 preset 和 babel 的 preset 概念實際上是不一樣的:
vue-cli 的 preset 一個腳手架建立方案, 也就是說它只作用於vue create
階段。比如vue create
時預設使用的就是 babel+eslint preset. preset 可以簡化專案腳手架的建立。團隊可以共享一個 preset 來建立腳手架。
而 babel 中的 preset 是一個外掛集合,他可以統一收納和管理一組外掛方案. 例如babel-preset-react
、 babel-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
為例:
Service 物件是 vue-cli 的核心物件,負責管理和應用外掛,所有命令和 webpack 配置都是以外掛的形式存在:
首先劃分為配置階段和執行階段。 配置階段 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 伺服器
- 檔案處理相關
- 資料檢驗
- @hapi/joi JSON schema 校驗
- 除錯
- 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 專案腳手架工具