阅读视图

发现新文章,点击刷新页面。

React 日历组件完全指南:从网格生成到农历转换

本文详细介绍如何从零实现一个功能完整的 React 日历组件,包括日历网格生成、农历显示和月份切换功能。

前言

在开发排班管理系统时,我们需要实现一个功能完整的日历组件。这个组件不仅要显示标准的月历网格,还要支持农历显示和流畅的月份切换。经过实践,我总结了一套完整的实现方案,适用于任何 React 项目。

一、日历网格生成

1.1 核心需求

一个标准的月历网格需要满足以下要求:

  • 显示当前月份的所有日期
  • 补齐上月末尾的日期(填充第一周)
  • 补齐下月开头的日期(填充最后一周)
  • 总是显示完整的 6 周(42 天)
  • 周日为每周的第一天

1.2 实现思路

我们使用 date-fns 库来处理日期计算,整个算法分为三个步骤:

// DateService.ts
getMonthCalendarGrid(date: Date): Date[] {
  // Step 1: 获取月份的起止日期
  const monthStart = startOfMonth(date);
  const monthEnd = endOfMonth(date);
  
  // Step 2: 扩展到完整的周
  const calendarStart = startOfWeek(monthStart, { weekStartsOn: 0 });
  const calendarEnd = endOfWeek(monthEnd, { weekStartsOn: 0 });
  
  // Step 3: 生成日期数组
  return eachDayOfInterval({
    start: calendarStart,
    end: calendarEnd
  });
}

关键点解析:

  1. 获取月份边界:使用 startOfMonthendOfMonth 获取当月的第一天和最后一天
  2. 扩展到完整周:使用 startOfWeekendOfWeek 确保日历从周日开始,到周六结束
  3. 生成连续日期:使用 eachDayOfInterval 生成两个日期之间的所有日期

1.3 实际案例

以 2024 年 11 月为例:

输入:new Date(2024, 10, 15)  // 2024-11-15

Step 1: 月份边界
  monthStart = 2024-11-01 (周五)
  monthEnd   = 2024-11-30 (周六)

Step 2: 扩展到周
  calendarStart = 2024-10-27 (周日)
  calendarEnd   = 2024-11-30 (周六)

Step 3: 生成日期
  共 35 天 (5周)

渲染结果:
日  一  二  三  四  五  六
27  28  29  30  31   1   2   ← 10月27-31 + 11月1-2
 3   4   5   6   7   8   9
10  11  12  13  14  15  16
17  18  19  20  21  22  23
24  25  26  27  28  29  30

1.4 为什么是 6 周?

大多数月份只需要 5 周(35 天)就能显示完整,但某些特殊情况需要 6 周(42 天)。

需要 6 周的条件:

  • 月份有 31 天
  • 月初是周六(需要补充前面 6 天)

为了保持布局一致性,我们统一使用 6 周布局,这样月份切换时高度不变,动画过渡更流畅。

二、农历(阴历)显示

2.1 实现原理

农历转换使用预定义数据表 + 算法计算的方式,无需外部依赖,支持 1900-2100 年。

2.2 数据结构

农历信息表

private static lunarInfo = [
  0x04bd8, 0x04ae0, 0x0a570, 0x054d5, 0x0d260,  // 1900-1904
  // ... 共 201 个元素(1900-2100年)
];

每个十六进制数编码了一年的农历信息:

0x04bd8 的二进制表示:
0000 0100 1011 1101 1000

解析:
├─ 后 4 位 (1000 = 8):闰月位置(8月)
├─ 第 5 位 (1):闰月天数(1=30天,0=29天)
└─ 第 6-17 位:每月天数(1=30天,0=29天)

农历日期文本

private static lunarDays = [
  '初一', '初二', '初三', ..., '廿九', '三十'
];

2.3 转换算法

公历转农历分为四个步骤:

Step 1: 计算与基准日期的天数差
  基准:1900-01-31(农历1900年正月初一)
  
Step 2: 从1900年开始,逐年累减天数,确定农历年份

Step 3: 逐月累减天数,确定农历月份(处理闰月)

Step 4: 剩余天数 + 1 = 农历日期

2.4 实际案例

以 2024-11-24 为例:

Step 1: 天数差
  (2024-11-24 - 1900-01-31) = 45590 

Step 2: 确定农历年
  1900年:354天,剩余 45236
  1901年:354天,剩余 44882
  ...
  2023年:384天,剩余 324
   农历2024年

Step 3: 确定农历月
  正月:30天,剩余 294
  二月:29天,剩余 265
  ...
  十月:30天,剩余 29
   农历十月

Step 4: 确定农历日
  29 + 1 = 30
   三十

结果:2024-11-24 = 农历2024年十月三十

2.5 使用方法

// 获取农历日期文本
const lunarText = LunarUtil.getLunarDateText(new Date(2024, 10, 24));
console.log(lunarText);  // 输出:三十

