普通视图

发现新文章,点击刷新页面。
昨天以前首页

React 19 时代的 StrictMode:原理、未来准备与最佳实践

2026年1月6日 13:58

什么是 StrictMode?

StrictMode 是 React 提供的开发者工具,它不渲染任何可见的 UI,而是通过为后代组件提供额外的检查来帮助开发者编写高质量代码。它的核心目标是:

  • 识别不安全的生命周期:为并发模式扫清障碍。
  • 警告过时的 API 使用:确保代码能平滑升级到新版本。
  • 检测意外的副作用:通过故意重复调用来暴露 Bug。

StrictMode 的“反直觉”行为

在开发模式下(仅限开发环境,生产环境不生效),StrictMode 会强制执行以下操作:

  1. 重复渲染(Double Render) :组件函数体会被执行两次。
  2. 重复 Effect(Double Effect) :执行 MountUnmountMount 的完整周期。

核心解密:为什么要“卸载再挂载”?

很多开发者认为重复请求是 StrictMode 的“副作用”,但实际上这是 React 为了未来特性所做的强制演习。

1. 为 React 19+ 的 Activity (Offscreen) API 做准备

React 正在引入一种能力(即将在 React 19 及后续版本完善的 <Activity>),允许组件在切换页面时不被销毁,而是“休眠”在后台

  • 场景:用户从 Tab A 切到 Tab B,再切回 Tab A。
  • 目标:Tab A 的状态(滚动位置、表单内容)瞬间恢复,无需重新请求。
  • StrictMode 的良苦用心:现在的 Mount -> Unmount -> Mount 就是在模拟“休眠 -> 唤醒”的过程。如果你的组件在 Unmount 时清理不彻底(如未取消订阅),这种 Bug 在 StrictMode 下会立即暴露,防止未来上线 Activity 功能时页面崩溃。

2. 确保并发渲染(Concurrent Rendering)的稳定性

React 19 全面拥抱并发。渲染过程可能会被中断、暂停或废弃。如果不保证渲染函数的“纯度”(Idempotency),多次计算可能会导致 UI 状态不一致。StrictMode 的重复执行正是为了确保无论渲染多少次,结果都是确定的。

典型的“重复请求”现象

<StrictMode>
  <RouterProvider router={router} />
</StrictMode>

StrictMode 下,常见的请求流程是:

  1. 组件首次挂载 → 执行 useEffect发起第 1 次网络请求
  2. React 立即卸载组件 → 执行 cleanup
  3. React 重新挂载组件 → 再次执行 useEffect发起第 2 次网络请求(内容相同)

最佳实践与解决方案

在 React 19 时代,我们强烈建议保留 StrictMode。与其关掉它,不如优化你的数据获取策略。

方案 1:使用数据请求库(React 19 推荐)

这是现代 React 开发的“标准答案”。使用 TanStack Query (React Query)SWR 或 React 19 的 Server Components (RSC)

// 使用 React Query,库内部会自动处理去重和缓存
// StrictMode 下的多次挂载不会导致重复的网络请求
const { data } = useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
});
  • 优势:直接物理隔离了 useEffect 的副作用问题,代码更声明式,完美适配 StrictMode。

方案 2:使用 AbortController(手动挡推荐)

如果你必须在 useEffect 中请求,请使用 AbortController 进行竞态处理和取消。这不仅解决了 StrictMode 的视觉干扰,更防止了网络竞态条件(Race Condition)。

useEffect(() => {
  const abortController = new AbortController();

  const fetchData = async () => {
    try {
      const res = await apiClient.request({
        method: 'POST',
        url: '/get/data',
        signal: abortController.signal // 绑定信号
      });
      // 处理成功逻辑
    } catch (error) {
      if (error.name !== 'AbortError') {
        // 真正的错误处理
        console.error(error);
      }
    }
  };

  fetchData();

  // Cleanup: 第一次卸载时取消请求 A,保留请求 B
  return () => {
    abortController.abort();
  };
}, []);

避坑:慎用 useRef 标记

一种常见的“歪门邪道”是使用 useRef 拦截请求:

