前言
我們知道,使用vue-cli創(chuàng)建的項(xiàng)目,其啟動(dòng)或者打包等命令是使用npm run serve或者npm run build等。而這些命令實(shí)際上執(zhí)行的是vue-cli-service serve、vue-cli-service build等。也就是這些都是執(zhí)行的vue-cli-service下的子命令。其插件化機(jī)制的實(shí)現(xiàn)也是在里面完成的。vue-cli-service的插件化機(jī)制與前邊分析的cli-ui的插件化機(jī)制類似。vue-cli ui插件化機(jī)制詳解。
為了幫助我們理解后續(xù)的邏輯,我們先看一下cli插件的寫法(和ui插件非常相似):
module.exports = (api, options) => {
api.registerCommand('build', {
description: 'build for production',
usage: 'vue-cli-service build [options] [entry|pattern]',
options: {
'--mode': `specify env mode (default: production)`,
// ...
}
}, async (args, rawArgs) => {
// ...
// 具體邏輯實(shí)現(xiàn)
await build(args, api, options)
}
});
// ...
}
正文
我們?nèi)匀皇窍葟娜肟诳雌稹?br>
我們執(zhí)行vue-cli-service xxxxx實(shí)際上是使用node執(zhí)行了packages/@vue/cli-service/bin/vue-cli-service.js文件。我們就從此文件看起。
#!/usr/bin/env node
const { semver, error } = require('@vue/cli-shared-utils')
// ...
// 引入Service,并創(chuàng)建實(shí)例
const Service = require('../lib/Service')
const service = new Service(process.env.VUE_CLI_CONTEXT || process.cwd())
// 處理參數(shù)
const rawArgv = process.argv.slice(2)
const args = require('minimist')(rawArgv, {
boolean: [
// build
'modern',
'report',
// ...
]
})
const command = args._[0]
// 執(zhí)行service實(shí)例上的run方法,將參數(shù)傳遞進(jìn)去
service.run(command, args, rawArgv).catch(err => {
error(err)
process.exit(1)
})
入口文件很簡單:
- 創(chuàng)建Service實(shí)例
- 處理命令行參數(shù)
- 執(zhí)行實(shí)例上的run方法
主要邏輯都在Service類中實(shí)現(xiàn)。
我們接下來看一下具體實(shí)現(xiàn):
先看其構(gòu)造函數(shù):
module.exports = class Service {
constructor (context, { plugins, pkg, inlineOptions, useBuiltIn } = {}) {
// 初始化一系列的值
process.VUE_CLI_SERVICE = this
this.initialized = false
this.context = context
this.inlineOptions = inlineOptions
this.webpackChainFns = []
this.webpackRawConfigFns = []
this.devServerConfigFns = []
this.commands = {}
// 從目錄下邊的package.json中解析插件信息
this.pkgContext = context
this.pkg = this.resolvePkg(pkg)
/*
如果使用了內(nèi)置插件,將會(huì)使用他們?nèi)ゴ鎝ackage.json中的插件;
useBuildIn為false時(shí),內(nèi)置插件會(huì)被禁用(這個(gè)在大多時(shí)候是被用于測試)。
在這里會(huì)去解析package.json中的依賴信息,將找到的插件require進(jìn)來,做一些簡單處理后放入this.plugins數(shù)組中保存
*/
this.plugins = this.resolvePlugins(plugins, useBuiltIn)
// 在運(yùn)行run方法時(shí),填充這個(gè)Set
this.pluginsToSkip = new Set()
// 為每個(gè)command解析默認(rèn)mode
this.modes = this.plugins.reduce((modes, { apply: { defaultModes }}) => {
return Object.assign(modes, defaultModes)
}, {})
}
// ...
}
構(gòu)造函數(shù)中做的事情主要是
- 初始化一些變量
- 加載插件信息(仍然是從依賴、以及package.json中的vuePlugins字段中加載,然后將其保存在一個(gè)數(shù)組中)。
回想一下,在入口處,創(chuàng)建插件實(shí)例之后,就是處理參數(shù),將其傳入run方法中去執(zhí)行。關(guān)鍵的就是run方法的實(shí)現(xiàn)。
下面就來看一下run方法中做了什么:
async run (name, args = {}, rawArgv = []) {
// 拿到當(dāng)前的mode
const mode = args.mode || (name === 'build' && args.watch ? 'development' : this.modes[name])
// 根據(jù)--skip-plugins設(shè)置pluginsToSkip
this.setPluginsToSkip(args)
// 加載環(huán)境變量、用戶配置、應(yīng)用插件。
this.init(mode)
args._ = args._ || []
let command = this.commands[name]
if (!command && name) {
error(`command "${name}" does not exist.`)
process.exit(1)
}
if (!command || args.help || args.h) {
command = this.commands.help
} else {
args._.shift() // remove command itself
rawArgv.shift()
}
const { fn } = command
return fn(args, rawArgv)
}
可以看到run方法本身非常簡單。
- 他接受命令名稱、命令參數(shù)以及原生命令參數(shù)作為參數(shù)。
- 執(zhí)行init方法之后,從this.commands上拿到當(dāng)前命令name對(duì)應(yīng)的命令信息對(duì)象
command。 - 然后將參數(shù)傳入
command.fn函數(shù)去執(zhí)行。
到這里,使用vue-cli-service執(zhí)行子命令的過程結(jié)束。
可是,我們?nèi)匀粵]有看到是如何加載的插件。也不知道this.commands中的信息是如何而來的。
實(shí)際上,關(guān)鍵也正是在run函數(shù)中間的init方法內(nèi)完成的,也就是做了初始化的工作。
看下其具體實(shí)現(xiàn):
init (mode = process.env.VUE_CLI_MODE) {
if (this.initialized) {
return
}
this.initialized = true
this.mode = mode
// 加載當(dāng)前mode對(duì)應(yīng)的環(huán)境變量
if (mode) {
this.loadEnv(mode)
}
// 加載基礎(chǔ)環(huán)境變量
this.loadEnv()
// 加載用戶配置,也就是在vue.config.js中的定義。
const userOptions = this.loadUserOptions()
this.projectOptions = defaultsDeep(userOptions, defaults())
// 應(yīng)用插件
this.plugins.forEach(({ id, apply }) => {
if (this.pluginsToSkip.has(id)) return
apply(new PluginAPI(id, this), this.projectOptions)
})
// 從項(xiàng)目配置文件中應(yīng)用webpack配置
if (this.projectOptions.chainWebpack) {
this.webpackChainFns.push(this.projectOptions.chainWebpack)
}
if (this.projectOptions.configureWebpack) {
this.webpackRawConfigFns.push(this.projectOptions.configureWebpack)
}
}
在init函數(shù)中,主要進(jìn)行了以下的過程:
- 加載環(huán)境變量
- 從vue.config.js中加載配置信息
- 對(duì)在構(gòu)造函數(shù)中拿到的插件信息進(jìn)行遍歷,運(yùn)行插件
- 將用戶的webpack配置加入到真正的webpack配置中
注意: 看運(yùn)行插件的方法,對(duì)于每一個(gè)插件都是新創(chuàng)建一個(gè)插件實(shí)例,將其傳入插件函數(shù)中去執(zhí)行。
而創(chuàng)建插件實(shí)例時(shí)是將this傳遞了進(jìn)去,也就是Service實(shí)例傳了過去,PluginApi實(shí)例內(nèi)的方法也是直接向傳過去的Service實(shí)例上去添加方法和屬性。
所有在運(yùn)行了這些插件之后,就會(huì)在當(dāng)前的Service實(shí)例上添加一系列的信息。
我們簡單看一下PluginApi的一些代碼片段:
class PluginAPI {
// 第二個(gè)參數(shù)也就是我們?cè)赟ervice.init方法內(nèi)運(yùn)行插件時(shí)傳入的Service實(shí)例
constructor (id, service) {
this.id = id
this.service = service
}
// 例如registerCommand方法,就是直接在Service實(shí)例上去添加信息
registerCommand (name, opts, fn) {
if (typeof opts === 'function') {
fn = opts
opts = null
}
this.service.commands[name] = { fn, opts: opts || {}}
}
}
init方法執(zhí)行完畢后,所有插件的信息都已經(jīng)存在于Service實(shí)例上。
我們回到最初的起點(diǎn)。
執(zhí)行vue-cli-service xxx命令時(shí),實(shí)際進(jìn)行了如下過程的操作:
- 創(chuàng)建Service實(shí)例;
- 從package.json中加載插進(jìn)信息,將其保存在一個(gè)數(shù)組中;
- 循環(huán)遍歷插件數(shù)組,對(duì)于每一個(gè)都將生成一個(gè)PluginApi實(shí)例,將其傳入插件函數(shù)中去執(zhí)行(執(zhí)行完畢就會(huì)在Service實(shí)例上添加一些信息);
- 拿到xxx命令對(duì)應(yīng)的信息,將命令行參數(shù)傳入xxx命令對(duì)應(yīng)的處理函數(shù)中去執(zhí)行。
vue-cli 的插件化機(jī)制到此結(jié)束。
對(duì)比一下,我們發(fā)現(xiàn),該過程與我們前面所分析的cli-ui的插件化過程非常相似。甚至過程業(yè)基本如出一轍,但ui插件化的實(shí)現(xiàn)比這里稍微復(fù)雜一點(diǎn)點(diǎn)。(主要是ui插件化的代碼比較分散,代碼量較多,各個(gè)過程牽扯比較復(fù)雜,開始時(shí)難以分析。)
總的來說,cli插件化與cli-ui插件化的過程是一致的。