// 在日历中应用
<div className="day-cell">
  <div className="day-text">{date.getDate()}</div>
  <div className="lunar-text">
    {LunarUtil.getLunarDateText(date)}
  </div>
</div>

渲染效果:

┌─────┬─────┬─────┬─────┬─────┬─────┬─────┐
│ 24  │ 25  │ 26  │ 27  │ 28  │ 29  │ 30  │
│廿四 │廿五 │廿六 │廿七 │廿八 │廿九 │三十 │
└─────┴─────┴─────┴─────┴─────┴─────┴─────┘

三、月份切换功能

3.1 核心思路

月份切换的本质是改变当前显示的月份,然后重新生成日历网格。

核心要素:

  1. 维护一个 currentDate 状态
  2. 提供切换方法(上一月/下一月)
  3. 根据 currentDate 重新生成日历网格

3.2 状态管理

const [currentDate, setCurrentDate] = useState(new Date());

currentDate 的作用:

  • 决定显示哪个月份
  • 作为生成日历网格的输入

3.3 切换方法

使用 date-fns 实现月份切换:

import { addMonths, subMonths } from 'date-fns';

// 下一个月
const goToNextMonth = () => {
  setCurrentDate(prevDate => addMonths(prevDate, 1));
};

// 上一个月
const goToPrevMonth = () => {
  setCurrentDate(prevDate => subMonths(prevDate, 1));
};

// 通用方法
const handleMonthChange = (direction: 'next' | 'prev') => {
  setCurrentDate(prevDate => {
    return direction === 'next' 
      ? addMonths(prevDate, 1)
      : subMonths(prevDate, 1);
  });
};

3.4 自动处理边界

JavaScript Date 构造函数会自动处理月份溢出:

// 12月 → 1月(跨年)
new Date(2024, 12, 1)  // 自动变为 2025-01-01

// 1月 → 12月(跨年)
new Date(2024, -1, 1)  // 自动变为 2023-12-01

3.5 响应式更新

使用 useMemo 实现响应式更新:

const MonthView: React.FC<MonthViewProps> = ({ currentDate }) => {
  const currentMonthDates = useMemo(() => {
    return DateService.getMonthCalendarGrid(currentDate);
  }, [currentDate]);  // 依赖 currentDate
  
  // currentDate 变化 → useMemo 重新计算 → 生成新的日历网格
};

3.6 完整数据流

用户点击"下一月"
  ↓
setCurrentDate(新月份)
  ↓
useMemo 重新计算
  ↓
生成新的日历网格
  ↓
渲染新月份

四、完整实现

4.1 日历组件

import React, { useState, useMemo } from 'react';
import { addMonths, subMonths, format } from 'date-fns';

function Calendar() {
  const [currentDate, setCurrentDate] = useState(new Date());
  
  // 切换月份
  const handleMonthChange = (direction: 'next' | 'prev') => {
    setCurrentDate(prev => {
      return direction === 'next' 
        ? addMonths(prev, 1)
        : subMonths(prev, 1);
    });
  };
  
  // 生成日历网格
  const dates = useMemo(() => {
    return generateCalendarGrid(currentDate);
  }, [currentDate]);
  
  return (
    <div className="calendar">
      {/* 标题 */}
      <h2>{format(currentDate, 'yyyy年MM月')}</h2>
      
      {/* 切换按钮 */}
      <button onClick={() => handleMonthChange('prev')}>上一月</button>
      <button onClick={() => handleMonthChange('next')}>下一月</button>
      
      {/* 日历网格 */}
      <CalendarGrid dates={dates} />
    </div>
  );
}

4.2 渲染网格

function CalendarGrid({ dates }) {
  // 分组为周
  const weeks = [];
  for (let i = 0; i < dates.length; i += 7) {
    weeks.push(dates.slice(i, i + 7));
  }
  
  return (
    <div className="calendar-grid">
      {/* 星期头部 */}
      <div className="week-header">
        {['日', '一', '二', '三', '四', '五', '六'].map(day => (
          <div key={day} className="week-day">{day}</div>
        ))}
      </div>
      
      {/* 日历网格 */}
      {weeks.map((week, weekIndex) => (
        <div key={weekIndex} className="week-row">
          {week.map((date, dayIndex) => (
            <div key={dayIndex} className="day-cell">
              {/* 公历日期 */}
              <div className="day-text">{date.getDate()}</div>
              
              {/* 农历日期 */}
              <div className="lunar-text">
                {LunarUtil.getLunarDateText(date)}
              </div>
            </div>
          ))}
        </div>
      ))}
    </div>
  );
}

五、性能优化

5.1 使用 useMemo 缓存计算

// 缓存日历网格
const dates = useMemo(() => {
  return generateCalendarGrid(currentDate);
}, [currentDate]);

5.2 使用 useCallback 缓存回调

