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

515 阅读5分钟

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

run() 方法简析

image-20211215212130117

可以看到,run() 方法接收的第一个参数是一个 name,也就是我们前面在 vue-cli-service.js 文件中调用 run() 方法时传入的 command 的值(执行 npm run serve 时,command 的值就是 serve;执行 npm run build 时,command 的值就是 build),也就是说 name 的值是 serve 或者 build

拿到 name 之后,根据这个 namethis.commands 中取出了对应的 command

取出来 command 之后,又从 command 中解构出了 fn,最后调用了这个 fn() 函数,并返回了函数调用的结果。

以上,就是 run() 方法主要做的事情。

但是,这里的 this.commands 是什么东西?它里面的 command 哪来的?从解构 command 这步操作中我们可以确定它是一个对象,那么这个 command 对象中的 fn 函数又是什么?要弄清楚这些问题,首先得知道 this.commands 到底是什么。

如果我们在当前的 Service.js 文件中搜索 this.command,你会在 Service 类的构造器中找到它的初始值是一个空对象:

image-20211215213023437

而当前文件中找不到再对 this.commands 进行赋值的其它地方了,也就是说,默认情况下它是一个空对象,但是从空对象中肯定取不到什么值(比如 this.commands['serve'] 取到的就是 undefined 了),那到时候 this.commands[name] 取出来的内容肯定不能用啊,那么它肯定是在其它地方又进行了赋值。在哪呢?

事实上,this.commands 真正进行赋值的地方是在 node_modules/@vue/cli-service/lib/PluginAPI.js 文件中的 PluginAPI 类的 registerCommand() 方法中:

image-20211215215919522

直接这么说你可能会懵,所以下面我们一步步来看:

  1. 之前在 vue-cli-service.js 中是通过 new Service() 创建的实例对象 service,即会执行 ../lib/Service.jsService 类的 constructor() 函数,而 constructor() 函数中有这样一行代码:

    image-20211215220647797

    即调用了 Service 类的实例对象的 resolvePlugins() 方法,并将方法调用的返回值赋值给了 Service 类的实例对象的 plugins 属性。

  2. resolvePlugins() 方法的主要内容如下:

    image-20211215221937381

    1. 方法中定义了一个名称为 idToPlugin 箭头函数,其实现的功能是把 id 映射到一个包含 idapply 属性的对象中。
    2. 然后定义了一个名为 builtInPlugins 的数组,数组里面的元素是一个个文件的相对路径,之后在该数组上调用了 map() 方法对这些文件的路径进行映射,映射过程就是去调用上一步的 idToPlugins() 函数。也就是说之后 builtInPlugins 数组中存放的是一个个对象,对象中有 idapply,而 apply 属性的值是 require(id) 的结果,即对应文件的导出内容。
    3. 之后又加载了一些其它插件;
    4. 最后返回了 plugins 数组,里面包含了所有的插件,包括 builtInPlugins 数组中的插件对象。
  3. 再来看 run() 方法,在从 this.commands 中取 command 之前,还调用了 Service 类的实例对象的 init() 方法,这个方法中主要做了四件事:加载环境变量、加载用户配置、应用插件以及应用 webpack 配置。我们来看应用插件这部分代码:

    image-20211216120803004

    可以看到,这里对 this.plugins 数组进行了遍历,根据前面的分析,我们可以将 this.plugins 的值理解成 resolvePlugins() 方法中 builtInPlugins 数组映射之后的数组(该数组中的每个元素都是一个对象,对象中有 idapply 两个属性),所以这里在遍历时解构出了 idapply,之后调用了 apply() 方法,而根据前面的分析,这个 apply() 方法其实是 resolvePlugins() 方法中箭头函数 idToPlugin 返回的对象中的 apply 属性对应的 require(id) 的结果。而 require(id) 中的 id 其实就是 './commands/serve' 或者 './commands/build'builtInPlugins 初始数组中的字符串元素。也就是说,假如这里的 id'./commands/serve',那么 require(id) 就是 require('./commands/serve'),即 ./commands/serve 文件中导出的内容,即 id'./commands/serve'apply() 方法就是 ./commands/serve 文件导出的内容。

  4. 我们来看 node_modules/@vue/cli-service/lib/commands/serve.js 文件:

    image-20211216194207611

    在该文件中,导出了一个箭头函数,也就是说,前面的 apply() 方法就是这个箭头函数了。那么前面在调用 apply() 方法时,其实调用的就是这个箭头函数。而这个箭头函数需要两个参数:apioptions,我们来看这第一个参数 api,前面在 Service.js 中的 init() 方法中应用插件时,遍历了插件,这一过程中调用了 apply() 方法,并且传入了参数,传入的第一个参数是 new PluginAPI(id, this),这里的 this 是当前的 service 对象。也就是说 ./commands/serve 中导出的箭头函数的第一个参数拿到的是一个 PluginAPI 类的实例对象,之后调用了这个实例对象的 registerCommand() 方法。

  5. 所以我们来看 PluginAPI 这个类,这个类在 node_modules/@vue/cli-service/lib/PluginAPI.js 这个文件中:

    image-20211216193038771

    可见,前面我们在 init() 方法中遍历插件时调用 apply() 方法时,传入了 new PluginAPI(id, this) 参数,那么在这个 PluginAPI 类的构造函数中的第二个参数 service,接收到的就是实例对象 servie 了,因此,这里的 this.service 便指向了 service 对象,即 Service.jsrun() 方法中的 this 对象。

    而这里的 registerCommand() 方法在被 lib/commands/serve.js 文件中导出的箭头函数中调用时,接收到的第一个参数 name 的值即为 serve,接收到的第二个参数 opts 的值即为一个对象,接收到的第三个参数 fn 的值即为一个名为 serve 的异步函数。之后就对 this.service.commands 对象(即 Service.jsrun() 方法中的 this.commands 对象)进行了赋值,赋值内容中就有将 fn 函数赋值进去。因此,之后在 Service.jsrun() 方法中才能从 command 对象中解构出 fn 函数。

所以,当我们执行 npm run serve 命令时,传入的 command 的值就是 serve,后续执行的 run() 方法的最后执行的 fn() 方法其实就是 lib/commands/serve.js 文件中导出的箭头函数中调用 api.registerCommand() 方法时传入的第三个参数即 serve() 函数了。