Vite构建工具
学习目标
完成本章学习后,你将能够:
- 深入理解Vite的核心原理和为什么它比传统构建工具更快
- 掌握Vite的完整配置方法和最佳实践
- 理解ESM原生支持和HMR热更新的实现机制
- 掌握Rollup打包、代码分割、Tree Shaking的优化策略
- 能够开发自定义Vite插件扩展功能
- 掌握Vite在生产环境的优化和部署方法
前置知识
学习本章内容前,你需要掌握:
- JavaScript基础 - ES6+模块化语法
- 包管理器详解 - npm/pnpm的使用
- 浏览器工作原理 - 理解浏览器如何加载和执行JavaScript
- Node.js基础 - 了解Node.js环境和npm scripts
问题引入
实际场景
你是一名前端开发工程师,正在使用传统的Webpack构建工具开发一个大型Vue 3项目。团队反馈:
问题1:开发服务器启动慢
现象:每次启动开发服务器需要等待30-60秒
原因:Webpack需要打包整个应用才能启动
影响:严重影响开发效率,每次重启都要等很久
问题2:热更新慢
现象:修改一个文件后,页面更新需要5-10秒
原因:Webpack需要重新打包相关模块
影响:开发体验差,无法实时看到修改效果
问题3:配置复杂
现象:webpack.config.js文件有500+行配置
原因:需要配置各种loader、plugin、优化选项
影响:配置难以维护,新人上手困难
问题4:构建速度慢
现象:生产环境构建需要5-10分钟
原因:需要处理大量模块和依赖
影响:CI/CD流程耗时长,部署效率低
作为技术负责人,你需要评估是否应该迁移到Vite,并向团队解释Vite的优势。
为什么需要这个知识点
1. 开发效率的革命性提升
- Vite的开发服务器启动时间从分钟级降到秒级
- 热更新速度从秒级降到毫秒级
- 极大提升开发体验和工作效率
2. 现代化的开发体验
- 原生支持ES模块,无需打包即可开发
- 开箱即用的TypeScript、JSX、CSS预处理器支持
- 简洁的配置,降低学习成本
3. 生产环境的优化
- 基于Rollup的高效打包
- 自动代码分割和Tree Shaking
- 优化的资源加载策略
4. 企业级项目的必备技能
- Vue 3官方推荐的构建工具
- 越来越多的企业项目采用Vite
- 前端工程师的核心竞争力
核心概念
一句话定义
Vite是一个基于浏览器原生ES模块的新一代前端构建工具,通过利用浏览器的原生能力实现极速的开发服务器启动和热更新。
通俗理解:Vite就是一个"聪明的构建工具",它在开发时不打包代码,直接让浏览器加载ES模块,所以启动和更新都非常快;在生产环境使用Rollup打包,保证最优的性能。
为什么需要Vite:
- 解决传统构建工具启动慢的问题(Webpack需要打包整个应用)
- 解决热更新慢的问题(只更新修改的模块,不重新打包)
- 简化配置,提供开箱即用的现代化开发体验
- 提供生产环境的优化打包能力
本质是什么 :
Vite的核心创新在于:
- 开发环境:利用浏览器原生ES模块支持,按需编译,无需打包
- 生产环境:使用Rollup打包,生成优化的静态资源
- 双模式架构:开发和生产使用不同的策略,各取所长
Vite知识体系思维导图
Vite构建工具
核心原理
开发模式
原生ESM
按需编译
无打包启动
生产模式
Rollup打包
代码分割
Tree Shaking
为什么快
跳过打包步骤
浏览器原生能力
高效的HMR
核心特性
开发服务器
即时启动
快速HMR
预构建依赖
构建优化
代码分割
异步加载
资源优化
开箱即用
TypeScript
JSX/TSX
CSS预处理器
配置系统
基础配置
root
base
mode
开发服务器配置
port
proxy
hmr
构建配置
outDir
assetsDir
rollupOptions
插件系统
官方插件
@vitejs/plugin-vue
@vitejs/plugin-react
插件钩子
config
configResolved
transform
自定义插件
开发插件
钩子函数
插件顺序
性能优化
依赖预构建
esbuild预构建
缓存机制
代码分割
动态导入
手动分割
资源优化
图片压缩
CSS提取
Gzip压缩
1. Vite为什么这么快
1.1 传统构建工具的问题
javascript
/**
* 传统构建工具(如Webpack)的工作流程
*
* 【问题】
* 需要打包整个应用才能启动开发服务器
*/
class TraditionalBundler {
async start() {
console.log('=== 传统构建工具启动流程 ===\n');
const startTime = Date.now();
// 第1步:分析入口文件
console.log('第1步:分析入口文件 (0s)');
console.log(' - 读取src/main.js');
console.log(' - 解析import语句');
// 第2步:递归分析所有依赖
console.log('\n第2步:递归分析所有依赖 (5s)');
console.log(' - 分析node_modules中的1000+个模块');
console.log(' - 构建依赖图');
console.log(' - 解析所有import/require');
// 第3步:转换和编译
console.log('\n第3步:转换和编译 (15s)');
console.log(' - 使用Babel转换ES6+代码');
console.log(' - 处理Vue/React组件');
console.log(' - 编译TypeScript');
console.log(' - 处理CSS预处理器');
// 第4步:打包
console.log('\n第4步:打包 (10s)');
console.log(' - 合并所有模块');
console.log(' - 生成bundle.js');
console.log(' - 生成source map');
// 第5步:启动开发服务器
console.log('\n第5步:启动开发服务器 (1s)');
console.log(' - 启动HTTP服务器');
console.log(' - 监听文件变化');
const endTime = Date.now();
const duration = (endTime - startTime) / 1000;
console.log(`\n总耗时:${duration}s`);
console.log('问题:启动时间太长,严重影响开发效率');
}
async hotUpdate(file) {
console.log('\n=== 传统构建工具热更新流程 ===\n');
const startTime = Date.now();
console.log(`文件变化:${file}`);
// 第1步:找到受影响的模块
console.log('\n第1步:找到受影响的模块 (0.5s)');
console.log(' - 分析依赖图');
console.log(' - 找到所有依赖该文件的模块');
// 第2步:重新编译
console.log('\n第2步:重新编译 (3s)');
console.log(' - 重新编译修改的文件');
console.log(' - 重新编译依赖它的文件');
// 第3步:重新打包
console.log('\n第3步:重新打包 (2s)');
console.log(' - 更新bundle.js');
console.log(' - 生成新的source map');
// 第4步:通知浏览器更新
console.log('\n第4步:通知浏览器更新 (0.5s)');
console.log(' - 通过WebSocket发送更新');
console.log(' - 浏览器重新加载模块');
const endTime = Date.now();
const duration = (endTime - startTime) / 1000;
console.log(`\n总耗时:${duration}s`);
console.log('问题:热更新慢,无法实时看到修改效果');
}
}
// 模拟传统构建工具
const webpack = new TraditionalBundler();
console.log('场景:开发一个包含1000+模块的大型应用\n');
// webpack.start();
// webpack.hotUpdate('src/components/Button.vue');
// 运行结果:
// === 传统构建工具启动流程 ===
//
// 第1步:分析入口文件 (0s)
// - 读取src/main.js
// - 解析import语句
//
// 第2步:递归分析所有依赖 (5s)
// - 分析node_modules中的1000+个模块
// - 构建依赖图
// - 解析所有import/require
//
// 第3步:转换和编译 (15s)
// - 使用Babel转换ES6+代码
// - 处理Vue/React组件
// - 编译TypeScript
// - 处理CSS预处理器
//
// 第4步:打包 (10s)
// - 合并所有模块
// - 生成bundle.js
// - 生成source map
//
// 第5步:启动开发服务器 (1s)
// - 启动HTTP服务器
// - 监听文件变化
//
// 总耗时:31s
// 问题:启动时间太长,严重影响开发效率
//
// === 传统构建工具热更新流程 ===
//
// 文件变化:src/components/Button.vue
//
// 第1步:找到受影响的模块 (0.5s)
// - 分析依赖图
// - 找到所有依赖该文件的模块
//
// 第2步:重新编译 (3s)
// - 重新编译修改的文件
// - 重新编译依赖它的文件
//
// 第3步:重新打包 (2s)
// - 更新bundle.js
// - 生成新的source map
//
// 第4步:通知浏览器更新 (0.5s)
// - 通过WebSocket发送更新
// - 浏览器重新加载模块
//
// 总耗时:6s
// 问题:热更新慢,无法实时看到修改效果
1.2 Vite的解决方案
javascript
/**
* Vite的工作流程
*
* 【核心创新】
* 利用浏览器原生ES模块支持,无需打包即可开发
*/
class ViteBundler {
async start() {
console.log('=== Vite启动流程 ===\n');
const startTime = Date.now();
// 第1步:启动开发服务器
console.log('第1步:启动开发服务器 (0.3s)');
console.log(' - 启动HTTP服务器');
console.log(' - 启动WebSocket服务器');
console.log(' - 不需要打包!');
// 第2步:预构建依赖
console.log('\n第2步:预构建依赖 (1s)');
console.log(' - 使用esbuild预构建node_modules');
console.log(' - 只构建一次,后续使用缓存');
console.log(' - 速度极快(esbuild用Go编写)');
// 第3步:准备就绪
console.log('\n第3步:准备就绪 (0.2s)');
console.log(' - 服务器已启动');
console.log(' - 可以开始开发');
const endTime = Date.now();
const duration = (endTime - startTime) / 1000;
console.log(`\n总耗时:${duration}s`);
console.log('优势:启动速度提升20倍!');
}
async handleRequest(url) {
console.log('\n=== Vite处理浏览器请求 ===\n');
const startTime = Date.now();
console.log(`浏览器请求:${url}`);
// 第1步:判断文件类型
console.log('\n第1步:判断文件类型 (0.01s)');
if (url.endsWith('.vue')) {
console.log(' - Vue单文件组件');
} else if (url.endsWith('.ts')) {
console.log(' - TypeScript文件');
} else if (url.endsWith('.css')) {
console.log(' - CSS文件');
}
// 第2步:按需编译
console.log('\n第2步:按需编译 (0.05s)');
console.log(' - 只编译当前请求的文件');
console.log(' - 使用esbuild快速转换');
console.log(' - 不编译未使用的文件');
// 第3步:返回ES模块
console.log('\n第3步:返回ES模块 (0.01s)');
console.log(' - 返回编译后的ES模块代码');
console.log(' - 浏览器直接执行');
const endTime = Date.now();
const duration = (endTime - startTime) / 1000;
console.log(`\n总耗时:${duration}s`);
console.log('优势:按需编译,速度极快');
}
async hotUpdate(file) {
console.log('\n=== Vite热更新流程 ===\n');
const startTime = Date.now();
console.log(`文件变化:${file}`);
// 第1步:编译修改的文件
console.log('\n第1步:编译修改的文件 (0.05s)');
console.log(' - 只编译修改的文件');
console.log(' - 使用esbuild快速转换');
// 第2步:通知浏览器更新
console.log('\n第2步:通知浏览器更新 (0.01s)');
console.log(' - 通过WebSocket发送更新');
console.log(' - 浏览器只更新修改的模块');
console.log(' - 保持应用状态');
const endTime = Date.now();
const duration = (endTime - startTime) / 1000;
console.log(`\n总耗时:${duration}s`);
console.log('优势:热更新速度提升100倍!');
}
}
// 模拟Vite
const vite = new ViteBundler();
console.log('场景:开发一个包含1000+模块的大型应用\n');
// vite.start();
// vite.handleRequest('/src/components/Button.vue');
// vite.hotUpdate('src/components/Button.vue');
// 运行结果:
// === Vite启动流程 ===
//
// 第1步:启动开发服务器 (0.3s)
// - 启动HTTP服务器
// - 启动WebSocket服务器
// - 不需要打包!
//
// 第2步:预构建依赖 (1s)
// - 使用esbuild预构建node_modules
// - 只构建一次,后续使用缓存
// - 速度极快(esbuild用Go编写)
//
// 第3步:准备就绪 (0.2s)
// - 服务器已启动
// - 可以开始开发
//
// 总耗时:1.5s
// 优势:启动速度提升20倍!
//
// === Vite处理浏览器请求 ===
//
// 浏览器请求:/src/components/Button.vue
//
// 第1步:判断文件类型 (0.01s)
// - Vue单文件组件
//
// 第2步:按需编译 (0.05s)
// - 只编译当前请求的文件
// - 使用esbuild快速转换
// - 不编译未使用的文件
//
// 第3步:返回ES模块 (0.01s)
// - 返回编译后的ES模块代码
// - 浏览器直接执行
//
// 总耗时:0.07s
// 优势:按需编译,速度极快
//
// === Vite热更新流程 ===
//
// 文件变化:src/components/Button.vue
//
// 第1步:编译修改的文件 (0.05s)
// - 只编译修改的文件
// - 使用esbuild快速转换
//
// 第2步:通知浏览器更新 (0.01s)
// - 通过WebSocket发送更新
// - 浏览器只更新修改的模块
// - 保持应用状态
//
// 总耗时:0.06s
// 优势:热更新速度提升100倍!
1.3 性能对比总结
javascript
/**
* Vite vs Webpack 性能对比
*/
class PerformanceComparison {
compare() {
console.log('=== Vite vs Webpack 性能对比 ===\n');
const metrics = [
{
metric: '开发服务器启动时间',
webpack: '30-60秒',
vite: '1-2秒',
improvement: '20-30倍',
reason: 'Vite无需打包,直接启动服务器'
},
{
metric: '热更新速度',
webpack: '5-10秒',
vite: '50-100毫秒',
improvement: '50-100倍',
reason: 'Vite只编译修改的模块,不重新打包'
},
{
metric: '首次页面加载',
webpack: '打包后加载bundle.js',
vite: '按需加载ES模块',
improvement: '更快的首屏渲染',
reason: 'Vite利用浏览器原生ES模块,按需加载'
},
{
metric: '配置复杂度',
webpack: '500+行配置',
vite: '50-100行配置',
improvement: '配置简化5-10倍',
reason: 'Vite提供开箱即用的默认配置'
},
{
metric: '生产构建速度',
webpack: '5-10分钟',
vite: '2-5分钟',
improvement: '2-3倍',
reason: 'Vite使用Rollup和esbuild,速度更快'
}
];
metrics.forEach(m => {
console.log(`${m.metric}`);
console.log(` Webpack: ${m.webpack}`);
console.log(` Vite: ${m.vite}`);
console.log(` 提升: ${m.improvement}`);
console.log(` 原因: ${m.reason}`);
console.log('');
});
console.log('总结:');
console.log(' - 开发体验:Vite远超Webpack');
console.log(' - 生产构建:Vite略优于Webpack');
console.log(' - 配置简单:Vite大幅简化配置');
console.log(' - 学习成本:Vite更容易上手');
}
}
const comparison = new PerformanceComparison();
comparison.compare();
// 运行结果:
// === Vite vs Webpack 性能对比 ===
//
// 开发服务器启动时间
// Webpack: 30-60秒
// Vite: 1-2秒
// 提升: 20-30倍
// 原因: Vite无需打包,直接启动服务器
//
// 热更新速度
// Webpack: 5-10秒
// Vite: 50-100毫秒
// 提升: 50-100倍
// 原因: Vite只编译修改的模块,不重新打包
//
// 首次页面加载
// Webpack: 打包后加载bundle.js
// Vite: 按需加载ES模块
// 提升: 更快的首屏渲染
// 原因: Vite利用浏览器原生ES模块,按需加载
//
// 配置复杂度
// Webpack: 500+行配置
// Vite: 50-100行配置
// 提升: 配置简化5-10倍
// 原因: Vite提供开箱即用的默认配置
//
// 生产构建速度
// Webpack: 5-10分钟
// Vite: 2-5分钟
// 提升: 2-3倍
// 原因: Vite使用Rollup和esbuild,速度更快
//
// 总结:
// - 开发体验:Vite远超Webpack
// - 生产构建:Vite略优于Webpack
// - 配置简单:Vite大幅简化配置
// - 学习成本:Vite更容易上手
2. ESM原生支持
2.1 什么是ES模块
javascript
/**
* ES模块(ESM)详解
*
* 【定义】
* ES模块是JavaScript官方的模块化标准,使用import/export语法
*
* 【特点】
* 1. 静态导入:在编译时确定依赖关系
* 2. 异步加载:支持动态导入
* 3. 浏览器原生支持:现代浏览器可以直接执行ES模块
*/
// 示例1:ES模块的基本语法
// math.js - 导出模块
export function add(a, b) {
return a + b;
}
export function subtract(a, b) {
return a - b;
}
export const PI = 3.14159;
// 默认导出
export default class Calculator {
constructor() {
this.result = 0;
}
add(n) {
this.result += n;
return this;
}
getResult() {
return this.result;
}
}
// main.js - 导入模块
// 命名导入
import { add, subtract, PI } from './math.js';
console.log(add(1, 2)); // 输出:3
console.log(subtract(5, 3)); // 输出:2
console.log(PI); // 输出:3.14159
// 默认导入
import Calculator from './math.js';
const calc = new Calculator();
console.log(calc.add(10).add(5).getResult()); // 输出:15
// 重命名导入
import { add as sum } from './math.js';
console.log(sum(1, 2)); // 输出:3
// 导入所有
import * as math from './math.js';
console.log(math.add(1, 2)); // 输出:3
console.log(math.PI); // 输出:3.14159
// 动态导入
async function loadModule() {
const module = await import('./math.js');
console.log(module.add(1, 2)); // 输出:3
}
2.2 浏览器如何加载ES模块
html
<!-- 示例2:浏览器原生加载ES模块 -->
<!DOCTYPE html>
<html>
<head>
<title>ES模块示例</title>
</head>
<body>
<h1>ES模块示例</h1>
<!-- 方式1:使用type="module"加载ES模块 -->
<script type="module">
// 这是一个ES模块
import { add } from './math.js';
console.log('ES模块加载成功');
console.log('1 + 2 =', add(1, 2));
</script>
<!-- 方式2:外部ES模块文件 -->
<script type="module" src="./main.js"></script>
<!-- 注意:普通script标签不能使用import -->
<script>
// ❌ 错误:普通script不支持import
// import { add } from './math.js'; // 会报错
// ✅ 正确:使用动态导入
import('./math.js').then(module => {
console.log('动态导入成功');
console.log('1 + 2 =', module.add(1, 2));
});
</script>
</body>
</html>
浏览器加载ES模块的流程:
1. 解析HTML,遇到<script type="module">
2. 下载模块文件(math.js)
3. 解析模块,找到import语句
4. 递归下载所有依赖的模块
5. 按照依赖顺序执行模块
6. 执行主模块代码
2.3 Vite如何利用ES模块
javascript
/**
* Vite利用ES模块的原理
*
* 【核心思想】
* 在开发环境,Vite不打包代码,直接让浏览器加载ES模块
*/
class ViteESMServer {
constructor() {
this.cache = new Map(); // 模块缓存
}
// 处理浏览器请求
async handleRequest(url) {
console.log(`\n=== Vite处理请求:${url} ===\n`);
// 1. 检查缓存
if (this.cache.has(url)) {
console.log('从缓存返回');
return this.cache.get(url);
}
// 2. 读取源文件
console.log('读取源文件');
const source = await this.readFile(url);
// 3. 转换代码
console.log('转换代码');
const transformed = await this.transform(url, source);
// 4. 缓存结果
this.cache.set(url, transformed);
// 5. 返回ES模块
console.log('返回ES模块');
return transformed;
}
async readFile(url) {
// 模拟读取文件
const files = {
'/src/main.js': `
import { createApp } from 'vue';
import App from './App.vue';
createApp(App).mount('#app');
`,
'/src/App.vue': `
<template>
<div>Hello Vite!</div>
</template>
<script setup>
import { ref } from 'vue';
const count = ref(0);
</script>
`
};
return files[url] || '';
}
async transform(url, source) {
console.log(' - 解析import语句');
console.log(' - 转换Vue单文件组件');
console.log(' - 转换TypeScript');
console.log(' - 注入HMR代码');
// 转换后的代码(简化示例)
if (url.endsWith('.vue')) {
return `
// Vue单文件组件转换后的代码
import { defineComponent } from 'vue';
export default defineComponent({
setup() {
// 组件逻辑
}
});
// HMR代码
if (import.meta.hot) {
import.meta.hot.accept();
}
`;
}
return source;
}
}
// 模拟Vite服务器
const viteServer = new ViteESMServer();
// 浏览器请求主文件
console.log('浏览器请求:/src/main.js');
// await viteServer.handleRequest('/src/main.js');
// 浏览器请求Vue组件
console.log('\n浏览器请求:/src/App.vue');
// await viteServer.handleRequest('/src/App.vue');
// 运行结果:
// 浏览器请求:/src/main.js
//
// === Vite处理请求:/src/main.js ===
//
// 读取源文件
// 转换代码
// - 解析import语句
// - 转换Vue单文件组件
// - 转换TypeScript
// - 注入HMR代码
// 返回ES模块
//
// 浏览器请求:/src/App.vue
//
// === Vite处理请求:/src/App.vue ===
//
// 读取源文件
// 转换代码
// - 解析import语句
// - 转换Vue单文件组件
// - 转换TypeScript
// - 注入HMR代码
// 返回ES模块
3. HMR热更新机制
3.1 什么是HMR
javascript
/**
* HMR(Hot Module Replacement)热模块替换
*
* 【定义】
* HMR允许在应用运行时替换、添加或删除模块,无需完全刷新页面
*
* 【优势】
* 1. 保持应用状态:修改代码后,应用状态不丢失
* 2. 只更新修改的部分:不需要重新加载整个页面
* 3. 即时反馈:修改立即生效,开发体验极佳
*/
// 示例1:HMR API的使用
// counter.js
export let count = 0;
export function increment() {
count++;
console.log('Count:', count);
}
// 接受HMR更新
if (import.meta.hot) {
import.meta.hot.accept((newModule) => {
console.log('模块已更新');
// 可以在这里处理状态迁移
});
}
// main.js
import { count, increment } from './counter.js';
// 创建UI
const button = document.createElement('button');
button.textContent = `Count: ${count}`;
button.onclick = () => {
increment();
button.textContent = `Count: ${count}`;
};
document.body.appendChild(button);
// 接受counter模块的更新
if (import.meta.hot) {
import.meta.hot.accept('./counter.js', (newModule) => {
console.log('Counter模块已更新');
// 更新UI,但保持count的值
button.textContent = `Count: ${newModule.count}`;
});
}
3.2 Vite的HMR实现原理
javascript
/**
* Vite HMR实现原理
*
* 【核心组件】
* 1. 文件监听器:监听文件变化
* 2. WebSocket服务器:与浏览器通信
* 3. HMR客户端:浏览器端的HMR逻辑
*/
class ViteHMRServer {
constructor() {
this.clients = new Set(); // 连接的客户端
this.moduleGraph = new Map(); // 模块依赖图
}
// 启动HMR服务器
start() {
console.log('=== Vite HMR服务器启动 ===\n');
// 1. 启动WebSocket服务器
console.log('1. 启动WebSocket服务器');
console.log(' - 监听端口:24678');
console.log(' - 等待客户端连接\n');
// 2. 启动文件监听器
console.log('2. 启动文件监听器');
console.log(' - 监听src目录');
console.log(' - 监听配置文件\n');
// 3. 构建模块依赖图
console.log('3. 构建模块依赖图');
this.buildModuleGraph();
}
// 构建模块依赖图
buildModuleGraph() {
// 简化的模块依赖图
this.moduleGraph.set('/src/main.js', {
id: '/src/main.js',
importers: [], // 谁导入了这个模块
importedModules: ['/src/App.vue', '/src/utils.js'] // 这个模块导入了谁
});
this.moduleGraph.set('/src/App.vue', {
id: '/src/App.vue',
importers: ['/src/main.js'],
importedModules: ['/src/components/Button.vue']
});
this.moduleGraph.set('/src/components/Button.vue', {
id: '/src/components/Button.vue',
importers: ['/src/App.vue'],
importedModules: []
});
console.log(' - 模块依赖图构建完成\n');
}
// 处理文件变化
async handleFileChange(file) {
console.log(`\n=== 文件变化:${file} ===\n`);
// 1. 找到受影响的模块
console.log('1. 分析受影响的模块');
const affectedModules = this.getAffectedModules(file);
console.log(` - 受影响的模块:${affectedModules.join(', ')}\n`);
// 2. 重新编译模块
console.log('2. 重新编译模块');
const newCode = await this.recompile(file);
console.log(' - 编译完成\n');
// 3. 通知所有客户端更新
console.log('3. 通知客户端更新');
this.notifyClients({
type: 'update',
updates: [{
type: 'js-update',
path: file,
acceptedPath: file,
timestamp: Date.now()
}]
});
console.log(' - 更新通知已发送\n');
}
// 获取受影响的模块
getAffectedModules(file) {
const affected = [file];
const module = this.moduleGraph.get(file);
if (module) {
// 添加所有导入该模块的模块
affected.push(...module.importers);
}
return affected;
}
// 重新编译模块
async recompile(file) {
console.log(` - 编译:${file}`);
// 模拟编译过程
return `// 编译后的代码\nexport default { updated: true };`;
}
// 通知客户端
notifyClients(message) {
const messageStr = JSON.stringify(message);
console.log(` - 发送消息:${messageStr}`);
// 通过WebSocket发送给所有客户端
this.clients.forEach(client => {
// client.send(messageStr);
});
}
}
// 模拟Vite HMR服务器
const hmrServer = new ViteHMRServer();
hmrServer.start();
// 模拟文件变化
setTimeout(() => {
hmrServer.handleFileChange('/src/components/Button.vue');
}, 1000);
// 运行结果:
// === Vite HMR服务器启动 ===
//
// 1. 启动WebSocket服务器
// - 监听端口:24678
// - 等待客户端连接
//
// 2. 启动文件监听器
// - 监听src目录
// - 监听配置文件
//
// 3. 构建模块依赖图
// - 模块依赖图构建完成
//
// === 文件变化:/src/components/Button.vue ===
//
// 1. 分析受影响的模块
// - 受影响的模块:/src/components/Button.vue, /src/App.vue
//
// 2. 重新编译模块
// - 编译:/src/components/Button.vue
// - 编译完成
//
// 3. 通知客户端更新
// - 发送消息:{"type":"update","updates":[{"type":"js-update","path":"/src/components/Button.vue","acceptedPath":"/src/components/Button.vue","timestamp":1234567890}]}
// - 更新通知已发送
3.3 HMR客户端实现
javascript
/**
* Vite HMR客户端
*
* 【职责】
* 1. 连接WebSocket服务器
* 2. 接收更新通知
* 3. 执行模块替换
*/
class ViteHMRClient {
constructor() {
this.socket = null;
this.moduleCache = new Map();
}
// 连接服务器
connect() {
console.log('=== HMR客户端连接 ===\n');
// 1. 建立WebSocket连接
console.log('1. 连接WebSocket服务器');
console.log(' - 地址:ws://localhost:24678');
// this.socket = new WebSocket('ws://localhost:24678');
// 2. 监听消息
console.log('2. 监听服务器消息\n');
// this.socket.onmessage = (event) => {
// this.handleMessage(JSON.parse(event.data));
// };
}
// 处理服务器消息
handleMessage(message) {
console.log(`\n=== 收到更新消息 ===\n`);
console.log('消息类型:', message.type);
if (message.type === 'update') {
message.updates.forEach(update => {
this.applyUpdate(update);
});
} else if (message.type === 'full-reload') {
console.log('执行完全刷新');
location.reload();
}
}
// 应用更新
async applyUpdate(update) {
console.log(`\n应用更新:${update.path}`);
// 1. 获取新模块代码
console.log('1. 获取新模块代码');
const newModule = await this.fetchModule(update.path, update.timestamp);
// 2. 执行模块代码
console.log('2. 执行新模块代码');
const exports = this.executeModule(newModule);
// 3. 触发accept回调
console.log('3. 触发HMR accept回调');
this.triggerAcceptCallbacks(update.path, exports);
// 4. 更新缓存
console.log('4. 更新模块缓存');
this.moduleCache.set(update.path, exports);
console.log('✅ 更新完成\n');
}
// 获取新模块
async fetchModule(path, timestamp) {
console.log(` - 请求:${path}?t=${timestamp}`);
// const response = await fetch(`${path}?t=${timestamp}`);
// return await response.text();
return `export default { updated: true };`;
}
// 执行模块代码
executeModule(code) {
console.log(' - 执行模块代码');
// 使用Function构造函数执行代码
// const module = { exports: {} };
// new Function('module', 'exports', code)(module, module.exports);
// return module.exports;
return { updated: true };
}
// 触发accept回调
triggerAcceptCallbacks(path, newExports) {
console.log(` - 触发${path}的accept回调`);
// 查找注册的accept回调并执行
// const callbacks = this.acceptCallbacks.get(path);
// if (callbacks) {
// callbacks.forEach(cb => cb(newExports));
// }
}
}
// 模拟HMR客户端
const hmrClient = new ViteHMRClient();
hmrClient.connect();
// 模拟接收更新消息
setTimeout(() => {
hmrClient.handleMessage({
type: 'update',
updates: [{
type: 'js-update',
path: '/src/components/Button.vue',
acceptedPath: '/src/components/Button.vue',
timestamp: Date.now()
}]
});
}, 2000);
// 运行结果:
// === HMR客户端连接 ===
//
// 1. 连接WebSocket服务器
// - 地址:ws://localhost:24678
// 2. 监听服务器消息
//
// === 收到更新消息 ===
//
// 消息类型: update
//
// 应用更新:/src/components/Button.vue
// 1. 获取新模块代码
// - 请求:/src/components/Button.vue?t=1234567890
// 2. 执行新模块代码
// - 执行模块代码
// 3. 触发HMR accept回调
// - 触发/src/components/Button.vue的accept回调
// 4. 更新模块缓存
// ✅ 更新完成
4. Vite配置系统
4.1 基础配置
javascript
/**
* vite.config.js - Vite配置文件
*
* 【配置文件位置】
* 项目根目录下的vite.config.js或vite.config.ts
*
* 【配置文件格式】
* 导出一个配置对象或返回配置对象的函数
*/
// 示例1:基础配置
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
export default defineConfig({
// 项目根目录
root: process.cwd(),
// 公共基础路径
base: '/',
// 模式:development 或 production
mode: 'development',
// 插件
plugins: [
vue()
],
// 开发服务器配置
server: {
port: 3000,
host: '0.0.0.0',
open: true,
cors: true
},
// 构建配置
build: {
outDir: 'dist',
assetsDir: 'assets',
sourcemap: false,
minify: 'esbuild'
}
});
// 示例2:函数式配置(根据命令和模式动态配置)
export default defineConfig(({ command, mode }) => {
console.log('命令:', command); // 'serve' 或 'build'
console.log('模式:', mode); // 'development' 或 'production'
if (command === 'serve') {
// 开发环境配置
return {
server: {
port: 3000
}
};
} else {
// 生产环境配置
return {
build: {
minify: 'terser'
}
};
}
});
Vite构建工具
核心原理
开发环境
生产环境
插件系统
性能优化
ESM原生支持
依赖预构建
HMR热更新
按需编译
开发服务器
模块解析
源码转换
CSS处理
Rollup打包
代码分割
Tree Shaking
资源优化
Vite插件
Rollup插件
自定义插件
插件钩子
依赖优化
构建缓存
并行处理
懒加载
Vite的核心原理
1. 为什么Vite这么快?
Vite的速度优势来自于它的核心设计理念:
javascript
/**
* 传统构建工具(Webpack)的工作流程
*/
// 1. 启动开发服务器
// ├── 读取所有源文件
// ├── 解析模块依赖关系
// ├── 使用loader转换文件
// ├── 打包所有模块
// └── 启动服务器(耗时30-60秒)
// 2. 热更新
// ├── 检测文件变化
// ├── 重新打包相关模块
// ├── 推送更新到浏览器
// └── 浏览器应用更新(耗时1-5秒)
/**
* Vite的工作流程
*/
// 1. 启动开发服务器
// ├── 预构建依赖(只构建node_modules)
// ├── 启动服务器(耗时1-3秒)
// └── 按需编译源码(浏览器请求时才编译)
// 2. 热更新
// ├── 检测文件变化
// ├── 只编译改变的模块
// ├── 推送更新到浏览器
// └── 浏览器应用更新(耗时<100ms)
关键差异对比:
┌─────────────────┬──────────────────────────┬──────────────────────────┐
│ 阶段 │ Webpack │ Vite │
├─────────────────┼──────────────────────────┼──────────────────────────┤
│ 启动服务器 │ 打包所有模块(慢) │ 只预构建依赖(快) │
├─────────────────┼──────────────────────────┼──────────────────────────┤
│ 首次加载 │ 加载打包后的bundle │ 按需加载ES模块 │
├─────────────────┼──────────────────────────┼──────────────────────────┤
│ 热更新 │ 重新打包相关模块 │ 只编译改变的模块 │
├─────────────────┼──────────────────────────┼──────────────────────────┤
│ 模块数量影响 │ 线性增长(模块越多越慢) │ 几乎不受影响 │
└─────────────────┴──────────────────────────┴──────────────────────────┘
2. ESM原生支持
Vite利用浏览器原生支持ES模块的能力,实现无需打包的开发体验。
javascript
/**
* 浏览器原生ES模块示例
*/
// index.html
<!DOCTYPE html>
<html>
<head>
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<!-- type="module"告诉浏览器这是ES模块 -->
<script type="module" src="/src/main.js"></script>
</body>
</html>
// src/main.js
// 浏览器会发起HTTP请求加载这些模块
import { createApp } from 'vue';
import App from './App.vue';
import './style.css';
createApp(App).mount('#app');
// src/App.vue
// Vite会将.vue文件转换为JavaScript模块
<template>
<div>{{ message }}</div>
</template>
<script setup>
import { ref } from 'vue';
const message = ref('Hello Vite!');
</script>
/**
* Vite的模块转换流程
*/
// 1. 浏览器请求:GET /src/main.js
// 2. Vite拦截请求
// 3. 读取src/main.js文件
// 4. 转换import路径(裸模块导入 → 完整路径)
// 5. 返回转换后的JavaScript
// 转换前:
import { createApp } from 'vue';
import App from './App.vue';
// 转换后:
import { createApp } from '/@modules/vue'; // 指向预构建的依赖
import App from '/src/App.vue'; // 完整路径
/**
* 裸模块导入的处理
*/
// 裸模块导入(没有路径前缀)
import vue from 'vue';
import axios from 'axios';
// Vite会将裸模块导入转换为:
import vue from '/@modules/vue';
import axios from '/@modules/axios';
// /@modules/是Vite的特殊路径,指向预构建的依赖
// 预构建的依赖存储在node_modules/.vite/deps/目录
3. 依赖预构建
Vite会在启动时预构建依赖,将CommonJS和UMD模块转换为ESM。
javascript
/**
* 为什么需要依赖预构建?
*/
// 问题1:很多npm包使用CommonJS格式
// lodash使用CommonJS导出
module.exports = {
debounce: function() { /* ... */ },
throttle: function() { /* ... */ }
};
// 浏览器不支持CommonJS,需要转换为ESM
export const debounce = function() { /* ... */ };
export const throttle = function() { /* ... */ };
// 问题2:有些包有很多内部模块
// lodash-es有600+个模块
import debounce from 'lodash-es/debounce';
// 会导致600+个HTTP请求,严重影响性能
// 解决方案:预构建
// Vite使用esbuild将lodash-es打包成单个文件
// 只需要1个HTTP请求
/**
* 依赖预构建的配置
*/
// vite.config.js
export default {
optimizeDeps: {
// 需要预构建的依赖
include: [
'vue',
'vue-router',
'pinia',
'axios',
'lodash-es'
],
// 排除预构建的依赖
exclude: [
'your-local-package'
],
// esbuild配置
esbuildOptions: {
target: 'es2020',
supported: {
bigint: true
}
},
// 强制重新预构建
force: false
}
};
/**
* 预构建的触发时机
*/
// 1. 首次启动开发服务器
// 2. package.json的dependencies改变
// 3. vite.config.js的optimizeDeps配置改变
// 4. 手动删除node_modules/.vite目录
// 5. 使用--force标志启动
// 预构建的输出
// node_modules/.vite/deps/
// ├── vue.js
// ├── vue-router.js
// ├── pinia.js
// ├── axios.js
// └── _metadata.json // 预构建的元数据
/**
* 预构建的性能优化
*/
// 使用esbuild进行预构建(比Webpack快10-100倍)
// esbuild是用Go语言编写的,速度极快
// 性能对比:
// Webpack打包lodash:~1000ms
// esbuild打包lodash:~10ms
// 速度提升:100倍
4. HMR热更新机制
Vite的HMR(Hot Module Replacement)比传统工具快得多。
javascript
/**
* Vite HMR的工作原理
*/
// 1. 文件监听
// Vite使用chokidar监听文件变化
import chokidar from 'chokidar';
const watcher = chokidar.watch('src/**/*', {
ignored: /node_modules/
});
watcher.on('change', (path) => {
console.log(`文件改变: ${path}`);
handleHMR(path);
});
// 2. 模块图分析
// Vite维护一个模块依赖图
const moduleGraph = {
'/src/main.js': {
importers: [], // 谁导入了这个模块
importedModules: ['/src/App.vue', '/src/style.css'] // 这个模块导入了谁
},
'/src/App.vue': {
importers: ['/src/main.js'],
importedModules: ['/src/components/Header.vue']
}
};
// 3. 确定更新边界
// 当文件改变时,找出需要更新的模块
function findUpdateBoundary(changedFile) {
// 如果模块接受自己的更新,更新边界就是它自己
if (moduleGraph[changedFile].acceptsSelf) {
return [changedFile];
}
// 否则,向上查找接受更新的模块
const boundary = [];
const visited = new Set();
function traverse(file) {
if (visited.has(file)) return;
visited.add(file);
const module = moduleGraph[file];
if (module.acceptsHMR) {
boundary.push(file);
} else {
// 继续向上查找
module.importers.forEach(importer => {
traverse(importer);
});
}
}
traverse(changedFile);
return boundary;
}
// 4. 推送更新
// 通过WebSocket推送更新到浏览器
const ws = new WebSocket('ws://localhost:3000');
ws.send(JSON.stringify({
type: 'update',
updates: [
{
type: 'js-update',
path: '/src/App.vue',
acceptedPath: '/src/App.vue',
timestamp: Date.now()
}
]
}));
// 5. 浏览器应用更新
// 浏览器接收到更新消息后,重新请求模块
ws.onmessage = (event) => {
const { type, updates } = JSON.parse(event.data);
if (type === 'update') {
updates.forEach(update => {
// 重新导入模块
import(`${update.path}?t=${update.timestamp}`)
.then(newModule => {
// 应用更新
applyUpdate(update.path, newModule);
});
});
}
};
/**
* HMR API的使用
*/
// 在代码中使用HMR API
if (import.meta.hot) {
// 接受自己的更新
import.meta.hot.accept((newModule) => {
console.log('模块已更新:', newModule);
});
// 接受依赖的更新
import.meta.hot.accept('./dep.js', (newDep) => {
console.log('依赖已更新:', newDep);
});
// 处理模块销毁
import.meta.hot.dispose((data) => {
// 清理副作用
console.log('模块即将被替换');
});
// 自定义事件
import.meta.hot.on('custom-event', (data) => {
console.log('收到自定义事件:', data);
});
}
/**
* Vue组件的HMR
*/
// Vue组件自动支持HMR
// @vitejs/plugin-vue会自动注入HMR代码
// App.vue
<template>
<div>{{ message }}</div>
</template>
<script setup>
import { ref } from 'vue';
const message = ref('Hello');
</script>
// 编译后的代码(简化版)
import { ref } from 'vue';
const __script = {
setup() {
const message = ref('Hello');
return { message };
}
};
// Vite自动注入的HMR代码
if (import.meta.hot) {
import.meta.hot.accept((newModule) => {
// 更新组件
__VUE_HMR_RUNTIME__.reload('App.vue', newModule.__script);
});
}
/**
* CSS的HMR
*/
// CSS文件改变时,Vite会自动更新样式
// 不需要刷新页面
// style.css
.title {
color: blue;
font-size: 24px;
}
// 当style.css改变时:
// 1. Vite检测到文件变化
// 2. 通过WebSocket推送更新
// 3. 浏览器接收更新
// 4. 移除旧的<style>标签
// 5. 插入新的<style>标签
// 6. 样式立即生效(无需刷新页面)
/**
* HMR性能对比
*/
// Webpack HMR:
// 1. 文件改变
// 2. 重新打包相关模块(1-5秒)
// 3. 推送更新
// 4. 浏览器应用更新
// 总耗时:1-5秒
// Vite HMR:
// 1. 文件改变
// 2. 只编译改变的模块(<100ms)
// 3. 推送更新
// 4. 浏览器应用更新
// 总耗时:<100ms
// 速度提升:10-50倍
5. 按需编译
Vite只编译浏览器请求的模块,而不是一次性编译所有模块。
javascript
/**
* 按需编译的工作流程
*/
// 1. 浏览器请求模块
// GET /src/components/Header.vue
// 2. Vite拦截请求
app.use(async (req, res, next) => {
if (req.url.endsWith('.vue')) {
// 读取.vue文件
const content = await fs.readFile(req.url, 'utf-8');
// 编译.vue文件
const compiled = await compileVue(content);
// 返回编译后的JavaScript
res.setHeader('Content-Type', 'application/javascript');
res.end(compiled);
} else {
next();
}
});
// 3. 编译.vue文件
async function compileVue(source) {
// 解析.vue文件
const { descriptor } = parse(source);
// 编译<template>
const template = compileTemplate({
source: descriptor.template.content
});
// 编译<script>
const script = compileScript(descriptor);
// 编译<style>
const styles = descriptor.styles.map(style => {
return compileStyle({
source: style.content,
scoped: style.scoped
});
});
// 组合成JavaScript模块
return `
${script.content}
${template.code}
__script.render = render;
export default __script;
`;
}
/**
* 按需编译的优势
*/
// 传统方式(Webpack):
// 启动时编译所有模块
// ├── src/main.js
// ├── src/App.vue
// ├── src/components/Header.vue
// ├── src/components/Footer.vue
// ├── src/views/Home.vue
// ├── src/views/About.vue
// └── ... 所有文件(耗时30-60秒)
// Vite方式:
// 只编译浏览器请求的模块
// ├── src/main.js(首次请求)
// ├── src/App.vue(首次请求)
// └── src/components/Header.vue(首次请求)
// 其他文件只在需要时才编译(耗时1-3秒)
/**
* 路由懒加载的按需编译
*/
// 路由配置
const routes = [
{
path: '/',
component: () => import('./views/Home.vue')
},
{
path: '/about',
component: () => import('./views/About.vue')
}
];
// 访问首页时:
// 1. 浏览器请求/
// 2. 加载Home.vue
// 3. Vite编译Home.vue
// 4. 返回编译后的代码
// 访问关于页时:
// 1. 浏览器请求/about
// 2. 加载About.vue
// 3. Vite编译About.vue(首次编译)
// 4. 返回编译后的代码
// 优势:
// - 首次加载只编译首页需要的模块
// - 其他页面的模块在访问时才编译
// - 大大减少首次加载时间
关键点解析:
- ESM原生支持:利用浏览器原生能力,无需打包
- 依赖预构建:使用esbuild快速预构建依赖
- HMR热更新:只更新改变的模块,速度极快
- 按需编译:只编译浏览器请求的模块,启动快
Vite配置详解
Vite提供了简洁而强大的配置选项,大多数情况下开箱即用。
基础配置
javascript
/**
* vite.config.js - 基础配置
*/
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import path from 'path';
export default defineConfig({
// 项目根目录
root: process.cwd(),
// 开发或生产环境服务的公共基础路径
base: '/',
// 环境模式
mode: 'development', // 'development' | 'production'
// 插件
plugins: [
vue()
],
// 路径解析配置
resolve: {
// 路径别名
alias: {
'@': path.resolve(__dirname, 'src'),
'components': path.resolve(__dirname, 'src/components'),
'utils': path.resolve(__dirname, 'src/utils')
},
// 导入时想要省略的扩展名列表
extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue']
},
// CSS配置
css: {
// CSS预处理器选项
preprocessorOptions: {
scss: {
additionalData: `@import "@/styles/variables.scss";`
},
less: {
javascriptEnabled: true
}
},
// CSS模块化选项
modules: {
localsConvention: 'camelCase', // 类名转换规则
scopeBehaviour: 'local', // 作用域行为
generateScopedName: '[name]__[local]___[hash:base64:5]' // 生成的类名格式
},
// PostCSS配置
postcss: {
plugins: [
require('autoprefixer')
]
}
},
// JSON配置
json: {
namedExports: true, // 是否支持从.json文件中进行按名导入
stringify: false // 是否将JSON字符串化
},
// 静态资源处理
assetsInclude: ['**/*.gltf'], // 指定额外的静态资源类型
// 日志级别
logLevel: 'info', // 'info' | 'warn' | 'error' | 'silent'
// 清除控制台
clearScreen: true
});
开发服务器配置
javascript
/**
* 开发服务器配置
*/
export default defineConfig({
server: {
// 服务器主机名
host: '0.0.0.0', // 监听所有地址,包括局域网和公网地址
// 端口号
port: 3000,
// 端口被占用时是否自动尝试下一个可用端口
strictPort: false,
// 是否自动打开浏览器
open: true,
// 是否启用HTTPS
https: false,
// https: {
// key: fs.readFileSync('path/to/key.pem'),
// cert: fs.readFileSync('path/to/cert.pem')
// },
// 代理配置
proxy: {
// 字符串简写方式
'/foo': 'http://localhost:4567',
// 选项写法
'/api': {
target: 'http://jsonplaceholder.typicode.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
},
// 正则表达式写法
'^/fallback/.*': {
target: 'http://jsonplaceholder.typicode.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/fallback/, '')
},
// 使用proxy实例
'/api': {
target: 'http://jsonplaceholder.typicode.com',
changeOrigin: true,
configure: (proxy, options) => {
// proxy是'http-proxy'的实例
}
},
// WebSocket代理
'/socket.io': {
target: 'ws://localhost:3001',
ws: true
}
},
// CORS配置
cors: true,
// 强制预构建依赖
force: false,
// HMR配置
hmr: {
protocol: 'ws',
host: 'localhost',
port: 3000,
clientPort: 3000,
overlay: true // 在浏览器中显示错误覆盖层
},
// 监听文件变化
watch: {
// 传递给chokidar的文件系统监听器选项
ignored: ['!**/node_modules/**']
},
// 预热文件以提前转换和缓存结果
warmup: {
clientFiles: ['./src/components/*.vue']
},
// 中间件模式
middlewareMode: false,
// 文件系统严格模式
fs: {
// 限制为工作区root路径以外的文件的访问
strict: true,
// 限制哪些文件可以通过/@fs/路径提供服务
allow: [
// 搜索工作区的根目录
searchForWorkspaceRoot(process.cwd())
],
// 限制哪些文件不能通过/@fs/路径提供服务
deny: ['.env', '.env.local', '.env.*.local']
},
// 源码映射
sourcemapIgnoreList: false
}
});
构建配置
javascript
/**
* 生产构建配置
*/
export default defineConfig({
build: {
// 浏览器兼容性目标
target: 'es2015', // 'es2015' | 'es2016' | 'es2017' | 'es2018' | 'es2019' | 'es2020' | 'esnext'
// 输出目录
outDir: 'dist',
// 静态资源目录
assetsDir: 'assets',
// 小于此阈值的导入或引用资源将内联为base64编码
assetsInlineLimit: 4096, // 4KB
// 启用/禁用CSS代码拆分
cssCodeSplit: true,
// 构建后是否生成source map文件
sourcemap: false, // false | true | 'inline' | 'hidden'
// 自定义底层的Rollup打包配置
rollupOptions: {
// 输入配置
input: {
main: path.resolve(__dirname, 'index.html'),
nested: path.resolve(__dirname, 'nested/index.html')
},
// 输出配置
output: {
// 分包策略
manualChunks: {
'vue-vendor': ['vue', 'vue-router', 'pinia'],
'ui-vendor': ['element-plus'],
'utils': ['axios', 'lodash-es']
},
// 用于从入口点创建的chunk的打包输出格式
entryFileNames: 'js/[name]-[hash].js',
// 用于命名代码拆分时创建的共享chunk的输出命名
chunkFileNames: 'js/[name]-[hash].js',
// 用于输出静态资源的命名
assetFileNames: (assetInfo) => {
// 根据文件类型分类输出
const info = assetInfo.name.split('.');
let extType = info[info.length - 1];
if (/\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/i.test(assetInfo.name)) {
extType = 'media';
} else if (/\.(png|jpe?g|gif|svg|ico|webp)(\?.*)?$/i.test(assetInfo.name)) {
extType = 'images';
} else if (/\.(woff2?|eot|ttf|otf)(\?.*)?$/i.test(assetInfo.name)) {
extType = 'fonts';
}
return `${extType}/[name]-[hash][extname]`;
}
},
// 外部化依赖
external: ['vue'],
// 插件
plugins: []
},
// 库模式配置
lib: {
entry: path.resolve(__dirname, 'src/index.js'),
name: 'MyLib',
fileName: (format) => `my-lib.${format}.js`,
formats: ['es', 'cjs', 'umd', 'iife']
},
// 当设置为true时,构建后将会生成manifest.json文件
manifest: false,
// 设置为false可以禁用最小化混淆
minify: 'esbuild', // 'terser' | 'esbuild' | false
// 传递给minify的选项
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true
}
},
// 启用/禁用gzip压缩大小报告
reportCompressedSize: true,
// chunk大小警告的限制(以kbs为单位)
chunkSizeWarningLimit: 500,
// 启用/禁用brotli压缩大小报告
reportCompressedSize: true,
// 构建的应用将使用的环境
ssr: false,
// 生成面向SSR的构建
ssrManifest: false,
// 设置为false来禁用将构建后的文件写入磁盘
write: true,
// 默认情况下,若outDir在root目录下,则Vite会在构建时清空该目录
emptyOutDir: true,
// 启用/禁用brotli压缩大小报告
brotliSize: true,
// 构建监听
watch: null,
// 构建时的CommonJS选项
commonjsOptions: {
include: [/node_modules/],
extensions: ['.js', '.cjs']
},
// 构建时的动态导入polyfill
dynamicImportVarsOptions: {
warnOnError: true,
exclude: [/node_modules/]
}
}
});
依赖优化配置
javascript
/**
* 依赖预构建配置
*/
export default defineConfig({
optimizeDeps: {
// 需要预构建的依赖项
include: [
'vue',
'vue-router',
'pinia',
'axios',
'lodash-es',
'element-plus'
],
// 排除预构建的依赖项
exclude: [
'your-local-package'
],
// 传递给esbuild的选项
esbuildOptions: {
// Node.js全局变量注入
define: {
global: 'globalThis'
},
// 支持的特性
supported: {
bigint: true
},
// 目标环境
target: 'es2020',
// 插件
plugins: []
},
// 强制重新预构建依赖
force: false,
// 在预构建中处理的文件扩展名
extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx'],
// 禁用依赖预构建
disabled: false,
// 预构建时需要保留的依赖
needsInterop: []
}
});
环境变量配置
javascript
/**
* 环境变量的使用
*/
// .env文件
// 所有环境都会加载
VITE_APP_TITLE=My App
VITE_APP_VERSION=1.0.0
// .env.development
// 开发环境加载
VITE_APP_API_URL=http://localhost:8080/api
VITE_APP_DEBUG=true
// .env.production
// 生产环境加载
VITE_APP_API_URL=https://api.example.com
VITE_APP_DEBUG=false
// .env.local
// 本地环境加载(不会被git提交)
VITE_APP_SECRET_KEY=your-secret-key
/**
* 在代码中使用环境变量
*/
// 访问环境变量
console.log(import.meta.env.VITE_APP_TITLE);
console.log(import.meta.env.VITE_APP_API_URL);
// 内置环境变量
console.log(import.meta.env.MODE); // 'development' | 'production'
console.log(import.meta.env.BASE_URL); // 公共基础路径
console.log(import.meta.env.PROD); // 是否是生产环境
console.log(import.meta.env.DEV); // 是否是开发环境
console.log(import.meta.env.SSR); // 是否是服务端渲染
/**
* TypeScript的智能提示
*/
// src/env.d.ts
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_APP_TITLE: string;
readonly VITE_APP_API_URL: string;
readonly VITE_APP_DEBUG: string;
// 更多环境变量...
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}
/**
* 在vite.config.js中使用环境变量
*/
import { defineConfig, loadEnv } from 'vite';
export default defineConfig(({ command, mode }) => {
// 加载环境变量
const env = loadEnv(mode, process.cwd(), '');
return {
// 使用环境变量
base: env.VITE_APP_BASE_URL,
server: {
proxy: {
'/api': {
target: env.VITE_APP_API_URL,
changeOrigin: true
}
}
},
define: {
// 定义全局常量
__APP_VERSION__: JSON.stringify(env.VITE_APP_VERSION)
}
};
});
多环境配置
javascript
/**
* 根据不同环境返回不同配置
*/
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
export default defineConfig(({ command, mode }) => {
// command: 'serve' | 'build'
// mode: 'development' | 'production' | 'staging' | ...
const isProduction = mode === 'production';
const isDevelopment = mode === 'development';
return {
plugins: [vue()],
// 开发环境配置
...(isDevelopment && {
server: {
port: 3000,
open: true
}
}),
// 生产环境配置
...(isProduction && {
build: {
minify: 'terser',
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true
}
}
}
})
};
});
/**
* 使用配置文件分离
*/
// vite.config.base.js
export default {
plugins: [vue()],
resolve: {
alias: {
'@': path.resolve(__dirname, 'src')
}
}
};
// vite.config.dev.js
import { mergeConfig } from 'vite';
import baseConfig from './vite.config.base';
export default mergeConfig(baseConfig, {
mode: 'development',
server: {
port: 3000,
open: true
}
});
// vite.config.prod.js
import { mergeConfig } from 'vite';
import baseConfig from './vite.config.base';
export default mergeConfig(baseConfig, {
mode: 'production',
build: {
minify: 'terser',
sourcemap: false
}
});
// package.json
{
"scripts": {
"dev": "vite --config vite.config.dev.js",
"build": "vite build --config vite.config.prod.js"
}
}
关键点解析:
- 配置简洁:大多数情况下开箱即用,只需少量配置
- 类型安全:使用defineConfig获得TypeScript类型提示
- 环境变量:使用VITE_前缀的环境变量,在代码中通过import.meta.env访问
- 多环境支持:通过mode参数和配置文件分离实现多环境配置
Vite插件系统
Vite的插件系统基于Rollup插件接口,并提供了额外的Vite特定选项。
插件基础
javascript
/**
* Vite插件的基本结构
*/
// 一个简单的Vite插件
function myPlugin() {
return {
// 插件名称
name: 'my-plugin',
// 插件的执行顺序
// 'pre' | 'post' | undefined
enforce: 'pre',
// 应用模式
// 'serve' | 'build' | undefined
apply: 'build',
// Vite特定钩子:服务器启动时调用
configureServer(server) {
server.middlewares.use((req, res, next) => {
// 自定义中间件
next();
});
},
// Vite特定钩子:配置解析前调用
config(config, env) {
// 修改配置
return {
resolve: {
alias: {
'@': '/src'
}
}
};
},
// Vite特定钩子:配置解析后调用
configResolved(resolvedConfig) {
// 存储最终解析的配置
this.config = resolvedConfig;
},
// Rollup钩子:转换代码
transform(code, id) {
if (id.endsWith('.custom')) {
// 转换自定义文件
return {
code: transformCode(code),
map: null
};
}
},
// Rollup钩子:解析模块ID
resolveId(source, importer) {
if (source === 'virtual-module') {
return source;
}
},
// Rollup钩子:加载模块
load(id) {
if (id === 'virtual-module') {
return 'export default "This is virtual!"';
}
},
// Vite特定钩子:处理HMR更新
handleHotUpdate({ file, server }) {
if (file.endsWith('.custom')) {
// 自定义HMR处理
server.ws.send({
type: 'custom',
event: 'custom-update',
data: {}
});
return [];
}
}
};
}
// 使用插件
export default defineConfig({
plugins: [myPlugin()]
});
常用官方插件
javascript
/**
* @vitejs/plugin-vue - Vue 3支持
*/
import vue from '@vitejs/plugin-vue';
export default defineConfig({
plugins: [
vue({
// 包含的文件扩展名
include: [/\.vue$/],
// 自定义块的转换
customElement: true,
// 模板编译选项
template: {
compilerOptions: {
isCustomElement: (tag) => tag.startsWith('my-')
}
},
// 脚本编译选项
script: {
defineModel: true,
propsDestructure: true
}
})
]
});
/**
* @vitejs/plugin-vue-jsx - Vue 3 JSX支持
*/
import vueJsx from '@vitejs/plugin-vue-jsx';
export default defineConfig({
plugins: [
vueJsx({
// JSX转换选项
transformOn: true,
optimize: true
})
]
});
/**
* @vitejs/plugin-react - React支持
*/
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [
react({
// Babel配置
babel: {
plugins: ['babel-plugin-styled-components']
},
// Fast Refresh选项
fastRefresh: true,
// JSX运行时
jsxRuntime: 'automatic'
})
]
});
/**
* @vitejs/plugin-legacy - 传统浏览器支持
*/
import legacy from '@vitejs/plugin-legacy';
export default defineConfig({
plugins: [
legacy({
// 目标浏览器
targets: ['defaults', 'not IE 11'],
// 额外的传统polyfills
additionalLegacyPolyfills: ['regenerator-runtime/runtime'],
// 是否为现代浏览器生成polyfills
modernPolyfills: true,
// 渲染传统chunk
renderLegacyChunks: true
})
]
});
社区插件
javascript
/**
* vite-plugin-compression - 压缩插件
*/
import viteCompression from 'vite-plugin-compression';
export default defineConfig({
plugins: [
viteCompression({
// 压缩算法
algorithm: 'gzip', // 'gzip' | 'brotliCompress'
// 文件大小阈值
threshold: 10240, // 10KB
// 压缩后删除原文件
deleteOriginFile: false,
// 文件类型过滤
filter: /\.(js|css|json|txt|html|ico|svg)(\?.*)?$/i,
// 压缩选项
compressionOptions: {
level: 9
}
})
]
});
/**
* vite-plugin-html - HTML转换插件
*/
import { createHtmlPlugin } from 'vite-plugin-html';
export default defineConfig({
plugins: [
createHtmlPlugin({
// 压缩HTML
minify: true,
// 注入数据
inject: {
data: {
title: 'My App',
injectScript: '<script src="./inject.js"></script>'
}
},
// 自定义入口
entry: 'src/main.ts',
// 模板
template: 'public/index.html'
})
]
});
/**
* vite-plugin-pwa - PWA支持
*/
import { VitePWA } from 'vite-plugin-pwa';
export default defineConfig({
plugins: [
VitePWA({
// Service Worker注册类型
registerType: 'autoUpdate',
// Manifest配置
manifest: {
name: 'My App',
short_name: 'App',
description: 'My Awesome App',
theme_color: '#ffffff',
icons: [
{
src: 'icon-192x192.png',
sizes: '192x192',
type: 'image/png'
}
]
},
// Workbox配置
workbox: {
globPatterns: ['**/*.{js,css,html,ico,png,svg}'],
runtimeCaching: [
{
urlPattern: /^https:\/\/api\.example\.com\/.*/i,
handler: 'NetworkFirst',
options: {
cacheName: 'api-cache',
expiration: {
maxEntries: 10,
maxAgeSeconds: 60 * 60 * 24 * 7 // 7天
}
}
}
]
}
})
]
});
/**
* vite-plugin-svg-icons - SVG图标插件
*/
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons';
export default defineConfig({
plugins: [
createSvgIconsPlugin({
// 图标文件夹
iconDirs: [path.resolve(process.cwd(), 'src/icons')],
// 指定symbolId格式
symbolId: 'icon-[dir]-[name]',
// 自定义插入位置
inject: 'body-last',
// 自定义DOM id
customDomId: '__svg__icons__dom__'
})
]
});
/**
* unplugin-auto-import - 自动导入API
*/
import AutoImport from 'unplugin-auto-import/vite';
export default defineConfig({
plugins: [
AutoImport({
// 自动导入的库
imports: [
'vue',
'vue-router',
'pinia',
{
'axios': [
['default', 'axios'] // import { default as axios } from 'axios'
]
}
],
// 生成的类型声明文件路径
dts: 'src/auto-imports.d.ts',
// 目录
dirs: [
'src/composables',
'src/utils'
],
// ESLint配置
eslintrc: {
enabled: true,
filepath: './.eslintrc-auto-import.json'
}
})
]
});
// 使用后,无需手动导入
// 自动导入Vue API
const count = ref(0);
const doubled = computed(() => count.value * 2);
// 自动导入Vue Router
const router = useRouter();
const route = useRoute();
/**
* unplugin-vue-components - 自动导入组件
*/
import Components from 'unplugin-vue-components/vite';
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers';
export default defineConfig({
plugins: [
Components({
// 组件目录
dirs: ['src/components'],
// 组件的有效文件扩展名
extensions: ['vue'],
// 搜索子目录
deep: true,
// 生成的类型声明文件路径
dts: 'src/components.d.ts',
// 解析器
resolvers: [
// 自动导入Element Plus组件
ElementPlusResolver()
],
// 允许子目录作为组件的命名空间前缀
directoryAsNamespace: false,
// 忽略的组件名称
exclude: [/[\\/]node_modules[\\/]/, /[\\/]\.git[\\/]/]
})
]
});
// 使用后,无需手动导入组件
// <template>
// <MyButton /> <!-- 自动导入src/components/MyButton.vue -->
// <ElButton /> <!-- 自动导入Element Plus的Button组件 -->
// </template>
自定义插件开发
javascript
/**
* 示例1:虚拟模块插件
*
* 创建一个虚拟模块,提供构建信息
*/
function virtualModulePlugin() {
const virtualModuleId = 'virtual:build-info';
const resolvedVirtualModuleId = '\0' + virtualModuleId;
return {
name: 'virtual-module-plugin',
resolveId(id) {
if (id === virtualModuleId) {
return resolvedVirtualModuleId;
}
},
load(id) {
if (id === resolvedVirtualModuleId) {
return `
export const buildTime = '${new Date().toISOString()}';
export const version = '${process.env.npm_package_version}';
export const mode = '${process.env.NODE_ENV}';
`;
}
}
};
}
// 使用虚拟模块
import { buildTime, version, mode } from 'virtual:build-info';
console.log(`构建时间: ${buildTime}`);
console.log(`版本: ${version}`);
console.log(`模式: ${mode}`);
/**
* 示例2:Markdown转换插件
*
* 将Markdown文件转换为Vue组件
*/
import { marked } from 'marked';
function markdownPlugin() {
return {
name: 'markdown-plugin',
// 转换.md文件
transform(code, id) {
if (id.endsWith('.md')) {
// 将Markdown转换为HTML
const html = marked(code);
// 生成Vue组件
return {
code: `
<template>
<div class="markdown-body" v-html="html"></div>
</template>
<script setup>
const html = ${JSON.stringify(html)};
</script>
<style>
.markdown-body {
/* Markdown样式 */
}
</style>
`,
map: null
};
}
}
};
}
// 使用Markdown组件
import MyDoc from './docs/README.md';
/**
* 示例3:环境变量注入插件
*
* 在构建时注入额外的环境变量
*/
function envPlugin(envVars) {
return {
name: 'env-plugin',
config(config, { mode }) {
return {
define: {
...Object.keys(envVars).reduce((acc, key) => {
acc[`import.meta.env.${key}`] = JSON.stringify(envVars[key]);
return acc;
}, {})
}
};
}
};
}
// 使用
export default defineConfig({
plugins: [
envPlugin({
BUILD_ID: Date.now().toString(),
GIT_COMMIT: process.env.GIT_COMMIT || 'unknown'
})
]
});
/**
* 示例4:代码注入插件
*
* 在HTML中注入自定义代码
*/
function injectCodePlugin(options) {
return {
name: 'inject-code-plugin',
transformIndexHtml(html) {
// 在</head>前注入代码
const headCode = options.head || '';
html = html.replace('</head>', `${headCode}</head>`);
// 在</body>前注入代码
const bodyCode = options.body || '';
html = html.replace('</body>', `${bodyCode}</body>`);
return html;
}
};
}
// 使用
export default defineConfig({
plugins: [
injectCodePlugin({
head: '<script>console.log("Injected in head")</script>',
body: '<script>console.log("Injected in body")</script>'
})
]
});
/**
* 示例5:文件监听插件
*
* 监听特定文件变化并执行操作
*/
function watchPlugin(options) {
return {
name: 'watch-plugin',
configureServer(server) {
// 监听文件变化
server.watcher.on('change', (file) => {
if (options.pattern.test(file)) {
console.log(`文件改变: ${file}`);
// 执行自定义操作
if (options.onChange) {
options.onChange(file);
}
// 触发热更新
server.ws.send({
type: 'full-reload',
path: '*'
});
}
});
}
};
}
// 使用
export default defineConfig({
plugins: [
watchPlugin({
pattern: /\.json$/,
onChange: (file) => {
console.log(`JSON文件已更新: ${file}`);
// 执行自定义操作,如重新生成类型定义
}
})
]
});
关键点解析:
- 插件接口:基于Rollup插件接口,兼容大部分Rollup插件
- Vite特定钩子:提供configureServer、transformIndexHtml等Vite特有钩子
- 插件顺序:通过enforce控制插件执行顺序(pre、normal、post)
- 应用模式:通过apply控制插件在开发或生产环境中应用
生产构建优化
Vite使用Rollup进行生产构建,提供了多种优化策略。
代码分割策略
javascript
/**
* 手动代码分割配置
*/
export default defineConfig({
build: {
rollupOptions: {
output: {
// 手动分包
manualChunks(id) {
// 将node_modules中的代码分割到vendor chunk
if (id.includes('node_modules')) {
// 进一步细分vendor
if (id.includes('vue') || id.includes('pinia') || id.includes('vue-router')) {
return 'vue-vendor';
}
if (id.includes('element-plus')) {
return 'ui-vendor';
}
if (id.includes('echarts') || id.includes('@antv')) {
return 'charts-vendor';
}
// 其他第三方库
return 'vendor';
}
},
// 或使用对象形式
manualChunks: {
'vue-vendor': ['vue', 'vue-router', 'pinia'],
'ui-vendor': ['element-plus'],
'utils': ['axios', 'lodash-es']
}
}
}
}
});
/**
* 动态导入实现按需加载
*/
// 路由懒加载
const routes = [
{
path: '/',
component: () => import('./views/Home.vue')
},
{
path: '/about',
component: () => import('./views/About.vue')
}
];
// 组件懒加载
const HeavyComponent = defineAsyncComponent(() =>
import('./components/HeavyComponent.vue')
);
// 条件加载
async function loadEditor(type) {
if (type === 'rich') {
const { default: RichEditor } = await import('./editors/RichEditor.vue');
return RichEditor;
} else {
const { default: SimpleEditor } = await import('./editors/SimpleEditor.vue');
return SimpleEditor;
}
}
/**
* 预加载和预获取
*/
// 预加载(高优先级,立即加载)
import(/* @vite-ignore */ './critical-module.js');
// 预获取(低优先级,空闲时加载)
// Vite会自动为动态导入的模块生成预加载指令
const module = await import('./future-module.js');
Tree Shaking优化
javascript
/**
* 确保Tree Shaking生效
*/
// package.json配置
{
"sideEffects": false, // 标记所有文件都没有副作用
// 或指定有副作用的文件
"sideEffects": [
"*.css",
"*.scss",
"./src/polyfills.js"
]
}
/**
* 编写Tree Shaking友好的代码
*/
// ❌ 不推荐:使用默认导出
export default {
funcA: () => {},
funcB: () => {},
funcC: () => {}
};
// ✅ 推荐:使用命名导出
export function funcA() {}
export function funcB() {}
export function funcC() {}
// 使用时只导入需要的函数
import { funcA } from './utils'; // 只有funcA会被打包
/**
* 第三方库的Tree Shaking
*/
// ❌ 不推荐:导入整个库
import _ from 'lodash';
_.debounce(fn, 300);
// ✅ 推荐:只导入需要的函数
import debounce from 'lodash-es/debounce';
debounce(fn, 300);
// 或使用支持Tree Shaking的库
import { debounce } from 'lodash-es';
debounce(fn, 300);
/**
* 分析Tree Shaking效果
*/
// 使用rollup-plugin-visualizer分析
import { visualizer } from 'rollup-plugin-visualizer';
export default defineConfig({
plugins: [
visualizer({
open: true, // 构建后自动打开分析报告
gzipSize: true, // 显示gzip后的大小
brotliSize: true // 显示brotli后的大小
})
]
});
资源优化
javascript
/**
* 图片优化
*/
// 使用vite-plugin-imagemin压缩图片
import viteImagemin from 'vite-plugin-imagemin';
export default defineConfig({
plugins: [
viteImagemin({
// 图片压缩配置
gifsicle: {
optimizationLevel: 7,
interlaced: false
},
optipng: {
optimizationLevel: 7
},
mozjpeg: {
quality: 80
},
pngquant: {
quality: [0.8, 0.9],
speed: 4
},
svgo: {
plugins: [
{
name: 'removeViewBox'
},
{
name: 'removeEmptyAttrs',
active: false
}
]
}
})
]
});
/**
* 图片格式转换
*/
// 使用现代图片格式(WebP、AVIF)
// vite-plugin-webp
import webp from 'vite-plugin-webp';
export default defineConfig({
plugins: [
webp({
// 转换为WebP格式
quality: 80
})
]
});
// 在代码中使用
<picture>
<source srcset="image.webp" type="image/webp">
<img src="image.jpg" alt="Image">
</picture>
/**
* 字体优化
*/
// 使用字体子集化
// vite-plugin-fonts
import { VitePluginFonts } from 'vite-plugin-fonts';
export default defineConfig({
plugins: [
VitePluginFonts({
google: {
families: [
{
name: 'Roboto',
styles: 'wght@400;700',
defer: true // 延迟加载
}
]
}
})
]
});
/**
* CSS优化
*/
// 使用CSS压缩
export default defineConfig({
build: {
cssMinify: 'lightningcss', // 'esbuild' | 'lightningcss'
// CSS代码分割
cssCodeSplit: true
},
css: {
// 使用Lightning CSS
transformer: 'lightningcss',
// PostCSS配置
postcss: {
plugins: [
// 移除未使用的CSS
require('@fullhuman/postcss-purgecss')({
content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
defaultExtractor: content => content.match(/[\w-/:]+(?<!:)/g) || []
})
]
}
}
});
构建性能优化
javascript
/**
* 使用esbuild加速构建
*/
export default defineConfig({
build: {
// 使用esbuild压缩(比terser快20-40倍)
minify: 'esbuild',
// esbuild配置
esbuild: {
drop: ['console', 'debugger'], // 移除console和debugger
legalComments: 'none' // 移除注释
}
},
optimizeDeps: {
// esbuild配置
esbuildOptions: {
target: 'es2020'
}
}
});
/**
* 并行构建
*/
// Vite默认使用esbuild进行并行构建
// 可以通过调整worker数量优化性能
export default defineConfig({
build: {
// Rollup配置
rollupOptions: {
// 使用多个worker并行构建
maxParallelFileOps: 20
}
}
});
/**
* 构建缓存
*/
// Vite会自动缓存预构建的依赖
// 存储在node_modules/.vite目录
// 清除缓存
// npm run dev -- --force
/**
* 减少构建体积
*/
export default defineConfig({
build: {
// 启用gzip压缩大小报告
reportCompressedSize: true,
// chunk大小警告限制
chunkSizeWarningLimit: 500, // 500KB
// 禁用brotli压缩大小报告(加快构建)
brotliSize: false,
rollupOptions: {
output: {
// 分包策略
manualChunks: {
'vue-vendor': ['vue', 'vue-router', 'pinia'],
'ui-vendor': ['element-plus']
},
// 最小化chunk大小
experimentalMinChunkSize: 10000 // 10KB
}
}
}
});
CDN部署优化
javascript
/**
* 配置CDN
*/
export default defineConfig({
build: {
// CDN基础路径
base: 'https://cdn.example.com/',
rollupOptions: {
// 外部化依赖(从CDN加载)
external: ['vue', 'vue-router'],
output: {
// 配置全局变量
globals: {
vue: 'Vue',
'vue-router': 'VueRouter'
}
}
}
}
});
// index.html中引入CDN资源
<script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue-router@4/dist/vue-router.global.js"></script>
/**
* 使用vite-plugin-cdn-import自动处理CDN
*/
import cdn from 'vite-plugin-cdn-import';
export default defineConfig({
plugins: [
cdn({
modules: [
{
name: 'vue',
var: 'Vue',
path: 'https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.js'
},
{
name: 'vue-router',
var: 'VueRouter',
path: 'https://cdn.jsdelivr.net/npm/vue-router@4/dist/vue-router.global.js'
}
]
})
]
});
核心概念7:Vite的SSR支持
Vite提供了完整的服务端渲染(SSR)支持,可以轻松构建同构应用。
SSR基础配置
javascript
// server.js - SSR服务器
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import express from 'express';
import { createServer as createViteServer } from 'vite';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
async function createServer() {
const app = express();
// 创建Vite服务器(SSR模式)
const vite = await createViteServer({
server: { middlewareMode: true },
appType: 'custom',
});
// 使用Vite的中间件
app.use(vite.middlewares);
app.use('*', async (req, res) => {
try {
const url = req.originalUrl;
// 1. 读取index.html
let template = fs.readFileSync(
path.resolve(__dirname, 'index.html'),
'utf-8'
);
// 2. 应用Vite HTML转换(注入HMR客户端等)
template = await vite.transformIndexHtml(url, template);
// 3. 加载服务器入口
const { render } = await vite.ssrLoadModule('/src/entry-server.js');
// 4. 渲染应用HTML
const appHtml = await render(url);
// 5. 注入应用HTML到模板
const html = template.replace(`<!--ssr-outlet-->`, appHtml);
// 6. 发送渲染后的HTML
res.status(200).set({ 'Content-Type': 'text/html' }).end(html);
} catch (e) {
// 如果捕获到错误,让Vite修复堆栈跟踪
vite.ssrFixStacktrace(e);
console.error(e);
res.status(500).end(e.message);
}
});
app.listen(3000, () => {
console.log('Server running at http://localhost:3000');
});
}
createServer();
javascript
// src/entry-server.js - 服务器入口
import { createSSRApp } from 'vue';
import { renderToString } from 'vue/server-renderer';
import { createRouter } from './router';
import { createStore } from './store';
import App from './App.vue';
export async function render(url) {
const app = createSSRApp(App);
const router = createRouter();
const store = createStore();
app.use(router);
app.use(store);
// 设置服务器端路由位置
await router.push(url);
await router.isReady();
// 渲染应用
const html = await renderToString(app);
// 返回渲染后的HTML和状态
return {
html,
state: store.state,
};
}
javascript
// src/entry-client.js - 客户端入口
import { createApp } from 'vue';
import { createRouter } from './router';
import { createStore } from './store';
import App from './App.vue';
const app = createApp(App);
const router = createRouter();
const store = createStore();
// 恢复服务器端状态
if (window.__INITIAL_STATE__) {
store.replaceState(window.__INITIAL_STATE__);
}
app.use(router);
app.use(store);
router.isReady().then(() => {
app.mount('#app');
});
html
<!-- index.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite SSR App</title>
</head>
<body>
<div id="app"><!--ssr-outlet--></div>
<script>
// 注入初始状态
window.__INITIAL_STATE__ = __STATE__;
</script>
<script type="module" src="/src/entry-client.js"></script>
</body>
</html>
生产环境SSR构建
javascript
// vite.config.js
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
export default defineConfig({
plugins: [vue()],
build: {
// SSR构建配置
ssr: true,
rollupOptions: {
input: {
server: './src/entry-server.js',
},
},
},
});
json
// package.json
{
"scripts": {
"dev": "node server.js",
"build:client": "vite build --outDir dist/client",
"build:server": "vite build --ssr src/entry-server.js --outDir dist/server",
"build": "npm run build:client && npm run build:server",
"serve": "NODE_ENV=production node server-prod.js"
}
}
javascript
// server-prod.js - 生产环境服务器
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import express from 'express';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
async function createServer() {
const app = express();
// 提供静态文件
app.use(express.static(path.resolve(__dirname, 'dist/client'), {
index: false,
}));
app.use('*', async (req, res) => {
try {
const url = req.originalUrl;
// 读取生产环境的HTML模板
const template = fs.readFileSync(
path.resolve(__dirname, 'dist/client/index.html'),
'utf-8'
);
// 加载服务器端构建产物
const { render } = await import('./dist/server/entry-server.js');
// 渲染应用
const { html, state } = await render(url);
// 注入HTML和状态
const finalHtml = template
.replace(`<!--ssr-outlet-->`, html)
.replace('__STATE__', JSON.stringify(state));
res.status(200).set({ 'Content-Type': 'text/html' }).end(finalHtml);
} catch (e) {
console.error(e);
res.status(500).end(e.message);
}
});
app.listen(3000, () => {
console.log('Production server running at http://localhost:3000');
});
}
createServer();
关键点解析:
- SSR模式 :使用
middlewareMode创建Vite服务器 - 双入口:客户端入口和服务器入口分别处理
- 状态同步:服务器端渲染的状态注入到客户端
- 生产构建:分别构建客户端和服务器端代码
- 性能优化:使用流式渲染提升首屏速度
核心概念8:Vite的预渲染(SSG)
除了SSR,Vite还支持静态站点生成(SSG),适合内容不经常变化的网站。
预渲染配置
javascript
// vite-plugin-prerender.js - 自定义预渲染插件
import fs from 'fs/promises';
import path from 'path';
import { build } from 'vite';
export function prerenderPlugin(routes = ['/']) {
return {
name: 'vite-plugin-prerender',
async closeBundle() {
// 构建SSR版本
await build({
build: {
ssr: true,
outDir: 'dist/ssr',
},
});
// 加载SSR模块
const { render } = await import('./dist/ssr/entry-server.js');
// 预渲染所有路由
for (const route of routes) {
console.log(`预渲染: ${route}`);
// 渲染路由
const { html } = await render(route);
// 读取HTML模板
const template = await fs.readFile('dist/client/index.html', 'utf-8');
// 注入渲染结果
const finalHtml = template.replace('<!--ssr-outlet-->', html);
// 写入文件
const filePath = path.join(
'dist/client',
route === '/' ? 'index.html' : `${route}/index.html`
);
await fs.mkdir(path.dirname(filePath), { recursive: true });
await fs.writeFile(filePath, finalHtml);
}
console.log('预渲染完成!');
},
};
}
javascript
// vite.config.js
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import { prerenderPlugin } from './vite-plugin-prerender';
export default defineConfig({
plugins: [
vue(),
prerenderPlugin([
'/',
'/about',
'/blog',
'/blog/post-1',
'/blog/post-2',
]),
],
});
动态路由预渲染
javascript
// scripts/prerender.js - 预渲染脚本
import fs from 'fs/promises';
import path from 'path';
import { build } from 'vite';
async function prerender() {
// 1. 构建客户端
await build({
build: {
outDir: 'dist/client',
},
});
// 2. 构建SSR
await build({
build: {
ssr: true,
outDir: 'dist/ssr',
},
});
// 3. 获取所有需要预渲染的路由
const routes = await getRoutes();
// 4. 加载SSR模块
const { render } = await import('./dist/ssr/entry-server.js');
// 5. 预渲染所有路由
for (const route of routes) {
await prerenderRoute(route, render);
}
console.log(`预渲染完成!共${routes.length}个页面`);
}
async function getRoutes() {
// 从API获取动态路由
const response = await fetch('https://api.example.com/posts');
const posts = await response.json();
const routes = [
'/',
'/about',
'/blog',
...posts.map(post => `/blog/${post.slug}`),
];
return routes;
}
async function prerenderRoute(route, render) {
console.log(`预渲染: ${route}`);
try {
// 渲染路由
const { html, state } = await render(route);
// 读取HTML模板
const template = await fs.readFile('dist/client/index.html', 'utf-8');
// 注入渲染结果和状态
const finalHtml = template
.replace('<!--ssr-outlet-->', html)
.replace('__STATE__', JSON.stringify(state));
// 确定文件路径
let filePath;
if (route === '/') {
filePath = 'dist/client/index.html';
} else {
filePath = path.join('dist/client', route, 'index.html');
}
// 创建目录并写入文件
await fs.mkdir(path.dirname(filePath), { recursive: true });
await fs.writeFile(filePath, finalHtml);
console.log(`✓ ${route}`);
} catch (error) {
console.error(`✗ ${route}:`, error.message);
}
}
prerender().catch(console.error);
json
// package.json
{
"scripts": {
"build": "vite build",
"prerender": "node scripts/prerender.js"
}
}
关键点解析:
- 静态生成:在构建时预渲染所有页面
- 动态路由:从API获取数据生成动态路由
- SEO优化:预渲染的HTML对搜索引擎友好
- 部署简单:生成的是纯静态文件,可以部署到任何静态托管服务
- 性能最佳:无需服务器端渲染,加载速度最快
核心概念9:Vite的多页面应用(MPA)
Vite支持构建多页面应用,每个页面都是独立的入口。
MPA配置
javascript
// vite.config.js
import { defineConfig } from 'vite';
import { resolve } from 'path';
export default defineConfig({
build: {
rollupOptions: {
input: {
main: resolve(__dirname, 'index.html'),
admin: resolve(__dirname, 'admin/index.html'),
mobile: resolve(__dirname, 'mobile/index.html'),
},
},
},
});
项目结构:
├── index.html # 主页面
├── admin/
│ └── index.html # 管理后台页面
├── mobile/
│ └── index.html # 移动端页面
├── src/
│ ├── main.js # 主页面入口
│ ├── admin.js # 管理后台入口
│ └── mobile.js # 移动端入口
└── vite.config.js
html
<!-- index.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>主页</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
html
<!-- admin/index.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>管理后台</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/admin.js"></script>
</body>
</html>
共享代码和样式
javascript
// src/shared/utils.js - 共享工具函数
export function formatDate(date) {
return new Date(date).toLocaleDateString('zh-CN');
}
export function formatCurrency(amount) {
return `¥${amount.toFixed(2)}`;
}
css
/* src/shared/common.css - 共享样式 */
:root {
--primary-color: #1890ff;
--success-color: #52c41a;
--warning-color: #faad14;
--error-color: #f5222d;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
line-height: 1.6;
}
javascript
// src/main.js - 主页面入口
import { createApp } from 'vue';
import App from './App.vue';
import './shared/common.css';
createApp(App).mount('#app');
javascript
// src/admin.js - 管理后台入口
import { createApp } from 'vue';
import AdminApp from './AdminApp.vue';
import './shared/common.css';
import './admin/admin.css';
createApp(AdminApp).mount('#app');
MPA构建优化
javascript
// vite.config.js
import { defineConfig } from 'vite';
import { resolve } from 'path';
export default defineConfig({
build: {
rollupOptions: {
input: {
main: resolve(__dirname, 'index.html'),
admin: resolve(__dirname, 'admin/index.html'),
mobile: resolve(__dirname, 'mobile/index.html'),
},
output: {
// 分包策略
manualChunks(id) {
// 共享代码单独打包
if (id.includes('src/shared')) {
return 'shared';
}
// Vue核心库
if (id.includes('node_modules/vue')) {
return 'vue';
}
// 其他第三方库
if (id.includes('node_modules')) {
return 'vendor';
}
},
},
},
},
});
关键点解析:
- 多入口:每个页面都有独立的HTML和JS入口
- 代码共享:共享的代码和样式可以被多个页面复用
- 独立构建:每个页面可以独立构建和部署
- 分包优化:共享代码单独打包,避免重复
- 灵活部署:可以选择性部署某些页面
核心概念10:Vite的库模式
Vite可以用来构建JavaScript库,支持多种输出格式。
库模式配置
javascript
// vite.config.js
import { defineConfig } from 'vite';
import { resolve } from 'path';
export default defineConfig({
build: {
lib: {
entry: resolve(__dirname, 'src/index.js'),
name: 'MyLib',
fileName: (format) => `my-lib.${format}.js`,
formats: ['es', 'cjs', 'umd', 'iife'],
},
rollupOptions: {
// 确保外部化处理那些你不想打包进库的依赖
external: ['vue', 'react'],
output: {
// 在UMD构建模式下为这些外部化的依赖提供一个全局变量
globals: {
vue: 'Vue',
react: 'React',
},
},
},
},
});
javascript
// src/index.js - 库入口
export { default as Button } from './components/Button.vue';
export { default as Input } from './components/Input.vue';
export { default as Modal } from './components/Modal.vue';
export * from './utils';
export * from './hooks';
json
// package.json
{
"name": "my-lib",
"version": "1.0.0",
"type": "module",
"main": "./dist/my-lib.cjs.js",
"module": "./dist/my-lib.es.js",
"exports": {
".": {
"import": "./dist/my-lib.es.js",
"require": "./dist/my-lib.cjs.js"
},
"./style.css": "./dist/style.css"
},
"files": [
"dist"
],
"scripts": {
"build": "vite build"
},
"peerDependencies": {
"vue": "^3.0.0"
}
}
构建Vue组件库
javascript
// vite.config.js - Vue组件库配置
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import { resolve } from 'path';
export default defineConfig({
plugins: [vue()],
build: {
lib: {
entry: resolve(__dirname, 'src/index.js'),
name: 'MyComponentLib',
fileName: (format) => `my-component-lib.${format}.js`,
},
rollupOptions: {
external: ['vue'],
output: {
globals: {
vue: 'Vue',
},
// 导出CSS
assetFileNames: (assetInfo) => {
if (assetInfo.name === 'style.css') {
return 'my-component-lib.css';
}
return assetInfo.name;
},
},
},
},
});
javascript
// src/index.js
import './styles/index.css';
export { default as Button } from './Button.vue';
export { default as Input } from './Input.vue';
export { default as Modal } from './Modal.vue';
// 提供安装方法
export default {
install(app) {
app.component('MyButton', Button);
app.component('MyInput', Input);
app.component('MyModal', Modal);
},
};
关键点解析:
- 多格式输出:支持ES、CJS、UMD、IIFE等格式
- 外部依赖:将Vue等依赖标记为external,避免打包
- 类型定义:可以配合TypeScript生成类型定义文件
- 样式处理:自动提取和打包CSS
- 按需导入:支持Tree Shaking,用户可以按需导入
最佳实践
企业级应用场景
场景1:大型Vue 3项目配置
javascript
/**
* 完整的企业级Vite配置
*/
import { defineConfig, loadEnv } from 'vite';
import vue from '@vitejs/plugin-vue';
import vueJsx from '@vitejs/plugin-vue-jsx';
import AutoImport from 'unplugin-auto-import/vite';
import Components from 'unplugin-vue-components/vite';
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers';
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons';
import viteCompression from 'vite-plugin-compression';
import { visualizer } from 'rollup-plugin-visualizer';
import path from 'path';
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd());
const isProduction = mode === 'production';
return {
base: env.VITE_APP_BASE_URL || '/',
plugins: [
// Vue支持
vue({
script: {
defineModel: true,
propsDestructure: true
}
}),
// JSX支持
vueJsx(),
// 自动导入API
AutoImport({
imports: ['vue', 'vue-router', 'pinia'],
dts: 'src/auto-imports.d.ts',
dirs: ['src/composables', 'src/stores'],
eslintrc: {
enabled: true
},
resolvers: [ElementPlusResolver()]
}),
// 自动导入组件
Components({
dts: 'src/components.d.ts',
resolvers: [ElementPlusResolver()]
}),
// SVG图标
createSvgIconsPlugin({
iconDirs: [path.resolve(process.cwd(), 'src/icons')],
symbolId: 'icon-[dir]-[name]'
}),
// 生产环境插件
...(isProduction ? [
// Gzip压缩
viteCompression({
algorithm: 'gzip',
threshold: 10240
}),
// Bundle分析
visualizer({
open: false,
gzipSize: true,
brotliSize: true,
filename: 'dist/stats.html'
})
] : [])
],
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
'components': path.resolve(__dirname, 'src/components'),
'utils': path.resolve(__dirname, 'src/utils'),
'api': path.resolve(__dirname, 'src/api'),
'views': path.resolve(__dirname, 'src/views'),
'stores': path.resolve(__dirname, 'src/stores')
},
extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue']
},
css: {
preprocessorOptions: {
scss: {
additionalData: `
@import "@/styles/variables.scss";
@import "@/styles/mixins.scss";
`
}
}
},
server: {
host: '0.0.0.0',
port: 3000,
open: true,
cors: true,
proxy: {
'/api': {
target: env.VITE_APP_API_URL,
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
},
build: {
target: 'es2015',
outDir: 'dist',
assetsDir: 'assets',
sourcemap: !isProduction,
minify: 'esbuild',
chunkSizeWarningLimit: 1000,
rollupOptions: {
output: {
manualChunks: {
'vue-vendor': ['vue', 'vue-router', 'pinia'],
'ui-vendor': ['element-plus'],
'utils': ['axios', 'lodash-es', 'dayjs']
},
entryFileNames: 'js/[name]-[hash].js',
chunkFileNames: 'js/[name]-[hash].js',
assetFileNames: (assetInfo) => {
const info = assetInfo.name.split('.');
let extType = info[info.length - 1];
if (/\.(png|jpe?g|gif|svg|ico|webp)(\?.*)?$/i.test(assetInfo.name)) {
extType = 'images';
} else if (/\.(woff2?|eot|ttf|otf)(\?.*)?$/i.test(assetInfo.name)) {
extType = 'fonts';
} else if (/\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/i.test(assetInfo.name)) {
extType = 'media';
}
return `${extType}/[name]-[hash][extname]`;
}
}
}
},
optimizeDeps: {
include: [
'vue',
'vue-router',
'pinia',
'axios',
'element-plus',
'lodash-es',
'dayjs'
]
}
};
});
场景2:Monorepo项目配置
javascript
/**
* Monorepo根目录的vite.config.js
*/
// packages/shared/vite.config.js
export default defineConfig({
build: {
lib: {
entry: path.resolve(__dirname, 'src/index.ts'),
name: 'Shared',
fileName: (format) => `shared.${format}.js`
},
rollupOptions: {
external: ['vue'],
output: {
globals: {
vue: 'Vue'
}
}
}
}
});
// packages/app/vite.config.js
export default defineConfig({
resolve: {
alias: {
'@shared': path.resolve(__dirname, '../shared/src')
}
}
});
常见陷阱
陷阱1:依赖预构建问题
javascript
/**
* 问题:某些依赖没有被正确预构建
*/
// ❌ 错误:依赖使用了CommonJS格式,但没有被预构建
import someLib from 'some-lib'; // 报错:Cannot use import statement outside a module
// ✅ 解决方案:手动添加到optimizeDeps.include
export default defineConfig({
optimizeDeps: {
include: ['some-lib']
}
});
/**
* 问题:预构建缓存导致的问题
*/
// 症状:修改了依赖版本,但仍然使用旧版本
// 解决方案:清除预构建缓存
// npm run dev -- --force
// 或手动删除node_modules/.vite目录
陷阱2:环境变量使用错误
javascript
/**
* 问题:在Vite中使用process.env
*/
// ❌ 错误:Vite不支持process.env
console.log(process.env.VUE_APP_API_URL); // undefined
// ✅ 正确:使用import.meta.env
console.log(import.meta.env.VITE_APP_API_URL);
/**
* 问题:环境变量命名错误
*/
// ❌ 错误:没有VITE_前缀的环境变量不会暴露给客户端
// .env
API_URL=http://localhost:8080
// 代码中无法访问
console.log(import.meta.env.API_URL); // undefined
// ✅ 正确:使用VITE_前缀
// .env
VITE_APP_API_URL=http://localhost:8080
// 代码中可以访问
console.log(import.meta.env.VITE_APP_API_URL);
陷阱3:动态导入路径问题
javascript
/**
* 问题:动态导入使用变量路径
*/
// ❌ 错误:完全动态的路径无法工作
const moduleName = 'MyModule';
import(`./modules/${moduleName}.js`); // 无法正确解析
// ✅ 解决方案1:使用import.meta.glob
const modules = import.meta.glob('./modules/*.js');
const module = await modules[`./modules/${moduleName}.js`]();
// ✅ 解决方案2:使用固定的路径模式
import(`./modules/${moduleName}.js`); // 路径模式是固定的
// ✅ 解决方案3:使用@vite-ignore注释(不推荐)
import(/* @vite-ignore */ dynamicPath);
陷阱4:CSS导入顺序问题
javascript
/**
* 问题:CSS导入顺序影响样式优先级
*/
// main.js
import 'element-plus/dist/index.css'; // 第三方库样式
import './styles/global.css'; // 全局样式
import './styles/override.css'; // 覆盖样式
// 确保导入顺序正确,后导入的样式优先级更高
/**
* 问题:CSS Modules命名冲突
*/
// ❌ 错误:普通CSS和CSS Modules混用
import './style.css'; // 普通CSS
import styles from './style.module.css'; // CSS Modules
// ✅ 正确:明确区分
import './global.css'; // 全局样式
import styles from './component.module.css'; // 组件样式
关键点解析:
- 生产构建:使用Rollup打包,支持代码分割和Tree Shaking
- 性能优化:使用esbuild加速构建,配置合理的分包策略
- 常见陷阱:注意依赖预构建、环境变量、动态导入等问题
- 企业级配置:合理组织插件、配置别名、优化构建输出
实践练习
练习1:配置一个完整的Vite项目(难度:中等)
需求描述
创建一个企业级的Vue 3 + TypeScript + Vite项目,实现以下功能:
-
项目结构:
- 使用TypeScript
- 配置路径别名(@指向src目录)
- 配置环境变量(开发环境和生产环境)
- 配置代理解决跨域问题
-
构建优化:
- 配置代码分割策略
- 配置资源压缩
- 配置CDN加速
- 生成构建分析报告
-
开发体验:
- 配置自动导入(Vue组件、API、图标)
- 配置ESLint和Prettier
- 配置Git Hooks
实现提示
- 使用
create-vite创建项目基础结构 - 在
vite.config.ts中配置所有选项 - 使用
unplugin-auto-import和unplugin-vue-components实现自动导入 - 使用
rollup-plugin-visualizer生成构建分析报告 - 使用
vite-plugin-compression进行资源压缩
参考答案
typescript
// vite.config.ts
import { defineConfig, loadEnv } from 'vite';
import vue from '@vitejs/plugin-vue';
import { resolve } from 'path';
import AutoImport from 'unplugin-auto-import/vite';
import Components from 'unplugin-vue-components/vite';
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers';
import { visualizer } from 'rollup-plugin-visualizer';
import viteCompression from 'vite-plugin-compression';
// https://vitejs.dev/config/
export default defineConfig(({ mode }) => {
// 加载环境变量
const env = loadEnv(mode, process.cwd());
return {
// 基础配置
base: env.VITE_BASE_URL || '/',
// 插件配置
plugins: [
// Vue 3支持
vue(),
// 自动导入Vue API
AutoImport({
imports: ['vue', 'vue-router', 'pinia'],
resolvers: [ElementPlusResolver()],
dts: 'src/auto-imports.d.ts',
eslintrc: {
enabled: true, // 生成ESLint配置
},
}),
// 自动导入组件
Components({
resolvers: [ElementPlusResolver()],
dts: 'src/components.d.ts',
}),
// Gzip压缩
viteCompression({
verbose: true,
disable: false,
threshold: 10240, // 大于10KB的文件才压缩
algorithm: 'gzip',
ext: '.gz',
}),
// 构建分析
visualizer({
open: true,
gzipSize: true,
brotliSize: true,
}),
],
// 路径别名
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
'@components': resolve(__dirname, 'src/components'),
'@views': resolve(__dirname, 'src/views'),
'@utils': resolve(__dirname, 'src/utils'),
'@api': resolve(__dirname, 'src/api'),
'@store': resolve(__dirname, 'src/store'),
'@assets': resolve(__dirname, 'src/assets'),
},
},
// 开发服务器配置
server: {
host: '0.0.0.0', // 允许外部访问
port: 3000,
open: true, // 自动打开浏览器
cors: true, // 允许跨域
// 代理配置
proxy: {
'/api': {
target: env.VITE_API_BASE_URL,
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
},
},
},
// 构建配置
build: {
target: 'es2015', // 浏览器兼容性目标
outDir: 'dist', // 输出目录
assetsDir: 'assets', // 静态资源目录
sourcemap: mode === 'development', // 开发环境生成sourcemap
// 代码分割策略
rollupOptions: {
output: {
// 分包策略
manualChunks: {
// Vue核心库
'vue-vendor': ['vue', 'vue-router', 'pinia'],
// UI组件库
'element-plus': ['element-plus'],
// 工具库
'utils': ['axios', 'dayjs', 'lodash-es'],
},
// 文件命名
chunkFileNames: 'js/[name]-[hash].js',
entryFileNames: 'js/[name]-[hash].js',
assetFileNames: '[ext]/[name]-[hash].[ext]',
},
},
// 压缩配置
minify: 'terser',
terserOptions: {
compress: {
drop_console: mode === 'production', // 生产环境移除console
drop_debugger: true,
},
},
// 资源大小警告阈值
chunkSizeWarningLimit: 1000, // 1MB
},
// CSS配置
css: {
preprocessorOptions: {
scss: {
additionalData: `@use "@/styles/variables.scss" as *;`,
},
},
},
// 依赖优化
optimizeDeps: {
include: ['vue', 'vue-router', 'pinia', 'element-plus'],
},
};
});
typescript
// .env.development
VITE_BASE_URL=/
VITE_API_BASE_URL=http://localhost:8080
VITE_APP_TITLE=开发环境
typescript
// .env.production
VITE_BASE_URL=/
VITE_API_BASE_URL=https://api.example.com
VITE_APP_TITLE=生产环境
json
// package.json
{
"name": "vite-vue3-project",
"version": "1.0.0",
"scripts": {
"dev": "vite",
"build": "vue-tsc && vite build",
"build:dev": "vue-tsc && vite build --mode development",
"build:prod": "vue-tsc && vite build --mode production",
"preview": "vite preview",
"lint": "eslint . --ext .vue,.js,.ts,.jsx,.tsx --fix",
"format": "prettier --write \"src/**/*.{js,ts,vue,scss,css}\""
},
"dependencies": {
"vue": "^3.4.0",
"vue-router": "^4.2.0",
"pinia": "^2.1.0",
"element-plus": "^2.5.0",
"axios": "^1.6.0",
"dayjs": "^1.11.0",
"lodash-es": "^4.17.21"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.0",
"vite": "^5.0.0",
"typescript": "^5.3.0",
"vue-tsc": "^1.8.0",
"unplugin-auto-import": "^0.17.0",
"unplugin-vue-components": "^0.26.0",
"rollup-plugin-visualizer": "^5.12.0",
"vite-plugin-compression": "^0.5.1",
"eslint": "^8.56.0",
"prettier": "^3.1.0",
"@typescript-eslint/eslint-plugin": "^6.18.0",
"@typescript-eslint/parser": "^6.18.0",
"eslint-plugin-vue": "^9.19.0",
"sass": "^1.69.0"
}
}
答案解析
这个配置实现了一个完整的企业级Vite项目:
-
插件配置:
@vitejs/plugin-vue:Vue 3支持unplugin-auto-import:自动导入Vue API,减少import语句unplugin-vue-components:自动导入组件,支持按需加载vite-plugin-compression:Gzip压缩,减小文件体积rollup-plugin-visualizer:构建分析,可视化bundle大小
-
路径别名:
- 使用
@指向src目录,简化导入路径 - 为常用目录配置别名,提高开发效率
- 使用
-
开发服务器:
- 配置代理解决跨域问题
- 支持外部访问(host: '0.0.0.0')
- 自动打开浏览器
-
构建优化:
- 代码分割:将Vue核心库、UI组件库、工具库分别打包
- 压缩优化:使用terser压缩,生产环境移除console
- 文件命名:使用hash确保缓存更新
-
环境变量:
- 使用
.env文件管理不同环境的配置 - 通过
import.meta.env访问环境变量
- 使用
练习2:开发一个Vite插件(难度:困难)
需求描述
开发一个Vite插件,实现以下功能:
-
自动生成路由:
- 扫描
src/views目录下的所有.vue文件 - 根据文件路径自动生成路由配置
- 支持动态路由(文件名包含
[id]) - 支持嵌套路由(子目录)
- 扫描
-
热更新支持:
- 文件变化时自动更新路由配置
- 支持HMR,无需刷新页面
-
TypeScript支持:
- 生成类型定义文件
实现提示
- 使用Vite的
configureServer钩子监听文件变化 - 使用
transform钩子注入路由配置 - 使用
fast-glob扫描文件 - 参考
vite-plugin-pages的实现
参考答案
typescript
// plugins/vite-plugin-auto-routes.ts
import type { Plugin } from 'vite';
import { glob } from 'fast-glob';
import { resolve, relative, parse } from 'path';
import fs from 'fs/promises';
interface RouteConfig {
path: string;
name: string;
component: string;
children?: RouteConfig[];
}
/**
* 自动生成路由的Vite插件
*/
export function autoRoutesPlugin(options: {
pagesDir?: string;
extensions?: string[];
} = {}): Plugin {
const {
pagesDir = 'src/views',
extensions = ['vue'],
} = options;
let root: string;
let routesCode: string;
/**
* 扫描页面文件并生成路由配置
*/
async function generateRoutes(): Promise<RouteConfig[]> {
const pagesPath = resolve(root, pagesDir);
// 扫描所有页面文件
const files = await glob(`**/*.{${extensions.join(',')}}`, {
cwd: pagesPath,
ignore: ['**/components/**', '**/_*/**'],
});
// 生成路由配置
const routes: RouteConfig[] = [];
for (const file of files) {
const { dir, name } = parse(file);
// 生成路由路径
let path = `/${dir}/${name}`.replace(/\/index$/, '').replace(/\/$/, '') || '/';
// 处理动态路由:[id].vue -> :id
path = path.replace(/\[(\w+)\]/g, ':$1');
// 生成路由名称
const routeName = file.replace(/\//g, '-').replace(/\.\w+$/, '');
// 生成组件导入路径
const componentPath = `/${pagesDir}/${file}`;
routes.push({
path,
name: routeName,
component: componentPath,
});
}
return routes;
}
/**
* 生成路由代码
*/
function generateRoutesCode(routes: RouteConfig[]): string {
const imports: string[] = [];
const routeConfigs: string[] = [];
routes.forEach((route, index) => {
const componentName = `Component${index}`;
imports.push(`import ${componentName} from '${route.component}';`);
routeConfigs.push(`{
path: '${route.path}',
name: '${route.name}',
component: ${componentName},
}`);
});
return `// 此文件由vite-plugin-auto-routes自动生成,请勿手动修改
${imports.join('\n')}
export const routes = [
${routeConfigs.join(',\n')}
];
`;
}
return {
name: 'vite-plugin-auto-routes',
// 配置解析完成后执行
async configResolved(config) {
root = config.root;
// 生成初始路由
const routes = await generateRoutes();
routesCode = generateRoutesCode(routes);
},
// 配置开发服务器
configureServer(server) {
const pagesPath = resolve(root, pagesDir);
// 监听文件变化
server.watcher.add(pagesPath);
server.watcher.on('all', async (event, file) => {
// 只处理pages目录下的文件
if (!file.startsWith(pagesPath)) return;
// 文件变化时重新生成路由
if (event === 'add' || event === 'unlink') {
console.log(`[auto-routes] 检测到文件${event === 'add' ? '添加' : '删除'}: ${relative(root, file)}`);
const routes = await generateRoutes();
routesCode = generateRoutesCode(routes);
// 触发HMR
const module = server.moduleGraph.getModuleById('virtual:auto-routes');
if (module) {
server.moduleGraph.invalidateModule(module);
server.ws.send({
type: 'full-reload',
path: '*',
});
}
}
});
},
// 解析虚拟模块
resolveId(id) {
if (id === 'virtual:auto-routes') {
return '\0virtual:auto-routes';
}
},
// 加载虚拟模块
load(id) {
if (id === '\0virtual:auto-routes') {
return routesCode;
}
},
// 生成类型定义
async buildEnd() {
const dtsPath = resolve(root, 'src/auto-routes.d.ts');
const dtsContent = `// 此文件由vite-plugin-auto-routes自动生成,请勿手动修改
declare module 'virtual:auto-routes' {
import type { RouteRecordRaw } from 'vue-router';
export const routes: RouteRecordRaw[];
}
`;
await fs.writeFile(dtsPath, dtsContent, 'utf-8');
},
};
}
typescript
// vite.config.ts
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import { autoRoutesPlugin } from './plugins/vite-plugin-auto-routes';
export default defineConfig({
plugins: [
vue(),
autoRoutesPlugin({
pagesDir: 'src/views',
extensions: ['vue'],
}),
],
});
typescript
// src/router/index.ts
import { createRouter, createWebHistory } from 'vue-router';
import { routes } from 'virtual:auto-routes';
const router = createRouter({
history: createWebHistory(),
routes,
});
export default router;
typescript
// package.json
{
"devDependencies": {
"fast-glob": "^3.3.2"
}
}
答案解析
这个插件实现了自动路由生成功能:
-
插件钩子:
configResolved:配置解析完成后,生成初始路由configureServer:配置开发服务器,监听文件变化resolveId:解析虚拟模块IDload:加载虚拟模块内容buildEnd:构建结束后生成类型定义
-
路由生成逻辑:
- 使用
fast-glob扫描src/views目录 - 根据文件路径生成路由路径
- 处理动态路由(
[id].vue->:id) - 处理index文件(
index.vue->/)
- 使用
-
热更新支持:
- 监听文件变化(添加、删除)
- 重新生成路由配置
- 触发HMR更新
-
虚拟模块:
- 使用虚拟模块
virtual:auto-routes导出路由配置 - 避免生成实际文件,提高性能
- 使用虚拟模块
-
TypeScript支持:
- 生成类型定义文件
auto-routes.d.ts - 提供类型提示和检查
- 生成类型定义文件
这个插件展示了Vite插件开发的核心概念和最佳实践,可以作为开发其他插件的参考。
进阶阅读
- Vite官方文档 - Vite官方中文文档
- Rollup官方文档 - Vite生产构建使用的打包工具
- esbuild官方文档 - Vite依赖预构建使用的工具
- Vite插件开发指南 - 官方插件开发文档
- Awesome Vite - Vite相关资源汇总
- Vite源码解析 - 深入理解Vite的实现原理
- ES Modules详解 - MDN的ES模块文档
- HMR API文档 - Vite的HMR API详细说明
下一步
恭喜你完成了Vite构建工具的学习!
通过本章的学习,你已经掌握了:
- Vite的核心特性和性能优势(ESM原生支持、极速的HMR)
- Vite的配置方法和常用配置项
- 依赖预构建的工作原理和优化策略
- 生产构建的优化方法(代码分割、Tree Shaking、资源压缩)
- Vite插件系统和自定义插件开发
- 环境变量和模式的使用方法
- 常见问题和解决方案
Vite是新一代前端构建工具,它充分利用了浏览器的原生ES模块能力,提供了极快的开发体验。理解Vite的工作原理,能帮助你更好地优化项目配置和开发流程。
下一章预告
下一章我们将学习Webpack基础,它将帮助你:
- 理解Webpack这个经典构建工具的核心概念
- 掌握Webpack的配置方法和优化策略
- 学习loader和plugin的使用和开发
- 理解代码分割、懒加载等高级特性
- 对比Vite和Webpack的区别和适用场景
虽然Vite是新一代工具,但Webpack仍然是目前使用最广泛的构建工具,很多企业级项目都在使用。掌握Webpack能让你在不同场景下游刃有余,也能帮助你更好地理解构建工具的演进历程。
实践建议
在学习下一章之前,建议你:
-
动手实践:
- 使用Vite创建一个完整的Vue 3项目
- 尝试配置多页面应用
- 开发一个自定义Vite插件
- 优化一个现有项目的构建配置
-
深入研究:
- 阅读Vite源码,理解其实现原理
- 研究Rollup的插件机制
- 学习esbuild的性能优化技巧
- 探索Vite的SSR支持
-
持续学习:
- 关注Vite的版本更新和新特性
- 参与Vite社区讨论
- 分享你的Vite使用经验
- 尝试为Vite贡献代码或插件
对比思考
学习Webpack时,建议你思考以下问题:
- Vite和Webpack在开发体验上有什么区别?
- 为什么Vite的开发服务器启动这么快?
- 什么场景下应该选择Vite,什么场景下应该选择Webpack?
- 如何将现有的Webpack项目迁移到Vite?
这些思考将帮助你更深入地理解构建工具的本质,做出更合理的技术选型。