简介
最近在做vue项目时,遇到一些vue cli方面的报错,于是便想深入研究一下vue cli。这里先简单写一篇,如果有更细致的探究,再另作打算。
执行npm run dev
前提是你已经安装了node,并且附带了npm。
执行npm run dev
时,npm会自动搜索当前目录下的package.json,找到scripts
配置项中的dev
脚本。
"scripts": {
"dev": "vue-cli-service serve --mode dev",
"build": "vue-cli-service build --mode prod",
"rsync": "rsync -av -e ssh ./dist/template-webapp-client-vue3 root@aliyun:/srv/www",
"lint": "vue-cli-service lint"
},
可以看到npm run dev
实际上在执行vue-cli-service serve --mode dev
。
vue-cli-service
命令被放在node_modules/.bin
下,这是使用vue create
创建项目时添加的。
辅助资料:
- 附录/执行npm run时发生了什么
- 附录/node_modules/.bin下的文件是怎么来的
vue-cli-service做了什么
先查看node_modules/.bin/vue-cli-service.cmd
这份文件:
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\@vue\cli-service\bin\vue-cli-service.js" %*
这个批处理文件的主要目的是确定Node.js的路径,并使用该路径来运行一个特定的Vue CLI服务脚本。而这个被运行的脚本就是node_modules\@vue\cli-service\bin\vue-cli-service.js
。
好的,我们继续查看node_modules\@vue\cli-service\bin\vue-cli-service.js
:
#!/usr/bin/env node
// 指定该脚本使用 Node.js 来执行,并且使用 `/usr/bin/env` 来查找 node 的实际安装路径
const { semver, error } = require('@vue/cli-shared-utils') // `semver` 是一个用于处理语义化版本号的库 ,`error` 可能是一个用于输出错误并可能以某种方式退出的函数
const requiredVersion = require('../package.json').engines.node // 获取项目所需的 Node.js 版本
// 使用 semver 的 satisfies 函数检查当前 Node.js 的版本号是否满足项目所需版本
// 如果不满足,则使用 `error` 函数输出错误消息,并提示用户升级 Node.js 版本
if (!semver.satisfies(process.version, requiredVersion, { includePrerelease: true })) {
error(
`You are using Node ${process.version}, but vue-cli-service ` +
`requires Node ${requiredVersion}.\nPlease upgrade your Node version.`
)
process.exit(1) // 退出程序,并返回状态码 1,通常表示出现了错误
}
// 使用 `VUE_CLI_CONTEXT` 环境变量(如果存在)或当前工作目录作为上下文创建一个Service对象
const Service = require('../lib/Service')
const service = new Service(process.env.VUE_CLI_CONTEXT || process.cwd())
// 解析参数
const rawArgv = process.argv.slice(2) // 获取命令行参数(不包括 `node` 和脚本名),存储在 `rawArgv` 中
const args = require('minimist')(rawArgv, { // 使用 'minimist' 库解析命令行参数
boolean: [ // 定义哪些命令行参数是布尔类型的(即它们的存在与否表示 true 或 false)
'modern',
'report',
'report-json',
'inline-vue',
'watch',
'open',
'copy',
'https',
'verbose'
]
})
const command = args._[0] // 获取命令行中的第一个参数(通常是命令名),存储在 `command` 中
// 执行命令
service.run(command, args, rawArgv).catch(err => {
error(err)
process.exit(1)
})
这份文件的意思就是创建一个Service对象,并执行run()方法。执行的时候使用命令行传入的参数。
Service类
全局变量和函数
// # 三方库
const path = require('path')
const debug = require('debug')
const { merge } = require('webpack-merge')
const Config = require('webpack-chain')
const dotenv = require('dotenv')
const dotenvExpand = require('dotenv-expand')
const defaultsDeep = require('lodash.defaultsdeep')
// # vue-cli自带库
const { warn, error, isPlugin, resolvePluginId, loadModule, resolvePkg, resolveModule, sortPlugins } = require('@vue/cli-shared-utils')
const PluginAPI = require('./PluginAPI')
const { defaults } = require('./options')
const loadFileConfig = require('./util/loadFileConfig')
const resolveUserConfig = require('./util/resolveUserConfig')
// Seems we can't use `instanceof Promise` here (would fail the tests)
const isPromise = p => p && typeof p.then === 'function'
module.exports = class Service {
// ...
}
/** @type {import('../types/index').defineConfig} */
module.exports.defineConfig = (config) => config
constructor
constructor (context, { plugins, pkg, inlineOptions, useBuiltIn } = {}) {
process.VUE_CLI_SERVICE = this
this.initialized = false
this.context = context // process.pwd(),也就是我们的项目根目录
this.inlineOptions = inlineOptions
this.webpackChainFns = []
this.webpackRawConfigFns = []
this.devServerConfigFns = []
this.commands = {}
this.pkgContext = context
this.pkg = this.resolvePkg(pkg)
this.plugins = this.resolvePlugins(plugins, useBuiltIn)
this.pluginsToSkip = new Set() // 在执行run()时填充
// 采集每个command类插件所对应的默认mode
// command的默认mode可以在其文件中看到(modules.exports.defaultModes)
this.modes = this.plugins.reduce((modes, { apply: { defaultModes } }) => {
return Object.assign(modes, defaultModes)
}, {})
}
resolvePkg
// # 解析并读取packageJson配置
// 使用context指定路径(即项目根目录)下的package.json
// 如果package.json中有pkg.vuePlugins.resolveFrom这个配置,则使用该配置指定的package.json
resolvePkg (inlinePkg, context = this.context) {
// 如果有inlinePkg,则使用inlinePkg
// 然而从源码中来看,并没有传入inlinePkg,所以这个分支不会执行
if (inlinePkg) {
return inlinePkg
}
const pkg = resolvePkg(context) // 读取指定上下文下的package.json
if (pkg.vuePlugins && pkg.vuePlugins.resolveFrom) {
this.pkgContext = path.resolve(context, pkg.vuePlugins.resolveFrom)
return this.resolvePkg(null, this.pkgContext)
}
return pkg
}
const pkg = resolvePkg(context)
这一行中的resolvePkg()源码如下:
const fs = require('fs')
const path = require('path')
const readPkg = require('read-pkg')
exports.resolvePkg = function (context) {
if (fs.existsSync(path.join(context, 'package.json'))) {
return readPkg.sync({ cwd: context })
}
return {}
}
read-pkg是用于读取package.json的。这是因为在ES模块中,无法使用import导入json文件,所以需要read-pkg这个包来解决这个问题。
resolvePlugins
解析、加载plugins后返回。
plugin可以来自内置插件、命令行中指定的插件、package.json中指定的插件。
如果使用了命令行中指定的插件,则会忽略package.json中指定的插件。
内置(built-in)插件在@vue/cli-service/lib
目录下,包括:
// 命令类插件
@vue/cli-service/lib/commands/serve
@vue/cli-service/lib/commands/build
@vue/cli-service/lib/commands/inspect
@vue/cli-service/lib/commands/help
// 配置类插件,用于向webpack配置文件添加配置项
@vue/cli-service/lib/config/base
@vue/cli-service/lib/config/assets
@vue/cli-service/lib/config/css
@vue/cli-service/lib/config/prod
@vue/cli-service/lib/config/app
// package.json中的插件
@vue/cli-plugin-babel
@vue/cli-plugin-eslint
@vue/cli-plugin-router
@vue/cli-plugin-typescript
@vue/cli-plugin-vuex
resolvePlugins (inlinePlugins, useBuiltIn) {
const idToPlugin = (id, absolutePath) => ({
id: id.replace(/^.\//, 'built-in:'), // 如果是'./commands/serve'这样的内置插件,则转为'built-in:commands/serve'
apply: require(absolutePath || id) // 导入插件,可以是绝对路径的三方插件,也可以是相对路径的内置插件
})
let plugins
const builtInPlugins = [
'./commands/serve',
'./commands/build',
'./commands/inspect',
'./commands/help',
// config plugins are order sensitive
'./config/base',
'./config/assets',
'./config/css',
'./config/prod',
'./config/app'
].map((id) => idToPlugin(id))
// 如果有inlinePlugins,则使用inlinePlugins或[...ininePlugins, ...builtInPlugins]
if (inlinePlugins) {
plugins = useBuiltIn !== false
? builtInPlugins.concat(inlinePlugins)
: inlinePlugins
}
// 否则使用package.json中的dependencies和devDependencies中的vue plugin
// (这里会根据包名自动判断是否是vue plugin)
else {
const projectPlugins = Object.keys(this.pkg.devDependencies || {})
.concat(Object.keys(this.pkg.dependencies || {}))
.filter(isPlugin) // 包名符合@vue/cli-plugin-xxx或vue-cli-plugin-xxx
.map(id => {
if (
this.pkg.optionalDependencies &&
id in this.pkg.optionalDependencies
) {
let apply = loadModule(id, this.pkgContext)
if (!apply) {
warn(`Optional dependency ${id} is not installed.`)
apply = () => {}
}
return { id, apply }
} else {
return idToPlugin(id, resolveModule(id, this.pkgContext))
}
})
plugins = builtInPlugins.concat(projectPlugins)
}
// 加载本地插件
// 本地插件应该指的是放在项目根目录下由用户自定义的插件
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)
})))
}
debug('vue:plugins')(plugins)
// 对插件进行排序
// 有一些插件需要在指定的其他插件运行之后/前运行
const orderedPlugins = sortPlugins(plugins)
debug('vue:plugins-ordered')(orderedPlugins)
return orderedPlugins
}
run
async run (name, args = {}, rawArgv = []) {
// 计算mode,优先使用--mode参数
// 次之,在build命令且有--watch时使用development
// 最次,使用命令的默认mode
const mode = args.mode || (name === 'build' && args.watch ? 'development' : this.modes[name])
// 跳过--skip-plugins指定的插件
this.setPluginsToSkip(args, rawArgv)
// 加载环境变量、加载用户配置、应用插件
await this.init(mode)
// 执行name对应的命令
// 这些命令是在插件中注册到this.commands中的
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)
}
init
init做以下几件事:
- 加载mode .env和base .env中的内容到process.env中
- 加载vue.config.js,并且生成本项目的配置清单
- 应用插件
- 挂载webpack配置
init (mode = process.env.VUE_CLI_MODE) {
if (this.initialized) {
return
}
this.initialized = true
this.mode = mode
// 加载mode .env和base .env
if (mode) {
this.loadEnv(mode)
}
this.loadEnv()
// 加载vue.config.js(可能是异步的)
const userOptions = this.loadUserOptions()
// 加载完毕后的回调
const loadedCallback = (loadedUserOptions) => {
// 将vue.config.js和默认选项结合,形成本项目的配置
this.projectOptions = defaultsDeep(loadedUserOptions, defaults())
debug('vue:project-config')(this.projectOptions)
// 应用插件
this.plugins.forEach(({ id, apply }) => {
if (this.pluginsToSkip.has(id)) return
apply(new PluginAPI(id, this), this.projectOptions)
})
// 应用来自vue.config.js中的webpack配置
if (this.projectOptions.chainWebpack) {
this.webpackChainFns.push(this.projectOptions.chainWebpack)
}
if (this.projectOptions.configureWebpack) {
this.webpackRawConfigFns.push(this.projectOptions.configureWebpack)
}
}
if (isPromise(userOptions)) {
return userOptions.then(loadedCallback)
} else {
return loadedCallback(userOptions)
}
}
loadEnv
从项目根目录下加载.env或.env.${mode}和.env.${mode}.local中的环境变量到process.env中。
loadEnv (mode) {
const logger = debug('vue:env')
const basePath = path.resolve(this.context, `.env${mode ? `.${mode}` : ``}`)
const localPath = `${basePath}.local`
const load = envPath => {
try {
const env = dotenv.config({ path: envPath, debug: process.env.DEBUG })
dotenvExpand(env)
logger(envPath, env)
} catch (err) {
// only ignore error if file is not found
if (err.toString().indexOf('ENOENT') < 0) {
error(err)
}
}
}
load(localPath)
load(basePath)
// 设置NODE_ENV和BABEL_ENV
if (mode) {
// 在test模式时强制设置NODE_ENV和BABEL_ENV为defaultEnv
const shouldForceDefaultEnv = (
process.env.VUE_CLI_TEST &&
!process.env.VUE_CLI_TEST_TESTING_ENV
)
const defaultNodeEnv = (mode === 'production' || mode === 'test')
? mode
: 'development'
if (shouldForceDefaultEnv || process.env.NODE_ENV == null) {
process.env.NODE_ENV = defaultNodeEnv
}
if (shouldForceDefaultEnv || process.env.BABEL_ENV == null) {
process.env.BABEL_ENV = defaultNodeEnv
}
}
}
loadUserOptions
加载vue.config.js。
loadUserOptions () {
const { fileConfig, fileConfigPath } = loadFileConfig(this.context)
if (isPromise(fileConfig)) {
return fileConfig
.then(mod => mod.default)
.then(loadedConfig => resolveUserConfig({
inlineOptions: this.inlineOptions,
pkgConfig: this.pkg.vue,
fileConfig: loadedConfig,
fileConfigPath
}))
}
return resolveUserConfig({
inlineOptions: this.inlineOptions,
pkgConfig: this.pkg.vue,
fileConfig,
fileConfigPath
})
}
* PluginAPI
这是对Vue CLI插件暴露的API对象类。将PluginAPI对象传给插件,通过这个对象,插件可以向service对象进行以下关键操作:
- 获取cwd
- 注册command
- 添加webpack配置
- 添加dev server配置
总结
这个类做了以下事情:
- 读取package.json文件,主要需要知道其中关于Vue CLI插件的信息
- 读取并安装VueCLI内置插件、package.json中的三方Vue CLI插件
- 加载.env文件中的环境变量到process.env中
- 加载vue.config.js,并将其中的配置合并到webpack配置中
然后Vue CLI插件赋予Service额外的特性,比如:
- 添加command
- 添加webpack配置
- 添加dev server配置
serve.js
从Service类的resolvePlugins()函数中我们可以知道,vue-cli-service serve
实际上在执行@vue/cli-service/lib/commands/serve.js
这个脚本。那我们继续来看这个文件吧。
这份文件是一个命令型插件,service会加载并安装它:
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`,
'--stdin': `close when stdin ends`,
'--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`,
'--skip-plugins': `comma-separated list of plugin names to skip for this run`
}
}, async function serve (args) {
// ...
})
}
插件的主体部分比较琐碎,但主要就是围绕着两件事情:创建webpack对象用来编译,然后使用webpackDevServer运行编译完的结果。
// ...
const webpack = require('webpack')
const WebpackDevServer = require('webpack-dev-server')
// ...
const compiler = webpack(webpackConfig)
// ...
const server = new WebpackDevServer(
// 一些服务器配置
compiler
)
// ...
server.start().catch(err => reject(err))
总结
综上,vue cli项目启动的过程简单地说如下:
- 执行npm run dev
- 执行dev对应的vue-cli-service serve --mode dev
- vue-cli-service中创建Service类,读取从命令行、package.json、.env、vue.config.js这些配置文件中拿到的配置,应用插件获得特性
- 执行serve.js对应的命令,其主要内容就是创建webpack编译器和启动webpackDevServer。