乐趣区

关于vue.js:vuecliservice源码分析

1. vue-cli-service 解决什么问题?

依据官网文档的形容,vue-cli-servicevue-cli 的运行时依赖。它能够:

  1. 基于 webpack 构建,内置了正当的默认配置;
  2. 能够通过配置文件进行配置 webpack;
  3. 能够通过插件扩大 vue-cli-service 的命令

2. 带着问题看源码

  1. vue-cli-service 次要流程是怎么的?
  2. vue-cli-service 帮咱们预设了正当的 webpack 配置,也反对咱们在 vue.config.js 里批改 webpack 配置,是如何做到 webpack 配置的缝合的?
  3. vue-cli-service servevue-cli-service build 做了什么?
  4. 如何注册一个新的命令?

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 servevue-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'}

调用 PluginAPIregisterCommand 办法注册命令,传了三个参数:命令名称、命令的用法配置、命令函数。注册命令其实就是在 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 的命令办法,其实就做了两件事:

  1. 通过 api.chainWebpack 注入内置的 webpack 命令,之后通过 api.resolveWebpackConfig 来解析 webpack 配置,并通过 validateWebpackConfig 办法来验证 webpack 配置格局是否正确,并执行 webpack
  2. 创立 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 就是通过 webpackChainFnswebpackRawConfigFns 记录 webpack 配置,前者通过 webpack-chain 来合并配置,而后者通过 webpack-merge 来合并配置。
此时咱们回头看 Serviceinit,加载用户配置:

// 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 命令把它们别离存到 webpackChainFnswebpackRawConfigFns,而后执行命令的时候,再把内置的命令 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 配置了:

  1. 默认配置 sourcemap:eval-cheap-module-source-map
  2. 配置 output 的 globalObject 为 self/this,个别用处是作为 library 输入,尤其是 umd 规范,这个全局对象在 node.js/ 浏览器上须要指定为 this,相似 web 的指标则须要指定为 self。所以能够通过这样去赋值:(typeof self !== 'undefined' ? self : this)
  3. 应用插件 progress-webpack-plugin 显示进度
  4. 如果参数带了 dashboard,则加载插件 DashboardPlugin
  5. 配置 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 配置了:

  1. 判断 构建指标 以决定 webpack 入口 entry
  2. 设置浏览器兼容性,默认开启 古代模式(process.env.VUE_CLI_MODERN_MODE = true)。当开启 process.env.VUE_CLI_MODERN_BUILD,只打包一个现代版的包,面向反对 ES modules 的古代浏览器,否则会产生两个利用的版本:一个现代版的包,面向反对 ES modules 的古代浏览器,另一个旧版的包,面向不反对的旧浏览器。
  3. 如果参数传递了 dest,则指定对应的输入目录
  4. 依据参数 target 加载不同的 webpack,这里 target 的值为 app,所以会加载 resolveAppConfig 的配置,并验证 webpack 配置是否非法。见下方代码剖析。
  5. 如果参数传递了 watch,则配置为 watch 模式
  6. 如果参数传递了 stdin,则监听 end 办法,并且调用 process.stdin.resume() 来读取流
  7. 如果参数传递了 dashboard,则增加 DashboardPlugin 插件,能够将 CLI 剖析输入能够以面板的形式输入,成果能够看这里
  8. 如果参数传递了 report,则增加 BundleAnalyzerPlugin 插件,能够剖析打包大小
  9. 如果参数传递了 clean,则打包前先清空目标目录
  10. 打包

上面看看 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-serviceuni-build 命令的。
无非就是应用 PluginAPIapi.registerCommand 注册命令,以及应用 api.configureWebpackapi.chainWebpack 来合并 webpack 配置。

退出移动版