深入分析 Vue CLI 源码中 `Service` 类的初始化和插件加载机制,它是如何构建 Webpack 配置的?

嘿,各位!今天咱们来聊聊 Vue CLI 这位前端工程师的“老伙计”,特别是扒一扒它内部的 Service 类,看看它怎么“攒电脑”(构建 Webpack 配置)的。

开场白:Vue CLI,你的 Webpack 好帮手

Vue CLI,全称 Vue Command Line Interface,是 Vue.js 官方提供的脚手架工具。它能帮你快速搭建 Vue 项目,省去配置 Webpack、Babel 等繁琐的步骤。但你有没有好奇过,它到底是怎么做到的? 今天,我们聚焦在 Service 这个类上,揭秘其初始化和插件加载机制,以及如何一步步构建出 Webpack 配置。

第一部分:Service 类的初始化,一切的起点

首先,我们先从 Vue CLI 的源码入手,找到 Service 类。通常,它位于 @vue/cli-service 目录下。 让我们看看 Service 类是如何被初始化的。

// @vue/cli-service/lib/Service.js

class Service {
  constructor (context, { plugins = [], pkg = {}, inlineOptions = {}, useBuiltIn = true } = {}) {
    this.context = context // 项目根目录
    this.inlineOptions = inlineOptions // 命令行传入的选项
    this.plugins = this.resolvePlugins(plugins, pkg) // 解析插件
    this.pkg = this.resolvePkg(pkg) // 项目 package.json 内容
    this.webpackChainFns = []
    this.webpackRawConfigFns = []
    this.devServerConfigFns = []
    this.initialized = false
    this.useBuiltIn = useBuiltIn // 是否使用内置插件

    // 在构造函数中立即执行插件加载
    this.loadPlugins()
  }

  // ... 后面还有很多方法
}

构造函数接收几个重要的参数:

  • context: 项目的根目录,也就是 vue create my-project 之后生成的目录。
  • plugins: 一个数组,包含了要加载的插件。可以是字符串(插件名称)或者对象(包含 idapply 属性)。
  • pkg: 项目的 package.json 文件的内容。
  • inlineOptions: 从命令行传入的选项,比如 --mode development
  • useBuiltIn: 布尔值,决定是否使用 Vue CLI 内置的插件。

关键步骤:插件解析 resolvePlugins

resolvePlugins 方法负责解析插件数组,将字符串形式的插件名称转换为包含 idapply 属性的对象。

  resolvePlugins (plugins, pkg) {
    const idToPlugin = id => ({
      id: id.startsWith('@') || id.startsWith('vue-cli-plugin-') ? id : `vue-cli-plugin-${id}`,
      apply: require(id)
    })

    return plugins.map(plugin => {
      if (typeof plugin === 'string') {
        return idToPlugin(plugin)
      } else if (typeof plugin === 'object' && plugin.service) {
        return {
          id: plugin.id,
          apply: require(path.resolve(this.context, plugin.service))
        }
      } else {
        return plugin
      }
    })
  }

这个方法做了几件事:

  1. 处理插件 ID: 如果插件 ID 不是以 @vue-cli-plugin- 开头,就自动加上 vue-cli-plugin- 前缀。 这样做是为了规范插件的命名,方便查找和管理。
  2. 加载插件: 使用 require 函数加载插件。 如果插件是字符串,直接 require(id)。 如果插件是对象,则 require(path.resolve(this.context, plugin.service))。 这样就可以加载本地的插件。
  3. 返回插件对象: 返回一个包含 idapply 属性的对象。 id 是插件的唯一标识符,apply 是插件的入口函数。

关键步骤:加载插件 loadPlugins

loadPlugins 方法遍历 this.plugins 数组,执行每个插件的 apply 函数。

  loadPlugins () {
    // apply inlineOptions before other plugins
    if (this.inlineOptions.plugins) {
      this.inlineOptions.plugins.forEach(plugin => {
        plugin(this.api, this.inlineOptions)
      })
    }

    for (const plugin of this.plugins) {
      plugin.apply(new PluginAPI(this, plugin.id), this.projectOptions)
    }
  }

这个方法做了几件事:

  1. 处理 inlineOptions.plugins 如果 inlineOptions 中有 plugins 属性,则先执行这些插件。
  2. 遍历插件: 遍历 this.plugins 数组,执行每个插件的 apply 函数。
  3. 创建 PluginAPI 实例: 在执行插件之前,先创建一个 PluginAPI 实例。 PluginAPI 提供了插件与 Vue CLI 交互的接口,比如注册命令、修改 Webpack 配置等。
  4. 执行插件: 调用 plugin.apply(new PluginAPI(this, plugin.id), this.projectOptions) 执行插件。

第二部分:PluginAPI,插件的“遥控器”

PluginAPI 是插件与 Vue CLI 交互的桥梁。它提供了一系列方法,让插件可以注册命令、修改 Webpack 配置、添加开发服务器配置等。

// @vue/cli-service/lib/PluginAPI.js

class PluginAPI {
  constructor (service, id) {
    this.id = id
    this.service = service
  }

  // 注册命令
  registerCommand (name, opts, fn) {
    this.service.commands[name] = { fn, opts }
  }

