探索Vue-CLI 源码

542 阅读5分钟

探索Vue-CLI 源码

ps:建议配合源码看、基于@vue/cli 4.5.12

当我们看到vue-cli-service servevue-cli-service build,不想知道是发生什么事吗?

查看node_modules\@vue\cli-service\package.json

 "bin": {
    "vue-cli-service": "bin/vue-cli-service.js"
  }

这部分代码的作用是:当我安装@vue/cli的时候会注册一个vue-cli-service这个命令,也就是我们可以在控制台输入vue-cli-service,会调用bin/vue-cli-service.js,其中后面的参数也会传到bin/vue-cli-service.js这里面

// bin/vue-cli-service.js
const Service = require('../lib/Service')
const service = new Service(process.env.VUE_CLI_CONTEXT || process.cwd())
...
...
const command = args._[0]
service.run(command, args, rawArgv).catch(err => {
  error(err)
  process.exit(1)
})

Service 是一个类,还定义一个变量command(这也就是vue-cli-service serve中的serve参数),它会调用Service类中的run方法

 // ../lib/Service.js
async run (name, args = {}, rawArgv = []) {
    ...
    this.init(mode)
    ...
    let command = this.commands[name]
	...
    ...
    const { fn } = command
    return fn(args, rawArgv)
  }

我们看到有两个关键的地方:this.init(mode)let command = this.commands[name] ,下面会说到这有什么用

// ../lib/Service.js
module.exports = class Service {
  constructor (context, { plugins, pkg, inlineOptions, useBuiltIn } = {}) {
      ...
    // 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)
      ...
  }

Service类被实例化时,会用这么一句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) 
    ...
    ...
    return plugins
}

这个最终会返回一个对象数组,里面包括了builtInPlugins里的数据(因为做了一些处理),每条数据都是一个JSON对象,其中apply这个key里面存的是require('./xxx/xxxx'),它会将每个文件引进来。我们只对./commands/serve做解释。

// ../lib/Service.js

init (mode = process.env.VUE_CLI_MODE) {
	...

    // apply plugins.
    this.plugins.forEach(({ id, apply }) => {
      if (this.pluginsToSkip.has(id)) return
      apply(new PluginAPI(id, this), this.projectOptions)
    })
    
	...
  }

我们再看this.init(),由此可以看到,init的时候它会遍历this.plugins这个数组,也就是上面this.plugins = this.resolvePlugins(plugins, useBuiltIn)这句结果,它会进行遍历并且将apply给结构出来并调用,其实apply()就是我require('./commands/serve')(举个例子) 再调用它。所以我们接下来要看./commands/serve

