我用这个方式,拿到了高薪

81 阅读7分钟

vue-cli-service源码的简单分析

高薪就业葵花宝典

用到的包

  "dependencies": {
    "@babel/helper-compilation-targets": "^7.12.16",
    "@soda/friendly-errors-webpack-plugin": "^1.8.0", // 友好的webpack错误输出
    "@soda/get-current-script": "^1.0.2",
    "@types/minimist": "^1.2.0",
    "@vue/cli-overlay": "^5.0.8",
    "@vue/cli-plugin-router": "^5.0.8", // 代码模板相关
    "@vue/cli-plugin-vuex": "^5.0.8", // 代码模板相关
    "@vue/cli-shared-utils": "^5.0.8", // 一些工具方法
    "@vue/component-compiler-utils": "^3.3.0",
    "@vue/vue-loader-v15": "npm:vue-loader@^15.9.7", // vue2的vue-loader
    "@vue/web-component-wrapper": "^1.3.0",
    "acorn": "^8.0.5", // js的解析器,用于打包分析
    "acorn-walk": "^8.0.2", // js的执行器,用于打包分析
    "address": "^1.1.2", // 获取本机的ip地址
    "autoprefixer": "^10.2.4", // postcss插件,自动补前缀
    "browserslist": "^4.16.3", // 浏览器兼容相关
    "case-sensitive-paths-webpack-plugin": "^2.3.0", // 路径大小写敏感
    "cli-highlight": "^2.1.10", // 控制台文本高亮
    "clipboardy": "^2.3.0", // 访问系统剪切板
    "cliui": "^7.0.4", // 创建复杂的多列命令行界面
    "copy-webpack-plugin": "^9.0.1", // webpack的复制插件
    "css-loader": "^6.5.0",
    "css-minimizer-webpack-plugin": "^3.0.2",
    "cssnano": "^5.0.0", // 去除无用css
    "debug": "^4.1.1",
    "default-gateway": "^6.0.3", // 获取系统网关
    "dotenv": "^10.0.0", // 读取.env文件相关
    "dotenv-expand": "^5.1.0", // 读取.env文件相关
    "fs-extra": "^9.1.0", // fs模块的封装版
    "globby": "^11.0.2", // 分析文件目录
    "hash-sum": "^2.0.0", // 哈希值生成工具
    "html-webpack-plugin": "^5.1.0",
    "is-file-esm": "^1.0.0",
    "launch-editor-middleware": "^2.2.1", // 美化控制台输出
    "lodash.defaultsdeep": "^4.6.1",
    "lodash.mapvalues": "^4.6.0",
    "mini-css-extract-plugin": "^2.5.3", // css拆分插件
    "minimist": "^1.2.5", // 读取命令行参数
    "module-alias": "^2.2.2", // yarn的读取node_modules机制
    "portfinder": "^1.0.26", // 查找可用端口
    "postcss": "^8.2.6",
    "postcss-loader": "^6.1.1",
    "progress-webpack-plugin": "^1.0.12",
    "ssri": "^8.0.1",
    "terser-webpack-plugin": "^5.1.1", // 压缩插件
    "thread-loader": "^3.0.0",  // 多线程构建的loader
    "vue-loader": "^17.0.0", // vue3的vue-loader
    "vue-style-loader": "^4.1.3",
    "webpack": "^5.54.0",
    "webpack-bundle-analyzer": "^4.4.0", // webpack打包分析工具
    "webpack-chain": "^6.5.1",  // webpack配置的链式编程
    "webpack-dev-server": "^4.7.3",
    "webpack-merge": "^5.7.3", // 合并wenpack配置的工具
    "webpack-virtual-modules": "^0.4.2",
    "whatwg-fetch": "^3.6.2"
  },

目录结构

