01 - 构建工具与 Vite 基础
一、构建工具优点
前端源码通常包含现代语法、模块导入、样式与静态资源引用。 而且就本科时编写的直接引入css或js的方法,在项目中也容易出现各种作用域污染: 比如:
<!-- 页面直接挂多个脚本-->
<script src="./config.js"></script>
<script src="./request.js"></script>
<script src="./page.js"></script>// config.js
// 全局挂载,任何脚本都可改写
window.appConfig = { apiBase: '/api/v1' }// request.js
// 依赖全局变量,且有命名冲突风险
window.request = function request(path) {
return fetch(window.appConfig.apiBase + path)
}// page.js
// 另一个脚本若覆盖了 window.request,这里会直接受影响
window.request('/users')同一场景改成模块化后,依赖关系会更清晰。
// src/config.ts
// 显式导出,避免全局变量污染
export const apiBase = '/api/v1'// src/request.ts
import { apiBase } from './config'
// 参数 path 表示接口路径,返回 Promise<Response>
export function request(path: string) {
return fetch(apiBase + path)
}// src/page.ts
import { request } from './request'
// 依赖边在 import 里可见,构建工具可静态分析
request('/users')现代框架构建流程不仅提供了ts编译等方式,也提供了更多资源管理的途径。
构建工具负责的核心环节:
- 依赖图解析
- 语法转换
- 样式处理
- 资源处理
- 开发服务与热更新
- 生产产物输出
实际开发里的一个现象:
- 本地开发正常
- 线上子路径部署后资源 404
- 经常是构建 base 路径与部署目录不一致
这类的原因通常不在业务代码本身,而在资源定位规则。 本地开发阶段由开发服务器兜底,资源请求路径在根路径下看起来都能访问。 一旦部署到子路径,入口 js、css、图片的引用地址就必须统一带上前缀。 只要其中一类资源路径没有按同一规则生成,404 就会出现。
也因此,构建工具的价值不只是把代码打包。
- 构建阶段统一生成资源引用路径,减少拼接路径遗漏
- 把产物文件名做哈希,配合缓存策略降低旧资源误命中
- 输出稳定目录结构,便于静态服务和反向代理做一致映射
- 通过配置固定
base规则,让开发环境与生产环境的路径语义保持一致 - 环境变量集中管理,避免把测试地址写进生产包
- 支持按路由或模块拆分代码,减少首屏不必要加载
- 支持
sourcemap产物,线上报错可回溯到源码位置 - 支持统一插件链,图片压缩、样式前缀、语法降级可集中处理
- 支持构建产物分析,便于追踪体积变化来源
比如:环境隔离与路径注入。
import { defineConfig, loadEnv } from 'vite'
export default defineConfig(({ mode }) => {
// 根据 mode 读取 .env.development 或 .env.production
const env = loadEnv(mode, process.cwd(), '')
return {
// 子路径部署时用环境变量注入,避免手写固定值
base: env.APP_BASE || '/',
define: {
// 在构建阶段注入版本号,便于排查线上是否命中新包
__APP_VERSION__: JSON.stringify(env.APP_VERSION || '0.0.0')
}
}
})二、模块化与 ESM 基础
1. 为什么要模块化
模块化的目标是让代码拆分、复用、依赖管理更稳定。 一个业务模块需要明确输入与输出,避免全局变量污染,示例可见上文。
2. ESM 的核心
ESM 是浏览器和现代运行环境支持的模块系统。 关键字是 import 与 export。
不过微信小程序与 Node.js 里,require 的出现频率也不低,有说法是太多旧代码需要兼容。
对比一下的话,import:
- ESM 静态依赖更利于构建阶段分析
- tree shaking 更容易生效
- 动态 import 更容易做路由级懒加载
// CommonJS 写法
const dayjs = require('dayjs')
function formatTime(value) {
return dayjs(value).format('YYYY-MM-DD HH:mm:ss')
}
module.exports = { formatTime }// ESM 写法
import dayjs from 'dayjs'
export function formatTime(value) {
return dayjs(value).format('YYYY-MM-DD HH:mm:ss')
}// 静态导入,在编译阶段即可分析依赖边
import { createApp } from 'vue'
// 命名导出,便于按需导入
export const apiBase = '/api'
// 动态导入,常用于路由级别懒加载
const view = () => import('./views/Home.vue')ESM 两个特性:
- 依赖关系静态可分析
- 支持按模块粒度加载
3. ESM 与 CommonJS 的区别
常见差异:
声明时机 ESM 通过 import 在语法层声明依赖,构建工具可提前拿到依赖图。 CommonJS 通过 require 在执行阶段加载依赖,依赖关系更多依赖运行路径。
构建优化 ESM 更容易触发 tree shaking,未引用代码更容易被剔除。 不过其实 CommonJS 也能优化,但优化深度依赖额外转换链路。
代码拆分 ESM 支持动态 import,路由级懒加载。 CommonJS 场景下虽然能拆分,不过配置复杂度通常更高。
// CommonJS 片段
// 运行阶段按条件加载
const role = process.env.ROLE
const page = role === 'admin'
? require('./pages/admin')
: require('./pages/user')
module.exports = page// ESM 片段
// 动态 import
const role = import.meta.env.VITE_ROLE
const page = role === 'admin'
? () => import('./pages/admin.js')
: () => import('./pages/user.js')
export default page- 首屏仅请求主路由与必要公共包
- 次级路由代码在访问时拉取
- 包体积与首屏耗时更容易压下来
三、Vite 的最小可用配置
1. 初始化与基础命令
npm create vite@latest [项目名]
# 运行后,提示多种配置信息,勾选操作即可
cd [项目名]
npm install
npm run dev
npm run build
npm run preview另外一点,有两个类似的命令:npm create vite@latest 与 npm create vue@latest
npm create vite@latest定位偏通用,支持多框架模板,包含vanilla、vue、react、svelte。npm create vue@latest定位偏 Vue 官方项目模板,底层构建链路依赖 Vite,同时附带 Vue 生态。npm create vite@latest优点:模板覆盖面更广,跨框架更方便。 缺点:Vue 配套能力需后续手动接入,路由、状态管理、规范工具多一步配置。npm create vue@latest优点:创建阶段可勾选vue-router、pinia、eslint、vitest、cypress,比较一步到位一些。 短板:单面向 Vue 生态。
2. Vite创建后目录结构
my-app
index.html
src
main.ts
public
vite.config.ts- index.html 主入口
- src 放业务源码
- public 放无需构建处理的静态文件
- vite.config.ts 放项目级构建与开发配置
3. 常用配置项
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'node:path'
export default defineConfig({
// 插件链,按顺序参与源码转换
plugins: [vue()],
server: {
// 允许局域网设备访问,联调移动端时常用
host: '0.0.0.0',
// 开发端口
port: 5173,
// 启动后自动打开浏览器
open: true
},
resolve: {
alias: {
// 路径别名,避免 ../../ 多层相对路径
'@': path.resolve(__dirname, 'src')
}
},
// 依赖预构建配置,优化冷启动和依赖解析
optimizeDeps: {
// 显式预构建,适合体积大且多页面共用的依赖
include: ['lodash-es']
},
build: {
// 构建输出目录
outDir: 'dist',
// 产出 source map,便于错误平台定位源码
sourcemap: true,
// 静态资源目录
assetsDir: 'assets',
rollupOptions: {
output: {
// 分包示例,便于长期缓存稳定命中
manualChunks: {
vue: ['vue', 'vue-router'],
utils: ['lodash-es']
}
}
}
}
})配置说明。
- server 控制本地开发服务
- resolve.alias 统一路径别名,减少相对路径混乱
- optimizeDeps 对冷启动与依赖解析有明显影响
- build.outDir 指定构建输出目录
- build.assetsDir 指定静态资源子目录
- build.sourcemap 便于线上问题定位
- manualChunks 常用于公共包拆分与缓存稳定
4. 实际开发中的基础配置案例
后端接口通过网关提供,前端本地联调常见跨域。 开发阶段通常通过代理转发解决。
server: {
host: '0.0.0.0',
port: 5173,
proxy: {
'/api': {
// 本地访问 /api 开头路径时转发到目标服务
target: 'http://localhost:8080',
// 允许改变请求头中的 host
changeOrigin: true,
// 可选重写,按后端路由规则决定是否开启
// rewrite: path => path.replace(/^\/api/, '')
}
}
}这个配置在前后端分离项目中非常高频。
四、Vite 开发阶段执行流程
开发阶段最核心的感受是快。 快来自按需处理,而不是一次性全量打包。
1. 首次启动
- 启动本地开发服务
- 扫描依赖并执行预构建
- 浏览器请求页面入口
- 浏览器按 ESM 规则继续请求依赖模块
常见观察点。
- 首次启动日志里可看到依赖预构建
- 第二次启动通常更快,缓存命中后预构建成本下降
2. 模块请求与按需转换
Vite 不会在开发阶段先打完整包。 浏览器请求到哪个模块,Vite 就转换哪个模块。
这带来两个直接收益。
- 启动时间短
- 修改单文件后,失效范围更小
典型案例。
- 页面拆分为 40 个模块
- 修改其中一个表格组件
- 热更新只影响相关模块,页面状态通常可保留
3. 热更新路径
修改某个业务模块后,Vite 会。
- 识别变更文件
- 重新转换该文件及必要依赖
- 通过热更新协议通知浏览器
- 浏览器只替换受影响模块
这个过程决定了日常迭代时的反馈速度。
五、Vite 构建阶段执行流程
构建阶段目标是输出可部署产物。
1. 核心步骤
- 读取入口与依赖图
- 执行插件转换流程
- 做代码分割与产物生成
- 输出到 dist 目录
构建阶段和开发阶段的边界。
- 开发阶段强调实时响应
- 构建阶段强调可部署产物与缓存策略
2. 产物去向与命名
默认会在 dist 下看到这类结构。
dist
index.html
assets
index.xxxxx.js
index.xxxxx.css
vendor.xxxxx.js关键点。
- 资源文件通常带哈希,便于强缓存
- 业务代码与公共依赖会按策略切分
- index.html 会引用最终产物路径
3. 与部署协作的关注点
- 服务器要正确托管 dist 目录
- 需要为静态资源配置长期缓存
- 入口 html 通常使用短缓存,保证新版本可达
- sourcemap 上传策略要与监控系统一致
经典线上案例。
- 构建产物路径为 /assets
- 站点部署在 /admin 子路径
- 未设置 base 时浏览器会从根路径取资源,最终触发 404
对应配置通常是。
export default defineConfig({
// 子路径部署时常见配置
base: '/admin/'
})六、Vite 为何比 Webpack 更快
这个问题在面试与工作里都很常见。 可以按开发阶段和构建阶段分开理解。
1. 开发阶段速度优势
核心原因是基于原生 ESM 的按需加载。
对比思路。
- Webpack 开发阶段通常先做整图打包
- Vite 开发阶段以浏览器原生 ESM 请求为主
- Vite 只转换当前请求模块,不先打全量包
所以项目越大,冷启动与更新速度差距越明显。
一个常见联想。
- 当项目依赖图很大时
- 全量打包的初始成本持续上升
- 按需转换方案在开发阶段优势更明显
2. 依赖预构建策略
Vite 会把第三方依赖先做一次预构建。 预构建目标是把复杂依赖转为更适合浏览器快速处理的模块格式。
收益。
- 减少浏览器多层依赖解析开销
- 避免重复转换第三方包
- 提升后续页面加载稳定性
实际场景。
- 组件库和图表库都较重
- 首次启动后缓存预构建结果
- 后续迭代时依赖层转换成本明显下降
3. 构建阶段说明
Vite 构建阶段依然会做完整打包流程。 所以构建速度优势主要体现在开发阶段。
结论要准确。
- Vite 快的关键点不是完全不打包
- Vite 快的关键点是开发阶段利用原生 ESM 按需处理
- 生产构建仍然需要完整产物生成
七、工程化记录点
这部分更像复盘清单。 用于上线前后快速对照。
1. 配置层
- 别名统一在配置层维护
- 环境变量按开发、测试、生产分层
- base 路径与部署目录保持一致
2. 代码组织
- 业务模块按域拆分
- 公共工具与业务逻辑分层
- 静态资源按缓存策略划分目录
3. 发布链路
- 构建前做类型检查与代码检查
- 构建后做体积对比与关键路由冒烟
- 发布后观察错误率与资源命中率
八、常见问题排查清单
1. 本地启动慢
- 检查依赖数量与历史冗余包
- 检查是否误把大文件放入实时转换链路
- 清理缓存后复测启动耗时
- 检查 optimizeDeps 是否覆盖核心重依赖
2. 热更新不生效
- 检查文件是否在监听范围
- 检查插件顺序是否影响模块转换
- 检查是否存在全局副作用导致页面强刷
- 检查是否误用了无法被热更新边界接管的写法
3. 线上资源 404
- 检查构建 base 路径配置
- 检查静态资源托管目录映射
- 检查发布系统是否漏传 assets 目录
- 检查反向代理是否改写了静态资源路径
九、学习路径记录
第一阶段。
- 跑通最小 Vite 项目
- 看懂入口、依赖、产物目录
- 完成本地构建与预览
第二阶段。
- 接入别名、代理、环境变量
- 观察修改代码后的热更新影响范围
- 比对构建前后资源体积变化
第三阶段。
- 接入规范脚本与检查流程
- 完成一次完整发布演练
- 能定位一类构建链路问题