// ../lib/commands/serve.js
module.exports = (api, options) => {
  api.registerCommand('serve', {
   ...
  }, async function serve (args) {
      ...
})

我们可以看到,serve.js接收两个参数,也就是说apply(new PluginAPI(id, this), this.projectOptions)里面 new PluginAPI(id, this) 等于 apiapi.registerCommand即调用new PluginAPI().registerCommand(),接下来看PluginAPI.js

// ../lib/PluginAPI.js

class PluginAPI {
  constructor (id, service) {
    this.id = id
    this.service = service // 此时this.service 就是 Service 的实例
  }
    
  ...
  registerCommand (name, opts, fn) {
    if (typeof opts === 'function') {
      fn = opts
      opts = null
    }
    this.service.commands[name] = { fn, opts: opts || {}}
  }
  ...
}

../lib/commands/serve.jsapi.registerCommand()就是调用这里的registerCommand方法,name就是serveopts就是一些参数,fn是定义的方法,

最终:this.service.commands['serve'] = { fn, opts: opts || {}}

 // ../lib/Service.js
async run (name, args = {}, rawArgv = []) {
    ...
    this.init(mode)
    ...
    let command = this.commands[name] 
	...
    ...
    const { fn } = command
    return fn(args, rawArgv)
  }

name = 'serve'为例,所以fn就是../lib/commands/serve.js里的async function serve (args)方法

到此为止我们知道了vue-cli-serve run serve 调用的方法就是 ../lib/commands/serve.js里的async function serve (args)方法

快完结了

// ../lib/commands/serve.js
async function serve (args){
	...
    const webpack = require('webpack')
    const WebpackDevServer = require('webpack-dev-server')
    ...
    const webpackConfig = api.resolveWebpackConfig()
    ...
    const compiler = webpack(webpackConfig) // 根据配置通过webpack 得到 编译后结果
    ...
    const server = new WebpackDevServer(compiler,Object.assign{ // 将结果给到WebpackDevServer 起一个服务
                                        ...
                                        })
    server.listen(port, host, err => {
      if (err) {
         reject(err)
      }
    })   
        
}

我们可以看到 vue-cli 最终还是会通过 webpack 进行处理,有一点要讲的是 const webpackConfig = api.resolveWebpackConfig() 它是加载webpack的配置,我们上门说到 **api **其实就是 PluginAPI,我们接着看

// ../lib/PluginAPI.js
resolveWebpackConfig (chainableConfig) {
    return this.service.resolveWebpackConfig(chainableConfig)
}
// this.service 这里指的是Service 因为上面说到的Service 实例 PluginAPI 这个类时  也把自己传给PluginAPI构造器
// ../lib/Service.js
resolveChainableWebpackConfig () {
    const chainableConfig = new Config()
    // apply chains
    this.webpackChainFns.forEach(fn => fn(chainableConfig))
    return chainableConfig
  }

resolveWebpackConfig (chainableConfig = this.resolveChainableWebpackConfig()) {
	...
    // get raw config
    let config = chainableConfig.toConfig()
    ...
    // 对config做一些操作
    ...

    return config
  }

我们看到resolveWebpackConfig方法调用时默认会调用resolveChainableWebpackConfig 方法,在看resolveChainableWebpackConfig方法里面有一句this.webpackChainFns.forEach(fn => fn(chainableConfig))这个webpackChainFns是怎么来的呢?

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

还记得这个吗,我们只讲了./commands/serve里的方法,我们再看看./config/base里面

// ../lib/config/base.js
module.exports = (api, options) => {
   api.chainWebpack(webpackConfig => {
...
...
    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'))
        .set(
          'vue$',
          options.runtimeCompiler
            ? 'vue/dist/vue.esm.js'
            : 'vue/dist/vue.runtime.esm.js'
        )

    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 --------------------------------------------------------------
    const vueLoaderCacheConfig = api.genCacheConfig('vue-loader', {
      'vue-loader': require('vue-loader/package.json').version,
      /* eslint-disable-next-line node/no-extraneous-require */
      '@vue/component-compiler-utils': require('@vue/component-compiler-utils/package.json').version,
      'vue-template-compiler': require('vue-template-compiler/package.json').version
    })

    webpackConfig.module
      .rule('vue')
        .test(/\.vue$/)
        .use('cache-loader')
          .loader('cache-loader')
          .options(vueLoaderCacheConfig)
          .end()
        .use('vue-loader')
          .loader('vue-loader')
          .options(Object.assign({
            compilerOptions: {
              preserveWhitespace: false
            }
          }, vueLoaderCacheConfig))

    webpackConfig
      .plugin('vue-loader')
      .use(require('vue-loader/lib/plugin'))

    // static assets -----------------------------------------------------------

    webpackConfig.module
      .rule('images')
        .test(/\.(png|jpe?g|gif|webp)(\?.*)?$/)
        .use('url-loader')
          .loader('url-loader')
          .options(genUrlLoaderOptions('img'))

    // do not base64-inline SVGs.
    // https://github.com/facebookincubator/create-react-app/pull/1180
    webpackConfig.module
      .rule('svg')
        .test(/\.(svg)(\?.*)?$/)
        .use('file-loader')
          .loader('file-loader')
          .options({
            name: genAssetSubPath('img')
          })

    webpackConfig.module
      .rule('media')
        .test(/\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/)
        .use('url-loader')
          .loader('url-loader')
          .options(genUrlLoaderOptions('media'))

    webpackConfig.module
      .rule('fonts')
        .test(/\.(woff2?|eot|ttf|otf)(\?.*)?$/i)
        .use('url-loader')
          .loader('url-loader')
          .options(genUrlLoaderOptions('fonts'))

    // Other common pre-processors ---------------------------------------------

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

    // shims

    webpackConfig.node
      .merge({
        // prevent webpack from injecting useless setImmediate polyfill because Vue
        // source contains it (although only uses it if it's native).
        setImmediate: false,
        // prevent webpack from injecting mocks to Node native modules
        // that does not make sense for the client
        dgram: 'empty',
        fs: 'empty',
        net: 'empty',
        tls: 'empty',
        child_process: 'empty'
      })

    const resolveClientEnv = require('../util/resolveClientEnv')
    webpackConfig
      .plugin('define')
        .use(require('webpack/lib/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]
        }])
  })
}

我们可以看到这像不像我们一开始用webpack搭建vue环境一样,vue-cli帮我们配置了很多loader之类的,我们看api.chainWebpack,api 老朋友了,我们去看PluginAPI.js

// ../lib/Service.js  
chainWebpack (fn) {
    this.service.webpackChainFns.push(fn)
}

​ 由此可以看到 webpackConfig => { ... ....} 这个方法存到了 this.service.webpackChainFns这个数组里,也就是Service里的webpackChainFns数组,好我们再看回Service.js

// ../lib/Service.js
resolveChainableWebpackConfig () {
    const chainableConfig = new Config()
    // apply chains
    this.webpackChainFns.forEach(fn => fn(chainableConfig))
    return chainableConfig
}

所以, this.webpackChainFns存的是../lib/config/base.js里的方法,并把chainableConfig传进去经过处理返回回来new Config()就是new 了webpack-chain这个插件。vue.config.jsloadUserOptions方法解析,可以自己去看看。

到此为止只是粗略的讲一个流程,其实看起来比较绕,还有一些数据处理比较复杂,但是逻辑其实是清晰明了的。