为什么需要前端工程化
十年前写前端,一个 HTML 文件加一个 CSS 文件加一个 JS 文件就是全部。现在的前端项目动辄几百个组件、几十个第三方依赖、TypeScript 要编译、Less/Sass 要预处理、图片要压缩、代码要打包。这些事情手动做不完,也不该手动做——这就是前端工程化要解决的问题:把重复的构建、打包、优化工作交给工具,开发者只写业务代码。
一个现代前端项目的工具链通常包括:包管理器(npm/pnpm)、构建工具(Vite/Webpack)、语言转译(TypeScript/Babel)、代码规范(ESLint/Prettier)、测试(Vitest)。这些工具互相配合,让"写代码"和"上线"之间的距离尽量短。
工程化的目标不是堆工具,而是让团队里任何人 clone 下项目、装依赖、跑一条命令,就能在本地复现和线上一致的开发环境。
- Vite基于浏览器原生 ESM 的开发服务器,冷启动毫秒级。
- 分包把第三方依赖和业务代码拆成不同 chunk,利用浏览器缓存。
- Tree Shaking自动剔除没被用到的导出,减小产物体积。
- 缓存策略文件名带内容 hash,配合长缓存实现增量更新。
Vite 为什么比 Webpack 快
用过老 Webpack 项目的人都知道,项目一大,启动要等几十秒甚至几分钟,改一行代码热更新也要等好几秒。Vite 解决这个问题的思路完全不同:开发环境下不打包,而是让浏览器自己去加载模块。
原理是这样的:现代浏览器都原生支持 ES Module,浏览器请求哪个文件,Vite 就实时编译哪个文件返回。项目再大,启动时间也只是"把服务器起来"那一瞬间——因为它不需要先把所有模块打成一个包。热更新也一样:改了哪个文件,浏览器只重新请求那一个文件,速度和文件大小成正比,和项目整体大小无关。
| 维度 | Webpack(传统) | Vite(现代) |
|---|---|---|
| 启动方式 | 先打包整个项目再启动 | 起服务器,按需编译 |
| 冷启动 | 大项目几十秒 | 毫秒级 |
| 热更新 | 重新打包改动模块及依赖 | 浏览器直接请求改的文件 |
| 生产构建 | Webpack 本身 | 底层用 Rollup |
| 配置复杂度 | plugin / loader 概念多 | 开箱即用,约定优于配置 |
生产环境 Vite 仍然要把代码打包成浏览器能高效加载的静态文件,这一步底层用的是 Rollup——一个以产物体积小著称的打包器。所以 Vite 是"开发用 ESM 极速体验,生产用 Rollup 最优产物"的组合。
从零初始化一个 Vite 项目
官方推荐用 npm create 命令初始化,选对应模板即可:
# 初始化一个 Vue 项目(React / Vanilla 同理换模板名)
npm create vite@latest my-app -- --template vue
cd my-app
npm install
npm run dev # 启动开发服务器,默认 http://localhost:5173
项目结构非常简洁:index.html 是入口(注意 Vite 的入口在根目录而不是 public),src/ 放所有源代码,vite.config.js 是配置文件。和老工具最大的区别是:不用写一堆 loader 配置,开箱就支持 TypeScript、JSX、CSS 预处理——需要什么装什么插件,不需要就在配置里加一行。
// vite.config.js —— 一个最小配置
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
server: {
port: 5173,
open: true, // 启动自动开浏览器
},
})
路径别名、环境变量与代理配置
项目写深了,../../components/Button 这种相对路径会让人崩溃。Vite 内置了路径别名,配合 TypeScript 配置就能用 @/components/Button:
// vite.config.js
import { defineConfig } from 'vite'
import { fileURLToPath } from 'node:url'
export default defineConfig({
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
server: {
proxy: {
// 开发环境把 /api 开头的请求转发到后端
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
},
},
},
})
环境变量放在项目根目录的 .env 文件里,以 VITE_ 开头的变量会被注入到前端代码中,通过 import.meta.env.VITE_XXX 读取。分环境用 .env.development 和 .env.production,开发和构建时自动加载对应的文件。
# .env.development
VITE_API_BASE=/api
# .env.production
VITE_API_BASE=https://api.example.com
// 代码里使用
const base = import.meta.env.VITE_API_BASE
构建产物分包与 Tree Shaking
npm run build 之后,产物默认放在 dist/ 目录。打开一看,往往有一个几百 KB 的 js 文件——这是因为所有第三方依赖和业务代码被打到一起了。合理的做法是手动分包:把不常变的第三方库拆成独立 chunk,用户下次访问时浏览器直接用缓存。
// vite.config.js
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
'vue-vendor': ['vue', 'vue-router', 'pinia'],
'utils-vendor': ['lodash-es', 'axios'],
},
},
},
},
})
这样打包后会生成 vue-vendor.abc123.js、utils-vendor.def456.js 等独立文件。业务代码更新时,只要依赖版本没变,这些 vendor chunk 的 hash 就不变,浏览器直接用本地缓存——这是首屏加载速度提升最明显的一个优化。
Tree Shaking 是默认开启的:如果你的工具库用 ES Module 导出(比如 lodash-es 而不是 lodash),构建时会自动把没被 import 的函数从产物里剔除。举个例子,import 了 lodash-es 的 debounce,最终产物里只会有 debounce 相关的代码,不会把整个 lodash 打进来。
体积压缩、缓存与 gzip
Vite 生产构建默认就做了 ES 压缩(esbuild 压缩比 Terser 快很多),但有几件事仍然值得手动配:
- 文件名带内容 hash:Vite 默认就会给 js/css 文件名加 hash(如
index-abc123.js),内容变了 hash 才会变。配合服务器的长缓存头,实现"不更新就用缓存,更新就自动拿新的"。 - 图片压缩:把大图片转成 WebP 格式,或者用 vite-plugin-imagemin 在构建时自动压缩。一张几 MB 的背景图压缩后可能只剩几百 KB。
- 按需引入 UI 库:组件库全量引入会把整个库打进来,用按需引入插件只打包用到的组件。
- 分析包体积:用 rollup-plugin-visualizer 生成一个交互式的打包分析图,一眼看出哪个库占了最大体积,值不值得替换。
# 构建前先看看产物构成
npm run build -- --report
# 产物目录大概长这样
dist/
index.html
assets/
index-8f3a2b.js # 业务代码,带 hash
vue-vendor-1c9d4e.js # 第三方依赖,带 hash
index-2e7b8d.css # 样式,带 hash
部署时,服务器(Nginx 或 CDN)需要配两条缓存规则:带 hash 的静态资源(assets/ 下的 js/css/图片)设为强缓存一年,因为内容一变文件名就变;index.html 设为不缓存或短缓存,因为它是入口,必须每次拿最新版,才能找到新版本的 hash 文件名。这两条配错,会出现"用户拿到了新 html 但还引用着旧 js"的白屏事故。
一套上线前的检查清单
项目要上线了,按这份清单过一遍,能避免大部分线上事故:
- 本地 build 一遍:
npm run build跑通无报错,dist 目录生成。 - 预览产物:
npm run preview在本地起一个静态服务器预览 build 后的产物,确认和开发环境表现一致。 - 检查首屏体积:首屏加载的 js 总量建议控制在 200KB 以内(gzip 后),超过就要考虑分包和路由懒加载。
- 路由懒加载:用动态 import() 把每个路由组件拆成独立 chunk,首屏只加载首页的代码。
- 环境变量对不对:确认 .env.production 里的 API 地址是线上地址,不是 localhost。
- base 路径对不对:如果项目不是部署在域名根目录(比如放在 /blog/ 下),vite.config.js 里要配
base: '/blog/',否则资源会 404。 - 浏览器兼容性:需要兼容旧浏览器时,配 @vitejs/plugin-legacy 生成一份带 polyfill 的旧版产物。
// 路由懒加载示例:首屏不加载其他页面
const UserProfile = () => import('./views/UserProfile.vue')
const routes = [
{ path: '/user', component: UserProfile },
]
前端工程化的工具链一直在变,但核心问题从来没变:怎么让开发者写得快、让用户加载得快。Vite 解决了前者,分包、压缩、缓存解决了后者。把这两头都顾到,工程化这关就算过了。
工具会过时,但"为什么要这么配"的理解不会。每加一个插件、每调一个配置,都问一句"它解决了什么问题",这样换工具的时候你才不会束手无策。