各位朋友,大家好!我是今天的主讲人,很高兴能和大家一起深入 Vue CLI 的腹地,探索 Service
类的奥秘,揭开它如何巧妙地构建 Webpack 配置的神秘面纱。准备好了吗?咱们开始吧!
开场白:Vue CLI,你的脚手架,我的战场!
Vue CLI,作为 Vue.js 官方提供的脚手架工具,极大地简化了 Vue 项目的初始化和构建流程。但你有没有好奇过,当我们执行 vue create my-project
或者 vue serve
的时候,背后到底发生了什么?尤其是,它是如何根据我们的配置,最终生成一份可用的 Webpack 配置的?
答案就藏在 Service
类里。它就像 Vue CLI 的大脑,负责协调各种插件,收集配置信息,最终生成我们需要的 Webpack 配置。
第一部分:Service
类初始化,一切的起点
想象一下,Service
类就像一个项目经理,它需要了解项目的方方面面,才能更好地组织资源,完成任务。
-
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
方法,它负责解析插件。 -
插件解析:
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
类就要开始加载并应用这些插件了。
-
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 配置。
- 加载
-
插件的
apply
函数:各显神通的舞台每个插件都有一个
apply
函数,这个函数接收两个参数:api
: 一个包含了各种实用方法的对象,例如api.registerCommand
,api.chainWebpack
,api.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 配置修改函数(chainWebpack
,configureWebpack
)收集起来,然后在 resolveWebpackConfig
方法中依次执行这些函数,最终生成 Webpack 配置。
-
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 配置对象,与之前的配置进行合并。
- 调用
-
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
的优点在于,它提供了一种类型安全、可维护性强的配置修改方式。 -
配置合并:
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
类还负责注册和执行各种命令,例如 serve
,build
,inspect
等。
-
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
: 命令的选项,例如description
,usage
。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 命令的逻辑 }) }
-
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-chain 和 webpack-merge 。 |
resolveWebpackConfig , resolveWebpackOptions , chainWebpack , configureWebpack |
命令注册与执行 | 注册和执行各种命令,例如 serve ,build ,inspect 。 |
registerCommand , run |
插件机制 | 允许插件通过 api 对象注册命令,修改 Webpack 配置,配置 Dev Server 等。 |
api.registerCommand , api.chainWebpack , api.configureWebpack , api.configureDevServer |
理解 Service
类的原理,可以帮助我们更好地理解 Vue CLI 的工作方式,也能够让我们更灵活地定制 Vue 项目的构建流程。
结束语:掌握工具,才能创造价值
希望今天的分享能够帮助大家更深入地了解 Vue CLI。掌握工具,才能更好地创造价值。谢谢大家!