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

Vue CLI Service 剖析:Webpack 配置的炼金术

大家好,我是你们今天的导游,带大家一起深入 Vue CLI 的腹地,扒一扒 Service 这个核心类的底裤,看看它到底是如何施展魔法,把一堆插件和配置揉捏成一个 webpack 配置的。

准备好了吗?让我们开始这场代码探险之旅吧!

1. Service:Vue CLI 的大脑

首先,我们需要明确 Service 在 Vue CLI 中扮演的角色。简单来说,它就像一个大脑,负责:

  • 初始化项目: 创建必要的目录结构,生成配置文件等。
  • 加载插件:package.jsonvue.config.js 中识别并加载插件。
  • 构建 Webpack 配置: 基于插件和用户配置,生成最终的 Webpack 配置对象。
  • 执行任务: 运行 servebuildinspect 等命令。

也就是说,我们看到的那些酷炫的特性,比如热重载、代码分割、ESLint 集成等等,背后都离不开 Service 的辛勤工作。

2. Service 的初始化:地基的奠定

Service 的初始化过程主要发生在 packages/@vue/cli-service/lib/Service.js 这个文件中。让我们一步步来解剖它。

// 引入依赖
const PluginAPI = require('./PluginAPI')
const Config = require('webpack-chain')
const path = require('path')
const fs = require('fs')
const { defaults } = require('lodash')

class Service {
  constructor (context, { plugins, pkg, inlineOptions = {} } = {}) {
    this.context = context // 项目根目录
    this.inlineOptions = inlineOptions // 命令行传入的配置
    this.webpackChainFns = []
    this.webpackRawConfigFns = []
    this.devServerFns = []
    this.commands = {}

    // 加载 package.json
    this.pkg = this.resolvePkg(pkg)

    // 加载插件
    this.plugins = this.resolvePlugins(plugins)

    // 加载用户配置 (vue.config.js)
    this.resolveUserOptions()

    // 应用插件
    this.initPlugins()

  }

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

module.exports = Service

可以看到,Service 的构造函数主要做了以下几件事:

  1. 接收参数: 接收项目根目录 context、插件列表 pluginspackage.json 内容 pkg 和命令行配置 inlineOptions
  2. 初始化属性: 初始化一些内部属性,比如 webpackChainFnswebpackRawConfigFns 等,这些属性用于存储插件提供的配置函数。
  3. 加载 package.json 使用 resolvePkg 方法加载 package.json 文件。
  4. 加载插件: 使用 resolvePlugins 方法加载插件。
  5. 加载用户配置: 使用 resolveUserOptions 方法加载 vue.config.js 文件。
  6. 应用插件: 使用 initPlugins 方法应用插件,这是最关键的一步。

我们重点关注 resolvePluginsinitPlugins 这两个方法。

2.1 resolvePlugins:插件的寻宝游戏

resolvePlugins 的作用是根据 package.jsonvue.config.js 中的配置,找到需要加载的插件。

