最近看了看vuecli3,把自己的学习记录下来
首先看入口npm run dev即是vue-cli-service serve,之所以能运行vue-cli-service命令,就是因为cli3为我们安装了vue-cli-service(执行的是npm install --save-dev vue-cli-service),这样可以在node_modules/.bin文件夹里面找到vue-cli-service.js,上面实际上执行的就是node_modules/.bin/service serve命名

看看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, {
  boolean: [
    // build
    'modern',
    'report',
    'report-json',
    'watch',
    // serve
    'open',
    'copy',
    'https',
    // inspect
    'verbose'
  ]
})
const command = args._[0]

service.run(command, args, rawArgv).catch(err => {
  error(err)
  process.exit(1)
})

可以看出实际运行的就是run方法
这里的Service就是在node_modules/@vue/cli-service/lib/Service.js文件。
Service是一个class
先看构造函数

// lib/Service.js
constructor (context, { plugins, pkg, inlineOptions, useBuiltIn } = {}) {
    process.VUE_CLI_SERVICE = this
    this.initialized = false
    this.context = context
    this.inlineOptions = inlineOptions
    this.webpackChainFns = []
    this.webpackRawConfigFns = []
    this.devServerConfigFns = []
    this.commands = {}
    // Folder containing the target package.json for plugins
    this.pkgContext = context
    // package.json containing the plugins
    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)
    // 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.
    this.modes = this.plugins.reduce((modes, { apply: { defaultModes }}) => {
      return Object.assign(modes, defaultModes)
    }, {})
  }

然后看run()方法

// lib/Service.js
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])

    // load env variables, load user config, apply plugins
    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)
  }

可以看出最终执行语句是(后面还会提到的)

 const { fn } = command
 return fn(args, rawArgv)

command的fn是是怎么来的呢?这就只能追溯到构造函数了。
主要是看这一句this.plugins = this.resolvePlugins(plugins, useBuiltIn)

// lib/Service.js
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
  }


我们可以看到这里的builtInPlugins里面的分别对应着commandsconfig文件夹里面的对应的文件。
根据idToPlugin方法和return { id, apply },我可以知道最后得到的plugins都是形如{ id, apply }的对象了。
比如:

// lib/Service.js
{
    id: 'built-in:commands/serve'
    apply: require('./commands/serve')
}

调用apply方法就会加载对应文件模块

在上面提到的run方法里面有调用init方法。
init方法如下

// lib/Service.js
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()
    this.projectOptions = defaultsDeep(userOptions, defaults())

    debug('vue:project-config')(this.projectOptions)
    // apply plugins.
    this.plugins.forEach(({ id, apply }) => {
      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)
    }

这里有

// lib/Service.js
this.plugins.forEach(({ id, apply }) => {
      apply(new PluginAPI(id, this), this.projectOptions)
})

我们先看apply,就是执行对应的文件,这里我们以常用的./commands/serve.js为例。

// lib/commands/serve.js
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`,
      '--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`
    }
  }, async function serve (args) {
      ...
      const compiler = webpack(webpackConfig)
      const server = new WebpackDevServer(compiler, Object.assign({...}, {...});
      ...
      return new Promise((resolve, reject) => {
          ...
          compiler.hooks.done.tap('vue-cli-service serve', stats => {..})
          server.listen(port, host, err => {
            if (err) {
              reject(err)
            }
          })
      })
  }
}

我们主要看看第一个参数api,它就是上面的PluginAPI的实例new PluginAPI(id, this)(这里的this就是service哦):

// /lib/PluginAPI.js
constructor (id, service) {
    this.id = id
    this.service = service
}

registerCommand (name, opts, fn) {
    if (typeof opts === 'function') {
      fn = opts
      opts = null
    }
    this.service.commands[name] = { fn, opts: opts || {}}
}

这里的registerCommand方法使得我们平时用到的vue-cli-service serveserve命令得以注册。之类我们看到service里面的每个commands是形如{ fn, opts: opts || {}}的对象。
对上面的fn有印象吧,一开始我们提到

// /lib/Service.js
const { fn } = command
return fn(args, rawArgv)

上面的api.registerCommand的第三个参数(函数)就是fn了。实际我们执行的就是它了。vue-cli-service serve返回的是一个promise。并且在

compiler.hooks.done.tap('vue-cli-service serve', stats => { ...})

里面resolve该promise。
关于webpack的钩子下篇分析。


发表评论

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据