const handleDatePress = useCallback((date: Date) => {
  onDatePress(date);
}, [onDatePress]);

5.3 使用 React.memo 避免无效渲染

export default React.memo(MonthView);

六、关键要点总结

6.1 日历网格生成

核心 API:

  • startOfMonth / endOfMonth - 获取月份边界
  • startOfWeek / endOfWeek - 扩展到完整周
  • eachDayOfInterval - 生成连续日期

数据结构:

Date[] (35-42个元素)
  ↓ 分组
Date[][] (5-6个数组,每个7个元素)
  ↓ 渲染
6行 × 7列的网格

6.2 农历转换

核心算法:

  1. 计算天数差 - 与基准日期(1900-01-31)的差值
  2. 确定农历年 - 逐年累减天数
  3. 确定农历月 - 逐月累减天数,处理闰月
  4. 确定农历日 - 剩余天数 + 1

数据结构:

  • 预定义表 - 201个十六进制数(1900-2100年)
  • 位运算 - 高效解析农历信息
  • 文本数组 - 30个农历日期名称

6.3 月份切换

核心流程:

状态变化 → 网格重新生成 → 数据重新加载 → 组件重新渲染

关键技术:

  • useState - 状态管理
  • useMemo - 缓存计算结果
  • useEffect - 监听变化,自动加载数据
  • JavaScript Date - 自动处理月份边界

七、总结

通过本文,我们实现了一个功能完整的 React 日历组件,包括:

✅ 标准的月历网格生成(支持 5-6 周布局) ✅ 农历显示(支持 1900-2100 年) ✅ 流畅的月份切换(自动处理跨年) ✅ 响应式数据更新(状态驱动) ✅ 性能优化(useMemo、useCallback、React.memo)

核心思想是利用 date-fns 处理日期计算,使用 React Hooks 实现响应式更新,通过预定义数据表实现农历转换。整个实现简洁高效,易于维护和扩展。

这套方案不仅适用于 Web 应用,也可以轻松移植到 React Native 等其他 React 生态项目中。

希望这篇文章能帮助你理解日历组件的实现原理,并应用到自己的项目中。


相关资源:

React Compiler 完全指南:自动化性能优化的未来

React Compiler 完全指南:自动化性能优化的未来

一、什么是 React Compiler?

React Compiler(曾用名 React Forget)是 React 团队在 2025 年 10 月发布的稳定版 1.0 构建时优化工具。它不是一个传统意义上的"编译器"(将代码转为字节码),而是一个静态分析优化器,能够自动为你的 React 组件插入记忆化(memoization),从而消除不必要的重新渲染。

