Vue CLI 插件開發(fā)指南

2020-03-13 15:11 更新

核心概念

系統(tǒng)里有兩個(gè)主要的部分:

  • @vue/cli:全局安裝的,暴露 vue create <app> 命令;
  • @vue/cli-service:局部安裝,暴露 vue-cli-service 命令。

兩者皆應(yīng)用了基于插件的架構(gòu)。

Creator

Creator 是調(diào)用 vue create <app> 時(shí)創(chuàng)建的類。負(fù)責(zé)偏好對(duì)話、調(diào)用 generator 和安裝依賴。

Service

Service 是調(diào)用 vue-cli-service <command> [...args] 時(shí)創(chuàng)建的類。負(fù)責(zé)管理內(nèi)部的 webpack 配置、暴露服務(wù)和構(gòu)建項(xiàng)目的命令等。

CLI 插件

CLI 插件是一個(gè)可以為 @vue/cli 項(xiàng)目添加額外特性的 npm 包。它應(yīng)該始終包含一個(gè) Service 插件作為其主要導(dǎo)出,且可選的包含一個(gè) Generator 和一個(gè) Prompt 文件。

一個(gè)典型的 CLI 插件的目錄結(jié)構(gòu)看起來(lái)是這樣的:

.
├── README.md
├── generator.js  # generator (可選)
├── prompts.js    # prompt 文件 (可選)
├── index.js      # service 插件
└── package.json

Service 插件

Service 插件會(huì)在一個(gè) Service 實(shí)例被創(chuàng)建時(shí)自動(dòng)加載——比如每次 vue-cli-service 命令在項(xiàng)目中被調(diào)用時(shí)。

注意我們這里討論的“service 插件”的概念要比發(fā)布為一個(gè) npm 包的“CLI 插件”的要更窄。前者涉及一個(gè)會(huì)被 @vue/cli-service 在初始化時(shí)加載的模塊,也經(jīng)常是后者的一部分。

此外,@vue/cli-service 的內(nèi)建命令配置模塊也是全部以 service 插件實(shí)現(xiàn)的。

一個(gè) service 插件應(yīng)該導(dǎo)出一個(gè)函數(shù),這個(gè)函數(shù)接受兩個(gè)參數(shù):

  • 一個(gè) PluginAPI 實(shí)例
  • 一個(gè)包含 vue.config.js 內(nèi)指定的項(xiàng)目本地選項(xiàng)的對(duì)象,或者在 package.json 內(nèi)的 vue 字段。

這個(gè) API 允許 service 插件針對(duì)不同的環(huán)境擴(kuò)展/修改內(nèi)部的 webpack 配置,并向 vue-cli-service 注入額外的命令。例如:

module.exports = (api, projectOptions) => {
  api.chainWebpack(webpackConfig => {
    // 通過(guò) webpack-chain 修改 webpack 配置
  })

  api.configureWebpack(webpackConfig => {
    // 修改 webpack 配置
    // 或返回通過(guò) webpack-merge 合并的配置對(duì)象
  })

  api.registerCommand('test', args => {
    // 注冊(cè) `vue-cli-service test`
  })
}

為命令指定模式

注意:插件設(shè)置模式的方式從 beta.10 開始已經(jīng)改變了。

如果一個(gè)已注冊(cè)的插件命令需要運(yùn)行在特定的默認(rèn)模式下,則該插件需要通過(guò) module.exports.defaultModes 以 { [commandName]: mode } 的形式來(lái)暴露:

module.exports = api => {
  api.registerCommand('build', () => {
    // ...
  })
}

module.exports.defaultModes = {
  build: 'production'
}

這是因?yàn)槲覀冃枰诩虞d環(huán)境變量之前知道該命令的預(yù)期模式,所以需要提前加載用戶選項(xiàng)/應(yīng)用插件。

在插件中解析 webpack 配置

一個(gè)插件可以通過(guò)調(diào)用 api.resolveWebpackConfig() 取回解析好的 webpack 配置。每次調(diào)用都會(huì)新生成一個(gè) webpack 配置用來(lái)在需要時(shí)進(jìn)一步修改。

module.exports = api => {
  api.registerCommand('my-build', args => {
    const configA = api.resolveWebpackConfig()
    const configB = api.resolveWebpackConfig()

    // 針對(duì)不同的目的修改 `configA` 和 `configB`...
  })
}

// 請(qǐng)確保為正確的環(huán)境變量指定默認(rèn)模式
module.exports.defaultModes = {
  'my-build': 'production'
}

或者,一個(gè)插件也可以通過(guò)調(diào)用 api.resolveChainableWebpackConfig() 獲得一個(gè)新生成的鏈?zhǔn)脚渲?/a>:

