前言
我们知道,使用vue-cli创建的项目,其启动或者打包等命令是使用npm run serve
或者npm run build
等。而这些命令实际上执行的是vue-cli-service serve
、vue-cli-service build
等。也就是这些都是执行的vue-cli-service
下的子命令。其插件化机制的实现也是在里面完成的。vue-cli-service
的插件化机制与前边分析的cli-ui
的插件化机制类似。vue-cli ui插件化机制详解。
为了帮助我们理解后续的逻辑,我们先看一下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) => {
// ...
// 具体逻辑实现
await build(args, api, options)
}
});
// ...
}
正文
我们仍然是先从入口看起。
我们执行vue-cli-service xxxxx
实际上是使用node执行了packages/@vue/cli-service/bin/vue-cli-service.js
文件。我们就从此文件看起。
#!/usr/bin/env node
const { semver, error } = require('@vue/cli-shared-utils')
// ...
// 引入Service,并创建实例
const Service = require('../lib/Service')
const service = new Service(process.env.VUE_CLI_CONTEXT || process.cwd())
// 处理参数
const rawArgv = process.argv.slice(2)
const args = require('minimist')(rawArgv, {
boolean: [
// build
'modern',
'report',
// ...
]
})
const command = args._[0]
// 执行service实例上的run方法,将参数传递进去
service.run(command, args, rawArgv).catch(err => {
error(err)
process.exit(1)
})
入口文件很简单:
- 创建Service实例
- 处理命令行参数
- 执行实例上的run方法
主要逻辑都在Service类中实现。
我们接下来看一下具体实现:
先看其构造函数:
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)
/*
如果使用了内置插件,将会使用他们去代替package.json中的插件;
useBuildIn为false时,内置插件会被禁用(这个在大多时候是被用于测试)。
在这里会去解析package.json中的依赖信息,将找到的插件require进来,做一些简单处理后放入this.plugins数组中保存
*/
this.plugins = this.resolvePlugins(plugins, useBuiltIn)
// 在运行run方法时,填充这个Set
this.pluginsToSkip = new Set()
// 为每个command解析默认mode
this.modes = this.plugins.reduce((modes, { apply: { defaultModes }}) => {
return Object.assign(modes, defaultModes)
}, {})
}
// ...
}
构造函数中做的事情主要是
- 初始化一些变量
- 加载插件信息(仍然是从依赖、以及package.json中的vuePlugins字段中加载,然后将其保存在一个数组中)。
回想一下,在入口处,创建插件实例之后,就是处理参数,将其传入run方法中去执行。关键的就是run方法的实现。
下面就来看一下run
方法中做了什么:
async run (name, args = {}, rawArgv = []) {
// 拿到当前的mode
const mode = args.mode || (name === 'build' && args.watch ? 'development' : this.modes[name])
// 根据--skip-plugins设置pluginsToSkip
this.setPluginsToSkip(args)
// 加载环境变量、用户配置、应用插件。
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方法本身非常简单。
- 他接受命令名称、命令参数以及原生命令参数作为参数。
- 执行init方法之后,从this.commands上拿到当前命令name对应的命令信息对象
command
。 - 然后将参数传入
command.fn
函数去执行。
到这里,使用vue-cli-service执行子命令的过程结束。
可是,我们仍然没有看到是如何加载的插件。也不知道this.commands
中的信息是如何而来的。
实际上,关键也正是在run函数中间的init方法内完成的,也就是做了初始化的工作。
看下其具体实现:
init (mode = process.env.VUE_CLI_MODE) {
if (this.initialized) {
return
}
this.initialized = true
this.mode = mode
// 加载当前mode对应的环境变量
if (mode) {
this.loadEnv(mode)
}
// 加载基础环境变量
this.loadEnv()
// 加载用户配置,也就是在vue.config.js中的定义。
const userOptions = this.loadUserOptions()
this.projectOptions = defaultsDeep(userOptions, defaults())
// 应用插件
this.plugins.forEach(({ id, apply }) => {
if (this.pluginsToSkip.has(id)) return
apply(new PluginAPI(id, this), this.projectOptions)
})
// 从项目配置文件中应用webpack配置
if (this.projectOptions.chainWebpack) {
this.webpackChainFns.push(this.projectOptions.chainWebpack)
}
if (this.projectOptions.configureWebpack) {
this.webpackRawConfigFns.push(this.projectOptions.configureWebpack)
}
}
在init函数中,主要进行了以下的过程:
- 加载环境变量
- 从vue.config.js中加载配置信息
- 对在构造函数中拿到的插件信息进行遍历,运行插件
- 将用户的webpack配置加入到真正的webpack配置中
注意: 看运行插件的方法,对于每一个插件都是新创建一个插件实例,将其传入插件函数中去执行。
而创建插件实例时是将this传递了进去,也就是Service实例传了过去,PluginApi实例内的方法也是直接向传过去的Service实例上去添加方法和属性。
所有在运行了这些插件之后,就会在当前的Service实例上添加一系列的信息。
我们简单看一下PluginApi的一些代码片段:
class PluginAPI {
// 第二个参数也就是我们在Service.init方法内运行插件时传入的Service实例
constructor (id, service) {
this.id = id
this.service = service
}
// 例如registerCommand方法,就是直接在Service实例上去添加信息
registerCommand (name, opts, fn) {
if (typeof opts === 'function') {
fn = opts
opts = null
}
this.service.commands[name] = { fn, opts: opts || {}}
}
}
init方法执行完毕后,所有插件的信息都已经存在于Service实例上。
我们回到最初的起点。
执行vue-cli-service xxx
命令时,实际进行了如下过程的操作:
- 创建Service实例;
- 从package.json中加载插进信息,将其保存在一个数组中;
- 循环遍历插件数组,对于每一个都将生成一个PluginApi实例,将其传入插件函数中去执行(执行完毕就会在Service实例上添加一些信息);
- 拿到xxx命令对应的信息,将命令行参数传入xxx命令对应的处理函数中去执行。
vue-cli 的插件化机制到此结束。
对比一下,我们发现,该过程与我们前面所分析的cli-ui的插件化过程非常相似。甚至过程业基本如出一辙,但ui插件化的实现比这里稍微复杂一点点。(主要是ui插件化的代码比较分散,代码量较多,各个过程牵扯比较复杂,开始时难以分析。)
总的来说,cli插件化与cli-ui插件化的过程是一致的。