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 nodeconst { semver, error } = require('@vue/cli-shared-utils')const requiredVersion = require('../package.json').engines.nodeif (!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.jsconst 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.jsconst 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.jsmodule.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配置。