@vue/cli
- 起始文件bin/vue.js,定义了
vue createvue ui等命令,会调用相应的模块来处理命令
@vue/cli-service
定义了serve build inspect help 四个命令功能
vue-cli-service.js
bin/vue-cli-service.js 起始文件,创建了一个service单例,并传入命令名和参数去执行。
Service.js
核心的类 Service,作为@vue/cli运行时的服务,看执行命令时首先完成实例化的构造函数:
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 = []
// 缓存动态注册 CLI 命令
this.commands = {}
// package.json所有的目录
this.pkgContext = context
// package.json中的内容
this.pkg = this.resolvePkg(pkg)
// 命令行中指定了plugins时,使用内置插件和指定的插件(useBuiltIn=false 时禁用内置插件,一般测试时设置)
// 未指定plugins时,使用内置插件和package.json中添加的插件
// { id: string, apply: PluginModule }[]
this.plugins = this.resolvePlugins(plugins, useBuiltIn)
// 会在run()过程中被填充
this.pluginsToSkip = new Set()
// 从插件的module.exports.defaultModes字段获取默认Mode
// {[command]: mode} mode: test,development,product
this.modes = this.plugins.reduce((modes, { apply: { defaultModes }}) => {
return Object.assign(modes, defaultModes)
}, {})
}
在resolvePlugins方法中,解析并加载插件,然后存储到plugins中。主要包括两部分
- 内置插件:
// 这一类插件在内部动态注册新的 CLI 命令,可通过 npm script 的形式去启动对应的 CLI 命令服务。
'./commands/serve',
'./commands/build',
'./commands/inspect',
'./commands/help',
//这一类插件主要是处理 webpack 本地编译构建时的各种相关的配置。
// config plugins are order sensitive
'./config/base',
'./config/css',
'./config/dev',
'./config/prod',
'./config/app',
- package.json中
dependencies和devDependencies两个字段中添加的插件
接着调用实例上的run方法来启动命令对应的服务
/**
* @param name 命令名
* @param args 解析过后的参数对象
* @param rawArgv 命令行参数
*/
async run (name, args = {}, rawArgv = []) {
// 解析mode,优先级(命令参数、命令插件默认值、watch时回退到development)
const mode = args.mode || (name === 'build' && args.watch ? 'development' : this.modes[name])
// 如果有--skip-plugins 参数,设置init()时需要跳过的插件
this.setPluginsToSkip(args)
// 加载环境变量,加载用户配置(vue.config.js),执行插件(动态注册CLI命令服务、添加chianWebpack配置等)
this.init(mode)
... // args中获取命令名,并找到对应的插件
const { fn } = command
return fn(args, rawArgv) // 开始执行对应的命令服务
}
执行命令服务之前,有一个init()的初始化操作。从这里可以看出来,插件模块会暴露出一个函数,接收2个参数:
- api PluginAPI的实例
- projectOptions 配置选项
init (mode = process.env.VUE_CLI_MODE) {
... // 加载环境变量配置等
// 加载vue.config.json中的配置
const userOptions = this.loadUserOptions()
this.projectOptions = defaultsDeep(userOptions, defaults())
// 执行plugin
this.plugins.forEach(({ id, apply }) => {
if (this.pluginsToSkip.has(id)) return
// 传入一个api实例,在插件内部完成解析添加配置、注册命令功能。
// 最终这些配置和命令都会通过pluginAPI存储在service中。
apply(new PluginAPI(id, this), this.projectOptions)
})
// 执行插件中设置
if (this.projectOptions.chainWebpack) {
this.webpackChainFns.push(this.projectOptions.chainWebpack)
}
if (this.projectOptions.configureWebpack) {
this.webpackRawConfigFns.push(this.projectOptions.configureWebpack)
}
}
PluginAPI
PluginAPI是插件和service的连接器。
/**
* @param {string} id - Id of the plugin.
* @param {Service} service - A vue-cli-service instance.
*/
constructor (id, service) {
this.id = id
this.service = service
}
/**
* 向service中注册一个命令服务,可被以这样的命令访问 `vue-cli-service [name]`
*
* @param {string} name
* @param {object} [opts]
* {
* description: string,
* usage: string,
* options: { [string]: string }
* }
* @param {function} fn
* (args: { [string]: string }, rawArgs: string[]) => ?Promise
*/
registerCommand (name, opts, fn) {
if (typeof opts === 'function') {
fn = opts
opts = null
}
this.service.commands[name] = { fn, opts: opts || {}}
}
/**
* 注册一个链式配置webpack的函数,会在 `resolveWebpackConfig` 时被调用
* @param {function} fn
*/
chainWebpack (fn) {
this.service.webpackChainFns.push(fn)
}
/**
* 注册一个webpack配置对象,或者一个修改webpack配置对象的函数(可直接修改config参数,或返回一个对象)
* 这个webpack配置对象会被合并到最终配置
* @param {object | function} fn
*/
configureWebpack (fn) {
this.service.webpackRawConfigFns.push(fn)
}
/**
* 注册一个配置dev server的函数(参数为 dev server 的 experss `app` 实例化对象)
* @param {function} fn
*/
configureDevServer (fn) {
this.service.devServerConfigFns.push(fn)
}
/**
* 解析最终的webpack config,这个配置会被传递给webpack
* @param {ChainableWebpackConfig} [chainableConfig]
* @return {object} Raw webpack config.
*/
resolveWebpackConfig (chainableConfig) {
return this.service.resolveWebpackConfig(chainableConfig)
}
/**
* 解析出一个还可以进行修改的 webpack 链式配置对象(还未变成raw webpack config)
* 你可以多次调用这个方法生成不同版本的配置
* See https://github.com/mozilla-neutrino/webpack-chain
*
* @return {ChainableWebpackConfig}
*/
resolveChainableWebpackConfig () {
return this.service.resolveChainableWebpackConfig()
}
接下来看下几个 @vue/cli-service 命令插件的源码
serve 命令
// lib/commands/serve.js
module.exports = (api, options) => {
api.registerCommand... // 具体看下面
}
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`,
'--skip-plugins': `comma-separated list of plugin names to skip for this run`
}
}, async function serve (args) {
info('Starting development server...')
// although this is primarily a dev server, it is possible that we
// are running it in a mode with a production env, e.g. in E2E tests.
const isInContainer = checkInContainer()
// 解析得到 webpack config
const webpackConfig = api.resolveWebpackConfig()
// 检查常规配置错误(publicPath、outputDir)
validateWebpackConfig(webpackConfig, api, options)
... // 加载 devServer 配置
... // 添加 webpack Dashboard插件
... // 解析 entry 参数
... // 解析 devServer 相关参数、proxy 设置等
... // 注入 dev hot-reload 中间件
// 创建一个编译器
const compiler = webpack(webpackConfig)
// 创建一个dev server
const server = new WebpackDevServer(compiler, Object.assign({
logLevel: 'silent',
clientLogLevel: 'silent',
historyApiFallback: {
disableDotRule: true,
rewrites: genHistoryApiFallbackRewrites(options.publicPath, options.pages)
},
contentBase: api.resolve('public'),
watchContentBase: !isProduction,
hot: !isProduction,
compress: isProduction,
publicPath: options.publicPath,
overlay: isProduction // TODO disable this
? false
: { warnings: false, errors: true }
}, projectDevServerOptions, {
https: useHttps,
proxy: proxySettings,
before (app, server) {
// 开启编辑器支持,通过 vue-devtools 和 @vue/cli-overlay
app.use('/__open-in-editor', launchEditorMiddleware(() => console.log(
`To specify an editor, specify the EDITOR env variable or ` +
`add "editor" field to your Vue project config.\n`
)))
// 允许其他插件注册中间件。例如:PWA
api.service.devServerConfigFns.forEach(fn => fn(app, server))
// 执行项目中的中间件
projectDevServerOptions.before && projectDevServerOptions.before(app, server)
},
// 不打开浏览器
open: false
}))
... // 错误退出处理
return new Promise((resolve, reject) => {
// 记录指令,第一次编译完成打开浏览器
let isFirstCompile = true
compiler.hooks.done.tap('vue-cli-service serve', stats => {
... // 第一次编译完成拷贝访问url
... // 日志打印
if (isFirstCompile) {
isFirstCompile = false
...
// Send final app URL
if (args.dashboard) {
const ipc = new IpcMessenger()
ipc.send({
vueServe: {
url: localUrlForBrowser
}
})
}
// 标记Promise完成,其他命令就可以这样调用 pi.service.run('serve').then(...)
resolve({
server,
url: localUrlForBrowser
})
} else if (process.env.VUE_CLI_TEST) {
// signal for test to check HMR
console.log('App updated')
}
})
server.listen(port, host, err => {
if (err) {
reject(err)
}
})
})
})
build 命令
术语解释
- legacy 兼容版本,浏览器不支持ES2015+,会增加polyfills
- modern 现代版本,浏览器支持ES2015+语法
// lib/commands/build/index.js
module.exports = (api, options) => {
api.registerCommand... // 具体看下面
}
api.registerCommand('build', {
description: 'build for production',
usage: 'vue-cli-service build [options] [entry|pattern]',
options: {
'--mode': `specify env mode (default: production)`,
'--dest': `specify output directory (default: ${options.outputDir})`,
'--modern': `build app targeting modern browsers with auto fallback`,
'--no-unsafe-inline': `build app without introducing inline scripts`,
'--target': `app | lib | wc | wc-async (default: ${defaults.target})`,
'--inline-vue': 'include the Vue module in the final bundle of library or web component target',
'--formats': `list of output formats for library builds (default: ${defaults.formats})`,
'--name': `name for lib or web-component mode (default: "name" in package.json or entry filename)`,
'--filename': `file name for output, only usable for 'lib' target (default: value of --name)`,
'--no-clean': `do not remove the dist directory before building the project`,
'--report': `generate report.html to help analyze bundle content`,
'--report-json': 'generate report.json to help analyze bundle content',
'--skip-plugins': `comma-separated list of plugin names to skip for this run`,
'--watch': `watch for changes`
}
}, async (args, rawArgs) => {
for (const key in defaults) {
if (args[key] == null) {
args[key] = defaults[key]
}
}
args.entry = args.entry || args._[0]
if (args.target !== 'app') {
args.entry = args.entry || 'src/App.vue'
}
process.env.VUE_CLI_BUILD_TARGET = args.target
if (args.modern && args.target === 'app') {
process.env.VUE_CLI_MODERN_MODE = true
if (!process.env.VUE_CLI_MODERN_BUILD) {
// 主进程进行兼容版本构建
await build(Object.assign({}, args, {
modernBuild: false,
keepAlive: true
}), api, options)
// 发起一个子进程进行现代版本构建
const { execa } = require('@vue/cli-shared-utils')
const cliBin = require('path').resolve(__dirname, '../../../bin/vue-cli-service.js')
await execa(cliBin, ['build', ...rawArgs], {
stdio: 'inherit',
env: {
VUE_CLI_MODERN_BUILD: true
}
})
} else {
// 主进程进行现代版本构建
await build(Object.assign({}, args, {
modernBuild: true,
clean: false
}), api, options)
}
delete process.env.VUE_CLI_MODERN_MODE
} else {
if (args.modern) {
const { warn } = require('@vue/cli-shared-utils')
warn(
`Modern mode only works with default target (app). ` +
`For libraries or web components, use the browserslist ` +
`config to specify target browsers.`
)
}
await build(args, api, options)
}
delete process.env.VUE_CLI_BUILD_TARGET
})
下面是具体的构建方法
async function build (args, api, options) {
...
// 解析 raw webpack config,确定是打包成lib,WebComponent还是app
let webpackConfig
if (args.target === 'lib') {
webpackConfig = require('./resolveLibConfig')(api, args, options)
} else if (
args.target === 'wc' ||
args.target === 'wc-async'
) {
webpackConfig = require('./resolveWcConfig')(api, args, options)
} else {
webpackConfig = require('./resolveAppConfig')(api, args, options)
}
// 检查常规配置错误(publicPath、outputDir)
validateWebpackConfig(webpackConfig, api, options, args.target)
... // watch参数处理
... // dashboard参数插件处理
... // 分析参数插件处理
... // 删除发布目录处理
// 开始打包
return new Promise((resolve, reject) => {
webpack(webpackConfig, (err, stats) => {
stopSpinner(false)
if (err) {
return reject(err)
}
if (stats.hasErrors()) {
return reject(`Build failed with errors.`)
}
if (!args.silent) {
const targetDirShort = path.relative(
api.service.context,
targetDir
)
log(formatStats(stats, targetDirShort, api))
if (args.target === 'app' && !isLegacyBuild) {
if (!args.watch) {
done(`Build complete. The ${chalk.cyan(targetDirShort)} directory is ready to be deployed.`)
info(`Check out deployment instructions at ${chalk.cyan(`https://cli.vuejs.org/guide/deployment.html`)}\n`)
} else {
done(`Build complete. Watching for changes...`)
}
}
}
// test-only signal
if (process.env.VUE_CLI_TEST) {
console.log('Build complete.')
}
resolve()
})
})
}
eslint 插件
// @view/cli-plugin-eslint/index.js
module.exports = (api, options) => {
if (options.lintOnSave) {
...
api.chainWebpack...
}
api.registerCommand...
}
api.chainWebpack(webpackConfig => {
webpackConfig.resolveLoader.modules.prepend(
path.join(__dirname, 'node_modules')
)
const { lintOnSave } = options
const allWarnings = lintOnSave === true || lintOnSave === 'warning'
const allErrors = lintOnSave === 'error'
webpackConfig.module
.rule('eslint')
.pre()
.exclude
.add(/node_modules/)
.add(require('path').dirname(require.resolve('@vue/cli-service'))) // eslint-disable-line
.end()
.test(/\.(vue|(j|t)sx?)$/)
.use('eslint-loader')
.loader('eslint-loader')
.options({
extensions,
cache: true,
cacheIdentifier,
emitWarning: allWarnings,
// production mode 才发送错误信息
emitError: allErrors,
eslintPath: path.dirname(
resolveModule('eslint/package.json', cwd) ||
require.resolve('eslint/package.json')
),
formatter:
loadModule('eslint/lib/formatters/codeframe', cwd, true) ||
require('eslint/lib/formatters/codeframe')
})
})
api.registerCommand(
'lint',
{
description: 'lint and fix source files',
usage: 'vue-cli-service lint [options] [...files]',
options: {
'--format [formatter]': 'specify formatter (default: codeframe)',
'--no-fix': 'do not fix errors or warnings',
'--no-fix-warnings': 'fix errors, but do not fix warnings',
'--max-errors [limit]': 'specify number of errors to make build failed (default: 0)',
'--max-warnings [limit]': 'specify number of warnings to make build failed (default: Infinity)'
},
details: 'For more options, see https://eslint.org/docs/user-guide/command-line-interface#options'
},
args => {
require('./lint')(args, api)
}
)
// lint.js
module.exports = function lint (args = {}, api) {
// 解析获取config
// eslint 命令行
const engine = new CLIEngine(config)
// 获取需要进行lint操作的文件
// 执行 lint 操作
const report = engine.executeOnFiles(files)
process.cwd = processCwd
const formatter = engine.getFormatter(args.format || 'codeframe')
if (config.fix) {
CLIEngine.outputFixes(report)
}
// 错误处理
}
综上,我们可以看到,一个 @vue/cli-plugin 插件通过暴露一个函数进行插件的初始化,接收api实例和options配置作为参数,在函数中可进行options的修改,注册命令服务。
命令服务也是一个函数,这个函数接收命令行的参数作为参数。在这个函数中进行webpack配置修改,启动webpack编译打包,启动开发服务器等一系列操作。