核心定位:它让你可以忘记(Forget手动使用 useMemouseCallbackReact.memo,专注于编写声明式代码,性能优化由编译器在构建时自动完成。


二、解决了什么痛点?为什么需要它?

传统的性能优化困境

在 React Compiler 出现前,开发者面临两难选择

  1. 性能 vs 代码简洁性:为了防止不必要的重渲染,必须用 useMemo/useCallback 包裹一切,导致代码臃肿、难以维护
  2. 优化不一致性:不同开发者对何时使用记忆化的判断标准不同,导致性能优化碎片化
  3. 高认知负荷:维护复杂的依赖数组 deps 容易出错,遗漏依赖导致 bug,多余依赖导致性能浪费
  4. “死亡千刀” :大型项目中,性能相关的样板代码可能占代码总量的 20-30%

React 团队的观察:即使开发者知道需要优化,手动优化的覆盖率通常不到 30%。绝大多数组件本可以被记忆化,但出于"懒惰"或"风险考虑"被遗漏了。


三、工作原理:如何自动优化?

React Compiler 的技术架构经历了近十年的演进:

1. 核心架构:基于 HIR 的静态分析

编译器并非直接操作 AST,而是将代码降维到高级中间表示(High-Level Intermediate Representation, HIR)的控制流图(CFG)。这让它能够:

  • 精确分析数据流:追踪每个变量的赋值、引用和传递路径
  • 理解可变性:识别哪些数据可能被修改,哪些是纯函数
  • 细粒度记忆化:可以条件性地记忆化值(这是手动 useMemo 无法做到的)

2. React 规则编码

编译器内置了 React 核心规则的验证通道:

  • 幂等性:组件在相同输入下必须返回相同输出
  • 不可变性:props 和 state 值不能被直接修改
  • 副作用隔离:识别不纯的代码并跳过优化

安全策略:遇到不符合规则的代码时,编译器不会冒险优化,而是跳过该部分,确保不引入 bug。

3. 验证与诊断

编译器通过 eslint-plugin-react-hooks 暴露诊断信息。当你违反 React 规则时,它会提示你潜在的 bug,这甚至比优化本身更有价值。

// 编译器能检测并跳过此场景的优化
function BadComponent(props) {
  props.value = 123; // ❌ 直接修改 props
  return <div>{props.value}</div>;
}

四、核心特性与优势

1. 自动记忆化(Automatic Memoization)

编译器自动在组件、hook、计算值层面插入等效的 useMemo/useCallback/React.memo,覆盖率接近 100%

2. 静态分析 & 类型推断

在构建时分析组件的依赖关系,精确判断何时需要重新渲染。

3. 现有代码库零侵入

无需重写组件,编译器无缝集成到构建流程(Babel/Vite/Next.js),对开发者透明。

4. 性能提升显著

  • Meta Quest Store 案例:加载和导航时间提升 12% ,特定交互速度提升 2.5 倍
  • Sanity Studio:大型 CMS 应用性能提升 30%
  • 内存占用:优化后未增加内存消耗

5. 减少样板代码

代码库清晰度提升,开发者可以删除大量性能相关的"噪声代码"。


五、如何开始使用?

前置条件

  1. 启用 React Strict Mode
  2. 配置 ESLint React Hooks 插件eslint-plugin-react-hooks

快速开始

React Compiler 1.0 已与主流框架集成:

Next.js(v15+)
# 创建新项目时启用
npx create-next-app@latest --react-compiler
Vite
npm install babel-plugin-react-compiler
# 在 vite.config.js 中配置
Expo(React Native)
npx create-expo-app --template with-react-compiler
自定义 Babel 配置
{
  "plugins": [
    ["babel-plugin-react-compiler", { "target": "18" }]
  ]
}

渐进式采用

React Compiler 支持逐文件逐组件启用,风险可控:

// 在组件顶部添加指令禁用编译器
'use no memo';

function LegacyComponent() {
  // 这个组件不会被优化
}

六、与 Webpack Compiler 的区别

有人容易混淆 React CompilerWebpack Compiler

维度 React Compiler Webpack Compiler
定位 React 应用性能优化 模块打包与构建
工作层 源码级(AST/JSX) 模块级(依赖图)
核心功能 自动记忆化、静态分析 代码拆分、Tree Shaking、HMR
输出 优化后的 React 代码 打包后的 bundle
关系 互补(React Compiler 在 Webpack 的 loader 阶段工作)

协同工作:React Compiler 作为 Babel 插件,在 Webpack 处理 JS/JSX 之前优化代码。


七、未来展望

React Compiler 1.0 只是开始,路线图包括:

  • 更智能的优化:在构建时预计算常量、优化数据获取模式
  • React DevTools 集成:可视化显示哪些组件被编译器优化
  • Concurrent React 深度整合:让并发渲染更智能地决定更新优先级
  • 生态系统普及:Next.js、Expo、Vite 将默认启用编译器

八、常见问题解答

Q1:会完全取代 useMemo/useCallback 吗?
A:绝大多数场景可以,但手动优化仍是逃生舱,用于极端性能需求。

Q2:会增加构建时间吗?
A:会,但增量构建和缓存机制让影响可控(通常 +10%~20%)。

Q3:会改变代码行为吗?
A:不会,编译器只进行安全的记忆化,不改变逻辑。

Q4:对旧版本 React 兼容吗?
A:支持 React 17+,但推荐 React 18+ 以获得最佳效果。


九、总结

React Compiler 通过自动化消除了一代人的性能优化负担,让开发者回归本质:编写清晰、声明式的 UI 代码。正如 React 团队所说,这是未来十年 React 的新基础

现在就开始尝试:在你的项目中启用它,运行 npm run build,然后享受零手动优化的快感吧!

参考资料

官方来源

  1. React Compiler 1.0发布公告
    zh-hans.react.dev/blog/2025/1…
  2. React Compiler官方文档
    zh-hans.react.dev/learn/react…
  3. React Compiler安装指南
    zh-hans.react.dev/learn/react…
  4. React Compiler配置参考
    zh-hans.react.dev/reference/r…

PerformanceObserver 性能条目类型(Entry Types)

PerformanceObserver 是 Web 性能监测的核心 API,通过指定 entryTypes 可以观察不同类型的性能指标。以下是所有支持的条目类型及其详细说明。


一、核心 Entry Types 列表

1.  'navigation'  — 页面导航性能

说明:测量主文档的导航和加载时间,提供完整的页面加载生命周期数据。

常用指标

  • entry.duration:总加载时间
  • entry.domContentLoadedEventEnd - entry.domContentLoadedEventStart:DOM 解析时间
  • entry.loadEventEnd - entry.loadEventStart:资源加载时间

使用场景:分析页面整体加载性能,计算 DNS、TCP、TTFB 等关键指标

2.  'resource'  — 资源加载性能

说明:测量页面上所有资源的加载时间,包括图片、CSS、JavaScript、XHR/Fetch 请求等。

关键属性

  • entry.name:资源 URL
  • entry.initiatorType:资源类型(img, script, link, xmlhttprequest, fetch
  • entry.duration:加载耗时
  • entry.decodedBodySize:资源大小

使用场景:定位慢资源、监控 CDN 效果、优化资源加载策略

3.  'paint'  — 渲染性能

说明:测量关键渲染时间点,主要用于 FCP(首次内容绘制)。

事件名称

  • entry.name === 'first-contentful-paint':FCP 时间

使用场景:Core Web Vitals 核心指标监控

4.  'largest-contentful-paint'  — 最大内容绘制

说明:作为独立条目类型提供更精确的 LCP 数据,包含详细的元素信息。

关键属性

  • entry.startTime:LCP 发生时间
  • entry.element:触发 LCP 的 DOM 元素
  • entry.url:图片资源的 URL(如适用)

使用场景:精准定位影响 LCP 的元素,优化最大内容加载

const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.log(' LCP 详情:', {
      time: entry.startTime.toFixed(2) + 'ms',
      element: entry.element?.tagName,
      url: entry.url
    });
  }
});
observer.observe({ entryTypes: ['largest-contentful-paint'], buffered: true });