  // 修改 Webpack 配置
  chainWebpack (fn) {
    this.service.webpackChainFns.push(fn)
  }

  configureWebpack (fn) {
    this.service.webpackRawConfigFns.push(fn)
  }

  // 修改开发服务器配置
  configureDevServer (fn) {
    this.service.devServerConfigFns.push(fn)
  }

  // ... 还有其他方法
}

PluginAPI 提供了以下几个常用的方法:

  • registerCommand(name, opts, fn): 注册一个命令,比如 vue add 命令就是通过这个方法注册的。
  • chainWebpack(fn): 允许插件链式地修改 Webpack 配置。 使用 webpack-chain 这个库,可以更加灵活地修改 Webpack 配置。
  • configureWebpack(fn): 允许插件直接修改 Webpack 配置对象。
  • configureDevServer(fn): 允许插件修改开发服务器的配置。

插件的典型写法

一个典型的插件是这样的:

// my-plugin.js

module.exports = (api, options) => {
  // 注册一个命令
  api.registerCommand('my-command', {
    description: 'My custom command',
    usage: 'vue my-command'
  }, args => {
    console.log('Running my command!')
  })

  // 修改 Webpack 配置
  api.chainWebpack(config => {
    config.module
      .rule('vue')
        .use('vue-loader')
          .loader('vue-loader')
          .tap(options => {
            // 修改 vue-loader 的选项
            options.compilerOptions.preserveWhitespace = false
            return options
          })
  })
}

这个插件做了两件事:

  1. 注册了一个名为 my-command 的命令。
  2. 修改了 Webpack 配置,禁用了 vue-loaderpreserveWhitespace 选项。

第三部分:构建 Webpack 配置,积木搭建游戏

Vue CLI 构建 Webpack 配置的过程,就像搭积木一样,一步一步地将各种配置选项组合起来。

// @vue/cli-service/lib/Service.js

  resolveWebpackConfig () {
    let webpackConfig

    // 1. 从 vue.config.js 中获取配置
    const projectConfig = this.resolveProjectConfig()
    webpackConfig = Object.assign({}, projectConfig)

    // 2. 应用 configureWebpack 钩子
    if (this.webpackRawConfigFns.length > 0) {
      this.webpackRawConfigFns.forEach(fn => {
        if (typeof fn === 'function') {
          // merge result
          webpackConfig = merge(webpackConfig, fn(process.env.NODE_ENV) || {})
        } else if (typeof fn === 'object') {
          webpackConfig = merge(webpackConfig, fn)
        }
      })
    }

    // 3. 创建 webpack-chain 对象
    const chainableConfig = new Config()
    // apply webpackChain hooks
    this.webpackChainFns.forEach(fn => fn(chainableConfig))

    // 4. 合并 webpack-chain 对象到 webpackConfig
    webpackConfig = chainableConfig.toConfig()

    return webpackConfig
  }

这个方法做了以下几件事:

  1. 获取 vue.config.js 中的配置: vue.config.js 是一个可选的配置文件,用户可以在这里自定义 Webpack 配置。
  2. 应用 configureWebpack 钩子: 执行通过 PluginAPI.configureWebpack 注册的回调函数,允许插件直接修改 Webpack 配置对象。
  3. 创建 webpack-chain 对象: 创建一个 webpack-chain 对象,用于链式地修改 Webpack 配置。
  4. 应用 webpackChain 钩子: 执行通过 PluginAPI.chainWebpack 注册的回调函数,允许插件链式地修改 Webpack 配置。
  5. 合并配置:webpack-chain 对象转换为普通的 Webpack 配置对象,并与 vue.config.js 中的配置合并。

配置优先级

各个配置的优先级如下(从低到高):

  1. Vue CLI 默认配置
  2. vue.config.js 中的配置
  3. configureWebpack 钩子
  4. webpackChain 钩子

也就是说,webpackChain 钩子中的配置会覆盖 configureWebpack 钩子中的配置,configureWebpack 钩子中的配置会覆盖 vue.config.js 中的配置,以此类推。

总结:Vue CLI 的配置之道

Vue CLI 通过 Service 类实现了插件化的 Webpack 配置。 插件可以通过 PluginAPI 提供的接口,注册命令、修改 Webpack 配置、添加开发服务器配置等。 构建 Webpack 配置的过程就像搭积木一样,一步一步地将各种配置选项组合起来。

步骤 描述 涉及的方法
1. 初始化 Service 创建 Service 实例,解析插件,加载插件。 constructor, resolvePlugins, loadPlugins
2. 执行插件 遍历插件,执行插件的 apply 函数,并传入 PluginAPI 实例。 loadPlugins
3. 构建 Webpack 配置 vue.config.js 中获取配置,应用 configureWebpackwebpackChain 钩子。 resolveWebpackConfig
4. 合并配置 将各种配置合并成最终的 Webpack 配置对象。 resolveWebpackConfig (内部依赖 webpack-merge 或类似工具)

结束语:掌握 Vue CLI,成为配置高手

通过今天的讲解,相信你对 Vue CLI 的 Service 类有了更深入的了解。 掌握了 Vue CLI 的配置机制,你就可以更加灵活地定制你的 Vue 项目,成为真正的配置高手! 希望今天的分享对你有所帮助,下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注