04 工程化、质量体系与 React 生态

本章关注从"个人能写"到"团队能长期交付"的能力: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 />);

升级策略:

  1. 阅读 changelog。
  2. 升级测试环境。
  3. 跑 lint、typecheck、test、build。
  4. 修复废弃 API。
  5. 小流量灰度。
  6. 保留回滚路径。

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. 工程化能力索引

  1. 包管理:npm、pnpm、yarn。
  2. 构建:Vite、Webpack、Rollup。
  3. 路由:React Router、Next.js、Remix。
  4. 服务端状态:TanStack Query、SWR。
  5. 客户端状态:Zustand、Redux Toolkit、Jotai。
  6. 表单:React Hook Form、Formik。
  7. 校验:Zod、Yup、Valibot。
  8. CSS:CSS Modules、Tailwind、CSS-in-JS。
  9. 组件文档:Storybook。
  10. 单元测试:Vitest、Jest。
  11. 组件测试:Testing Library。
  12. E2E:Playwright、Cypress。
  13. Mock:MSW。
  14. 质量:ESLint、Prettier、TypeScript。
  15. CI:GitHub Actions。
  16. 发布:CDN、Docker、平台部署。
  17. 监控:Sentry、Datadog、OpenTelemetry。
  18. 包体积:bundle analyzer。
  19. 安全:CSP、CSRF、XSS 防护。
  20. 文档:ADR、README、Runbook。

28. 项目模板建议

text 复制代码
src/
├── app/
├── pages/
├── features/
├── entities/
├── shared/
├── test/
└── main.tsx

shared 应包含稳定基础能力,不能承载业务规则。业务规则应进入 featuresentities

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。回滚能力取决于版本记录、资源保留、缓存策略、监控告警和发布流程。没有回滚方案的发布不应被认为是安全发布。

相关推荐
空中海2 小时前
03 性能、动画与 React Native 新架构
react native·react.js·架构
好运的阿财2 小时前
OpenClaw工具拆解之host_workspace_write+host_workspace_edit
前端·javascript·人工智能·机器学习·ai编程·openclaw·openclaw工具
ffqws_3 小时前
Spring Boot 接收前端请求的四种参数方式
前端·spring boot·后端
zhangrelay3 小时前
云课实践速通系列-基础篇汇总-必修-通识基础和专业基础-2026--工科--自动化、电气、机器人、测控等
linux·笔记·单片机·学习·ubuntu·机器人·自动化
空中海3 小时前
02 React Native状态、导航、数据流与设备能力
javascript·react native·react.js
是安迪吖3 小时前
企业资产管理系统练习
前端·ai
zhouwy1133 小时前
AI 编程工具结合 Figma MCP 实现前端设计高保真还原
前端·人工智能·figma
kyriewen4 小时前
WebAssembly:前端界的“外挂”,让C++代码在浏览器里跑起来
前端·c++·webassembly
悟空和大王4 小时前
核心 SDK 详细设计文档 (Visual-Render-SDK)
前端