5.  'first-input'  — 首次输入延迟

说明:测量用户首次交互(点击、触摸、按键)到浏览器响应的延迟时间,是 Core Web Vitals 核心指标。

关键属性

  • entry.name:交互类型(click, pointerdown, keydown
  • entry.duration:总延迟时间
  • entry.processingStart / entry.processingEnd:事件处理时间

使用场景:评估用户对应用响应性的第一印象

const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.log('首次交互延迟:', {
      type: entry.name,
      delay: entry.duration + 'ms'
    });
  }
});
observer.observe({ entryTypes: ['first-input'], buffered: true });

6.  'layout-shift'  — 布局偏移(CLS)

说明:测量页面元素的意外移动,用于计算 Cumulative Layout Shift (CLS)

关键属性

  • entry.value:单次布局偏移得分
  • entry.hadRecentInput:是否由用户输入触发(通常只关注 false 的情况)

使用场景:提升视觉稳定性,优化用户体验

7.  'event'  — 用户事件延迟

说明:测量所有用户交互事件(点击、键盘输入)的延迟时间。

关键属性

  • entry.name:事件类型(click, keydown 等)
  • entry.duration:事件处理耗时

使用场景:监控交互响应性,发现事件处理瓶颈

8.  'longtask'  — 长任务监控

说明:识别主线程上耗时超过 50 毫秒的任务,这些任务会阻塞用户交互。

关键属性

  • entry.duration:任务耗时
  • entry.attribution:导致长任务的代码信息

使用场景:发现 JavaScript 性能瓶颈,优化主线程执行

9.  'long-animation-frame'  — 长动画帧(LoAF)

说明新一代长任务监控,比 'longtask' 更细粒度,提供完整的调用栈和脚本归因。

关键属性

  • entry.duration:帧耗时
  • entry.scripts:详细的脚本执行信息数组

使用场景:精确诊断阻塞动画和交互的脚本

const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.log('长动画帧:', {
      duration: entry.duration + 'ms',
      scripts: entry.scripts.map(s => ({
        source: s.sourceURL,
        function: s.sourceFunctionName,
        duration: s.duration + 'ms'
      }))
    });
  }
});
observer.observe({ entryTypes: ['long-animation-frame'], buffered: true });

10.  'element'  — 元素级性能

说明:观察特定 DOM 元素的渲染时间,用于更精确的 LCP 测量。

使用要求:元素需添加 elementtiming 属性

<img src="hero.jpg" elementtiming="hero-image" />

使用场景:监控关键业务元素的渲染性能

11.  'mark'  — 自定义时间戳标记

说明:通过 performance.mark() 创建命名时间戳,标记业务关键节点。

使用方式

performance.mark('user_login_start');
// ... 执行代码
performance.mark('user_login_end');

使用场景:自定义业务逻辑性能监控,如组件初始化时间、API 调用耗时等

const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntriesByType('mark')) {
    console.log('标记点:', entry.name, entry.startTime + 'ms');
  }
});
observer.observe({ entryTypes: ['mark'], buffered: true });

12.  'measure'  — 自定义时间区间测量

说明:通过 performance.measure() 计算两个 mark 之间的时间差,是业务性能监控的黄金标准。

使用方式

performance.measure('login_duration', 'user_login_start', 'user_login_end');

使用场景:精确测量业务操作耗时,生成自定义性能指标

const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntriesByType('measure')) {
    console.log('测量区间:', entry.name, entry.duration + 'ms');
  }
});
observer.observe({ entryTypes: ['measure'], buffered: true });

13.  'visibility-state'  — 页面可见性变化

说明:监控页面从可见到隐藏、从隐藏到可见的状态转换,对移动端性能优化至关重要。

