Vue CLI Service 剖析:Webpack 配置的炼金术
大家好,我是你们今天的导游,带大家一起深入 Vue CLI 的腹地,扒一扒 Service
这个核心类的底裤,看看它到底是如何施展魔法,把一堆插件和配置揉捏成一个 webpack 配置的。
准备好了吗?让我们开始这场代码探险之旅吧!
1. Service
:Vue CLI 的大脑
首先,我们需要明确 Service
在 Vue CLI 中扮演的角色。简单来说,它就像一个大脑,负责:
- 初始化项目: 创建必要的目录结构,生成配置文件等。
- 加载插件: 从
package.json
和vue.config.js
中识别并加载插件。 - 构建 Webpack 配置: 基于插件和用户配置,生成最终的 Webpack 配置对象。
- 执行任务: 运行
serve
、build
、inspect
等命令。
也就是说,我们看到的那些酷炫的特性,比如热重载、代码分割、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
的构造函数主要做了以下几件事:
- 接收参数: 接收项目根目录
context
、插件列表plugins
、package.json
内容pkg
和命令行配置inlineOptions
。 - 初始化属性: 初始化一些内部属性,比如
webpackChainFns
、webpackRawConfigFns
等,这些属性用于存储插件提供的配置函数。 - 加载
package.json
: 使用resolvePkg
方法加载package.json
文件。 - 加载插件: 使用
resolvePlugins
方法加载插件。 - 加载用户配置: 使用
resolveUserOptions
方法加载vue.config.js
文件。 - 应用插件: 使用
initPlugins
方法应用插件,这是最关键的一步。
我们重点关注 resolvePlugins
和 initPlugins
这两个方法。
2.1 resolvePlugins
:插件的寻宝游戏
resolvePlugins
的作用是根据 package.json
和 vue.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
}
这个方法做了以下几件事:
- 处理内置插件: 加载 Vue CLI 内置的插件,比如
serve
、build
、inspect
等。 - 处理项目插件: 从
package.json
的devDependencies
和dependencies
中找到以@vue/cli-plugin-
或vue-cli-plugin-
开头的依赖,这些就是项目插件。 - 返回插件列表: 将内置插件和项目插件合并成一个插件列表,每个插件都是一个包含
id
和apply
属性的对象。id
是插件的名称,apply
是插件的入口函数。
2.2 initPlugins
:插件的登场亮相
initPlugins
的作用是遍历插件列表,执行每个插件的入口函数。
initPlugins () {
this.plugins.forEach(({ id, apply }) => {
apply(new PluginAPI(id, this), this.projectOptions)
})
}
这个方法很简单,它遍历插件列表,然后调用每个插件的 apply
函数,并将 PluginAPI
和 projectOptions
作为参数传递给它。
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
}
这个方法做了以下几件事:
- 生成基础配置: 将
webpackChain
对象转换为 Webpack 配置对象。webpackChain
是一个webpack-chain
实例,它包含了所有插件通过chainWebpack
方法添加的配置。 - 应用原始配置: 遍历
webpackRawConfigFns
数组,执行每个配置函数,或者将配置对象合并到 Webpack 配置对象中。webpackRawConfigFns
数组包含了所有插件通过configureWebpack
方法添加的配置。 - 返回最终配置: 返回最终的 Webpack 配置对象。
5. 流程总结:一次配置的华丽转身
为了更好地理解整个过程,我们用一张表格来总结一下:
步骤 | 描述 | 涉及方法 | 关键对象/函数 |
---|---|---|---|
1 | Service 初始化:加载 package.json 、插件和用户配置。 |
constructor , resolvePkg , resolvePlugins , resolveUserOptions |
this.pkg , this.plugins , this.projectOptions |
2 | 应用插件:执行每个插件的入口函数,并将 PluginAPI 和 projectOptions 作为参数传递给它。 |
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。
要在项目中使用这个插件,我们需要:
- 将
my-plugin.js
放到项目的根目录下。 - 在
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-loader
,npm 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 打包结果,找出体积过大的模块。 |
希望这个附录能帮助你更好地选择合适的插件,提升开发效率。