Webpack 与 Vite 完全指南 --- 从原理到实战
一、引言:为什么前端需要构建工具?
1.1 "刀耕火种"的时代
在构建工具普及之前,前端开发是这样写代码的:
xml
<script src="https://unpkg.com/jquery@3/dist/jquery.js"></script>
<script src="js/utils.js"></script>
<script src="js/app.js"></script>
<script src="js/components/nav.js"></script>
这种方式有一堆问题:
| 问题 | 表现 |
|---|---|
| 全局变量污染 | 所有 script 的变量都在同一作用域,命名冲突是家常便饭 |
| 依赖顺序敏感 | app.js 必须在 utils.js 之后加载,否则报错 |
| 没法用 NPM 包 | 只能手动下载 JS 文件放到项目里 |
| 无法使用高级语法 | ES6+、TypeScript、JSX 浏览器不认识 |
| 没有模块化 | 代码组织只能靠文件夹和命名约定 |
| 资源优化靠手动 | 压缩代码、合并文件、图片压缩,全都手动操作 |
1.2 构建工具解决了什么
构建工具(Build Tool) 是一个"翻译 + 打包 + 优化"的流水线:
源代码(.vue / .tsx / .scss ...)
↓ 构建工具
转译(TS → JS, SCSS → CSS, JSX → JS...)
↓
打包(合并文件、代码分割)
↓
优化(压缩、tree-shaking、图片优化...)
↓
最终产物(可在浏览器中运行的 .js / .css / .html)
核心价值:
- 模块化 :
import/export让代码组织清晰 - 转译:TypeScript、JSX、Vue SFC 等语法浏览器不认识?构建工具来翻译
- 优化:代码压缩、Tree Shaking、Code Splitting 自动完成
- 开发体验:热更新(HMR),改代码即时看到效果
而 Webpack 和 Vite,是目前最主流的两套构建工具。这篇文章会带你深入理解它们的原理,并学会在实战中使用它们。
二、Webpack 核心概念与原理
2.1 核心概念速览
Webpack 有五个核心概念,理解了它们就理解了 Webpack 的骨架:
bash
Entry(入口) 指示 Webpack 从哪个文件开始打包
↓
Loaders(加载器) 处理非 JS 文件(CSS、图片、TS 等),将它们转为 Webpack 能处理的模块
↓
Plugins(插件) 执行范围更广的任务(打包优化、资源管理、环境变量注入)
↓
Output(输出) 打包完成后输出到哪里
↓
Module(模块) Webpack 中一切皆模块(JS/CSS/图片/font 都是模块)
819
最小配置示例
javascript
// webpack.config.js
const path = require('path')
module.exports = {
// 1. 入口
entry: './src/index.js',
// 2. 输出
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js',
clean: true, // 每次构建前清理 dist 目录
},
// 3. Loader 规则
module: {
rules: [
{
test: /.css$/,
use: ['style-loader', 'css-loader'],
},
],
},
// 4. 插件
plugins: [],
// 5. 模式
mode: 'development',
}
2.2 打包流程全解析
当你运行 webpack 命令时,内部发生了一件精密的"装配线"作业:
markdown
1. 读取配置(webpack.config.js + CLI 参数)
2. 创建 Compiler 对象(webpack 的核心调度器)
3. 运行 Compiler.run(),启动编译
4. 从 Entry 开始,解析入口文件
5. 遇到 import/require → 递归解析依赖 → 构建依赖关系图(Module Graph)
6. 遇到非 JS 文件 → 找匹配的 Loader 处理
7. 遍历完所有依赖 → 得到完整的 Module Graph
8. 根据 Module Graph,将模块合并成 Chunk
9. 将 Chunk 输出为文件(写入 Output 目录)
这个过程有一个很重要的概念叫 AST(抽象语法树) :
Loader 的转换链路
xml
.css 文件
↓ css-loader 解析 @import 和 url(),将 CSS 转为 JS 模块
↓ style-loader 将 CSS 注入到页面的 <style> 标签中
↓ 最终产物:JS 代码(运行时向 DOM 插入样式)
Loader 的本质是一个函数 ,接收源文件内容,返回转换后的内容。多个 Loader 串联执行,顺序是从右到左,从下到上。
javascript
// 一个最简单的 Loader
module.exports = function(source) {
return source.replace(/console.log(.*?)/g, '') // 移除 console.log
}
2.3 Loader 机制详解
Loader 是 Webpack 处理"非 JS 文件"的唯一方式。因为 Webpack 本质上只理解 JavaScript,其他一切文件(CSS、图片、字体、模板)都需要通过 Loader 转换为 JS 模块。
常用 Loader 一览
| Loader | 用途 |
|---|---|
babel-loader |
将 ES6+ 转译为 ES5 |
ts-loader |
处理 TypeScript |
css-loader |
解析 CSS 中的 @import 和 url() |
style-loader |
将 CSS 注入到 DOM 的 <style> 标签 |
sass-loader |
将 SCSS/SASS 编译为 CSS |
less-loader |
将 Less 编译为 CSS |
postcss-loader |
自动添加 CSS 前缀(autoprefixer) |
asset/resource |
处理图片/字体等文件(Webpack 5 内置) |
vue-loader |
处理 .vue 单文件组件 |
html-loader |
处理 HTML 中的图片引用 |
Loader 的执行顺序
javascript
module: {
rules: [
{
test: /.scss$/,
// 执行顺序:从右到左
// sass-loader → postcss-loader → css-loader → style-loader
use: [
'style-loader',
{ loader: 'css-loader', options: { modules: true } },
'postcss-loader',
'sass-loader',
],
},
],
}
理解顺序:最后一个 Loader 最先处理源文件,然后依次往前传递结果。就像工厂流水线------原料从右端进入,从左端产出成品。
2.4 Plugin 机制:Tapable 钩子系统
如果说 Loader 是处理"文件转换"的,那 Plugin 就是干预构建流程本身的。
Webpack 的构建流程就像一条流水线,不同阶段会触发不同的"钩子(hooks)" 。Plugin 可以在这些钩子上注册自己的逻辑,干预构建过程。
javascript
// 一个最简单的 Plugin
class MyPlugin {
// apply 方法接收 compiler 对象
apply(compiler) {
// emit 是 Webpack 的一个钩子------即将输出文件时触发
compiler.hooks.emit.tap('MyPlugin', (compilation) => {
console.log('文件即将输出,当前有', Object.keys(compilation.assets).length, '个文件')
})
}
}
Webpack 的钩子系统由 Tapable 库实现,这是一套发布-订阅模式,有几十个生命周期钩子:
arduino
开始编译(run)
↓
编译开始(compile)
↓
创建 Compilation(compilation)
↓
make(开始递归构建模块)
↓
build-module(开始构建一个模块)
↓
seal(封装所有模块为 Chunk)
↓
optimize(优化阶段,多个钩子)
↓
emit(输出文件到 output 目录)
↓
done(构建完成)
常用 Plugin 一览
arduino
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html',
}), // 自动生成 HTML,并注入 JS 引用
new MiniCssExtractPlugin({
filename: 'css/[name].[contenthash:8].css',
}), // 将 CSS 提取为独立文件(代替 style-loader)
new DefinePlugin({
'process.env.API_URL': JSON.stringify('https://api.example.com'),
}), // 注入全局变量
new webpack.HotModuleReplacementPlugin(), // 启用 HMR
],
2.5 HMR 热更新原理
HMR(Hot Module Replacement)是 Webpack 最受欢迎的特性之一------修改代码后只替换改动的模块,不刷新整个页面,保留应用状态。
HMR 的工作流程:
arduino
你在编辑器中修改了 style.css
↓
Webpack Dev Server 监听到文件变化
↓
Webpack 重新编译被修改的模块
↓
通过 WebSocket 向浏览器发送 "hash" 和 "ok" 消息
↓
浏览器收到消息,通过 JSONP 请求新的模块代码
↓
HMR Runtime 执行模块热替换逻辑
↓
如果是 CSS:直接替换 <style> 标签内容(无需任何额外配置)
如果是 JS:需要模块自身声明 accept 才能热替换
为什么 CSS 默认支持 HMR? 因为 CSS 不涉及应用状态,直接替换样式即可。JS 模块的 HMR 需要开发者显式处理,比如:
javascript
if (module.hot) {
module.hot.accept('./component.js', () => {
// 重新渲染组件,保留当前状态
rerender()
})
}
2.6 动手实战:从零搭建一个 Webpack 项目
前面讲了这么多概念,现在我们来真刀真枪地建一个项目。打开终端,跟着下面的步骤一步步做。
第 1 步:创建项目目录并初始化
perl
# 创建项目文件夹
mkdir my-webpack-app
cd my-webpack-app
# 初始化 package.json
npm init -y
执行完 npm init -y 后,项目里会出现一个 package.json 文件。这个文件记录了项目的依赖和脚本。
第 2 步:安装 webpack 核心包
css
npm install webpack webpack-cli --save-dev
这条命令干了什么?
| 安装的包 | 作用 |
|---|---|
webpack |
Webpack 的核心库,负责打包逻辑 |
webpack-cli |
命令行工具,让你能在终端敲 npx webpack 命令 |
--save-dev 表示这些是开发依赖(devDependencies),生产环境不需要------你的线上代码已经是打包好的成品了。
安装完成后,package.json 里会出现 devDependencies 字段:
json
{
"devDependencies": {
"webpack": "^5.97.0",
"webpack-cli": "^6.0.0"
}
}
第 3 步:创建项目文件结构
arduino
# 创建源码目录和 HTML 目录
mkdir src public
现在创建你的第一个入口文件。新建 src/index.js:
javascript
// src/index.js
function sayHello(name) {
return `Hello, ${name}! 这是 Webpack 打包的。`
}
console.log(sayHello('新手'))
再创建 public/index.html------这是你的 HTML 模板:
xml
<!-- public/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>我的第一个 Webpack 项目</title>
</head>
<body>
<div id="app"></div>
</body>
</html>
此时项目结构如下:
csharp
my-webpack-app/
├── node_modules/ # 安装的依赖(自动生成,不用管)
├── public/
│ └── index.html # HTML 模板
├── src/
│ └── index.js # JS 入口文件
├── package.json # 项目配置文件
└── package-lock.json # 依赖版本锁定文件(自动生成)
第 4 步:添加 npm 脚本
打开 package.json,在 "scripts" 字段中添加 "build" 脚本:
json
{
"scripts": {
"build": "webpack"
}
}
此时运行 npm run build 会报错?别急------我们还没告诉 Webpack 怎么打包 呢。Webpack 需要知道三件事:入口在哪 、输出到哪 、用哪种模式。
第 5 步:创建第一个 webpack.config.js
在项目根目录创建 webpack.config.js:
java
// webpack.config.js
const path = require('path')
module.exports = {
// ① 入口:Webpack 从哪个文件开始打包
entry: './src/index.js',
// ② 输出:打包好的文件放到哪里
output: {
// 输出目录的绝对路径(__dirname 是当前文件所在目录)
path: path.resolve(__dirname, 'dist'),
// 输出的文件名
filename: 'bundle.js',
// 每次构建前先清空 dist 目录
clean: true,
},
// ③ 模式:development(开发)或 production(生产)
mode: 'development',
}
最基本的 Webpack 配置只需要这三个字段。现在运行:
arduino
npm run build
你会看到类似这样的输出:
less
> my-webpack-app@1.0.0 build
> webpack
asset bundle.js 19 KiB [emitted] (name: main)
./src/index.js 79 bytes [built] [code generated]
webpack 5.97.0 compiled successfully in 92 ms
发生了什么事?
arduino
src/index.js(你的源代码)
↓ Webpack 读取 entry,开始处理
↓ ES module 语法(import/export 等)被识别
↓ 生成 bundle.js(浏览器能直接运行的纯 JS)
↓ 输出到 dist/bundle.js
看看打包产物是什么样的:
bash
cat dist/bundle.js
虽然代码看起来经过了转换,但功能是一样的。你可以试试在浏览器中打开 dist/bundle.js 看看------它已经是一个独立的、可以引用的文件了。
👆 你已经成功运行了 Webpack! 只看不练永远觉得难,实际上核心流程就三步:
entry➔output➔mode。
第 6 步:添加 HTML 自动注入
目前只是打包了 JS,但 HTML 还需要手动引用 bundle.js。安装一个插件让它自动完成:
css
npm install html-webpack-plugin --save-dev
修改 webpack.config.js:
javascript
// webpack.config.js(新增内容用 👈 标注)
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin') // 👈 引入插件
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js',
clean: true,
},
// 👈 新增:插件配置
plugins: [
new HtmlWebpackPlugin({
// 以 public/index.html 为模板
template: './public/index.html',
}),
],
mode: 'development',
}
再次运行:
arduino
npm run build
看看 dist/ 目录下发生了什么变化:
bash
dist/
├── bundle.js # 打包后的 JS
└── index.html # 自动生成的 HTML(已自动引用 bundle.js)
打开 dist/index.html,你会看到 </body> 前自动插入了:
xml
<script defer src="bundle.js"></script>
你不需要手动管理 JS 引用------Webpack 会自动处理好。
第 7 步:处理 CSS 文件
Webpack 默认只认识 JS。要处理 CSS,需要 Loader 来"翻译"。创建 CSS 文件:
bash
# 创建样式文件目录
mkdir src/styles
css
/* src/styles/main.css */
body {
background-color: #f0f0f0;
font-family: 'Arial', sans-serif;
}
#app::after {
content: '🎉 CSS 已加载成功!';
display: block;
margin-top: 20px;
font-size: 24px;
color: #42b883;
}
修改 src/index.js,引入 CSS:
javascript
// src/index.js
import './styles/main.css' // 👈 引入 CSS
function sayHello(name) {
return `Hello, ${name}! 这是 Webpack 打包的。`
}
// 将内容显示到页面上
document.querySelector('#app').textContent = sayHello('新手')
安装处理 CSS 所需的 Loader:
css
npm install style-loader css-loader --save-dev
| Loader | 作用 |
|---|---|
css-loader |
解析 CSS 文件中的 @import 和 url(),把 CSS 变成 JS 能用的模块 |
style-loader |
把 CSS 通过 <style> 标签注入到 HTML 页面中 |
在 webpack.config.js 中添加 Loader 规则:
css
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js',
clean: true,
},
// 👈 新增:Loader 配置
module: {
rules: [
{
// 正则匹配 .css 结尾的文件
test: /.css$/,
// 使用哪些 Loader(执行顺序:从右到左!)
use: ['style-loader', 'css-loader'],
},
],
},
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html',
}),
],
mode: 'development',
}
再次构建并查看效果。在浏览器中打开 dist/index.html,你应该能看到样式生效了。
理解 Loader 执行顺序 :
use: ['style-loader', 'css-loader']是从 右到左 执行的。先把
.css给css-loader解析成 JS 模块,再把结果交给style-loader注入到页面。
第 8 步:搭建开发服务器(Dev Server + HMR)
每次改代码都要手动 npm run build 太麻烦了。Webpack 提供了开发服务器,自动监听文件变化,热更新页面。
css
npm install webpack-dev-server --save-dev
在 webpack.config.js 末尾添加 devServer 配置:
yaml
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js',
clean: true,
},
module: {
rules: [
{
test: /.css$/,
use: ['style-loader', 'css-loader'],
},
],
},
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html',
}),
],
// 👈 新增:开发服务器配置
devServer: {
port: 3000, // 服务端口
hot: true, // 启用 HMR(热更新)
open: true, // 启动后自动打开浏览器
},
mode: 'development',
}
在 package.json 中添加 dev 脚本:
json
{
"scripts": {
"build": "webpack",
"dev": "webpack serve" // 👈 新增
}
}
启动开发服务器:
arduino
npm run dev
浏览器会自动打开 http://localhost:3000。现在试试:
- 修改
src/index.js中的文字 → 页面自动更新(不刷新页面哦) - 修改
src/styles/main.css中的颜色 → 样式即时更新
这就是 HMR(热模块替换) ------改代码即时生效,开发体验直线上升。
第 9 步:处理图片资源
Webpack 5 内置了资源模块(Asset Modules),不需要额外安装 Loader。在 src/ 下创建 images/ 目录,放一张图片进去,然后在 src/index.js 中使用:
javascript
// src/index.js
import './styles/main.css'
import logo from './images/logo.png' // 👈 引入图片
// 使用图片
const img = document.createElement('img')
img.src = logo
img.style.width = '200px'
document.querySelector('#app').appendChild(img)
在 webpack.config.js 中添加图片处理规则:
javascript
module.exports = {
// ... 前面已有的配置不变
module: {
rules: [
{
test: /.css$/,
use: ['style-loader', 'css-loader'],
},
// 👈 新增:图片处理
{
test: /.(png|jpg|gif|svg)$/,
type: 'asset', // 内置资源模块
parser: {
dataUrlCondition: {
maxSize: 8 * 1024, // 小于 8KB 的图片转成 base64 内联
},
},
},
],
},
// ...
}
第 10 步:区分开发和生产环境
开发环境需要 HMR、source map、读着舒服的错误提示。生产环境需要代码压缩、文件名带 hash(方便缓存)、提取 CSS 为独立文件。
javascript
// webpack.config.js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
// 👈 判断当前环境
const isDev = process.env.NODE_ENV !== 'production'
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
// 开发环境用可读的文件名,生产环境加 contenthash
filename: isDev ? 'bundle.js' : 'bundle.[contenthash:8].js',
clean: true,
},
module: {
rules: [
{
test: /.css$/,
use: ['style-loader', 'css-loader'],
},
{
test: /.(png|jpg|gif|svg)$/,
type: 'asset',
parser: {
dataUrlCondition: { maxSize: 8 * 1024 },
},
},
],
},
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html',
}),
],
devServer: {
port: 3000,
hot: true,
open: true,
},
// 开发环境启用 source map,方便调试
devtool: isDev ? 'eval-source-map' : false,
mode: isDev ? 'development' : 'production',
}
更新 package.json 的脚本:
json
{
"scripts": {
"dev": "webpack serve",
"build": "NODE_ENV=production webpack",
"build:win": "set NODE_ENV=production && webpack" // Windows 用户用这个
}
}
现在:
arduino
npm run dev # 开发模式 --- 启动服务器,HMR,source map
npm run build # 生产模式 --- 代码压缩,contenthash,性能最优
目录结构总览
经过这 10 步,你的完整项目结构:
csharp
my-webpack-app/
├── node_modules/ # 依赖
├── public/
│ └── index.html # HTML 模板
├── src/
│ ├── images/
│ │ └── logo.png # 图片资源
│ ├── styles/
│ │ └── main.css # 样式文件
│ └── index.js # JS 入口
├── dist/ # 构建输出(自动生成)
├── webpack.config.js # Webpack 配置
└── package.json # 项目配置
一个功能完整的 Webpack 项目就搭好了!现在你的项目知识体系:
| 你要做什么 | 对应 Webpack 知识 |
|---|---|
| 告诉 Webpack 从哪开始 | entry(入口) |
| 控制最终输出 | output(输出) |
| 处理非 JS 文件 | Loader(module.rules) |
| 干预构建流程 | Plugin(plugins) |
| 开发时自动刷新 | Dev Server + HMR |
| 开发 vs 生产 | mode + 环境变量 |
| 加速路径引用 | resolve.alias |
| 代码分割 | optimization.splitChunks |
💡 回顾一下:最开始你觉得 Webpack 配置复杂。但跟着这 10 步走过来,你会发现每步只学一个新概念,每一步都能实实在在地看到效果。这就是从零搭建的意义。
三、Vite 核心概念与原理
3.1 Vite 的核心理念:基于原生 ESM 的"No-Bundle"方案
Vite 是法语中"快"的意思------它的核心理念就是 快。
要理解 Vite,先要理解一个关键点:现代浏览器已经原生支持 ES Module(ESM)了。
xml
<!-- 浏览器原生支持这种写法 -->
<script type="module">
import { createApp } from 'vue'
</script>
Webpack 的做法:开发时也要把所有代码打包成一个 bundle,即使你只改了一行 CSS。
Vite 的做法:开发时根本不打包,直接利用浏览器原生 ESM 加载模块。
这带来了一个质的飞跃:
css
Webpack 开发模式:
[所有源代码] → Webpack 打包成一个巨大的 bundle → 浏览器加载并执行
Vite 开发模式:
[浏览器请求 main.js] → Vite Dev Server 实时转换并返回 ESM 模块
[浏览器请求 App.vue] → Vite 将 .vue 编译为 JS 后返回
[浏览器请求 style.css] → Vite 将 CSS 转为 JS 模块后返回
每个模块按需加载,浏览器只转换当前请求的文件。这就是 Vite 快的根本原因。
3.2 依赖预构建:为什么要用 esbuild?
虽然浏览器原生支持 ESM,但有一个实际痛点:node_modules 中的依赖往往不是 ESM 格式。
比如 lodash 是 CommonJS 格式,浏览器不认识 require()。Vite 的思路:
arduino
Vite Dev Server 启动时
↓
用 esbuild(Go 编写,比 JS 快 10-100 倍)扫描 node_modules
↓
将所有依赖预打包为 ESM 格式
↓
存放到 node_modules/.vite/deps/
↓
浏览器请求 'lodash' 时,Vite 直接返回预构建好的 ESM 版本
javascript
// 你代码中这样写
import _ from 'lodash'
// 经过 Vite 预构建,实际请求的是
import __vite__cjsImport0_lodash from "/node_modules/.vite/deps/lodash.js?v=abc123"
预构建的三个收益:
- CommonJS → ESM:让所有依赖统一为 ESM 格式
- 请求合并:lodash 有几百个内部文件,预构建后合并为一个请求,避免 HTTP 瀑布
- 内容 hash 缓存:依赖不变时,浏览器直接使用缓存
3.3 开发服务器的工作原理
当你访问 http://localhost:5173 时:
bash
浏览器请求 /index.html
↓
Vite 返回 index.html(内联了 <script type="module" src="/src/main.js">)
↓
浏览器解析到 <script type="module">,开始请求 /src/main.js
↓
Vite Dev Server 收到请求,进行实时编译:
├── .js/.ts 文件 → esbuild 转译(去掉类型注解等)
├── .vue 文件 → @vitejs/plugin-vue 编译为 JS
├── .css 文件 → 转换为 JS 模块(注入 <style> 或 HMR)
└── .svg/.png → 返回 URL 或 data URI
↓
浏览器接收编译后的 ESM 模块,执行
Vite 对请求的拦截和处理:
javascript
// Vite 内部做的核心工作(示意)
// 当浏览器请求 /src/main.ts 时
// Vite 的 transform 中间件会:
// 1. 读取文件内容
// 2. 抹除 TypeScript 类型注解
// 3. 将裸模块导入('vue')转为浏览器可用的 URL
// 4. 返回编译后的内容
// 你的代码:
import { createApp } from 'vue'
import App from './App.vue'
// 浏览器实际收到:
import { createApp } from '/node_modules/.vite/deps/vue.js?v=abc123'
import App from '/src/App.vue'
3.4 HMR:基于 ESM 的模块热更新
Vite 的 HMR 比 Webpack 快,根本原因是范围不同:
markdown
修改一个文件时:
Webpack:
1. 重新打包这个模块及其依赖链
2. 通过 WebSocket 发送整个更新 chunk
3. 浏览器执行 chunk
Vite:
1. Vite Dev Server 通过 WebSocket 通知浏览器:"xxx 文件变了"
2. 浏览器直接请求这个文件的最新 ESM 模块
3. 浏览器原生切换为新模块(精确到单个文件)
关键区别:Webpack 的 HMR 需要重新打包并传输更新 chunk ,Vite 只需要浏览器重新请求被修改的文件。因为浏览器原生 ESM 可以做到"只更新这一个模块"。
javascript
// Vite 的 HMR API(框架插件已封装,一般无需手写)
if (import.meta.hot) {
import.meta.hot.accept('./render.js', (newModule) => {
// 只替换 render.js 模块,页面不刷新
newModule.render()
})
}
3.5 生产构建:为什么最终还是用 Rollup?
你可能想问:Vite 开发环境那么快,为什么生产构建不用 esbuild,而是用 Rollup?
| 方面 | esbuild | Rollup |
|---|---|---|
| 速度 | 极快(Go 编写) | 中等(JS 编写) |
| 代码分割(Code Splitting) | 有限支持 | 完善支持 |
| CSS 处理 | 基础支持 | 完善(PostCSS 集成) |
| 插件生态 | 小而精 | 丰富成熟 |
| Tree Shaking | 基础 | 深入且可配置 |
| 产物体积优化 | 一般 | 更好(更小的 bundle) |
Vite 的选择策略 :在开发时用 esbuild 追求速度 ,在构建时用 Rollup 追求质量和生态。
markdown
开发环境(dev):esbuild 转译 + 浏览器原生 ESM
↓
生产构建(build):Rollup 打包优化
↓
产出:经过 Tree Shaking、代码分割、压缩的优化代码
3.6 实战:一个完整的 Vite 配置
javascript
// vite.config.js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'
export default defineConfig({
// 插件
plugins: [react()],
// 路径别名
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
},
},
// 开发服务器
server: {
port: 3000,
open: true,
proxy: {
'/api': {
target: 'https://api.example.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^/api/, ''),
},
},
},
// 构建配置
build: {
outDir: 'dist',
sourcemap: false,
chunkSizeWarningLimit: 500,
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
ui: ['antd', '@ant-design/icons'],
},
},
},
},
// CSS 配置
css: {
modules: {
localsConvention: 'camelCase',
},
preprocessorOptions: {
scss: {
additionalData: `@import "@/styles/variables.scss";`,
},
},
},
})
对比 Webpack,Vite 的配置明显更简洁------很多"标配"功能(如 HMR、TypeScript 支持、CSS 处理)都开箱即用,不需要手写 Loader。
四、Webpack vs Vite 核心对比
4.1 开发体验对比
| 维度 | Webpack | Vite |
|---|---|---|
| 启动速度 | 慢(需打包整个应用后才启动 dev server) | 极快(启动 dev server 几乎是瞬时的) |
| HMR 速度 | 随项目增大而变慢 | 恒快(只处理修改的文件) |
| 配置复杂度 | 较复杂(需要明确配置 Loader/Plugin) | 简洁(大部分功能开箱即用) |
| TypeScript 支持 | 需配置 babel-loader / ts-loader | 原生支持(esbuild 转译,无需额外配置) |
| CSS 处理 | 需配置多个 loader | 内置支持(CSS Modules、PostCSS) |
| 调试体验 | source map 配置较复杂 | source map 默认配置良好 |
4.2 生产构建对比
| 维度 | Webpack | Vite (Rollup) |
|---|---|---|
| 产物体积 | 优化成熟,插件丰富 | 通常更小(Rollup 的 Tree Shaking 更彻底) |
| 代码分割 | 支持完善(SplitChunksPlugin) | 支持完善(Rollup 原生 + manualChunks) |
| CSS 提取 | MiniCssExtractPlugin | 内置支持 |
| Tree Shaking | 依赖 sideEffects 配置 | Rollup 更彻底(基于 ESM 静态分析) |
| 构建速度 | 项目越大越慢 | 通常更快 |
| 兼容性 | 自动 polyfill | 需要额外配置 polyfill |
4.3 生态对比
| 维度 | Webpack | Vite |
|---|---|---|
| 插件数量 | 极其丰富(十年积累) | 快速增长中 |
| 社区资源 | 教程、问答最多 | 已很成熟,官方文档优秀 |
| SSR 支持 | Next.js、Gatsby 等基于 Webpack | Nuxt 3、Astro、SvelteKit 等已转向 Vite |
| 微前端 | 方案成熟(Module Federation) | 正在发展中 |
| 企业级项目 | 大量现有项目使用 | 新项目越来越多选择 Vite |
4.4 何时选择哪个?
sql
选 Webpack:
├── 维护现有的大型 Webpack 项目(没有必要迁移)
├── 需要 Module Federation 做微前端
├── 依赖一些 Webpack 独有的插件
└── 团队对 Webpack 生态非常熟悉
选 Vite:
├── 新建项目(强烈推荐)
├── 追求开发体验和构建速度
├── 项目会不断增长,需要可维护性
└── 使用 Vue / React / Svelte 等现代框架
五、常见场景配置示例
场景 1:React + TypeScript 项目
Webpack 配置(需要自行配置 babel):
javascript
// webpack.config.js --- 针对 React + TS
module.exports = {
// ...
module: {
rules: [
{
test: /.(ts|tsx)$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: [
'@babel/preset-env',
['@babel/preset-react', { runtime: 'automatic' }],
'@babel/preset-typescript',
],
},
},
},
],
},
}
Vite 配置(一行插件搞定):
javascript
// vite.config.js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()], // 自动处理 JSX、TS、HMR
})
场景 2:Vue 3 项目
Webpack:
javascript
// webpack.config.js
const { VueLoaderPlugin } = require('vue-loader')
module.exports = {
module: {
rules: [
{
test: /.vue$/,
loader: 'vue-loader',
},
],
},
plugins: [
new VueLoaderPlugin(),
],
}
Vite(Vue 生态首选):
javascript
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
})
场景 3:环境变量与多环境配置
Webpack (使用 webpack.DefinePlugin 或 dotenv):
javascript
// webpack.config.js
const webpack = require('webpack')
require('dotenv').config({ path: `.env.${process.env.NODE_ENV}` })
module.exports = {
plugins: [
new webpack.DefinePlugin({
'process.env.API_URL': JSON.stringify(process.env.API_URL),
'process.env.APP_TITLE': JSON.stringify(process.env.APP_TITLE),
}),
],
}
Vite (内置环境变量支持,import.meta.env):
ini
# .env.development
VITE_API_URL=https://dev-api.example.com
VITE_APP_TITLE=My App (Dev)
# .env.production
VITE_API_URL=https://api.example.com
VITE_APP_TITLE=My App
javascript
// 代码中使用
const apiUrl = import.meta.env.VITE_API_URL
// vite.config.js 中也可读取
import { defineConfig, loadEnv } from 'vite'
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd())
return {
define: {
__APP_TITLE__: JSON.stringify(env.VITE_APP_TITLE),
},
}
})
Vite 约定 :只有
VITE_开头的变量才会暴露给客户端,防止意外泄露敏感信息。
场景 4:代码分割与懒加载
两者语法一致(基于动态 import()),但配置不同:
javascript
// 懒加载组件(Webpack 和 Vite 写法相同)
const Dashboard = () => import('./pages/Dashboard.vue')
const Settings = () => import('./pages/Settings.vue')
Webpack 的代码分割配置:
yaml
// webpack.config.js
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\/]node_modules[\/]/,
name: 'vendor',
chunks: 'all',
},
common: {
minChunks: 2,
minSize: 0,
name: 'common',
},
},
},
},
}
Vite 的代码分割配置:
php
// vite.config.js
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
utils: ['lodash-es', 'dayjs'],
},
},
},
},
})
场景 5:CSS 预处理(Sass/PostCSS)
Webpack:
npm install sass-loader sass postcss-loader autoprefixer -D
java
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /.scss$/,
use: [
'style-loader',
{ loader: 'css-loader', options: { modules: true } },
'postcss-loader',
'sass-loader',
],
},
],
},
}
Vite:
bash
npm install sass -D # 只需安装 sass,Vite 内置支持
php
// vite.config.js(PostCSS 基于 postcss.config.js 自动加载)
export default defineConfig({
css: {
modules: {
localsConvention: 'camelCase',
},
preprocessorOptions: {
scss: {
additionalData: `@import "@/styles/variables.scss";`,
},
},
},
})
css
// postcss.config.js(Vite 自动读取,无需在 vite.config.js 中配置)
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
六、从 Webpack 迁移到 Vite
如果你有一个现有 Webpack 项目,迁移到 Vite 的核心步骤:
6.1 配置映射对照表
| Webpack | Vite 对应 |
|---|---|
webpack.config.js |
vite.config.js |
entry |
约定 index.html 在根目录 |
output |
build.outDir(默认 dist) |
resolve.alias |
resolve.alias |
module.rules → test: /.css$/ |
内置支持 |
module.rules → babel-loader |
插件 @vitejs/plugin-react |
DefinePlugin |
define 配置 |
HtmlWebpackPlugin |
内置(index.html 在根目录) |
MiniCssExtractPlugin |
内置 |
CopyWebpackPlugin |
插件 vite-plugin-static-copy |
ProvidePlugin |
不推荐,用 ESM import 替代 |
optimization.splitChunks |
build.rollupOptions.output.manualChunks |
devServer.proxy |
server.proxy |
public/ 目录 |
public/ 目录(功能相同) |
6.2 常见迁移踩坑点
-
process.env不可用- Webpack 中:
process.env.NODE_ENV - Vite 中:改用
import.meta.env.MODE(或以VITE_开头的环境变量)
- Webpack 中:
-
require不再可用- Vite 在 ESM 模式下不支持
require() - 改为
import动态导入:const module = await import('/path')
- Vite 在 ESM 模式下不支持
-
图片路径处理
- Webpack:
require('./logo.png')或配置 loader - Vite:直接
import logo from './logo.png'(内置支持)
- Webpack:
-
全局样式引入
- Vite 中需要在
main.ts中使用import './styles/global.scss',或在vite.config.js中配置css.preprocessorOptions
- Vite 中需要在
-
动态 import 的变量路径
- Webpack 支持完全变量化的
import(path)(context module) - Vite/Rollup 要求路径至少有一部分是静态的:
import('./pages/' + pageName + '.vue')
- Webpack 支持完全变量化的
javascript
// ❌ 不兼容 Vite
const module = await import(path)
// ✅ 兼容 Vite(路径前缀静态)
const module = await import(`./pages/${pageName}.vue`)
6.3 快速迁移清单
shell
# 1. 安装 Vite 和相关插件
npm install -D vite @vitejs/plugin-react # 或 @vitejs/plugin-vue
# 2. 创建 vite.config.js(参考上文的配置映射)
# 3. 将 index.html 从 public/ 移到项目根目录
# 并在其中添加 <script type="module" src="/src/main.tsx">
# 4. 将 tsconfig.json 中的 "module" 改为 "ESNext"
# 添加 "types": ["vite/client"]
# 5. 替换代码中的 process.env 为 import.meta.env
# 6. 运行测试
npx vite # 开发模式
npx vite build # 生产构建
npx vite preview # 预览构建结果
七、总结与选型建议
7.1 核心观点回顾
arduino
Webpack:
└── 打包 → 启动 Dev Server → 等 bundle 完成 → 浏览器加载
└── "先打包,再服务"
Vite:
└── 启动 Dev Server → 浏览器请求什么就编译什么
└── "先服务,按需编译"
这看似简单的顺序变化,带来了开发体验的质的飞跃。
7.2 给初学者的建议
- 学 Vite 从新项目开始 :用
npm create vite@latest创建项目,体验零配置开发的快感 - 学 Webpack 理解"底层" :Webpack 虽然配置繁琐,但它让你真正理解构建工具的运作机制
- 不必非此即彼:Vite 项目也可能用到 Webpack 的知识(Plugin/Loader 的概念是共通的)
- 遇到问题先查官方文档:Vite 的文档非常优秀,Webpack 的文档虽全但需要耐心
7.3 一句话总结
Webpack 是一个打包器,它把一切打包好再交给浏览器;Vite 是一个翻译器,它在浏览器需要的时候才翻译。两者目标相同------让前端开发更高效------但走了完全不同的路。