api.registerCommand('my-build', args => {
  const configA = api.resolveChainableWebpackConfig()
  const configB = api.resolveChainableWebpackConfig()

  // 針對(duì)不同的目的鏈?zhǔn)叫薷?`configA` 和 `configB`...

  const finalConfigA = configA.toConfig()
  const finalConfigB = configB.toConfig()
})

第三方插件的自定義選項(xiàng)

vue.config.js 的導(dǎo)出將會(huì)通過(guò)一個(gè) schema 的驗(yàn)證以避免筆誤和錯(cuò)誤的配置值。然而,一個(gè)第三方插件仍然允許用戶通過(guò) pluginOptions 字段配置其行為。例如,對(duì)于下面的 vue.config.js:

module.exports = {
  pluginOptions: {
    foo: { /* ... */ }
  }
}

該第三方插件可以讀取 projectOptions.pluginOptions.foo 來(lái)做條件式的決定配置。

Generator

一個(gè)發(fā)布為 npm 包的 CLI 插件可以包含一個(gè) generator.js 或 generator/index.js 文件。插件內(nèi)的 generator 將會(huì)在兩種場(chǎng)景下被調(diào)用:

  • 在一個(gè)項(xiàng)目的初始化創(chuàng)建過(guò)程中,如果 CLI 插件作為項(xiàng)目創(chuàng)建 preset 的一部分被安裝。
  • 插件在項(xiàng)目創(chuàng)建好之后通過(guò) vue invoke 獨(dú)立調(diào)用時(shí)被安裝。

這里的 GeneratorAPI 允許一個(gè) generator 向 package.json 注入額外的依賴或字段,并向項(xiàng)目中添加文件。

一個(gè) generator 應(yīng)該導(dǎo)出一個(gè)函數(shù),這個(gè)函數(shù)接收三個(gè)參數(shù):

  1. 一個(gè) GeneratorAPI 實(shí)例:
  2. 這個(gè)插件的 generator 選項(xiàng)。這些選項(xiàng)會(huì)在項(xiàng)目創(chuàng)建對(duì)話過(guò)程中被解析,或從一個(gè)保存在 ~/.vuerc 中的 preset 中加載。例如,如果保存好的 ~/.vuerc 像如下的這樣:{ "presets" : { "foo": { "plugins": { "@vue/cli-plugin-foo": { "option": "bar" } } } } } 如果用戶使用 preset foo 創(chuàng)建了一個(gè)項(xiàng)目,那么 @vue/cli-plugin-foo 的 generator 就會(huì)收到 { option: 'bar' } 作為第二個(gè)參數(shù)。對(duì)于一個(gè)第三方插件來(lái)說(shuō),該選項(xiàng)將會(huì)解析自對(duì)話或用戶執(zhí)行 vue invoke 時(shí)的命令行參數(shù)中 (詳見(jiàn)第三方插件的對(duì)話)。
  3. 整個(gè) preset (presets.foo) 將會(huì)作為第三個(gè)參數(shù)傳入。

示例:

module.exports = (api, options, rootOptions) => {
  // 修改 `package.json` 里的字段
  api.extendPackage({
    scripts: {
      test: 'vue-cli-service test'
    }
  })

  // 復(fù)制并用 ejs 渲染 `./template` 內(nèi)所有的文件
  api.render('./template')

  if (options.foo) {
    // 有條件地生成文件
  }
}

Generator 的模板處理

當(dāng)你調(diào)用 api.render('./template') 時(shí),該 generator 將會(huì)使用 EJS 渲染 ./template 中的文件 (相對(duì)于 generator 中的文件路徑進(jìn)行解析)

此外,你可以使用 YAML 前置元信息繼承并替換已有的模板文件的一部分:

---
extend: '@vue/cli-service/generator/template/src/App.vue'
replace: !!js/regexp /<script>[^]*?<\/script>/
---

<script>
export default {
  // 替換默認(rèn)腳本
}
</script>

你也可以完成多處替換,當(dāng)然你需要將要替換的字符串用 <%# REPLACE %> 和 <%# END_REPLACE %> 塊包裹起來(lái):

---
extend: '@vue/cli-service/generator/template/src/App.vue'
replace:
  - !!js/regexp /歡迎來(lái)到你的 Vue\.js 應(yīng)用/
  - !!js/regexp /<script>[^]*?<\/script>/
---

<%# REPLACE %>
替換歡迎信息
<%# END_REPLACE %>

<%# REPLACE %>
<script>
export default {
  // 替換默認(rèn)腳本
}
</script>
<%# END_REPLACE %>

文件名的極端情況