.
├── bin
│   └── vue-cli-service.js  // 程序入口文件
├── generator // 代码模板相关的
│   ├── index.js
│   ├── router.js
│   ├── template
│   │   ├── _gitignore
│   │   ├── public
│   │   │   ├── favicon.ico
│   │   │   └── index.html
│   │   └── src
│   │       ├── App.vue
│   │       ├── assets
│   │       │   └── logo.png
│   │       ├── components
│   │       │   └── HelloWorld.vue
│   │       └── main.js
│   └── vuex.js
├── lib
│   ├── commands  // 可执行的一些命令
│   │   ├── build // 生产环境构建命令
│   │   │   ├── demo-lib.html
│   │   │   ├── demo-lib-js.html
│   │   │   ├── demo-wc.html
│   │   │   ├── entry-lib.js
│   │   │   ├── entry-lib-no-default.js
│   │   │   ├── formatStats.js
│   │   │   ├── index.js
│   │   │   ├── resolveAppConfig.js
│   │   │   ├── resolveLibConfig.js
│   │   │   ├── resolveWcConfig.js
│   │   │   ├── resolveWcEntry.js
│   │   │   └── setPublicPath.js
│   │   ├── help.js // help命令
│   │   ├── inspect.js  // 生成webpack配置的命令
│   │   └── serve.js  // 本地开发的命令
│   ├── config  // webpack相关配置
│   │   ├── app.js  // 主要是html-webpack-plugin的配置
│   │   ├── base.js // 基本webpack配置
│   │   ├── css.js  // css相关的webpack配置
│   │   ├── index-default.html  // 默认html模板
│   │   ├── prod.js // 生产环境的webpack配置
│   │   └── terserOptions.js  // 压缩配置
│   ├── options.js  // 默认配置
│   ├── PluginAPI.js  // 插件和service之间的桥梁
│   ├── Service.js  // vue-cli-service的类定义
│   ├── util  // 工具方法
│   │   ├── getAssetPath.js
│   │   ├── getPadLength.js
│   │   ├── isAbsoluteUrl.js
│   │   ├── prepareProxy.js
│   │   ├── prepareURLs.js
│   │   ├── resolveClientEnv.js
│   │   ├── resolveLoaderError.js
│   │   ├── resolveLocal.js
│   │   └── validateWebpackConfig.js
│   └── webpack // 一些自定义的webpack插件
│       ├── analyzeBundle.js
│       ├── CorsPlugin.js
│       ├── DashboardPlugin.js
│       ├── ModernModePlugin.js
│       └── MovePlugin.js
├── LICENSE
├── logo.png
├── package.json
├── README.md
├── types
│   ├── index.d.ts
│   └── ProjectOptions.d.ts
└── webpack.config.js

入口文件

bin/vue-cli-service.js

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, {
  // 下面的表示如果命令行有--modern,就表示modern: true
  boolean: [
    // build
    'modern',
    'report',
    'report-json',
    'inline-vue',
    'watch',
    // serve
    'open',
    'copy',
    'https',
    // inspect
    'verbose'
  ]
})
// 获取要执行命令,例如serve,build...
const command = args._[0]

// 开始执行命令
service.run(command, args, rawArgv).catch(err => {
  error(err)
  process.exit(1)
})

初始化Services实例

  1. 加载package.json文件
  2. 加载插件
  3. 创建模式表

constructor-构造器

/**
   * 
   * @param {*} context 当前工作的环境(目录)
   * @param {*} param1 一些命令行传过来的参数(一般无用) 
   */
  constructor (context, { plugins, pkg, inlineOptions, useBuiltIn } = {}) {
    process.VUE_CLI_SERVICE = this
    this.initialized = false
    // 工作目录
    this.context = context
    // 命令行传过来的参数,一般没用
    this.inlineOptions = inlineOptions
    // chainWebpack的配置
    this.webpackChainFns = []
    // 原生的webpack配置
    this.webpackRawConfigFns = []
    // devServer的配置
    this.devServerConfigFns = []
    // 所有可执行的指令,commands文件夹里面
    this.commands = {}
    // package.json所在的环境(目录)
    this.pkgContext = context
    // 加载package.json
    this.pkg = this.resolvePkg(pkg)
    // 加载插件,包括构建的命令和配置,package.json安装的vue插件
    this.plugins = this.resolvePlugins(plugins, useBuiltIn)
    // 要忽略的插件的id结合
    this.pluginsToSkip = new Set()
    // 合并插件module.exports.defaultModes得到modes
    this.modes = this.plugins.reduce((modes, { apply: { defaultModes } }) => {
      return Object.assign(modes, defaultModes)
    }, {})
  }

