做者:肖磊javascript
Vue-cli@3.0 是一個全新的 Vue 項目腳手架。不一樣於 1.x/2.x 基於模板的腳手架,Vue-cli@3.0 採用了一套基於插件的架構,它將部分核心功能收斂至 CLI 內部,同時對開發者暴露可拓展的 API 以供開發者對 CLI 的功能進行靈活的拓展和配置。接下來咱們就經過 Vue-cli@3.0 的源碼來看下這套插件架構是如何設計的。css
整個插件系統當中包含2個重要的組成部分:html
vue create
建立一個新的項目;當你使用 vue create <project-name>
建立一個新的 Vue 項目,你會發現生成的項目相較於 1.x/2.x 初始化一個項目時從遠程拉取的模板發生了很大的變化,其中關於 webpack 相關的配置以及 npm script 都沒有在模板裏面直接暴露出來,而是提供了新的 npm script:vue
// package.json "scripts": { "serve": "vue-cli-service serve", "build": "vue-cli-service build", "lint": "vue-cli-service lint" } 複製代碼
前 2 個腳本命令是項目本地安裝的 @vue/cli-service 所提供的基於 webpack 及相關的插件進行封裝的本地開發/構建的服務。@vue/cli-service 將 webpack 及相關插件提供的功能都收斂到 @vue/cli-service 內部來實現。java
這 2 個命令對應於 node_modules/@vue/cli-service/lib/commands 下的 serve.js 和 build/index.js。node
在 serve.js 和 build/index.js 的內部分別暴露了一個函數及一個 defaultModes 屬性供外部來使用。事實上這二者都是做爲 built-in(內置)插件來供 vue-cli-service 來使用的。webpack
說到這裏那麼就來看看 @vue/cli-service 內部是如何搭建整個插件系統的。就拿執行npm run serve
啓動本地開發服務來講,大概流程是這樣的:git
首先來看下 @vue/cli-service 提供的 cli 啓動入口服務(@vue/cli-service/bin/vue-cli-service.js):github
#!/usr/bin/env node const semver = require('semver') const { error } = require('@vue/cli-shared-utils') const Service = require('../lib/Service') // 引入 Service 基類 const service = new Service(process.env.VUE_CLI_CONTEXT || process.cwd()) // 實例化 service const rawArgv = process.argv.slice(2) const args = require('minimist')(rawArgv) const command = args._[0] service.run(command, args, rawArgv).catch(err => { // 開始執行對應的 service 服務 error(err) process.exit(1) }) 複製代碼
看到這裏你會發如今 bin 裏面並未提供和本地開發 serve 相關的服務,事實上在項目當中本地安裝的 @vue/cli-service 提供的不論是內置的仍是插件提供的服務都是動態的去完成相關 CLI 服務的註冊。web
在 lib/Service.js 內部定義了一個核心的類 Service,它做爲 @vue/cli 的運行時的服務而存在。在執行npm run serve
後,首先完成 Service 的實例化工做:
class Service { constructor(context) { ... this.webpackChainFns = [] // 數組內部每項爲一個fn this.webpackRawConfigFns = [] // 數組內部每項爲一個 fn 或 webpack 對象字面量配置項 this.devServerConfigFns = [] this.commands = {} // 緩存動態註冊 CLI 命令 ... this.plugins = this.resolvePlugins(plugins, useBuiltIn) // 完成插件的加載 this.modes = this.plugins.reduce((modes, { apply: { defaultModes }}) => { // 緩存不一樣 CLI 命令執行時所對應的mode值 return Object.assign(modes, defaultModes) }, {}) } } 複製代碼
在實例化 Service 的過程中完成了兩個比較重要的工做:
當 Service 實例化完成後,調用實例上的 run
方法來啓動對應的 CLI 命令所提供的服務。
async run (name, args = {}, rawArgv = []) { const mode = args.mode || (name === 'build' && args.watch ? 'development' : this.modes[name]) // load env variables, load user config, apply plugins // 執行全部被加載進來的插件 this.init(mode) ... const { fn } = command return fn(args, rawArgv) // 開始執行對應的 cli 命令服務 } init (mode = process.env.VUE_CLI_MODE) { ... // 執行plugins // apply plugins. this.plugins.forEach(({ id, apply }) => { // 傳入一個實例化的PluginAPI實例,插件名做爲插件的id標識,在插件內部完成註冊 cli 命令服務和 webpack 配置的更新的工做 apply(new PluginAPI(id, this), this.projectOptions) }) ... // apply webpack configs from project config file if (this.projectOptions.chainWebpack) { this.webpackChainFns.push(this.projectOptions.chainWebpack) } if (this.projectOptions.configureWebpack) { this.webpackRawConfigFns.push(this.projectOptions.configureWebpack) } } 複製代碼
接下來咱們先看下 @vue/cli-service 當中的 Service 實例化的過程:經過 resolvePlugins 方法去完成插件的加載工做:
resolvePlugins(inlinePlugins, useBuiltIn) { const idToPlugin = id => ({ id: id.replace(/^.\//, 'built-in:'), apply: require(id) // 加載對應的插件 }) let plugins // @vue/cli-service內部提供的插件 const builtInPlugins = [ './commands/serve', './commands/build', './commands/inspect', './commands/help', // config plugins are order sensitive './config/base', './config/css', './config/dev', './config/prod', './config/app' ].map(idToPlugin) if (inlinePlugins) { plugins = useBuiltIn !== false ? builtInPlugins.concat(inlinePlugins) : inlinePlugins } else { // 加載項目當中使用的插件 const projectPlugins = Object.keys(this.pkg.devDependencies || {}) .concat(Object.keys(this.pkg.dependencies || {})) .filter(isPlugin) .map(idToPlugin) plugins = builtInPlugins.concat(projectPlugins) } // Local plugins if (this.pkg.vuePlugins && this.pkg.vuePlugins.service) { const files = this.pkg.vuePlugins.service if (!Array.isArray(files)) { throw new Error(`Invalid type for option 'vuePlugins.service', expected 'array' but got ${typeof files}.`) } plugins = plugins.concat(files.map(file => ({ id: `local:${file}`, apply: loadModule(file, this.pkgContext) }))) } return plugins } 複製代碼
在這個 resolvePlugins 方法當中,主要完成了對於 @vue/cli-service 內部提供的插件以及項目應用(package.json)當中須要使用的插件的加載,並將對應的插件進行緩存。在其提供的內部插件當中又分爲兩類:
'./commands/serve' './commands/build' './commands/inspect' './commands/help' 複製代碼
這一類插件在內部動態註冊新的 CLI 命令,開發者便可經過 npm script 的形式去啓動對應的 CLI 命令服務。
'./config/base' './config/css' './config/dev' './config/prod' './config/app' 複製代碼
這一類插件主要是完成 webpack 本地編譯構建時的各類相關的配置。@vue/cli-service 將 webpack 的開發構建功能收斂到內部來完成。
插件加載完成,開始調用 service.run
方法,在這個方法內部開始執行全部被加載的插件:
this.plugins.forEach(({ id, apply }) => { apply(new PluginAPI(id, this), this.projectOptions) }) 複製代碼
在每一個插件執行的過程當中,接收到的第一個參數都是 PluginAPI 的實例,PluginAPI 也是整個 @vue/cli-service 服務當中一個核心的基類:
class PluginAPI { constructor (id, service) { this.id = id // 對應這個插件名 this.service = service // 對應 Service 類的實例(單例) } ... registerCommand (name, opts, fn) { // 註冊自定義 cli 命令 if (typeof opts === 'function') { fn = opts opts = null } this.service.commands[name] = { fn, opts: opts || {}} } chainWebpack (fn) { // 緩存變動的 webpack 配置 this.service.webpackChainFns.push(fn) } configureWebpack (fn) { // 緩存變動的 webpack 配置 this.service.webpackRawConfigFns.push(fn) } ... } 複製代碼
每一個由 PluginAPI 實例化的 api 實例都提供了:
api.registerCommand
)api.chainWebpack
)api.configureWebpack
),與api.chainWebpack
提供的鏈式 api 操做 webpack 配置的方式不一樣,api.configureWebpack
可接受raw式的配置形式,並經過 webpack-merge 對 webpack 配置進行合併。api.resolveWebpackConfig
),調用以前經過 chainWebpack 和 configureWebpack 上完成的對於 webpack 配置的改造,並生成最終的 webpack 配置首先咱們來看下 @vue/cli-service 提供的關於動態註冊 CLI 服務的插件,拿 serve 服務(./commands/serve
)來講:
// commands/serve module.exports = (api, options) => { api.registerCommand( 'serve', { description: 'start development server', usage: 'vue-cli-service serve [options] [entry]', options: { '--open': `open browser on server start`, '--copy': `copy url to clipboard on server start`, '--mode': `specify env mode (default: development)`, '--host': `specify host (default: ${defaults.host})`, '--port': `specify port (default: ${defaults.port})`, '--https': `use https (default: ${defaults.https})`, '--public': `specify the public network URL for the HMR client` } }, async function serve(args) { // do something } ) } 複製代碼
./commands/serve
對外暴露一個函數,接收到的第一個參數 PluginAPI 的實例 api,並經過 api 提供的 registerCommand 方法來完成 CLI 命令(即 serve 服務)的註冊。
再來看下 @vue/cli-service 內部提供的關於 webpack 配置的插件(./config/base
):
module.exports = (api, options) => { api.chainWebpack(webpackConfig => { webpackConfig.module .rule('vue') .test(/\.vue$/) .use('cache-loader') .loader('cache-loader') .options(vueLoaderCacheConfig) .end() .use('vue-loader') .loader('vue-loader') .options( Object.assign( { compilerOptions: { preserveWhitespace: false } }, vueLoaderCacheConfig ) ) }) } 複製代碼
這個插件完成了 webpack 的基本配置內容,例如 entry、output、加載不一樣文件類型的 loader 的配置。不一樣於以前使用的配置式的 webpack 使用方式,@vue/cli-service 默認使用 webpack-chain(連接請戳我) 來完成 webpack 配置的修改。這種方式也使得 webpack 的配置更加靈活,當你的項目遷移至 @vue/cli@3.0,使用的 webpack 插件也必需要使用 API 式的配置,同時插件不只僅要提供插件自身的功能,同時也須要幫助調用方完成插件的註冊等工做。
@vue/cli-service 將基於 webpack 的本地開發構建配置收斂至內部來實現,當你沒有特殊的開發構建需求的時候,內部配置能夠開箱即用,不用開發者去關心一些細節。固然在實際團隊開發當中,內部配置確定是沒法知足的,得益於 @vue-cli@3.0 的插件構建設計,開發者不須要將內部的配置進行 Eject,而是直接使用 @vue/cli-service 暴露出來的 API 去完成對於特殊的開發構建需求。
以上介紹了 @vue/cli-service 插件系統當中幾個核心的模塊,即:
Service.js 提供服務的基類,它提供了 @vue/cli 生態當中本地開發構建時:插件加載(包括內部插件和項目應用插件)、插件的初始化,它的單例被全部的插件所共享,插件使用它的單例去完成 webpack 的更新。
PluginAPI.js 提供供插件使用的對象接口,它和插件是一一對應的關係。全部供 @vue/cli-service 使用的本地開發構建的插件接收的第一個參數都是 PluginAPI 的實例(api
),插件使用這個實例去完成 CLI 命令的註冊及對應服務的執行、webpack 配置的更新等。
以上就是 @vue/cli-service 插件系統簡單的分析,感興趣的同窗能夠深刻閱讀相關源碼(連接請戳我)進行學習。
不一樣於以前 1.x/2.x 的 vue-cli 工具都是基於遠程模板去完成項目的初始化的工做,它屬於那種大而全的方式,當你須要完成自定義的腳手架工具時,你可能要對 vue-cli 進行源碼級別的改造,或者是在遠程模板裏面幫開發者將全部的配置文件初始化完成好。而 @vue/cli@3.0 主要是基於插件的 generator 去完成項目的初始化的工做,它將原來的大而全的模板拆解爲如今基於插件系統的工做方式,每一個插件完成本身所要對於項目應用的模板拓展工做。
@vue/cli 提供了終端裏面的 vue 命令,例如:
vue create <project>
建立一個新的 vue 項目vue ui
打開 vue-cli 的可視化配置當你須要對 vue-cli 進行改造,自定義符合本身開發要求的腳手架的時候,那麼你須要經過開發 vue-cli 插件來對 vue-cli 提供的服務進行拓展來知足相關的要求。vue-cli 插件始終包含一個 Service 插件做爲其主要導出,且可選的包含一個 Generator 和一個 Prompt 文件。這裏不細講如何去開發一個 vue-cli 插件了,你們感興趣的能夠閱讀vue-cli-plugin-eslint
這裏主要是來看下 vue-cli 是如何設計整個插件系統以及整個插件系統是如何工做的。
@vue/cli@3.0 提供的插件安裝方式爲一個 cli 服務:vue add <plugin>
:
install a plugin and invoke its generator in an already created project
執行這條命令後,@vue/cli 會幫你完成插件的下載,安裝以及執行插件所提供的 generator。整個流程的執行順序可經過以下的流程圖去歸納:
咱們來看下具體的代碼邏輯:
// @vue/cli/lib/add.js async function add (pluginName, options = {}, context = process.cwd()) { ... const packageManager = loadOptions().packageManager || (hasProjectYarn(context) ? 'yarn' : 'npm') // 開始安裝這個插件 await installPackage(context, packageManager, null, packageName) log(`${chalk.green('✔')} Successfully installed plugin: ${chalk.cyan(packageName)}`) log() // 判斷插件是否提供了 generator const generatorPath = resolveModule(`${packageName}/generator`, context) if (generatorPath) { invoke(pluginName, options, context) } else { log(`Plugin ${packageName} does not have a generator to invoke`) } } 複製代碼
首先 cli 內部會安裝這個插件,並判斷這個插件是否提供了 generator,若提供了那麼去執行對應的 generator。
// @vue/cli/lib/invoke.js async function invoke (pluginName, options = {}, context = process.cwd()) { const pkg = getPkg(context) ... // 從項目應用package.json中獲取插件名 const id = findPlugin(pkg.devDependencies) || findPlugin(pkg.dependencies) ... // 加載對應插件提供的generator方法 const pluginGenerator = loadModule(`${id}/generator`, context) ... const plugin = { id, apply: pluginGenerator, options } // 開始執行generator方法 await runGenerator(context, plugin, pkg) } async function runGenerator (context, plugin, pkg = getPkg(context)) { ... // 實例化一個Generator實例 const generator = new Generator(context, { pkg plugins: [plugin], // 插件提供的generator方法 files: await readFiles(context), // 將項目當中的文件讀取爲字符串的形式保存到內存當中,被讀取的文件規則具體見readFiles方法 completeCbs: createCompleteCbs, invoking: true }) ... // resolveFiles 將內存當中的全部緩存的 files 輸出到文件當中 await generator.generate({ extractConfigFiles: true, checkExisting: true }) } 複製代碼
和 @vue/cli-service 相似,在 @vue/cli 內部也有一個核心的類Generator
,每一個@vue/cli
的插件對應一個Generator
的實例。在實例化Generator
方法的過程中,完成插件提供的 generator 的執行。
// @vue/cli/lib/Generator.js module.exports = class Generator { constructor (context, { pkg = {}, plugins = [], completeCbs = [], files = {}, invoking = false } = {}) { this.context = context this.plugins = plugins this.originalPkg = pkg this.pkg = Object.assign({}, pkg) this.imports = {} this.rootOptions = {} ... this.invoking = invoking // for conflict resolution this.depSources = {} // virtual file tree this.files = files this.fileMiddlewares = [] this.postProcessFilesCbs = [] ... const cliService = plugins.find(p => p.id === '@vue/cli-service') const rootOptions = cliService ? cliService.options : inferRootOptions(pkg) // apply generators from plugins // 每一個插件對應生成一個 GeneratorAPI 實例,並將實例 api 傳入插件暴露出來的 generator 函數 plugins.forEach(({ id, apply, options }) => { const api = new GeneratorAPI(id, this, options, rootOptions) apply(api, options, rootOptions, invoking) }) } } 複製代碼
和 @vue/cli-service 所使用的插件相似,@vue/cli 插件所提供的 generator 也是向外暴露一個函數,接收的第一個參數 api,而後經過該 api 提供的方法去完成應用的拓展工做。
開發者利用這個 api 實例去完成項目應用的拓展工做,這個 api 實例提供了:
api.extendPackage
)api.render
)api.onCreateComplete
)import
語法的方法(api.injectImports
)例如 @vue/cli-plugin-eslint 插件的 generator 方法主要是完成了:vue-cli-service cli lint 服務命令的添加、相關 lint 標準庫的依賴添加等工做:
module.exports = (api, { config, lintOn = [] }, _, invoking) => { if (typeof lintOn === 'string') { lintOn = lintOn.split(',') } const eslintConfig = require('./eslintOptions').config(api) const pkg = { scripts: { lint: 'vue-cli-service lint' }, eslintConfig, devDependencies: {} } if (config === 'airbnb') { eslintConfig.extends.push('@vue/airbnb') Object.assign(pkg.devDependencies, { '@vue/eslint-config-airbnb': '^3.0.0-rc.10' }) } else if (config === 'standard') { eslintConfig.extends.push('@vue/standard') Object.assign(pkg.devDependencies, { '@vue/eslint-config-standard': '^3.0.0-rc.10' }) } else if (config === 'prettier') { eslintConfig.extends.push('@vue/prettier') Object.assign(pkg.devDependencies, { '@vue/eslint-config-prettier': '^3.0.0-rc.10' }) } else { // default eslintConfig.extends.push('eslint:recommended') } ... api.extendPackage(pkg) ... // lint & fix after create to ensure files adhere to chosen config if (config && config !== 'base') { api.onCreateComplete(() => { require('./lint')({ silent: true }, api) }) } } 複製代碼
以上介紹了 @vue/cli 和插件系統相關的幾個核心的模塊,即:
add.js 提供了插件下載的 cli 命令服務和安裝的功能;
invoke.js 完成插件所提供的 generator 方法的加載和執行,同時將項目當中的文件轉化爲字符串緩存到內存當中;
Generator.js 和插件進行橋接,@vue/cli 每次 add 一個插件時,都會實例化一個 Generator 實例與之對應;
GeneratorAPI.js 和插件一一對應,是 @vue/cli 暴露給插件的 api 對象,提供了不少項目應用的拓展工做。
以上是對 Vue-cli@3.0 的插件系統當中兩個主要部分:@vue/cli 和 @vue/cli-service 簡析。
vue create <projectName>
、vue add <pluginName>
前者主要完成了對於插件的依賴管理,項目模板的拓展等,後者主要是提供了在運行時本地開發構建的服務,同時後者也做爲 @vue/cli 整個插件系統當中的內部核心插件而存在。在插件系統內部也對核心功能進行了插件化的拆解,例如 @vue/cli-service 內置的基礎 webpack 配置,npm script 命令等。兩者使用約定式的方式向開發者提供插件的拓展能力,具體到如何開發 @vue/cli 的插件,請參考官方文檔。