Vue CLI 之 cli-service 源码解析(3)

845 阅读6分钟

「这是我参与2022首次更文挑战的第12天,活动详情查看:2022首次更文挑战」。

1. vue-cli-service 运行流程画图简析

下面,用一张图来简单总结一下 vue-cli-service 命令运行的整个过程:

vue-cli-service 运行流程简析

2. serve.jsserve() 函数简析

下面我们来看 serve() 函数中主要做了什么。

image-20211216223133881

image-20211216223338752

你会发现,serve() 函数中导入了 webpack 函数(通过 require('webpack') 方式导入的其实是一个名为 webpack 的函数,因此,我们完全可以自己编写一套配置,然后将配置传给这个函数进行编译)。

webpack 做编译时有两种方式:

  1. 编写 webpack 的配置文件,然后通过命令去加载这个配置文件;
  2. 编写 webpack 的配置文件,然后自己将这个配置文件传给 webpack 函数并调用这个函数;

除了 webpack,还导入了 webpack-dev-server,由此可见,这里也不是通过命令来开启 DevServer 的,而是直接通过 require 的方式先拿到 WebpackDevServer

之后,通过执行 api.resolveWebpackConfig() 拿到了 webpack 的所有配置(webpackConfig)。接着又对拿到的配置进行了验证。后续又做了一些服务器相关的配置(protocolhostport 等等)。

接着,调用了 webpack 函数,并将 webpack 的所有配置即 webpackConfig 传入了函数,函数调用后返回的是 webpack 配置编译之后的结果(compiler)。之后将这个 compiler 传给了 WebpackDevServer 类的构造函数,获得 WebpackDevServer 类的一个实例对象 server。最后,在 server 对象上调用 listen() 方法来启动服务进行监听。

开启 webpack-dev-server 有两种方式:

  1. 通过执行 webpack serve 命令启动服务器;
  2. 通过传入 webpack 编译好的配置信息来创建 WebpackDevServer 类的实例对象 server,之后在 server 对象上调用 listen() 方法来启动服务进行监听。

以上,就是 serve() 函数中比较大的逻辑:

serve.js 中 serve() 函数的大逻辑

下面,我们再来看加载 webpack 配置信息的具体过程,即 api.resolveWebpackConfig() 执行的内容。根据前面的分析,这里的 apiPluginAPI 类的一个实例对象,所以 api.resolveWebpackConfig() 执行的其实是 PluginsAPI 类中的 resolveWebpackConfig 方法:

image-20211217122343983

而这个 resolveWebpackConfig() 方法中又调用了 Service 类中的 resolveWebpackConfig() 方法,因此,下面我们要看的就是 Service.js 文件中的 resolveWebpackConfig() 方法:

image-20211217124027408

因为前面在 serve() 函数中调用 api.resolveWebpackConfig() 方法时没有传参,所以这里在调用 resolveWebpackConfig() 方法时,该方法的 chainableConfig 参数会被默认赋上 resolveChainableWebpackConfig() 方法调用后的返回值。而在这个 resolveChainableWebpackConfig() 方法中,对 this.webpackChainFns 数组进行了遍历,遍历过程中执行了 this.webpackChainFns 中的每个函数并传入了 new Config() 的结果 chainableConfig,最后返回了 chainableConfig(最终,所有 webpack 的配置其实都会放到这个 chainableConfig 中去)。之后,在 resolveWebpackConfig() 方法中会拿到 chainableConfig 中的配置,然后和 webpack 原始的配置进行合并(merge)。最终再返回合并后的配置信息(config)。

所以,下面我们要看的就是 chainableConfig 中的 webpack 配置信息到底是如何拿到的。事实上,webpack 的配置信息都在 this.webpackChainFns 这个数组中的一个个函数中。那么这些函数是在哪里被添加进 this.webpackChainFns 数组的呢?如果我们在当前的 Service.js 文件中查找 this.webpackChainFns,你会在 init() 方法中找到往 this.webpackChainFns 数组中添加内容的代码:

image-20211217184904967

但事实上,这里加载的是我们项目目录下自己添加的配置,比如添加 vue.config.js 文件,文件中可以有如下配置:

