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

各位朋友,大家好!我是今天的主讲人,很高兴能和大家一起深入 Vue CLI 的腹地,探索 Service 类的奥秘,揭开它如何巧妙地构建 Webpack 配置的神秘面纱。准备好了吗?咱们开始吧!

开场白:Vue CLI,你的脚手架,我的战场!

Vue CLI,作为 Vue.js 官方提供的脚手架工具,极大地简化了 Vue 项目的初始化和构建流程。但你有没有好奇过,当我们执行 vue create my-project 或者 vue serve 的时候,背后到底发生了什么?尤其是,它是如何根据我们的配置,最终生成一份可用的 Webpack 配置的?

答案就藏在 Service 类里。它就像 Vue CLI 的大脑,负责协调各种插件,收集配置信息,最终生成我们需要的 Webpack 配置。

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

想象一下,Service 类就像一个项目经理,它需要了解项目的方方面面,才能更好地组织资源,完成任务。

  1. Service 类的构造函数

    // @vue/cli-service/lib/Service.js
    
    class Service {
     constructor (context, { plugins, pkg, inlineOptions, useBuiltIn } = {}) {
       this.context = context // 项目根目录
       this.plugins = this.resolvePlugins(plugins, pkg) // 解析插件
       this.pkg = pkg // package.json 内容
       this.inlineOptions = inlineOptions // 命令行传入的选项
       this.useBuiltIn = useBuiltIn // 是否使用内置插件
       this.webpackChainFns = [] // Webpack Chain 函数队列
       this.webpackRawConfigFns = [] // Webpack Raw 配置函数队列
       this.devServerConfigFns = [] // Dev Server 配置函数队列
       this.commands = {} // 注册的命令
       this.modes = {} // 注册的模式
       this.initialized = false // 初始化状态
       this.chainWebpack = this.chainWebpack.bind(this)
       this.configureWebpack = this.configureWebpack.bind(this)
       this.configureDevServer = this.configureDevServer.bind(this)
     }
    
     // ... 其他方法
    }

    可以看到,构造函数接收几个关键参数:

    • context: 项目的根目录,所有操作都基于这个目录。
    • plugins: 一个包含需要加载的插件信息的数组。
    • pkg: package.json 文件的内容,包含了项目的依赖信息。
    • inlineOptions: 通过命令行传入的选项,例如 --mode 等。

    重点在于 this.resolvePlugins 方法,它负责解析插件。

  2. 插件解析:resolvePlugins 方法

    // @vue/cli-service/lib/Service.js
    
    resolvePlugins (inlinePlugins, pkg) {
     const idToPlugin = id => ({
       id: id.replace(/^.//, 'built-in:'),
       apply: require(id)
     })
    
     let plugins
    
     if (inlinePlugins) {
       plugins = inlinePlugins
     } else if (pkg && Array.isArray(pkg.vuePlugins)) {
       plugins = pkg.vuePlugins
     } else {
       plugins = []
     }
    
     return plugins.map(plugin => {
       if (typeof plugin === 'string') {
         return idToPlugin(plugin)
       } else if (typeof plugin === 'object' && plugin.plugin) {
         return {
           id: plugin.id || plugin.plugin,
           apply: require(plugin.plugin),
           options: plugin.options
         }
       } else {
         throw new Error(`Invalid plugin ${JSON.stringify(plugin)}`)
       }
     })
    }

    这个方法的作用是将插件信息规范化成一个统一的格式:

    {
     id: string, // 插件的 ID,例如 'built-in:babel'
     apply: Function, // 插件的 apply 函数,插件的核心逻辑
     options: Object // 插件的选项
    }

    它会从以下几个地方查找插件:

    • inlinePlugins: 直接传入的插件。
    • pkg.vuePlugins: package.json 文件中 vuePlugins 字段定义的插件。

    idToPlugin 函数的作用是将插件 ID 转换成插件对象。

第二部分:插件加载与应用,构建的基石

插件解析完成后,Service 类就要开始加载并应用这些插件了。

  1. init 方法:插件的启动仪式

    // @vue/cli-service/lib/Service.js
    
    async init (mode = process.env.VUE_CLI_MODE) {
     if (this.initialized) {
       return
     }
     this.initialized = true
     this.mode = mode
    
     // load mode .env
     if (mode) {
       this.loadEnv(mode)
     }
     // load base .env
     this.loadEnv()
    
     // apply plugins
     for (const plugin of this.plugins) {
       plugin.apply(this, plugin.options)
     }
    
     // apply webpack configuration
     this.resolveWebpackConfig()
    }

    init 方法是 Service 类初始化的关键步骤。它主要做了以下几件事:

    • 加载 .env 文件:根据 mode 加载不同的环境变量,例如 .env.development.env.production
    • 应用插件:遍历 this.plugins 数组,执行每个插件的 apply 函数。
    • 解析 Webpack 配置:调用 this.resolveWebpackConfig 方法,生成最终的 Webpack 配置。
  2. 插件的 apply 函数:各显神通的舞台

    每个插件都有一个 apply 函数,这个函数接收两个参数:

    • api: 一个包含了各种实用方法的对象,例如 api.registerCommandapi.chainWebpackapi.configureWebpack 等。
    • options: 插件的选项。

    插件可以通过 api 对象,来注册命令、修改 Webpack 配置、配置 Dev Server 等。

    举个例子,@vue/cli-plugin-babel 插件的 apply 函数:

    // @vue/cli-plugin-babel/index.js
    
    module.exports = (api, options) => {
     api.chainWebpack(config => {
       // 配置 Babel Loader
       config.module
         .rule('js')
           .test(/.m?jsx?$/)
           .exclude
             .add(filepath => {
               // always transpile js in vue files
               if (/.vue.js$/.test(filepath)) {
                 return false
               }
               return /node_modules/.test(filepath)
             })
             .end()
           .use('babel-loader')
             .loader(require.resolve('babel-loader'))
    
       // ... 其他配置
     })
    }

    这个插件使用 api.chainWebpack 方法,向 Webpack 配置中添加了一个 Babel Loader,用于编译 JavaScript 代码。

第三部分:Webpack 配置生成,水到渠成

Service 类在初始化阶段,会将所有插件注册的 Webpack 配置修改函数(chainWebpackconfigureWebpack)收集起来,然后在 resolveWebpackConfig 方法中依次执行这些函数,最终生成 Webpack 配置。

  1. resolveWebpackConfig 方法:配置的炼丹炉

    // @vue/cli-service/lib/Service.js
    
    resolveWebpackConfig (...args) {
     if (!this.webpackConfig) {
       let config = this.resolveWebpackOptions(...args)
       // ... apply chainWebpack/configureWebpack
       config = this.webpackChainFns.reduce((config, fn) => {
         return fn(config)
       }, config)
       config = this.webpackRawConfigFns.reduce((config, fn) => {
         return merge(config, fn)
       }, config)
       this.webpackConfig = config
     }
     return this.webpackConfig
    }

    这个方法的核心逻辑是:

    • 调用 this.resolveWebpackOptions 方法,生成一个基础的 Webpack 配置。
    • 依次执行 this.webpackChainFns 数组中的函数,这些函数使用 webpack-chain API 修改 Webpack 配置。
    • 依次执行 this.webpackRawConfigFns 数组中的函数,这些函数直接返回一个 Webpack 配置对象,与之前的配置进行合并。
  2. webpack-chain:优雅的配置修改

    webpack-chain 是一个用于以链式 API 的方式修改 Webpack 配置的工具。它可以让我们更方便地添加、删除、修改 Webpack 配置中的各种选项。

    例如,添加一个 Loader:

    // 使用 webpack-chain
    
    const Config = require('webpack-chain')
    const config = new Config()
    
    config.module
     .rule('vue')
       .test(/.vue$/)
       .use('vue-loader')
         .loader('vue-loader')

    webpack-chain 的优点在于,它提供了一种类型安全、可维护性强的配置修改方式。

  3. 配置合并:webpack-merge

    webpack-merge 是一个用于合并 Webpack 配置对象的工具。它可以将多个配置对象合并成一个,解决配置冲突。

    例如:

    // 使用 webpack-merge
    
    const merge = require('webpack-merge')
    
    const configA = {
     entry: './src/index.js',
     output: {
       filename: 'bundle.js'
     }
    }
    
    const configB = {
     mode: 'production',
     plugins: [
       // ...
     ]
    }
    
    const finalConfig = merge(configA, configB)

    webpack-merge 提供了多种合并策略,可以根据不同的场景选择合适的策略。

第四部分:命令注册与执行,项目的指挥棒

Service 类还负责注册和执行各种命令,例如 servebuildinspect 等。

  1. registerCommand 方法:命令的登记处

    // @vue/cli-service/lib/Service.js
    
    registerCommand (name, opts, fn) {
     if (typeof opts === 'function') {
       fn = opts
       opts = null
     }
     opts = opts || {}
     this.commands[name] = { fn, opts }
    }

    这个方法用于注册一个新的命令。它接收三个参数:

    • name: 命令的名称,例如 ‘serve’。
    • opts: 命令的选项,例如 descriptionusage
    • fn: 命令的处理函数,当命令被执行时,会调用这个函数。

    举个例子,@vue/cli-service 插件注册 serve 命令:

    // @vue/cli-service/lib/commands/serve.js
    
    module.exports = (api, options) => {
     api.registerCommand('serve', {
       description: 'start development server',
       usage: 'vue-cli-service serve [options]',
       options: {
         '--open': `Open browser on server start`,
         '--copy': `Copy local url to clipboard`,
         '--mode': `specify env mode (default: development)`
       }
     }, async (args) => {
       // ... serve 命令的逻辑
     })
    }
  2. run 方法:命令的执行者

    // @vue/cli-service/lib/Service.js
    
    async run (name, args = {}, rawArgv = []) {
     if (!this.commands[name]) {
       console.error(`Unknown command "${name}".`)
       process.exit(1)
     }
     const { fn } = this.commands[name]
     return fn(args, rawArgv)
    }

    这个方法用于执行一个已注册的命令。它接收三个参数:

    • name: 命令的名称。
    • args: 命令的选项。
    • rawArgv: 原始的命令行参数。

    当我们执行 vue serve 的时候,实际上就是调用了 Service 类的 run 方法,并传入 ‘serve’ 作为命令名称。

总结:Service 类,Vue CLI 的灵魂

通过以上的分析,我们可以看到,Service 类在 Vue CLI 中扮演着至关重要的角色。它负责:

  • 解析和加载插件。
  • 收集和合并 Webpack 配置。
  • 注册和执行命令。
功能 描述 关键方法
初始化 初始化项目上下文,加载环境变量,解析插件。 constructor, resolvePlugins, loadEnv, init
Webpack 配置生成 生成和合并 Webpack 配置,使用 webpack-chainwebpack-merge resolveWebpackConfig, resolveWebpackOptions, chainWebpack, configureWebpack
命令注册与执行 注册和执行各种命令,例如 servebuildinspect registerCommand, run
插件机制 允许插件通过 api 对象注册命令,修改 Webpack 配置,配置 Dev Server 等。 api.registerCommand, api.chainWebpack, api.configureWebpack, api.configureDevServer

理解 Service 类的原理,可以帮助我们更好地理解 Vue CLI 的工作方式,也能够让我们更灵活地定制 Vue 项目的构建流程。

结束语:掌握工具,才能创造价值

希望今天的分享能够帮助大家更深入地了解 Vue CLI。掌握工具,才能更好地创造价值。谢谢大家!

发表回复

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