  resolvePlugins (inlinePlugins) {
    const idToPlugin = id => ({
      id: id.replace(/^.//, 'built-in:'),
      apply: require(id)
    })

    let plugins

    const builtInPlugins = [
      './commands/serve',
      './commands/build',
      './commands/inspect',
      './commands/help',
      './commands/info'
    ].map(idToPlugin)

    if (inlinePlugins) {
      plugins = inlinePlugins.map(idToPlugin)
    } else {
      const projectPlugins = Object.keys(this.pkg.devDependencies || {})
        .concat(Object.keys(this.pkg.dependencies || {}))
        .filter(isPlugin)
        .map(id => [id, require(id)])
        .map(([id, apply]) => ({ id, apply }))

      plugins = builtInPlugins.concat(projectPlugins)
    }

    return plugins
  }

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

  1. 处理内置插件: 加载 Vue CLI 内置的插件,比如 servebuildinspect 等。
  2. 处理项目插件:package.jsondevDependenciesdependencies 中找到以 @vue/cli-plugin-vue-cli-plugin- 开头的依赖,这些就是项目插件。
  3. 返回插件列表: 将内置插件和项目插件合并成一个插件列表,每个插件都是一个包含 idapply 属性的对象。id 是插件的名称,apply 是插件的入口函数。

2.2 initPlugins:插件的登场亮相

initPlugins 的作用是遍历插件列表,执行每个插件的入口函数。

  initPlugins () {
    this.plugins.forEach(({ id, apply }) => {
      apply(new PluginAPI(id, this), this.projectOptions)
    })
  }

这个方法很简单,它遍历插件列表,然后调用每个插件的 apply 函数,并将 PluginAPIprojectOptions 作为参数传递给它。

PluginAPI 是一个非常重要的类,它提供了一系列方法,供插件使用,比如:

  • registerCommand:注册一个新的命令。
  • chainWebpack:修改 Webpack 配置。
  • configureWebpack:合并 Webpack 配置。
  • configureDevServer:配置 Dev Server。

projectOptions 是用户在 vue.config.js 中定义的配置。

3. PluginAPI:插件的武器库

PluginAPI 就像一个工具箱,提供了各种工具,让插件能够与 Vue CLI 进行交互。我们来看几个常用的 API。

3.1 registerCommand:扩展命令

registerCommand 允许插件注册一个新的命令,比如一个自定义的 lint 命令。

// 插件代码
module.exports = (api, options) => {
  api.registerCommand('lint', {
    description: 'lint and fix source files',
    usage: 'vue-cli-service lint [options]',
    options: {
      '--fix': 'Automatically fix linting problems'
    },
    fn: args => {
      // 执行 lint 命令的逻辑
      console.log('Running lint command...')
    }
  })
}

3.2 chainWebpack:链式修改 Webpack 配置

chainWebpack 允许插件使用 webpack-chain 这个库,以链式的方式修改 Webpack 配置。

// 插件代码
module.exports = (api, options) => {
  api.chainWebpack(config => {
    config.module
      .rule('vue')
      .test(/.vue$/)
      .use('vue-loader')
        .loader('vue-loader')
  })
}

webpack-chain 提供了一套流畅的 API,可以方便地添加、修改和删除 Webpack 配置项。

3.3 configureWebpack:合并 Webpack 配置

configureWebpack 允许插件直接修改 Webpack 配置对象,或者返回一个配置对象与现有配置合并。

// 插件代码
module.exports = (api, options) => {
  api.configureWebpack(config => {
    // 直接修改 config 对象
    config.plugins.push(new MyPlugin())

    // 返回一个配置对象
    return {
      resolve: {
        alias: {
          '@': api.resolve('src')
        }
      }
    }
  })
}

3.4 configureDevServer:配置 Dev Server

configureDevServer 允许插件配置 Dev Server。

// 插件代码
module.exports = (api, options) => {
  api.configureDevServer(app => {
    // 监听 API 请求
    app.get('/api/data', (req, res) => {
      res.json({ message: 'Hello from the API!' })
    })
  })
}

4. 构建 Webpack 配置:炼金术的巅峰

经过插件的洗礼,Service 最终会构建出一个完整的 Webpack 配置对象。这个过程主要发生在 resolveWebpackConfig 方法中。

  resolveWebpackConfig (chainableConfig = undefined) {
    const webpackConfig = chainableConfig
      ? chainableConfig.toConfig()
      : this.webpackChain.toConfig()
    // apply raw config fns
    this.webpackRawConfigFns.forEach(fn => {
      if (typeof fn === 'function') {
        // function with arity 1, e.g. function (config) {}
        fn(webpackConfig)
      } else if (fn) {
        Object.assign(webpackConfig, fn)
      }
    })

    return webpackConfig
  }

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

  1. 生成基础配置:webpackChain 对象转换为 Webpack 配置对象。webpackChain 是一个 webpack-chain 实例,它包含了所有插件通过 chainWebpack 方法添加的配置。
  2. 应用原始配置: 遍历 webpackRawConfigFns 数组,执行每个配置函数,或者将配置对象合并到 Webpack 配置对象中。webpackRawConfigFns 数组包含了所有插件通过 configureWebpack 方法添加的配置。
  3. 返回最终配置: 返回最终的 Webpack 配置对象。

5. 流程总结:一次配置的华丽转身

为了更好地理解整个过程,我们用一张表格来总结一下:

步骤 描述 涉及方法 关键对象/函数
1 Service 初始化:加载 package.json、插件和用户配置。 constructor, resolvePkg, resolvePlugins, resolveUserOptions this.pkg, this.plugins, this.projectOptions
2 应用插件:执行每个插件的入口函数,并将 PluginAPIprojectOptions 作为参数传递给它。 initPlugins PluginAPI, this.projectOptions
3 插件使用 PluginAPI 修改 Webpack 配置:插件可以通过 chainWebpack 方法使用 webpack-chain 修改 Webpack 配置,或者通过 configureWebpack 方法直接修改 Webpack 配置对象。 chainWebpack, configureWebpack webpack-chain, Webpack 配置对象
4 构建 Webpack 配置:将 webpackChain 对象转换为 Webpack 配置对象,然后应用所有通过 configureWebpack 方法添加的配置。 resolveWebpackConfig webpackChain, webpackRawConfigFns

6. 一个简单的例子:手写一个插件

为了更好地理解插件的开发,我们来手写一个简单的插件,它可以自动在每个 JavaScript 文件中添加一个版权声明。

// my-plugin.js
module.exports = (api, options) => {
  api.chainWebpack(config => {
    config.module
      .rule('add-copyright')
      .test(/.js$/)
      .use('banner-loader')
        .loader('banner-loader')
        .options({
          banner: `/*!
 * Copyright (c) ${new Date().getFullYear()} Your Name
 */`
        })
  })
}

这个插件使用了 banner-loader 这个 Webpack Loader,它可以自动在每个 JavaScript 文件的顶部添加一个 banner。

要在项目中使用这个插件,我们需要:

  1. my-plugin.js 放到项目的根目录下。
  2. vue.config.js 中配置插件:
// vue.config.js
module.exports = {
  configureWebpack: config => {
    config.resolveLoader = {
      modules: ['node_modules', '.'] // 确保可以找到本地的 loader
    };
  },
  chainWebpack: config => {
    config.resolveLoader.modules.add(path.resolve(__dirname));
  },
  pluginOptions: {
    'my-plugin': {} // 插件配置
  }
}

确保你安装了banner-loadernpm install banner-loader -D

这样,每次构建项目时,banner-loader 就会自动在每个 JavaScript 文件的顶部添加版权声明。

7. 总结:掌控 Vue CLI 的力量

通过这次深入的剖析,我们了解了 Vue CLI 中 Service 类的初始化和插件加载机制,以及它是如何构建 Webpack 配置的。

掌握这些知识,你就可以:

  • 更好地理解 Vue CLI 的工作原理。
  • 开发更强大的 Vue CLI 插件。
  • 更灵活地定制 Webpack 配置。
  • 解决更复杂的构建问题。

希望这次旅程对你有所帮助。记住,代码的世界充满了乐趣,只要你敢于探索,就能发现无限的可能!

感谢大家的参与,下次再见!

8. 附录:常用插件列表

插件名称 功能描述
@vue/cli-plugin-babel 集成 Babel,支持 ES6+ 语法。
@vue/cli-plugin-eslint 集成 ESLint,进行代码检查。
@vue/cli-plugin-typescript 集成 TypeScript,支持 TypeScript 语法。
@vue/cli-plugin-router 集成 Vue Router,用于构建单页面应用。
@vue/cli-plugin-vuex 集成 Vuex,用于管理应用状态。
@vue/cli-plugin-pwa 集成 PWA,将应用转换为渐进式 Web 应用。
@vue/cli-plugin-unit-jest 集成 Jest,进行单元测试。
@vue/cli-plugin-e2e-cypress 集成 Cypress,进行端到端测试。
vue-cli-plugin-style-resources-loader 自动导入全局样式文件(如 variables.scss、mixins.scss 等)。
vue-cli-plugin-element 快速集成 Element UI 组件库。
vue-cli-plugin-vuetify 快速集成 Vuetify 组件库。
webpack-bundle-analyzer 分析 Webpack 打包结果,找出体积过大的模块。

希望这个附录能帮助你更好地选择合适的插件,提升开发效率。

发表回复

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