嘿,各位!今天咱们来聊聊 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 项目,成为真正的配置高手! 希望今天的分享对你有所帮助,下次再见!