// ❌ 不推荐:可能导致依赖更新失效
const hasRequested = useRef(false);
useEffect(() => {
  if (hasRequested.current) return;
  hasRequested.current = true;
  fetchData();
}, [userId]); // 如果 userId 变化,ref 依然是 true,会导致请求被拦截

这种方法虽然能在这个场景下生效,但如果依赖项数组(Dependency Array)不为空,很容易导致后续合法的请求被错误拦截,增加维护成本。

总结

在 React 19 中,StrictMode 不仅仅是一个调试工具,它是代码质量的体检员

  1. 不要移除它:它帮助你确保代码兼容未来的“后台保活”特性和并发渲染。
  2. 正确处理副作用:重复请求不是 Bug,而是提示你需要清理副作用(Cleanup)或使用更现代的数据流方案(如 React Query 或 RSC)。
  3. 拥抱标准:适应 StrictMode 的检查,意味着你的代码已经做好了迎接 React 未来新特性的准备。

公私分明:为什么你不应该共用 SSH Key(附多账号最佳实践指南)

2026年1月4日 15:32

在开发者的日常工作中,我们经常面临一个选择:个人的 GitHub 和公司的 GitLab,到底是用同一把 SSH Key,还是分开生成?

这是一个典型的 “便利性 vs 安全性” 的博弈。本文将从安全原理出发,分析共用 Key 的隐患,并提供一套只需配置一次的“优雅分治”方案。


⚠️ 核心结论

技术上:完全可行

SSH 协议只验证公私钥匹配,并不关心这把钥匙还开了哪扇门。你可以一把钥匙通吃所有 Git 平台,代码提交没有任何障碍。

安全上:极度危险(不推荐)

共用 SSH Key 违反了安全领域的“最小权限原则”和“故障隔离原则”。


🛑 为什么不建议共用?(三大隐患)

虽然共用能省去几分钟的配置时间,但它留下的隐患如同在家里和公司保险柜上装了同一把锁。

1. 爆炸半径失控(Security Blast Radius)

这是最致命的风险。如果你的个人电脑中毒、私钥被窃,或者你不小心将私钥误传到云端:

  • 后果: 黑客不仅攻破了你的个人 GitHub,还能顺藤摸瓜直接利用该 Key 访问公司的内部代码库。
  • 影响: 原本只是个人账号泄漏,瞬间升级为严重的公司安全事故。

2. 离职交接的泥潭

当你离开公司时,理论上必须作废所有用于公司权限的凭证。

  • 如果你共用 Key: 你不仅要在公司系统删掉公钥,还被迫要在自己的电脑上重新生成新 Key,再去 GitHub 重新上传,并修改本地所有项目的配置。
  • 如果你不换: 公司审计日志里若出现这把 Key 的活动记录,会混淆“前员工”与“个人开发者”的身份,带来合规风险。

3. 资产归属权争议

许多公司的《信息安全协议》规定:访问公司资产的凭证必须专用于公司业务。公私混用可能在法律层面违反你签署的入职协议。


🛠 最佳实践:如何优雅地分开配置?

最推荐的做法是:生成两对不同的 Key,通过 config 文件自动路由。

(注:以下教程基于 macOS/Linux 环境,推荐使用更安全短小的 ed25519 算法)

第一步:生成两把不同的钥匙

在生成时,不要一路回车,通过 -f 参数指定不同的文件名。

# 1. 生成个人用的 Key (GitHub)
ssh-keygen -t ed25519 -C "personal@email.com" -f ~/.ssh/id_ed25519_github

# 2. 生成公司用的 Key (GitLab)
ssh-keygen -t ed25519 -C "work@company.com" -f ~/.ssh/id_ed25519_gitlab

建议: 为了安全,生成时建议设置 passphrase(密码)。Mac 用户后续可以通过系统钥匙串实现免密调用。

第二步:配置 SSH 路由(核心步骤)

创建或编辑 ~/.ssh/config 文件,告诉系统:“去 GitHub 用这把锁,去公司用那把锁”。

vim ~/.ssh/config

在该文件中写入以下内容:

# --- 个人 GitHub 配置 ---
Host github.com
    HostName github.com
    User git                    # 注意:这里必须是 git,不能填你的用户名
    IdentityFile ~/.ssh/id_ed25519_github
    UseKeychain yes             # Mac 专用:利用钥匙串记住密码
    AddKeysToAgent yes          # 自动添加到 agent

# --- 公司 GitLab 配置 ---
# 假设公司 Git 地址是 git.company.com
Host git.company.com
    HostName git.company.com
    User git                    # 注意:这里也保持 git
    IdentityFile ~/.ssh/id_ed25519_gitlab
    UseKeychain yes             # Mac 专用
    AddKeysToAgent yes

第三步:上传公钥

最后,将以 .pub 结尾的公钥内容分别贴到对应的后台。

  • GitHub: 复制 id_ed25519_github.pub -> GitHub Settings -> SSH Keys
  • GitLab: 复制 id_ed25519_gitlab.pub -> GitLab Settings -> SSH Keys

第四步:测试连接

配置完成后,可以用以下命令测试(注意不要用 git clone,直接用 ssh -T 测试握手):

# 测试 GitHub
ssh -T git@github.com
# 预期输出:Hi username! You've successfully authenticated...

# 测试公司 GitLab
ssh -T git@git.company.com
# 预期输出:Welcome to GitLab, @workname!

💡 总结

  • 如果你只是为了省事: 暂时混用确实代码能跑,但你在拿安全性赌博。
  • 如果你想做个专业的开发者: 强烈建议花 5 分钟把它们分开。

公私分明,不仅是保护公司的资产,更是保护你自己职业生涯的最佳方式。

TypeScript 类型断言和类型注解的区别

2025年12月31日 10:12

1. 类型注解(Type Annotation)- useState<string | null>(null)

const [msg, setMsg] = useState<string | null>(null)

作用: 告诉 TypeScript 这个变量的类型应该是什么

特点:

  • 声明类型:定义变量应该是什么类型
  • 编译时检查:TypeScript 会检查赋值是否符合类型
  • 类型推导:如果类型不匹配会报错

示例:

const [msg, setMsg] = useState<string | null>(null)
// msg 的类型是 string | null(只读)
// setMsg 的类型是 (value: string | null) => void

// ✅ 正确:使用 setter 函数
setMsg("hello")  // ✅ 可以
setMsg(null)     // ✅ 可以
setMsg(123)      // ❌ 错误:不能传入 number

2. 类型断言(Type Assertion)- as HTMLInputElement

(input as HTMLInputElement).value = ''

作用: 告诉 TypeScript "相信我,这个值就是这个类型"

特点:

  • 强制转换:不改变运行时值,只改变编译时的类型
  • 绕过检查:告诉编译器"我知道我在做什么"
  • 风险:如果类型不对,运行时可能出错

示例:

const input: HTMLElement | null = document.querySelector('input')
// input 的类型是 HTMLElement | null
// HTMLElement 没有 .value 属性

// 使用类型断言
(input as HTMLInputElement).value = ''  // ✅ 编译通过
// 告诉 TypeScript:我知道 input 是 HTMLInputElement

对比表格

特性 类型注解 useState<string> 类型断言 as HTMLInputElement
时机 声明变量时 使用变量时
作用 定义类型 强制转换类型
检查 严格检查,类型不匹配会报错 绕过检查,相信开发者
风险 低(编译时检查) 高(运行时可能出错)
使用场景 定义新变量 已知类型但 TypeScript 不知道

实际例子

// 类型注解 - 定义类型
const [count, setCount] = useState<number>(0)
// count 的类型是 number,setCount 只能接收 number

// 类型断言 - 强制转换类型
const element = document.querySelector('.my-input')
// element 的类型是 Element | null
const input = element as HTMLInputElement
// 现在 input 的类型是 HTMLInputElement,可以访问 .value

总结

  • 类型注解:定义类型,让 TypeScript 帮你检查
  • 类型断言:强制转换,告诉 TypeScript "相信我"

最佳实践:

  • 优先使用类型注解(更安全)
  • 只在确实知道类型时才使用类型断言
  • 避免过度使用类型断言(会失去类型检查的好处)
❌
❌