resolvePlugins-加载插件

    // 加载插件,包括构建的命令和配置,package.json安装的vue插件
    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',
      // webpack配置
      './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安装的插件
      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)
    }

    // 获取package.json配置中vuePlugins选项的依赖,一般不用
    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
  }

执行命令

  1. 加载环境变量
  2. 合并用户配置
  3. 运行插件
  4. 执行命令

run 方法

  // 执行命令
  async run (name, args = {}, rawArgv = []) {
    // 一般是使用this.modes[name],开发是name为server,构建是name为build
    const mode = args.mode || (name === 'build' && args.watch ? 'development' : this.modes[name])

    // 要忽略的插件
    this.setPluginsToSkip(args, rawArgv)

    // 加载环境变量,合并用户配置,运行插件
    await this.init(mode)

    args._ = args._ || []
    let command = this.commands[name]
    if (!command && name) {
      error(`command "${name}" does not exist.`)
      process.exit(1)
    }
    if (!command || args.help || args.h) {
      command = this.commands.help
    } else {
      args._.shift() // remove command itself
      rawArgv.shift()
    }
    const { fn } = command
    // 执行命令
    return fn(args, rawArgv)
  }

init 方法

  init (mode = process.env.VUE_CLI_MODE) {
    if (this.initialized) {
      return
    }
    this.initialized = true
    this.mode = mode

    // 加载对应模式的.env文件
    if (mode) {
      this.loadEnv(mode)
    }
    // 加载基本的.env文件
    this.loadEnv()

    // 加载用户配置(vue.config.js)
    const userOptions = this.loadUserOptions()
    const loadedCallback = (loadedUserOptions) => {
      // 合并用户配置和默认配置
      this.projectOptions = defaultsDeep(loadedUserOptions, defaults())

      // 校验配置
      debug('vue:project-config')(this.projectOptions)

      this.plugins.forEach(({ id, apply }) => {
        if (this.pluginsToSkip.has(id)) return
        apply(new PluginAPI(id, this), this.projectOptions)
      })
      
      // 加入配置里的chainWebpack
      if (this.projectOptions.chainWebpack) {
        this.webpackChainFns.push(this.projectOptions.chainWebpack)
      }
      // 加入配置里的configureWebpack
      if (this.projectOptions.configureWebpack) {
        this.webpackRawConfigFns.push(this.projectOptions.configureWebpack)
      }
    }

    // 支持promise形式的配置
    if (isPromise(userOptions)) {
      return userOptions.then(loadedCallback)
    } else {
      return loadedCallback(userOptions)
    }
  }

