建
第 6 章 前端工程化:从写代码到上生产
Build, Lint, Format, Test, CI/CD
一个真实项目不是写几个 HTML 文件。工程化就是把"写 → 构建 → 检查 → 测试 → 部署"这条流水线自动化。后端同学对 Maven/Gradle 不陌生,前端对应的就是 Vite + ESLint + pnpm + CI。
包管理器:npm / pnpm / yarn
| 工具 | 特点 |
| npm | Node 自带,生态最大。慢、磁盘重复多。 |
| pnpm | 硬链接 + 内容寻址,装得快、省空间。当前推荐。 |
| yarn | 早期比 npm 快,现在差别不大。 |
包版本用语义化版本 semver:主版本.次版本.补丁,如 1.4.2。^1.4.2 允许 minor/patch 升级,~1.4.2 只允许 patch。锁文件(pnpm-lock.yaml / package-lock.json)必须提交 Git。
构建工具:Vite 是当前首选
| 工具 | 定位 |
| Vite 6+ | 新一代构建工具。开发时浏览器原生 ESM,不打包,秒启动;生产时用 Rollup 打包。当前新项目首选。 |
| Webpack 5 | 老牌打包器,生态全,配置复杂。老项目维护用。 |
| Rollup | 打库用(组件库、工具库),输出干净。 |
| Turbopack | Vercel 用 Rust 写的 Webpack 继任者,还在演进。 |
30 秒起一个 Vite + React + TS 项目
# 初始化项目
pnpm create vite@latest my-app -- --template react-ts
# 装依赖、跑起来
cd my-app
pnpm install
pnpm dev # 打开 http://localhost:5173
pnpm build # 产出 dist/ 目录,直接扔服务器
vite.config.ts 常用配置
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import path from "node:path";
export default defineConfig({
plugins: [react()],
resolve: {
alias: { "@": path.resolve(__dirname, "src") }, // 路径别名
},
server: {
proxy: {
// 开发时 /api 转发到后端,解决跨域
"/api": "http://localhost:8080",
},
},
});
代码规范:ESLint + Prettier + Husky
范为什么要强制规范
多人协作最大的内耗不是技术,是缩进几个空格、单引号还是双引号、分号写不写。工具替你吵完:
ESLint:找潜在错误(未使用变量、== 用错、可疑比较)。Prettier:只负责格式化,风格统一。两者用 eslint-config-prettier 关掉冲突规则。
Husky + lint-staged:Git 提交前自动跑 eslint --fix 和 prettier --write,不通过就不让提交。
commitlint:规定提交信息格式 feat: 新功能 / fix: 修 bug,自动生成 changelog。
测试:Vitest + Testing Library + Playwright
// sum.test.ts —— 单元测试
import { describe, it, expect } from "vitest";
import { sum } from "./sum";
describe("sum", () => {
it("1+1=2", () => {
expect(sum(1, 1)).toBe(2);
});
});
Vitest 是 Vite 生态的测试框架,比 Jest 快。@testing-library/react 用来测组件(模拟用户点击)。Playwright 做端到端测试,真开浏览器点一遍。
项目结构:按功能组织
src/
components/ # 通用组件
pages/ # 页面级组件
hooks/ # 自定义 Hook
services/ # API 请求层
stores/ # 全局状态
utils/ # 纯工具函数
types/ # 全局 TS 类型
assets/ # 图片、字体
styles/ # 全局样式
App.tsx
main.tsx
工程化深化:包管理、Vite、Lint、测试、CI 全链路
工程化就是让"写代码 → 跑起来 → 上线"这条路全自动。后端同学熟悉 Maven/Gradle,前端这套是同一个思路,只是工具换成了 npm/pnpm + Vite + GitHub Actions。
package.json 字段全解与 semver
{
"name": "my-app",
"version": "1.0.0",
"type": "module", // ESM
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"test": "vitest"
},
"dependencies": { "react": "^19.0.0" }, // 运行时依赖
"devDependencies": { "vite": "^6.0.0" }, // 只在开发时用
"peerDependencies": { "react": ">=18" }, // 库:宿主必须提供
"optionalDependencies": {}, // 装失败也不报错
"engines": { "node": ">=20.19.0" }, // 要求 Node 版本
"browserslist": [ "> 0.5%", "last 2 versions", "not dead" ]
}
/* semver:主.次.补丁
^1.4.2 → 允许次/补丁升级(1.x);~1.4.2 → 只允许补丁;* → 任意 */
- npm ci:严格按 lock 文件装,CI 用它,保证人人装出一样的依赖树。
- npm audit:扫已知安全漏洞;
npm publish 发自己的包。
- npmmirror:淘宝镜像,国内
pnpm config set registry https://registry.npmmirror.com。
pnpm:为什么现在都推荐它
# pnpm 命令速查
pnpm install # 装全部
pnpm add react # 加 dependencies
pnpm add -D vite # 加 devDependencies
pnpm remove react # 卸载
pnpm up # 升级
pnpm dlx create-vite # 临时执行,等价 npx
# 原理:全局 store 存一份真实文件,项目里用硬链接/符号链接指过去
# → 装得快、省磁盘、幽灵依赖(没声明却能 import)被严格禁止
# monorepo:pnpm-workspace.yaml 定义 packages/*,跨包共享
Vite 深入:为什么快
快Vite 的快从哪来
开发时不打包:浏览器原生 ESM 按需请求,你打开哪个模块才编译哪个,项目再大启动也是毫秒级。
esbuild 预构建依赖:用 Go 写的 esbuild 把 node_modules 里的依赖打包一次(比 JS 写的 Webpack 快 10-100 倍),合并请求。
HMR 基于 ESM:模块改了只替换那一个模块,不刷新整页,状态还在。
生产时用 Rollup 打包:开发快不等于生产糙,生产照样做 tree-shake、压缩、分包。
vite.config.ts 完整常用配置
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import path from "node:path";
export default defineConfig({
plugins: [react()],
resolve: { alias: { "@": path.resolve(__dirname, "src") } },
define: { __APP_VERSION__: JSON.stringify("1.0.0") },
server: {
port: 5173,
proxy: { "/api": "http://localhost:8080" },
},
build: {
target: "es2020",
minify: "esbuild", // 比 terser 快
chunkSizeWarningLimit: 800,
rollupOptions: {
output: {
// 把 react 等大依赖单独打一个 chunk,利用浏览器缓存
manualChunks: { react: ["react", "react-dom"] },
},
},
},
optimizeDeps: { include: ["lodash-es"] },
});
环境变量:import.meta.env.MODE / .env / .env.development / .env.production,只有 VITE_ 前缀的变量才暴露给前端(防密钥泄漏)。
ESLint + Prettier + Husky
// eslint.config.js(Flat Config,新版默认)
import js from "@eslint/js";
import tseslint from "typescript-eslint";
export default defineConfig([
js.configs.recommended,
...tseslint.configs.recommended,
{
rules: {
"no-unused-vars": "warn", // off 0 / warn 1 / error 2
"eqeqeq": ["error", "always"], // 强制 ===
},
},
]);
/* .prettierrc:printWidth 100 / singleQuote true / semi false / trailingComma es5 */
/* 配合:eslint-config-prettier 关掉和 Prettier 冲突的规则 */
/* husky + lint-staged:git 提交前只对暂存文件跑 eslint --fix */
测试:Vitest 与 React Testing Library
// sum.test.ts
import { describe, it, expect, vi } from "vitest";
import { render, screen, fireEvent } from "@testing-library/react";
import "@testing-library/jest-dom/vitest";
import Counter from "./Counter";
describe("Counter", () => {
it("点击后数字加一", async () => {
render(<Counter />);
fireEvent.click(screen.getByRole("button", { name: /加一/ }));
await screen.findByText("当前:1");
// 按角色/文本查元素,不查 DOM 实现细节,UI 重构测试不挂
});
});
/* vitest.config:test.environment = 'jsdom' 模拟浏览器;setupFiles 引 jest-dom */
/* E2E:Playwright/Cypress 真开浏览器跑用户流程,和单测互补 */
CI/CD:GitHub Actions
.github/workflows/ci.yml —— 推代码自动 lint/test/build
name: CI
on: { push: { branches: [main] }, pull_request: {} }
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 22, cache: "pnpm" }
- run: pnpm install --frozen-lockfile
- run: pnpm lint
- run: pnpm test
- run: pnpm build
# 构建产物可自动部署到 Vercel / Netlify / GitHub Pages
完整案例:30 秒起一个规范项目
pnpm create vite@latest my-app -- --template react-ts
cd my-app && pnpm install
# 加规范与测试
pnpm add -D eslint @eslint/js typescript-eslint prettier eslint-config-prettier
pnpm add -D vitest @testing-library/react @testing-library/jest-dom jsdom
# Git 钩子
pnpm dlx husky init
# echo "pnpm lint && pnpm test" > .husky/pre-commit
pnpm dev
本章面试题(工程化)
1. npm 和 pnpm 区别?为什么 pnpm 不会有"幽灵依赖"?
看答案
npm 是每个包各自复制一份(node_modules 嵌套),依赖提升导致你没声明的包也能 import(幽灵依赖)。pnpm 用全局 store + 硬链接,项目里只平铺你直接依赖的包,间接依赖挂在 .pnpm 下,没声明就 import 不到,强制诚实。
2. Vite 为什么冷启动比 Webpack 快?
看答案
Webpack 启动要把整个依赖图打一个 bundle 才开始服务;Vite 开发时利用浏览器原生 ESM,只编译你当前访问的那一个模块,依赖用 esbuild 预构建一次,启动时间和项目大小几乎无关。
3. ESLint 和 Prettier 分工?为什么要两个?
看答案
ESLint 找"潜在 bug 和坏味道"(没用的变量、== 用错);Prettier 只负责"格式"(缩进、引号、分号)。两者偶尔规则打架,用 eslint-config-prettier 关掉 ESLint 里的格式规则,让 ESLint 管逻辑、Prettier 管排版。
4. 环境变量为什么要 VITE_ 前缀?
看答案
Vite 为防止把服务器密钥打进前端包,只有带 VITE_ 前缀的 .env 变量才会被替换进客户端代码。NODE_ENV、BASE_URL 是内置的。
5. monorepo 是什么?pnpm workspace 解决什么?
看答案
monorepo 把多个相关包/应用放在一个仓库(如一个 UI 库 + 两个用它的应用)。pnpm workspace 让包之间用 link 互相引用,改一处、所有用它的包立刻生效,版本统一发布,避免 publish/反复装。