嘿,各位!今天咱们来聊聊 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: 一个数组,包含了要加载的插件。可以是字符串(插件名称)或者对象(包含id和apply属性)。pkg: 项目的package.json文件的内容。inlineOptions: 从命令行传入的选项,比如--mode development。useBuiltIn: 布尔值,决定是否使用 Vue CLI 内置的插件。
关键步骤:插件解析 resolvePlugins
resolvePlugins 方法负责解析插件数组,将字符串形式的插件名称转换为包含 id 和 apply 属性的对象。
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
}
})
}
这个方法做了几件事:
- 处理插件 ID: 如果插件 ID 不是以
@或vue-cli-plugin-开头,就自动加上vue-cli-plugin-前缀。 这样做是为了规范插件的命名,方便查找和管理。 - 加载插件: 使用
require函数加载插件。 如果插件是字符串,直接require(id)。 如果插件是对象,则require(path.resolve(this.context, plugin.service))。 这样就可以加载本地的插件。 - 返回插件对象: 返回一个包含
id和apply属性的对象。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)
}
}
这个方法做了几件事:
- 处理
inlineOptions.plugins: 如果inlineOptions中有plugins属性,则先执行这些插件。 - 遍历插件: 遍历
this.plugins数组,执行每个插件的apply函数。 - 创建
PluginAPI实例: 在执行插件之前,先创建一个PluginAPI实例。PluginAPI提供了插件与 Vue CLI 交互的接口,比如注册命令、修改 Webpack 配置等。 - 执行插件: 调用
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
})
})
}
这个插件做了两件事:
- 注册了一个名为
my-command的命令。 - 修改了 Webpack 配置,禁用了
vue-loader的preserveWhitespace选项。
第三部分:构建 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
}
这个方法做了以下几件事:
- 获取
vue.config.js中的配置:vue.config.js是一个可选的配置文件,用户可以在这里自定义 Webpack 配置。 - 应用
configureWebpack钩子: 执行通过PluginAPI.configureWebpack注册的回调函数,允许插件直接修改 Webpack 配置对象。 - 创建
webpack-chain对象: 创建一个webpack-chain对象,用于链式地修改 Webpack 配置。 - 应用
webpackChain钩子: 执行通过PluginAPI.chainWebpack注册的回调函数,允许插件链式地修改 Webpack 配置。 - 合并配置: 将
webpack-chain对象转换为普通的 Webpack 配置对象,并与vue.config.js中的配置合并。
配置优先级
各个配置的优先级如下(从低到高):
- Vue CLI 默认配置
vue.config.js中的配置configureWebpack钩子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 中获取配置,应用 configureWebpack 和 webpackChain 钩子。 |
resolveWebpackConfig |
| 4. 合并配置 | 将各种配置合并成最终的 Webpack 配置对象。 | resolveWebpackConfig (内部依赖 webpack-merge 或类似工具) |
结束语:掌握 Vue CLI,成为配置高手
通过今天的讲解,相信你对 Vue CLI 的 Service 类有了更深入的了解。 掌握了 Vue CLI 的配置机制,你就可以更加灵活地定制你的 Vue 项目,成为真正的配置高手! 希望今天的分享对你有所帮助,下次再见!