Vue CLI 原理與實踐

皮蛋很白發表於2020-12-02

理解 Vue CLI 的目的

Vue CLI 架構上的一些設計,值得前端開發者去了解和學習。

前端開發過程中用到的很多的工具,基本上都是基於類似的架構去設計。

通過理解 Vue CLI 的原理和實踐,瞭解一個可擴充套件性、適應能力更強的工具是怎樣去設計和架構的。

vue-cli 早期面貌

Vue CLI 3 之前的模組是 vue-clinpmjs 上已經被標記為棄用。

近幾年開發都使用的 @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 的實現方式是基於模板建立專案。

  1. 從 github(vuejs-templates) 下載模板
  2. 詢問一系列配置問題
  3. 把模板中的檔案經過渲染,替換一些詢問後的內容
  4. 輸出到目標目錄,並安裝依賴

根據不同的模板可以建立不同的專案,所以早期的 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/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
    • 最終分別建立所需檔案

收集問題

  1. 首先建構函式中初始化幾個空陣列,用於存放問題。
  2. 然後建立 PromptModuleAPI 物件,它提供一個介面 injectPrompt,用於向 injectedPrompts 新增問題。
  3. 遍歷 CLI 互動提問的模組,每個模組都接收 PromptModuleAPI 物件,如果有問題,就會呼叫 injectPrompt 方法,新增問題。
  4. 在 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 過程總結

  1. 先準備命令列互動的問題
  2. 根據使用者的回答決定是否使用某個外掛(這裡都是內建/官方外掛,第三方外掛需要後續自己手動加到專案中)
  3. 呼叫 npm / yarn 自動在專案本地安裝這些外掛
  4. 呼叫每個外掛內部的 generator
  5. 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 外掛

  • 建立專案環節
  • 構建環節
    • 注入對應功能的一些必要的 Webpack 配置
    • Plugin API

開發本地外掛

參考文件:

命令外掛

專案中可以開發一個本地外掛,通過 package.jsonvuePlugins欄位註冊使用。

{
  "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 刪除或禁用這些配置

不過優化空間並不大。

相關文章