loadEnv 方法

  // 加载环境变量文件
  loadEnv (mode) {
    const logger = debug('vue:env')
    const basePath = path.resolve(this.context, `.env${mode ? `.${mode}` : ``}`)
    const localPath = `${basePath}.local`

    const load = envPath => {
      try {
        // 读取.env文件的变量
        const env = dotenv.config({ path: envPath, debug: process.env.DEBUG })
        // 将env的值赋值到process.env
        dotenvExpand(env)
        logger(envPath, env)
      } catch (err) {
        // only ignore error if file is not found
        if (err.toString().indexOf('ENOENT') < 0) {
          error(err)
        }
      }
    }

    load(localPath)
    load(basePath)

    // by default, NODE_ENV and BABEL_ENV are set to "development" unless mode
    // is production or test. However the value in .env files will take higher
    // priority.
    if (mode) {
      // always set NODE_ENV during tests
      // as that is necessary for tests to not be affected by each other
      const shouldForceDefaultEnv = (
        process.env.VUE_CLI_TEST &&
        !process.env.VUE_CLI_TEST_TESTING_ENV
      )
      const defaultNodeEnv = (mode === 'production' || mode === 'test')
        ? mode
        : 'development'
      if (shouldForceDefaultEnv || process.env.NODE_ENV == null) {
        // 将mode赋值给process.env.NODE_ENV
        process.env.NODE_ENV = defaultNodeEnv
      }
      if (shouldForceDefaultEnv || process.env.BABEL_ENV == null) {
        // 将mode赋值给process.env.BABEL_ENV
        process.env.BABEL_ENV = defaultNodeEnv
      }
    }
  }

PluginAPI