关键属性

  • entry.prevState:之前的状态(visible, hidden, prerender
  • entry.currState:当前的状态

使用场景:页面隐藏时暂停非关键任务,节省 CPU 和电量

const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.log('可见性变化:', {
      from: entry.prevState,
      to: entry.currState
    });
    if (entry.currState === 'hidden') {
      pauseBackgroundTasks();
    }
  }
});
observer.observe({ entryTypes: ['visibility-state'], buffered: true });

二、浏览器兼容性说明

动态检查支持类型

通过 PerformanceObserver.supportedEntryTypes 获取当前浏览器支持的类型:

console.log(PerformanceObserver.supportedEntryTypes);
// 现代浏览器输出 (13):
// ['element', 'event', 'first-input', 'largest-contentful-paint', 
//  'layout-shift', 'long-animation-frame', 'longtask', 'mark', 
//  'measure', 'navigation', 'paint', 'resource', 'visibility-state']

注意:该属性是静态只读属性,返回数组reactnative.dev/docs/next/g…

兼容性差异

  • 现代浏览器 (Chrome 120+, Edge 120+):支持全部 13 种类型
  • Safari:支持基础类型,对新类型(如 long-animation-frame)支持较慢
  • Firefox:支持核心类型,部分实验性类型不支持
  • IE:完全不支持 PerformanceObserver

推荐做法

const supportedTypes = PerformanceObserver.supportedEntryTypes || [];
const desiredTypes = [
  'navigation', 'resource', 'paint', 'first-input', 'layout-shift',
  'longtask', 'event', 'largest-contentful-paint', 'long-animation-frame'
];
const entryTypes = desiredTypes.filter(type => supportedTypes.includes(type));

if (entryTypes.length > 0) {
  observer.observe({ entryTypes, buffered: true });
}

三、完整使用示例

监控多种性能指标

// 创建观察者实例
const observer = new PerformanceObserver((entryList) => {
  const entries = entryList.getEntries();
  
  entries.forEach(entry => {
    console.log(`类型: ${entry.entryType}, 名称: ${entry.name}, 耗时: ${entry.duration}ms`);
    
    // 根据类型分别处理
    switch(entry.entryType) {
      case 'navigation':
        console.log('页面加载完成:', entry.duration + 'ms');
        break;
      case 'resource':
        if (entry.duration > 200) {
          console.warn('慢资源:', entry.name);
        }
        break;
      case 'largest-contentful-paint':
        console.log('LCP:', entry.startTime.toFixed(2) + 'ms');
        break;
      case 'first-input':
        console.log('🖱️ 首次交互延迟:', entry.duration + 'ms');
        break;
      case 'layout-shift':
        if (!entry.hadRecentInput) {
          console.log('CLS 累积:', entry.value);
        }
        break;
      case 'long-animation-frame':
        console.error('长动画帧:', entry.duration + 'ms');
        break;
      case 'visibility-state':
        console.log('可见性变化:', entry.currState);
        break;
    }
  });
});

// 开始观察(推荐设置 buffered: true 获取历史数据)
observer.observe({
  entryTypes: [
    'navigation',
    'resource',
    'largest-contentful-paint',
    'first-input',
    'layout-shift',
    'long-animation-frame',
    'visibility-state'
  ],
  buffered: true
});

关键点

  • buffered: true :收集 observe() 调用之前已发生的性能条目,防止遗漏
  • disconnect() :不再需要时调用,释放资源

五、总结矩阵

# Entry Type 主要指标 典型阈值 所属标准 监控场景
1 navigation 页面加载时间 < 3s Navigation Timing 整体加载性能
2 resource 资源加载耗时 < 200ms Resource Timing 慢资源定位
3 paint FCP < 1.8s Paint Timing 首次渲染
4 largest-contentful-paint LCP < 2.5s LCP API 最大内容渲染
5 first-input FID < 100ms Event Timing 首次交互响应
6 layout-shift CLS < 0.1 Layout Instability 视觉稳定性
7 event 交互延迟 < 100ms Event Timing 所有事件延迟
8 longtask 主线程阻塞 > 50ms Long Tasks API 卡顿检测
9 long-animation-frame 长动画帧阻塞 > 50ms LoAF API 精细化卡顿分析
10 element 元素渲染时间 业务自定义 Element Timing 关键元素监控
11 mark 自定义时间戳 - User Timing 业务埋点
12 measure 自定义时间区间 业务自定义 User Timing 区间测量
13 visibility-state 页面可见性变化 - Page Visibility 节能优化

通过合理组合这些 entry types,可以构建全面的前端性能监控体系,为优化提供数据支撑

从零构建 React Native 导航体系-React Navigation

一、React Navigation v7 介绍

React Navigation v7 在架构和性能上都有显著优化:

  • 原生性能提升:推荐使用 @react-navigation/native-stack,利用原生导航控制器实现更流畅的动画
  • 模块化设计:核心包与导航器分离,按需安装减少包体积
  • 统一的配置 API:无论是 Stack、Tab 还是 Drawer,配置方式更加一致
  • 更好的 TypeScript 支持:内置类型推断,开发体验更友好