module.exports = {
  chainWebpack() {
    // ...
  },
  configureWebpack: {
    // ...
  }
  // ...
}

这里具体是如何加载的呢?其实,加载过程在 this.loadUserOptions() 方法中:

image-20211217195935502

image-20211217200748720

可以看到,在 loadUserOptions() 方法中是有加载 vue.config.js 文件的,因此,我们到时候在项目目录下编写的 vue.config.js 文件就会在这里被加载。之后,就会通过调用 defaultsDeep(userOptions, defaults()) 将我们自己编写的配置和一些默认的 webpack 配置进行合并了。

关于加载项目目录下自己添加的配置文件中的内容以及将其与默认配置进行合并的过程,你也可以通过添加打印语句进行简单查看:

image-20211217201830225

但是,上面解释的是我们自己编写的一些配置(比如 vue.config.js 文件)是在 loadUserOptions() 方法中进行加载的。那么 webpack 更多的(主要的)配置信息是在哪加载的呢?我们可以来到 Service.js 文件中的 resolvePlugins() 方法中,你会发现在 builtInPlugins 数组中除了有之前我们分析过的 ./command/serve 文件,还包括了 4 个配置文件:

image-20211217202939873

那么这些配置文件导出的函数就会在 run() 方法中调用 this.init() 方法时,在 init() 方法中遍历插件时调用 apply() 方法时被调用。类似于之前我们讲过的,在遍历 this.plugins 时,遍历到 ./commands/serve 对应的函数时,调用 apply() 方法即调用了 ./commands/serve.js 文件中导出的函数,同样地,遍历到 ./config/base 对应的函数时,调用 apply() 方法即调用了 ./config/base.js 文件中导出的函数:

image-20211217204707252

而执行这个函数时,里面执行的则不再是 PluginAPI.js 中的 registerCommand() 方法,而是 chainWebpack() 方法,同时传入了一个接收一个参数(webpackConfig)的箭头函数。所以我们来看 PluginAPI.js 中的 chainWebpack() 方法:

image-20211217205718545

chainWebpack() 方法中将传入的 fn 函数添加进了 this.service.webpackChainFns 数组中。那就意味着 base.js 文件中调用 api.chainWebpack() 方法时传入的 webpackConfig => { ... } 函数就被放入了 webpackChainsFns 数组中。那也意味着,service.webpackChainFns 数组中还被放入了很多函数(config/css.jsconfig/prod.jsconfig/app.js 中都有对应的函数被放入该数组中)。

现在,我们再回到 serve.js 文件中,api.resolveWebpackConfig() 本质上会去调用 PluginAPI.js 文件中的 resolveWebpackConfig() 方法,而这个方法中又调用了 this.service 中的 resolveWebpackConfig() 方法,即 Service.js 文件中的 resolveWebpackConfig() 方法,而调用该方法时,又去调用了 resolveChainableWebpackConfig() 方法,而在该方法中,就有对 this.webpackChainFns 数组进行遍历,而此时,该数组中就已经有一大堆的函数了,这些函数就是 config/base.jsconfig/css.js 等文件中调用 api.chainWebpack() 时传入的函数。那么这些函数中做了什么事情呢?我们以 config/base.js 文件举例:

image-20211218084427874

可以看到,该函数中通过在 webpackConfig 上链式(通过 webpack-chain 这个第三方库实现的)调用很多方法,设置了 webpack 相应的很多配置。同样,在 config/css.jsconfig/prod.jsconfig/app.js 文件中也是以同样的方式添加 webpack 的相关配置的。

因此,在 this.webpackChainFns 数组遍历完成后,chainableConfig 中就被添加了很多 webpack 的配置了,包括之前 init() 方法中调用 loadUserOptions() 方法后加载到的我们自己编写的配置(vue.config.js)。

获取到 webpack 所有的配置(chainableConfig)以后,又对配置内容做了一系列的检查(怕我们有些配置写错了),最终返回了包含了所有配置信息的 config 对象。那么之前我们在 serve.js 中的 webpackConfig 就拿到了所有的 webpack 配置,之后将 webpackConfig 传给了 webpack() 函数进行编译,编译的结果再传给 WebpackDevServer,最后成功开启一个本地服务。