第14天:React 工程化与设计模式

今天复习了 React 工程化中的六个核心问题:错误边界、组件库设计、原子设计、受保护路由、状态管理选型和状态提升。以下是问答整理与知识补充。


1. 什么是错误边界(Error Boundaries)?如何实现?

我的回答:

  • 错误边界不能捕获事件处理器和异步代码里的错误,这些需要用 try/catch。只捕获渲染阶段的错误,进行UI降级。

  • 类组件中需要两个生命周期方法:getDerivedStateFromError(用于渲染降级 UI)和 componentDidCatch(用于记录错误日志)。

  • 错误边界可以放在根组件防止页面白屏,也可以包裹子组件,实现局部降级。

  • React 19 可以直接引入 <ErrorBoundary> 组件并用 fallback 属性指定兜底 UI;旧版 React 需要安装 react-error-boundary 库。

补充:

  • 错误边界不能捕获的错误包括:
  1. 事件处理器内部的错误(需 try/catch)
  2. 异步代码(setTimeout、requestAnimationFrame、Promise)
  3. 服务端渲染(SSR)期间抛出的错误
  4. 错误边界自身抛出的错误(无法捕获自己)
  • 错误边界能捕获的错误包括:
  1. 组件渲染时的错误(render 期间)
  2. 生命周期方法里的错误
  3. 子组件树抛出的错误
  4. 函数组件 return 里的错误
  5. Hooks 渲染阶段的错误
  • getDerivedStateFromError 是静态方法,接收错误参数,返回新状态(如 { hasError: true })。它会在渲染阶段调用,不允许有副作用。

  • componentDidCatch 在提交阶段调用,可执行日志上报等副作用。

    javascript 复制代码
    class ErrorBoundary extends React.Component {
      static getDerivedStateFromError(error) {
        // 返回新 state → 触发降级 UI
        return { hasError: true };
      }
    
      componentDidCatch(error, info) {
        // 副作用:上报错误
        console.error(error, info);
      }
    
      render() {
        if (this.state.hasError) {
          return <h1>出错了</h1>;
        }
        return this.props.children;
      }
    }
  • 推荐将错误边界放在组件树的顶层(包裹路由出口)以及关键子模块周围,实现细粒度降级。

扩展:

  • 函数组件中可以用 react-error-boundary 提供的 useErrorHandler Hook 处理事件回调中的错误。

  • 如何测试错误边界?可以使用 render 和 act 配合抛出错误。

    javascript 复制代码
    import { render, screen } from '@testing-library/react';
    import { act } from 'react-dom/test-utils';
    
    // 错误边界组件
    class ErrorBoundary extends React.Component {
      constructor(props) {
        super(props);
        this.state = { hasError: false };
      }
    
      static getDerivedStateFromError() {
        return { hasError: true };
      }
    
      render() {
        if (this.state.hasError) {
          return <div>出错啦</div>;
        }
        return this.props.children;
      }
    }
    
    // 一渲染就报错的组件
    const Crash = () => {
      throw new Error('test error');
    };
    
    // 测试用例
    test('错误边界应该捕获渲染错误', () => {
      //让 React 的所有状态更新、渲染、副作用都同步执行完成
      act(() => {
        render(
          <ErrorBoundary>
            <Crash />
          </ErrorBoundary>
        );
      });
    
      //页面上能找到 "出错啦" 这句话,并且这个元素确实在 DOM 里。
      expect(screen.getByText('出错啦')).toBeInTheDocument();
    });

2. 如果让你设计一个组件库,你会考虑哪些方面?

我的回答:

  • 从开发者和用户体验出发:完善的文档、TypeScript 支持、可自定义样式(通过 className 或主题变量)、易测试。
  • 构建与发布:按需加载(如 babel-plugin-import)、支持多种模块格式(ESM、CJS)、版本管理。
  • 组件筛选:优先包含通用性强、高频使用的组件(按钮、输入框、轮播图等)。

补充:

  • API 设计 :遵循一致性原则,如尺寸(size="small")、变体(variant="primary")、回调命名(onChangeonClick)。

  • 样式方案:

  1. CSS-in-JS(如 styled-components):适合动态样式,但运行时开销大。
  2. CSS Modules:静态样式,无运行时,支持按需加载。
  3. 纯 CSS + 主题变量(CSS Variables):适合多主题切换。
  • **可访问性(a11y):**支持键盘导航、aria 属性、语义化标签。

  • 测试策略:单元测试(Jest + React Testing Tools)、视觉回归测试(Chromatic)、端到端测试(Cypress)。

  • **发布与版本:**遵循语义化版本(SemVer)。

扩展:

  • 如何实现按需加载?可以使用 babel-plugin-import(编译时替换路径) 或直接导出 ESM 格式,利用 Tree Shaking(打包阶段删除未使用代码)。

3. 谈谈你对原子设计(Atomic Design)的理解

我的回答:

  • 五个层级:原子、分子、组织、模板、页面。
  1. 原子:按钮、输入框。
  2. 分子:输入框 + 按钮(搜索框)。
  3. 组织:导航栏、侧边栏。
  4. 模板:页面架构(无真实数据)。
  5. 页面:填充数据后的完整页面。
  • 优点:复用性强、便于维护、样式统一。
  • 缺点:开发成本高,不适合小型项目,对新手不友好。
  • 在网易云项目中,SongItem 作为可复用的分子/组织组件。

