关于 npm run serve 命令启动vue应用开发环境的过程分析
1、npm run 命令执行时
npm run 命令执行时,会把./node_modules/.bin目录添加到执行环境的PATH变量中。
全局的没有安装的包,在node_modules中安装了,通过npm run 可以调用该命令。
打开./node_modules/.bin 目录,我们看到了很多执行脚本,包括我们认识的
vue-cli-service
2、执行 serve 命令
npm 会从当前目录下的 package.json 文件的 scripts 配置里找命令别名对应的实际脚本命令,我们看到的配置是这样的
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"test": "vue-cli-service build --mode test"
},
所以虽然我们敲入的命令是 npm run serve,实际就相当于是执行下面的命令行
node_modules\.bin\vue-cli-service.cmd serve
3、vue-cli-service.cmd
我们来看下这个文件里的内容,很少的内容,分析下代码, %~dp0 表示当前所执行的脚本文件的路径,这样我们就能大概知道,代码的意思是先判断当前脚本目
录下是否存在node.exe,存在就直接用当前目录下的node.exe来执行一个js,不存在就用环境变量里的node命令来执行js ,
执行的 js 文件位于当前目录下的 @vue\cli-service\bin\vue-cli-service.js
@IF EXIST "%~dp0\node.exe" (
"%~dp0\node.exe" "%~dp0\..\@vue\cli-service\bin\vue-cli-service.js" %*
) ELSE (
@SETLOCAL
@SET PATHEXT=%PATHEXT:;.JS;=;%
node "%~dp0\..\@vue\cli-service\bin\vue-cli-service.js" %*
)
那么我们就知道了,其实最终执行的脚本命令其实就是一个node执行js的命令
node vue-cli-service.js serve
4、vue-cli-service.js
接下来,我们来分析下 vue-cli-service.js 这个文件的代码是如何执行的。
#!/usr/bin/env node
// 加载 semver
const semver = require('semver')
// 加载 @vue/cli-shared-utils、
const { error } = require('@vue/cli-shared-utils')
// 加载 @vue/cli-service/package.json里配置要求的node版本号描述 (文件中配置的是 "node": ">=8")
const requiredVersion = require('../package.json').engines.node
// 判断当前使用的node版本号是否符合 @vue/cli-service/package.json里配置的要求 ,不符合就中断执行,并给出错误提示信息
if (!semver.satisfies(process.version, requiredVersion)) {
error(
`You are using Node ${process.version}, but vue-cli-service ` +
`requires Node ${requiredVersion}.\nPlease upgrade your Node version.`
)
process.exit(1)
}
// 加载 Service
const Service = require('../lib/Service')
console.log('context:', process.env.VUE_CLI_CONTEXT || process.cwd()) // 输出为:项目主目录如 D:\wamp64\www\SUMEC-HT-YF\CODE\YF-V
// 实例化一个 Service,传入环境变量文件内容
const service = new Service(process.env.VUE_CLI_CONTEXT || process.cwd())
// 获取命令行执行携带的参数, process.argv 这个是命令行数组,调试可知道其值为
/*
[ 'D:\\Program Files\\nodejs\\node.exe',
'D:\\wamp64\\www\\SUMEC-HT-YF\\CODE\\YF-V\\node_modules\\@vue\\cli-service\\bin\\vue-cli-service.js',
'serve' ]
*/
const rawArgv = process.argv.slice(2)
// minimist 是一个用来解析命令行选项的库,这里没有深入了解minimist
const args = require('minimist')(rawArgv, {
boolean: [
// build
'modern',
'report',
'report-json',
'watch',
// serve
'open',
'copy',
'https',
// inspect
'verbose'
]
})
const command = args._[0]
console.log('rawArgv:', rawArgv) // 输出为:rawArgv: [ 'serve' ]
console.log('args:', args)
/* 输出为:
args: { _: [ 'serve' ],
modern: false,
report: false,
'report-json': false,
watch: false,
open: false,
copy: false,
https: false,
verbose: false }
*/
// 运行服务 service.run('serve', args, [ 'serve' ])
service.run(command, args, rawArgv).catch(err => {
error(err)
process.exit(1)
})
5、Service.js
我们现在来分析下这个Service对象的代码,从上一步我们看到了对应的实例化代码,和执行的service.run() 方法
5.1、实例化 Service 分析
process.cwd() 是指当前node命令执行时所在的文件夹目录,我们后面的分析就以在目录 D:\wamp64\www\SUMEC-HT-YF\CODE\YF-V 下面执行为例
// 实例化一个 Service,传入环境变量文件内容
const service = new Service(process.env.VUE_CLI_CONTEXT || process.cwd())
// 相当于 const service = new Service('D:\wamp64\www\SUMEC-HT-YF\CODE\YF-V')
下面我们看一下service实例化方法中的代码执行
constructor (context, { plugins, pkg, inlineOptions, useBuiltIn } = {}) {
......
// 这一句是取出 D:\wamp64\www\SUMEC-HT-YF\CODE\YF-V\package.json配置内容
this.pkg = this.resolvePkg(pkg)
// 这一步是把基础构建用的内部插件 和 this.pkg 里面的开发依耐、生产依耐中的@vue/cli-plugin-* 插件的加载初始化
this.plugins = this.resolvePlugins(plugins, useBuiltIn)
//解析用于每个命令的默认模式
//{ serve: 'development',
// build: 'production',
// inspect: 'development' }
this.modes = this.plugins.reduce((modes, { apply: { defaultModes }}) => {
return Object.assign(modes, defaultModes)
}, {})
......
}
1、resolvePkg
this.resolvePkg(“D:\wamp64\www\SUMEC-HT-YF\CODE\YF-V\package.json”),可以看出这里就是读取json文件,将文件内容读取到对象中,并返回;
resolvePkg (inlinePkg, context = this.context) {
if (inlinePkg) {
return inlinePkg
} else if (fs.existsSync(path.join(context, 'package.json'))) {
const pkg = readPkg.sync({ cwd: context })
// 这边如果package.json有 vuePlugins 并且配置了 vuePlugins.resolveFrom,就会递归读取
if (pkg.vuePlugins && pkg.vuePlugins.resolveFrom) {
this.pkgContext = path.resolve(context, pkg.vuePlugins.resolveFrom)
return this.resolvePkg(null, this.pkgContext)
}
return pkg
} else {
return {}
}
}
2、resolvePlugins
分析下 this.resolvePlugins(),可以知道该方法主要就是加载相关的插件
resolvePlugins (inlinePlugins, useBuiltIn) {
const idToPlugin = id => ({
id: id.replace(/^.\//, 'built-in:'),
apply: require(id)
})
let plugins
// 1、加载下面的基础构建插件到 builtInPlugins 里
const builtInPlugins = [
'./commands/serve',
'./commands/build',
'./commands/inspect',
'./commands/help',
'./config/base',
'./config/css',
'./config/dev',
'./config/prod',
'./config/app'
].map(idToPlugin)
if (inlinePlugins) {
plugins = useBuiltIn !== false
? builtInPlugins.concat(inlinePlugins)
: inlinePlugins
} else {
// 2、将package.json中配置的 devDependencies 和 dependencies里的 @vue/cli-plugin-* 插件放到 projectPlugins 里
const projectPlugins = Object.keys(this.pkg.devDependencies || {})
.concat(Object.keys(this.pkg.dependencies || {}))
.filter(isPlugin)
.map(id => {
if (
this.pkg.optionalDependencies &&
id in this.pkg.optionalDependencies
) {
let apply = () => {}
try {
// 根据插件ID加载插件模块js赋值给 apply
apply = require(id)
} catch (e) {
warn(`Optional dependency ${id} is not installed.`)
}
return { id, apply }
} else {
return idToPlugin(id)
}
})
// 3、builtInPlugins + projectPlugins 合并到 plugins
plugins = builtInPlugins.concat(projectPlugins)
}
// 4、加载package.json中配置的 vuePlugins 到 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
}
this.resolvePlugins() 执行后,this.plugins 的值是这样的:
[ { id: 'built-in:commands/serve', apply: { [Function] defaultModes: [Object] } },
{ id: 'built-in:commands/build', apply: { [Function] defaultModes: [Object] } },
{ id: 'built-in:commands/inspect', apply: { [Function] defaultModes: [Object] } },
{ id: 'built-in:commands/help', apply: [Function] },
{ id: 'built-in:config/base', apply: [Function] },
{ id: 'built-in:config/css', apply: [Function] },
{ id: 'built-in:config/dev', apply: [Function] },
{ id: 'built-in:config/prod', apply: [Function] },
{ id: 'built-in:config/app', apply: [Function] },
{ id: '@vue/cli-plugin-babel', apply: [Function] } ]
实际跟踪可以知道 id = ‘built-in:commands/serve’ 的对应 apply 是 ./commands/serve.js 插件模块,serve.js里的定义如下
....
module.exports = (api, options) => {
......
}
....
module.exports.defaultModes = {
serve: 'development'
}
同样的 ‘built-in:commands/build’ ,对应的 apply 是 ./commands/build/index.js 插件模块 ,index.js里的定义如下
....
module.exports = (api, options) => {
......
}
....
module.exports.defaultModes = {
serve: 'production'
}
其他的也都一样,不再细说了。
3、modes
从第二步得出的 plugins ,把已加载配置的插件中的命令默认模式取到modes里
//解析用于每个命令的默认模式
//{ serve: 'development',
// build: 'production',
// inspect: 'development' }
this.modes = this.plugins.reduce((modes, { apply: { defaultModes }}) => {
return Object.assign(modes, defaultModes)
}, {})
执行代码后,this.modes的值是这样的:
{ serve: 'development',
build: 'production',
inspect: 'development' }
5.2、service.run 分析
运行方法中,主要执行了四个步骤
async run (name, args = {}, rawArgv = []) {
// 1、得到运行模式(先从命令行参数中获取,所以使用命令行 vue-cli-service build --mode test 执行时,这里的得到的mode就是test)
const mode = args.mode || (name === 'build' && args.watch ? 'development' : this.modes[name])
// 2、根据运行模式加载环境变量参数,加载vue.config,应用插件
this.init(mode)
args._ = args._ || []
// 3、根据命令模式name得到对应的执行命令项
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()
}
// 4、取出命令项中的执行方法函数配置
const { fn } = command
return fn(args, rawArgv)
}
1、mode
运行模式
this.modes
中包含的信息是这样的 :
{ serve: 'development',
build: 'production',
inspect: 'development' }
所以,我们使用npm run serve 执行时,这里得到的 mode 就是 = this.modes[‘serve’ ] = ‘development’
//1、得到运行模式(先从命令行参数中获取,所以使用命令行 vue-cli-service build --mode test 执行时,这里的得到的mode就是test)
const mode = args.mode || (name === 'build' && args.watch ? 'development' : this.modes[name])
2、init
this.init(mode)
这个初始化方法会加载模式对应的环境变量配置文件,并且加载 vue.config 文件配置信息,还会把相关的插件都初始化加载起来
init (mode = process.env.VUE_CLI_MODE) {
if (this.initialized) {
return
}
this.initialized = true
this.mode = mode
// 1、加载环境变量配置文件 .env.development
if (mode) {
this.loadEnv(mode)
}
// 2、加载环境变量基础配置文件 .env.local
this.loadEnv()
// 3、加载用户配置。读取vue.config.js文件内部的配置
const userOptions = this.loadUserOptions()
this.projectOptions = defaultsDeep(userOptions, defaults())
debug('vue:project-config')(this.projectOptions)
// 4、遍历插件集合,应用插件,这一步执行后 this.commands的值就有了
this.plugins.forEach(({ id, apply }) => {
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)
}
}
3、commands
this.init(mode) 执行完后,然后
this.commands
的值是这样的:
{
serve: {
fn: [AsyncFunction: serve],
opts: {
description: 'start development server',
usage: 'vue-cli-service serve [options] [entry]',
options: [Object]
}
},
build: {
fn: [AsyncFunction],
opts: {
description: 'build for production',
usage: 'vue-cli-service build [options] [entry|pattern]',
options: [Object]
}
},
inspect: {
fn: [Function],
opts: {
description: 'inspect internal webpack config',
usage: 'vue-cli-service inspect [options] [...paths]',
options: [Object]
}
},
help: {
fn: [Function],
opts: {}
}
}
所我们再来看run里的代码 ,就知道this.commands[‘serve’]的值
// 3、根据命令模式name得到对应的执行命令项
let command = this.commands[name]
.....
if (!command || args.help || args.h) {
command = this.commands.help
} else {
args._.shift() // remove command itself
rawArgv.shift()
}
// 4、取出命令项中的执行方法函数配置
const { fn } = command
return fn(args, rawArgv)
command的值就等于:
{
fn: [AsyncFunction: serve],
opts: {
description: 'start development server',
usage: 'vue-cli-service serve [options] [entry]',
options: [Object]
}
}
那么 this.commands.serve.fn 对应的方法是哪个呢?
4、command.fn()
要想知道this.commands.serve.fn 执行的是哪段逻辑代码,首先我们要知道commands对象是怎么生成的;
我们上面说了 commands 是在 init 方法中的下面代码执行后,才生成值的,那我们就分析这段代码吧;
this.plugins.forEach(({ id, apply }) => {
apply(new PluginAPI(id, this), this.projectOptions)
})
this.plugins 的值我们上面也已经列出来了;apply() 对应的我们也都知道就是 serve.js、build/index.js 里的 module.exports = (api, options) => {} 方法。
new PluginAPI(id, this) 这是实例了一个插件API对象,this.projectOptions 这是项目配置信息;
下面,我们就主要看下 serve.js、build/index.js 里的 module.exports = (api, options) => {} 方法执行了什么,方法里面的代码很长,但仔细一看,其实就是执行
了一句 api.registerCommand(‘serve’, { … }, async function serve (args) { … } )
registerCommand ,看意思应该是注册命令的方法,这里的api,就是前面 new PluginAPI(id, this) 实例化出来的;
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`
}
........
},
进入 api.registerCommand() 方法定义代码里,我们终于看到了 this.service.commands 的赋值(this.service 是实例化 PluginAPI 传进来的 service,
就是当前run的service)。
registerCommand (name, opts, fn) {
if (typeof opts === 'function') {
fn = opts
opts = null
}
this.service.commands[name] = { fn, opts: opts || {}}
}
这个fn 我们也知道了就是 api.registerCommand(‘serve’, { … }, async function serve (args) { … } ) 里的 async function serve (args) { … } ;
5、serve (args)
serve (args) 这个方法很长,服务和插件的运行都在这里执行;
未完…