本章关注从"个人能写"到"团队能长期交付"的能力:TypeScript、测试、构建、路由、样式、组件库、可访问性、安全、迁移和生态选型。
1. 工程化目标
工程化不是堆工具,而是建立约束:
- 错误尽早暴露。
- 代码风格统一。
- 发布过程可重复。
- 线上问题可定位。
- 性能和质量不悄悄退化。
2. Vite
json
{
"scripts": {
"dev": "vite --host 127.0.0.1",
"build": "vite build",
"preview": "vite preview --host 127.0.0.1"
}
}
常用命令:
bash
npm install
npm run dev
npm run build
npm run preview
Vite 开发体验好,适合现代 React 项目。生产构建底层使用 Rollup。
3. TypeScript 领域建模
ts
type Level = '入门' | '进阶' | '高级' | '精通' | '专家';
type KnowledgeItem = {
id: string;
title: string;
level: Level;
summary: string;
tags: string[];
stage: number;
};
Props:
tsx
type KnowledgeCardProps = {
item: KnowledgeItem;
favorite: boolean;
onFavorite: (id: string) => void;
};
function KnowledgeCard({ item, favorite, onFavorite }: KnowledgeCardProps) {
return (
<article>
<h3>{item.title}</h3>
<button onClick={() => onFavorite(item.id)}>
{favorite ? '已收藏' : '收藏'}
</button>
</article>
);
}
Action:
ts
type KnowledgeAction =
| { type: 'query-changed'; query: string }
| { type: 'level-changed'; level: Level | '全部' }
| { type: 'favorite-toggled'; id: string }
| { type: 'completed-toggled'; id: string }
| { type: 'reset' };
穷尽检查:
ts
function assertNever(value: never): never {
throw new Error(`Unhandled action: ${JSON.stringify(value)}`);
}
4. API 类型边界
不要盲信后端返回。
ts
async function getKnowledgeItems(): Promise<KnowledgeItem[]> {
const response = await fetch('/api/knowledge-items');
const data: unknown = await response.json();
return parseKnowledgeItems(data);
}
真实项目可以使用 Zod:
ts
const KnowledgeItemSchema = z.object({
id: z.string(),
title: z.string(),
level: z.enum(['入门', '进阶', '高级', '精通', '专家']),
tags: z.array(z.string()),
});
5. ESLint、Prettier 与 Hook 规则
Hook 规则必须由 lint 保护。
错误:
jsx
if (enabled) {
useEffect(() => {}, []);
}
正确:
jsx
useEffect(() => {
if (!enabled) return;
}, [enabled]);
React Compiler 时代 lint 更重要:
- rules-of-hooks。
- exhaustive-deps。
- purity。
- immutability。
- preserve-manual-memoization。
6. 测试金字塔
text
单元测试:领域函数、Reducer、工具函数
组件测试:用户交互、表单、状态变化
集成测试:页面与数据流
E2E:关键业务路径
Reducer 测试:
js
test('toggles favorite', () => {
const state = {
query: '',
level: '全部',
favorites: [],
completed: [],
};
const next = reducer(state, {
type: 'favorite-toggled',
id: 'components',
});
expect(next.favorites).toEqual(['components']);
});
组件测试:
jsx
render(<SearchBox value="" onChange={handleChange} />);
await user.type(screen.getByRole('textbox'), 'Hooks');
expect(handleChange).toHaveBeenCalledWith('Hooks');
E2E:
ts
test('user completes a lesson', async ({ page }) => {
await page.goto('/knowledge');
await page.getByPlaceholder('搜索知识点').fill('组件');
await page.getByRole('button', { name: '切换完成' }).click();
await expect(page.getByText('学习进度')).toBeVisible();
});
7. CI/CD
yaml
name: web
on:
pull_request:
push:
branches: [main]
jobs:
verify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm run lint
- run: npm run typecheck
- run: npm run test
- run: npm run build
8. 环境变量
env
VITE_API_BASE_URL=https://api.example.com
使用:
js
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL;
前端环境变量会进入浏览器,不要放密钥。
9. 目录规范
中大型项目推荐:
text
src/
├── app/
│ ├── routes/
│ └── providers/
├── pages/
├── features/
│ └── knowledge-hub/
├── entities/
├── shared/
└── main.tsx
shared 必须克制,不要变成杂物目录。
10. 路由与页面架构
React Router:
jsx
createBrowserRouter([
{
path: '/',
element: <RootLayout />,
children: [
{ path: 'knowledge', element: <KnowledgePage /> },
{ path: 'settings', element: <SettingsPage /> },
],
},
]);
页面组件负责:
- 读取路由参数。
- 组织布局。
- 接入页面级数据。
- 处理页面级权限和错误。
页面组件不应该写大量业务规则。
11. URL 查询参数
搜索和筛选适合放 URL:
jsx
function useCourseFilters() {
const [params, setParams] = useSearchParams();
return {
keyword: params.get('keyword') ?? '',
level: params.get('level') ?? '全部',
setKeyword(keyword) {
setParams((current) => {
current.set('keyword', keyword);
return current;
});
},
};
}
好处:
- 可分享链接。
- 刷新后保留。
- 前进后退符合预期。
12. 权限路由
jsx
function ProtectedRoute({ children, permission }) {
const user = useCurrentUser();
if (!user) {
return <Navigate to="/login" replace />;
}
if (!user.permissions.includes(permission)) {
return <NoPermission />;
}
return children;
}
隐藏按钮不是权限控制,服务端必须校验。
13. React 生态选型
| 场景 | 推荐 |
|---|---|
| 后台 SPA | Vite + React Router + TanStack Query |
| SEO 内容站 | Next.js |
| 表单驱动全栈 | Remix |
| 大型客户端领域状态 | Redux Toolkit / Zustand |
| 大表单 | React Hook Form + Zod |
| 组件文档 | Storybook |
选型原则:用最少复杂度解决当前和可预见问题。
14. 样式体系
普通 CSS:
jsx
import './Button.css';
CSS Modules:
jsx
import styles from './Button.module.css';
function Button() {
return <button className={styles.button}>保存</button>;
}
Tailwind:
jsx
<button className="rounded-md bg-emerald-700 px-3 py-2 text-white">
保存
</button>
CSS-in-JS 适合动态主题和组件库,但要关注运行时成本与 SSR。
15. 设计 Token
css
:root {
--color-primary: #296a59;
--color-danger: #b42318;
--space-2: 8px;
--space-3: 12px;
--radius-sm: 6px;
}
主题:
css
[data-theme='dark'] {
--color-bg: #172026;
--color-text: #ffffff;
}
16. 组件库
Button 示例:
tsx
type ButtonVariant = 'primary' | 'secondary' | 'danger';
type ButtonProps = {
variant?: ButtonVariant;
loading?: boolean;
children: React.ReactNode;
} & React.ButtonHTMLAttributes<HTMLButtonElement>;
function Button({
variant = 'primary',
loading = false,
children,
disabled,
...props
}: ButtonProps) {
return (
<button
{...props}
disabled={disabled || loading}
className={`button button-${variant}`}
>
{loading ? '处理中...' : children}
</button>
);
}
组件 API 警惕布尔爆炸:
jsx
<Button isBlue isLarge hasIcon isAdminMode useNewStyle />
更好:
jsx
<Button variant="primary" size="large" icon={<PlusIcon />}>
新建
</Button>
17. 可访问性
语义化 HTML:
jsx
<button onClick={submit}>提交</button>
label:
jsx
const id = useId();
return (
<>
<label htmlFor={id}>邮箱</label>
<input id={id} type="email" />
</>
);
图标按钮:
jsx
<button aria-label="删除课程">
<TrashIcon />
</button>
错误提示:
jsx
<input aria-invalid={Boolean(error)} aria-describedby={errorId} />
{error && <p id={errorId} role="alert">{error}</p>}
18. 国际化
不要拼接复杂句子:
js
t('selectedItems', { count });
日期和数字:
js
new Intl.DateTimeFormat(locale).format(date);
new Intl.NumberFormat(locale).format(amount);
组件库层应考虑 RTL、文案入口、日期格式、复数规则。
19. Refs、动画与命令式集成
第三方图表:
jsx
function Chart({ data }) {
const containerRef = useRef(null);
const chartRef = useRef(null);
useEffect(() => {
chartRef.current = createChart(containerRef.current);
return () => chartRef.current.destroy();
}, []);
useEffect(() => {
chartRef.current.setData(data);
}, [data]);
return <div ref={containerRef} />;
}
原则:
- 初始化一次。
- 数据变化调用库 API 更新。
- 卸载时清理。
- 第三方 API 不泄漏到业务组件。
20. 安全
React 默认转义文本:
jsx
<p>{userInput}</p>
危险:
jsx
<div dangerouslySetInnerHTML={{ __html: html }} />
必须可信或严格清洗。
URL 安全:
jsx
function SafeLink({ href, children }) {
const url = new URL(href, window.location.origin);
if (!['http:', 'https:'].includes(url.protocol)) {
return null;
}
return <a href={url.href}>{children}</a>;
}
Server Function 必须做权限校验。
21. 迁移与版本
旧根 API:
jsx
ReactDOM.render(<App />, root);
新:
jsx
createRoot(root).render(<App />);
旧 hydrate:
jsx
ReactDOM.hydrate(<App />, root);
新:
jsx
hydrateRoot(root, <App />);
升级策略:
- 阅读 changelog。
- 升级测试环境。
- 跑 lint、typecheck、test、build。
- 修复废弃 API。
- 小流量灰度。
- 保留回滚路径。
22. 质量门禁
上线前:
- build 通过。
- lint 通过。
- typecheck 通过。
- 核心测试通过。
- 监控接入。
- 回滚方案明确。
- 环境变量校验。
- 包体积可控。
23. TypeScript、测试与工程扩展
API DTO 与领域类型要分离:
ts
type CourseDTO = {
course_id: string;
course_title: string;
};
type Course = {
id: string;
title: string;
};
function mapCourse(dto: CourseDTO): Course {
return {
id: dto.course_id,
title: dto.course_title,
};
}
不要让后端字段污染 UI。
测试必须优先覆盖:
- Reducer 状态转移。
- 权限判断。
- 表单校验。
- 错误状态。
- 空状态。
- 关键交互。
- 数据转换。
Mock 策略:
- API 用 MSW。
- 时间用 fake timers。
- 路由用 MemoryRouter。
- 浏览器 API 单独封装再 mock。
24. Storybook、可观测性与 Feature Flag
组件库 Story 应覆盖基础态、hover、focus、disabled、loading、error、long text、empty、dark mode、mobile viewport。
线上观测应包含:
- JS error。
- Promise rejection。
- API error。
- Web Vitals。
- 用户路径。
- 资源加载失败。
- 白屏。
- 包版本。
Feature Flag:
tsx
if (flags.newKnowledgeHub) {
return <NewKnowledgeHub />;
}
return <LegacyKnowledgeHub />;
用途是灰度、A/B、快速回滚和大功能分阶段发布。风险是 flag 长期不清理、状态组合爆炸、测试矩阵变复杂。
25. 包体积、安全与发布扩展
包体积检查:
- 图表库是否全量引入。
- 日期库是否过重。
- icon 是否按需。
- 富文本编辑器是否 lazy。
- 多版本依赖是否重复。
安全:
- XSS:谨慎使用
dangerouslySetInnerHTML。 - CSRF:Cookie 鉴权的 mutation 需要防护。
- Open Redirect:校验跳转目标 origin。
- 权限:前端权限只控制体验,服务端权限控制安全。
发布类型:
- 全量发布。
- 灰度发布。
- 金丝雀发布。
- 回滚发布。
- 多环境发布。
上线前问:
- 前端是否兼容旧接口?
- feature flag 是否可关闭?
- 错误监控是否能区分版本?
- 回滚是否需要清理缓存?
26. 工程专家评审题
- TypeScript 类型是否表达业务,而不是只消灭报错?
- 测试是否覆盖关键风险?
- CI 是否能阻止坏代码进入主分支?
- 包体积是否有预算?
- 依赖是否有退出方案?
- 安全边界是否在服务端?
- 组件库是否有文档和废弃策略?
- 发布失败后多久能回滚?
27. 工程化能力索引
- 包管理:npm、pnpm、yarn。
- 构建:Vite、Webpack、Rollup。
- 路由:React Router、Next.js、Remix。
- 服务端状态:TanStack Query、SWR。
- 客户端状态:Zustand、Redux Toolkit、Jotai。
- 表单:React Hook Form、Formik。
- 校验:Zod、Yup、Valibot。
- CSS:CSS Modules、Tailwind、CSS-in-JS。
- 组件文档:Storybook。
- 单元测试:Vitest、Jest。
- 组件测试:Testing Library。
- E2E:Playwright、Cypress。
- Mock:MSW。
- 质量:ESLint、Prettier、TypeScript。
- CI:GitHub Actions。
- 发布:CDN、Docker、平台部署。
- 监控:Sentry、Datadog、OpenTelemetry。
- 包体积:bundle analyzer。
- 安全:CSP、CSRF、XSS 防护。
- 文档:ADR、README、Runbook。
28. 项目模板建议
text
src/
├── app/
├── pages/
├── features/
├── entities/
├── shared/
├── test/
└── main.tsx
shared 应包含稳定基础能力,不能承载业务规则。业务规则应进入 features 或 entities。
29. Runbook 模板
md
# Runbook: 前端发布失败
## 现象
用户看到白屏或接口失败。
## 排查
1. 查看监控 release。
2. 检查 CDN 资源。
3. 检查接口兼容。
4. 检查 feature flag。
## 回滚
关闭 flag 或回滚到上一版本资源。
面试题完整答案总集:工程化、质量与生态
TypeScript 类型是否表达业务,而不是只消灭报错?
好的类型应表达领域语言和业务约束,例如 Level = '入门' | '进阶' | '高级',而不是全部写成 string。DTO 和领域模型应分离,避免后端字段污染 UI。类型的目标是让非法状态更难出现,而不是仅让编译器闭嘴。
测试是否覆盖关键风险?
关键风险包括状态转移、权限、表单校验、错误状态、数据转换和核心用户路径。测试不必覆盖所有样式和简单展示组件。优先用单元测试保护 reducer 和领域函数,用组件测试保护交互,用 E2E 保护关键流程。
CI 应该阻止哪些问题进入主分支?
CI 至少应阻止构建失败、类型错误、lint 错误、测试失败和格式不一致。成熟项目还应检查包体积、E2E 冒烟、可访问性和安全扫描。CI 的作用是让问题尽早暴露。
包体积为什么需要预算?
包体积直接影响首屏加载、解析和执行时间。没有预算时,依赖会逐渐膨胀,性能退化却难以察觉。应对关键入口设置 gzip/brotli 预算,低频重型模块 lazy load,并定期分析重复依赖和全量引入。
依赖是否有退出方案是什么意思?
引入依赖前要判断它是否可替换,API 是否会渗透到业务各处。如果一个库深度侵入领域模型,未来替换成本会很高。更好的做法是在项目内封装适配层,让业务不直接依赖第三方细节。
安全边界为什么必须在服务端?
前端代码运行在用户设备上,用户可以修改请求、绕过按钮隐藏、伪造参数。因此前端权限只负责体验,服务端必须做身份认证、权限校验、参数校验和审计。任何只靠前端判断的权限都不是安全边界。
组件库为什么需要废弃策略?
组件库一旦被多个业务使用,随意修改会造成大面积破坏。废弃策略包括标记 deprecated、提供替代方案、迁移脚本、版本记录、截止时间和 owner。没有废弃策略的组件库会积累大量旧 API,最终没人敢维护。
发布失败后多久能回滚?
成熟团队应能在分钟级回滚前端静态资源或关闭 feature flag。回滚能力取决于版本记录、资源保留、缓存策略、监控告警和发布流程。没有回滚方案的发布不应被认为是安全发布。