((props, ref) => {
+ useImperativeHandle(ref, () => ({
+ validate: () => validator.validate(),
+ reset: () => setFields(initialValues),
+ }));
+
+ return ;
+});
+```
+
+## 决策树
+
+```mermaid
+graph TD
+ A["需要通信?"] -->|"是"| B["通信范围?"]
+
+ B --> C["仅父子两层"]
+ B --> D["多层级穿透"]
+ B --> E["跨层级 + 兄弟之间"]
+ B --> F["需命令式调用子组件方法"]
+
+ C --> G["Props + Callback ✅"]
+ D --> H["Context ✅"]
+ E --> I["Zustand / Redux ✅"]
+ F --> J["forwardRef + useImperativeHandle ✅"]
+
+ style G fill:#4FC08D,color:#fff
+ style H fill:#61DAFB,color:#000
+ style I fill:#F5A87D,color:#000
+ style J fill:#A0AEC0,color:#000
+```
+
+## 关联笔记
diff --git a/hhs/REACT/4. 进阶篇/12-HOC 与 Render Props.md b/hhs/REACT/4. 进阶篇/12-HOC 与 Render Props.md
new file mode 100644
index 0000000..ed38a19
--- /dev/null
+++ b/hhs/REACT/4. 进阶篇/12-HOC 与 Render Props.md
@@ -0,0 +1,201 @@
+---
+tags: [React, HOC, Render Props, Pattern, Frontend]
+create time: 2026-04-29 22:11
+---
+
+# HOC 与 Render Props
+
+## 概述
+
+Hooks 出现之前,HOC(高阶组件)和 Render Props 是 React 中复用它逻辑的两种主要模式。理解它们的原理、适用场景和局限,有助于阅读遗留代码并理解为什么 Hooks 成为更优解。
+
+## HOC(高阶组件)
+
+### 概念
+
+```mermaid
+graph LR
+ A["原始组件 Component"] -->|"注入 props"| B["HOC 函数"]
+ B --> C["增强组件 EnhancedComponent"]
+ C --> D["获得额外能力:日志/权限/数据"]
+
+ style B fill:#F5A87D,color:#000
+```
+
+HOC 是一个**函数**,接收组件作为参数,返回增强后的新组件:
+
+```tsx
+// 基础模式
+function withAuth(
+ WrappedComponent: React.ComponentType
+) {
+ return function WithAuth(props: P) {
+ const user = useAuth();
+ if (!user) return ;
+ return ;
+ };
+}
+
+// 使用:装饰器风格
+const ProtectedPage = withAuth(function Dashboard() {
+ return
Dashboard
;
+});
+
+// TS 类型推导:保留原始 props
+interface Props { title: string }
+const StyledTitle = withTheme(function Title({ title }: Props) {
+ return {title}
;
+}); // ✅ TS 推断 Props 不变
+```
+
+### 常见 HOC 模式
+
+```tsx
+// 1. 日志/HUD
+function withLogger(Comp: React.FC
): React.FC
{
+ return function LoggedComponent(props: P) {
+ useEffect(() => console.log(`${Comp.name} mounted`), []);
+ return ;
+ };
+}
+
+// 2. 加载态封装
+function withLoading
(
+ Comp: React.FC
,
+ fetchData: () => Promise
+): React.FC {
+ return function LoadingWrapper(props: P) {
+ const [loading, setLoading] = useState(true);
+ useEffect(() => { fetchData().then(() => setLoading(false)); }, []);
+ return loading ? : ;
+ };
+}
+
+// 3. Props 转换(驼峰→kebab)
+function withPropsTransformer
(
+ Comp: React.FC
,
+ transform: (p: P) => Record
+): React.FC {
+ return function Transformed(props: P) {
+ const extra = transform(props);
+ return ;
+ };
+}
+```
+
+### HOC 的局限性
+
+| 问题 | 说明 |
+|------|------|
+| **Static 属性丢失** | `withAuth(Page)` 返回的新组件没有 Page.getInitialProps |
+| **Wrapper Hell** | `withAuth(withLogging(withData(Page)))`——嵌套过深调试困难 |
+| **Props 冲突** | 多个 HOC 都注入 `data` prop,后一个覆盖前一个 |
+| **ref 丢失** | 直接传 ref 给增强组件会报错(需用 forwardRef 包装) |
+
+> [!tip] HOC 兼容 ref
+> ```tsx
+> export function withAuth(Wrapped: React.ComponentType
) {
+> return React.forwardRef((props, ref) => {
+> const { authenticated } = useAuth();
+> if (!authenticated) return ;
+> return ;
+> });
+> }
+> ```
+
+## Render Props
+
+### 核心思想
+
+通过 prop 传递一个**函数**,该函数返回 JSX——将 UI 渲染逻辑委托给调用方。
+
+```tsx
+interface MouseTrackerProps {
+ render: (position: { x: number; y: number }) => React.ReactNode;
+}
+
+function MouseTracker({ render }: MouseTrackerProps) {
+ const [pos, setPos] = useState({ x: 0, y: 0 });
+
+ useEffect(() => {
+ const handler = (e: MouseEvent) => setPos({ x: e.clientX, y: e.clientY });
+ window.addEventListener("mousemove", handler);
+ return () => window.removeEventListener("mousemove", handler);
+ }, []);
+
+ // 🎯 关键:render 函数返回什么就渲染什么
+ return {render(pos)}
;
+}
+
+// 使用
+ (
+ Cursor at ({x}, {y})
+)} />;
+```
+
+### 等价于 children 的情况
+
+```tsx
+// 当 render prop 只是把数据传给 children 时,可以用 children 替代
+function DataProvider({ children }: { children: (data: Data) => React.ReactNode }) {
+ const data = useDatabase();
+ return <>{children(data)}>;
+}
+
+
+ {(data) => }
+;
+```
+
+## HOC vs Render Props vs Custom Hook
+
+```mermaid
+graph TB
+ A["逻辑复用需求"] --> B["方案对比"]
+
+ B --> C["HOC"]
+ B --> D["Render Props"]
+ B --> E["Custom Hook"]
+
+ C --> F["⚠️ Wrapper 嵌套深"]
+ C --> G["⚠️ 静态方法丢失"]
+ C --> H["✅ 不修改原组件结构"]
+
+ D --> I["⚠️ 回调地狱"]
+ D --> J["⚠️ Prop 命名冲突风险"]
+ D --> K["✅ 灵活的 UI 控制"]
+
+ E --> L["✅ 简洁直观"]
+ E --> M["✅ 可直接操作 state / effect"]
+ E --> N["✅ 无 wrapper 嵌套"]
+ E --> O["❌ 只能用于组件内部"]
+
+ style L fill:#4FC08D,color:#fff
+ style M fill:#4FC08D,color:#fff
+ style N fill:#4FC08D,color:#fff
+```
+
+## 为什么 Hooks 取代了它们?
+
+```tsx
+// ❌ HOC 方式
+const ConnectedUserList = withAuth(withCache(withPagination(UserList)));
+// 三层嵌套 → 调试困难、性能不可见、type 推导混乱
+
+// ✅ Hook 方式
+function UserList() {
+ useAuth(); // 身份验证
+ const cache = useCache(); // 数据缓存
+ const pagination = usePagination(); // 分页管理
+
+ return ;
+}
+// 扁平可读、天然共享 state、TS 完美推断
+```
+
+> [!note] HOC 和 Render Props 真的被淘汰了吗?
+> - HOC:在需要**包裹**组件但不修改其内部的场景仍有价值(如第三方库封装)
+> - Render Props:当父组件需要**完全控制子组件的渲染内容**时仍然有用
+> - 但 90%+ 的场景,Custom Hook 是更好的选择
+
+## 关联笔记
diff --git a/hhs/REACT/4. 进阶篇/13-并发特性.md b/hhs/REACT/4. 进阶篇/13-并发特性.md
new file mode 100644
index 0000000..63f73c9
--- /dev/null
+++ b/hhs/REACT/4. 进阶篇/13-并发特性.md
@@ -0,0 +1,177 @@
+---
+tags: [React, Concurrent, Suspense, Transitions, Frontend]
+create time: 2026-04-29 22:12
+---
+
+# 并发特性
+
+## 概述
+
+React 18 引入了并发渲染(Concurrent Rendering)架构,将 UI 更新划分为可中断、可恢复、可优先级调度的任务。理解并发的核心概念——Suspend、Transition、时间切片——能帮助你写出更流畅的用户体验。
+
+## React 渲染架构演进
+
+```mermaid
+graph LR
+ A["React 17 同步渲染"] -->|"全部一次性完成"| B["长时间阻塞主线程 ❌"]
+
+ C["React 18 并发渲染"] -->|"可打断/可恢复"| D["Fiber Scheduler"]
+ D --> E["高优先级任务:用户输入 ⏩"]
+ D --> F["低优先级任务:数据加载 🐢"]
+ F -->|"被高优先级打断"| G["暂停 → 之后恢复 ✅"]
+
+ style B fill:#F5A87D,color:#000
+ style G fill:#4FC08D,color:#fff
+```
+
+## Suspense —— 声明式等待
+
+### 基本用法
+
+```tsx
+// LazyComponent 在首次挂载时自动 code-split
+const SettingsPage = lazy(() => import("./pages/SettingsPage"));
+
+}>
+
+
+```
+
+### Suspense + Data Fetching(实验性 API)
+
+```tsx
+import { use } from "react";
+
+// 资源标记为 Suspense-compatible
+const promise = fetchData("/api/profile");
+
+function Profile() {
+ // use() 会 suspend 直到 promise resolve
+ const profile = use(promise);
+
+ return {profile.name}
;
+}
+
+// 外层用 Suspense 包裹
+}>
+
+
+```
+
+## useTransition —— 标记低优先级更新
+
+```tsx
+function SearchPage() {
+ const [query, setQuery] = useState("");
+ const [results, setResults] = useState([]);
+ const [isPending, startTransition] = useTransition();
+
+ const handleInputChange = (e: React.ChangeEvent) => {
+ const value = e.target.value;
+
+ // 立即响应输入
+ setQuery(value);
+
+ // 低优先级的结果更新(可被输入打断)
+ startTransition(() => {
+ setResults(heavySearch(value));
+ });
+ };
+
+ return (
+ <>
+
+ {isPending && }
+
+ >
+ );
+}
+```
+
+### Transition 与直接 setState 对比
+
+```mermaid
+timeline
+ title "输入 "hello" 的渲染行为"
+
+ 直接 setState : 每次按键 → re-render\n(h/h/e/l/o 共 5 次)
+ useTransition : h,e,l,l → 跳过中间\no → 最终渲染一次
+```
+
+## useDeferredValue —— 延迟副本
+
+```tsx
+function TodoApp() {
+ const [filter, setFilter] = useState("");
+ const deferredFilter = useDeferredValue(filter); // 延迟 ~16ms
+
+ return (
+ <>
+ {/* 快速响应用户输入 */}
+ setFilter(e.target.value)} placeholder="过滤..." />
+
+ {/* 耗时操作使用延迟值 */}
+
+ >
+ );
+}
+```
+
+> [!tip] Transition vs DeferredValue 选择指南
+>
+> | 场景 | 推荐 |
+> |------|------|
+> | 表单提交后展示新视图 | `useTransition` |
+> | 输入框即时搜索 + 延迟加载 | `useDeferredValue` |
+> | 多个状态联动更新 | `useTransition`(更明确控制粒度) |
+> | 简单延迟一个值 | `useDeferredValue`(一行搞定) |
+
+## 时间切片原理
+
+```mermaid
+sequenceDiagram
+ participant Browser as 浏览器主线程
+ participant Fiber as Fiber Scheduler
+
+ Browser->>Fiber: 构建整棵组件树(工作单元)
+ Fiber->>Browser: 渲染帧 1(约 16ms)✅
+ Note over Fiber,Browser: 把任务切分为 ~5ms 的工作单元
+ Browser->>Fiber: 下一帧开始
+ Fiber->>Browser: 渲染帧 2 ✅
+ Note over Fiber: 如果用户点击了按钮
高优先级任务插入队列
+ Fiber-->>Browser: 暂停当前帧
+ Browser->>Fiber: 处理用户输入 ✅
+ Fiber->>Browser: 继续未完成的渲染帧
+
+ style Browser fill:#F5A87D,color:#000
+ style Fiber fill:#4FC08D,color:#fff
+```
+
+### 关键概念
+
+- **Work Breakdown**:React 将渲染工作拆成多个小单元(work unit),每个单元约 1ms
+- **Yield to Browser**:每个单元完成后让出控制权给浏览器,确保 UI 响应性
+- **Priority Scheduling**:用户输入 > 网络响应 > 后台数据更新 > 动画
+
+## 并发模式下的陷阱
+
+> [!warning] 需要注意的行为变化
+
+```tsx
+// 1. useEffect 的执行时机变了
+useEffect(() => {
+ console.log("effect");
+}, [dep]);
+// 效果不再与渲染同步,可能在异步 commit 阶段执行
+
+// 2. 浏览器生命周期方法(如 getSnapshotBeforeUpdate)已过时
+// 并发模式下无法保证同步 commit
+
+// 3. 旧版第三方库可能与并发不兼容
+// 遇到 "Cannot update a component during rendering" → 检查是否有不规范的 side effect
+
+// 4. StrictMode 双重渲染(开发环境)
+// 用于发现不纯的 render 函数和清理逻辑缺失
+```
+
+## 关联笔记
diff --git a/hhs/REACT/4. 进阶篇/14-Serverside Rendering.md b/hhs/REACT/4. 进阶篇/14-Serverside Rendering.md
new file mode 100644
index 0000000..5e10b9e
--- /dev/null
+++ b/hhs/REACT/4. 进阶篇/14-Serverside Rendering.md
@@ -0,0 +1,190 @@
+---
+tags: [React, Next.js, SSR, SSG, ISR, Frontend]
+create time: 2026-04-29 22:13
+---
+
+# Server-Side Rendering
+
+## 概述
+
+服务端渲染(SSR)让 React 组件在服务器端预渲染为 HTML,显著改善首屏加载速度和 SEO。本文档以 Next.js App Router 为核心,介绍 SSR/SSG/ISR 的渲染策略与最佳实践。
+
+## 渲染模式对比
+
+```mermaid
+graph TB
+ subgraph "客户端渲染 CSR"
+ A[HTML空白页] --> B["下载 JS Bundle"]
+ B --> C["执行 React hydration"]
+ C --> D["显示内容"]
+ end
+
+ subgraph "服务端渲染 SSR"
+ E[请求页面] --> F["服务器渲染 React → HTML"]
+ F --> G["发送含内容的 HTML"]
+ G --> H["客户端 hydration"]
+ H --> I["交互可用"]
+ end
+
+ subgraph "静态生成 SSG"
+ J["构建时渲染"] --> K["生成纯 HTML 文件"]
+ K --> L["CDN 分发"]
+ end
+
+ style F fill:#4FC08D,color:#fff
+ style H fill:#F5A87D,color:#000
+ style J fill:#61DAFB,color:#000
+```
+
+### 三种策略决策表
+
+| 策略 | 适用场景 | 数据时效性 | 构建参与 |
+|------|----------|-----------|---------|
+| **SSR**(Server Render) | 个性化页面、实时数据 | ✅ 每次请求实时生成 | ❌ |
+| **SSG**(Static Site Generation) | 博客、文档、营销页 | ⏱️ 构建时生成 | ✅ |
+| **ISR**(Incremental Static Regeneration) | 新闻列表、商品目录 | 🔄 定时后台更新 | ✅(增量) |
+
+## Next.js App Router 架构
+
+```tsx
+// app/layout.tsx —— 根布局(所有页面共享)
+export default function RootLayout({ children }: { children: React.ReactNode }) {
+ return (
+
+
+
+ {children}
+
+
+ );
+}
+
+// app/page.tsx —— 首页(SSR by default)
+async function HomePage() {
+ // ✅ 直接在组件中 await API
+ const posts = await fetchPosts();
+
+ return (
+
+ {posts.map(post => )}
+
+ );
+}
+
+// app/blog/[slug]/page.tsx —— 动态路由页
+async function PostPage({ params }: { params: { slug: string } }) {
+ const post = await getPostBySlug(params.slug);
+ return {post.content};
+}
+```
+
+## Streaming SSR + Suspense
+
+```mermaid
+sequenceDiagram
+ participant Client as 浏览器
+ participant Server as 服务器
+
+ Client->>Server: GET /dashboard
+ Server->>Server: 并行请求 user/profile/orders
+ Note over Server: 每个请求可有自己的 Suspense boundary
+
+ Server-->>Client: HTML: Navbar + Sidebar
(立即显示,~200ms)
+
+ Server-->>Client: Stream: Profile card
(中等优先级,~800ms)
+
+ Server-->>Client: Stream: Order history
(低优先级,~1500ms)
+
+ Note over Client: 用户体验:渐进式展示,而非等全部完成
+```
+
+```tsx
+// Dashboard layout(流式渲染的关键)
+function DashboardLayout({ children }: { children: React.ReactNode }) {
+ return (
+ <>
+ {/* 非关键 UI 用 Suspense 包裹 */}
+ }>
+
+
+
+ {children}
+
+ {/* 各区域独立 Suspense */}
+ }>
+
+
+ }>
+
+
+ >
+ );
+}
+```
+
+## Data Fetching 策略
+
+```tsx
+// 方案1:直接 await(默认缓存 + 共享缓存)
+async function Page() {
+ const data = await fetchData(); // 自动缓存,同路径请求去重
+ return {data}
;
+}
+
+// 方案2:带 revalidate 的 ISR
+async function Page() {
+ const data = await fetchData({ next: { revalidate: 60 } }); // 60秒后后台重新验证
+ return {data}
;
+}
+
+// 方案3:no-store(强制 SSR,不走缓存)
+async function Page() {
+ const data = await fetchData({ cache: "no-store" }); // 每次请求都获取最新数据
+ return {data}
;
+}
+
+// 方案4:client component 中的 fetch(使用 TanStack Query)
+"use client";
+function Page() {
+ const { data } = useQuery({ queryKey: ["data"], queryFn: () => fetch("/api/data").then(r => r.json()) });
+ return {data}
;
+}
+```
+
+## SSR vs Client Component 边界
+
+```tsx
+// server component(默认,无需声明)
+async function ServerComponent() {
+ // ✅ 可以直接访问数据库、API密钥、文件系统
+ const db = await dbConnection.query("SELECT * FROM users");
+ return ;
+}
+
+// client component(需显式声明)
+"use client";
+
+function InteractiveChart() {
+ // ✅ 可以使用 useState/useEffect/DOM API
+ const [zoom, setZoom] = useState(1);
+ useEffect(() => { ... }, []);
+
+ return ;
+}
+
+// Parent
+function Dashboard() {
+ return (
+ <>
+ {/* 在服务端渲染 */}
+ {/* 在客户端渲染 */}
+ >
+ );
+}
+```
+
+> [!warning] Client → Server 通信限制
+> - Client Component 无法直接调用 Server Component 的方法或 props
+> - 解决方案:通过 URL 参数、cookies、或后端 API 传递数据
+
+## 关联笔记
diff --git a/hhs/REACT/5. 工程实践篇/15-性能优化.md b/hhs/REACT/5. 工程实践篇/15-性能优化.md
new file mode 100644
index 0000000..780f32a
--- /dev/null
+++ b/hhs/REACT/5. 工程实践篇/15-性能优化.md
@@ -0,0 +1,157 @@
+---
+tags: [React, Performance, Optimization, Frontend]
+create time: 2026-04-29 22:14
+---
+
+# 性能优化
+
+## 概述
+
+React 性能优化的核心原则是 **"减少不必要的渲染"**。本文档从诊断工具到具体手段,提供完整的调优方法论。
+
+## 性能诊断工具箱
+
+```mermaid
+graph TB
+ A[发现性能问题] --> B["选择诊断工具"]
+
+ B --> C["React DevTools Profiler"]
+ B --> D["Chrome Performance Tab"]
+ B --> E["Lighthouse"]
+ B --> F["Web Vitals 监控"]
+
+ C --> G["定位重渲染的组件和原因"]
+ D --> H["分析主线程阻塞时段"]
+ E --> I["整体 LCP/FID/CLS 评分"]
+ F --> J["生产环境真实用户数据"]
+
+ style C fill:#61DAFB,color:#000
+ style H fill:#F5A87D,color:#000
+```
+
+### React DevTools Profiler 使用要点
+
+1. 录制期间进行关键交互(点击、输入)
+2. 观察 **Commit 颜色** —— 红色越深表示重渲染越多
+3. 展开组件树,关注 "why did this render" 原因
+4. 对比优化前后的 Commit 时间变化
+
+## React.memo —— 阻止子组件重渲染
+
+```tsx
+const ExpensiveList = React.memo(({ items }: { items: Item[] }) => {
+ // props.items 引用不变时,跳过整个子树的 re-render
+ return (
+
+ {items.map(item => - {item.name}
)}
+
+ );
+}, (prevProps, nextProps) => prevProps.items === nextProps.items); // 自定义比较函数
+```
+
+> [!warning] React.memo 的适用边界
+> - 只对**纯组件**有用——相同 props 必须产生相同的输出
+> - 父组件每次传新对象/函数作 prop → memo 无效
+> - 简单列表(几十项以内)不需要 memo
+
+## 虚拟列表(Virtual Scrolling)
+
+当列表项超过数百条时,虚拟滚动通过只渲染可视区域内的 DOM 元素来大幅降低内存占用:
+
+```tsx
+// 方案1:tanstack/virtual(推荐)
+import { useVirtualizer } from "@tanstack/react-virtual";
+
+function VirtualList({ items }: { items: string[] }) {
+ const parentRef = useRef(null);
+
+ const virtualizer = useVirtualizer({
+ count: items.length,
+ getScrollElement: () => parentRef.current,
+ estimateSize: () => 50, // 预估每项高度
+ overscan: 5, // 视口上下各多渲染 5 项
+ });
+
+ return (
+
+
+ {virtualizer.getVirtualItems().map(virtualRow => (
+
+ {items[virtualRow.index]}
+
+ ))}
+
+
+ );
+}
+```
+
+> [!tip] 何时需要虚拟列表?
+> - 列表项 > ~50 且每帧渲染耗时可感知
+> - 固定高度的项目比可变高度更容易实现
+> - React Window / React Virtualized 是老牌的成熟方案
+
+## Code Splitting
+
+### 路由级拆分
+
+```tsx
+// Next.js App Router(内置)
+const AdminPage = lazy(() => import("./pages/Admin"));
+
+// vite + React Router
+const Settings = lazy(() => import(/* vite: preload */ "./pages/Settings"));
+```
+
+### 组件级拆分
+
+```tsx
+import dynamic from "next/dynamic";
+
+// 不加载图表库直到真正需要
+const Chart = dynamic(() => import("recharts"), { ssr: false });
+// 或带 loading fallback
+const HeavyEditor = dynamic(() => import("@monaco-editor/react"), {
+ loading: () => Loading editor...
,
+ ssr: false,
+});
+```
+
+### Bundle 分析与优化
+
+```bash
+# 安装插件
+npm install --save-dev rollup-plugin-visualizer
+# 或在 Next.js 中使用 next-bundle-analyzer
+
+# 生成可视化报告
+npx run build && npx visualizer
+```
+
+> [!tip] Bundle 大小目标
+> | 层级 | 目标大小(gzipped)|
+> |------|---------------------|
+> | 首屏 chunk | < 150KB |
+> | 单个 chunk | < 300KB |
+> | JS Total | < 500KB(SPA)/ < 200KB(PWA) |
+
+## Lighthouse 关键指标调优
+
+| 指标 | 含义 | 优化方向 |
+|------|------|----------|
+| **FCP**(First Contentful Paint) | 首次内容绘制 | 减小首屏 HTML/JS 体积 |
+| **LCP**(Largest Contentful Paint) | 最大内容绘制 | 图片懒加载、预加载关键资源 |
+| **INP**(Interaction to Next Paint) | 交互响应延迟 | useTransition、删除同步 heavy work |
+| **CLS**(Cumulative Layout Shift) | 布局偏移 | 预留图片宽高、避免字体闪烁 |
+
+## 关联笔记
diff --git a/hhs/REACT/5. 工程实践篇/16-测试.md b/hhs/REACT/5. 工程实践篇/16-测试.md
new file mode 100644
index 0000000..547b10a
--- /dev/null
+++ b/hhs/REACT/5. 工程实践篇/16-测试.md
@@ -0,0 +1,225 @@
+---
+tags: [React, Testing, Vitest, RTL, Frontend]
+create time: 2026-04-29 22:15
+---
+
+# 测试
+
+## 概述
+
+可靠的测试是大型 React 项目长期维护的基石。本文档以 Vitest + React Testing Library (RTL) 为主,介绍组件单元测试、Hook 测试和集成测试的最佳实践。
+
+## 测试金字塔
+
+```mermaid
+graph TB
+ A["测试金字塔"]
+
+ A --> B["单元测试 ~70%"]
+ A --> C["集成测试 ~20%"]
+ A --> D["E2E 测试 ~10%"]
+
+ B --> B1["纯函数 / util"]
+ B --> B2["自定义 Hook"]
+ B --> B3["原子组件(Button)"]
+
+ C --> C1["多组件交互流程"]
+ C --> C2["表单提交 → API → 状态更新"]
+
+ D --> D1["用户旅程:登录→搜索→下单"]
+
+ style B fill:#4FC08D,color:#fff
+ style C fill:#F5A87D,color:#000
+ style D fill:#61DAFB,color:#000
+```
+
+## 环境配置
+
+```jsonc
+// vitest.config.ts
+import { defineConfig } from "vitest/config";
+import react from "@vitejs/plugin-react";
+
+export default defineConfig({
+ plugins: [react()],
+ test: {
+ environment: "jsdom", // 模拟浏览器 DOM
+ setupFiles: "./src/test/setup.ts",
+ globals: true,
+ },
+});
+```
+
+```ts
+// src/test/setup.ts
+import "@testing-library/jest-dom/vitest"; // 扩展 expect 匹配器
+import { vi } from "vitest";
+
+// Mock window.matchMedia(解决媒体查询测试报错)
+Object.defineProperty(window, "matchMedia", {
+ writable: true,
+ value: vi.fn().mockImplementation(query => ({
+ matches: false,
+ media: query,
+ onchange: null,
+ addListener: vi.fn(), // deprecated
+ removeListener: vi.fn(), // deprecated
+ addEventListener: vi.fn(),
+ removeEventListener: vi.fn(),
+ dispatchEvent: vi.fn(),
+ })),
+});
+```
+
+## RTL 核心哲学
+
+> [!tip] RTL 设计原则
+> - **测试行为,不测试实现** — 关注用户能感知到的东西(文本、按钮、网络请求)
+> - **像用户一样思考** — 用 `screen.getByRole("button", { name: "Submit" })` 而非 `.querySelector(".btn-primary"`
+> - **断言明确期望的结果** — 不要测试 state 的值,测试渲染输出
+
+```tsx
+// ❌ 反例:耦合于内部实现
+expect(component.state.count).toBe(2);
+expect(wrapper.find(Button).length).toBe(1);
+
+// ✅ 正例:基于用户感知
+const button = screen.getByRole("button", { name: /add/i });
+userEvent.click(button);
+await screen.findByText(/added!/i);
+```
+
+## 组件单元测试
+
+### 基础模式
+
+```tsx
+import { render, screen, fireEvent, waitFor } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { Counter } from "./Counter";
+
+describe("", () => {
+ it("初始显示 0", () => {
+ render();
+ expect(screen.getByText("0")).toBeInTheDocument();
+ });
+
+ it("点击按钮后计数增加", async () => {
+ render();
+ const button = screen.getByRole("button");
+
+ await userEvent.click(button);
+ expect(screen.getByText("1")).toBeInTheDocument();
+
+ await userEvent.click(button);
+ await userEvent.click(button);
+ expect(screen.getByText("3")).toBeInTheDocument();
+ });
+
+ it("禁用态不可点击", () => {
+ render();
+ const button = screen.getByRole("button");
+ expect(button).toBeDisabled();
+ });
+});
+```
+
+### Props 驱动 UI
+
+```tsx
+describe("", () => {
+ it("显示用户基本信息", () => {
+ render();
+ expect(screen.getByText("Alice")).toBeInTheDocument();
+ expect(screen.getByRole("img", { name: /avatar/i })).toHaveAttribute("alt", "Alice avatar");
+ });
+
+ it("显示操作菜单当 admin 时", () => {
+ render();
+ expect(screen.getByRole("button", { name: /edit/i })).toBeInTheDocument();
+ });
+
+ it("普通用户不显示操作菜单", () => {
+ render();
+ expect(screen.queryByRole("button", { name: /edit/i })).not.toBeInTheDocument();
+ });
+});
+```
+
+## Mock 异步操作
+
+```tsx
+it("loading 态在请求完成后消失", async () => {
+ vi.mocked(fetch).mockResolvedValueOnce({
+ ok: true,
+ json: async () => [{ id: 1, name: "Test" }],
+ } as Response);
+
+ render();
+
+ // 等待 loading 态出现再消失
+ const spinner = await screen.findByRole("status");
+ expect(spinner).toHaveTextContent("Loading...");
+
+ // 数据渲染完成
+ const item = await screen.findByText("Test");
+ expect(item).toBeInTheDocument();
+});
+
+it("网络错误显示错误提示", async () => {
+ vi.mocked(fetch).mockRejectedValueOnce(new Error("Network error"));
+
+ render();
+
+ await waitFor(() => {
+ expect(screen.getByText("Failed to load")).toBeInTheDocument();
+ });
+});
+```
+
+## Hook 测试
+
+```tsx
+import { renderHook, act } from "@testing-library/react";
+import { useDebounce } from "../hooks/useDebounce";
+
+describe("useDebounce", () => {
+ beforeEach(() => vi.useFakeTimers());
+ afterEach(() => vi.useRealTimers());
+
+ it("值不变时返回原始值", () => {
+ const { result } = renderHook(({ value }) => useDebounce(value, 300), {
+ initialProps: { value: "hello", delay: 300 },
+ });
+
+ expect(result.current).toBe("hello");
+ });
+
+ it("延迟后返回新值", async () => {
+ const { result, rerender } = renderHook(
+ ({ value }) => useDebounce(value, 300),
+ { initialProps: { value: "a" } }
+ );
+
+ rerender({ value: "b" });
+ expect(result.current).toBe("a"); // 尚未变化
+
+ act(() => vi.advanceTimersByTime(300));
+ expect(result.current).toBe("b"); // 防抖完成
+ });
+});
+```
+
+## E2E 测试选择
+
+| 工具 | 适用场景 | 特点 |
+|------|----------|------|
+| **Playwright** | 全功能 E2E(推荐) | 跨浏览器、内置 trace、重试机制 |
+| **Cypress** | 可视化调试友好 | DevTools 体验好、社区活跃 |
+| **Puppeteer** | Google 官方、精细控制 | 底层 API、灵活性高 |
+
+> [!note] E2E 边界
+> - E2E 只应覆盖**关键用户旅程**(登录、下单、支付)
+> - 不要为每个页面的每个字段写 E2E——那属于集成测试的范畴
+
+## 关联笔记
diff --git a/hhs/REACT/5. 工程实践篇/17-可访问性.md b/hhs/REACT/5. 工程实践篇/17-可访问性.md
new file mode 100644
index 0000000..14f5f36
--- /dev/null
+++ b/hhs/REACT/5. 工程实践篇/17-可访问性.md
@@ -0,0 +1,238 @@
+---
+tags: [React, A11y, Accessibility, Frontend]
+create time: 2026-04-29 22:16
+---
+
+# 可访问性
+
+## 概述
+
+可访问性(Accessibility,简称 a11y)确保残障用户也能正常使用应用。这不仅是道德责任,在许多国家和地区也是法律要求。本文档梳理 React 中实现无障碍的关键实践。
+
+## WCAG 核心原则
+
+```mermaid
+graph TB
+ A["WCAG 2.1 四大原则"] --> B["Perceivable
可感知"]
+ A --> C["Operable
可操作"]
+ A --> D["Understandable
可理解"]
+ A --> E["Robust
鲁棒性"]
+
+ B --> B1["文本替代"]
+ B --> B2["颜色对比度 ≥ 4.5:1"]
+
+ C --> C1["键盘可达"]
+ C --> C2["足够的时间"]
+
+ D --> D1["可读的文本"]
+ D --> D2["一致导航"]
+
+ E --> E1["兼容辅助技术"]
+
+ style A fill:#F5A87D,color:#000
+ style B fill:#4FC08D,color:#fff
+ style C fill:#61DAFB,color:#000
+ style D fill:#A0AEC0,color:#000
+ style E fill:#ED8936,color:#000
+```
+
+## 语义化 HTML(最重要的一条)
+
+```tsx
+// ❌ 滥用 div + onClick
+ navigate("/home")}>Home
+ goTo("/about")}>About
+
+// ✅ 使用原生元素——天生支持键盘、屏幕阅读器、SEO
+
+首页
+关于
+```
+
+### 常用语义标签对照表
+
+| 功能 | 错误写法 | 正确写法 |
+|------|---------|---------|
+| 按钮行为 | `