道招

从vuecli3学习webpack记录(一)vue-cli-serve机制

如果您发现本文排版有问题,可以先点击下面的链接切换至老版进行查看!!!

从vuecli3学习webpack记录(一)vue-cli-serve机制

最近看了看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里面的分别对应着commands和config文件夹里面的对应的文件。 根据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 || {}}
}
chainWebpack (fn) {
    this.service.webpackChainFns.push(fn)
}

这里的registerCommand方法使得我们平时用到的vue-cli-service serve的serve命令得以注册。 file vue cli3中在commands文件夹里面的是调用api.registerCommand方法,在config文件夹里面的(teserOptions.js和html除外)是调用api.chainWebpack方法,该方法会将传得的参数(该参数是一个方法)push到this.service.webpackChainFns数组。

特别提醒下:这里的this.serivce.commands就是Service.js里面的this.commands,不是吗?

之后我们看到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的钩子下篇分析。

更新时间:
上一篇:用个数组来理解vue的diff算法(一)下一篇:pm2管理node,运行 npm run server等命令

相关文章

支持取消单选组件vue版

原生的单选就是 <input type="radio"/> ,正常情况在 name 相同的单选之间只能选一个,如果只有一个单选框的情况下,一经选中是无法自己取消的,和 阅读更多…

element-ui表单源码解析之el-input

关于表单校验el-input做的主要工作就是跟el-form-item交互,把input的相关事件发送给el-form-item,上一篇已经讲到在el-form-item的mounted的生命周期里 阅读更多…

vue发送请求是应该在mounted还是在created生命周期

一个经常会被问到的问题: 为什么不在 created 里去发ajax? created 可是比 mounted 更早调用啊,更早调用意味着更早返回结果,那样性能不是更高? 首先,一 阅读更多…

Vue在chrome44偶现点击子元素事件无法冒泡

公司的一个项目大致是这样的:一个左侧列表,点击左侧列表的文章标题,右侧展开该文章对应的内容的。 现在的问题出现在极少部分客户有时左侧的标题,无法打开对应的右侧的内容,给人的改进就是‘卡’、点不动、点 阅读更多…

vue实现自定义组件的v-model双向数据绑定

一般来说我们用v-model是在input中 常见用法如下 <input type="text" v-model="username"> 用户名:{{username}} 其 阅读更多…

用个数组来理解vue的diff算法(一)

原文地址: 道招网 的 用个数组来理解vue的diff算法(一) Vue使用的diff算法,我相信用vue的估计都听过,并且看到源码的也不在少数。 先对下面的代码做下说明: 由于这里用 阅读更多…

关注道招网公众帐号
道招开发者二群