补充:

  • 缺点还包括:过度拆分可能导致组件碎片化;组件层级过深,数据传递复杂。

扩展:

  • 在大型项目中,如何避免原子设计带来的性能问题?(使用 React.memo、懒加载等)

4. 如何实现一个受保护的路由(Protected Route)?

我的回答:

  • 核心逻辑:判断是否登录,未登录则重定向到登录页(<Navigate to="/login" />);已登录则渲染子路由(<Outlet />)或指定组件。
  • 角色权限:根据用户角色动态生成路由或控制侧边栏显示。
  • 保存原路径:将当前路径存入 state,登录后跳回原路径。
  • 异步鉴权:先展示全局 loading,请求接口验证 token 有效后再决定跳转。

补充:

  • 受保护路由通常用包裹组件布局组件 实现:

    javascript 复制代码
    function ProtectedRoute({ children }) {
      const { user } = useAuth();
      if (!user) return <Navigate to="/login" replace />;
      return children;
    }
    // 使用
    <Route path="/dashboard" element={
      <ProtectedRoute>
        <Dashboard />
      </ProtectedRoute>
    } />
  • 使用 replace 属性避免重定向后无法返回。

  • 保存原路径:通过 useLocation 获取当前路径,作为 state 传给登录页;登录成功后从 location.state 中读取并跳转。


5. React 项目中,你是如何做状态管理选型的?

我的回答:

  • 组件内部状态用 useState;小型、静态的跨组件通信用 Context,但要注意性能(配合 React.memo)。
  • 大型项目使用 Redux Toolkit(RTK)。
  • 选型判断标准:状态范围、更新频率、调试需求、团队习惯。

补充:

  • 状态分类:
  1. 本地状态(组件内部):useState、useReducer
  2. 跨组件状态(少量、低频):Context + useReducer
  3. 全局状态(大量、高频、复杂逻辑):Redux / Zustand / MobX
  • **Context 性能问题:**当 Context 值变化时,所有 useContext 的组件都会重渲染。解决方案:拆分 Context(将不变和变化的值分开),或使用 useMemo 稳定 value。
  • **Redux Toolkit 优势:**内置 Immer(简化不可变更新)、Thunk(异步)、DevTools 集成。
  • Zustand 轻量级,适合中小项目,无需 Provider 包裹。

扩展:

  • 什么是"乐观更新"?如何在 RTK 中实现?
  1. 先假设接口一定会成功,直接更新 UI,等请求真正返回后再做最终确认;如果失败再回滚。

6. "状态提升"的优缺点是什么?它的边界在哪里?

我的回答:

  • 定义:将多个子组件共同依赖的状态提升到最近的父组件中,由父组件统一管理并通过 props 传递。
  • 优点:数据来源清晰(单一数据源),便于调试和维护。
  • 缺点:可能导致 props drilling(逐层传递),造成不必要的重渲染,需要配合 React.memo 或 useMemo 优化。
  • 边界:提升到最近的共同父组件即可,不必再向上;当状态需要被较远层级的组件使用时,应考虑全局状态管理(Context / Redux)。

补充:

  • **优点:**状态逻辑集中,避免重复;符合 React 单向数据流;易于实现时间旅行调试
  • **缺点:**中间组件可能被迫接收无关 props,增加耦合;重构时可能需要移动大量状态

今日知识点总结

| 问题 | 核心要点 |
| 错误边界 | 类组件 getDerivedStateFromError + componentDidCatch;不能捕获事件/异步错误;React 19 内置 ErrorBoundary |
| 组件库设计 | API 一致性、TypeScript、文档、按需加载、可访问性、测试、版本管理 |
| 原子设计 | 原子 → 分子 → 组织 → 模板 → 页面;优点复用性,缺点开发成本高 |
| 受保护路由 | 基于登录状态重定向;支持角色权限、保存原路径、异步鉴权 loading |
| 状态管理选型 | 本地 useState → 少量跨组件 Context → 复杂全局 RTK/Zustand;注意 Context 性能 |

状态提升 提升到最近共同父组件;优点单一数据源,缺点 props drilling;超出范围用全局状态
相关推荐
FmZero2 小时前
后端全栈路线(9小时前端速成)
前端·vscode·学习
万世浮华戏骨2 小时前
Web 后端 Python 基础安全
前端·python·安全
Dontla2 小时前
JWT认证流程(JSON Web Token)
前端·数据库·json
余人于RenYu7 小时前
Claude + Figma MCP
前端·ui·ai·figma
杨艺韬10 小时前
vite内核解析-第2章 架构总览
前端·vite
我是伪码农11 小时前
外卖餐具智能推荐
linux·服务器·前端
2401_8858850411 小时前
营销推广短信接口集成:结合营销策略实现的API接口动态变量填充方案
前端·python
小李子呢021111 小时前
前端八股性能优化(2)---回流(重排)和重绘
前端·javascript
程序员buddha11 小时前
深入理解ES6 Promise
前端·ecmascript·es6