楼层: 首页/ 软件技术/ 前端基础/ 第 6 章 前端工程化:从写代码到上生产
建

第 6 章 前端工程化:从写代码到上生产

Build, Lint, Format, Test, CI/CD

一个真实项目不是写几个 HTML 文件。工程化就是把"写 → 构建 → 检查 → 测试 → 部署"这条流水线自动化。后端同学对 Maven/Gradle 不陌生,前端对应的就是 Vite + ESLint + pnpm + CI。

包管理器:npm / pnpm / yarn

工具特点
npmNode 自带,生态最大。慢、磁盘重复多。
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打库用(组件库、工具库),输出干净。
TurbopackVercel 用 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/反复装。