二、环境搭建:三步完成安装配置

步骤 1:安装核心依赖

npm install @react-navigation/native

步骤 2:添加原生能力库

这两个库是性能优化的关键,必须安装:

npm install react-native-screens react-native-safe-area-context

iOS 用户注意:安装后需执行 npx pod-install ios 链接原生代码。

步骤 3:按需安装导航器

# 堆栈导航(最常用)
npm install @react-navigation/native-stack

# 底部标签导航
npm install @react-navigation/bottom-tabs

# 抽屉导航
npm install @react-navigation/drawer

三、核心基础:Stack Navigator 实战

Stack Navigator 是移动应用的基石,它管理页面的堆栈历史,支持手势返回和切换动画。

3.1 创建你的第一个导航器

屏幕组件screens/Home.jsx):

import { SafeAreaView, StyleSheet, Text, Pressable } from 'react-native';

const Home = ({ navigation }) => {
    return (
        <SafeAreaView style={styles.container}>
            <Text style={styles.title}>Home Screen</Text>
            <Pressable 
                onPress={() => navigation.navigate('Profile')}
                style={styles.button}
            >
                <Text style={styles.buttonText}>→ 跳转到 Profile</Text>
            </Pressable>
        </SafeAreaView>
    );
}

const styles = StyleSheet.create({
    container: { flex: 1, justifyContent: 'center', alignItems: 'center' },
    title: { fontSize: 24, marginBottom: 20 },
    button: { 
        backgroundColor: '#4BC1D2', 
        padding: 12, 
        borderRadius: 8 
    },
    buttonText: { color: '#fff', fontWeight: 'bold' }
});

根组件配置App.js):

import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import Home from './screens/Home';
import Profile from './screens/Profile';

const Stack = createNativeStackNavigator();

export default function App() {
  return (
    <NavigationContainer>
      <Stack.Navigator initialRouteName="Home">
        <Stack.Screen name="Home" component={Home} />
        <Stack.Screen 
          name="Profile" 
          component={Profile}
          options={{ title: '个人中心' }}  // 自定义标题
        />
      </Stack.Navigator>
    </NavigationContainer>
  );
}

关键点解析

  • NavigationContainer :整个应用的导航大脑,管理状态与深度链接
  • navigation.navigate('RouteName') :最核心的跳转方法,自动管理堆栈
  • 所有屏幕自动接收 navigation 属性 :无需手动传递

四、丰富导航体验:Tab 与 Drawer

4.1 Bottom Tabs:主流 App 的首选

底部标签栏让用户能快速切换核心功能模块:

import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import Ionicons from 'react-native-vector-icons/Ionicons';

const Tab = createBottomTabNavigator();

function AppTabs() {
  return (
    <Tab.Navigator
      screenOptions={({ route }) => ({
        // 动态图标配置
        tabBarIcon: ({ focused, color, size }) => {
          const iconMap = {
            Home: focused ? 'ios-home' : 'ios-home-outline',
            Settings: focused ? 'ios-settings' : 'ios-settings-outline',
          };
          return <Ionicons name={iconMap[route.name]} size={size} color={color} />;
        },
        tabBarActiveTintColor: '#4BC1D2',  // 激活颜色
        tabBarInactiveTintColor: '#999',     // 非激活颜色
        headerShown: false,  // 隐藏堆栈标题,由内部导航器管理
      })}
    >
      <Tab.Screen name="Home" component={HomeStackScreen} />
      <Tab.Screen name="Settings" component={SettingsScreen} />
    </Tab.Navigator>
  );
}

4.2 Drawer:侧边菜单的优雅实现

适合功能模块较多、需要层级导航的应用:

import { createDrawerNavigator } from '@react-navigation/drawer';

const Drawer = createDrawerNavigator();

function AppDrawer() {
  return (
    <Drawer.Navigator
      drawerPosition="left"  // 抽屉从左侧滑出
      drawerStyle={{ width: 280 }}  // 自定义宽度
      drawerContentOptions={{
        activeTintColor: '#4BC1D2',
        labelStyle: { fontSize: 16 },
      }}
    >
      <Drawer.Screen name="Main" component={AppTabs} />
      <Drawer.Screen name="Profile" component={ProfileScreen} />
    </Drawer.Navigator>
  );
}

五、导航器组合:构建复杂应用结构

真实场景通常是导航器嵌套:标签导航包含堆栈导航,抽屉导航包裹标签导航。

// 1. 首页模块的堆栈导航
function HomeStackScreen() {
  return (
    <Stack.Navigator>
      <Stack.Screen name="HomeMain" component={HomeScreen} />
      <Stack.Screen name="Details" component={DetailsScreen} />
    </Stack.Navigator>
  );
}

