vue-cli之vue-cli-service整體架構淺析。

概述

vue啓動一個項目的時候,需要執行npm run serve,其中這個serve的內容就是vue-cli-service serve。可見,項目的啓動關鍵是這個vue-cli-service與它的參數serve。接下來我們一起看看service中主要寫了什麼東東(主要內容以備註形式寫到代碼中。)。

vue-cli-service.js

const semver = require('semver')
const { error } = require('@vue/cli-shared-utils')
const requiredVersion = require('../package.json').engines.node

// 檢測node版本是否符合vue-cli運行的需求。不符合則打印錯誤並退出。
if (!semver.satisfies(process.version, requiredVersion)) {
  error(
    `You are using Node ${process.version}, but vue-cli-service ` +
    `requires Node ${requiredVersion}.\nPlease upgrade your Node version.`
  )
  process.exit(1)
}

// cli-service的核心類。
const Service = require('../lib/Service')
// 新建一個service的實例。並將項目路徑傳入。一般我們在項目根路徑下運行該cli命令。所以process.cwd()的結果一般是項目根路徑
const service = new Service(process.env.VUE_CLI_CONTEXT || process.cwd())

// 參數處理。
const rawArgv = process.argv.slice(2)
const args = require('minimist')(rawArgv, {
  boolean: [
    // build
    'modern',
    'report',
    'report-json',
    'watch',
    // serve
    'open',
    'copy',
    'https',
    // inspect
    'verbose'
  ]
})
const command = args._[0]

// 將參數傳入service這個實例並啓動後續工作。如果我們運行的是npm run serve。則command = "serve"。
service.run(command, args, rawArgv).catch(err => {
  error(err)
  process.exit(1)
})

Service.js

上面調用了service的run方法,這裏從run開始一路瀏覽即可。

const fs = require('fs')
const path = require('path')
const debug = require('debug')
const chalk = require('chalk')
const readPkg = require('read-pkg')
const merge = require('webpack-merge')
const Config = require('webpack-chain')
const PluginAPI = require('./PluginAPI')
const loadEnv = require('./util/loadEnv')
const defaultsDeep = require('lodash.defaultsdeep')
const { warn, error, isPlugin, loadModule } = require('@vue/cli-shared-utils')