插件执行用到的工具类

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}".`
    )
  }

  // 当前工作目录
  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))
  }

  /**
   * 注册command,插入到service的commands中
   * 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 || {} }
  }

  /**
   * 修改chainWebpack的配置
   * 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)
  }

  /**
   * 修改原生Webpack的配置
   * 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)
  }

  /**
   * 将chainWebpack转成原生的webpack配置
   * 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)
  }

  /**
   * 执行webpackChainFns里面的方法
   * 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()
  }

  /**
   * 生成cache-loader的配置
   * 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
    }

    // 缓存的hash值对象
    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')
    })

    // 转换成hash值
    const cacheIdentifier = hash(variables)
    return { cacheDirectory, cacheIdentifier }
  }
}

插件

配置文件

base.js

module.exports = (api, options) => {
  const cwd = api.getCwd()
  const webpack = require('webpack')
  const vueMajor = require('../util/getVueMajor')(cwd)

  api.chainWebpack(webpackConfig => {
    const isLegacyBundle = process.env.VUE_CLI_MODERN_MODE && !process.env.VUE_CLI_MODERN_BUILD
    const resolveLocal = require('../util/resolveLocal')

    // https://github.com/webpack/webpack/issues/14532#issuecomment-947525539
    webpackConfig.output.set('hashFunction', 'xxhash64')

    // https://github.com/webpack/webpack/issues/11467#issuecomment-691873586
    webpackConfig.module
      .rule('esm')
        .test(/\.m?jsx?$/)
        .resolve.set('fullySpecified', false)

    // 入口和出口
    webpackConfig
      .mode('development')
      .context(api.service.context)
      .entry('app')
        .add('./src/main.js')
        .end()
      .output
        .path(api.resolve(options.outputDir))
        .filename(isLegacyBundle ? '[name]-legacy.js' : '[name].js')
        .publicPath(options.publicPath)

    // 解析模块
    webpackConfig.resolve
      .extensions
        .merge(['.mjs', '.js', '.jsx', '.vue', '.json', '.wasm'])
        .end()
      .modules
        .add('node_modules')
        .add(api.resolve('node_modules'))
        .add(resolveLocal('node_modules'))
        .end()
      .alias
        .set('@', api.resolve('src'))

    // 解析loader
    webpackConfig.resolveLoader
      .modules
        .add('node_modules')
        .add(api.resolve('node_modules'))
        .add(resolveLocal('node_modules'))

    // 下面的包不引用其他包,所以不用去解析
    webpackConfig.module
      .noParse(/^(vue|vue-router|vuex|vuex-router-sync)$/)

    // js is handled by cli-plugin-babel ---------------------------------------

    // vue-loader --------------------------------------------------------------
    let cacheLoaderPath
    try {
      cacheLoaderPath = require.resolve('cache-loader')
    } catch (e) {}

    if (vueMajor === 2) {
      // for Vue 2 projects
      const partialIdentifier = {
        'vue-loader': require('@vue/vue-loader-v15/package.json').version,
        '@vue/component-compiler-utils': require('@vue/component-compiler-utils/package.json').version
      }

      try {
        partialIdentifier['vue-template-compiler'] = require('vue-template-compiler/package.json').version
      } catch (e) {
        // For Vue 2.7 projects, `vue-template-compiler` is not required
      }

      const vueLoaderCacheConfig = api.genCacheConfig('vue-loader', partialIdentifier)

      webpackConfig.resolve
        .alias
          .set(
            'vue$',
            options.runtimeCompiler
              ? 'vue/dist/vue.esm.js'
              : 'vue/dist/vue.runtime.esm.js'
          )

      if (cacheLoaderPath) {
        webpackConfig.module
          .rule('vue')
            .test(/\.vue$/)
            .use('cache-loader')
              .loader(cacheLoaderPath)
              .options(vueLoaderCacheConfig)
      }

      webpackConfig.module
        .rule('vue')
          .test(/\.vue$/)
          .use('vue-loader')
            .loader(require.resolve('@vue/vue-loader-v15'))
            .options(Object.assign({
              compilerOptions: {
                whitespace: 'condense'
              }
            }, cacheLoaderPath ? vueLoaderCacheConfig : {}))

      webpackConfig
        .plugin('vue-loader')
          .use(require('@vue/vue-loader-v15').VueLoaderPlugin)

      // some plugins may implicitly relies on the `vue-loader` dependency path name
      // such as vue-cli-plugin-apollo
      // <https://github.com/Akryum/vue-cli-plugin-apollo/blob/d9fe48c61cc19db88fef4e4aa5e49b31aa0c44b7/index.js#L88>
      // so we need a hotfix for that
      webpackConfig
        .resolveLoader
          .modules
            .prepend(path.resolve(__dirname, './vue-loader-v15-resolve-compat'))
    } else if (vueMajor === 3) {
      // for Vue 3 projects
      const vueLoaderCacheConfig = api.genCacheConfig('vue-loader', {
        'vue-loader': require('vue-loader/package.json').version
      })

      webpackConfig.resolve
        .alias
          .set(
            'vue$',
            options.runtimeCompiler
              ? 'vue/dist/vue.esm-bundler.js'
              : 'vue/dist/vue.runtime.esm-bundler.js'
          )

      if (cacheLoaderPath) {
        webpackConfig.module
          .rule('vue')
            .test(/\.vue$/)
            .use('cache-loader')
              .loader(cacheLoaderPath)
              .options(vueLoaderCacheConfig)
      }

      webpackConfig.module
        .rule('vue')
          .test(/\.vue$/)
          .use('vue-loader')
            .loader(require.resolve('vue-loader'))
            .options({
              ...vueLoaderCacheConfig,
              babelParserPlugins: ['jsx', 'classProperties', 'decorators-legacy']
            })

      webpackConfig
        .plugin('vue-loader')
          .use(require('vue-loader').VueLoaderPlugin)

      // feature flags <http://link.vuejs.org/feature-flags>
      webpackConfig
        .plugin('feature-flags')
          .use(webpack.DefinePlugin, [{
            __VUE_OPTIONS_API__: 'true',
            __VUE_PROD_DEVTOOLS__: 'false'
          }])
    }

    // https://github.com/vuejs/vue-loader/issues/1435#issuecomment-869074949
    webpackConfig.module
      .rule('vue-style')
        .test(/\.vue$/)
          .resourceQuery(/type=style/)
            .sideEffects(true)

    // Other common pre-processors ---------------------------------------------
    const maybeResolve = name => {
      try {
        return require.resolve(name)
      } catch (error) {
        return name
      }
    }

    webpackConfig.module
      .rule('pug')
        .test(/\.pug$/)
          .oneOf('pug-vue')
            .resourceQuery(/vue/)
            .use('pug-plain-loader')
              .loader(maybeResolve('pug-plain-loader'))
              .end()
            .end()
          .oneOf('pug-template')
            .use('raw')
              .loader(maybeResolve('raw-loader'))
              .end()
            .use('pug-plain-loader')
              .loader(maybeResolve('pug-plain-loader'))
              .end()
            .end()

    const resolveClientEnv = require('../util/resolveClientEnv')
    webpackConfig
      .plugin('define')
        .use(webpack.DefinePlugin, [
          resolveClientEnv(options)
        ])

    webpackConfig
      .plugin('case-sensitive-paths')
        .use(require('case-sensitive-paths-webpack-plugin'))

    // friendly error plugin displays very confusing errors when webpack
    // fails to resolve a loader, so we provide custom handlers to improve it
    const { transformer, formatter } = require('../util/resolveLoaderError')
    webpackConfig
      .plugin('friendly-errors')
        .use(require('@soda/friendly-errors-webpack-plugin'), [{
          additionalTransformers: [transformer],
          additionalFormatters: [formatter]
        }])

    const TerserPlugin = require('terser-webpack-plugin')
    const terserOptions = require('./terserOptions')
    webpackConfig.optimization
      .minimizer('terser')
        .use(TerserPlugin, [terserOptions(options)])
  })
}

命令

server.js

// 默认配置
const defaults = {
  host: '0.0.0.0',
  port: 8080,
  https: false
}

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...')

    // although this is primarily a dev server, it is possible that we
    // are running it in a mode with a production env, e.g. in E2E tests.
    // 是否在docker容器中
    const isInContainer = checkInContainer()
    // 是否生产环境
    const isProduction = process.env.NODE_ENV === 'production'

    const url = require('url')
    const { chalk } = require('@vue/cli-shared-utils')
    const webpack = require('webpack')
    const WebpackDevServer = require('webpack-dev-server')
    const portfinder = require('portfinder')
    const prepareURLs = require('../util/prepareURLs')
    const prepareProxy = require('../util/prepareProxy')
    const launchEditorMiddleware = require('launch-editor-middleware')
    const validateWebpackConfig = require('../util/validateWebpackConfig')
    const isAbsoluteUrl = require('../util/isAbsoluteUrl')

    // configs that only matters for dev server
    api.chainWebpack(webpackConfig => {
      if (process.env.NODE_ENV !== 'production' && process.env.NODE_ENV !== 'test') {
        webpackConfig
          .devtool('cheap-module-eval-source-map')

        // 热加载插件
        webpackConfig
          .plugin('hmr')
            .use(require('webpack/lib/HotModuleReplacementPlugin'))

        // 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) {
          // 打包进度插件
          webpackConfig
            .plugin('progress')
            .use(require('webpack/lib/ProgressPlugin'))
        }
      }
    })

    // 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 = webpackConfig.plugins || []).push(new DashboardPlugin({
        type: 'serve'
      }))
    }

    // entry arg
    const entry = args._[0]
    if (entry) {
      webpackConfig.entry = {
        app: api.resolve(entry)
      }
    }

    // resolve server options
    const useHttps = args.https || projectDevServerOptions.https || defaults.https
    const protocol = useHttps ? 'https' : 'http'
    const host = args.host || process.env.HOST || projectDevServerOptions.host || defaults.host
    portfinder.basePort = args.port || process.env.PORT || projectDevServerOptions.port || defaults.port
    const port = await portfinder.getPortPromise()
    const rawPublicUrl = args.public || projectDevServerOptions.public
    const publicUrl = rawPublicUrl
      ? /^[a-zA-Z]+:\/\//.test(rawPublicUrl)
        ? rawPublicUrl
        : `${protocol}://${rawPublicUrl}`
      : null

    const urls = prepareURLs(
      protocol,
      host,
      port,
      isAbsoluteUrl(options.publicPath) ? '/' : options.publicPath
    )
    const localUrlForBrowser = publicUrl || urls.localUrlForBrowser

    const proxySettings = prepareProxy(
      projectDevServerOptions.proxy,
      api.resolve('public')
    )

    // inject dev & hot-reload middleware entries
    if (!isProduction) {
      const sockjsUrl = publicUrl
        // explicitly configured via devServer.public
        ? `?${publicUrl}/sockjs-node`
        : isInContainer
          // can't infer public network url if inside a container...
          // use client-side inference (note this would break with non-root publicPath)
          ? ``
          // otherwise infer the url
          : `?` + url.format({
            protocol,
            port,
            hostname: urls.lanUrlForConfig || 'localhost',
            pathname: '/sockjs-node'
          })
      const devClients = [
        // dev server client
        require.resolve(`webpack-dev-server/client`) + sockjsUrl,
        // hmr client
        require.resolve(projectDevServerOptions.hotOnly
          ? 'webpack/hot/only-dev-server'
          : 'webpack/hot/dev-server')
        // TODO custom overlay client
        // `@vue/cli-overlay/dist/client`
      ]
      if (process.env.APPVEYOR) {
        devClients.push(`webpack/hot/poll?500`)
      }
      // inject dev/hot client
      addDevClientToEntry(webpackConfig, devClients)
    }

    // create compiler
    const compiler = webpack(webpackConfig)

    // create server
    const server = new WebpackDevServer(compiler, Object.assign({
      logLevel: 'silent',
      clientLogLevel: 'silent',
      historyApiFallback: {
        disableDotRule: true,
        rewrites: genHistoryApiFallbackRewrites(options.publicPath, options.pages)
      },
      contentBase: api.resolve('public'),
      watchContentBase: !isProduction,
      hot: !isProduction,
      injectClient: false,
      compress: isProduction,
      publicPath: options.publicPath,
      overlay: isProduction // TODO disable this
        ? false
        : { warnings: false, errors: true }
    }, projectDevServerOptions, {
      https: useHttps,
      proxy: proxySettings,
      // eslint-disable-next-line no-shadow
      before (app, server) {
        // launch editor support.
        // this works with vue-devtools & @vue/cli-overlay
        app.use('/__open-in-editor', launchEditorMiddleware(() => console.log(
          `To specify an editor, specify the EDITOR env variable or ` +
          `add "editor" field to your Vue project config.\n`
        )))
        // allow other plugins to register middlewares, e.g. PWA
        api.service.devServerConfigFns.forEach(fn => fn(app, server))
        // apply in project middlewares
        projectDevServerOptions.before && projectDevServerOptions.before(app, server)
      },
      // avoid opening browser
      open: false
    }))

    ;['SIGINT', 'SIGTERM'].forEach(signal => {
      process.on(signal, () => {
        server.close(() => {
          process.exit(0)
        })
      })
    })

    if (args.stdin) {
      process.stdin.on('end', () => {
        server.close(() => {
          process.exit(0)
        })
      })

      process.stdin.resume()
    }

    // on appveyor, killing the process with SIGTERM causes execa to
    // throw error
    if (process.env.VUE_CLI_TEST) {
      process.stdin.on('data', data => {
        if (data.toString() === 'close') {
          console.log('got close signal!')
          server.close(() => {
            process.exit(0)
          })
        }
      })
    }

    return new Promise((resolve, reject) => {
      // log instructions & open browser on first compilation complete
      let isFirstCompile = true
      compiler.hooks.done.tap('vue-cli-service serve', stats => {
        if (stats.hasErrors()) {
          return
        }

        let copied = ''
        if (isFirstCompile && args.copy) {
          try {
            require('clipboardy').writeSync(localUrlForBrowser)
            copied = chalk.dim('(copied to clipboard)')
          } catch (_) {
            /* catch exception if copy to clipboard isn't supported (e.g. WSL), see issue #3476 */
          }
        }

        const networkUrl = publicUrl
          ? publicUrl.replace(/([^/])$/, '$1/')
          : urls.lanUrlForTerminal

        console.log()
        console.log(`  App running at:`)
        console.log(`  - Local:   ${chalk.cyan(urls.localUrlForTerminal)} ${copied}`)
        if (!isInContainer) {
          console.log(`  - Network: ${chalk.cyan(networkUrl)}`)
        } else {
          console.log()
          console.log(chalk.yellow(`  It seems you are running Vue CLI inside a container.`))
          if (!publicUrl && options.publicPath && options.publicPath !== '/') {
            console.log()
            console.log(chalk.yellow(`  Since you are using a non-root publicPath, the hot-reload socket`))
            console.log(chalk.yellow(`  will not be able to infer the correct URL to connect. You should`))
            console.log(chalk.yellow(`  explicitly specify the URL via ${chalk.blue(`devServer.public`)}.`))
            console.log()
          }
          console.log(chalk.yellow(`  Access the dev server via ${chalk.cyan(
            `${protocol}://localhost:<your container's external mapped port>${options.publicPath}`
          )}`))
        }
        console.log()

        if (isFirstCompile) {
          isFirstCompile = false

          if (!isProduction) {
            const buildCommand = hasProjectYarn(api.getCwd()) ? `yarn build` : hasProjectPnpm(api.getCwd()) ? `pnpm run build` : `npm run build`
            console.log(`  Note that the development build is not optimized.`)
            console.log(`  To create a production build, run ${chalk.cyan(buildCommand)}.`)
          } else {
            console.log(`  App is served in production mode.`)
            console.log(`  Note this is for preview or E2E testing only.`)
          }
          console.log()

          if (args.open || projectDevServerOptions.open) {
            const pageUri = (projectDevServerOptions.openPage && typeof projectDevServerOptions.openPage === 'string')
              ? projectDevServerOptions.openPage
              : ''
            openBrowser(localUrlForBrowser + pageUri)
          }

          // Send final app URL
          if (args.dashboard) {
            const ipc = new IpcMessenger()
            ipc.send({
              vueServe: {
                url: localUrlForBrowser
              }
            })
          }

          // resolve returned Promise
          // so other commands can do api.service.run('serve').then(...)
          resolve({
            server,
            url: localUrlForBrowser
          })
        } else if (process.env.VUE_CLI_TEST) {
          // signal for test to check HMR
          console.log('App updated')
        }
      })

      server.listen(port, host, err => {
        if (err) {
          reject(err)
        }
      })
    })
  })
}

function addDevClientToEntry (config, devClient) {
  const { entry } = config
  if (typeof entry === 'object' && !Array.isArray(entry)) {
    Object.keys(entry).forEach((key) => {
      entry[key] = devClient.concat(entry[key])
    })
  } else if (typeof entry === 'function') {
    config.entry = entry(devClient)
  } else {
    config.entry = devClient.concat(entry)
  }
}

// https://stackoverflow.com/a/20012536
function checkInContainer () {
  const fs = require('fs')
  if (fs.existsSync(`/proc/1/cgroup`)) {
    const content = fs.readFileSync(`/proc/1/cgroup`, 'utf-8')
    return /:\/(lxc|docker|kubepods)\//.test(content)
  }
}

function genHistoryApiFallbackRewrites (baseUrl, pages = {}) {
  const path = require('path')
  const multiPageRewrites = Object
    .keys(pages)
    // sort by length in reversed order to avoid overrides
    // eg. 'page11' should appear in front of 'page1'
    .sort((a, b) => b.length - a.length)
    .map(name => ({
      from: new RegExp(`^/${name}`),
      to: path.posix.join(baseUrl, pages[name].filename || `${name}.html`)
    }))
  return [
    ...multiPageRewrites,
    { from: /./, to: path.posix.join(baseUrl, 'index.html') }
  ]
}

module.exports.defaultModes = {
  serve: 'development'
}

----------------下方加我,要面试资料--------------------------

add0fee5c92ab28cac595c50a0b9265.jpg