共计 26506 个字符,预计需要花费 67 分钟才能阅读完成。
1. vue-cli-service
解决什么问题?
依据官网文档的形容,vue-cli-service
是 vue-cli
的运行时依赖。它能够:
- 基于 webpack 构建,内置了正当的默认配置;
- 能够通过配置文件进行配置 webpack;
- 能够通过插件扩大
vue-cli-service
的命令
2. 带着问题看源码
vue-cli-service
次要流程是怎么的?vue-cli-service
帮咱们预设了正当的 webpack 配置,也反对咱们在vue.config.js
里批改 webpack 配置,是如何做到 webpack 配置的缝合的?vue-cli-service serve
和vue-cli-service build
做了什么?- 如何注册一个新的命令?
3. vue-cli-service
次要流程是怎么的?
3.0 目录构造
├─lib
| ├─options.js
| ├─PluginAPI.js
| ├─Service.js
| ├─commands
| | ├─help.js
| | ├─inspect.js
| | ├─serve.js
| | ├─build
| | | ├─demo-lib-js.html
| | | ├─demo-lib.html
| | | ├─demo-wc.html
| | | ├─entry-lib-no-default.js
| | | ├─entry-lib.js
| | | ├─entry-wc.js
| | | ├─formatStats.js
| | | ├─index.js
| | | ├─resolveAppConfig.js
| | | ├─resolveLibConfig.js
| | | ├─resolveWcConfig.js
| | | ├─resolveWcEntry.js
| | | └setPublicPath.js
├─bin
| └vue-cli-service.js
留神:这里只展现了局部目录和文件,为了直观显示本文须要讲述的文件以及所在的目录。
3.1 packages\@vue\cli-service\bin\vue-cli-service.js
#!/usr/bin/env node
const {semver, error} = require('@vue/cli-shared-utils')
const requiredVersion = require('../package.json').engines.node
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)
}
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
// FIXME: --no-module, --no-unsafe-inline, no-clean, etc.
'modern',
'report',
'report-json',
'inline-vue',
'watch',
// serve
'open',
'copy',
'https',
// inspect
'verbose'
]
})
const command = args._[0]
service.run(command, args, rawArgv).catch(err => {error(err)
process.exit(1)
})
从入口能够看出,vue-cli-service
的外围是应用了 Service
类,实例化并调用 run
办法。上面咱们看看 Service
类。
3.2 packages\@vue\cli-service\lib\Service.js
// ...
module.exports = class Service {constructor (context, { plugins, pkg, inlineOptions, useBuiltIn} = {}) {
process.VUE_CLI_SERVICE = this
this.initialized = false
// 我的项目的工作门路
this.context = context
this.inlineOptions = inlineOptions
// webpack 配置相干
this.webpackChainFns = []
// webpack 配置相干
this.webpackRawConfigFns = []
this.devServerConfigFns = []
this.commands = {}
// Folder containing the target package.json for plugins
this.pkgContext = context
// package.json containing the plugins
// 解析 package.json
this.pkg = this.resolvePkg(pkg)
// If there are inline plugins, they will be used instead of those
// found in package.json.
// When useBuiltIn === false, built-in plugins are disabled. This is mostly
// for testing.
// 解析插件
this.plugins = this.resolvePlugins(plugins, useBuiltIn)
// pluginsToSkip will be populated during run()
// 须要跳过的插件
this.pluginsToSkip = new Set()
// resolve the default mode to use for each command
// this is provided by plugins as module.exports.defaultModes
// so we can get the information without actually applying the plugin.
// 模式,基于每个插件设置的 defaultModes 来决定
this.modes = this.plugins.reduce((modes, { apply: { defaultModes} }) => {return Object.assign(modes, defaultModes)
}, {})
}
// ...
构造函数,初始化变量,上面看看 this.resolvePlugins
初始化插件是如何实现的。
resolvePlugins (inlinePlugins, useBuiltIn) {const idToPlugin = (id, absolutePath) => ({id: id.replace(/^.\//, 'built-in:'),
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))
// 行内插件
if (inlinePlugins) {
plugins = useBuiltIn !== false
? builtInPlugins.concat(inlinePlugins)
: inlinePlugins
} else {
// 在 package.json 合乎 @vue/cli-plugin-xxx 标准的插件
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 = 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)
}
// 本地插件
// Local 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)
})))
}
debug('vue:plugins')(plugins)
const orderedPlugins = sortPlugins(plugins)
debug('vue:plugins-ordered')(orderedPlugins)
return orderedPlugins
}
// @vue\cli-shared-utils\lib\pluginResolution.js
const pluginRE = /^(@vue\/|vue-|@[\w-]+(\.)?[\w-]+\/vue-)cli-plugin-/
exports.isPlugin = id => pluginRE.test(id)
能够看到,插件依据引入的地位能够分为四种:内置插件、行内插件、本地插件、在 package.json 合乎 pluginRE
正则的插件。而后用 idToPlugin
封装成 {id: 插件 id, apply: 插件办法}
。上面先看看 run
办法,再看看内置插件 commands/serve
是如何实现的。
async run (name, args = {}, rawArgv = []) {
// resolve mode
// prioritize inline --mode
// fallback to resolved default modes from plugins or development if --watch is defined
const mode = args.mode || (name === 'build' && args.watch ? 'development' : this.modes[name])
// --skip-plugins arg may have plugins that should be skipped during init()
// 依据入参来决定跳过的插件
this.setPluginsToSkip(args)
// load env variables, load user config, apply plugins
// 初始化:加载环境变量、加载用户配置,执行插件
await this.init(mode)
args._ = args._ || []
// 依据 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()}
// 执行命令办法
const {fn} = command
return fn(args, rawArgv)
}
run
办法,次要做了以下事件:通过参数设置跳过的插件,通过 this.init
来初始化(加载环境变量、加载用户配置,执行插件),执行命令办法。上面看看 this.init
的实现
init (mode = process.env.VUE_CLI_MODE) {if (this.initialized) {return}
this.initialized = true
this.mode = mode
// load mode .env
if (mode) {this.loadEnv(mode)
}
// load base .env
this.loadEnv()
// load user config
const userOptions = this.loadUserOptions()
const loadedCallback = (loadedUserOptions) => {this.projectOptions = defaultsDeep(loadedUserOptions, defaults())
debug('vue:project-config')(this.projectOptions)
// apply plugins.
this.plugins.forEach(({id, apply}) => {if (this.pluginsToSkip.has(id)) return
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)
}
}
if (isPromise(userOptions)) {return userOptions.then(loadedCallback)
} else {return loadedCallback(userOptions)
}
}
能够看到 this.init
加载环境变量、加载用户配置,执行插件。其中在执行插件的时候,api 传入了 PluginAPI
实例,它是插件的一些 api,上面先贴一下代码,前面在讲插件的时候会用到。
3.3 packages\@vue\cli-service\lib\PluginAPI.js
const path = require('path')
const hash = require('hash-sum')
const {semver, matchesPluginId} = require('@vue/cli-shared-utils')
// Note: if a plugin-registered command needs to run in a specific default mode,
// the plugin needs to expose it via `module.exports.defaultModes` in the form
// of {[commandName]: mode }. This is because the command mode needs to be
// known and applied before loading user options / applying plugins.
class PluginAPI {
/**
* @param {string} id - Id of the plugin.
* @param {Service} service - A vue-cli-service instance.
*/
constructor (id, service) {
this.id = id
this.service = service
}
get version () {return require('../package.json').version
}
assertVersion (range) {if (typeof range === 'number') {if (!Number.isInteger(range)) {throw new Error('Expected string or integer value.')
}
range = `^${range}.0.0-0`
}
if (typeof range !== 'string') {throw new Error('Expected string or integer value.')
}
if (semver.satisfies(this.version, range, { includePrerelease: true})) return
throw new Error(`Require @vue/cli-service "${range}", but was loaded with "${this.version}".`
)
}
/**
* Current working directory.
*/
getCwd () {return this.service.context}
/**
* Resolve path for a project.
*
* @param {string} _path - Relative path from project root
* @return {string} The resolved absolute path.
*/
resolve (_path) {return path.resolve(this.service.context, _path)
}
/**
* Check if the project has a given plugin.
*
* @param {string} id - Plugin id, can omit the (@vue/|vue-|@scope/vue)-cli-plugin- prefix
* @return {boolean}
*/
hasPlugin (id) {return this.service.plugins.some(p => matchesPluginId(id, p.id))
}
/**
* Register a command that will become available as `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 || {} }
}
/**
* Register a function that will receive a chainable webpack config
* the function is lazy and won't be called until `resolveWebpackConfig` is
* called
*
* @param {function} fn
*/
chainWebpack (fn) {this.service.webpackChainFns.push(fn)
}
/**
* Register
* - a webpack configuration object that will be merged into the config
* OR
* - a function that will receive the raw webpack config.
* the function can either mutate the config directly or return an object
* that will be merged into the config.
*
* @param {object | function} fn
*/
configureWebpack (fn) {this.service.webpackRawConfigFns.push(fn)
}
/**
* Register a dev serve config function. It will receive the express `app`
* instance of the dev server.
*
* @param {function} fn
*/
configureDevServer (fn) {this.service.devServerConfigFns.push(fn)
}
/**
* Resolve the final raw webpack config, that will be passed to webpack.
*
* @param {ChainableWebpackConfig} [chainableConfig]
* @return {object} Raw webpack config.
*/
resolveWebpackConfig (chainableConfig) {return this.service.resolveWebpackConfig(chainableConfig)
}
/**
* Resolve an intermediate chainable webpack config instance, which can be
* further tweaked before generating the final raw webpack config.
* You can call this multiple times to generate different branches of the
* base webpack config.
* See https://github.com/mozilla-neutrino/webpack-chain
*
* @return {ChainableWebpackConfig}
*/
resolveChainableWebpackConfig () {return this.service.resolveChainableWebpackConfig()
}
/**
* Generate a cache identifier from a number of variables
*/
genCacheConfig (id, partialIdentifier, configFiles = []) {const fs = require('fs')
const cacheDirectory = this.resolve(`node_modules/.cache/${id}`)
// replace \r\n to \n generate consistent hash
const fmtFunc = conf => {if (typeof conf === 'function') {return conf.toString().replace(/\r\n?/g, '\n')
}
return conf
}
const variables = {
partialIdentifier,
'cli-service': require('../package.json').version,
env: process.env.NODE_ENV,
test: !!process.env.VUE_CLI_TEST,
config: [fmtFunc(this.service.projectOptions.chainWebpack),
fmtFunc(this.service.projectOptions.configureWebpack)
]
}
try {variables['cache-loader'] = require('cache-loader/package.json').version
} catch (e) {// cache-loader is only intended to be used for webpack 4}
if (!Array.isArray(configFiles)) {configFiles = [configFiles]
}
configFiles = configFiles.concat([
'package-lock.json',
'yarn.lock',
'pnpm-lock.yaml'
])
const readConfig = file => {const absolutePath = this.resolve(file)
if (!fs.existsSync(absolutePath)) {return}
if (absolutePath.endsWith('.js')) {
// should evaluate config scripts to reflect environment variable changes
try {return JSON.stringify(require(absolutePath))
} catch (e) {return fs.readFileSync(absolutePath, 'utf-8')
}
} else {return fs.readFileSync(absolutePath, 'utf-8')
}
}
variables.configFiles = configFiles.map(file => {const content = readConfig(file)
return content && content.replace(/\r\n?/g, '\n')
})
const cacheIdentifier = hash(variables)
return {cacheDirectory, cacheIdentifier}
}
}
module.exports = PluginAPI
4. vue-cli-service serve
和 vue-cli-service build
做了什么?
4.1 内置插件 serve 做了什么?
const {
info,
error,
hasProjectYarn,
hasProjectPnpm,
IpcMessenger
} = require('@vue/cli-shared-utils')
const defaults = {
host: '0.0.0.0',
port: 8080,
https: false
}
/** @type {import('@vue/cli-service').ServicePlugin} */
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) {info('Starting development server...')
// ...
// configs that only matters for dev server
api.chainWebpack(webpackConfig => {// ...})
// resolve webpack config
const webpackConfig = api.resolveWebpackConfig()
// check for common config errors
validateWebpackConfig(webpackConfig, api, options)
// ...
// create compiler
const compiler = webpack(webpackConfig)
// handle compiler error
compiler.hooks.failed.tap('vue-cli-service serve', msg => {error(msg)
process.exit(1)
})
// create server
const server = new WebpackDevServer(Object.assign({// ...}), compiler)
// ...
return new Promise((resolve, reject) => {
// ...
resolve({
server,
url: localUrlForBrowser
})
// ...
server.start().catch(err => reject(err))
})
})
}
// ...
module.exports.defaultModes = {serve: 'development'}
调用 PluginAPI
的 registerCommand
办法注册命令,传了三个参数:命令名称、命令的用法配置、命令函数。注册命令其实就是在 service 实例
的 commands
变量增加一个字段,以 {fn: 命令办法, otps: 命令应用办法选项}
的形式存起来
// packages\@vue\cli-service\lib\PluginAPI.js
chainWebpack (fn) {this.service.webpackChainFns.push(fn)
}
resolveWebpackConfig (chainableConfig) {return this.service.resolveWebpackConfig(chainableConfig)
}
// packages\@vue\cli-service\lib\Service.js
resolveChainableWebpackConfig () {const chainableConfig = new Config()
// apply chains
this.webpackChainFns.forEach(fn => fn(chainableConfig))
return chainableConfig
}
resolveWebpackConfig (chainableConfig = this.resolveChainableWebpackConfig()) {if (!this.initialized) {throw new Error('Service must call init() before calling resolveWebpackConfig().')
}
// get raw config
let config = chainableConfig.toConfig()
const original = config
// apply raw config fns
this.webpackRawConfigFns.forEach(fn => {if (typeof fn === 'function') {
// function with optional return value
const res = fn(config)
if (res) config = merge(config, res)
} else if (fn) {
// merge literal values
config = merge(config, fn)
}
})
// #2206 If config is merged by merge-webpack, it discards the __ruleNames
// information injected by webpack-chain. Restore the info so that
// vue inspect works properly.
if (config !== original) {
cloneRuleNames(
config.module && config.module.rules,
original.module && original.module.rules
)
}
// check if the user has manually mutated output.publicPath
const target = process.env.VUE_CLI_BUILD_TARGET
if (
!process.env.VUE_CLI_TEST &&
(target && target !== 'app') &&
config.output.publicPath !== this.projectOptions.publicPath
) {
throw new Error(
`Do not modify webpack output.publicPath directly. ` +
`Use the "publicPath" option in vue.config.js instead.`
)
}
if (
!process.env.VUE_CLI_ENTRY_FILES &&
typeof config.entry !== 'function'
) {
let entryFiles
if (typeof config.entry === 'string') {entryFiles = [config.entry]
} else if (Array.isArray(config.entry)) {entryFiles = config.entry} else {entryFiles = Object.values(config.entry || []).reduce((allEntries, curr) => {return allEntries.concat(curr)
}, [])
}
entryFiles = entryFiles.map(file => path.resolve(this.context, file))
process.env.VUE_CLI_ENTRY_FILES = JSON.stringify(entryFiles)
}
return config
}
serve 的命令办法,其实就做了两件事:
- 通过
api.chainWebpack
注入内置的 webpack 命令,之后通过api.resolveWebpackConfig
来解析 webpack 配置,并通过validateWebpackConfig
办法来验证 webpack 配置格局是否正确,并执行webpack
- 创立
WebpackDevServer
4.2 如何实现 webpack 配置的缝合?
// packages\@vue\cli-service\lib\PluginAPI.js
registerCommand (name, opts, fn) {if (typeof opts === 'function') {
fn = opts
opts = null
}
this.service.commands[name] = {fn, opts: opts || {} }
}
// packages\@vue\cli-service\lib\Service.js
resolveChainableWebpackConfig () {const chainableConfig = new Config()
// apply chains
this.webpackChainFns.forEach(fn => fn(chainableConfig))
return chainableConfig
}
resolveWebpackConfig (chainableConfig = this.resolveChainableWebpackConfig()) {if (!this.initialized) {throw new Error('Service must call init() before calling resolveWebpackConfig().')
}
// get raw config
let config = chainableConfig.toConfig()
const original = config
// apply raw config fns
this.webpackRawConfigFns.forEach(fn => {if (typeof fn === 'function') {
// function with optional return value
const res = fn(config)
if (res) config = merge(config, res)
} else if (fn) {
// merge literal values
config = merge(config, fn)
}
})
// ...
return config
}
能够看出,service
就是通过 webpackChainFns
和 webpackRawConfigFns
记录 webpack 配置,前者通过 webpack-chain
来合并配置,而后者通过 webpack-merge
来合并配置。
此时咱们回头看 Service
的 init
,加载用户配置:
// packages\@vue\cli-service\lib\Service.js
init (mode = process.env.VUE_CLI_MODE) {
// ...
// load user config
// 加载用户配置,就是 vue.config.js
const userOptions = this.loadUserOptions()
const loadedCallback = (loadedUserOptions) => {this.projectOptions = defaultsDeep(loadedUserOptions, defaults())
// apply plugins.
// 注册插件,放到 this.service.commands 下
this.plugins.forEach(({id, apply}) => {if (this.pluginsToSkip.has(id)) return
apply(new PluginAPI(id, this), this.projectOptions)
})
// apply webpack configs from project config file
// 把用户的 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)
}
}
也就是说,初始化的时候,先加载本地配置,加载完了,先插件的注册,用户的 webpack 命令把它们别离存到 webpackChainFns
和 webpackRawConfigFns
,而后执行命令的时候,再把内置的命令 push 进去。
// packages\@vue\cli-service\lib\Service.js
// apply plugins.
this.plugins.forEach(({id, apply}) => {if (this.pluginsToSkip.has(id)) return
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)
}
4.3 serve 内置 webpack 配置了什么?
// configs that only matters for dev server
api.chainWebpack(webpackConfig => {if (process.env.NODE_ENV !== 'production' && process.env.NODE_ENV !== 'test') {if (!webpackConfig.get('devtool')) {
webpackConfig
.devtool('eval-cheap-module-source-map')
}
// https://github.com/webpack/webpack/issues/6642
// https://github.com/vuejs/vue-cli/issues/3539
webpackConfig
.output
.globalObject(`(typeof self !== 'undefined' ? self : this)`)
if (!process.env.VUE_CLI_TEST && options.devServer.progress !== false) {
// the default progress plugin won't show progress due to infrastructreLogging.level
webpackConfig
.plugin('progress')
.use(require('progress-webpack-plugin'))
}
}
})
// resolve webpack config
const webpackConfig = api.resolveWebpackConfig()
// check for common config errors
validateWebpackConfig(webpackConfig, api, options)
// load user devServer options with higher priority than devServer
// in webpack config
const projectDevServerOptions = Object.assign(webpackConfig.devServer || {},
options.devServer
)
// expose advanced stats
if (args.dashboard) {const DashboardPlugin = require('../webpack/DashboardPlugin')
webpackConfig.plugins.push(new DashboardPlugin({type: 'serve'}))
}
// entry arg
const entry = args._[0]
if (entry) {
webpackConfig.entry = {app: api.resolve(entry)
}
}
serve 命令 webpack 配置了:
- 默认配置 sourcemap:
eval-cheap-module-source-map
- 配置 output 的 globalObject 为 self/this,个别用处是作为 library 输入,尤其是
umd
规范,这个全局对象在 node.js/ 浏览器上须要指定为this
,相似 web 的指标则须要指定为self
。所以能够通过这样去赋值:(typeof self !== 'undefined' ? self : this)
- 应用插件
progress-webpack-plugin
显示进度 - 如果参数带了
dashboard
,则加载插件DashboardPlugin
- 配置 entry
剩下的就是配置 webpackDevServer
,就不开展了。同理,咱们能够看看 build
命令内置了哪些 webpack 配置
4.4 build 内置 webpack 配置了什么?
// packages\@vue\cli-service\lib\commands\build\index.js
const defaults = {
clean: true,
target: 'app',
module: true,
formats: 'commonjs,umd,umd-min'
}
const buildModes = {
lib: 'library',
wc: 'web component',
'wc-async': 'web component (async)'
}
const modifyConfig = (config, fn) => {if (Array.isArray(config)) {config.forEach(c => fn(c))
} else {fn(config)
}
}
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)`,
'--dest': `specify output directory (default: ${options.outputDir})`,
'--no-module': `build app without generating <script type="module"> chunks for modern browsers`,
'--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 contents 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`,
'--stdin': `close when stdin ends`
}
}, 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
const {log, execa} = require('@vue/cli-shared-utils')
const {allProjectTargetsSupportModule} = require('../../util/targets')
let needsDifferentialLoading = args.target === 'app' && args.module
if (allProjectTargetsSupportModule) {
log(
`All browser targets in the browserslist configuration have supported ES module.\n` +
`Therefore we don't build two separate bundles for differential loading.\n`
)
needsDifferentialLoading = false
}
args.needsDifferentialLoading = needsDifferentialLoading
if (!needsDifferentialLoading) {await build(args, api, options)
return
}
process.env.VUE_CLI_MODERN_MODE = true
if (!process.env.VUE_CLI_MODERN_BUILD) {
// main-process for legacy build
const legacyBuildArgs = {...args, moduleBuild: false, keepAlive: true}
await build(legacyBuildArgs, api, options)
// spawn sub-process of self for modern build
const cliBin = require('path').resolve(__dirname, '../../../bin/vue-cli-service.js')
await execa('node', [cliBin, 'build', ...rawArgs], {
stdio: 'inherit',
env: {VUE_CLI_MODERN_BUILD: true}
})
} else {
// sub-process for modern build
const moduleBuildArgs = {...args, moduleBuild: true, clean: false}
await build(moduleBuildArgs, api, options)
}
})
}
async function build (args, api, options) {const fs = require('fs-extra')
const path = require('path')
const webpack = require('webpack')
const {chalk} = require('@vue/cli-shared-utils')
const formatStats = require('./formatStats')
const validateWebpackConfig = require('../../util/validateWebpackConfig')
const {
log,
done,
info,
logWithSpinner,
stopSpinner
} = require('@vue/cli-shared-utils')
log()
const mode = api.service.mode
if (args.target === 'app') {
const bundleTag = args.needsDifferentialLoading
? args.moduleBuild
? `module bundle `
: `legacy bundle `
: ``
logWithSpinner(`Building ${bundleTag}for ${mode}...`)
} else {const buildMode = buildModes[args.target]
if (buildMode) {const additionalParams = buildMode === 'library' ? ` (${args.formats})` : ``
logWithSpinner(`Building for ${mode} as ${buildMode}${additionalParams}...`)
} else {throw new Error(`Unknown build target: ${args.target}`)
}
}
if (args.dest) {// Override outputDir before resolving webpack config as config relies on it (#2327)
options.outputDir = args.dest
}
const targetDir = api.resolve(options.outputDir)
const isLegacyBuild = args.needsDifferentialLoading && !args.moduleBuild
// resolve raw webpack config
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)
}
// check for common config errors
validateWebpackConfig(webpackConfig, api, options, args.target)
if (args.watch) {
modifyConfig(webpackConfig, config => {config.watch = true})
}
if (args.stdin) {process.stdin.on('end', () => {process.exit(0)
})
process.stdin.resume()}
// Expose advanced stats
if (args.dashboard) {const DashboardPlugin = require('../../webpack/DashboardPlugin')
modifyConfig(webpackConfig, config => {
config.plugins.push(new DashboardPlugin({
type: 'build',
moduleBuild: args.moduleBuild,
keepAlive: args.keepAlive
}))
})
}
if (args.report || args['report-json']) {const { BundleAnalyzerPlugin} = require('webpack-bundle-analyzer')
modifyConfig(webpackConfig, config => {
const bundleName = args.target !== 'app'
? config.output.filename.replace(/\.js$/, '-')
: isLegacyBuild ? 'legacy-' : ''
config.plugins.push(new BundleAnalyzerPlugin({
logLevel: 'warn',
openAnalyzer: false,
analyzerMode: args.report ? 'static' : 'disabled',
reportFilename: `${bundleName}report.html`,
statsFilename: `${bundleName}report.json`,
generateStatsFile: !!args['report-json']
}))
})
}
if (args.clean) {await fs.emptyDir(targetDir)
}
return new Promise((resolve, reject) => {webpack(webpackConfig, (err, stats) => {stopSpinner(false)
if (err) {return reject(err)
}
if (stats.hasErrors()) {return reject(new Error('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()})
})
}
module.exports.defaultModes = {build: 'production'}
build 命令 webpack 配置了:
- 判断 构建指标 以决定 webpack 入口 entry
- 设置浏览器兼容性,默认开启
古代模式(process.env.VUE_CLI_MODERN_MODE = true)
。当开启process.env.VUE_CLI_MODERN_BUILD
,只打包一个现代版的包,面向反对 ES modules 的古代浏览器,否则会产生两个利用的版本:一个现代版的包,面向反对 ES modules 的古代浏览器,另一个旧版的包,面向不反对的旧浏览器。 - 如果参数传递了
dest
,则指定对应的输入目录 - 依据参数
target
加载不同的 webpack,这里target
的值为app
,所以会加载resolveAppConfig
的配置,并验证 webpack 配置是否非法。见下方代码剖析。 - 如果参数传递了
watch
,则配置为 watch 模式 - 如果参数传递了
stdin
,则监听end
办法,并且调用process.stdin.resume()
来读取流 - 如果参数传递了
dashboard
,则增加DashboardPlugin
插件,能够将 CLI 剖析输入能够以面板的形式输入,成果能够看这里 - 如果参数传递了
report
,则增加BundleAnalyzerPlugin
插件,能够剖析打包大小 - 如果参数传递了
clean
,则打包前先清空目标目录 - 打包
上面看看 resolveAppConfig
做了哪些 webpack 配置
// packages\@vue\cli-service\lib\commands\build\resolveAppConfig.js
module.exports = (api, args, options) => {
// respect inline entry
if (args.entry && !options.pages) {
api.configureWebpack(config => {config.entry = { app: api.resolve(args.entry) }
})
}
const config = api.resolveChainableWebpackConfig()
const targetDir = api.resolve(args.dest || options.outputDir)
// respect inline build destination in copy plugin
if (args.dest && config.plugins.has('copy')) {config.plugin('copy').tap(pluginArgs => {pluginArgs[0].patterns.to = targetDir
return pluginArgs
})
}
if (process.env.VUE_CLI_MODERN_MODE) {const ModernModePlugin = require('../../webpack/ModernModePlugin')
const SafariNomoduleFixPlugin = require('../../webpack/SafariNomoduleFixPlugin')
if (!args.moduleBuild) {
// Inject plugin to extract build stats and write to disk
config
.plugin('modern-mode-legacy')
.use(ModernModePlugin, [{
targetDir,
isModuleBuild: false
}])
} else {
config
.plugin('safari-nomodule-fix')
.use(SafariNomoduleFixPlugin, [{// as we may generate an addition file asset (if Safari 10 fix is needed)
// we need to provide the correct directory for that file to place in
jsDirectory: require('../../util/getAssetPath')(options, 'js')
}])
// Inject plugin to read non-modern build stats and inject HTML
config
.plugin('modern-mode-modern')
.use(ModernModePlugin, [{
targetDir,
isModuleBuild: truea
}])
}
}
return api.resolveWebpackConfig(config)
}
其实也就是针对 古代模式
增加插件,是否既反对古代浏览器 es module 模式的,也反对旧浏览器。还是只须要打包反对古代浏览器的。
5. 如何注册一个 vue-cli-service 命令?
依据官网文档形容,咱们是能够本人写拆件,来扩大 vue-cli-service
命令的。能够参考《uni-app 是如何构建小程序的?》,解析了 uniapp
的 @dcloudio/vue-cli-plugin-uni
包是如何注册一个 vue-cli-service
的 uni-build
命令的。
无非就是应用 PluginAPI
的 api.registerCommand
注册命令,以及应用 api.configureWebpack
和 api.chainWebpack
来合并 webpack 配置。