// 2. 设置模块的堆栈导航
function SettingsStackScreen() {
  return (
    <Stack.Navigator>
      <Stack.Screen name="SettingsMain" component={SettingsScreen} />
      <Stack.Screen name="Account" component={AccountScreen} />
    </Stack.Navigator>
  );
}

// 3. 底部标签导航组合两个堆栈
function AppTabs() {
  return (
    <Tab.Navigator>
      <Tab.Screen name="HomeTab" component={HomeStackScreen} />
      <Tab.Screen name="SettingsTab" component={SettingsStackScreen} />
    </Tab.Navigator>
  );
}

// 4. 根组件:抽屉包裹标签导航
export default function App() {
  return (
    <NavigationContainer>
      <Drawer.Navigator>
        <Drawer.Screen name="App" component={AppTabs} />
      </Drawer.Navigator>
    </NavigationContainer>
  );
}

设计模式解读

  • 模块化:每个功能模块独立管理自己的页面堆栈
  • 职责分离:Tab 负责一级导航,Stack 负责二级页面流转
  • 用户体验:保持底部标签常驻,堆栈跳转不影响全局导航

六、页面通信:参数传递与返回处理

6.1 向下一个页面传参

// 发送方:HomeScreen
navigation.navigate('Details', {
  itemId: 86,
  itemName: 'React Navigation 指南',
  timestamp: Date.now()
});
// 接收方:DetailsScreen
function DetailsScreen({ route, navigation }) {
  const { itemId, itemName } = route.params;
  
  return (
    <View>
      <Text>商品ID: {itemId}</Text>
      <Text>商品名称: {itemName}</Text>
    </View>
  );
}

6.2 返回时携带数据

// DetailsScreen 返回时传参
navigation.navigate('Home', { result: 'success' });

// HomeScreen 接收返回参数
useEffect(() => {
  if (route.params?.result) {
    console.log('操作结果:', route.params.result);
  }
}, [route.params]);

6.3 useNavigation Hook:在深层组件中导航

当组件未直接接收 navigation prop 时:

import { useNavigation } from '@react-navigation/native';

function DeepChildComponent() {
  const navigation = useNavigation();
  
  return (
    <Button 
      title="返回首页" 
      onPress={() => navigation.navigate('Home')} 
    />
  );
}

七、进阶定制:让导航更贴合你的 App

7.1 自定义头部样式

<Stack.Screen 
  name="Details" 
  component={DetailsScreen}
  options={{
    headerStyle: { backgroundColor: '#f4511e' },
    headerTintColor: '#fff',  // 返回按钮和标题颜色
    headerTitleStyle: { fontWeight: 'bold' },
    headerRight: () => (
      <Button onPress={() => alert('分享')} title="分享" />
    ),
  }}
/>

7.2 深度链接(Deep Linking)

让外部链接能直接打开 App 内特定页面:

import { Linking } from 'react-native';

const linking = {
  prefixes: ['myapp://', 'https://myapp.com'],
  config: {
    screens: {
      Home: 'home',
      Details: 'details/:itemId',  // 动态参数
      Profile: 'profile',
    },
  },
};

function App() {
  return (
    <NavigationContainer linking={linking}>
      <Stack.Navigator>
        {/* ... */}
      </Stack.Navigator>
    </NavigationContainer>
  );
}

7.3 权限路由守卫

在渲染前进行权限检查:

function RequireAuth({ children }) {
  const { hasToken } = useAuth();
  const location = useLocation();
  
  const publicPaths = ["/login", "/register"];
  const isPublic = publicPaths.includes(location.pathname);
  
  if (!hasToken && !isPublic) {
    return <Navigate to="/login" replace state={{ from: location }} />;
  }
  
  return children;
}

八、版本升级指南与兼容性

v7 与旧版本的主要差异

表格

复制

特性 v6/v7 推荐 旧版本 (v4/v5) 说明
堆栈导航器 @react-navigation/native-stack react-navigation-stack 原生性能,API 更简洁
Tab 导航器 @react-navigation/bottom-tabs react-navigation-tabs 独立成包,配置更灵活
Drawer 导航器 @react-navigation/drawer react-navigation-drawer 手势体验优化

提示@react-navigation/stack 已逐步被 @react-navigation/native-stack 替代,新项目强烈推荐使用后者以获得原生性能。


九、总结

React Navigation v7 通过模块化架构原生性能优化,成为 React Native 导航的最佳选择。记住三个核心原则:

  1. @react-navigation/native 是必需基础 :所有项目都从此开始
  2. 根据场景选择导航器:Stack 处理页面流,Tab 负责一级导航,Drawer 提供全局菜单
  3. 嵌套而非平铺:复杂应用采用导航器组合,保持结构清晰

现在打开你的编辑器,开始构建流畅的导航体验吧!


参考资料

❌