如果你想要渲染一個(gè)以點(diǎn)開頭的模板文件 (例如 .env),則需要遵循一個(gè)特殊的命名約定,因?yàn)橐渣c(diǎn)開頭的文件會(huì)在插件發(fā)布到 npm 的時(shí)候被忽略:

# 以點(diǎn)開頭的模板需要使用下劃線取代那個(gè)點(diǎn):

/generator/template/_env

# 調(diào)用 api.render('./template') 會(huì)在項(xiàng)目目錄中渲染成為:

.env

同時(shí)這也意味著當(dāng)你想渲染以下劃線開頭的文件時(shí),同樣需要遵循一個(gè)特殊的命名約定:

# 這種模板需要使用兩個(gè)下劃線來(lái)取代單個(gè)下劃線:

/generator/template/__variables.scss

# 調(diào)用 api.render('./template') 會(huì)在項(xiàng)目目錄中渲染成為:

_variables.scss

Prompts

內(nèi)建插件的對(duì)話

只有內(nèi)建插件可以定制創(chuàng)建新項(xiàng)目時(shí)的初始化對(duì)話,且這些對(duì)話模塊放置在 @vue/cli 包的內(nèi)部。

一個(gè)對(duì)話模塊應(yīng)該導(dǎo)出一個(gè)函數(shù),這個(gè)函數(shù)接收一個(gè) PromptModuleAPI 實(shí)例。這些對(duì)話的底層使用 inquirer 進(jìn)行展示:

module.exports = api => {
  // 一個(gè)特性對(duì)象應(yīng)該是一個(gè)有效的 inquirer 選擇對(duì)象
  api.injectFeature({
    name: 'Some great feature',
    value: 'my-feature'
  })

  // injectPrompt 期望接收一個(gè)有效的 inquirer 對(duì)話對(duì)象
  api.injectPrompt({
    name: 'someFlag',
    // 確認(rèn)對(duì)話只在用戶已經(jīng)選取了特性的時(shí)候展示
    when: answers => answers.features.include('my-feature'),
    message: 'Do you want to turn on flag foo?',
    type: 'confirm'
  })

  // 當(dāng)所有的對(duì)話都完成之后,將你的插件注入到
  // 即將傳遞給 Generator 的 options 中
  api.onPromptComplete((answers, options) => {
    if (answers.features.includes('my-feature')) {
      options.plugins['vue-cli-plugin-my-feature'] = {
        someFlag: answers.someFlag
      }
    }
  })
}

第三方插件的對(duì)話

第三方插件通常會(huì)在一個(gè)項(xiàng)目創(chuàng)建完畢后被手動(dòng)安裝,且用戶將會(huì)通過(guò)調(diào)用 vue invoke 來(lái)初始化這個(gè)插件。如果這個(gè)插件在其根目錄包含一個(gè) prompts.js,那么它將會(huì)用在該插件被初始化調(diào)用的時(shí)候。這個(gè)文件應(yīng)該導(dǎo)出一個(gè)用于 Inquirer.js 的問(wèn)題的數(shù)組。這些被解析的答案對(duì)象會(huì)作為選項(xiàng)被傳遞給插件的 generator。

或者,用戶可以通過(guò)在命令行傳遞選項(xiàng)來(lái)跳過(guò)對(duì)話直接初始化插件,比如:

vue invoke my-plugin --mode awesome

發(fā)布插件

為了讓一個(gè) CLI 插件能夠被其它開發(fā)者使用,你必須遵循 vue-cli-plugin-<name> 的命名約定將其發(fā)布到 npm 上。插件遵循命名約定之后就可以:

  • 被 @vue/cli-service 發(fā)現(xiàn);
  • 被其它開發(fā)者搜索到;
  • 通過(guò) vue add <name> 或 vue invoke <name> 安裝下來(lái)。

開發(fā)核心插件的注意事項(xiàng)

注意

這個(gè)章節(jié)只用于 vuejs/vue-cli 倉(cāng)庫(kù)內(nèi)部的內(nèi)建插件工作。

一個(gè)帶有為本倉(cāng)庫(kù)注入額外依賴的 generator 的插件 (比如 chai 會(huì)通過(guò) @vue/cli-plugin-unit-mocha/generator/index.js 被注入) 應(yīng)該將這些依賴列入其自身的 devDependencies 字段。這會(huì)確保:

  1. 這個(gè)包始終存在于該倉(cāng)庫(kù)的根 node_modules 中,因此我們不必在每次測(cè)試的時(shí)候重新安裝它們。
  2. yarn.lock 會(huì)保持其一致性,因此 CI 程序可以更好地利用緩存。


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)