const { defaults, validate } = require('./options')

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 = []
    this.webpackRawConfigFns = []
    this.devServerConfigFns = []
    //存儲的命令。
    this.commands = {}
    // Folder containing the target package.json for plugins
    this.pkgContext = context
    // 鍵值對存儲的pakcage.json對象,不是本文重點。所以未列出該方法實現
    this.pkg = this.resolvePkg(pkg)

    this.plugins = this.resolvePlugins(plugins, useBuiltIn)
    
    // 結果爲{build: production, serve: development, ... }。大意是收集插件中的默認配置信息
    // 標註build命令主要用於生產環境。
    this.modes = this.plugins.reduce((modes, { apply: { defaultModes }}) => {
      return Object.assign(modes, defaultModes)
    }, {})
  }

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

    // 加載.env文件中的配置
    if (mode) {
      this.loadEnv(mode)
    }
    // load base .env
    this.loadEnv()

    // 讀取用戶的配置信息.一般爲vue.config.js
    const userOptions = this.loadUserOptions()
    // 讀取項目的配置信息並與用戶的配置合併(用戶的優先級高)
    this.projectOptions = defaultsDeep(userOptions, defaults())

    debug('vue:project-config')(this.projectOptions)

    // 註冊插件。
    this.plugins.forEach(({ id, apply }) => {
      apply(new PluginAPI(id, this), this.projectOptions)
    })

    // wepback相關配置收集
    if (this.projectOptions.chainWebpack) {
      this.webpackChainFns.push(this.projectOptions.chainWebpack)
    }
    if (this.projectOptions.configureWebpack) {
      this.webpackRawConfigFns.push(this.projectOptions.configureWebpack)
    }
  }


  resolvePlugins (inlinePlugins, useBuiltIn) {
    const idToPlugin = id => ({
      id: id.replace(/^.\//, 'built-in:'),
      apply: require(id)
    })

    let plugins

    const builtInPlugins = [
      './commands/serve',
      './commands/build',
      './commands/inspect',
      './commands/help',
      // config plugins are order sensitive
      './config/base',
      './config/css',
      './config/dev',
      './config/prod',
      './config/app'
    ].map(idToPlugin)

    if (inlinePlugins) {
      plugins = useBuiltIn !== false
        ? builtInPlugins.concat(inlinePlugins)
        : inlinePlugins
    } else {
      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 = () => {}
            try {
              apply = require(id)
            } catch (e) {
              warn(`Optional dependency ${id} is not installed.`)
            }

            return { id, apply }
          } else {
            return idToPlugin(id)
          }
        })
      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)
      })))
    }

    return plugins
  }

  async run (name, args = {}, rawArgv = []) {
    // mode是dev還是prod?
    const mode = args.mode || (name === 'build' && args.watch ? 'development' : this.modes[name])

    // 收集環境變量、插件、用戶配置
    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) {
      command = this.commands.help
    } else {
      args._.shift() // remove command itself
      rawArgv.shift()
    }
    // 執行命令。例如vue-cli-service serve 則,執行serve命令。
    const { fn } = command
    return fn(args, rawArgv)
  }

  // 收集vue.config.js中的用戶配置。並以對象形式返回。
  loadUserOptions () {
    // vue.config.js
    let fileConfig, pkgConfig, resolved, resolvedFrom
    const configPath = (
      process.env.VUE_CLI_SERVICE_CONFIG_PATH ||
      path.resolve(this.context, 'vue.config.js')
    )
    if (fs.existsSync(configPath)) {
      try {
        fileConfig = require(configPath)
        if (!fileConfig || typeof fileConfig !== 'object') {
          error(
            `Error loading ${chalk.bold('vue.config.js')}: should export an object.`
          )
          fileConfig = null
        }
      } catch (e) {
        error(`Error loading ${chalk.bold('vue.config.js')}:`)
        throw e
      }
    }

    // package.vue
    pkgConfig = this.pkg.vue
    if (pkgConfig && typeof pkgConfig !== 'object') {
      error(
        `Error loading vue-cli config in ${chalk.bold(`package.json`)}: ` +
        `the "vue" field should be an object.`
      )
      pkgConfig = null
    }

    if (fileConfig) {
      if (pkgConfig) {
        warn(
          `"vue" field in package.json ignored ` +
          `due to presence of ${chalk.bold('vue.config.js')}.`
        )
        warn(
          `You should migrate it into ${chalk.bold('vue.config.js')} ` +
          `and remove it from package.json.`
        )
      }
      resolved = fileConfig
      resolvedFrom = 'vue.config.js'
    } else if (pkgConfig) {
      resolved = pkgConfig
      resolvedFrom = '"vue" field in package.json'
    } else {
      resolved = this.inlineOptions || {}
      resolvedFrom = 'inline options'
    }

    if (typeof resolved.baseUrl !== 'undefined') {
      if (typeof resolved.publicPath !== 'undefined') {
        warn(
          `You have set both "baseUrl" and "publicPath" in ${chalk.bold('vue.config.js')}, ` +
          `in this case, "baseUrl" will be ignored in favor of "publicPath".`
        )
      } else {
        warn(
          `"baseUrl" option in ${chalk.bold('vue.config.js')} ` +
          `is deprecated now, please use "publicPath" instead.`
        )
        resolved.publicPath = resolved.baseUrl
      }
    }

    // if (typeof resolved.publicPath === 'undefined') {
    //   resolved.publicPath = '/'
    // }

    // normalize some options
    ensureSlash(resolved, 'publicPath')
    if (typeof resolved.publicPath === 'string') {
      resolved.publicPath = resolved.publicPath.replace(/^\.\//, '')
    }
    removeSlash(resolved, 'outputDir')

    // deprecation warning
    // TODO remove in final release
    if (resolved.css && resolved.css.localIdentName) {
      warn(
        `css.localIdentName has been deprecated. ` +
        `All css-loader options (except "modules") are now supported via css.loaderOptions.css.`
      )
    }

    // validate options
    validate(resolved, msg => {
      error(
        `Invalid options in ${chalk.bold(resolvedFrom)}: ${msg}`
      )
    })

    return resolved
  }
}

// 路徑中的斜槓槓處理
function ensureSlash (config, key) {
  let val = config[key]
  if (typeof val === 'string') {
    if (!/^https?:/.test(val)) {
      val = val.replace(/^([^/.])/, '/$1')
    }
    config[key] = val.replace(/([^/])$/, '$1/')
  }
}
// 路徑中的斜槓槓處理
function removeSlash (config, key) {
  if (typeof config[key] === 'string') {
    config[key] = config[key].replace(/\/$/g, '')
  }
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章