Vue CLI 原理與實踐
理解 Vue CLI 的目的
Vue CLI 架構上的一些設計,值得前端開發者去了解和學習。
前端開發過程中用到的很多的工具,基本上都是基於類似的架構去設計。
通過理解 Vue CLI 的原理和實踐,瞭解一個可擴充套件性、適應能力更強的工具是怎樣去設計和架構的。
vue-cli
早期面貌
Vue CLI 3 之前的模組是 vue-cli
,npmjs 上已經被標記為棄用。
近幾年開發都使用的 @vue/cli
。
@vue/cli
也整合了早期 vue-cli
的功能,需要安裝 @vue/cli-init
,內部還是依賴 vue-cli@2.9.6
這個模組。
npm install -g @vue/cli-init
# vue init [模板名稱] [專案名稱]
vue init webpack old-vue-cli
不僅僅可以建立 Vue.js 專案
早期的 vue-cli
的實現方式是基於模板建立專案。
- 從 github(vuejs-templates) 下載模板
- 詢問一系列配置問題
- 把模板中的檔案經過渲染,替換一些詢問後的內容
- 輸出到目標目錄,並安裝依賴
根據不同的模板可以建立不同的專案,所以早期的 vue-cli
並不僅僅給 Vue.js 專案用的,還可以建立其他各種型別的專案,只需要找到對應的模板。
只是一個「單純」的腳手架工具
vue-cli
只是一個「單純」的腳手架工具
腳手架只是建立專案的一個工具。
它作為前端工程化的起始點,就是用來自動建立專案所需要的一個必需的結構。
建立完結構之後,腳手架就和專案沒有什麼關係了。
「單純」腳手架的不足
「單純」腳手架 的不足就是用完即丟。
建立專案的過程倒沒有什麼。
核心是在於建立完專案後,把所需要用到的一系列配置,例如 webpack、babel等,全部以原始檔案的方式存在了專案裡面。
如果某個模組過時了,或者模組中使用的 API 已經刪除了,開發者就需要維護這些配置中細節上的變化。
維護這些不屬於業務範圍的程式碼,對開發者的維護成本都是一種挑戰。
解決思路:把這些繁瑣的工具和配置裝進一個「黑盒子」,把它交出去,不再由自身去維護。
就是把與框架相關的公共的配置和依賴的工具,全部抽象,封裝成一個個的模組,一方維護,多方共用。
Vue CLI 升級後,就是一個黑盒子,由 Vue 專門的團隊負責Vue CLI 工具鏈的維護。
目前主流的框架都採用這種方案提供自己的開發工具鏈:
- @vue/cli
- @angular/cli
- create-react-app
create-react-app 不足
create-react-app 相比其他兩個框架不足的是,要麼完全聽它的,要麼完全自己配置。
它安裝一個 react-scripts
的模組,所有的配置都包含在其中。
例如,如果想要自定義 webpack 的配置或一些優化,就要把它的配置 eject
出來。
執行eject
命令時會警告,這是一個不可逆的操作。
執行後,會發現專案中多出了很多檔案,最核心的就是一些配置和構建命令。
其實就是回到了早期 vue-cli
建立的專案的樣子,所有的配置都在專案本地,需要自己維護。
對於一些想自定義一些配置,但又不想影響其他配置的開發者來說就不太友好。
相比下來,Vue CLI 幾乎可以做到完全可配置,Angular CLI 也是類似。
不過 React 也可以使用 react-app-rewired
來解決。
Vue CLI 工具鏈
新版 Vue CLI 已經不僅僅是一個腳手架,它整體是一個工具鏈,裡面包含了基於 Vue 開發的所有功能。
相當於提供了 Vue.js 型別的專案整體工程化的絕大部分。
Vue CLI 核心功能
Vue CLI 最核心的兩個功能:
- 腳手架工具 - CLI 互動自動化建立專案基礎機構
- 沒有像早期那樣在一開始就下載模板,而是整合在了模組中。
- 開發工具 - 提供 Vue.js 開發環境的 CLI 服務
- 本地測試、生產環境構建等
**可擴充套件性:**除了最核心的兩功能,還有很多可擴充套件的空間,可以通過外掛,提供更多的功能。
Vue CLI 的優勢
使用 Vue CLI 相比 webpack 的優勢:
- 開箱即用
- 預設情況下不需要任何的配置,所有配置和工具都包含在
@vue/cli-service
中
- 預設情況下不需要任何的配置,所有配置和工具都包含在
- 漸進主義
- 可以傻瓜式使用,也可以完全自定義,中間存在所有的灰度
- 零維護成本(幾乎)
- 對開發者而言,開發所用的工具和配置都被包裝到
@vue/cli-service
內部,一旦這些工具和配置需要升級,只要@vue/cli-service
有人維護,開發者只需要在專案中升級這個模組即可。 - 開發者只需要維護自定義的部分即可
- 對開發者而言,開發所用的工具和配置都被包裝到
@vue/cli-service
Vue CLI 建立的專案使用的構建命令 vue-cli-service
是 @vue/cli-service
提供的。
它內部做了兩件事情:
- 提供一個適用於大多數 Vue.js 專案的 Webpack 配置
- 把 Webpack 包裝進來
開發一個開箱即用的 webpack
開發一個開箱即用的打包工具,裡面有一些預設的工具和配置。
例如,將 webpack 的配置都放在模組內部,在模組內部呼叫 webpack,使用模組內的配置。
建立CLI工具專案結構
安裝 webpack
# 注意這裡 webpack 要作為這個工具的生產依賴全裝
npm install webpack --save
編寫 webpack 配置
// /lib/index.js
// 多個專案中公用的 Webpack 配置
/** @type {import('webpack').Configuration} */
module.exports = {
entry: './src/main.js',
output: {
filename: 'bundle.js'
}
}
編寫 cli 命令
在 cli 檔案中使用 webpack。
webpack 除了作為命令列方式被呼叫,也對外暴露了API,可以以程式設計的方式去使用。
它作為一個模組被匯入的時候,實際是一個函式。
#!/usr/bin/env node
// /bin/cli.js
const webpack = require('webpack')
// 預設會尋找 package.json 中的 main 欄位指向的檔案
const config = require('..')
webpack(config, (err,stats) => {
// err 是構建過程中發生的錯誤
// stats 包含打包的檔案資訊,其中也包含錯誤資訊
if (err || stats.hasErrors()) {
return console.log('build failed')
}
console.log('success')
})
連結到全域性
npm link
建立測試專案的結構
# 返回上一級目錄
cd ../
# 建立專案目錄
mkdir mypack-demo
專案只需要包含 /src/main.js
檔案:
// /src/main.js
console.log('Hellor Mypack~')
測試
# 直接在mypack-demo目錄下執行工具命令
mypack
# success
所以只要這些公共的配置能夠適用於當下的專案,都可以封裝到這個工具中使用。
甚至可以釋出到 NPM 上,在公司其他專案中安裝並使用,不再需要額外的配置。
@vue/cli-service
做的就是類似的事情。
Vue CLI 架構
基於外掛的架構
開發一個適用於大多數 Vue.js 專案的通用 Webpack 配置的模組,還需要考慮對於使用它的開發者有哪些可能性。
例如使用 less 還是 sass,是否使用 postcss,使用 typescript 或者 ES6新特性。
這時就會想到把所有有可能用到的配置全部加進去。
事實上這些可能用到的配置也並沒有很多。
但是如果這樣做,這個模組內部就會依賴很多其他模組,它將會變得超級重。
那些沒有用到的模組和配置,就顯得很多餘。
為了解決這樣的問題,Vue CLI 就採用了一套基於外掛的架構,使得 Vue CLI 更加靈活、更容易擴充套件、適應能力更強。
圖解
- 最外層就是
@vue/cli
,它僅僅是用來建立專案基礎結構的腳手架工具。 - 建立的專案包含:
- Application 應用程式碼
@vue/cli-service
- 外掛
- 另一個核心功能 - 提供 Vue.js 開發環境的 CLI 服務
@vue/cli-service
只包含最通用的配置- 例如Vue.js專案必然用到的
vue-loader
- 例如Vue.js專案必然用到的
- 個性化的配置,單獨做成一個外掛
- 例如
@vue/cli-plugin-typescript
包含 TypeScript 相關配置
- 例如
@vue/cli 原始碼
通過原始碼檢視 Vue CLI 如何實現的這個架構(看原始碼建議看 tag 版本)。
Vue CLI 也是採用的 Monorepo 的方式,將相關模組放在同一個倉庫中管理。
所有模組都放在 packages
目錄下。
@vue/cli/bin/vue.js
首先檢視 @vue/cli
模組的 vue create
命令入口檔案
@vue/cli/lib/create.js
create 模組匯出一個函式,函式返回撥用 create
方法的結果。
create
方法的主要內容:
validateProjectName
校驗專案名稱validate-npm-package-name
- 用 npm 模組包的約定校驗專案名稱
- 判斷輸出的目標目錄是否存在
- 建立 Creator 物件,接收引數:
- 專案名稱
- 目標目錄
- 用於 CLI 互動提問的模組
getPromptModules()
獲取所有用於 CLI 互動提問的模組
- 呼叫 Creator 物件的
create
方法。
@vue/cli/lib/Creator.js
- Creator 建構函式中初始化問題變數,收集 CLI 互動提問的模組的問題
create
方法中收集所有問題,包括註冊的外掛,獲取答案,生成preset
- 之後會向
preset
中新增一些外掛,如@vue/cli-service
- 從
preset
中收集要安裝的依賴
- 之後會向
- 開始建立專案所需的檔案
- 根據使用者回答新增外掛
- 執行外掛中的 Generator
- 最終分別建立所需檔案
收集問題
- 首先建構函式中初始化幾個空陣列,用於存放問題。
- 然後建立
PromptModuleAPI
物件,它提供一個介面injectPrompt
,用於向injectedPrompts
新增問題。 - 遍歷 CLI 互動提問的模組,每個模組都接收
PromptModuleAPI
物件,如果有問題,就會呼叫injectPrompt
方法,新增問題。 - 在 create 方法中進行提問的時候,通過
resolveFinalPrompts
方法合併所有的問題,包括外掛的。
其他註冊的外掛,會提供一個提問的模組(如果需要提問)。
可以列印 prompts
看看。
Vue CLI 內部使用了 debug 模組。
開發應用的時候,一般會使用 debug 這樣的工具做一些內部的日誌輸出。
每個作業系統使用會不一樣,可以檢視文件。
# PowerShell
$env:DEBUG='vue-cli:prompts'
vue create demo
收集問題的答案
create
方法中會把所有內部、外部外掛等模組的問題進行合併,並提問,然後把答案收集起來,最終整理出一個 preset
物件。
create() -> promptAndResolvePreset() -> resolveFinalPrompts() -> prompts -> preset
建立專案所需的檔案
檢視 Vue CLI 建立的專案,會發現它會先建立一個初始的 package.json
檔案,沒有其他任何檔案,然後立即 npm install
。
然後才會擴充套件修改 package.json
,一點一點生成其他檔案,例如 App.vue
。
開始建立的 package.json
(選擇了預設預設):
{
"name": "demo",
"version": "0.1.0",
"private": true,
"devDependencies": {
"@vue/cli-plugin-babel": "~4.5.0",
"@vue/cli-plugin-eslint": "~4.5.0",
"@vue/cli-service": "~4.5.0"
}
}
建立完成後的 package.json
:
{
"name": "demo",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"core-js": "^3.6.5",
"vue": "^2.6.11"
},
"devDependencies": {
"@vue/cli-plugin-babel": "~4.5.0",
"@vue/cli-plugin-eslint": "~4.5.0",
"@vue/cli-service": "~4.5.0",
"babel-eslint": "^10.1.0",
"eslint": "^6.7.2",
"eslint-plugin-vue": "^6.2.2",
"vue-template-compiler": "^2.6.11"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/essential",
"eslint:recommended"
],
"parserOptions": {
"parser": "babel-eslint"
},
"rules": {}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead"
]
}
cli 本身只是用來在專案根目錄中安裝外掛,真正負責在專案目錄下生成檔案的是 cli 所安裝的外掛。
每個外掛目錄下都有一個 generator
,它會在 create
方法的 invoking-generators
環節執行,進行生成檔案,修改 package.json
等操作。
以 cli-service
為例:
為什麼要在外掛內設計一個 generator?
使用 Babel ,就要有 babel.config 檔案;使用 TypeScript 就要有 tsconfig 檔案…
選擇不同的功能,可能會用到不同的外掛,讓外掛自己去決定它要生成哪些檔案更合理一些。
各司其職,而不是由 cli 主觀去決定需要什麼檔案。
所以外掛內部不僅僅是做一些額外的配置,還要承擔生成一部分檔案的工作。
外掛中的 Generator 才是真正生成檔案的地方。
@vue/cli
本身只是在安裝外掛。
@vue/cli 過程總結
- 先準備命令列互動的問題
- 根據使用者的回答決定是否使用某個外掛(這裡都是內建/官方外掛,第三方外掛需要後續自己手動加到專案中)
- 呼叫 npm / yarn 自動在專案本地安裝這些外掛
- 呼叫每個外掛內部的 generator
- generator 內部建立所有檔案、修改package.json
@vue/cli-service 原始碼
@vue/cli
負責建立專案,@vue/cli-service
負責構建環節(CLI 服務)。
專案使用 vue-cli-service
命令進行執行開發環境、打包專案操作。
@vue/cli-service/vue-cli-service
內部主要匯入 Service 模組,建立了一個 Service
例項,呼叫了它的 run
方法。
@vue/cli-service/lib/Service.js
獲取全部外掛列表
Service 建構函式中主要工作就是獲取所有的外掛列表 resolvePlugins
。
run()
內部主要根據命令列引數,決定執行哪個命令。
有的外掛也會註冊一些命令,例如 eslint 外掛註冊了 lint 命令。
vue-cli-service serve
以 serve
命令為例:
每個外掛內部其實都對 webpack 進行了一些修改,例如 eslint:
@vue/cli-service 過程總結
- 建立 Service 物件
- 獲取專案中所有已安裝 cli 外掛
- 執行
run
方法- 執行
init
方法- 載入
.env
檔案 vue.config.js
使用者專案配置檔案- 執行所有 cli 外掛,載入這個外掛對應的 webpack 配置
- 載入
- 根據命令列引數執行命令
- 執行
cli 外掛
- 建立專案環節
- generator - 生成所需要的檔案
- Generator API
- 構建環節
- 注入對應功能的一些必要的 Webpack 配置
- Plugin API
開發本地外掛
參考文件:
命令外掛
專案中可以開發一個本地外掛,通過 package.json
的vuePlugins
欄位註冊使用。
{
"vuePlugins": {
"service": [
"clean-commander.js"
]
},
"scripts": {
"clean": "vue-cli-service clean"
},
}
專案根目錄下建立外掛檔案 clean-commander.js
。
// /clean-commander.js
/**@type {import('@vue/cli-service').ServicePlugin} */
module.exports = (api, options) => {
// 註冊命令
api.registerCommand('clean', (args, rawArgs) => {
console.log('clean-commander')
})
}
執行命令:
npm run clean
# or
# npx vue-cli-service clean
# clean-commander
繼續擴充套件該外掛,實現刪除 dist 目錄的功能。
為避免出現檔案被佔用無法刪除的情況,可以安裝 rimraf
模組來刪除。
rimraf
模組可以在 node 環境執行強制刪除命令 rm -rf
。
# 由於開發的是構建命令外掛,所以依賴安裝在開發環境
npm install rimraf --save-dev
修改 clean-commander.js
// /clean-commander.js
const rimraf = require('rimraf')
/**@type {import('@vue/cli-service').ServicePlugin} */
module.exports = (api, options) => {
// 註冊命令
api.registerCommand('clean', (args, rawArgs) => {
// console.log('clean-commander')
rimraf('./dist', err => {
if (err) return console.log('failed')
console.log('success')
})
})
}
devServer 外掛
Vue CLI 執行的 devServer,在路由跳轉時預設不會有任何日誌。
這裡開發一個外掛,修改 devServer 配置。
參考:Plugin API - configureDevServer
建立檔案 server-log.js
// /server-log.js
/** @type {import('@vue/cli-service').ServicePlugin} */
module.exports = (api, options) => {
// app 是 express 例項
api.configureDevServer(app => {
// 新增一箇中介軟體
app.use((req, res, next) => {
console.log(`${req.method.toUpperCase()} ${req.url}`)
next()
})
})
}
新增到 vuePlugins
{
"vuePlugins": {
"service": [
"clean-commander.js",
"server-log.js"
]
}
}
執行專案 npm run serve
,點選頁面路由跳轉,檢視效果
Vue CLI 打包優化
- 使用
vue-cli-service inspect >> webpack.config.js
命令將專案最終構建採用的 Webpack 配置輸出 - 根據專案實際需要,找到用不到的 多餘 的配置
- 在
vue.config.js
中通過 api 刪除或禁用這些配置
不過優化空間並不大。
相關文章
- vue實踐01之vue-cli腳手架Vue
- Vue-cli原理分析Vue
- angr原理與實踐(一)——原理
- vue-cli3 vue2 保留 webpack 支援 vite 成功實踐VueWebVite
- Webpack原理與實踐Web
- Vue 探索與實踐Vue
- MelGan原理與實踐篇
- RocketMQ的原理與實踐MQ
- 「Vue實踐」專案升級vue-cli3的正確姿勢Vue
- vue-cli多頁面應用實踐,實現元件預覽Vue元件
- Vue CLI 4與專案構建實戰指南Vue
- Flutter原理與美團的實踐Flutter
- mysql 複製原理與實踐MySql
- 代理重加密原理與實踐加密
- Redis核心原理與實踐--事務實踐與原始碼分析Redis原始碼
- [Vue CLI 3] 多頁應用實踐和原始碼設計Vue原始碼
- WebSocket原理與實踐(一)---基本原理Web
- Redis核心原理與實踐--列表實現原理之ziplistRedis
- Vue-Cli3外掛實戰一:vue-cli-plugin-dllVuePlugin
- JDK ThreadPoolExecutor核心原理與實踐JDKthread
- Guava Cache 原理分析與最佳實踐Guava
- Docker容器的原理與實踐 (下)Docker
- Webpack原理與實踐(一):打包流程Web
- 前端效能優化原理與實踐前端優化
- 執行緒池原理與實踐執行緒
- vue-cli 實戰總結Vue
- 分散式鎖實現原理與最佳實踐分散式
- VUE 全家桶 vue-cli 2 | vue-cli 3Vue
- vue-cli3.0與vant的引入Vue
- Redis核心原理與實踐--列表實現原理之quicklist結構RedisUI
- @vue/cli 3.0 實打實的介紹Vue
- Vue CLI 2&3 下的專案優化實踐 —— CDN + Gzip + PrerenderVue優化
- Flink Sql Gateway的原理與實踐SQLGateway
- Spark Connector Reader 原理與實踐Spark
- 微服務快取原理與最佳實踐微服務快取
- 中間人攻擊原理與實踐
- 前端 JS 安全對抗原理與實踐前端JS
- WebSocket原理與實踐(二)---WebSocket協議Web協議