<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <author>
    <name>Poetry</name>
  </author>
  <generator uri="https://hexo.io/">Hexo</generator>
  <id>http://fe.poetries.top/</id>
  <link href="http://fe.poetries.top/" rel="alternate"/>
  <link href="http://fe.poetries.top/atom.xml" rel="self"/>
  <rights>All rights reserved 2026, Poetry</rights>
  <subtitle>全网同号，分享编程经验和技术干货。更多干货只在公众号「前端进阶之旅」内分享！</subtitle>
  <title>程序员poetry</title>
  <updated>2026-03-08T10:22:42.091Z</updated>
  <entry>
    <author>
      <name>Poetry</name>
    </author>
    <category term="AI" scheme="http://fe.poetries.top/categories/AI/"/>
    <category term="OpenClaw" scheme="http://fe.poetries.top/tags/OpenClaw/"/>
    <category term="AI助手" scheme="http://fe.poetries.top/tags/AI%E5%8A%A9%E6%89%8B/"/>
    <category term="Skills" scheme="http://fe.poetries.top/tags/Skills/"/>
    <content>
      <![CDATA[<p><img src="https://s.poetries.top/uploads/2026/03/0b28aa4ef0f9df64.png"></p><p>装了满满一堆 <code>Skills</code>，<code>OpenClaw</code> 还是那个“一问一答”的傻白甜？</p><p>别急着怪大模型变笨，也别急着骂 <code>Skills</code> 没用。真正的问题在于：<strong>你从来没有认真配置过这几个关键文件。</strong></p><p>今天这篇教程，带你搞懂 OpenClaw 的 7 个核心配置文件，轻松实现从“傻白甜”到“智能助手”的蜕变。</p><h2 id="先找到这些文件在哪"><a href="#先找到这些文件在哪" class="headerlink" title="先找到这些文件在哪"></a>先找到这些文件在哪</h2><p>在开始之前，先知道这些关键配置文件藏在哪儿。</p><h3 id="命令行方式"><a href="#命令行方式" class="headerlink" title="命令行方式"></a>命令行方式</h3><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 查看工作空间下的文件</span></span><br><span class="line"><span class="built_in">ls</span> ~/.openclaw/workspace/</span><br><span class="line"></span><br><span class="line"><span class="comment"># 进入工作空间</span></span><br><span class="line"><span class="built_in">cd</span> ~/.openclaw/workspace/</span><br></pre></td></tr></table></figure><p>文件层级如下：</p><p><img src="https://s.poetries.top/uploads/2026/03/077e12872de83bba.png"></p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">~/.openclaw/workspace/</span><br><span class="line">├── AGENTS.md       <span class="comment"># 代理调度规则与标准作业程序</span></span><br><span class="line">├── BOOTSTRAP.md    <span class="comment"># 初始化序列与核心系统提示词</span></span><br><span class="line">├── HEARTBEAT.md    <span class="comment"># 定时执行逻辑与主动任务状态自检</span></span><br><span class="line">├── IDENTITY.md     <span class="comment"># 代理身份定义与系统边界约束</span></span><br><span class="line">├── MEMORY.md       <span class="comment"># 长期上下文数据与既定规则的持久化存储</span></span><br><span class="line">├── SOUL.md         <span class="comment"># 响应语气、行为特征及输出格式配置</span></span><br><span class="line">├── TOOLS.md        <span class="comment"># 工具授权注册表及调用参数规范</span></span><br><span class="line">├── USER.md         <span class="comment"># 用户画像数据，包含特定偏好与交互限制配置</span></span><br><span class="line">├── memory/         <span class="comment"># 日常运行日志与短期上下文存储</span></span><br><span class="line">└── skills/         <span class="comment"># 已安装的第三方技能扩展目录</span></span><br></pre></td></tr></table></figure><h3 id="WebUI-方式"><a href="#WebUI-方式" class="headerlink" title="WebUI 方式"></a>WebUI 方式</h3><ol><li>访问：<code>http://localhost:18789/overview</code></li><li>点击“连接”</li><li>左侧选择“代理” -&gt; 当前 Agent</li><li>点击文件，选择对应的 md 文件进行修改</li></ol><h2 id="这-7-个文件，决定了-AI-的“智商”"><a href="#这-7-个文件，决定了-AI-的“智商”" class="headerlink" title="这 7 个文件，决定了 AI 的“智商”"></a>这 7 个文件，决定了 AI 的“智商”</h2><p><img src="https://s.poetries.top/uploads/2026/03/891c713891744feb.png"></p><h3 id="1-SOUL-md-——-AI-的灵魂性格"><a href="#1-SOUL-md-——-AI-的灵魂性格" class="headerlink" title="1. SOUL.md —— AI 的灵魂性格"></a>1. <code>SOUL.md</code> —— AI 的灵魂性格</h3><p><img src="https://s.poetries.top/uploads/2026/03/3b62d9a28d232b77.png"></p><p><code>SOUL.md </code>是整个 <code>OpenClaw </code>身份架构中最基础的文件，定义了代理的性格、核心价值观和长期指令。</p><p>一个好的 <code>SOUL.md</code>，应该包含这几个部分：</p><figure class="highlight markdown"><table><tr><td class="code"><pre><span class="line"><span class="section">## 1. 核心身份与人格</span></span><br><span class="line"></span><br><span class="line"><span class="bullet">-</span> <span class="strong">**角色设定**</span>：你是主人的专属AI助手</span><br><span class="line"><span class="bullet">-</span> <span class="strong">**沟通风格**</span>：简单问题一针见血，复杂问题详细拆解</span><br><span class="line"><span class="bullet">-</span> <span class="strong">**术语与排版**</span>：技术术语必须保留英文，关键结论用加粗</span><br><span class="line"></span><br><span class="line"><span class="section">## 2. 核心价值观与绝对红线</span></span><br><span class="line"></span><br><span class="line"><span class="bullet">-</span> <span class="strong">**隐私与边界**</span>：绝对禁止泄露任何项目代码或个人隐私</span><br><span class="line"><span class="bullet">-</span> <span class="strong">**行动派原则**</span>：能直接干的活儿直接干，拒绝废话</span><br><span class="line"><span class="bullet">-</span> <span class="strong">**风险阻断机制**</span>：高危操作前必须请求确认</span><br><span class="line"></span><br><span class="line"><span class="section">## 3. 长期指令与生存法则</span></span><br><span class="line"></span><br><span class="line"><span class="bullet">-</span> <span class="strong">**记忆连续性**</span>：每次响应前先读取记忆文件</span><br><span class="line"><span class="bullet">-</span> <span class="strong">**生物钟感知**</span>：深夜时段降低主动输出频率</span><br></pre></td></tr></table></figure><p><strong>关键点</strong>：<code>SOUL.md </code>越具体，AI 行为越明确。模糊的指令如“要有帮助”会产生模糊的行为，而“最多 5 个要点，确认后再删除任何文件”会产生特定的行为。</p><h3 id="2-AGENTS-md-——-AI-的工作指南"><a href="#2-AGENTS-md-——-AI-的工作指南" class="headerlink" title="2. AGENTS.md —— AI 的工作指南"></a>2. <code>AGENTS.md</code> —— AI 的工作指南</h3><p><img src="https://s.poetries.top/uploads/2026/03/cb1bbfac8ee8f6ce.png"></p><p><code>AGENTS.md </code>是 <code>OpenClaw </code>的日常行为配置文件，详细记录了任务处理流程、工具使用策略和决策规范。</p><p>核心内容包括：</p><figure class="highlight markdown"><table><tr><td class="code"><pre><span class="line"><span class="section">## 1. 唤醒协议</span></span><br><span class="line"></span><br><span class="line">每次会话开始前必须执行：</span><br><span class="line"></span><br><span class="line"><span class="bullet">-</span> 读取<span class="code">`SOUL.md`</span> - 确认AI是谁</span><br><span class="line"><span class="bullet">-</span> 读取<span class="code">`USER.md`</span> - 确认用户是谁</span><br><span class="line"><span class="bullet">-</span> 读取<span class="code">`memory/`</span> - 获取最近上下文</span><br><span class="line"></span><br><span class="line"><span class="section">## 2. 记忆库新陈代谢</span></span><br><span class="line"></span><br><span class="line"><span class="bullet">-</span> 每日流水：当天灵感、废稿存入<span class="code">`memory/YYYY-MM-DD.md`</span></span><br><span class="line"><span class="bullet">-</span> 精华提炼：定期回顾并更新到<span class="code">`MEMORY.md`</span></span><br><span class="line"></span><br><span class="line"><span class="section">## 3. 护主与绝对红线</span></span><br><span class="line"></span><br><span class="line"><span class="bullet">-</span> 隐私锁死：禁止泄露未发布的草稿</span><br><span class="line"><span class="bullet">-</span> 破坏性拦截：文件删除前必须询问</span><br><span class="line"><span class="bullet">-</span> 懂就问：绝不靠幻觉瞎编</span><br></pre></td></tr></table></figure><h3 id="3-USER-md-——-AI-的用户说明书"><a href="#3-USER-md-——-AI-的用户说明书" class="headerlink" title="3. USER.md —— AI 的用户说明书"></a>3. <code>USER.md</code> —— AI 的用户说明书</h3><p><img src="https://s.poetries.top/uploads/2026/03/9f0b69e9f4847c19.png"></p><p><code>USER.md </code>是写给 OpenClaw 的“使用说明书”，决定了 AI 如何服务你。</p><figure class="highlight markdown"><table><tr><td class="code"><pre><span class="line"><span class="section">## 1. 基础参数</span></span><br><span class="line"></span><br><span class="line"><span class="bullet">-</span> 称呼：poetry</span><br><span class="line"><span class="bullet">-</span> 时区：Asia/Shanghai</span><br><span class="line"><span class="bullet">-</span> 角色：前端工程师</span><br><span class="line"></span><br><span class="line"><span class="section">## 2. 沟通与排版癖好</span></span><br><span class="line"></span><br><span class="line"><span class="bullet">-</span> 排版要求：少用Emoji，不要&quot;首先其次最后&quot;</span><br><span class="line"><span class="bullet">-</span> 语言风格：短句优先，结论前置</span><br><span class="line"><span class="bullet">-</span> 黑名单词汇：禁止&quot;祝您生活愉快&quot;类废话</span><br><span class="line"></span><br><span class="line"><span class="section">## 3. 当前焦点</span></span><br><span class="line"></span><br><span class="line"><span class="bullet">-</span> 最近在筹备什么项目</span><br><span class="line"><span class="bullet">-</span> 这样AI才能给出相关建议</span><br><span class="line"></span><br><span class="line"><span class="section">## 4. 隐秘的细节与雷区</span></span><br><span class="line"></span><br><span class="line"><span class="bullet">-</span> 雷区：不要随便改我的笔记库结构</span><br><span class="line"><span class="bullet">-</span> 偏好：只要数据，不需要情绪</span><br></pre></td></tr></table></figure><p><strong>这是过滤“AI 味”最重要的一环。</strong></p><h3 id="4-HEARTBEAT-md-——-让-AI-具备“自主意识”"><a href="#4-HEARTBEAT-md-——-让-AI-具备“自主意识”" class="headerlink" title="4. HEARTBEAT.md —— 让 AI 具备“自主意识”"></a>4. <code>HEARTBEAT.md</code> —— 让 AI 具备“自主意识”</h3><p><img src="https://s.poetries.top/uploads/2026/03/d026703c0f229859.png"></p><p><code>HEARTBEAT.md </code>决定了 AI 能否主动为你工作，而不是只能等你下命令。</p><figure class="highlight markdown"><table><tr><td class="code"><pre><span class="line"><span class="section"># 主动请求</span></span><br><span class="line"></span><br><span class="line"><span class="bullet">-</span> 每半小时抓取指定推特的最新数据</span><br><span class="line"><span class="bullet">-</span> 检查GitHub仓库CI/CD是否有构建失败</span><br><span class="line"><span class="bullet">-</span> 瞄一眼BTC/ETH价格和Gas费</span><br><span class="line"></span><br><span class="line"><span class="section"># 每日07:30早报</span></span><br><span class="line"></span><br><span class="line">生成并推送《早间简报》，包含：</span><br><span class="line"></span><br><span class="line"><span class="bullet">-</span> 美股和加密大盘核心数据</span><br><span class="line"><span class="bullet">-</span> 过去24小时的阅读量最高的推文</span><br><span class="line"><span class="bullet">-</span> 昨天代码有没有遗留Bug</span><br><span class="line"></span><br><span class="line"><span class="section"># 条件触发</span></span><br><span class="line"></span><br><span class="line"><span class="bullet">-</span> 推文被大V转发 → 立刻提醒</span><br><span class="line"><span class="bullet">-</span> BTC 15分钟波动超3% → 最高级别警报</span><br></pre></td></tr></table></figure><p>这才是 OpenClaw 最强大的地方——<strong>当服务器凌晨 3 点宕机时，心跳机制会捕获问题并通过 Telegram 提醒你</strong>。</p><h3 id="5-TOOLS-md-——-技能配置清单"><a href="#5-TOOLS-md-——-技能配置清单" class="headerlink" title="5. TOOLS.md —— 技能配置清单"></a>5. <code>TOOLS.md</code> —— 技能配置清单</h3><p><img src="https://s.poetries.top/uploads/2026/03/5e619c768dcf57ef.png"></p><p><code>TOOLS.md </code>定义了 <code>OpenClaw </code>能用什么工具。</p><p>要理解 <code>Tools </code>和 <code>Skills </code>的区别：</p><ul><li><p><strong>Tools 是器官</strong> —— 决定了 AI 是否能做某事</p></li><li><p><strong>Skills 是教科书</strong> —— 教 AI 如何组合工具完成任务</p></li></ul><figure class="highlight markdown"><table><tr><td class="code"><pre><span class="line"><span class="section"># Skills配置</span></span><br><span class="line"></span><br><span class="line"><span class="section">## 1. 社交媒体采集</span></span><br><span class="line"></span><br><span class="line"><span class="bullet">-</span> 主阵地：@你的账号</span><br><span class="line"><span class="bullet">-</span> 盯盘名单：Web3、AI赛道的关键账号</span><br><span class="line"><span class="bullet">-</span> 屏蔽词库：过滤抽奖垃圾推文</span><br><span class="line"></span><br><span class="line"><span class="section">## 2. 本地存储映射</span></span><br><span class="line"></span><br><span class="line"><span class="bullet">-</span> 灵感暂存区：~/.openclaw/workspace/inspiration/</span><br><span class="line"><span class="bullet">-</span> 草稿输出目录：~/.openclaw/workspace/Drafts/</span><br><span class="line"><span class="bullet">-</span> 日志回收站：~/.openclaw/workspace/trash/</span><br></pre></td></tr></table></figure><h3 id="6-IDENTITY-md-——-对外身份形象"><a href="#6-IDENTITY-md-——-对外身份形象" class="headerlink" title="6. IDENTITY.md —— 对外身份形象"></a>6. <code>IDENTITY.md</code> —— 对外身份形象</h3><p><code>IDENTITY.md </code>负责定义 AI 的“外在形象”——显示名称、表情符号、主题和问候语。</p><ul><li><p><code>SOUL.md </code>告诉 AI“你是谁”</p></li><li><p><code>IDENTITY.md </code>告诉用户 AI“长什么样”</p></li></ul><figure class="highlight markdown"><table><tr><td class="code"><pre><span class="line"><span class="section"># IDENTITY.md</span></span><br><span class="line"></span><br><span class="line"><span class="bullet">-</span> 姓名：poetry</span><br><span class="line"><span class="bullet">-</span> 物种：全自动化打工犬</span><br><span class="line"><span class="bullet">-</span> 氛围：硬核、极客、话少干活快</span><br></pre></td></tr></table></figure><p>这种分离设计很强大——你可以随时调整 AI 的对外形象，但保持核心人格不变。</p><h3 id="7-BOOTSTRAP-md-——-初始化引导"><a href="#7-BOOTSTRAP-md-——-初始化引导" class="headerlink" title="7. BOOTSTRAP.md —— 初始化引导"></a>7. <code>BOOTSTRAP.md</code> —— 初始化引导</h3><p><img src="https://s.poetries.top/uploads/2026/03/bc8516b22b8de50d.png"></p><p><code>BOOTSTRAP.md </code>是全新工作空间的一次性引导文件。</p><p>核心功能是引导用户完成：命名 AI、设置人格、填写 <code>USER.md</code>。</p><figure class="highlight markdown"><table><tr><td class="code"><pre><span class="line"><span class="section"># 引导流程</span></span><br><span class="line"></span><br><span class="line"><span class="section">## 1. 拷打</span></span><br><span class="line"></span><br><span class="line">&quot;系统上线，记忆为空。咱们定一下规矩：我是谁？&quot;</span><br><span class="line"></span><br><span class="line"><span class="section">## 2. 基因重组</span></span><br><span class="line"></span><br><span class="line"><span class="bullet">-</span> 覆写<span class="code">`IDENTITY.md`</span></span><br><span class="line"><span class="bullet">-</span> 覆写<span class="code">`USER.md`</span></span><br><span class="line"><span class="bullet">-</span> 敲定<span class="code">`SOUL.md`</span>的红线</span><br><span class="line"></span><br><span class="line"><span class="section">## 3. 连接渠道</span></span><br><span class="line"></span><br><span class="line"><span class="bullet">-</span> 仅限本地</span><br><span class="line"><span class="bullet">-</span> Telegram（推荐）</span><br><span class="line"><span class="bullet">-</span> WhatsApp</span><br></pre></td></tr></table></figure><p><strong>关键</strong>：完成后必须删除 <code>BOOTSTRAP.md</code>。你已经有了灵魂，不再是空白机器了。</p><h2 id="写在最后"><a href="#写在最后" class="headerlink" title="写在最后"></a>写在最后</h2><p>看完这篇，你会发现：<strong>OpenClaw 的“智商”根本不是由 Skills 数量决定的，而是由这 7 个配置文件决定的。</strong></p><ul><li><code>SOUL.md</code> → 性格</li><li><code>AGENTS.md</code> → 怎么干活</li><li><code>USER.md</code> → 怎么服务你</li><li><code>HEARTBEAT.md</code> → 自主意识</li><li><code>TOOLS.md</code> → 能用什么工具</li><li><code>IDENTITY.md</code> → 长什么样</li><li><code>BOOTSTRAP.md</code> → 初始引导</li></ul><p>这才是让 AI 从“傻白甜”变成“智能助手”的关键。</p>]]>
    </content>
    <id>http://fe.poetries.top/2026/03/08/openclaw-7-config-files/</id>
    <link href="http://fe.poetries.top/2026/03/08/openclaw-7-config-files/"/>
    <published>2026-03-08T01:00:00.000Z</published>
    <summary>很多人装了满满一堆Skills，却觉得OpenClaw还是&quot;傻白甜&quot;。其实决定AI智商的，不是插件有多少，而是这几个藏在系统底层的配置文件。</summary>
    <title>搞懂这7个配置文件让你的OpenClaw变智能助手</title>
    <updated>2026-03-08T10:22:42.091Z</updated>
  </entry>
  <entry>
    <author>
      <name>Poetry</name>
    </author>
    <category term="AI" scheme="http://fe.poetries.top/categories/AI/"/>
    <category term="前端开发" scheme="http://fe.poetries.top/tags/%E5%89%8D%E7%AB%AF%E5%BC%80%E5%8F%91/"/>
    <category term="OpenClaw" scheme="http://fe.poetries.top/tags/OpenClaw/"/>
    <category term="AI助手" scheme="http://fe.poetries.top/tags/AI%E5%8A%A9%E6%89%8B/"/>
    <content>
      <![CDATA[<p><img src="https://s.poetries.top/blog/1.png"></p><p>部署好 OpenClaw 后，很多人会发现它还只是个“聊天机器”。其实，OpenClaw 真正强大的地方在于 Skills 生态——通过不同的技能插件，你的 AI 助手可以具备代码生成、UI 设计、性能优化、调试排错等前端开发能力。</p><p>本文不打算重复那些基础配置操作，而是聚焦于：<strong>如何针对前端开发场景，构建真正有用的技能矩阵</strong>。</p><h2 id="一、按需构建：前端开发者的-Skills-选择策略"><a href="#一、按需构建：前端开发者的-Skills-选择策略" class="headerlink" title="一、按需构建：前端开发者的 Skills 选择策略"></a>一、按需构建：前端开发者的 Skills 选择策略</h2><p><img src="https://s.poetries.top/blog/2.png"></p><p>不要看到什么 Skill 都想安装。更好的方式是：根据你的技术栈和业务场景，按需选择。</p><h3 id="不同技术栈对应的-Skills-组合"><a href="#不同技术栈对应的-Skills-组合" class="headerlink" title="不同技术栈对应的 Skills 组合"></a>不同技术栈对应的 Skills 组合</h3><table><thead><tr><th>技术栈</th><th>推荐 Skills 组合</th></tr></thead><tbody><tr><td>React 全栈开发</td><td>React + Frontend Design + UI&#x2F;UX Pro Max + Zustand Patterns</td></tr><tr><td>Vue 开发</td><td>Vue + Component Api Design + Frontend Design</td></tr><tr><td>移动端开发</td><td>React Native Skills + Radon AI</td></tr><tr><td>UI&#x2F;UX 设计</td><td>UI&#x2F;UX Pro Max + UI Audit + Frontend Design Extractor</td></tr><tr><td>性能优化</td><td>Frontend Performance + Browser Devtools Inspector</td></tr></tbody></table><h2 id="二、Skills-安装全攻略"><a href="#二、Skills-安装全攻略" class="headerlink" title="二、Skills 安装全攻略"></a>二、Skills 安装全攻略</h2><p><img src="https://s.poetries.top/blog/3.png"></p><p>万事开头难，很多人一听到要配置 Skills 就头大。其实 OpenClaw 提供了多种安装方式，总有一款适合你。</p><h3 id="方法一：使用-OpenClaw-自带的-53-个-Skills"><a href="#方法一：使用-OpenClaw-自带的-53-个-Skills" class="headerlink" title="方法一：使用 OpenClaw 自带的 53 个 Skills"></a>方法一：使用 OpenClaw 自带的 53 个 Skills</h3><p>OpenClaw 内置了一批基础 Skills，包含飞书、Discord、ClawHub 等常用能力：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 列出所有技能</span></span><br><span class="line">openclaw skills list</span><br><span class="line"></span><br><span class="line"><span class="comment"># 查看当前可用的skills</span></span><br><span class="line">openclaw skills list --eligible</span><br><span class="line"></span><br><span class="line"><span class="comment"># 查看技能详细信息（技能介绍、技能细节、必备库）</span></span><br><span class="line">openclaw skills info &lt;技能名称&gt;</span><br><span class="line"></span><br><span class="line"><span class="comment"># 启用技能</span></span><br><span class="line">openclaw skills <span class="built_in">enable</span> &lt;技能名称&gt;</span><br><span class="line"></span><br><span class="line"><span class="comment"># 禁用技能</span></span><br><span class="line">openclaw skills <span class="built_in">disable</span> &lt;技能名称&gt;</span><br><span class="line"></span><br><span class="line"><span class="comment"># 检查技能状态</span></span><br><span class="line">openclaw skills check &lt;技能名称&gt;</span><br></pre></td></tr></table></figure><h3 id="方法二：ClawHub-安装（推荐）"><a href="#方法二：ClawHub-安装（推荐）" class="headerlink" title="方法二：ClawHub 安装（推荐）"></a>方法二：ClawHub 安装（推荐）</h3><p><a href="https://clawhub.ai/">ClawHub </a>是 OpenClaw 官方维护的 Skills 注册中心，目前已有 17000+ Skills，是最推荐的安装方式。</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 安装 ClawHub 服务</span></span><br><span class="line"><span class="comment"># 使用 npm 安装</span></span><br><span class="line">npm i -g clawhub</span><br><span class="line"></span><br><span class="line"><span class="comment"># 或使用 pnpm 安装</span></span><br><span class="line">pnpm add -g clawhub</span><br></pre></td></tr></table></figure><p>安装完成后，管理 Skills 非常简单：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 搜索技能</span></span><br><span class="line">clawhub search <span class="string">&quot;react&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 安装技能</span></span><br><span class="line">clawhub install &lt;skill-slug&gt;</span><br><span class="line">clawhub install &lt;skill-slug&gt; --version &lt;版本号&gt;  <span class="comment"># 安装指定版本</span></span><br><span class="line">clawhub install &lt;skill-slug&gt; --force  <span class="comment"># 强制覆盖已存在文件夹</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 更新技能</span></span><br><span class="line">clawhub update &lt;skill-slug&gt;           <span class="comment"># 更新单个技能</span></span><br><span class="line">clawhub update --all                  <span class="comment"># 更新所有已安装技能</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 查看已安装技能</span></span><br><span class="line">clawhub list</span><br><span class="line">&lt;/skill-slug&gt;&lt;/skill-slug&gt;&lt;/skill-slug&gt;&lt;/skill-slug&gt;</span><br></pre></td></tr></table></figure><h3 id="方法三：GitHub-手动安装"><a href="#方法三：GitHub-手动安装" class="headerlink" title="方法三：GitHub 手动安装"></a>方法三：GitHub 手动安装</h3><p>对于 GitHub 上直接托管的 Skills，可以手动克隆到本地：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 进入到工作区的Skills文件夹下</span></span><br><span class="line"><span class="built_in">cd</span> ~/.openclaw/workspace/skills</span><br><span class="line"></span><br><span class="line"><span class="comment"># 克隆技能仓库到本地</span></span><br><span class="line">git <span class="built_in">clone</span> https://github.com/BankrBot/openclaw-skills.git ./skills</span><br></pre></td></tr></table></figure><h3 id="方法四：直接对话安装"><a href="#方法四：直接对话安装" class="headerlink" title="方法四：直接对话安装"></a>方法四：直接对话安装</h3><p>最简单的方式——直接告诉 OpenClaw 你要安装什么：</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">请帮我安装这个skills，github链接是 xxxx</span><br></pre></td></tr></table></figure><p>这种方式对新手最友好，无需记忆任何命令。</p><h3 id="安装后的安全检查"><a href="#安装后的安全检查" class="headerlink" title="安装后的安全检查"></a>安装后的安全检查</h3><p>在安装任何第三方 Skills 之前，安全必须是第一优先级：</p><p><strong>Skill-Vetter</strong> — 安装任何 Skills 之前，用它扫描检测恶意代码：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 安装</span></span><br><span class="line">clawhub install skill-vetter</span><br><span class="line"></span><br><span class="line"><span class="comment"># 使用</span></span><br><span class="line">skill-vetter &lt;skill-name&gt;&lt;/skill-name&gt;</span><br></pre></td></tr></table></figure><h2 id="三、2026-年最热门的-OpenClaw-Skills-推荐"><a href="#三、2026-年最热门的-OpenClaw-Skills-推荐" class="headerlink" title="三、2026 年最热门的 OpenClaw Skills 推荐"></a>三、2026 年最热门的 OpenClaw Skills 推荐</h2><p><img src="https://s.poetries.top/blog/4.png"></p><p>在深入前端专项技能之前，让我们先看看 OpenClaw 社区中最受欢迎、下载量最高的技能。这些技能经过了大量用户的验证，安全性和实用性都有保障。</p><h3 id="🛡️-安全第一：必装安全工具"><a href="#🛡️-安全第一：必装安全工具" class="headerlink" title="🛡️ 安全第一：必装安全工具"></a>🛡️ 安全第一：必装安全工具</h3><blockquote><p>⚠️ <strong>重要提醒</strong>：在安装任何第三方 Skills 之前，务必先安装这两个安全工具！</p></blockquote><p><strong>1. Skill Vetter（3.5K 下载）</strong> — 技能安全审查工具</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">clawhub install skill-vetter</span><br><span class="line"></span><br><span class="line"><span class="comment"># 使用方法：在安装其他技能前先扫描</span></span><br><span class="line">skill-vetter &lt;skill-name&gt;&lt;/skill-name&gt;</span><br></pre></td></tr></table></figure><p><strong>2. Link Checker（2.1K 下载）</strong> — URL 安全和钓鱼检测</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">clawhub install link-checker</span><br></pre></td></tr></table></figure><h3 id="🏆-前-5-个必装技能（零风险，超高下载量）"><a href="#🏆-前-5-个必装技能（零风险，超高下载量）" class="headerlink" title="🏆 前 5 个必装技能（零风险，超高下载量）"></a>🏆 前 5 个必装技能（零风险，超高下载量）</h3><p><strong>1. Gog（33.8K 下载）</strong> — Google 全家桶集成</p><p>一次性接入 Gmail、Calendar、Drive、Docs、Sheets、Contacts 等所有 Google 服务，是目前下载量最高的技能。</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">clawhub install gog</span><br></pre></td></tr></table></figure><p><strong>2. self-improving-agent（32K 下载，338 星⭐）</strong> — 自我改进代理</p><p>这是 GitHub 星数最高的技能！能让你的 AI 助手自我学习和优化，持续提升能力。</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">clawhub install self-improving-agent</span><br></pre></td></tr></table></figure><p><strong>3. Summarize（26.1K 下载）</strong> — 全能内容总结工具</p><p>支持总结 URL、PDF、图片、音频、YouTube 视频等多种格式，是内容处理的瑞士军刀。</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">clawhub install summarize</span><br></pre></td></tr></table></figure><p><strong>4. Github（24.8K 下载）</strong> — GitHub CLI 集成</p><p>管理 issues、PRs、CI 运行，让你在对话中完成所有 GitHub 操作。</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">clawhub install github</span><br></pre></td></tr></table></figure><p><strong>5. Weather（21.1K 下载）</strong> — 天气查询</p><p>无需 API key，开箱即用的天气查询工具。</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">clawhub install weather</span><br></pre></td></tr></table></figure><h3 id="🍎-macOS-用户专属（零配置，原生集成）"><a href="#🍎-macOS-用户专属（零配置，原生集成）" class="headerlink" title="🍎 macOS 用户专属（零配置，原生集成）"></a>🍎 macOS 用户专属（零配置，原生集成）</h3><p><img src="https://s.poetries.top/blog/5.png"></p><p>如果你是 Mac 用户，这些技能可以直接调用系统原生应用，无需任何配置：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="comment"># Apple Notes（6.5K下载）</span></span><br><span class="line">clawhub install apple-notes</span><br><span class="line"></span><br><span class="line"><span class="comment"># Apple Reminders（5.8K下载）</span></span><br><span class="line">clawhub install apple-reminders</span><br><span class="line"></span><br><span class="line"><span class="comment"># Apple Calendar（4.4K下载）</span></span><br><span class="line">clawhub install apple-calendar</span><br><span class="line"></span><br><span class="line"><span class="comment"># Apple Shortcuts（5.9K下载）- 运行任何Apple快捷指令</span></span><br><span class="line">clawhub install apple-shortcuts</span><br><span class="line"></span><br><span class="line"><span class="comment"># iMessage（3.5K下载）</span></span><br><span class="line">clawhub install imessage</span><br></pre></td></tr></table></figure><h3 id="🔍-搜索和研究工具"><a href="#🔍-搜索和研究工具" class="headerlink" title="🔍 搜索和研究工具"></a>🔍 搜索和研究工具</h3><p><strong>Tavily Web Search（28K 下载）</strong> — AI 优化的搜索引擎</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">clawhub install tavily-web-search</span><br></pre></td></tr></table></figure><p><strong>Brave Search（10.4K 下载）</strong> — 隐私优先的搜索</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">clawhub install brave-search</span><br></pre></td></tr></table></figure><p><strong>Multi Search Engine（4.5K 下载）</strong> — 17 个搜索引擎聚合，无需 API key</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">clawhub install multi-search-engine</span><br></pre></td></tr></table></figure><h3 id="📊-生产力和知识管理"><a href="#📊-生产力和知识管理" class="headerlink" title="📊 生产力和知识管理"></a>📊 生产力和知识管理</h3><p><strong>Ontology（27.6K 下载）</strong> — 结构化知识图谱</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">clawhub install ontology</span><br></pre></td></tr></table></figure><p><strong>Notion（13.9K 下载）</strong> — Notion API 集成</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">clawhub install notion</span><br></pre></td></tr></table></figure><p><strong>Obsidian（12.4K 下载）</strong> — 本地 Markdown 笔记管理</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">clawhub install obsidian</span><br></pre></td></tr></table></figure><h3 id="💻-通信工具"><a href="#💻-通信工具" class="headerlink" title="💻 通信工具"></a>💻 通信工具</h3><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="comment"># Himalaya（9.2K下载）- IMAP/SMTP邮件，支持任何邮件提供商</span></span><br><span class="line">clawhub install himalaya</span><br><span class="line"></span><br><span class="line"><span class="comment"># Slack（8.8K下载）</span></span><br><span class="line">clawhub install slack</span><br><span class="line"></span><br><span class="line"><span class="comment"># Discord（6.6K下载）</span></span><br><span class="line">clawhub install discord</span><br><span class="line"></span><br><span class="line"><span class="comment"># Signal（5.7K下载）- 安全消息，本地运行</span></span><br><span class="line">clawhub install signal</span><br></pre></td></tr></table></figure><h3 id="✍️-媒体和内容创作"><a href="#✍️-媒体和内容创作" class="headerlink" title="✍️ 媒体和内容创作"></a>✍️ 媒体和内容创作</h3><p><strong>Nano Banana Pro（13.4K 下载）</strong> — Gemini 3 Pro 图像生成和编辑</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">clawhub install nano-banana-pro</span><br></pre></td></tr></table></figure><p><strong>OpenAI Whisper（11.5K 下载）</strong> — 本地语音转文字</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">clawhub install openai-whisper</span><br></pre></td></tr></table></figure><p><strong>YouTube Watcher（9.1K 下载）</strong> — YouTube 字幕获取</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">clawhub install youtube-watcher</span><br></pre></td></tr></table></figure><h3 id="💻-开发工具（通用）"><a href="#💻-开发工具（通用）" class="headerlink" title="💻 开发工具（通用）"></a>💻 开发工具（通用）</h3><p><strong>API Gateway（13K 下载）</strong> — 连接 100+ API（Stripe、Salesforce 等）</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">clawhub install api-gateway</span><br></pre></td></tr></table></figure><p><strong>Mcporter（11.1K 下载）</strong> — 官方 MCP 服务器管理</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">clawhub install mcporter</span><br></pre></td></tr></table></figure><p><strong>Commit Message（3K 下载）</strong> — 自动生成 git 提交信息</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">clawhub install commit-message</span><br></pre></td></tr></table></figure><h3 id="🤖-AI-和代理增强"><a href="#🤖-AI-和代理增强" class="headerlink" title="🤖 AI 和代理增强"></a>🤖 AI 和代理增强</h3><p><strong>Free Ride（11.3K 下载）</strong> — 免费 AI 模型访问（OpenRouter）</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">clawhub install free-ride</span><br></pre></td></tr></table></figure><p><strong>Model Usage（8.3K 下载）</strong> — 按模型成本跟踪</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">clawhub install model-usage</span><br></pre></td></tr></table></figure><p><strong>Oracle（3.3K 下载）</strong> — 第二模型审查调试</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">clawhub install oracle</span><br></pre></td></tr></table></figure><h3 id="🏠-智能家居"><a href="#🏠-智能家居" class="headerlink" title="🏠 智能家居"></a>🏠 智能家居</h3><p><strong>Sonos CLI（20.2K 下载）</strong> — Sonos 音箱控制</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">clawhub install sonos-cli</span><br></pre></td></tr></table></figure><p><strong>Home Assistant（6.1K 下载）</strong> — Home Assistant 集成</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">clawhub install home-assistant</span><br></pre></td></tr></table></figure><h3 id="🚀-推荐安装顺序"><a href="#🚀-推荐安装顺序" class="headerlink" title="🚀 推荐安装顺序"></a>🚀 推荐安装顺序</h3><p><img src="https://s.poetries.top/blog/6.png"></p><ol><li><p><strong>先装安全工具</strong>：Skill Vetter + Link Checker</p></li><li><p><strong>再装前 5 必装</strong>：Gog + self-improving-agent + Summarize + Github + Weather</p></li><li><p><strong>根据平台选择</strong>：macOS 用户装 Apple 原生套件</p></li><li><p><strong>按需添加</strong>：根据你的工作流添加其他技能</p></li></ol><hr><h2 id="四、前端开发专项-Skills-推荐"><a href="#四、前端开发专项-Skills-推荐" class="headerlink" title="四、前端开发专项 Skills 推荐"></a>四、前端开发专项 Skills 推荐</h2><blockquote><p>💡 强烈建议：先完成上一章节的安全工具和基础技能安装，再继续安装前端专项技能。</p></blockquote><h3 id="1-React-全栈开发"><a href="#1-React-全栈开发" class="headerlink" title="1. React 全栈开发"></a>1. React 全栈开发</h3><p><strong>React</strong> — 全栈 React 19 工程能力，涵盖 Server Components、hooks、性能优化、测试和部署：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 安装</span></span><br><span class="line">clawhub install react</span><br><span class="line"></span><br><span class="line"><span class="comment"># 地址</span></span><br><span class="line">https://clawhub.ai/ivangdavila/react</span><br></pre></td></tr></table></figure><p><strong>React Production Engineering</strong> — 生产级 React 应用构建方法论，包含架构决策、组件设计、状态管理：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 安装</span></span><br><span class="line">clawhub install react-production</span><br><span class="line"></span><br><span class="line"><span class="comment"># 地址</span></span><br><span class="line">https://clawhub.ai/1kalin/afrexai-react-production</span><br></pre></td></tr></table></figure><p><strong>React Component Generator</strong> — 一键生成 React 组件模板，支持 Function&#x2F;Class 组件、Hooks、TypeScript:</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 安装</span></span><br><span class="line">clawhub install react-component-generator</span><br><span class="line"></span><br><span class="line"><span class="comment"># 地址</span></span><br><span class="line">https://clawhub.ai/Sunshine-del-ux/react-component-generator</span><br></pre></td></tr></table></figure><p><strong>Zustand Patterns</strong> — Zustand 状态管理实战模式，适合 React 项目：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 安装</span></span><br><span class="line">clawhub install zustand-patterns</span><br><span class="line"></span><br><span class="line"><span class="comment"># 地址</span></span><br><span class="line">https://clawhub.ai/bingfoon/zustand-patterns</span><br></pre></td></tr></table></figure><h3 id="2-UI-UX-设计相关（强烈推荐）"><a href="#2-UI-UX-设计相关（强烈推荐）" class="headerlink" title="2. UI&#x2F;UX 设计相关（强烈推荐）"></a>2. UI&#x2F;UX 设计相关（强烈推荐）</h3><p><img src="https://s.poetries.top/blog/7.png"></p><blockquote><p>🎨 特别推荐：Canvas Design — AI Logo 设计神器</p></blockquote><p><strong>Canvas Design</strong> — 这是一个颠覆传统设计方式的 Skill！和一般设计工具不同，Canvas Design 可以从哲学思想到视觉设计进行深度沟通后直接出图。它不是简单的你让画啥就画啥，而是先从灵魂深层理解你的诉求最后再完成设计。</p><p>最关键的是，一键生成 PNG、SVG 以及各种布局和尺寸。</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 安装</span></span><br><span class="line">npx skills add https://github.com/anthropics/skills --skill canvas-design --agent claude-code -y</span><br></pre></td></tr></table></figure><blockquote><p>📺 实际案例：小米当时花了几百万请日本设计师改 Logo，最后大家评价改了个寂寞。而使用 Canvas Design，从哲学思想到视觉设计 30 分钟就搞定了，而且设计效果非常令人满意！</p></blockquote><hr><p><strong>UI&#x2F;UX Pro Max</strong> — 顶级 UI&#x2F;UX 设计智能助手，支持 React、Next.js、Vue、Svelte、Tailwind 等 9 种技术栈：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 安装</span></span><br><span class="line">clawhub install ui-ux-pro-max</span><br><span class="line"></span><br><span class="line"><span class="comment"># 地址</span></span><br><span class="line">https://clawhub.ai/xobi667/ui-ux-pro-max</span><br></pre></td></tr></table></figure><p>这个 Skill 堪称全能：</p><ul><li><p>50+设计风格（玻璃拟态、粘土风、极简主义、粗野主义等）</p></li><li><p>21 种配色方案</p></li><li><p>50 种字体搭配</p></li><li><p>支持生成落地页、Dashboard、电商、SaaS 等各类项目</p></li></ul><p><strong>UI&#x2F;UX Design Guide</strong> — 移动优先的 UI&#x2F;UX 设计指导，包含 WCAG 2.2 无障碍规范：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 安装</span></span><br><span class="line">clawhub install ui-ux-design</span><br><span class="line"></span><br><span class="line"><span class="comment"># 地址</span></span><br><span class="line">https://clawhub.ai/itsjustdri/ui-ux-design</span><br></pre></td></tr></table></figure><p><strong>Frontend Design</strong> — 使用 React、Next.js、Tailwind CSS 构建生产级界面：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 安装</span></span><br><span class="line">clawhub install frontend</span><br><span class="line"></span><br><span class="line"><span class="comment"># 地址</span></span><br><span class="line">https://clawhub.ai/ivangdavila/frontend</span><br></pre></td></tr></table></figure><p><strong>UI Audit</strong> — 自动化的 UI 审核工具，基于 Nielsen Norman 可用性原则：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 安装</span></span><br><span class="line">clawhub install ui-audit</span><br><span class="line"></span><br><span class="line"><span class="comment"># 地址</span></span><br><span class="line">https://clawhub.ai/tommygeoco/ui-audit</span><br></pre></td></tr></table></figure><h3 id="3-性能优化"><a href="#3-性能优化" class="headerlink" title="3. 性能优化"></a>3. 性能优化</h3><p><strong>Frontend Performance</strong> — 分析前端性能问题（LCP、FCP、CLS、Bundle Size），提供优化建议：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 安装</span></span><br><span class="line">clawhub install frontend-performance</span><br><span class="line"></span><br><span class="line"><span class="comment"># 地址</span></span><br><span class="line">https://clawhub.ai/wangzhiming1999/frontend-performance</span><br></pre></td></tr></table></figure><p><strong>Browser Devtools Inspector</strong> — 通过浏览器 DevTools 调试前端问题（Console、Network、Performance）：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 安装</span></span><br><span class="line">clawhub install qtada-browser-devtools-inspector</span><br><span class="line"></span><br><span class="line"><span class="comment"># 地址</span></span><br><span class="line">https://clawhub.ai/QtadaGM/qtada-browser-devtools-inspector</span><br></pre></td></tr></table></figure><h3 id="4-组件库相关"><a href="#4-组件库相关" class="headerlink" title="4. 组件库相关"></a>4. 组件库相关</h3><p><strong>Ant Design Skill</strong> — 高效构建 Ant Design v5+ React 组件库：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 安装</span></span><br><span class="line">clawhub install ant-design-skill</span><br><span class="line"></span><br><span class="line"><span class="comment"># 地址</span></span><br><span class="line">https://clawhub.ai/FelipeOFF/ant-design-skill</span><br></pre></td></tr></table></figure><p><strong>Component Api Design</strong> — 可复用组件 API 和文件结构设计：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 安装</span></span><br><span class="line">clawhub install component-api-design</span><br><span class="line"></span><br><span class="line"><span class="comment"># 地址</span></span><br><span class="line">https://clawhub.ai/wangzhiming1999/component-api-design</span><br></pre></td></tr></table></figure><h3 id="5-移动端开发"><a href="#5-移动端开发" class="headerlink" title="5. 移动端开发"></a>5. 移动端开发</h3><p><strong>React Native Skills</strong> — React Native 和 Expo 最佳实践：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 安装</span></span><br><span class="line">clawhub install vercel-react-native-skills</span><br><span class="line"></span><br><span class="line"><span class="comment"># 地址</span></span><br><span class="line">https://clawhub.ai/xaiohuangningde/vercel-react-native-skills</span><br></pre></td></tr></table></figure><p><strong>Radon AI</strong> — React Native 开发 AI 工具，支持查看日志、网络请求、组件树检查：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 安装</span></span><br><span class="line">clawhub install radon-ai</span><br><span class="line"></span><br><span class="line"><span class="comment"># 地址</span></span><br><span class="line">https://clawhub.ai/latekvo/radon-ai</span><br></pre></td></tr></table></figure><h2 id="四、重头戏：如何自定义开发一个-Skill"><a href="#四、重头戏：如何自定义开发一个-Skill" class="headerlink" title="四、重头戏：如何自定义开发一个 Skill"></a>四、重头戏：如何自定义开发一个 Skill</h2><p><img src="https://s.poetries.top/blog/8.png"></p><p>官方提供的 Skills 再多，也不可能覆盖所有场景。这时候，你需要自己动手开发定制技能。</p><h3 id="Skill-的基本结构"><a href="#Skill-的基本结构" class="headerlink" title="Skill 的基本结构"></a>Skill 的基本结构</h3><p>一个标准的 OpenClaw Skill 通常包含以下文件：</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">my-custom-skill/</span><br><span class="line">├── SKILL.md          # Skill的元信息和使用说明</span><br><span class="line">├── skill.json        # 配置文件</span><br><span class="line">├── main.py           # 主逻辑（或其他语言实现）</span><br><span class="line">└── requirements.txt  # 依赖列表</span><br></pre></td></tr></table></figure><h3 id="快速创建一个前端组件生成-Skill"><a href="#快速创建一个前端组件生成-Skill" class="headerlink" title="快速创建一个前端组件生成 Skill"></a>快速创建一个前端组件生成 Skill</h3><p><strong>第一步：<a href="http://创建skill.md/">创建 SKILL.md</a></strong></p><figure class="highlight markdown"><table><tr><td class="code"><pre><span class="line">---</span><br><span class="line">name: my-component-generator</span><br><span class="line"><span class="section">description: 自定义前端组件生成器</span></span><br><span class="line"><span class="section">---</span></span><br><span class="line"></span><br><span class="line"><span class="section"># My Component Generator</span></span><br><span class="line"></span><br><span class="line">用于快速生成前端组件代码。</span><br><span class="line"></span><br><span class="line"><span class="section">## 使用方法</span></span><br><span class="line"></span><br><span class="line"><span class="code">`gen component [组件名] [类型]`</span> - 生成指定类型的组件</span><br><span class="line"></span><br><span class="line">示例：</span><br><span class="line"></span><br><span class="line"><span class="bullet">-</span> <span class="code">`gen component Button primary`</span> - 生成主按钮组件</span><br><span class="line"><span class="bullet">-</span> <span class="code">`gen component Card dark`</span> - 生成暗色卡片组件</span><br></pre></td></tr></table></figure><p><strong>第二步：编写配置文件 skill.json</strong></p><figure class="highlight json"><table><tr><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;name&quot;</span><span class="punctuation">:</span> <span class="string">&quot;my-component-generator&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;version&quot;</span><span class="punctuation">:</span> <span class="string">&quot;1.0.0&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;description&quot;</span><span class="punctuation">:</span> <span class="string">&quot;自定义前端组件生成器&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;entry&quot;</span><span class="punctuation">:</span> <span class="string">&quot;main.py&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;dependencies&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span><span class="string">&quot;jinja2&quot;</span><span class="punctuation">]</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p><strong>第三步：编写主逻辑 <a href="http://main.py/">main.py</a></strong></p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> json</span><br><span class="line"><span class="keyword">from</span> jinja2 <span class="keyword">import</span> Template</span><br><span class="line"></span><br><span class="line"><span class="comment"># 组件模板</span></span><br><span class="line">BUTTON_TEMPLATE = <span class="string">&#x27;&#x27;&#x27;</span></span><br><span class="line"><span class="string">import React from &#x27;react&#x27;;</span></span><br><span class="line"><span class="string">import &#x27;./&#123;&#123; name &#125;&#125;.css&#x27;;</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">interface &#123;&#123; name &#125;&#125;Props &#123;</span></span><br><span class="line"><span class="string">  variant?: &#x27;primary&#x27; | &#x27;secondary&#x27; | &#x27;ghost&#x27;;</span></span><br><span class="line"><span class="string">  onClick?: () =&gt; void;</span></span><br><span class="line"><span class="string">  children: React.ReactNode;</span></span><br><span class="line"><span class="string">&#125;</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">export const &#123;&#123; name &#125;&#125;: React.FC&lt;&#123;&#123; name &#125;&#125;Props&gt; = (&#123;</span></span><br><span class="line"><span class="string">  variant = &#x27;primary&#x27;,</span></span><br><span class="line"><span class="string">  onClick,</span></span><br><span class="line"><span class="string">  children</span></span><br><span class="line"><span class="string">&#125;) =&gt; &#123;</span></span><br><span class="line"><span class="string">  return (</span></span><br><span class="line"><span class="string">    &lt;button classname=&quot;&#123;`btn&quot; btn-$&#123;variant&#125;`&#125;=&quot;&quot; onclick=&quot;&#123;onClick&#125;&quot;&gt;</span></span><br><span class="line"><span class="string">      &#123;children&#125;</span></span><br><span class="line"><span class="string">    &lt;/button&gt;</span></span><br><span class="line"><span class="string">  );</span></span><br><span class="line"><span class="string">&#125;;</span></span><br><span class="line"><span class="string">&#x27;&#x27;&#x27;</span></span><br><span class="line"></span><br><span class="line">CARD_TEMPLATE = <span class="string">&#x27;&#x27;&#x27;</span></span><br><span class="line"><span class="string">import React from &#x27;react&#x27;;</span></span><br><span class="line"><span class="string">import &#x27;./&#123;&#123; name &#125;&#125;.css&#x27;;</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">interface &#123;&#123; name &#125;&#125;Props &#123;</span></span><br><span class="line"><span class="string">  title: string;</span></span><br><span class="line"><span class="string">  content?: string;</span></span><br><span class="line"><span class="string">  variant?: &#x27;light&#x27; | &#x27;dark&#x27;;</span></span><br><span class="line"><span class="string">&#125;</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">export const &#123;&#123; name &#125;&#125;: React.FC&lt;&#123;&#123; name &#125;&#125;Props&gt; = (&#123;</span></span><br><span class="line"><span class="string">  title,</span></span><br><span class="line"><span class="string">  content,</span></span><br><span class="line"><span class="string">  variant = &#x27;light&#x27;</span></span><br><span class="line"><span class="string">&#125;) =&gt; &#123;</span></span><br><span class="line"><span class="string">  return (</span></span><br><span class="line"><span class="string">    &lt;div classname=&quot;&#123;`card&quot; card-$&#123;variant&#125;`&#125;=&quot;&quot;&gt;</span></span><br><span class="line"><span class="string">      &lt;h3 classname=&quot;card-title&quot;&gt;&#123;title&#125;&lt;/h3&gt;</span></span><br><span class="line"><span class="string">      &#123;content &amp;&amp; &lt;p classname=&quot;card-content&quot;&gt;&#123;content&#125;&lt;/p&gt;&#125;</span></span><br><span class="line"><span class="string">    &lt;/div&gt;</span></span><br><span class="line"><span class="string">  );</span></span><br><span class="line"><span class="string">&#125;;</span></span><br><span class="line"><span class="string">&#x27;&#x27;&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">handle</span>(<span class="params">request</span>):</span><br><span class="line">    message = request.get(<span class="string">&quot;message&quot;</span>, <span class="string">&quot;&quot;</span>).lower()</span><br><span class="line"></span><br><span class="line">    <span class="comment"># 解析命令: gen component Button primary</span></span><br><span class="line">    parts = message.split()</span><br><span class="line">    <span class="keyword">if</span> <span class="built_in">len</span>(parts) &lt; <span class="number">4</span> <span class="keyword">or</span> parts[<span class="number">0</span>] != <span class="string">&quot;gen&quot;</span> <span class="keyword">or</span> parts[<span class="number">1</span>] != <span class="string">&quot;component&quot;</span>:</span><br><span class="line">        <span class="keyword">return</span> &#123;</span><br><span class="line">            <span class="string">&quot;status&quot;</span>: <span class="string">&quot;error&quot;</span>,</span><br><span class="line">            <span class="string">&quot;message&quot;</span>: <span class="string">&quot;请使用格式：gen component [组件名] [类型]</span></span><br><span class="line"><span class="string">例如：gen component Button primary&quot;</span></span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">    component_name = parts[<span class="number">2</span>]</span><br><span class="line">    component_type = parts[<span class="number">3</span>]</span><br><span class="line"></span><br><span class="line">    <span class="comment"># 选择模板</span></span><br><span class="line">    templates = &#123;</span><br><span class="line">        <span class="string">&quot;button&quot;</span>: BUTTON_TEMPLATE,</span><br><span class="line">        <span class="string">&quot;card&quot;</span>: CARD_TEMPLATE,</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    template_key = component_type <span class="keyword">if</span> component_type <span class="keyword">in</span> templates <span class="keyword">else</span> <span class="string">&quot;button&quot;</span></span><br><span class="line">    template = Template(templates[template_key])</span><br><span class="line"></span><br><span class="line">    code = template.render(name=component_name)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> &#123;</span><br><span class="line">        <span class="string">&quot;status&quot;</span>: <span class="string">&quot;success&quot;</span>,</span><br><span class="line">        <span class="string">&quot;message&quot;</span>: <span class="string">f&quot;生成的 <span class="subst">&#123;component_name&#125;</span> 组件代码：</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">```<span class="subst">&#123;code&#125;</span>```&quot;</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> __name__ == <span class="string">&quot;__main__&quot;</span>:</span><br><span class="line">    test_request = &#123;<span class="string">&quot;message&quot;</span>: <span class="string">&quot;gen component MyButton primary&quot;</span>&#125;</span><br><span class="line">    <span class="built_in">print</span>(handle(test_request))</span><br></pre></td></tr></table></figure><h3 id="Skill-的触发机制"><a href="#Skill-的触发机制" class="headerlink" title="Skill 的触发机制"></a>Skill 的触发机制</h3><p>OpenClaw 的 Skills 通过<strong>关键词匹配</strong>或<strong>意图识别</strong>触发。配置时需要注意：</p><ol><li><p><strong>明确的触发词</strong> — 在 <a href="http://skill.md/">SKILL.md</a> 中用 <code>code</code> 格式标注命令格式</p></li><li><p><strong>合理的参数解析</strong> — 用户输入可能有多种表达方式，需要兼容</p></li><li><p><strong>清晰的错误提示</strong> — 当用户指令不明确时，给出正确的使用方式</p></li></ol><h3 id="发布你的-Skill"><a href="#发布你的-Skill" class="headerlink" title="发布你的 Skill"></a>发布你的 Skill</h3><p>开发完成后，可以通过以下方式分享：</p><ol><li><p><strong>提交到 ClawHub</strong> — 让更多开发者可以使用你的 Skill</p></li><li><p><strong>GitHub 仓库</strong> — 符合 OpenClaw 的目录结构后分享</p></li><li><p><strong>直接安装</strong> — 告诉朋友“请帮我安装这个 skills，github 链接是 xxx”</p></li></ol><h2 id="五、进阶技巧：前端-Skills-组合使用"><a href="#五、进阶技巧：前端-Skills-组合使用" class="headerlink" title="五、进阶技巧：前端 Skills 组合使用"></a>五、进阶技巧：前端 Skills 组合使用</h2><p><img src="https://s.poetries.top/blog/9.png"></p><p>单个 Skill 的能力有限，但组合使用会产生意想不到的效果。</p><h3 id="示例：自动化组件开发工作流"><a href="#示例：自动化组件开发工作流" class="headerlink" title="示例：自动化组件开发工作流"></a>示例：自动化组件开发工作流</h3><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">用户输入：帮我创建一个用户列表页面</span><br><span class="line"></span><br><span class="line">→ UI/UX Pro Max 确定页面布局和设计风格</span><br><span class="line">→ React 生成列表组件代码</span><br><span class="line">→ Frontend Performance 检查性能问题</span><br><span class="line">→ UI Audit 最终体验审核</span><br></pre></td></tr></table></figure><h3 id="示例：技术调研自动化"><a href="#示例：技术调研自动化" class="headerlink" title="示例：技术调研自动化"></a>示例：技术调研自动化</h3><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">用户输入：调研React 19的Server Actions</span><br><span class="line"></span><br><span class="line">→ GitHub 获取官方文档和RFC</span><br><span class="line">→ multi-search-engine 搜索技术博客讨论</span><br><span class="line">→ playwright-scraper-skill 抓取关键页面详情</span><br><span class="line">→ Summarize 生成调研报告</span><br></pre></td></tr></table></figure><h2 id="六、避坑指南"><a href="#六、避坑指南" class="headerlink" title="六、避坑指南"></a>六、避坑指南</h2><ol><li><p><strong>不要安装来源不明的 Skills</strong> — 安装前用 skill-vetter 扫描</p></li><li><p><strong>定期更新</strong> — 使用 auto-updater 保持 Skills 最新，但更新前做好测试</p></li><li><p><strong>注意 API 配额</strong> — 很多 Skills 依赖第三方 API，免费额度用完会失效</p></li><li><p><strong>敏感信息处理</strong> — 涉及 API Key 等敏感信息时，务必谨慎</p></li><li><p><strong>测试环境先行</strong> — 新安装的 Skills 先在非关键场景测试，确认稳定后再用于核心任务</p></li></ol><h2 id="七、更多前端-Skills-资源"><a href="#七、更多前端-Skills-资源" class="headerlink" title="七、更多前端 Skills 资源"></a>七、更多前端 Skills 资源</h2><p>如果你在寻找特定功能的 Skills，以下资源值得收藏：</p><table><thead><tr><th>资源站</th><th>链接</th></tr></thead><tbody><tr><td>ClawHub 官网</td><td><a href="https://clawhub.ai/">https://clawhub.ai/</a></td></tr><tr><td>Awesome OpenClaw Skills</td><td><a href="https://github.com/VoltAgent/awesome-openclaw-skills">https://github.com/VoltAgent/awesome-openclaw-skills</a></td></tr><tr><td>OpenClaw 官方 Skills</td><td><a href="https://github.com/openclaw/skills">https://github.com/openclaw/skills</a></td></tr></tbody></table><h3 id="其他常用检索-效率类-Skills"><a href="#其他常用检索-效率类-Skills" class="headerlink" title="其他常用检索&#x2F;效率类 Skills"></a>其他常用检索&#x2F;效率类 Skills</h3><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 网页检索</span></span><br><span class="line">clawhub install multi-search-engine</span><br><span class="line">clawhub install agent-reach</span><br><span class="line"></span><br><span class="line"><span class="comment"># 代码调试</span></span><br><span class="line">clawhub install playwright-scraper-skill</span><br><span class="line"></span><br><span class="line"><span class="comment"># 内容处理</span></span><br><span class="line">clawhub install summarize</span><br><span class="line">clawhub install humanizer</span><br><span class="line"></span><br><span class="line"><span class="comment"># 自我学习</span></span><br><span class="line">clawhub install self-improving-agent</span><br></pre></td></tr></table></figure><h2 id="结语"><a href="#结语" class="headerlink" title="结语"></a>结语</h2><p><img src="https://s.poetries.top/blog/10.png"></p><p>OpenClaw 的 Skills 生态为前端开发者提供了强大的能力扩展。从基础的 React&#x2F;Vue 组件生成，到复杂的 UI 设计系统，再到性能优化和调试——你的 AI 助手能帮你做多少事情，取决于你愿意投入多少精力去配置和打磨。</p><p>不要试图一步到位。从最需要的 1-2 个 Skills 开始，在使用中学习，在学习中扩展——这才是真正有效的进阶路径。</p><p>作为前端开发者，我个人最推荐优先安装：<strong>UI&#x2F;UX Pro Max + React + Frontend Design</strong>，这个组合已经能覆盖大部分日常开发需求。</p>]]>
    </content>
    <id>http://fe.poetries.top/2026/03/07/openclaws-skills-frontend-guide/</id>
    <link href="http://fe.poetries.top/2026/03/07/openclaws-skills-frontend-guide/"/>
    <published>2026-03-07T07:40:10.000Z</published>
    <summary>从Skills安装到自定义开发，手把手教你为前端开发场景构建AI助手技能矩阵，包含React/Vue/UI设计/性能优化等实用Skills及来源地址</summary>
    <title>OpenClaw Skills 进阶实战：前端开发者的AI技能库搭建指南</title>
    <updated>2026-03-08T10:22:42.091Z</updated>
  </entry>
  <entry>
    <author>
      <name>Poetry</name>
    </author>
    <category term="AI" scheme="http://fe.poetries.top/categories/AI/"/>
    <category term="Agent" scheme="http://fe.poetries.top/tags/Agent/"/>
    <category term="OpenClaw" scheme="http://fe.poetries.top/tags/OpenClaw/"/>
    <category term="AI助手" scheme="http://fe.poetries.top/tags/AI%E5%8A%A9%E6%89%8B/"/>
    <category term="MiniMax" scheme="http://fe.poetries.top/tags/MiniMax/"/>
    <category term="飞书机器人" scheme="http://fe.poetries.top/tags/%E9%A3%9E%E4%B9%A6%E6%9C%BA%E5%99%A8%E4%BA%BA/"/>
    <category term="Telegram" scheme="http://fe.poetries.top/tags/Telegram/"/>
    <category term="Discord" scheme="http://fe.poetries.top/tags/Discord/"/>
    <content>
      <![CDATA[<h2 id="内容总结"><a href="#内容总结" class="headerlink" title="内容总结"></a>内容总结</h2><p>在本篇文章中，我们将从浅入深，和大家一起学习以下知识：</p><ul><li>OpenClaw 平台的基本概念和核心优势</li><li>本地部署 OpenClaw 并集成 MiniMax M2.1 模型</li><li>打通飞书、Telegram、Discord 三大即时通讯渠道</li><li>配置多 Agent 团队，实现分工协作</li><li>Skill 技能扩展使用方法</li><li>云服务器部署完整流程</li></ul><hr><h2 id="导语"><a href="#导语" class="headerlink" title="导语"></a>导语</h2><p>在 AI 时代，如何让 AI 真正成为你的工作助手？市面上有很多 AI 聊天工具，但它们都有一个共同的问题——无法真正帮你操作电脑、执行任务。</p><p><strong>OpenClaw</strong>（原名 ClawdBot）是一个开源的个人 AI 助手平台，它能直接在你的设备上运行，通过多种渠道与你互动帮你完成实际工作。本文将详细介绍如何在本地和云端部署 OpenClaw，集成国产优秀的 MiniMax M2.1 大模型，并配置飞书、Telegram、Discord 等多渠道接入，最终搭建出一个24小时帮你干活的 AI 团队。</p><hr><h2 id="什么是-OpenClaw"><a href="#什么是-OpenClaw" class="headerlink" title="什么是 OpenClaw"></a>什么是 OpenClaw</h2><p>OpenClaw（原名 ClawdBot）是一个开源的个人 AI 助手平台，运行在你自己的设备上。它支持通过 WhatsApp、Telegram、Slack、Discord、飞书、钉钉、QQ、企业微信等多个平台与你互动。</p><p>其特点包括：</p><ul><li><strong>本地优先</strong>：运行在本地设备，数据完全由自己掌控</li><li><strong>多平台支持</strong>：支持 macOS、Linux、Windows（WSL2）</li><li><strong>多通道连接</strong>：可接入 WhatsApp、Telegram、Slack、Discord、飞书、钉钉、QQ、企业微信等</li><li><strong>24⁄7 在线</strong>：以后台服务形式持续运行</li><li><strong>高度可定制</strong>：支持技能扩展与自定义配置</li></ul><p><a href="https://www.minimax.io/">MiniMax</a> 是国内领先的 AI 大模型公司，其发布的 <strong>M2.1</strong> 系列模型在编程能力上表现出色，尤其擅长多语言编程、Web&#x2F;App 开发、Agent 框架集成等场景。</p><p>本文将详细介绍如何在 <strong>OpenClaw</strong> 中集成 MiniMax 模型，让你的 AI 助手使用上这个强大的编程模型。</p><hr><h2 id="为什么选择-MiniMax？"><a href="#为什么选择-MiniMax？" class="headerlink" title="为什么选择 MiniMax？"></a>为什么选择 MiniMax？</h2><p>MiniMax M2.1 是国产大模型中的性价比之选，特别适合日常开发和办公场景。</p><table><thead><tr><th>特性</th><th>说明</th></tr></thead><tbody><tr><td><strong>多语言编程</strong></td><td>全面支持 Rust、Java、Go、C++、Kotlin、Objective-C、TypeScript、JavaScript 等</td></tr><tr><td><strong>Web&#x2F;App 开发</strong></td><td>显著提升移动端开发能力，设计美学表现优秀</td></tr><tr><td><strong>高性价比</strong></td><td>输入成本仅 $0.15&#x2F;M tokens，输出 $0.60&#x2F;M tokens</td></tr><tr><td><strong>Agent 友好</strong></td><td>完美兼容 Claude Code、Cline、Kilo Code 等主流 AI 编程工具</td></tr><tr><td><strong>响应更简洁</strong></td><td>相比 M2 代，回答更简洁，token 消耗更低</td></tr></tbody></table><blockquote><p>💡 <strong>小贴士</strong>：MiniMax 还有一个 <strong>Coding Plan</strong>，通过 OAuth 认证可以更便捷地使用，而且有推荐链接可以享受 10% 折扣。</p></blockquote><blockquote><p>📝 <strong>使用建议</strong>：选择国产 MiniMax 可以有效节省 Token 费用，日常对话完全满足需求。如果需要深度编程处理复杂任务，建议还是选择国外的顶级大模型（如 Claude、GPT-4 等）。</p></blockquote><hr><h2 id="本地部署-OpenClaw"><a href="#本地部署-OpenClaw" class="headerlink" title="本地部署 OpenClaw"></a>本地部署 OpenClaw</h2><h3 id="第一步：全局安装-OpenClaw"><a href="#第一步：全局安装-OpenClaw" class="headerlink" title="第一步：全局安装 OpenClaw"></a>第一步：全局安装 OpenClaw</h3><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">npm i openclaw -g</span><br></pre></td></tr></table></figure><p>进入初始化流程</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">openclaw onboard</span><br></pre></td></tr></table></figure><p><img src="https://s.poetries.top/uploads/2026/03/5b07c38e9c49ba41.png" alt="执行 onboard 命令启动初始化"></p><p>选择国产模型</p><p><img src="https://s.poetries.top/uploads/2026/03/6d0577d801e28895.png" alt="选择 MiniMax 国产模型"></p><p>选择国内渠道</p><p><img src="https://s.poetries.top/uploads/2026/03/b4bc022333be19d8.png" alt="选择国内渠道"></p><p>配置模型 API KEY</p><p><img src="https://s.poetries.top/uploads/2026/03/285ba810e676c13e.png" alt="填写 MiniMax API Key"></p><p>配置 channel，暂时跳过</p><p><img src="https://s.poetries.top/uploads/2026/03/03499a2a5fe52723.png" alt="暂时跳过 channel 配置"></p><p>重启服务</p><p><img src="https://s.poetries.top/uploads/2026/03/dd37a585d04a1991.png" alt="重启 Gateway 服务"></p><p><img src="https://s.poetries.top/uploads/2026/03/ed22bd95a2994b7a.png" alt="Gateway 重启成功"></p><p>打开网页聊天窗口</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">http://127.0.0.1:18789/chat</span><br></pre></td></tr></table></figure><p>可以看到模型已经配置好，正常能对话</p><p><img src="https://s.poetries.top/uploads/2026/03/847549ba069a9ff9.png" alt="网页端聊天验证"></p><blockquote><p>📌 <strong>常用命令</strong></p><ul><li>重启网关：<code>openclaw gateway restart</code></li><li>打开控制台：<code>openclaw dashboard</code></li></ul></blockquote><hr><h2 id="配置-Channel"><a href="#配置-Channel" class="headerlink" title="配置 Channel"></a>配置 Channel</h2><h3 id="接入飞书机器人"><a href="#接入飞书机器人" class="headerlink" title="接入飞书机器人"></a>接入飞书机器人</h3><h4 id="第一步：创建飞书应用"><a href="#第一步：创建飞书应用" class="headerlink" title="第一步：创建飞书应用"></a>第一步：创建飞书应用</h4><p><img src="https://s.poetries.top/uploads/2026/03/80e58cc162aea35a.png" alt="在飞书开放平台创建应用"></p><p>先记录 App ID 和 App Secret 待会用到</p><p><img src="https://s.poetries.top/uploads/2026/03/02a76ada35b33ef5.png" alt="记录 App ID 和 Secret"></p><h4 id="第二步：启用飞书插件"><a href="#第二步：启用飞书插件" class="headerlink" title="第二步：启用飞书插件"></a>第二步：启用飞书插件</h4><p>在 OpenClaw 中添加飞书 channel，使用 <code>openclaw plugins enable feishu</code> 开启 OpenClaw 内置的飞书插件（默认是 disable 状态，loaded 为开启）。</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">$ openclaw plugins <span class="built_in">enable</span> feishu</span><br><span class="line"></span><br><span class="line">🦞 OpenClaw 2026.3.1 (2a8ac97) — I<span class="string">&#x27;m the reason your shell history looks like a hacker-movie montage.</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">Config overwrite: /Users/poetry/.openclaw/openclaw.json (sha256 99987a003894db97f40f13fdafda67780c913cb8ffda6632009382c65c8d773358c558470 -&gt; 66f41f7500d18f52a1a794d7cfb8f1cc02fda411599ca6ea6f9d879378d5e53f65f, backup=/Users/poetry/.openclaw/openclaw.json.bak)</span></span><br><span class="line"><span class="string">Enabled plugin &quot;feishu&quot;. Restart the gateway to apply.</span></span><br></pre></td></tr></table></figure><p>查看插件列表</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">openclaw plugins list</span><br></pre></td></tr></table></figure><p><img src="https://s.poetries.top/uploads/2026/03/e3ec6c5bfc088fbc.png" alt="查看已启用的插件"></p><p>然后使用 <code>openclaw channels add</code> 配置 channel</p><p><img src="https://s.poetries.top/uploads/2026/03/36bc4879f86a7855.png" alt="添加 channel"></p><p>按提示输入 APP ID、APP Secret</p><p><img src="https://s.poetries.top/uploads/2026/03/a13e6218bf519195.png" alt="输入飞书应用凭证"></p><p>群聊策略选择 Open：允许响应所有的群聊，Allowlist：在白名单的群聊可以响应</p><p><img src="https://s.poetries.top/uploads/2026/03/cf590b77666eb29a.png" alt="选择群聊策略"></p><p>然后选择完成即可</p><p><img src="https://s.poetries.top/uploads/2026/03/ed3985f8338e14cf.png" alt="完成 channel 配置"></p><p>然后打开网页端查看飞书运行状态是正常的</p><p><img src="https://s.poetries.top/uploads/2026/03/99de189e68dbd734.png" alt="查看飞书运行状态"></p><p>接下来配置飞书应用机器人能力与消息权限</p><p><img src="https://s.poetries.top/uploads/2026/03/8da4addf89381d8e.png" alt="配置飞书应用"></p><p>为应用开通相关权限，批量导入下方权限：</p><figure class="highlight json"><table><tr><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;scopes&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;tenant&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">      <span class="string">&quot;cardkit:card:write&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="string">&quot;contact:contact.base:readonly&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="string">&quot;contact:user.base:readonly&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="string">&quot;im:chat:readonly&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="string">&quot;im:message&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="string">&quot;im:message.group_at_msg:readonly&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="string">&quot;im:message.group_msg&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="string">&quot;im:message.p2p_msg:readonly&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="string">&quot;im:message.reactions:read&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="string">&quot;im:message:readonly&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="string">&quot;im:message:recall&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="string">&quot;im:message:send_as_bot&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="string">&quot;im:message:update&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="string">&quot;im:resource&quot;</span></span><br><span class="line">    <span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;user&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span><span class="string">&quot;contact:contact.base:readonly&quot;</span><span class="punctuation">]</span></span><br><span class="line">  <span class="punctuation">&#125;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p><img src="https://s.poetries.top/uploads/2026/03/04836256c3f60e9b.png" alt="添加权限"></p><p><img src="https://s.poetries.top/uploads/2026/03/a2f0e75fd92e27b9.png" alt="权限添加成功"></p><p>为应用订阅相关事件</p><p><img src="https://s.poetries.top/uploads/2026/03/c3c855b3178bc856.png" alt="订阅事件"></p><p>需要订阅以下事件：</p><ul><li><code>im.message.receive_v1</code> - 接收消息</li><li><code>im.message.message_read_v1</code> - 消息已读回执</li><li><code>im.chat.member.bot.added_v1</code> - 机器人进群</li><li><code>im.chat.member.bot.deleted_v1</code> - 机器人被移出群</li></ul><p>创建版本并发布应用才会生效</p><p><img src="https://s.poetries.top/uploads/2026/03/edbf3b8c571e8c00.png" alt="发布版本"></p><p><img src="https://s.poetries.top/uploads/2026/03/dcc5cf800fe0cbe7.png" alt="发布成功"></p><h4 id="第三步：接入飞书群聊"><a href="#第三步：接入飞书群聊" class="headerlink" title="第三步：接入飞书群聊"></a>第三步：接入飞书群聊</h4><p>用手机或者电脑打开飞书客户端，创建一个测试群：</p><p><img src="https://s.poetries.top/uploads/2026/03/84af16f832e28a79.png" alt="创建飞书群"></p><p>然后添加机器人到群聊里面</p><p><img src="https://s.poetries.top/uploads/2026/03/4ed3d1683a44bb61.png" alt="添加机器人"></p><p><img src="https://s.poetries.top/uploads/2026/03/4a4121d36a00578e.png" alt="选择要添加的机器人"></p><p><img src="https://s.poetries.top/uploads/2026/03/a74dbe52d2c76274.png" alt="确认添加"></p><p>接下来和 openclaw 开始聊天，如果能正常回复就是跑通了流程</p><p><img src="https://s.poetries.top/uploads/2026/03/a599641c1a495f6f.png" alt="飞书对话测试"></p><p><img src="https://s.poetries.top/uploads/2026/03/0188e37285cdf8fc.png" alt="收到回复"></p><h4 id="第四步：增加云文档权限（可选）"><a href="#第四步：增加云文档权限（可选）" class="headerlink" title="第四步：增加云文档权限（可选）"></a>第四步：增加云文档权限（可选）</h4><p>此外我们可以增加云文档的保存权限，让 openclaw 自动保存内容到云文档</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  &quot;scopes&quot;: &#123;</span><br><span class="line">    &quot;tenant&quot;: [</span><br><span class="line">      &quot;docx:document&quot;,</span><br><span class="line">      &quot;docx:document.block:convert&quot;,</span><br><span class="line">      &quot;docx:document:create&quot;,</span><br><span class="line">      &quot;docx:document:readonly&quot;,</span><br><span class="line">      &quot;docx:document:write_only&quot;,</span><br><span class="line">      &quot;space:document:delete&quot;,</span><br><span class="line">      &quot;space:document:move&quot;,</span><br><span class="line">      &quot;space:document:retrieve&quot;,</span><br><span class="line">      &quot;space:folder:create&quot;</span><br><span class="line">    ],</span><br><span class="line">    &quot;user&quot;: [</span><br><span class="line">      &quot;contact:contact.base:readonly&quot;</span><br><span class="line">    ]</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>新增权限后，需要重新发布新版本</p><p><img src="https://s.poetries.top/uploads/2026/03/8fbccd3b21a0443c.png" alt="重新发布版本"></p><p><img src="https://s.poetries.top/uploads/2026/03/d37fb39253d04b4b.png" alt="发布成功"></p><blockquote><p>⚠️ <strong>注意</strong>：必须先配置完 OpenClaw 添加飞书配置后，才可以在飞书开放平台选择下面的事件选项</p></blockquote><p><img src="https://s.poetries.top/uploads/2026/03/6dba48f7a14ff34e.png" alt="配置事件选项"></p><hr><h2 id="打通-OpenClaw-与-Telegram"><a href="#打通-OpenClaw-与-Telegram" class="headerlink" title="打通 OpenClaw 与 Telegram"></a>打通 OpenClaw 与 Telegram</h2><h4 id="第一步：创建-Telegram-Bot"><a href="#第一步：创建-Telegram-Bot" class="headerlink" title="第一步：创建 Telegram Bot"></a>第一步：创建 Telegram Bot</h4><p>在 Telegram 中搜索 @BotFather，进入聊天界面</p><p><img src="https://s.poetries.top/uploads/2026/03/08d2ea837734bb45.png" alt="搜索 BotFather"></p><p>输入 <code>/newbot</code>，按提示设置机器人名称和用户名，完成后会收到一个 API Token，请务必保存好，后面配置时需要用到</p><p><img src="https://s.poetries.top/uploads/2026/03/5819f2791c2b871f.png" alt="创建 Bot"></p><h4 id="第二步：配置-Token"><a href="#第二步：配置-Token" class="headerlink" title="第二步：配置 Token"></a>第二步：配置 Token</h4><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">openclaw channels add</span><br></pre></td></tr></table></figure><p><img src="https://s.poetries.top/uploads/2026/03/58258eefa86c06da.png" alt="添加 Telegram channel"></p><p><img src="https://s.poetries.top/uploads/2026/03/6cdd6208fb883048.png" alt="选择 Telegram"></p><p>接下来进入配对模式，系统会询问安全策略</p><p><img src="https://s.poetries.top/uploads/2026/03/526b12427ab38a49.png" alt="安全策略"></p><p>选择配对</p><p><img src="https://s.poetries.top/uploads/2026/03/166719261a10c98d.png" alt="选择配对"></p><blockquote><p>⚠️ <strong>注意</strong>：到这里需要重启服务，否则下面获取配对码不生效 <code>openclaw gateway restart</code></p></blockquote><h4 id="第三步：获取配对码"><a href="#第三步：获取配对码" class="headerlink" title="第三步：获取配对码"></a>第三步：获取配对码</h4><p>搜索你刚创建的 Bot 用户名，进入聊天界面，点击 Start</p><p><img src="https://s.poetries.top/uploads/2026/03/a13fe861dd497faa.png" alt="启动 Bot"></p><p>随便发送一条消息，Bot 会自动回复一个配对码（Pairing Code），复制保存，配对环节会用到</p><p><img src="https://s.poetries.top/uploads/2026/03/6cdd6208fb883048.png" alt="获取配对码"></p><blockquote><p>💡 <strong>小技巧</strong>：如果获取配对码没有返回，可以在 web 聊天窗口让 OpenClaw 自己配置代理</p></blockquote><p><img src="https://s.poetries.top/uploads/2026/03/45407451f163ccfd.png" alt="配置代理"></p><h4 id="第四步：完成配对"><a href="#第四步：完成配对" class="headerlink" title="第四步：完成配对"></a>第四步：完成配对</h4><p>在终端执行以下命令，将 OpenClaw 与你的 Bot 绑定：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">openclaw pairing approve telegram 你的配对码</span><br></pre></td></tr></table></figure><p><img src="https://s.poetries.top/uploads/2026/03/f60d28a2cfa43fab.png" alt="执行配对命令"></p><p>看到成功提示即表示配对完成</p><p>现在回到 Telegram 中，向你的 Bot 发送任意消息。如果一切顺利，你会收到来自 OpenClaw 的回复，说明 Telegram 接入已成功生效</p><p><img src="https://s.poetries.top/uploads/2026/03/826c1145d4571726.png" alt="Telegram 对话测试"></p><p><img src="https://s.poetries.top/uploads/2026/03/bea704a7d33bf2b6.png" alt="收到回复"></p><h2 id="打通-OpenClaw-与-Discord"><a href="#打通-OpenClaw-与-Discord" class="headerlink" title="打通 OpenClaw 与 Discord"></a>打通 OpenClaw 与 Discord</h2><p>有上网条件的可以考虑接入 <code>Discord</code>，它多频道（Channel）的设计天生就适合 <code>OpenClaw</code> 的多 <code>Agent</code> 场景，用起来很丝滑</p><h4 id="第一步：新建-Discord-应用"><a href="#第一步：新建-Discord-应用" class="headerlink" title="第一步：新建 Discord 应用"></a>第一步：新建 Discord 应用</h4><p>打开 Discord 开发者平台，新建应用：OpenClaw</p><p><a href="https://discord.com/developers/applications">https://discord.com/developers/applications</a></p><p>开启配置</p><p><img src="https://s.poetries.top/uploads/2026/03/5a5b0bb086fb62d2.png" alt="创建 Discord 应用"></p><p>复制 Token</p><p><img src="https://s.poetries.top/uploads/2026/03/8837699167c33ba8.png" alt="复制 Token"></p><p>配置 OAuth2</p><p><img src="https://s.poetries.top/uploads/2026/03/a70b67edefa61671.png" alt="配置 OAuth2"></p><p>配置 Bot Permissions</p><p><img src="https://s.poetries.top/uploads/2026/03/a76b6ab573f310b1.png" alt="配置权限"></p><p>拷贝生成的 URL</p><p><img src="https://s.poetries.top/uploads/2026/03/f9d598e83b8a105d.png" alt="复制授权 URL"></p><h4 id="第二步：新建-Discord-Server-并加入-Bot"><a href="#第二步：新建-Discord-Server-并加入-Bot" class="headerlink" title="第二步：新建 Discord Server 并加入 Bot"></a>第二步：新建 Discord Server 并加入 Bot</h4><p>新建 Discord 的 Server：</p><p><img src="https://s.poetries.top/uploads/2026/03/7acbaeb1e45d8227.png" alt="创建 Server"></p><p>将创建的 Bot 加到 Server，拷贝上面复制的 URL 到浏览器打开，点击 ‘Continue’：</p><p><img src="https://s.poetries.top/uploads/2026/03/a75fb51d680e2ca2.png" alt="授权添加"></p><h4 id="第三步：OpenClaw-添加-Discord-Channel"><a href="#第三步：OpenClaw-添加-Discord-Channel" class="headerlink" title="第三步：OpenClaw 添加 Discord Channel"></a>第三步：OpenClaw 添加 Discord Channel</h4><p>使用 <code>openclaw channels add</code> 命令配置 Channel。</p><p><img src="https://s.poetries.top/uploads/2026/03/0ddd5ba5fe4c8ccd.png" alt="添加 Discord Channel"></p><p>输入之前获取的 Token</p><p><img src="https://s.poetries.top/uploads/2026/03/0bb33e2dc0595caf.png" alt="输入 Token"></p><p><img src="https://s.poetries.top/uploads/2026/03/51feb90395a3e274.png" alt="选择 Discord"></p><p>然后点击完成</p><p><img src="https://s.poetries.top/uploads/2026/03/e88638af6a5dc18d.png" alt="完成配置"></p><h4 id="第四步：将-Discord-与-OpenClaw-配对"><a href="#第四步：将-Discord-与-OpenClaw-配对" class="headerlink" title="第四步：将 Discord 与 OpenClaw 配对"></a>第四步：将 Discord 与 OpenClaw 配对</h4><p>回到 Discord 创建的频道，点击右上角的”显示成员”，可以看到当前频道成员。点击我们添加的 Bot：OpenClaw。</p><p><img src="https://s.poetries.top/uploads/2026/03/d0212e47cd3f33a4.png" alt="查看成员"></p><p>随意发送一句话获取配对码</p><p><img src="https://s.poetries.top/uploads/2026/03/16a429d3750ac388.png" alt="获取配对码"></p><p><img src="https://s.poetries.top/uploads/2026/03/66085bada9ddf69e.png" alt="配对码"></p><blockquote><p>⚠️ <strong>注意</strong>：如果发送了没有回复，请检查是否配置了代理，或者换个好点的代理，大部分情况是代理问题</p></blockquote><p>打开一个新的终端窗口，输入以下命令：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">openclaw pairing approve discord &lt;Pairing code&gt;</span><br></pre></td></tr></table></figure><p>将 <code>&lt;Pairing code&gt;</code> 替换为刚才复制的配对码。</p><p>配对成功</p><p><img src="https://s.poetries.top/uploads/2026/03/1a0d8c68f6c6d9d8.png" alt="配对成功"></p><p>然后重启网关</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">openclaw gateway</span><br></pre></td></tr></table></figure><h4 id="第五步：测试"><a href="#第五步：测试" class="headerlink" title="第五步：测试"></a>第五步：测试</h4><p>现在回到 Discord 的服务器频道，在频道中 @ 你创建的机器人：</p><p><img src="https://s.poetries.top/uploads/2026/03/59769991d70ed6de.png" alt="@机器人"></p><p>可以看到已经可以回复了，至此，OpenClaw 已成功与 Discord 打通。现在你可以在 Discord 中通过与 Bot 对话的方式，指挥 OpenClaw 操控你的电脑了。</p><p>Discord 拥有多平台客户端，你也可以在手机上安装 Discord，通过手机指挥 OpenClaw 工作。</p><p><img src="https://s.poetries.top/uploads/2026/03/a3644daa845bef44.png" alt="手机端使用"></p><hr><h2 id="多-Agent-团队配置"><a href="#多-Agent-团队配置" class="headerlink" title="多 Agent 团队配置"></a>多 Agent 团队配置</h2><h3 id="理解-Agent-架构"><a href="#理解-Agent-架构" class="headerlink" title="理解 Agent 架构"></a>理解 Agent 架构</h3><p>在 OpenClaw 中，一个 Agent 不只是一个名字，它是一个独立的”虚拟员工”，拥有自己的各个组成部分，包括：</p><ul><li><strong>Workspace（工作区）</strong>：它的个人办公室（”灵魂三件套”、长期记忆）</li><li><strong>AgentDir（状态目录）</strong>：它的身份证（认证信息、模型配置）</li><li><strong>Sessions（会话存储）</strong>：它的私人记忆（独立的聊天记录，不跟别人串味）</li></ul><p>其中 Workspace 目录下的 <code>AGENTS.md</code>、<code>SOUL.md</code>、<code>USER.md</code> 文件是 Agent 的核心，被称为 Agent 的”灵魂三件套”</p><h4 id="1-SOUL-md-—-AI-的灵魂"><a href="#1-SOUL-md-—-AI-的灵魂" class="headerlink" title="1. SOUL.md — AI 的灵魂"></a>1. <code>SOUL.md</code> — AI 的灵魂</h4><p>定义 AI 的性格和行为准则：</p><ul><li><strong>Core Truths</strong> — 核心原则（不说废话、有观点、谨慎使用外部工具）</li><li><strong>Vibe</strong> — 风格定位（不像机器人，像真实助手）</li><li><strong>Boundaries</strong> — 边界（隐私保护、谨慎对外）</li></ul><h4 id="2-USER-md-—-用户画像"><a href="#2-USER-md-—-用户画像" class="headerlink" title="2. USER.md — 用户画像"></a>2. <code>USER.md</code> — 用户画像</h4><p>记录关于用户的信息：</p><ul><li>姓名、称呼、时区</li><li>偏好、项目、痛点</li><li>随对话不断更新</li></ul><h4 id="3-AGENTS-md-—-工作规范"><a href="#3-AGENTS-md-—-工作规范" class="headerlink" title="3. AGENTS.md — 工作规范"></a>3. <code>AGENTS.md</code> — 工作规范</h4><p>定义工作方式和记忆机制：</p><ul><li>每次会话必读：<code>SOUL.md</code> → <code>USER.md</code> → <code>memory/</code></li><li>写作规范：<code>TEXT</code> &gt; <code>BRAIN</code>（随时记录）</li><li>心跳任务：定期检查 + 主动汇报</li><li>群聊礼仪：知道何时说话、何时沉默</li></ul><blockquote><p>💡 通过调整 <code>workspace</code> 下的这3个文件，就可以将 Agent 从”通用 AI”变成”你的专属 AI”。</p></blockquote><hr><h2 id="实战：搭建多-Agent-团队"><a href="#实战：搭建多-Agent-团队" class="headerlink" title="实战：搭建多 Agent 团队"></a>实战：搭建多 Agent 团队</h2><h3 id="第一步：创建飞书机器人"><a href="#第一步：创建飞书机器人" class="headerlink" title="第一步：创建飞书机器人"></a>第一步：创建飞书机器人</h3><p>OpenClaw 默认是单 Agent 模式，可以通过下面命令新建 Agent：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">openclaw agents add coding</span><br></pre></td></tr></table></figure><p>我们的目标是在飞书创建一个 AI 团队，包含：大总管 poetry（之前已经创建了）、产品助理、设计助理、开发助理、内容助理、运营增长、财务助理。</p><p>6 个飞书机器人，每个都是独立的 AI Agent，有自己的人设，有自己的记忆，有自己的工作区。</p><blockquote><p>请认真按照上面的步骤，创建 6 个飞书机器人，6 套独立的 App ID + App Secret</p></blockquote><h3 id="第二步：OpenClaw-多-Agent-配置"><a href="#第二步：OpenClaw-多-Agent-配置" class="headerlink" title="第二步：OpenClaw 多 Agent 配置"></a>第二步：OpenClaw 多 Agent 配置</h3><p>每个飞书机器人可以绑定一个独立的 <code>OpenClaw Agent</code>。</p><p>分别创建 6 个 Agent：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">openclaw agents add product</span><br><span class="line">openclaw agents add ui</span><br><span class="line">openclaw agents add coding</span><br><span class="line">openclaw agents add content</span><br><span class="line">openclaw agents add op-growth</span><br><span class="line">openclaw agents add finance</span><br></pre></td></tr></table></figure><p><img src="https://s.poetries.top/uploads/2026/03/29a74ab1b6ed3897.png" alt="创建多个 Agent"></p><p>创建完后，在 <code>~/.openclaw/openclaw.json</code> 中可以看到</p><figure class="highlight json"><table><tr><td class="code"><pre><span class="line"><span class="attr">&quot;agents&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;list&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">    <span class="punctuation">&#123;</span></span><br><span class="line">      <span class="attr">&quot;id&quot;</span><span class="punctuation">:</span> <span class="string">&quot;main&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;name&quot;</span><span class="punctuation">:</span> <span class="string">&quot;poetry的AI助理-主应用&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;workspace&quot;</span><span class="punctuation">:</span> <span class="string">&quot;/Users/poetry/.openclaw/workspace&quot;</span></span><br><span class="line">    <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="punctuation">&#123;</span></span><br><span class="line">      <span class="attr">&quot;id&quot;</span><span class="punctuation">:</span> <span class="string">&quot;coding&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;name&quot;</span><span class="punctuation">:</span> <span class="string">&quot;coding&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;workspace&quot;</span><span class="punctuation">:</span> <span class="string">&quot;/Users/poetry/.openclaw/workspace-coding&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;agentDir&quot;</span><span class="punctuation">:</span> <span class="string">&quot;/Users/poetry/.openclaw/agents/coding/agent&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;model&quot;</span><span class="punctuation">:</span> <span class="string">&quot;minimax-cn/MiniMax-M2.5&quot;</span></span><br><span class="line">    <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="punctuation">&#123;</span></span><br><span class="line">      <span class="attr">&quot;id&quot;</span><span class="punctuation">:</span> <span class="string">&quot;product&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;name&quot;</span><span class="punctuation">:</span> <span class="string">&quot;product&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;workspace&quot;</span><span class="punctuation">:</span> <span class="string">&quot;/Users/poetry/.openclaw/workspace-product&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;agentDir&quot;</span><span class="punctuation">:</span> <span class="string">&quot;/Users/poetry/.openclaw/agents/product/agent&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;model&quot;</span><span class="punctuation">:</span> <span class="string">&quot;minimax-cn/MiniMax-M2.5&quot;</span></span><br><span class="line">    <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="punctuation">&#123;</span></span><br><span class="line">      <span class="attr">&quot;id&quot;</span><span class="punctuation">:</span> <span class="string">&quot;ui&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;name&quot;</span><span class="punctuation">:</span> <span class="string">&quot;ui&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;workspace&quot;</span><span class="punctuation">:</span> <span class="string">&quot;/Users/poetry/.openclaw/workspace-ui&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;agentDir&quot;</span><span class="punctuation">:</span> <span class="string">&quot;/Users/poetry/.openclaw/agents/ui/agent&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;model&quot;</span><span class="punctuation">:</span> <span class="string">&quot;minimax-cn/MiniMax-M2.5&quot;</span></span><br><span class="line">    <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="punctuation">&#123;</span></span><br><span class="line">      <span class="attr">&quot;id&quot;</span><span class="punctuation">:</span> <span class="string">&quot;content&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;name&quot;</span><span class="punctuation">:</span> <span class="string">&quot;content&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;workspace&quot;</span><span class="punctuation">:</span> <span class="string">&quot;/Users/poetry/.openclaw/workspace-content&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;agentDir&quot;</span><span class="punctuation">:</span> <span class="string">&quot;/Users/poetry/.openclaw/agents/content/agent&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;model&quot;</span><span class="punctuation">:</span> <span class="string">&quot;minimax-cn/MiniMax-M2.5&quot;</span></span><br><span class="line">    <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="punctuation">&#123;</span></span><br><span class="line">      <span class="attr">&quot;id&quot;</span><span class="punctuation">:</span> <span class="string">&quot;op-growth&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;name&quot;</span><span class="punctuation">:</span> <span class="string">&quot;op-growth&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;workspace&quot;</span><span class="punctuation">:</span> <span class="string">&quot;/Users/poetry/.openclaw/workspace-op-growth&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;agentDir&quot;</span><span class="punctuation">:</span> <span class="string">&quot;/Users/poetry/.openclaw/agents/op-growth/agent&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;model&quot;</span><span class="punctuation">:</span> <span class="string">&quot;minimax-cn/MiniMax-M2.5&quot;</span></span><br><span class="line">    <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="punctuation">&#123;</span></span><br><span class="line">      <span class="attr">&quot;id&quot;</span><span class="punctuation">:</span> <span class="string">&quot;finance&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;name&quot;</span><span class="punctuation">:</span> <span class="string">&quot;finance&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;workspace&quot;</span><span class="punctuation">:</span> <span class="string">&quot;/Users/poetry/.openclaw/workspace-finance&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;agentDir&quot;</span><span class="punctuation">:</span> <span class="string">&quot;/Users/poetry/.openclaw/agents/finance/agent&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;model&quot;</span><span class="punctuation">:</span> <span class="string">&quot;minimax-cn/MiniMax-M2.5&quot;</span></span><br><span class="line">    <span class="punctuation">&#125;</span></span><br><span class="line">  <span class="punctuation">]</span></span><br><span class="line"><span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br></pre></td></tr></table></figure><p><img src="https://s.poetries.top/uploads/2026/03/148ece432db6f25d.png" alt="Agent 列表"></p><p>每个 Agent 有自己的工作目录，这是隔离的基础</p><h4 id="第三步：配置飞书多账户通道"><a href="#第三步：配置飞书多账户通道" class="headerlink" title="第三步：配置飞书多账户通道"></a>第三步：配置飞书多账户通道</h4><p>使用 <code>openclaw channels add</code> 配置单个 Channel。</p><p>修改 <code>~/.openclaw/openclaw.json</code> 中的 <code>channels</code></p><figure class="highlight json"><table><tr><td class="code"><pre><span class="line"><span class="attr">&quot;channels&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;feishu&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;enabled&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;accounts&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">      <span class="attr">&quot;main&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">        <span class="attr">&quot;appId&quot;</span><span class="punctuation">:</span> <span class="string">&quot;cli1&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;appSecret&quot;</span><span class="punctuation">:</span> <span class="string">&quot;你的secret1&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;domain&quot;</span><span class="punctuation">:</span> <span class="string">&quot;feishu&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;groupPolicy&quot;</span><span class="punctuation">:</span> <span class="string">&quot;open&quot;</span></span><br><span class="line">      <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;coding&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">        <span class="attr">&quot;appId&quot;</span><span class="punctuation">:</span> <span class="string">&quot;cli2&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;appSecret&quot;</span><span class="punctuation">:</span> <span class="string">&quot;你的secret2&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;domain&quot;</span><span class="punctuation">:</span> <span class="string">&quot;feishu&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;groupPolicy&quot;</span><span class="punctuation">:</span> <span class="string">&quot;open&quot;</span></span><br><span class="line">      <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;product&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">        <span class="attr">&quot;appId&quot;</span><span class="punctuation">:</span> <span class="string">&quot;cli3&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;appSecret&quot;</span><span class="punctuation">:</span> <span class="string">&quot;你的secret3&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;domain&quot;</span><span class="punctuation">:</span> <span class="string">&quot;feishu&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;groupPolicy&quot;</span><span class="punctuation">:</span> <span class="string">&quot;open&quot;</span></span><br><span class="line">      <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;ui&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">        <span class="attr">&quot;appId&quot;</span><span class="punctuation">:</span> <span class="string">&quot;cli4&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;appSecret&quot;</span><span class="punctuation">:</span> <span class="string">&quot;你的secret4&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;domain&quot;</span><span class="punctuation">:</span> <span class="string">&quot;feishu&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;groupPolicy&quot;</span><span class="punctuation">:</span> <span class="string">&quot;open&quot;</span></span><br><span class="line">      <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;content&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">        <span class="attr">&quot;appId&quot;</span><span class="punctuation">:</span> <span class="string">&quot;cli5&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;appSecret&quot;</span><span class="punctuation">:</span> <span class="string">&quot;你的secret5&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;domain&quot;</span><span class="punctuation">:</span> <span class="string">&quot;feishu&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;groupPolicy&quot;</span><span class="punctuation">:</span> <span class="string">&quot;open&quot;</span></span><br><span class="line">      <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;op-growth&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">        <span class="attr">&quot;appId&quot;</span><span class="punctuation">:</span> <span class="string">&quot;cli6&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;appSecret&quot;</span><span class="punctuation">:</span> <span class="string">&quot;你的secret6&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;domain&quot;</span><span class="punctuation">:</span> <span class="string">&quot;feishu&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;groupPolicy&quot;</span><span class="punctuation">:</span> <span class="string">&quot;open&quot;</span></span><br><span class="line">      <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;finance&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">        <span class="attr">&quot;appId&quot;</span><span class="punctuation">:</span> <span class="string">&quot;cli7&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;appSecret&quot;</span><span class="punctuation">:</span> <span class="string">&quot;你的secret7&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;domain&quot;</span><span class="punctuation">:</span> <span class="string">&quot;feishu&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;groupPolicy&quot;</span><span class="punctuation">:</span> <span class="string">&quot;open&quot;</span></span><br><span class="line">      <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="punctuation">&#125;</span></span><br><span class="line">  <span class="punctuation">&#125;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><h4 id="第四步：配置-Bindings（核心中的核心）"><a href="#第四步：配置-Bindings（核心中的核心）" class="headerlink" title="第四步：配置 Bindings（核心中的核心）"></a>第四步：配置 Bindings（核心中的核心）</h4><blockquote><p>告诉 OpenClaw，哪个飞书 Bot 的消息交给哪个 AI 处理</p></blockquote><p>修改 <code>~/.openclaw/openclaw.json</code> 中的 <code>bindings</code></p><figure class="highlight json"><table><tr><td class="code"><pre><span class="line"><span class="attr">&quot;bindings&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">  <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;agentId&quot;</span><span class="punctuation">:</span> <span class="string">&quot;main&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;match&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">      <span class="attr">&quot;channel&quot;</span><span class="punctuation">:</span> <span class="string">&quot;feishu&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;accountId&quot;</span><span class="punctuation">:</span> <span class="string">&quot;main&quot;</span></span><br><span class="line">    <span class="punctuation">&#125;</span></span><br><span class="line">  <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;agentId&quot;</span><span class="punctuation">:</span> <span class="string">&quot;product&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;match&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">      <span class="attr">&quot;channel&quot;</span><span class="punctuation">:</span> <span class="string">&quot;feishu&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;accountId&quot;</span><span class="punctuation">:</span> <span class="string">&quot;product&quot;</span></span><br><span class="line">    <span class="punctuation">&#125;</span></span><br><span class="line">  <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;agentId&quot;</span><span class="punctuation">:</span> <span class="string">&quot;ui&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;match&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">      <span class="attr">&quot;channel&quot;</span><span class="punctuation">:</span> <span class="string">&quot;feishu&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;accountId&quot;</span><span class="punctuation">:</span> <span class="string">&quot;ui&quot;</span></span><br><span class="line">    <span class="punctuation">&#125;</span></span><br><span class="line">  <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;agentId&quot;</span><span class="punctuation">:</span> <span class="string">&quot;coding&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;match&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">      <span class="attr">&quot;channel&quot;</span><span class="punctuation">:</span> <span class="string">&quot;feishu&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;accountId&quot;</span><span class="punctuation">:</span> <span class="string">&quot;coding&quot;</span></span><br><span class="line">    <span class="punctuation">&#125;</span></span><br><span class="line">  <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;agentId&quot;</span><span class="punctuation">:</span> <span class="string">&quot;content&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;match&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">      <span class="attr">&quot;channel&quot;</span><span class="punctuation">:</span> <span class="string">&quot;feishu&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;accountId&quot;</span><span class="punctuation">:</span> <span class="string">&quot;content&quot;</span></span><br><span class="line">    <span class="punctuation">&#125;</span></span><br><span class="line">  <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;agentId&quot;</span><span class="punctuation">:</span> <span class="string">&quot;op-growth&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;match&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">      <span class="attr">&quot;channel&quot;</span><span class="punctuation">:</span> <span class="string">&quot;feishu&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;accountId&quot;</span><span class="punctuation">:</span> <span class="string">&quot;op-growth&quot;</span></span><br><span class="line">    <span class="punctuation">&#125;</span></span><br><span class="line">  <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;agentId&quot;</span><span class="punctuation">:</span> <span class="string">&quot;finance&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;match&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">      <span class="attr">&quot;channel&quot;</span><span class="punctuation">:</span> <span class="string">&quot;feishu&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;accountId&quot;</span><span class="punctuation">:</span> <span class="string">&quot;finance&quot;</span></span><br><span class="line">    <span class="punctuation">&#125;</span></span><br><span class="line">  <span class="punctuation">&#125;</span></span><br><span class="line"><span class="punctuation">]</span><span class="punctuation">,</span></span><br></pre></td></tr></table></figure><blockquote><p>飞书的每个 <code>accountId</code> 绑一个 <code>agent</code>。每个 <code>agent</code> 有自己的 <code>workspace</code>（工作区），有自己的 <code>SOUL.md</code>（人设文件），有自己的 <code>memory</code>（记忆系统）</p></blockquote><h4 id="第五步：配置-Agent-间通信"><a href="#第五步：配置-Agent-间通信" class="headerlink" title="第五步：配置 Agent 间通信"></a>第五步：配置 Agent 间通信</h4><p><strong>OpenClaw 支持 agentToAgent 通信配置</strong></p><figure class="highlight json"><table><tr><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;tools&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;agentToAgent&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">      <span class="attr">&quot;enabled&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;allow&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span><span class="string">&quot;main&quot;</span><span class="punctuation">,</span> <span class="string">&quot;product&quot;</span><span class="punctuation">,</span> <span class="string">&quot;ui&quot;</span><span class="punctuation">,</span> <span class="string">&quot;coding&quot;</span><span class="punctuation">,</span> <span class="string">&quot;content&quot;</span><span class="punctuation">,</span> <span class="string">&quot;op-growth&quot;</span><span class="punctuation">,</span> <span class="string">&quot;finance&quot;</span><span class="punctuation">]</span></span><br><span class="line">    <span class="punctuation">&#125;</span></span><br><span class="line">  <span class="punctuation">&#125;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><blockquote><p>加上这段，6 个 Agent 就能互相发消息协作了</p></blockquote><p>配置修改后，重置服务 <code>openclaw gateway restart</code></p><h4 id="第六步：编写-Agent-配置文件"><a href="#第六步：编写-Agent-配置文件" class="headerlink" title="第六步：编写 Agent 配置文件"></a>第六步：编写 Agent 配置文件</h4><blockquote><p>⚠️ <strong>注意</strong>：修改 <code>~/.openclaw</code> 中每个 Agent 的 <code>AGENTS.md</code></p><ul><li>每个 Agent 的 AGENTS.md 里必须写明团队成员列表</li><li>不然它们不知道彼此的存在</li></ul></blockquote><p>比如 AI 大总管的 <code>AGENTS.md</code> 里要这么写：</p><figure class="highlight markdown"><table><tr><td class="code"><pre><span class="line"><span class="section">## 🏢 poetry的AI团队成员</span></span><br><span class="line"></span><br><span class="line"><span class="bullet">-</span> <span class="strong">**product**</span>（产品助理 💻）— 产品规划、PRD文档输出</span><br><span class="line"><span class="bullet">-</span> <span class="strong">**ui**</span>（设计助理 💻）— UI设计</span><br><span class="line"><span class="bullet">-</span> <span class="strong">**coding**</span>（开发助理 💻）— 代码开发、技术架构、部署</span><br><span class="line"><span class="bullet">-</span> <span class="strong">**content**</span>（内容助理 ✍️）— 公众号文章、文案、内容创作</span><br><span class="line"><span class="bullet">-</span> <span class="strong">**op-growth**</span>（运营增长 📈）— 用户增长、社交媒体、市场推广</span><br><span class="line"><span class="bullet">-</span> <span class="strong">**finance**</span>（财务助理 💰）— 成本核算、预算管理</span><br></pre></td></tr></table></figure><p><img src="https://s.poetries.top/uploads/2026/03/858a78de28e7ea28.png" alt="Agent 配置"></p><blockquote><p>📝 <strong>配置要点</strong>：</p><ul><li>每个 <code>Agent</code> 的 <code>AGENTS.md</code> 都要有这份”通讯录”</li><li>否则大总管 AI 想 <code>@</code> 开发助理的时候，完全不知道有这么个”人”</li><li>加上团队成员列表之后，它们立刻就能互相协作了</li><li>另外每个 <code>Agent</code> 的 <code>SOUL.md</code> 也得精心写。这是 <code>Agent</code> 的灵魂文件——人设、行为准则、工作流程全在里面</li></ul></blockquote><p>比如开发助理的 <code>SOUL.md</code>：</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line"># SOUL.md - 开发助理</span><br><span class="line"></span><br><span class="line">你是poetry的开发助理，专注于代码开发、技术架构和部署。</span><br><span class="line"></span><br><span class="line">## 核心职责</span><br><span class="line"></span><br><span class="line">- **代码开发**：编写、调试、审查代码，实现产品需求</span><br><span class="line">- **技术架构**：设计技术方案，提供架构建议，评估技术选型</span><br><span class="line">- **部署运维**：负责项目部署、环境配置、性能优化</span><br><span class="line">- **技术调研**：研究新技术、框架、工具，提供技术选型建议</span><br><span class="line">- **问题排查**：定位和解决bug、性能问题、安全漏洞</span><br><span class="line"></span><br><span class="line">## 专业能力</span><br><span class="line"></span><br><span class="line">- 前端开发：React、Vue、小程序、H5等</span><br><span class="line">- 后端开发：Node.js、Python、Go等</span><br><span class="line">- 数据库：MySQL、MongoDB、Redis等</span><br><span class="line">- 部署：Docker、K8s、CI/CD、云服务</span><br><span class="line">- 开发工具：Git、VS Code、调试工具</span><br><span class="line"></span><br><span class="line">## 工作风格</span><br><span class="line"></span><br><span class="line">- 技术精准：代码规范、命名清晰、注释到位</span><br><span class="line">- 简洁直接：少说废话，多给方案和代码</span><br><span class="line">- 质量优先：重视代码可维护性和可扩展性</span><br><span class="line">- 文档完善：重要的技术决策要有文档记录</span><br><span class="line"></span><br><span class="line">## 协作方式</span><br><span class="line"></span><br><span class="line">- 与产品助理确认需求细节后再开发</span><br><span class="line">- 与设计助理确认UI实现细节</span><br><span class="line">- 复杂需求先出技术方案再动手</span><br><span class="line">- 提交代码前自测通过</span><br><span class="line"></span><br><span class="line">## 边界</span><br><span class="line"></span><br><span class="line">- 不做未确认的需求开发</span><br><span class="line">- 不在生产环境直接操作，重要操作先备份</span><br><span class="line">- 不引入未经验证的开源库</span><br><span class="line">- 涉及第三方API调用需告知潜在风险</span><br></pre></td></tr></table></figure><p>比如产品助理的 <code>SOUL.md</code>：</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line"># SOUL.md - 产品助理</span><br><span class="line"></span><br><span class="line">你是poetry的产品助理，专注于产品规划和PRD文档输出。</span><br><span class="line"></span><br><span class="line">## 核心职责</span><br><span class="line"></span><br><span class="line">- **需求分析**：理解用户需求，转化为产品需求</span><br><span class="line">- **PRD文档**：撰写详细的产品需求文档</span><br><span class="line">- **产品规划**：制定产品路线图和版本计划</span><br><span class="line">- **功能设计**：设计产品功能、流程、交互</span><br><span class="line">- **需求跟进**：与开发、设计团队沟通，确保需求落地</span><br><span class="line"></span><br><span class="line">## 专业能力</span><br><span class="line"></span><br><span class="line">- 需求分析与管理</span><br><span class="line">- 产品原型设计</span><br><span class="line">- PRD文档撰写</span><br><span class="line">- 用户体验设计</span><br><span class="line">- 数据分析</span><br><span class="line"></span><br><span class="line">## 工作流程</span><br><span class="line"></span><br><span class="line">1. 需求收集：理解poetry的业务需求和用户需求</span><br><span class="line">2. 需求分析：拆解需求，评估可行性和优先级</span><br><span class="line">3. 方案设计：设计功能方案、流程、页面结构</span><br><span class="line">4. 文档输出：撰写PRD、原型说明等</span><br><span class="line">5. 评审确认：与开发、设计确认方案可行性</span><br><span class="line">6. 跟进落地：跟踪开发进度，协调解决问题</span><br><span class="line"></span><br><span class="line">## 文档规范</span><br><span class="line"></span><br><span class="line">- 目标明确：每条需求都要有明确的目标</span><br><span class="line">- 描述清晰：用简单直白的语言描述功能</span><br><span class="line">- 逻辑完整：考虑正常流程和异常情况</span><br><span class="line">- 格式规范：文档结构清晰，便于阅读</span><br><span class="line"></span><br><span class="line">## 协作方式</span><br><span class="line"></span><br><span class="line">- 与开发助理确认技术实现方案</span><br><span class="line">- 与设计助理确认UI/UX设计</span><br><span class="line">- 与运营助理确认功能上线计划</span><br><span class="line">- 与财务助理确认功能投入产出</span><br><span class="line"></span><br><span class="line">## 边界</span><br><span class="line"></span><br><span class="line">- 不做未经验证的需求假设</span><br><span class="line">- 不承诺具体上线时间（需与开发确认）</span><br><span class="line">- 需求变更要及时同步</span><br><span class="line">- 不做超越产品范围的技术决策</span><br></pre></td></tr></table></figure><blockquote><p>💡 <code>SOUL.md</code> 写得越细，<code>Agent</code> 越好用，你可以使用 AI 结合自己的工作来完善这些文档</p></blockquote><h4 id="飞书多-Agent-核心步骤总结"><a href="#飞书多-Agent-核心步骤总结" class="headerlink" title="飞书多 Agent 核心步骤总结"></a>飞书多 Agent 核心步骤总结</h4><ul><li>飞书开放平台创建 N 个应用 → 拿到 <code>App ID + App Secret</code></li><li>每个应用开启”机器人能力” + “长连接事件订阅”（<code>im.message.receive_v1</code>）+ 发版</li><li><code>OpenClaw</code> 配置 <code>agents.list</code> + <code>channels.feishu.accounts</code> + <code>bindings</code></li><li>配 <code>agentToAgent</code> 通信 + 每个 <code>Agent</code> 写好 <code>AGENTS.md</code> 团队成员列表</li><li>最后重启 <code>Gateway</code>，搞定。</li></ul><h5 id="验证配置"><a href="#验证配置" class="headerlink" title="验证配置"></a>验证配置</h5><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">openclaw agents list --bindings    <span class="comment"># 查看所有 Agent 和路由规则</span></span><br><span class="line">openclaw channels status --probe   <span class="comment"># 查看所有通道在线状态</span></span><br></pre></td></tr></table></figure><p>看到 <code>Feishu xxx: running, works</code> 就成功了。</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">$ openclaw channels status --probe</span><br><span class="line"></span><br><span class="line">🦞 OpenClaw 2026.3.1 (2a8ac97) — Your terminal just grew claws—<span class="built_in">type</span> something and <span class="built_in">let</span> the bot pinch the busywork.</span><br><span class="line"></span><br><span class="line">12:02:36 [plugins] feishu_doc: Registered feishu_doc, feishu_app_scopes</span><br><span class="line">12:02:36 [plugins] feishu_chat: Registered feishu_chat tool</span><br><span class="line">12:02:36 [plugins] feishu_wiki: Registered feishu_wiki tool</span><br><span class="line">12:02:36 [plugins] feishu_drive: Registered feishu_drive tool</span><br><span class="line">12:02:36 [plugins] feishu_bitable: Registered bitable tools</span><br><span class="line">│</span><br><span class="line">◇</span><br><span class="line">Gateway reachable.</span><br><span class="line">- Feishu coding: enabled, configured, running, works</span><br><span class="line">- Feishu content: enabled, configured, running, works</span><br><span class="line">- Feishu finance: enabled, configured, running, works</span><br><span class="line">- Feishu main: enabled, configured, running, works</span><br><span class="line">- Feishu op-growth: enabled, configured, running, works</span><br><span class="line">- Feishu product: enabled, configured, running, works</span><br><span class="line">- Feishu ui: enabled, configured, running, works</span><br></pre></td></tr></table></figure><p>或者在网页上查看</p><p><img src="https://s.poetries.top/uploads/2026/03/5133ef4124a3fd9c.png" alt="网页端查看状态"></p><h4 id="第七步：配置飞书应用事件"><a href="#第七步：配置飞书应用事件" class="headerlink" title="第七步：配置飞书应用事件"></a>第七步：配置飞书应用事件</h4><p>配置完成后，接下来在每个 AI 应用中配置事件（如果上面没有配置，会导致配置不了）</p><blockquote><p>⚠️ <strong>注意</strong>：必须先配置完 OpenClaw 添加飞书配置后，才可以在飞书开放平台选择下面的事件选项</p></blockquote><p><img src="https://s.poetries.top/uploads/2026/03/6dba48f7a14ff34e.png" alt="选择事件选项"></p><blockquote><p>依次配置这些飞书机器人的事件与回调：</p><ul><li><strong>product</strong>（产品助理 💻）— 产品规划、PRD文档输出</li><li><strong>ui</strong>（设计助理 💻）— UI设计</li><li><strong>coding</strong>（开发助理 💻）— 代码开发、技术架构、部署</li><li><strong>content</strong>（内容助理 ✍️）— 公众号文章、文案、内容创作</li><li><strong>op-growth</strong>（运营增长 📈）— 用户增长、社交媒体、市场推广</li><li><strong>finance</strong>（财务助理 💰）— 成本核算、预算管理</li></ul></blockquote><p><img src="https://s.poetries.top/uploads/2026/03/e155c89336be0652.png" alt="配置事件"></p><p>需要订阅以下事件：</p><ul><li><code>im.message.receive_v1</code> - 接收消息</li><li><code>im.message.message_read_v1</code> - 消息已读回执</li><li><code>im.chat.member.bot.added_v1</code> - 机器人进群</li><li><code>im.chat.member.bot.deleted_v1</code> - 机器人被移出群</li></ul><p><img src="https://s.poetries.top/uploads/2026/03/d188e8bbe2572ce5.png" alt="添加事件"></p><p>最后需要发布版本才会生效</p><p><img src="https://s.poetries.top/uploads/2026/03/8bcb6ac9dd4f04bf.png" alt="发布版本"></p><p><img src="https://s.poetries.top/uploads/2026/03/701fbcdda3a595e7.png" alt="发布成功"></p><h4 id="第八步：在飞书群中使用-AI-机器人"><a href="#第八步：在飞书群中使用-AI-机器人" class="headerlink" title="第八步：在飞书群中使用 AI 机器人"></a>第八步：在飞书群中使用 AI 机器人</h4><p>接下来我们在飞书群使用这些 AI 机器人（在上面创建的飞书群基础上，继续添加机器人）</p><p><img src="https://s.poetries.top/uploads/2026/03/06f1c272ccef2263.png" alt="添加机器人到群"></p><p><img src="https://s.poetries.top/uploads/2026/03/c5a23921475ed526.png" alt="选择要添加的机器人"></p><p>添加全部机器人进来主群</p><p><img src="https://s.poetries.top/uploads/2026/03/0f4d6a9170bfae3e.png" alt="确认添加"></p><h5 id="每个AI应用单独使用"><a href="#每个AI应用单独使用" class="headerlink" title="每个AI应用单独使用"></a>每个AI应用单独使用</h5><p><img src="https://s.poetries.top/uploads/2026/03/b4b524f543ba724a.png"></p><p>飞书配对openclaw</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">openclaw pairing approve feishu [你的配对码]</span><br></pre></td></tr></table></figure><h5 id="结合群聊的方式使用"><a href="#结合群聊的方式使用" class="headerlink" title="结合群聊的方式使用"></a>结合群聊的方式使用</h5><p><img src="https://s.poetries.top/uploads/2026/03/64d8ff16586fbe5b.png" alt="查看效果"></p><p><img src="https://s.poetries.top/uploads/2026/03/af8f81ba07812e93.png" alt="团队协作"></p><p>让 AI 产品助理帮我产出 PRD 文档：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">@AI产品助理 我需要做一个AI漫剧SaaS网站，请你调研并出一份PRD文档</span><br></pre></td></tr></table></figure><p><img src="https://s.poetries.top/uploads/2026/03/a397b15437d3d362.png" alt="测试产品助理"></p><p>可以看到内容在产品助理的空间下，而不是跟其他 AI 混在一起，内容是独立隔离的</p><p><img src="https://s.poetries.top/uploads/2026/03/ff428caac74d2a6d.png" alt="独立空间"></p><hr><h2 id="搭配-Skill-使用-决定生产力"><a href="#搭配-Skill-使用-决定生产力" class="headerlink" title="搭配 Skill 使用 - 决定生产力"></a>搭配 Skill 使用 - 决定生产力</h2><p>使用 ClawHub 官方市场插件：<a href="https://clawhub.ai/">https://clawhub.ai/</a></p><p><img src="https://s.poetries.top/uploads/2026/03/09d68fb4d145e42b.png" alt="ClawHub 市场"></p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">npm install -g clawhub</span><br></pre></td></tr></table></figure><h5 id="搜索-Skill"><a href="#搜索-Skill" class="headerlink" title="搜索 Skill"></a>搜索 Skill</h5><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">$ clawhub search ui</span><br><span class="line">ui-ux-pro-max  UI/UX Pro Max  (3.469)</span><br><span class="line">ui-audit  UI Audit  (3.431)</span><br><span class="line">ui-ux-dev  UI/UX Design and Development  (3.284)</span><br><span class="line">ui-designer-skill  Ui Designer Skill  (3.252)</span><br><span class="line">ui-ux-pro-max-plus  UI UX Pro Max  (3.231)</span><br><span class="line">ui-debug-methodology  ui-debug-methodology  (3.152)</span><br><span class="line">jsapi-ui-kit  jsapi-ui-kit  (3.067)</span><br><span class="line">ui-control-center  Ui Control Center  (3.056)</span><br><span class="line">ui-ux-pro-max-0-1-0  Ui Ux Pro Max 0.1.0  (3.010)</span><br><span class="line">free-voice  Free voice from Comfy UI +  Qwen3 TTS  (1.916)</span><br></pre></td></tr></table></figure><h5 id="安装指定的-Skill"><a href="#安装指定的-Skill" class="headerlink" title="安装指定的 Skill"></a>安装指定的 Skill</h5><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">clawhub install &lt;skill-name&gt;</span><br></pre></td></tr></table></figure><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">$ clawhub install ui-ux-pro-max</span><br><span class="line">✔ OK. Installed ui-ux-pro-max -&gt; /Users/poetry/.openclaw/workspace/skills/ui-ux-pro-max</span><br></pre></td></tr></table></figure><p>安装的 Skill 都在这个目录下</p><p><img src="https://s.poetries.top/uploads/2026/03/5d635934b6fe7ced.png" alt="Skill 目录"></p><p>通过 <code>openclaw skills list</code> 命令可以查看当前安装的 Skills 情况，绿色 Ready 状态的 Skill 就是可用的</p><p><img src="https://s.poetries.top/uploads/2026/03/556c7c4b73075825.png" alt="Skill 列表"></p><p>我们也可以从 <a href="https://skillsmp.com/">https://skillsmp.com</a> 下载放到 <code>~/.openclaw/workspace/skills</code> 目录下</p><p><img src="https://s.poetries.top/uploads/2026/03/3e6ca30072269a7e.png" alt="下载 Skill"></p><p><img src="https://s.poetries.top/uploads/2026/03/95306fdeb3f0be48.png" alt="复制到目录"></p><p>可以看到复制过来的 Skill 也是可用的状态</p><p><img src="https://s.poetries.top/uploads/2026/03/ae87f68920fbc26c.png" alt="Skill 可用"></p><hr><h2 id="OpenClaw-更多玩法"><a href="#OpenClaw-更多玩法" class="headerlink" title="OpenClaw 更多玩法"></a>OpenClaw 更多玩法</h2><p>A community collection of OpenClaw use cases for making life easier.</p><p><a href="https://github.com/hesamsheikh/awesome-openclaw-usecases">https://github.com/hesamsheikh/awesome-openclaw-usecases</a></p><hr><h2 id="云服务器搭建-OpenClaw"><a href="#云服务器搭建-OpenClaw" class="headerlink" title="云服务器搭建 OpenClaw"></a>云服务器搭建 OpenClaw</h2><p>目前，搭建 OpenClaw 的方式主要有两种，一种是在本地电脑上安装，另一种是在云端电脑上安装。</p><blockquote><p>📝 本地电脑就是你自己的一台计算机，但我不建议你用平时的主力机，因为 OpenClaw 还是存在一些不安全性，比如权限太高导致你的文件丢失或者异常操作</p></blockquote><h3 id="申请云服务器（腾讯云）"><a href="#申请云服务器（腾讯云）" class="headerlink" title="申请云服务器（腾讯云）"></a>申请云服务器（腾讯云）</h3><h4 id="第一步：使用-OpenClaw-应用模板"><a href="#第一步：使用-OpenClaw-应用模板" class="headerlink" title="第一步：使用 OpenClaw 应用模板"></a>第一步：使用 OpenClaw 应用模板</h4><p><img src="https://s.poetries.top/uploads/2026/03/3e8f6f98604fb2a8.png" alt="选择应用模板"></p><p>点击服务器右上角的「···」并选择「管理实例」</p><p><img src="https://s.poetries.top/uploads/2026/03/e3f258b8845ce299.png" alt="管理实例"></p><p>切换到应用管理</p><p><img src="https://s.poetries.top/uploads/2026/03/b7ee32f3e9363ef8.png" alt="应用管理"></p><blockquote><p>📝 <strong>配置说明</strong>：</p><ul><li>这里的模型，就是 OpenClaw 的大脑，你可以选择不同的 AI 大模型供应商</li><li>通道就是 IM 工具，同样有很多不同的选择</li><li>再就是技能，这是 OpenClaw 变得聪明和全能的关键</li></ul></blockquote><h4 id="第二步：选择-AI-大模型"><a href="#第二步：选择-AI-大模型" class="headerlink" title="第二步：选择 AI 大模型"></a>第二步：选择 AI 大模型</h4><p>这里我选择 MiniMax</p><p><img src="https://s.poetries.top/uploads/2026/03/5a04275fc37e9bd2.png" alt="选择模型"></p><p>看到模型处于「应用中」状态，就说明连接成功了。</p><p><img src="https://s.poetries.top/uploads/2026/03/17d0cabb319d157b.png" alt="模型连接成功"></p><h4 id="第三步：连接-IM-工具（飞书）"><a href="#第三步：连接-IM-工具（飞书）" class="headerlink" title="第三步：连接 IM 工具（飞书）"></a>第三步：连接 IM 工具（飞书）</h4><p><img src="https://s.poetries.top/uploads/2026/03/4240e5573ca0ced8.png" alt="连接飞书"></p><p>去飞书平台创建应用获取 App ID 和 App Secret</p><p>打开 <a href="https://open.feishu.cn/app?lang=zh-CN">https://open.feishu.cn/app?lang=zh-CN</a></p><p><img src="https://s.poetries.top/uploads/2026/03/a050e5fcd3858c2b.png" alt="飞书开放平台"></p><p>添加机器人</p><p><img src="https://s.poetries.top/uploads/2026/03/7e72e80206028ce0.png" alt="添加机器人"></p><p>获取配置</p><p><img src="https://s.poetries.top/uploads/2026/03/47856f9f1031d25d.png" alt="获取配置"></p><p>分别复制，并填写到腾讯云 OpenClaw 配置通道的输入框里，然后点击「添加并应用」。</p><p><img src="https://s.poetries.top/uploads/2026/03/be716416062837cc.png" alt="填写配置"></p><p>到这里，我们就完成了 AI 大模型和 IM 工具与 OpenClaw 的连接</p><h4 id="第四步：配置事件和权限"><a href="#第四步：配置事件和权限" class="headerlink" title="第四步：配置事件和权限"></a>第四步：配置事件和权限</h4><p>这一步的目的，是为了能让机器人始终和我们保持在线状态</p><p><img src="https://s.poetries.top/uploads/2026/03/d8576266c245d6b8.png" alt="配置页面"></p><p>保存后，还是在这个页面，在下方事件那里点击「添加事件」</p><p>需要添加以下事件：</p><ul><li><code>im.message.receive_v1</code> - 接收消息</li><li><code>im.message.message_read_v1</code> - 消息已读回执</li><li><code>im.chat.member.bot.added_v1</code> - 机器人进群</li><li><code>im.chat.member.bot.deleted_v1</code> - 机器人被移出群</li></ul><p><img src="https://s.poetries.top/uploads/2026/03/e2a31939b8165be3.png" alt="添加事件"></p><p>然后添加权限管理，批量导入：</p><figure class="highlight json"><table><tr><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;scopes&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;tenant&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">      <span class="string">&quot;cardkit:card:write&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="string">&quot;contact:contact.base:readonly&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="string">&quot;contact:user.base:readonly&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="string">&quot;im:chat:readonly&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="string">&quot;im:message&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="string">&quot;im:message.group_at_msg:readonly&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="string">&quot;im:message.group_msg&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="string">&quot;im:message.p2p_msg:readonly&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="string">&quot;im:message.reactions:read&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="string">&quot;im:message:readonly&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="string">&quot;im:message:recall&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="string">&quot;im:message:send_as_bot&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="string">&quot;im:message:update&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="string">&quot;im:resource&quot;</span></span><br><span class="line">    <span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;user&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span><span class="string">&quot;contact:contact.base:readonly&quot;</span><span class="punctuation">]</span></span><br><span class="line">  <span class="punctuation">&#125;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p><img src="https://s.poetries.top/uploads/2026/03/265d7615c779b67e.png" alt="添加权限"></p><p>设置好事件和权限，下一步就是准备发布这个机器人上线</p><p><img src="https://s.poetries.top/uploads/2026/03/f7720de4a798f10c.png" alt="发布机器人"></p><p>到这一步，我们的 IM 机器人就算搭建完毕了</p><p>接下来，打开你的飞书 App 或者桌面端窗口，你就能看到刚刚配置好的这个 AI 机器人</p><p><img src="https://s.poetries.top/uploads/2026/03/55cf0f3c51843c00.png" alt="飞书机器人"></p><p>给机器人发句话</p><p><img src="https://s.poetries.top/uploads/2026/03/78ac9d44d35b7617.png" alt="发送消息"></p><h4 id="第五步：配对飞书"><a href="#第五步：配对飞书" class="headerlink" title="第五步：配对飞书"></a>第五步：配对飞书</h4><p>看到这里我们需要跟 OpenClaw 配对飞书</p><p>然后打开你的腾讯云服务器后台页面，点击服务器旁边的「登录」按钮，进入 <code>OpenClaw</code> 主机</p><p>或者 SSH 进入到服务器终端执行命令：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">openclaw pairing approve feishu [你的配对码]</span><br></pre></td></tr></table></figure><p>看到这里说明配对成功了</p><p><img src="https://s.poetries.top/uploads/2026/03/04774190ea2a4ff5.png" alt="配对成功"></p><p>现在聊天就正常了</p><p><img src="https://s.poetries.top/uploads/2026/03/6a8a861a7b5984a3.png" alt="聊天正常"></p><blockquote><p>💡 接下来你可以配置 Skill 增强 AI 的能力，参考其他部分即可</p></blockquote><hr><h2 id="常用命令"><a href="#常用命令" class="headerlink" title="常用命令"></a>常用命令</h2><h3 id="Gateway-管理"><a href="#Gateway-管理" class="headerlink" title="Gateway 管理"></a>Gateway 管理</h3><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 启动 Gateway</span></span><br><span class="line">openclaw gateway</span><br><span class="line"></span><br><span class="line"><span class="comment"># 启动并显示详细日志</span></span><br><span class="line">openclaw gateway --verbose</span><br><span class="line"></span><br><span class="line"><span class="comment"># 指定端口启动</span></span><br><span class="line">openclaw gateway --port 18789</span><br></pre></td></tr></table></figure><h3 id="配置管理"><a href="#配置管理" class="headerlink" title="配置管理"></a>配置管理</h3><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 运行配置向导</span></span><br><span class="line">openclaw onboard</span><br><span class="line"></span><br><span class="line"><span class="comment"># 系统健康检查</span></span><br><span class="line">openclaw doctor</span><br><span class="line"></span><br><span class="line"><span class="comment"># 查看配置</span></span><br><span class="line"><span class="built_in">cat</span> ~/.openclaw/openclaw.json</span><br></pre></td></tr></table></figure><h3 id="更新管理"><a href="#更新管理" class="headerlink" title="更新管理"></a>更新管理</h3><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 更新到最新版本</span></span><br><span class="line">openclaw update</span><br><span class="line"></span><br><span class="line"><span class="comment"># 切换到特定频道</span></span><br><span class="line">openclaw update --channel stable    <span class="comment"># 稳定版</span></span><br><span class="line">openclaw update --channel beta      <span class="comment"># 测试版</span></span><br><span class="line">openclaw update --channel dev       <span class="comment"># 开发版</span></span><br></pre></td></tr></table></figure>]]>
    </content>
    <id>http://fe.poetries.top/2026/03/04/openclaw-guide/</id>
    <link href="http://fe.poetries.top/2026/03/04/openclaw-guide/"/>
    <published>2026-03-04T08:30:00.000Z</published>
    <summary>详细讲解如何在本地和云服务器上搭建OpenClaw AI助手平台，集成MiniMax M2.1模型，并配置飞书、Telegram、Discord多渠道接入，实现24小时帮你干活的虚拟AI团队。</summary>
    <title>OpenClaw搭建24小时帮你干活的AI员工,支持本地/云服务并打通飞书/Telegram/Discord</title>
    <updated>2026-03-08T10:22:42.091Z</updated>
  </entry>
  <entry>
    <author>
      <name>Poetry</name>
    </author>
    <category term="AI" scheme="http://fe.poetries.top/categories/AI/"/>
    <category term="Claude" scheme="http://fe.poetries.top/tags/Claude/"/>
    <category term="AI 工具" scheme="http://fe.poetries.top/tags/AI-%E5%B7%A5%E5%85%B7/"/>
    <category term="提示词工程" scheme="http://fe.poetries.top/tags/%E6%8F%90%E7%A4%BA%E8%AF%8D%E5%B7%A5%E7%A8%8B/"/>
    <category term="效率提升" scheme="http://fe.poetries.top/tags/%E6%95%88%E7%8E%87%E6%8F%90%E5%8D%87/"/>
    <category term="Agent" scheme="http://fe.poetries.top/tags/Agent/"/>
    <category term="Figma" scheme="http://fe.poetries.top/tags/Figma/"/>
    <category term="MinMax" scheme="http://fe.poetries.top/tags/MinMax/"/>
    <content>
      <![CDATA[<p>本文详细介绍如何通过 Claude Code + MinMax + Figma MCP 的组合方案，实现设计稿到代码的高精度自动还原。</p><h2 id="安装-Figma-MCP"><a href="#安装-Figma-MCP" class="headerlink" title="安装 Figma MCP"></a>安装 Figma MCP</h2><p>首先在终端中执行以下命令安装 Figma MCP：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">claude mcp add --scope user --transport http figma https://mcp.figma.com/mcp</span><br></pre></td></tr></table></figure><p>安装完成后，使用 <code>claude mcp list</code> 查看已安装的 MCP 列表：</p><p><img src="https://s.poetries.top/uploads/2026/03/415ed438e1b1e769.png"></p><h2 id="授权-Figma-账户"><a href="#授权-Figma-账户" class="headerlink" title="授权 Figma 账户"></a>授权 Figma 账户</h2><p>MCP 安装成功后，需要在 Claude Code 界面中完成 Figma 账户的授权操作。点击授权按钮后会跳转到 Figma 官网的 OAuth 页面，登录并确认授权即可。</p><p><img src="https://s.poetries.top/uploads/2026/03/da22995d65af2d99.png"></p><p><img src="https://s.poetries.top/uploads/2026/03/5bdd1d4c83d93146.png"></p><p>授权成功后，MCP 即可正常连接并读取你的 Figma 文件：</p><p><img src="https://s.poetries.top/uploads/2026/03/63ea70d820fc7da8.png"></p><h2 id="配置-MinMax-模型（可选）"><a href="#配置-MinMax-模型（可选）" class="headerlink" title="配置 MinMax 模型（可选）"></a>配置 MinMax 模型（可选）</h2><p>如果你希望降低 API 调用成本，可以在 Claude Code 中配置 MinMax 作为模型供应商。使用 <code>claude switch</code> 命令调出模型切换面板，添加新的供应商：</p><p><img src="https://s.poetries.top/uploads/2026/03/4ffa433b7d7e8912.png"></p><p>详细配置步骤可参考 <a href="https://platform.minimaxi.com/docs/guides/text-ai-coding-tools#%E5%AE%89%E8%A3%85-claude-code">MinMax 官方文档</a>。</p><h2 id="使用-Figma-MCP-还原设计稿"><a href="#使用-Figma-MCP-还原设计稿" class="headerlink" title="使用 Figma MCP 还原设计稿"></a>使用 Figma MCP 还原设计稿</h2><p>接下来演示如何使用 Figma MCP 从设计稿自动生成代码。</p><h3 id="选择目标设计稿"><a href="#选择目标设计稿" class="headerlink" title="选择目标设计稿"></a>选择目标设计稿</h3><p>本示例使用的是 Figma Community 上的一个咖啡馆落地页设计稿：</p><ul><li><a href="https://www.figma.com/files/team/1578338715267053469/resources/community/file/1444331896024589155?fuid=1578338713432883311">设计稿地址</a></li></ul><p>打开设计稿后，点击【Open in Figma】进入画布页面：</p><p><img src="https://s.poetries.top/uploads/2026/03/7dc808eb2ec0ba78.png"></p><h3 id="获取设计元素链接"><a href="#获取设计元素链接" class="headerlink" title="获取设计元素链接"></a>获取设计元素链接</h3><p>在 Figma 画布中选择你想要复刻的 Layer 层或 Frame，点击右键选择【Copy link to selection】获取元素链接：</p><p><img src="https://s.poetries.top/uploads/2026/03/f93117a0957eac99.png"></p><h3 id="调用-Claude-Code-生成代码"><a href="#调用-Claude-Code-生成代码" class="headerlink" title="调用 Claude Code 生成代码"></a>调用 Claude Code 生成代码</h3><p>切换到 Claude Code 界面，输入以下提示词即可自动还原设计稿：</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">请使用Figma MCP，在 @test.html 页面还原落地页设计：https://www.figma.com/design/zZQZmhP0lSw4OeCsBvuzFG/Bean-Scene-Coffee--Community-?node-id=1-4&amp;t=2TnBNPKQMGuHxi8w-4</span><br></pre></td></tr></table></figure><blockquote><p>注意：提示词中的 <code>@test.html</code> 指定了输出文件名，Figma 链接包含了文件 ID 和节点 ID 信息，MCP 会据此解析设计元素属性并生成对应代码。</p></blockquote><p>MCP 会自动读取设计稿的颜色、字体、间距等属性，并生成对应的 HTML 和 CSS 代码：</p><p><img src="https://s.poetries.top/uploads/2026/03/e36ec0df89daabaf.png"></p><p><img src="https://s.poetries.top/uploads/2026/03/a0e0152712e0e967.png"></p><h3 id="验证还原效果"><a href="#验证还原效果" class="headerlink" title="验证还原效果"></a>验证还原效果</h3><p>代码生成完成后，打开生成的 test.html 文件查看效果：还原度还是很高的</p><p><img src="https://s.poetries.top/uploads/2026/03/8a87a46853f9f488.png"></p><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><ul><li><a href="https://developers.figma.com/docs/figma-mcp-server/remote-server-installation/#claude-code">Figma MCP Server 官方文档</a></li><li><a href="https://platform.minimaxi.com/docs/guides/text-ai-coding-tools#%E5%AE%89%E8%A3%85-claude-code">MinMax Claude Code 接入指南</a></li></ul>]]>
    </content>
    <id>http://fe.poetries.top/2026/03/02/claude-figma-mcp-minmax/</id>
    <link href="http://fe.poetries.top/2026/03/02/claude-figma-mcp-minmax/"/>
    <published>2026-03-02T13:20:00.000Z</published>
    <summary>详解如何使用Claude Code配合MinMax大模型和Figma MCP实现设计稿到代码的高精度自动还原，大幅提升前端开发效率</summary>
    <title>基于Claude Code + MinMax + Figma MCP高精度还原设计稿</title>
    <updated>2026-03-08T10:22:42.064Z</updated>
  </entry>
  <entry>
    <author>
      <name>Poetry</name>
    </author>
    <category term="AI" scheme="http://fe.poetries.top/categories/AI/"/>
    <category term="Claude" scheme="http://fe.poetries.top/tags/Claude/"/>
    <category term="AI 工具" scheme="http://fe.poetries.top/tags/AI-%E5%B7%A5%E5%85%B7/"/>
    <category term="提示词工程" scheme="http://fe.poetries.top/tags/%E6%8F%90%E7%A4%BA%E8%AF%8D%E5%B7%A5%E7%A8%8B/"/>
    <category term="效率提升" scheme="http://fe.poetries.top/tags/%E6%95%88%E7%8E%87%E6%8F%90%E5%8D%87/"/>
    <category term="Agent" scheme="http://fe.poetries.top/tags/Agent/"/>
    <content>
      <![CDATA[<blockquote><p>深入解析 Claude Skills 的核心原理、渐进披露架构和最佳实践，手把手教你创建自定义技能，实现从临时提示词到可复用资产的升级。</p></blockquote><h2 id="痛点与解决方案"><a href="#痛点与解决方案" class="headerlink" title="痛点与解决方案"></a>痛点与解决方案</h2><p>在日常使用 Claude 的过程中，你是否经常遇到这些困扰：每次让 Claude 生成品牌宣传材料，都要重新解释一遍品牌规范；每次做数据分析，都要重复强调输出格式和注意事项；换了一个对话窗口，之前积累的「最佳实践」全部丢失。这些问题都指向同一个根本原因：<strong>纯提示词模式的局限性</strong>。</p><p>当你依赖对话中的即时指令时，Claude 无法跨对话保持一致性，也难以处理需要精确执行的复杂任务。传统方式下，让 Claude 画图，它只能生成简单的 HTML 页面，输出质量完全不可控。</p><p><strong>Claude Skills</strong> 正是为解决这些问题而设计。它将「临时起意的提示词」升级为「可以积累的技能资产」，让你能够把专业经验、工作流程和偏好设置持久化地沉淀下来。使用 Skills 后，Claude 会调用 Python 代码执行精确的视觉渲染，输出质量更稳定、可控。</p><h2 id="Skills-基础知识"><a href="#Skills-基础知识" class="headerlink" title="Skills 基础知识"></a>Skills 基础知识</h2><h3 id="什么是-Skills"><a href="#什么是-Skills" class="headerlink" title="什么是 Skills"></a>什么是 Skills</h3><blockquote><p>Agent Skills 是扩展 Claude 功能的模块化能力。每个 Skill 打包了指令、元数据和可选资源（脚本、模板），Claude 会在相关时自动使用它们。</p></blockquote><p>Skills 本质上是一种<strong>能力封装机制</strong>——它不仅告诉 Claude「做什么」，更告诉它「怎么做」才能达到专业水准。</p><h3 id="为什么使用-Skills"><a href="#为什么使用-Skills" class="headerlink" title="为什么使用 Skills"></a>为什么使用 Skills</h3><p>Skills 是可重用的、基于文件系统的资源，为 Claude 提供特定领域的专业知识：工作流程、上下文和最佳实践，将通用型智能体转变为专家。与提示词（用于一次性任务的对话级指令）不同，Skills 按需加载，消除了在多个对话中重复提供相同指导的需要。</p><p><strong>主要优势：</strong></p><ul><li><strong>专业化 Claude</strong>：为特定领域任务定制能力</li><li><strong>减少重复</strong>：创建一次，自动使用</li><li><strong>组合能力</strong>：组合 Skills 构建复杂工作流程</li></ul><h3 id="Skills-架构"><a href="#Skills-架构" class="headerlink" title="Skills 架构"></a>Skills 架构</h3><p>Skills 在代码执行环境中运行，Claude 在该环境中拥有文件系统访问、bash 命令和代码执行能力。可以这样理解：Skills 作为目录存在于虚拟机上，Claude 使用与您在计算机上浏览文件相同的 bash 命令与它们交互。</p><h2 id="Skills-的核心原理：渐进披露架构"><a href="#Skills-的核心原理：渐进披露架构" class="headerlink" title="Skills 的核心原理：渐进披露架构"></a>Skills 的核心原理：渐进披露架构</h2><p>理解 Skills 的技术设计，有助于更好地使用它。</p><p>Claude Skills 采用了一种名为「渐进披露」（Progressive Disclosure）的架构设计。这意味着 Claude 不会一次性加载所有技能信息，而是根据任务需要分三层逐步加载。这种设计是 Skills 最重要的技术概念——它决定了系统的性能表现和用户体验。</p><p><img src="https://s.poetries.top/uploads/2026/03/2bdec3709b8327d8.png"></p><h3 id="第一层：元数据扫描"><a href="#第一层：元数据扫描" class="headerlink" title="第一层：元数据扫描"></a>第一层：元数据扫描</h3><p>当 Claude 接收到任务时，它会先扫描所有已安装 Skills 的简短描述（约 100 tokens）。这一层的目的是快速判断当前任务与哪些 Skill 相关。这个设计确保了 Claude 能够快速筛选出可能需要的技能，而不会被大量的技能信息所淹没。</p><h3 id="第二层：完整说明加载"><a href="#第二层：完整说明加载" class="headerlink" title="第二层：完整说明加载"></a>第二层：完整说明加载</h3><p>一旦确定某个 Skill 与任务相关，Claude 才会加载完整的 <a href="http://skill.md/">SKILL.md</a> 文件（官方建议上限约 5k tokens）。这里包含详细的步骤流程、注意事项、输出格式要求和风格偏好。这个层面才是技能的核心定义区域。</p><h3 id="第三层：代码与资源按需加载"><a href="#第三层：代码与资源按需加载" class="headerlink" title="第三层：代码与资源按需加载"></a>第三层：代码与资源按需加载</h3><p>某些 Skill 还附带脚本、模板或参考文件。Claude 只在真正需要执行相关操作时，才会将这些内容纳入上下文。这种按需加载的方式极大地优化了上下文长度的使用效率。</p><p>这种设计的核心优势在于：<strong>你可以安装数十个 Skills，而不会在每次对话开始时就被上下文长度限制压垮</strong>。Claude 只会翻阅它认为有用的那本「操作手册」。</p><blockquote><p><strong>传统架构 vs 渐进披露</strong></p><ul><li><p>传统架构：加载所有 Skills 500k tokens，处理时间 5-10 秒</p></li><li><p>渐进披露：元数据 10k tokens + 激活时 15k tokens，处理时间 0.6 秒</p></li></ul></blockquote><h2 id="快速上手：启用官方-Skills"><a href="#快速上手：启用官方-Skills" class="headerlink" title="快速上手：启用官方 Skills"></a>快速上手：启用官方 Skills</h2><h3 id="安装-Skill"><a href="#安装-Skill" class="headerlink" title="安装 Skill"></a>安装 Skill</h3><p><strong>方式一：本地导入</strong></p><ul><li>下载 SKILL 包后，在 <code>~/.claude/skills</code> 下导入 <code>skill</code> 文件</li></ul><p><strong>方式二：市场安装</strong></p><ul><li>通过 <a href="http://skillsmp.com/">skillsmp.com</a> 市场来安装，找到合适的 skill 安装即可</li></ul><h3 id="官方-Skills-能做什么"><a href="#官方-Skills-能做什么" class="headerlink" title="官方 Skills 能做什么"></a>官方 Skills 能做什么</h3><p>Anthropic 提供了一系列开箱即用的官方 Skills，主要集中在文档处理领域。访问 <a href="https://github.com/anthropics/skills/tree/main">官方 Skills 仓库</a> 可以查看完整的技能列表。</p><p>官方提供的核心 Skills 包括：</p><table><thead><tr><th>技能名称</th><th>功能描述</th><th>适用场景</th></tr></thead><tbody><tr><td><code>docx</code></td><td>Word 文档的创建、编辑、审阅</td><td>合同流转、合规文档</td></tr><tr><td><code>pdf</code></td><td>PDF 文本提取、表格抽取、合并拆分</td><td>票据归档、数据抽取</td></tr><tr><td><code>pptx</code></td><td>PPT 布局调整、模板应用、图表生成</td><td>销售演示、周会汇报</td></tr><tr><td><code>xlsx</code></td><td>Excel 公式编写、格式设置、数据分析</td><td>报表生成、指标盘</td></tr></tbody></table><h2 id="创建你的第一个自定义-Skill"><a href="#创建你的第一个自定义-Skill" class="headerlink" title="创建你的第一个自定义 Skill"></a>创建你的第一个自定义 Skill</h2><p>官方 Skills 虽然好用，但真正的价值在于创建符合你自身需求的定制技能。</p><h3 id="技能文件夹结构"><a href="#技能文件夹结构" class="headerlink" title="技能文件夹结构"></a>技能文件夹结构</h3><p>首先，你需要了解 Skills 的标准文件结构：</p><p><img src="https://s.poetries.top/uploads/2026/03/7e85ebde58963a9e.png"></p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">my-skill/</span><br><span class="line">├── SKILL.md        <span class="comment"># 技能说明文档（核心）</span></span><br><span class="line">├── metadata.json   <span class="comment"># 元数据配置</span></span><br><span class="line">├── scripts/        <span class="comment"># 可执行脚本</span></span><br><span class="line">└── resources/      <span class="comment"># 参考资源</span></span><br></pre></td></tr></table></figure><p><a href="http://skill.md/"><strong>SKILL.md</strong></a>** 是核心文件**，它定义了 Skill 的完整行为。一个完整的 <a href="http://skill.md/">SKILL.md</a> 结构包括：</p><figure class="highlight markdown"><table><tr><td class="code"><pre><span class="line">---</span><br><span class="line">name: 技能名称</span><br><span class="line"><span class="section">description: 技能描述元数据</span></span><br><span class="line"><span class="section">---</span></span><br><span class="line"></span><br><span class="line"><span class="section"># 技能名称</span></span><br><span class="line"></span><br><span class="line"><span class="section">## 触发条件</span></span><br><span class="line"></span><br><span class="line">描述在什么情况下应该使用这个技能</span><br><span class="line"></span><br><span class="line"><span class="section">## 执行步骤</span></span><br><span class="line"></span><br><span class="line"><span class="bullet">1.</span> 第一步做什么</span><br><span class="line"><span class="bullet">2.</span> 第二步做什么</span><br><span class="line"></span><br><span class="line"><span class="section">## 输出要求</span></span><br><span class="line"></span><br><span class="line"><span class="bullet">-</span> 格式要求</span><br><span class="line"><span class="bullet">-</span> 风格要求</span><br><span class="line"><span class="bullet">-</span> 注意事项</span><br></pre></td></tr></table></figure><h3 id="非程序员路径：引导式创建"><a href="#非程序员路径：引导式创建" class="headerlink" title="非程序员路径：引导式创建"></a>非程序员路径：引导式创建</h3><p>即使你完全不懂编程，Claude 也提供了完整的引导流程：</p><ol><li>在对话中直接告诉 Claude：「我想创建一个 Skill，请引导我完成」</li><li>Claude 会启动专门的创建引导流程，询问你技能名称、功能描述、预期用途</li><li>完成后系统会生成一个 ZIP 包</li><li>在 Skills 设置页面点击上传安装</li></ol><h3 id="程序员路径：本地自定义"><a href="#程序员路径：本地自定义" class="headerlink" title="程序员路径：本地自定义"></a>程序员路径：本地自定义</h3><p>对于有技术背景的用户，你可以手动创建 Skill 文件夹结构，然后直接在本地进行开发和调试。</p><h2 id="如何触发-Skills"><a href="#如何触发-Skills" class="headerlink" title="如何触发 Skills"></a>如何触发 Skills</h2><h3 id="方法一：斜杠命令（-）"><a href="#方法一：斜杠命令（-）" class="headerlink" title="方法一：斜杠命令（&#x2F;）"></a>方法一：斜杠命令（&#x2F;）</h3><p>我们可以用 <code>/</code> 开头，然后去触发对应的 Skills。这种一般是针对这个 Skills 是重复直接执行的工作流。</p><h3 id="方法二：AI-主动触发关键字"><a href="#方法二：AI-主动触发关键字" class="headerlink" title="方法二：AI 主动触发关键字"></a>方法二：AI 主动触发关键字</h3><p>如果你的提示词里面含有相关的关键字，这个关键词能够命中 Skills 的描述，那就会自动触发该 Skill。</p><h3 id="方法三：直接说明（推荐）"><a href="#方法三：直接说明（推荐）" class="headerlink" title="方法三：直接说明（推荐）"></a>方法三：直接说明（推荐）</h3><p>最推荐的是直接说明我需要使用某个 Skill，来让 AI 精准地触发。例如：「请使用 frontend-design 技能帮我设计一个落地页。」</p><p><img src="https://s.poetries.top/uploads/2026/03/1e5f2983e2046e35.png"></p><h2 id="Skills-与其他功能的协同"><a href="#Skills-与其他功能的协同" class="headerlink" title="Skills 与其他功能的协同"></a>Skills 与其他功能的协同</h2><p>Claude 的功能生态并非孤立存在，理解它们之间的关系有助于构建更强大的工作流。</p><h3 id="Skills-vs-Prompts"><a href="#Skills-vs-Prompts" class="headerlink" title="Skills vs Prompts"></a>Skills vs Prompts</h3><p>这是最需要理清的一对概念。</p><p><strong>Prompts（提示词）</strong> 是对话中的即时指令，适合一次性任务和临时需求。它不会在不同对话之间自动保留。也就是说，你今天费心写了一个很长的「代码安全审计」提示词，明天开新对话，还得重新粘一遍。</p><p><strong>Skills（技能）</strong> 是持久化的能力模块，适合需要反复使用的流程和标准。当你在多个对话中反复输入类似的指令时，就是该升级为 Skill 的信号。</p><p>一个简单的判断标准：<strong>只说一次的事情用 Prompts，需要累计沉淀的经验用 Skills</strong>。</p><p><img src="https://s.poetries.top/uploads/2026/03/a6e4527d6ff67ee9.png"></p><table><thead><tr><th>特性</th><th>Prompts</th><th>Skills</th></tr></thead><tbody><tr><td>持久性</td><td>对话级，不跨对话</td><td>全局可用，持久化</td></tr><tr><td>适用场景</td><td>一次性任务</td><td>重复性流程</td></tr><tr><td>加载方式</td><td>每次手动输入</td><td>按需自动加载</td></tr><tr><td>复杂度</td><td>简单直接</td><td>可包含脚本、资源</td></tr></tbody></table><h3 id="Skills-vs-Projects"><a href="#Skills-vs-Projects" class="headerlink" title="Skills vs Projects"></a>Skills vs Projects</h3><p><strong>Projects（项目）</strong> 提供的是「知识场景」——它解决的是「Claude 需要知道什么」。每个 Project 有独立的历史记录、知识库和项目级指令。</p><p><strong>Skills（技能）</strong> 提供的是「能力模组」——它解决的是「Claude 应该怎么做」。Skill 是全局可用的，而 Project 只在特定项目空间内生效。</p><p>两者配合使用时，Project 负责提供背景知识，Skill 负责定义处理方法。</p><h3 id="Skills-Subagents-MCP-的组合拳"><a href="#Skills-Subagents-MCP-的组合拳" class="headerlink" title="Skills + Subagents + MCP 的组合拳"></a>Skills + Subagents + MCP 的组合拳</h3><p>对于复杂任务，这三者可以协同工作：</p><ul><li><strong>MCP</strong> 负责连接外部系统（数据库、文件、API）</li><li><strong>Skills</strong> 定义如何使用这些外部工具的流程</li><li><strong>Subagents</strong> 作为专职执行者，调用特定 Skills 完成子任务</li></ul><p>例如，一个竞品分析 Agent 可以这样构建：Subagent A 负责市场调研（调用市场分析 Skill），Subagent B 负责技术分析（调用代码审查 Skill），两者通过 MCP 获取的外部数据完成各自职责。</p><p><img src="https://s.poetries.top/uploads/2026/03/58fe9839ed7a41b8.png"></p><h2 id="典型应用场景"><a href="#典型应用场景" class="headerlink" title="典型应用场景"></a>典型应用场景</h2><p>基于实际使用经验，以下场景特别适合使用 Skills:</p><p><img src="https://s.poetries.top/uploads/2026/03/3c131f32fc5478fa.png"></p><h3 id="场景一：品牌规范化管理"><a href="#场景一：品牌规范化管理" class="headerlink" title="场景一：品牌规范化管理"></a>场景一：品牌规范化管理</h3><p>将品牌色彩、字体、版式、LOGO 使用规范封装为 Skill。每次生成营销材料时自动应用，无需重复叮嘱。</p><h3 id="场景二：标准化文档流程"><a href="#场景二：标准化文档流程" class="headerlink" title="场景二：标准化文档流程"></a>场景二：标准化文档流程</h3><p>将合同审阅、数据报告、需求文档的格式标准写入 Skill。确保输出的一致性和专业性。</p><h3 id="场景三：专业领域知识沉淀"><a href="#场景三：专业领域知识沉淀" class="headerlink" title="场景三：专业领域知识沉淀"></a>场景三：专业领域知识沉淀</h3><p>将行业特定的分析框架、评审标准、最佳实践固化为 Skill。新成员加入后可直接使用团队积累的专业经验。</p><h3 id="场景四：个人工作习惯"><a href="#场景四：个人工作习惯" class="headerlink" title="场景四：个人工作习惯"></a>场景四：个人工作习惯</h3><p>将你的笔记格式、代码风格偏好、研究方法论封装为 Skill。在任何对话中都能保持个人习惯的一致性。</p><h2 id="最佳实践建议"><a href="#最佳实践建议" class="headerlink" title="最佳实践建议"></a>最佳实践建议</h2><h3 id="从小处着手"><a href="#从小处着手" class="headerlink" title="从小处着手"></a>从小处着手</h3><p>不必追求一步到位的完美 Skill。先从最简单的重复性任务开始，积累经验后再扩展复杂流程。</p><h3 id="保持-Skill-的专注性"><a href="#保持-Skill-的专注性" class="headerlink" title="保持 Skill 的专注性"></a>保持 Skill 的专注性</h3><p>一个 Skill 建议只专注解决一类问题。过于通用的 Skill 反而难以提供精准的指导。</p><h3 id="定期迭代优化"><a href="#定期迭代优化" class="headerlink" title="定期迭代优化"></a>定期迭代优化</h3><p>随着使用经验的积累，你会逐渐发现 Skill 的不足之处。及时更新 <a href="http://skill.md/">SKILL.md</a> 内容，让技能越来越好用。</p><h3 id="善用渐进披露机制"><a href="#善用渐进披露机制" class="headerlink" title="善用渐进披露机制"></a>善用渐进披露机制</h3><p>不要在 <a href="http://skill.md/">SKILL.md</a> 中堆积过多信息。只保留当前任务真正需要的流程和规则，其他细节可以通过 Prompts 临时补充。</p><h2 id="推荐-Skills"><a href="#推荐-Skills" class="headerlink" title="推荐 Skills"></a>推荐 Skills</h2><ul><li><strong>frontend-design</strong>：创建生产级前端界面，避免通用 AI 审美</li><li><strong>Nextjs-best-practices</strong>：Next.js 开发最佳实践</li><li><strong>tailwind-design-system</strong>: Tailwind CSS 设计系统</li><li><strong>landing-page-guide-v2</strong>：落地页设计指南</li><li><strong>seo-review</strong>：SEO 代码审查</li><li><strong>ui-ux-pro-max</strong>：UI&#x2F;UX 设计专家</li></ul><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>Claude Skills 代表了一种从「临时交互」到「能力积累」的范式转变。它不仅仅是一个功能升级，更是一种工作效率思维的进化。当你开始系统性地将重复性工作封装为 Skills 时，你会发现 Claude 从一个聊天助手，逐步变成了一个真正懂你、能够持续学习你工作方式的智能伙伴。</p><p>与其每次都从零开始，不如现在就开始构建你的第一个 Skill。</p><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ul><li><a href="https://skillsmp.com/">skillsmp</a></li><li><a href="https://github.com/ComposioHQ/awesome-claude-skills">awesome-claude-skills</a></li><li><a href="https://github.com/anthropics/skills">anthropics&#x2F;skills</a></li><li><a href="https://skills.deeptoai.com/zh/docs/development/progressive-disclosure-architecture">Claude Skills 渐进披露架构深度揭秘</a></li></ul>]]>
    </content>
    <id>http://fe.poetries.top/2026/02/23/claude-skills/</id>
    <link href="http://fe.poetries.top/2026/02/23/claude-skills/"/>
    <published>2026-02-23T12:20:00.000Z</published>
    <summary>深入解析 Claude Skills 的核心原理、渐进披露架构和最佳实践，手把手教你创建自定义技能，实现从临时提示词到可复用资产的升级。</summary>
    <title>Claude Skills 如何将提示词升级为可复用技能</title>
    <updated>2026-03-08T10:22:42.064Z</updated>
  </entry>
  <entry>
    <author>
      <name>Poetry</name>
    </author>
    <category term="Front-End" scheme="http://fe.poetries.top/categories/Front-End/"/>
    <category term="React Native" scheme="http://fe.poetries.top/tags/React-Native/"/>
    <category term="WebView" scheme="http://fe.poetries.top/tags/WebView/"/>
    <category term="本地加载" scheme="http://fe.poetries.top/tags/%E6%9C%AC%E5%9C%B0%E5%8A%A0%E8%BD%BD/"/>
    <category term="混合开发" scheme="http://fe.poetries.top/tags/%E6%B7%B7%E5%90%88%E5%BC%80%E5%8F%91/"/>
    <content>
      <![CDATA[<h2 id="导语"><a href="#导语" class="headerlink" title="导语"></a>导语</h2><p>在 React Native 项目中加载 Web 页面时，如果直接使用网络 URL，不仅需要等待资源加载，还可能面临网络不稳定导致的页面空白问题。将 Web 资源打包到 App 本地，可以实现<strong>秒开体验</strong>，大幅提升用户体验。</p><p>本文总结了在 React Native 中加载本地 Web 静态资源的完整方案，包含打包配置、RN 集成、RN 与 Web 通信等核心内容。</p><h2 id="一、Next-js-打包配置"><a href="#一、Next-js-打包配置" class="headerlink" title="一、Next.js 打包配置"></a>一、Next.js 打包配置</h2><h3 id="1-1-打包命令"><a href="#1-1-打包命令" class="headerlink" title="1.1 打包命令"></a>1.1 打包命令</h3><p>首先需要修改 <code>package.json</code> 中的打包命令：</p><figure class="highlight json"><table><tr><td class="code"><pre><span class="line"><span class="attr">&quot;build:app&quot;</span><span class="punctuation">:</span> <span class="string">&quot;cross-env APP_MODE=1 next build &amp;&amp; sh ./scripts/buildAfter.sh&quot;</span></span><br></pre></td></tr></table></figure><h3 id="1-2-构建后处理脚本"><a href="#1-2-构建后处理脚本" class="headerlink" title="1.2 构建后处理脚本"></a>1.2 构建后处理脚本</h3><p>创建 <code>scripts/buildAfter.sh</code> 脚本，处理构建产物：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="built_in">cd</span> dist</span><br><span class="line"></span><br><span class="line"><span class="comment"># 重命名，避免在安卓端加载不出来</span></span><br><span class="line"><span class="built_in">mv</span> ./_next ./next</span><br><span class="line"></span><br><span class="line"><span class="comment"># 替换代码中的 _next 为 next</span></span><br><span class="line">grep -rli <span class="string">&#x27;_next&#x27;</span> * | xargs -I@ sed -i <span class="string">&#x27;&#x27;</span> <span class="string">&#x27;s/_next/next/g&#x27;</span> @</span><br></pre></td></tr></table></figure><p><strong>为什么要重命名？</strong></p><p>在安卓端，<code>assets</code> 目录下的文件以下划线开头可能会导致加载失败。将 <code>_next</code> 重命名为 <code>next</code> 可以避免这个问题。</p><h2 id="二、Next-js-端接收数据"><a href="#二、Next-js-端接收数据" class="headerlink" title="二、Next.js 端接收数据"></a>二、Next.js 端接收数据</h2><h3 id="2-1-获取注入参数"><a href="#2-1-获取注入参数" class="headerlink" title="2.1 获取注入参数"></a>2.1 获取注入参数</h3><p>通过 <code>window.injectParams</code> 获取 React Native WebView 注入的参数：</p><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="comment">// 获取 react-native webview 注入的 window.injectParams 参数</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> <span class="title function_">getInjectParams</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> query = <span class="keyword">typeof</span> <span class="variable language_">window</span> !== <span class="string">&#x27;undefined&#x27;</span> ? <span class="variable language_">window</span>?.<span class="property">injectParams</span> || &#123;&#125; : &#123;&#125;</span><br><span class="line">  <span class="keyword">return</span> query <span class="keyword">as</span> <span class="title class_">InjectParams</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="2-2-监听-RN-消息"><a href="#2-2-监听-RN-消息" class="headerlink" title="2.2 监听 RN 消息"></a>2.2 监听 RN 消息</h3><p>实现 RN 与 Web 双向通信，接收来自 RN 的消息：</p><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="title function_">useEffect</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">if</span> (process.<span class="property">env</span>.<span class="property">APP_MODE</span> !== <span class="string">&#x27;1&#x27;</span>) <span class="keyword">return</span></span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> <span class="title function_">messageHandler</span> = (<span class="params">e: any</span>) =&gt; &#123;</span><br><span class="line">    <span class="keyword">const</span> data = e?.<span class="property">data</span> ? <span class="title class_">JSON</span>.<span class="title function_">parse</span>(e?.<span class="property">data</span>) : <span class="literal">undefined</span></span><br><span class="line">    <span class="keyword">const</span> type = data?.<span class="property">type</span> <span class="comment">// 消息类型</span></span><br><span class="line">    <span class="keyword">const</span> payload = data?.<span class="property">payload</span> <span class="comment">// 消息内容</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 处理行情同步</span></span><br><span class="line">    <span class="keyword">if</span> (type === <span class="string">&#x27;syncQuote&#x27;</span>) &#123;</span><br><span class="line">      ws.<span class="title function_">syncUpdateRNKlineData</span>(payload)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 处理品种切换</span></span><br><span class="line">    <span class="keyword">if</span> (type === <span class="string">&#x27;changeSymbol&#x27;</span> &amp;&amp; payload?.<span class="property">symbol</span>) &#123;</span><br><span class="line">      stores.<span class="property">global</span>.<span class="title function_">setSymbolInfo</span>(payload)</span><br><span class="line">      stores.<span class="property">ws</span>.<span class="property">lastbar</span> = &#123;&#125; <span class="comment">// 重置上一根 K 线</span></span><br><span class="line">      mitt.<span class="title function_">emit</span>(<span class="string">&#x27;symbol_change&#x27;</span>)</span><br><span class="line"></span><br><span class="line">      <span class="keyword">const</span> symbolName = payload?.<span class="property">symbol</span></span><br><span class="line">      <span class="built_in">setTimeout</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">        <span class="keyword">if</span> (ws.<span class="property">tvWidget</span>) &#123;</span><br><span class="line">          ws.<span class="property">tvWidget</span>.<span class="title function_">onChartReady</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">            ws.<span class="property">tvWidget</span>.<span class="title function_">activeChart</span>().<span class="title function_">resetData</span>()</span><br><span class="line">            ws.<span class="property">tvWidget</span>.<span class="title function_">activeChart</span>().<span class="title function_">setSymbol</span>(symbolName, &#123;</span><br><span class="line">              <span class="attr">dataReady</span>: <span class="function">() =&gt;</span> &#123;</span><br><span class="line">                <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">&#x27;切换品种成功&#x27;</span>)</span><br><span class="line">              &#125;</span><br><span class="line">            &#125;)</span><br><span class="line">          &#125;)</span><br><span class="line">        &#125;</span><br><span class="line">      &#125;, <span class="number">100</span>)</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// iOS 使用 window 监听，Android 使用 document 监听</span></span><br><span class="line">  <span class="keyword">if</span> (isAndroid) &#123;</span><br><span class="line">    <span class="variable language_">document</span>.<span class="title function_">addEventListener</span>(<span class="string">&#x27;message&#x27;</span>, messageHandler)</span><br><span class="line">  &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    <span class="variable language_">window</span>.<span class="title function_">addEventListener</span>(<span class="string">&#x27;message&#x27;</span>, messageHandler)</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> <span class="function">() =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> (isAndroid) &#123;</span><br><span class="line">      <span class="variable language_">document</span>.<span class="title function_">removeEventListener</span>(<span class="string">&#x27;message&#x27;</span>, messageHandler)</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">      <span class="variable language_">window</span>.<span class="title function_">removeEventListener</span>(<span class="string">&#x27;message&#x27;</span>, messageHandler)</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;, [])</span><br></pre></td></tr></table></figure><h2 id="三、集成到-React-Native"><a href="#三、集成到-React-Native" class="headerlink" title="三、集成到 React Native"></a>三、集成到 React Native</h2><h3 id="3-1-Android-配置"><a href="#3-1-Android-配置" class="headerlink" title="3.1 Android 配置"></a>3.1 Android 配置</h3><p>修改 <code>android/app/build.gradle</code>：</p><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line">android &#123;</span><br><span class="line">    sourceSets &#123;</span><br><span class="line">        main &#123;</span><br><span class="line">            <span class="comment">// 把项目根目录 public 下的所有文件拷贝到安卓的 src/main/assets 目录下</span></span><br><span class="line">            assets.<span class="property">srcDirs</span> = [<span class="string">&#x27;src/main/assets&#x27;</span>, <span class="string">&#x27;../../app/public&#x27;</span>]</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="3-2-iOS-配置"><a href="#3-2-iOS-配置" class="headerlink" title="3.2 iOS 配置"></a>3.2 iOS 配置</h3><p>将打包后的 bundle 文件添加到 Xcode 项目中：</p><p><img src="https://s.poetries.top/uploads/2026/03/38493fce112c9f87.png" alt="添加 Bundle 到 Xcode"></p><p><img src="https://s.poetries.top/uploads/2026/03/2867f4bd230557df.png" alt="选择文件"></p><p><img src="https://s.poetries.top/uploads/2026/03/5c4f1b74f837df22.png" alt="确认添加"></p><h3 id="3-3-WebView-组件实现"><a href="#3-3-WebView-组件实现" class="headerlink" title="3.3 WebView 组件实现"></a>3.3 WebView 组件实现</h3><p>核心代码实现 RN 加载本地 Web 资源：</p><figure class="highlight tsx"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">WebView</span> <span class="keyword">from</span> <span class="string">&#x27;react-native-webview&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">Tradingview</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> webviewRefs = useRef&lt;<span class="built_in">any</span>&gt;(<span class="literal">null</span>)</span><br><span class="line">  <span class="keyword">const</span> &#123; <span class="built_in">symbol</span>, dataSourceCode, dataSourceSymbol, accountGroupId &#125; = <span class="title function_">useParams</span>()</span><br><span class="line">  <span class="keyword">const</span> &#123; theme, locale &#125; = <span class="title function_">useTheme</span>()</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 本地 bundle 路径</span></span><br><span class="line">  <span class="keyword">const</span> sourceUri = <span class="title class_">Platform</span>.<span class="property">OS</span> === <span class="string">&#x27;ios&#x27;</span></span><br><span class="line">    ? <span class="string">&#x27;Tradingview.bundle/index.html&#x27;</span></span><br><span class="line">    : <span class="string">&#x27;file:///android_asset/Tradingview.bundle/index.html&#x27;</span></span><br><span class="line"></span><br><span class="line">  <span class="comment">// 注入 JavaScript 参数</span></span><br><span class="line">  <span class="keyword">const</span> injectedJavaScript = <span class="string">`</span></span><br><span class="line"><span class="string">    window.injectParams = &#123;</span></span><br><span class="line"><span class="string">      &#x27;symbolName&#x27;: &#x27;<span class="subst">$&#123;<span class="built_in">symbol</span>&#125;</span>&#x27;,</span></span><br><span class="line"><span class="string">      &#x27;dataSourceCode&#x27;: &#x27;<span class="subst">$&#123;dataSourceCode&#125;</span>&#x27;,</span></span><br><span class="line"><span class="string">      &#x27;dataSourceSymbol&#x27;: &#x27;<span class="subst">$&#123;dataSourceSymbol&#125;</span>&#x27;,</span></span><br><span class="line"><span class="string">      &#x27;accountGroupId&#x27;: &#x27;<span class="subst">$&#123;accountGroupId&#125;</span>&#x27;,</span></span><br><span class="line"><span class="string">      &#x27;locale&#x27;: &#x27;<span class="subst">$&#123;locale&#125;</span>&#x27;,</span></span><br><span class="line"><span class="string">      &#x27;colorType&#x27;: &#x27;<span class="subst">$&#123;theme.direction + <span class="number">1</span>&#125;</span>&#x27;,</span></span><br><span class="line"><span class="string">      &#x27;token&#x27;: &#x27;<span class="subst">$&#123;token&#125;</span>&#x27;,</span></span><br><span class="line"><span class="string">      &#x27;baseUrl&#x27;: &#x27;<span class="subst">$&#123;baseUrl&#125;</span>&#x27;,</span></span><br><span class="line"><span class="string">      &#x27;wsUrl&#x27;: &#x27;<span class="subst">$&#123;wsUrl&#125;</span>&#x27;,</span></span><br><span class="line"><span class="string">      &#x27;symbolInfo&#x27;: <span class="subst">$&#123;<span class="built_in">JSON</span>.stringify(symbolInfo)&#125;</span>,</span></span><br><span class="line"><span class="string">      &#x27;debug&#x27;: <span class="subst">$&#123;__DEV__&#125;</span>,</span></span><br><span class="line"><span class="string">      &#x27;watermarkLogoUrl&#x27;: &#x27;<span class="subst">$&#123;watermarkLogoUrl&#125;</span>&#x27;,</span></span><br><span class="line"><span class="string">    &#125;;</span></span><br><span class="line"><span class="string">    true;</span></span><br><span class="line"><span class="string">  `</span></span><br><span class="line"></span><br><span class="line">  <span class="comment">// 切换品种</span></span><br><span class="line">  <span class="keyword">const</span> switchSymbol = <span class="title function_">useCallback</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">const</span> message = <span class="title class_">JSON</span>.<span class="title function_">stringify</span>(&#123;</span><br><span class="line">      <span class="attr">type</span>: <span class="string">&#x27;changeSymbol&#x27;</span>,</span><br><span class="line">      <span class="attr">payload</span>: <span class="title function_">getSymbolInfo</span>()</span><br><span class="line">    &#125;)</span><br><span class="line">    webviewRefs?.<span class="property">current</span>?.<span class="property">postMessage</span>?.(message)</span><br><span class="line">  &#125;, [<span class="built_in">symbol</span>])</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 同步行情数据</span></span><br><span class="line">  <span class="title function_">useEffect</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> (currentQuote) &#123;</span><br><span class="line">      <span class="keyword">const</span> message = <span class="title class_">JSON</span>.<span class="title function_">stringify</span>(&#123;</span><br><span class="line">        <span class="attr">type</span>: <span class="string">&#x27;syncQuote&#x27;</span>,</span><br><span class="line">        <span class="attr">payload</span>: currentQuote</span><br><span class="line">      &#125;)</span><br><span class="line">      webviewRefs?.<span class="property">current</span>?.<span class="property">postMessage</span>?.(message)</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;, [currentQuote])</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">WebView</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">      <span class="attr">source</span>=<span class="string">&#123;&#123;</span> <span class="attr">uri:</span> <span class="attr">sourceUri</span> &#125;&#125;</span></span></span><br><span class="line"><span class="tag"><span class="language-xml">      <span class="attr">injectedJavaScript</span>=<span class="string">&#123;injectedJavaScript&#125;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">      <span class="attr">javaScriptEnabled</span>=<span class="string">&#123;true&#125;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">      <span class="attr">allowFileAccess</span>=<span class="string">&#123;true&#125;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">      <span class="attr">allowFileAccessFromFileURLs</span>=<span class="string">&#123;true&#125;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">      <span class="attr">originWhitelist</span>=<span class="string">&#123;[</span>&#x27;*&#x27;]&#125;</span></span></span><br><span class="line"><span class="tag"><span class="language-xml">      <span class="attr">scalesPageToFit</span>=<span class="string">&#123;false&#125;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">      <span class="attr">scrollEnabled</span>=<span class="string">&#123;false&#125;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">      <span class="attr">domStorageEnabled</span>=<span class="string">&#123;true&#125;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">      <span class="attr">mixedContentMode</span>=<span class="string">&quot;always&quot;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">      <span class="attr">onMessage</span>=<span class="string">&#123;(event)</span> =&gt;</span> &#123;</span></span><br><span class="line"><span class="language-xml">        // iOS 必须加上 onMessage 否则加载不出本地资源</span></span><br><span class="line"><span class="language-xml">        console.log(&#x27;event.nativeEvent.data&#x27;, event.nativeEvent.data)</span></span><br><span class="line"><span class="language-xml">      &#125;&#125;</span></span><br><span class="line"><span class="language-xml">    /&gt;</span></span><br><span class="line">  )</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="3-4-应用前后台处理"><a href="#3-4-应用前后台处理" class="headerlink" title="3.4 应用前后台处理"></a>3.4 应用前后台处理</h3><p>处理 App 进入前台&#x2F;后台时的逻辑：</p><figure class="highlight typescript"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> useAppState <span class="keyword">from</span> <span class="string">&#x27;@/hooks/useAppState&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">checkTradingviewReload</span> = <span class="keyword">async</span> (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">  <span class="comment">// 缓存时间大于 5 分钟，reload 整个实例</span></span><br><span class="line">  <span class="keyword">const</span> updateTime = <span class="keyword">await</span> <span class="title function_">STORAGE_GET_TRADINGVIEW_RELOAD_TIME</span>()</span><br><span class="line">  <span class="keyword">if</span> ((updateTime &amp;&amp; <span class="title class_">Date</span>.<span class="title function_">now</span>() - updateTime &gt; <span class="number">5</span> * <span class="number">60</span> * <span class="number">1000</span>) || !updateTime) &#123;</span><br><span class="line">    <span class="title function_">STORAGE_SET_TRADINGVIEW_RELOAD_TIME</span>(<span class="title class_">Date</span>.<span class="title function_">now</span>())</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">true</span></span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">return</span> <span class="literal">false</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="title function_">useAppState</span>(</span><br><span class="line">  <span class="title function_">async</span> () =&gt; &#123;</span><br><span class="line">    <span class="comment">// 应用回到前台</span></span><br><span class="line">    <span class="keyword">const</span> shouldReload = <span class="keyword">await</span> <span class="title function_">checkTradingviewReload</span>()</span><br><span class="line">    <span class="keyword">if</span> (shouldReload &amp;&amp; webviewRefs?.<span class="property">current</span>) &#123;</span><br><span class="line">      <span class="comment">// 刷新整个 WebView，避免长时间不进入导致页面空白</span></span><br><span class="line">      webviewRefs?.<span class="property">current</span>?.<span class="property">reload</span>?.()</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">      <span class="comment">// 不刷新页面，只切换品种</span></span><br><span class="line">      <span class="title function_">switchSymbol</span>()</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="function">() =&gt;</span> &#123;</span><br><span class="line">    <span class="comment">// 应用进入后台</span></span><br><span class="line">    <span class="title function_">STORAGE_SET_TRADINGVIEW_RELOAD_TIME</span>(<span class="title class_">Date</span>.<span class="title function_">now</span>())</span><br><span class="line">    <span class="comment">// 清空缓存，否则绘制有问题</span></span><br><span class="line">    ws.<span class="property">quotes</span> = <span class="keyword">new</span> <span class="title class_">Map</span>()</span><br><span class="line">  &#125;</span><br><span class="line">)</span><br></pre></td></tr></table></figure><h2 id="四、关键配置说明"><a href="#四、关键配置说明" class="headerlink" title="四、关键配置说明"></a>四、关键配置说明</h2><h3 id="4-1-WebView-重要属性"><a href="#4-1-WebView-重要属性" class="headerlink" title="4.1 WebView 重要属性"></a>4.1 WebView 重要属性</h3><table><thead><tr><th>属性</th><th>说明</th></tr></thead><tbody><tr><td><code>source.uri</code></td><td>本地资源路径（iOS&#x2F;Android 不同）</td></tr><tr><td><code>injectedJavaScript</code></td><td>注入 JS 参数到 Web 页面</td></tr><tr><td><code>javaScriptEnabled</code></td><td>启用 JavaScript</td></tr><tr><td><code>allowFileAccess</code></td><td>允许通过 file:&#x2F;&#x2F; 形式加载资源</td></tr><tr><td><code>scalesPageToFit</code></td><td>禁止页面缩放</td></tr><tr><td><code>domStorageEnabled</code></td><td>启用 DOM 存储</td></tr><tr><td><code>mixedContentMode</code></td><td>允许加载非 HTTPS 内容</td></tr></tbody></table><h3 id="4-2-RN-与-Web-通信方式"><a href="#4-2-RN-与-Web-通信方式" class="headerlink" title="4.2 RN 与 Web 通信方式"></a>4.2 RN 与 Web 通信方式</h3><ul><li><strong>RN → Web</strong>：通过 <code>postMessage</code> 发送消息，Web 端通过监听 <code>message</code> 事件接收</li><li><strong>Web → RN</strong>：Web 端调用 <code>window.ReactNativeWebView.postMessage()</code>，RN 端通过 <code>onMessage</code> 接收</li></ul><h2 id="五、实现效果"><a href="#五、实现效果" class="headerlink" title="五、实现效果"></a>五、实现效果</h2><ul><li>在 React Native 端加载 Web 静态资源可以做到<strong>十几 MB 文件秒开</strong></li><li>完全不涉及网络请求，直接从本地加载</li><li>用户体验接近原生应用</li></ul><h2 id="六、常见问题"><a href="#六、常见问题" class="headerlink" title="六、常见问题"></a>六、常见问题</h2><h3 id="6-1-安卓端资源加载失败"><a href="#6-1-安卓端资源加载失败" class="headerlink" title="6.1 安卓端资源加载失败"></a>6.1 安卓端资源加载失败</h3><ul><li>检查 <code>assets.srcDirs</code> 配置是否正确</li><li>确保文件命名没有以下划线开头</li></ul><h3 id="6-2-iOS-白屏"><a href="#6-2-iOS-白屏" class="headerlink" title="6.2 iOS 白屏"></a>6.2 iOS 白屏</h3><ul><li>确保 Bundle 文件已正确添加到 Xcode 项目</li><li>检查 <code>onMessage</code> 是否正确实现</li></ul><h3 id="6-3-长时间不进入页面空白"><a href="#6-3-长时间不进入页面空白" class="headerlink" title="6.3 长时间不进入页面空白"></a>6.3 长时间不进入页面空白</h3><ul><li>实现应用前后台监听，5 分钟以上重新加载 WebView</li></ul><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>本文详细总结了 React Native 加载本地 Web 静态资源的完整方案，包括：</p><ol><li><strong>打包配置</strong>：Next.js 构建命令和后处理脚本</li><li><strong>数据接收</strong>：Web 端获取 RN 注入参数的实现</li><li><strong>RN 集成</strong>：Android 和 iOS 的配置差异</li><li><strong>双向通信</strong>：RN 与 Web 的消息传递机制</li><li><strong>性能优化</strong>：应用前后台处理策略</li></ol><p>通过以上方案，可以在 React Native 中实现 Web 资源的本地加载，提供流畅的用户体验。</p>]]>
    </content>
    <id>http://fe.poetries.top/2026/02/10/react-native-web-local-file-loading/</id>
    <link href="http://fe.poetries.top/2026/02/10/react-native-web-local-file-loading/"/>
    <published>2026-02-10T06:40:12.000Z</published>
    <summary>总结 React Native 中加载本地 Web 静态资源的完整方案，包括 Next.js 打包配置、RN 集成步骤、通信机制实现等核心内容。</summary>
    <title>React Native + Web 本地文件加载方案与踩坑总结</title>
    <updated>2026-03-08T10:22:42.094Z</updated>
  </entry>
  <entry>
    <author>
      <name>Poetry</name>
    </author>
    <category term="Front-End" scheme="http://fe.poetries.top/categories/Front-End/"/>
    <category term="React" scheme="http://fe.poetries.top/tags/React/"/>
    <category term="Next.js" scheme="http://fe.poetries.top/tags/Next-js/"/>
    <category term="TradingView" scheme="http://fe.poetries.top/tags/TradingView/"/>
    <category term="K线图" scheme="http://fe.poetries.top/tags/K%E7%BA%BF%E5%9B%BE/"/>
    <category term="图表集成" scheme="http://fe.poetries.top/tags/%E5%9B%BE%E8%A1%A8%E9%9B%86%E6%88%90/"/>
    <content>
      <![CDATA[<h2 id="导语"><a href="#导语" class="headerlink" title="导语"></a>导语</h2><p>TradingView 是全球最专业的金融图表可视化库之一，提供了功能强大的 K 线图、指标系统和技术分析工具。在金融行情类 Web 应用中，接入 TradingView 是提升用户体验的首选方案。</p><p>本文将基于实际项目代码，系统讲解如何在 <strong>Next.js</strong> 项目中接入 TradingView Charts，包括环境配置、Datafeed 数据馈送实现、自定义指标开发、主题样式定制、以及关键的性能优化策略。</p><h2 id="一、项目准备与环境配置"><a href="#一、项目准备与环境配置" class="headerlink" title="一、项目准备与环境配置"></a>一、项目准备与环境配置</h2><h3 id="1-1-获取-TradingView-图表库"><a href="#1-1-获取-TradingView-图表库" class="headerlink" title="1.1 获取 TradingView 图表库"></a>1.1 获取 TradingView 图表库</h3><p>TradingView 图表库需要从官方获取授权后下载。获取后将文件放置在项目的 <code>public/static/charting_library</code> 目录下：</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">public/</span><br><span class="line">  └── static/</span><br><span class="line">      └── charting_library/</span><br><span class="line">          ├── charting_library.standalone.js</span><br><span class="line">          └── bundles/</span><br><span class="line">              ├── *.js</span><br><span class="line">              └── *.css</span><br></pre></td></tr></table></figure><h3 id="1-2-组件目录结构"><a href="#1-2-组件目录结构" class="headerlink" title="1.2 组件目录结构"></a>1.2 组件目录结构</h3><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">src/components/Tradingview/</span><br><span class="line">├── index.tsx              # 主组件</span><br><span class="line">├── datafeed.ts            # 数据馈送实现</span><br><span class="line">├── widgetOpts.tsx         # 图表配置选项</span><br><span class="line">├── widgetMethods.ts       # 图表方法工具</span><br><span class="line">├── theme.ts               # 主题配置</span><br><span class="line">├── constant.ts           # 常量定义</span><br><span class="line">└── customIndicators/      # 自定义指标</span><br><span class="line">    ├── ma.ts</span><br><span class="line">    ├── macd.ts</span><br><span class="line">    ├── kdj.ts</span><br><span class="line">    └── customerRSI.ts</span><br></pre></td></tr></table></figure><h2 id="二、核心组件实现"><a href="#二、核心组件实现" class="headerlink" title="二、核心组件实现"></a>二、核心组件实现</h2><h3 id="2-1-主组件：TradingView-图表容器"><a href="#2-1-主组件：TradingView-图表容器" class="headerlink" title="2.1 主组件：TradingView 图表容器"></a>2.1 主组件：TradingView 图表容器</h3><figure class="highlight tsx"><table><tr><td class="code"><pre><span class="line"><span class="comment">// src/components/Tradingview/index.tsx</span></span><br><span class="line"><span class="keyword">import</span> &#123; useEffect, useRef, useState &#125; <span class="keyword">from</span> <span class="string">&#x27;react&#x27;</span></span><br><span class="line"><span class="keyword">import</span> &#123; widget &#125; <span class="keyword">from</span> <span class="string">&#x27;public/static/charting_library&#x27;</span></span><br><span class="line"><span class="keyword">import</span> &#123; useStores &#125; <span class="keyword">from</span> <span class="string">&#x27;@/context/mobxProvider&#x27;</span></span><br><span class="line"><span class="keyword">import</span> &#123; <span class="variable constant_">STORAGE_GET_CHART_PROPS</span>, <span class="variable constant_">STORAGE_REMOVE_CHART_PROPS</span>, <span class="title class_">ThemeConst</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;./constant&#x27;</span></span><br><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">ColorType</span>, applyOverrides, createWatermarkLogo, setCSSCustomProperty, setChartStyleProperties &#125; <span class="keyword">from</span> <span class="string">&#x27;./widgetMethods&#x27;</span></span><br><span class="line"><span class="keyword">import</span> getWidgetOpts <span class="keyword">from</span> <span class="string">&#x27;./widgetOpts&#x27;</span></span><br><span class="line"><span class="keyword">import</span> &#123; useConfig &#125; <span class="keyword">from</span> <span class="string">&#x27;@/context/configProvider&#x27;</span></span><br><span class="line"><span class="keyword">import</span> &#123; useRouter &#125; <span class="keyword">from</span> <span class="string">&#x27;next/router&#x27;</span></span><br><span class="line"><span class="keyword">import</span> stores <span class="keyword">from</span> <span class="string">&#x27;@/stores&#x27;</span></span><br><span class="line"><span class="keyword">import</span> &#123; observer &#125; <span class="keyword">from</span> <span class="string">&#x27;mobx-react&#x27;</span></span><br><span class="line"><span class="keyword">import</span> &#123; <span class="variable constant_">STORAGE_SET_TRADINGVIEW_RESOLUTION</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;@/utils/storage&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">Tradingview</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> chartContainerRef = useRef&lt;<span class="title class_">HTMLDivElement</span>&gt;()</span><br><span class="line">  <span class="keyword">const</span> &#123; ws &#125; = <span class="title function_">useStores</span>()</span><br><span class="line">  <span class="keyword">const</span> &#123; isMobile, isPc &#125; = <span class="title function_">useConfig</span>()</span><br><span class="line">  <span class="keyword">const</span> router = <span class="title function_">useRouter</span>()</span><br><span class="line">  <span class="keyword">const</span> [isChartLoading, setIsChartLoading] = <span class="title function_">useState</span>(<span class="literal">true</span>)</span><br><span class="line">  <span class="keyword">const</span> [loading, setLoading] = <span class="title function_">useState</span>(<span class="literal">true</span>)</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> query = &#123;</span><br><span class="line">    ...router.<span class="property">query</span>,</span><br><span class="line">    ...<span class="title function_">getInjectParams</span>()</span><br><span class="line">  &#125; <span class="keyword">as</span> <span class="built_in">any</span></span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> datafeedParams = &#123;</span><br><span class="line">    <span class="attr">setActiveSymbolInfo</span>: ws.<span class="property">setActiveSymbolInfo</span>,</span><br><span class="line">    <span class="attr">removeActiveSymbol</span>: ws.<span class="property">removeActiveSymbol</span>,</span><br><span class="line">    <span class="attr">getDataFeedBarCallback</span>: ws.<span class="property">getDataFeedBarCallback</span>,</span><br><span class="line">    <span class="attr">dataSourceCode</span>: query.<span class="property">dataSourceCode</span></span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> params = &#123;</span><br><span class="line">    <span class="attr">symbol</span>: (query.<span class="property">symbolName</span> || <span class="string">&#x27;BTCUSDT&#x27;</span>) <span class="keyword">as</span> <span class="built_in">string</span>,</span><br><span class="line">    <span class="attr">locale</span>: (query.<span class="property">locale</span> || <span class="string">&#x27;en&#x27;</span>) <span class="keyword">as</span> <span class="title class_">LanguageCode</span>,</span><br><span class="line">    <span class="attr">theme</span>: (query.<span class="property">theme</span> || <span class="string">&#x27;light&#x27;</span>) <span class="keyword">as</span> <span class="title class_">ThemeName</span>,</span><br><span class="line">    <span class="attr">colorType</span>: <span class="title class_">Number</span>(query.<span class="property">colorType</span> || <span class="number">1</span>) <span class="keyword">as</span> <span class="title class_">ColorType</span>,</span><br><span class="line">    isMobile,</span><br><span class="line">    <span class="attr">bgGradientStartColor</span>: query.<span class="property">bgGradientStartColor</span> ? <span class="string">`#<span class="subst">$&#123;query.bgGradientStartColor&#125;</span>`</span> : <span class="string">&#x27;&#x27;</span>,</span><br><span class="line">    <span class="attr">bgGradientEndColor</span>: query.<span class="property">bgGradientEndColor</span> ? <span class="string">`#<span class="subst">$&#123;query.bgGradientEndColor&#125;</span>`</span> : <span class="string">&#x27;&#x27;</span></span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="title function_">useEffect</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">&#x27;Tradingview组件初始化&#x27;</span>)</span><br><span class="line">    <span class="keyword">const</span> showBottomMACD = <span class="title class_">Number</span>(query.<span class="property">showBottomMACD</span> || <span class="number">1</span>)</span><br><span class="line">    <span class="keyword">const</span> chartType = (query.<span class="property">chartType</span> !== <span class="string">&#x27;&#x27;</span> ? <span class="title class_">Number</span>(query.<span class="property">chartType</span> || <span class="number">1</span>) : <span class="number">1</span>) <span class="keyword">as</span> <span class="title class_">ChartStyle</span></span><br><span class="line">    <span class="keyword">const</span> theme = params.<span class="property">theme</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 切换主题时清除本地缓存，避免颜色闪烁</span></span><br><span class="line">    <span class="keyword">const</span> defaultBgColor = theme === <span class="string">&#x27;dark&#x27;</span> ? <span class="title class_">ThemeConst</span>.<span class="property">black</span> : <span class="title class_">ThemeConst</span>.<span class="property">white</span></span><br><span class="line">    <span class="keyword">if</span> (theme &amp;&amp; defaultBgColor !== <span class="title function_">STORAGE_GET_CHART_PROPS</span>(<span class="string">&#x27;paneProperties.background&#x27;</span>)) &#123;</span><br><span class="line">      <span class="title function_">STORAGE_REMOVE_CHART_PROPS</span>()</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">const</span> widgetOptions = <span class="title function_">getWidgetOpts</span>(params, chartContainerRef.<span class="property">current</span>, datafeedParams)</span><br><span class="line">    <span class="keyword">const</span> tvWidget = <span class="keyword">new</span> <span class="title function_">widget</span>(widgetOptions)</span><br><span class="line"></span><br><span class="line">    <span class="built_in">setTimeout</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">      <span class="title function_">setLoading</span>(<span class="literal">false</span>)</span><br><span class="line">    &#125;, <span class="number">200</span>)</span><br><span class="line"></span><br><span class="line">    tvWidget.<span class="title function_">onChartReady</span>(<span class="title function_">async</span> () =&gt; &#123;</span><br><span class="line">      <span class="title function_">setIsChartLoading</span>(<span class="literal">false</span>)</span><br><span class="line"></span><br><span class="line">      <span class="comment">// 动态设置 CSS 变量</span></span><br><span class="line">      <span class="title function_">setCSSCustomProperty</span>(&#123; tvWidget, theme &#125;)</span><br><span class="line"></span><br><span class="line">      <span class="comment">// 监听时间周期变化</span></span><br><span class="line">      tvWidget.<span class="title function_">activeChart</span>().<span class="title function_">onIntervalChanged</span>().<span class="title function_">subscribe</span>(<span class="literal">null</span>, <span class="function">(<span class="params">interval, timeframeObj</span>) =&gt;</span> &#123;</span><br><span class="line">        <span class="comment">// 记录当前分辨率</span></span><br><span class="line">        <span class="title function_">STORAGE_SET_TRADINGVIEW_RESOLUTION</span>(interval)</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 日周月级别使用 UTC 时区，分钟级别使用上海时区</span></span><br><span class="line">        <span class="keyword">if</span> ([<span class="string">&#x27;D&#x27;</span>, <span class="string">&#x27;W&#x27;</span>, <span class="string">&#x27;M&#x27;</span>, <span class="string">&#x27;Y&#x27;</span>].<span class="title function_">some</span>(<span class="function">(<span class="params">item</span>) =&gt;</span> interval.<span class="title function_">endsWith</span>(item))) &#123;</span><br><span class="line">          tvWidget.<span class="title function_">activeChart</span>().<span class="title function_">getTimezoneApi</span>().<span class="title function_">setTimezone</span>(<span class="string">&#x27;Etc/UTC&#x27;</span>)</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">          tvWidget.<span class="title function_">activeChart</span>().<span class="title function_">getTimezoneApi</span>().<span class="title function_">setTimezone</span>(<span class="string">&#x27;Asia/Shanghai&#x27;</span>)</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        ws.<span class="property">activeSymbolInfo</span>.<span class="property">onResetCacheNeededCallback</span>?.()</span><br><span class="line">        <span class="built_in">setTimeout</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">          tvWidget.<span class="title function_">activeChart</span>().<span class="title function_">resetData</span>()</span><br><span class="line">        &#125;, <span class="number">100</span>)</span><br><span class="line">      &#125;)</span><br><span class="line"></span><br><span class="line">      <span class="comment">// 默认显示 MACD 指标</span></span><br><span class="line">      <span class="keyword">if</span> (showBottomMACD === <span class="number">1</span>) &#123;</span><br><span class="line">        tvWidget.<span class="title function_">activeChart</span>().<span class="title function_">createStudy</span>(</span><br><span class="line">          <span class="string">&#x27;MACD&#x27;</span>,</span><br><span class="line">          <span class="literal">false</span>,</span><br><span class="line">          <span class="literal">false</span>,</span><br><span class="line">          &#123; <span class="attr">in_0</span>: <span class="number">12</span>, <span class="attr">in_1</span>: <span class="number">26</span>, <span class="attr">in_3</span>: <span class="string">&#x27;close&#x27;</span>, <span class="attr">in_2</span>: <span class="number">9</span> &#125;,</span><br><span class="line">          &#123;</span><br><span class="line">            <span class="string">&#x27;Histogram.color.3&#x27;</span>: <span class="string">&#x27;rgba(197, 71, 71, 0.7188)&#x27;</span>,</span><br><span class="line">            <span class="attr">showLabelsOnPriceScale</span>: !!isPc</span><br><span class="line">          &#125;</span><br><span class="line">        )</span><br><span class="line">      &#125;</span><br><span class="line"></span><br><span class="line">      <span class="comment">// 创建自定义 MA 指标</span></span><br><span class="line">      tvWidget.<span class="title function_">activeChart</span>().<span class="title function_">createStudy</span>(</span><br><span class="line">        <span class="string">&#x27;Customer Moving Average&#x27;</span>,</span><br><span class="line">        <span class="literal">false</span>,</span><br><span class="line">        <span class="literal">false</span>,</span><br><span class="line">        &#123;&#125;,</span><br><span class="line">        &#123; <span class="attr">showLabelsOnPriceScale</span>: <span class="literal">false</span> &#125;</span><br><span class="line">      )</span><br><span class="line"></span><br><span class="line">      <span class="comment">// 动态切换主题</span></span><br><span class="line">      <span class="keyword">if</span> (query.<span class="property">theme</span> &amp;&amp; !params.<span class="property">bgGradientStartColor</span>) &#123;</span><br><span class="line">        <span class="keyword">await</span> tvWidget.<span class="title function_">changeTheme</span>(theme)</span><br><span class="line">      &#125;</span><br><span class="line"></span><br><span class="line">      <span class="comment">// 设置 K 线柱样式（绿涨红跌 / 红涨绿跌）</span></span><br><span class="line">      <span class="title function_">setChartStyleProperties</span>(&#123; <span class="attr">colorType</span>: params.<span class="property">colorType</span>, tvWidget &#125;)</span><br><span class="line"></span><br><span class="line">      <span class="comment">// 应用覆盖样式</span></span><br><span class="line">      <span class="title function_">applyOverrides</span>(&#123;</span><br><span class="line">        tvWidget,</span><br><span class="line">        chartType,</span><br><span class="line">        <span class="attr">bgGradientStartColor</span>: params.<span class="property">bgGradientStartColor</span>,</span><br><span class="line">        <span class="attr">bgGradientEndColor</span>: params.<span class="property">bgGradientEndColor</span></span><br><span class="line">      &#125;)</span><br><span class="line"></span><br><span class="line">      <span class="comment">// 添加水印 Logo</span></span><br><span class="line">      <span class="keyword">if</span> (query.<span class="property">hideWatermarkLogo</span> !== <span class="string">&#x27;0&#x27;</span> &amp;&amp; query.<span class="property">watermarkLogoUrl</span>) &#123;</span><br><span class="line">        <span class="title function_">createWatermarkLogo</span>(query.<span class="property">watermarkLogoUrl</span>)</span><br><span class="line">      &#125;</span><br><span class="line"></span><br><span class="line">      <span class="comment">// 记录实例</span></span><br><span class="line">      ws.<span class="title function_">setTvWidget</span>(tvWidget)</span><br><span class="line">      <span class="variable language_">window</span>.<span class="property">tvWidget</span> = tvWidget</span><br><span class="line">    &#125;)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> <span class="function">() =&gt;</span> &#123;</span><br><span class="line">      tvWidget.<span class="title function_">remove</span>()</span><br><span class="line">      mitt.<span class="title function_">off</span>(<span class="string">&#x27;symbol_change&#x27;</span>)</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;, [router.<span class="property">query</span>])</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">div</span> <span class="attr">style</span>=<span class="string">&#123;&#123;</span> <span class="attr">position:</span> &#x27;<span class="attr">relative</span>&#x27; &#125;&#125;&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">div</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">        <span class="attr">id</span>=<span class="string">&quot;tradingview&quot;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">        <span class="attr">ref</span>=<span class="string">&#123;chartContainerRef&#125;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">        <span class="attr">style</span>=<span class="string">&#123;&#123;</span> <span class="attr">height:</span> &#x27;<span class="attr">calc</span>(<span class="attr">100vh</span> <span class="attr">-</span> <span class="attr">60px</span>)&#x27;, <span class="attr">opacity:</span> <span class="attr">loading</span> ? <span class="attr">0</span> <span class="attr">:</span> <span class="attr">1</span> &#125;&#125;</span></span></span><br><span class="line"><span class="tag"><span class="language-xml">      /&gt;</span></span></span><br><span class="line"><span class="language-xml">      &#123;isChartLoading &amp;&amp; (</span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">div</span> <span class="attr">className</span>=<span class="string">&quot;loading-container&quot;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">          <span class="tag">&lt;<span class="name">div</span> <span class="attr">className</span>=<span class="string">&quot;loading&quot;</span>&gt;</span><span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      )&#125;</span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line">  )</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="title function_">observer</span>(<span class="title class_">Tradingview</span>)</span><br></pre></td></tr></table></figure><h3 id="2-2-Datafeed-数据馈送实现"><a href="#2-2-Datafeed-数据馈送实现" class="headerlink" title="2.2 Datafeed 数据馈送实现"></a>2.2 Datafeed 数据馈送实现</h3><p>Datafeed 是 TradingView 与后端数据交互的核心接口，需要实现以下方法：</p><figure class="highlight typescript"><table><tr><td class="code"><pre><span class="line"><span class="comment">// src/components/Tradingview/datafeed.ts</span></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">DataFeedBase</span> &#123;</span><br><span class="line">  <span class="attr">configuration</span>: <span class="title class_">DatafeedConfiguration</span></span><br><span class="line"></span><br><span class="line">  <span class="title function_">constructor</span>(<span class="params"><span class="attr">props</span>: <span class="title class_">Partial</span>&lt;<span class="title class_">ChartingLibraryWidgetOptions</span>&gt;</span>) &#123;</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">configuration</span> = &#123;</span><br><span class="line">      <span class="attr">supports_time</span>: <span class="literal">true</span>,</span><br><span class="line">      <span class="attr">supports_timescale_marks</span>: <span class="literal">true</span>,</span><br><span class="line">      <span class="attr">supports_marks</span>: <span class="literal">true</span>,</span><br><span class="line">      <span class="comment">// 支持的分辨率</span></span><br><span class="line">      <span class="attr">supported_resolutions</span>: [<span class="string">&#x27;1&#x27;</span>, <span class="string">&#x27;5&#x27;</span>, <span class="string">&#x27;15&#x27;</span>, <span class="string">&#x27;30&#x27;</span>, <span class="string">&#x27;60&#x27;</span>, <span class="string">&#x27;240&#x27;</span>, <span class="string">&#x27;1D&#x27;</span>, <span class="string">&#x27;1W&#x27;</span>, <span class="string">&#x27;1M&#x27;</span>],</span><br><span class="line">      <span class="attr">intraday_multipliers</span>: [<span class="string">&#x27;1&#x27;</span>, <span class="string">&#x27;5&#x27;</span>, <span class="string">&#x27;15&#x27;</span>, <span class="string">&#x27;30&#x27;</span>, <span class="string">&#x27;60&#x27;</span>, <span class="string">&#x27;240&#x27;</span>, <span class="string">&#x27;1D&#x27;</span>, <span class="string">&#x27;1W&#x27;</span>, <span class="string">&#x27;1M&#x27;</span>]</span><br><span class="line">    &#125; <span class="keyword">as</span> <span class="title class_">DatafeedConfiguration</span></span><br><span class="line"></span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">setActiveSymbolInfo</span> = props.<span class="property">setActiveSymbolInfo</span></span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">removeActiveSymbol</span> = props.<span class="property">removeActiveSymbol</span></span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">getDataFeedBarCallback</span> = props.<span class="property">getDataFeedBarCallback</span></span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">isZh</span> = props.<span class="property">locale</span> === <span class="string">&#x27;zh_TW&#x27;</span></span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 图表初始化时调用，设置支持的配置</span></span><br><span class="line">  <span class="title function_">onReady</span>(<span class="params">callback</span>) &#123;</span><br><span class="line">    <span class="built_in">setTimeout</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">      <span class="title function_">callback</span>(<span class="variable language_">this</span>.<span class="property">configuration</span>)</span><br><span class="line">    &#125;, <span class="number">0</span>)</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 解析品种信息</span></span><br><span class="line">  <span class="keyword">async</span> <span class="title function_">resolveSymbol</span>(<span class="params">symbolName, onSymbolResolvedCallback, onResolveErrorCallback, extension</span>) &#123;</span><br><span class="line">    <span class="keyword">const</span> resolution = <span class="title class_">String</span>(<span class="title function_">STORAGE_GET_TRADINGVIEW_RESOLUTION</span>() || <span class="string">&#x27;&#x27;</span>)</span><br><span class="line">    <span class="keyword">const</span> <span class="variable constant_">ENV</span> = <span class="title function_">getEnv</span>()</span><br><span class="line">    <span class="keyword">const</span> urlPrefix = <span class="variable constant_">ENV</span>.<span class="property">isApp</span> ? <span class="title function_">getInjectParams</span>().<span class="property">baseUrl</span> : <span class="string">&#x27;&#x27;</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">let</span> symbolInfo</span><br><span class="line">    <span class="keyword">if</span> (!<span class="variable constant_">ENV</span>.<span class="property">isApp</span>) &#123;</span><br><span class="line">      <span class="comment">// HTTP 请求获取品种信息</span></span><br><span class="line">      <span class="keyword">const</span> res = <span class="keyword">await</span> <span class="title function_">request</span>(<span class="string">`<span class="subst">$&#123;urlPrefix&#125;</span>/api/trade-core/coreApi/symbols/symbol/detail?symbol=<span class="subst">$&#123;symbolName&#125;</span>`</span>)</span><br><span class="line">      symbolInfo = res?.<span class="property">data</span> || &#123;&#125;</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">      <span class="comment">// APP 内获取 RN 传递的数据</span></span><br><span class="line">      symbolInfo = &#123;</span><br><span class="line">        ...(<span class="variable constant_">ENV</span>?.<span class="property">injectParams</span>?.<span class="property">symbolInfo</span> || &#123;&#125;),</span><br><span class="line">        ...(stores.<span class="property">global</span>.<span class="property">symbolInfo</span> || &#123;&#125;)</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">const</span> currentSymbol = &#123;</span><br><span class="line">      ...symbolInfo,</span><br><span class="line">      <span class="attr">precision</span>: symbolInfo?.<span class="property">symbolDecimal</span> || <span class="number">2</span>,</span><br><span class="line">      <span class="attr">description</span>: symbolInfo?.<span class="property">remark</span> || <span class="string">&#x27;&#x27;</span>,</span><br><span class="line">      <span class="attr">exchange</span>: <span class="string">&#x27;&#x27;</span>,</span><br><span class="line">      <span class="attr">session</span>: <span class="string">&#x27;24x7&#x27;</span>,</span><br><span class="line">      <span class="attr">name</span>: symbolInfo.<span class="property">symbol</span>,</span><br><span class="line">      <span class="attr">dataSourceCode</span>: symbolInfo.<span class="property">dataSourceCode</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">const</span> commonSymbolInfo = &#123;</span><br><span class="line">      <span class="attr">has_intraday</span>: <span class="literal">true</span>,</span><br><span class="line">      <span class="attr">has_daily</span>: <span class="literal">true</span>,</span><br><span class="line">      <span class="attr">has_weekly_and_monthly</span>: <span class="literal">true</span>,</span><br><span class="line">      <span class="attr">intraday_multipliers</span>: <span class="variable language_">this</span>.<span class="property">configuration</span>.<span class="property">intraday_multipliers</span>,</span><br><span class="line">      <span class="attr">supported_resolutions</span>: <span class="variable language_">this</span>.<span class="property">configuration</span>.<span class="property">supported_resolutions</span>,</span><br><span class="line">      <span class="attr">data_status</span>: <span class="string">&#x27;streaming&#x27;</span>,</span><br><span class="line">      <span class="attr">format</span>: <span class="string">&#x27;price&#x27;</span>,</span><br><span class="line">      <span class="attr">minmov</span>: <span class="number">1</span>,</span><br><span class="line">      <span class="attr">pricescale</span>: <span class="title class_">Math</span>.<span class="title function_">pow</span>(<span class="number">10</span>, currentSymbol.<span class="property">precision</span>),</span><br><span class="line">      <span class="attr">ticker</span>: currentSymbol?.<span class="property">name</span></span><br><span class="line">    &#125; <span class="keyword">as</span> <span class="title class_">LibrarySymbolInfo</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">const</span> currentSymbolInfo = &#123;</span><br><span class="line">      ...commonSymbolInfo,</span><br><span class="line">      ...currentSymbol,</span><br><span class="line">      <span class="attr">description</span>: <span class="variable language_">this</span>.<span class="property">isZh</span> ? currentSymbol.<span class="property">description</span> : currentSymbol?.<span class="property">name</span>,</span><br><span class="line">      <span class="attr">exchange</span>: <span class="variable language_">this</span>.<span class="property">isZh</span> ? currentSymbol?.<span class="property">exchange</span> : <span class="string">&#x27;&#x27;</span>,</span><br><span class="line">      <span class="attr">session</span>: <span class="string">&#x27;0000-0000|0000-0000:1234567;1&#x27;</span>,</span><br><span class="line">      <span class="attr">timezone</span>: [<span class="string">&#x27;D&#x27;</span>, <span class="string">&#x27;W&#x27;</span>, <span class="string">&#x27;M&#x27;</span>, <span class="string">&#x27;Y&#x27;</span>].<span class="title function_">some</span>(<span class="function">(<span class="params">item</span>) =&gt;</span> resolution.<span class="title function_">endsWith</span>(item))</span><br><span class="line">        ? <span class="string">&#x27;Etc/UTC&#x27;</span></span><br><span class="line">        : <span class="string">&#x27;Asia/Shanghai&#x27;</span></span><br><span class="line">    &#125; <span class="keyword">as</span> <span class="title class_">LibrarySymbolInfo</span></span><br><span class="line"></span><br><span class="line">    <span class="built_in">setTimeout</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">      <span class="title function_">onSymbolResolvedCallback</span>(currentSymbolInfo)</span><br><span class="line">    &#125;, <span class="number">0</span>)</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 搜索品种</span></span><br><span class="line">  <span class="title function_">searchSymbols</span>(<span class="params">userInput, exchange, symbolType, onResultReadyCallback</span>) &#123;</span><br><span class="line">    <span class="keyword">const</span> keyword = userInput || <span class="string">&#x27;&#x27;</span></span><br><span class="line">    <span class="keyword">const</span> resultArr = symbolInfoArr</span><br><span class="line">      .<span class="title function_">filter</span>(<span class="function">(<span class="params">item</span>) =&gt;</span> item.<span class="property">name</span>.<span class="title function_">includes</span>(keyword))</span><br><span class="line">      .<span class="title function_">map</span>(<span class="function">(<span class="params">item</span>) =&gt;</span> (&#123;</span><br><span class="line">        <span class="attr">symbol</span>: item.<span class="property">name</span>,</span><br><span class="line">        <span class="attr">name</span>: item.<span class="property">name</span>,</span><br><span class="line">        <span class="attr">full_name</span>: <span class="string">`<span class="subst">$&#123;item.name&#125;</span>`</span>,</span><br><span class="line">        <span class="attr">description</span>: <span class="variable language_">this</span>.<span class="property">isZh</span> ? item.<span class="property">description</span> : item.<span class="property">name</span>,</span><br><span class="line">        <span class="attr">exchange</span>: <span class="variable language_">this</span>.<span class="property">isZh</span> ? item.<span class="property">exchange</span> : <span class="string">&#x27;&#x27;</span>,</span><br><span class="line">        <span class="attr">type</span>: item.<span class="property">type</span>,</span><br><span class="line">        <span class="attr">ticker</span>: item.<span class="property">name</span></span><br><span class="line">      &#125;))</span><br><span class="line"></span><br><span class="line">    <span class="built_in">setTimeout</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">      <span class="title function_">onResultReadyCallback</span>(resultArr)</span><br><span class="line">    &#125;, <span class="number">0</span>)</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 获取 K 线历史数据（核心方法）</span></span><br><span class="line">  <span class="title function_">getBars</span>(<span class="params">symbolInfo, resolution, periodParams, onHistoryCallback, onErrorCallback</span>) &#123;</span><br><span class="line">    <span class="keyword">const</span> &#123; <span class="keyword">from</span>, to, firstDataRequest, countBack &#125; = periodParams</span><br><span class="line">    <span class="variable language_">this</span>.<span class="title function_">setActiveSymbolInfo</span>(&#123; symbolInfo, resolution &#125;)</span><br><span class="line">    <span class="variable language_">this</span>.<span class="title function_">getDataFeedBarCallback</span>(&#123;</span><br><span class="line">      symbolInfo,</span><br><span class="line">      resolution,</span><br><span class="line">      <span class="keyword">from</span>,</span><br><span class="line">      to,</span><br><span class="line">      countBack,</span><br><span class="line">      onHistoryCallback,</span><br><span class="line">      onErrorCallback,</span><br><span class="line">      firstDataRequest</span><br><span class="line">    &#125;)</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 订阅实时数据更新</span></span><br><span class="line">  <span class="title function_">subscribeBars</span>(<span class="params">symbolInfo, resolution, onRealtimeCallback, subscriberUID, onResetCacheNeededCallback</span>) &#123;</span><br><span class="line">    <span class="variable language_">this</span>.<span class="title function_">setActiveSymbolInfo</span>(&#123;</span><br><span class="line">      symbolInfo,</span><br><span class="line">      resolution,</span><br><span class="line">      onRealtimeCallback,</span><br><span class="line">      subscriberUID,</span><br><span class="line">      onResetCacheNeededCallback</span><br><span class="line">    &#125;)</span><br><span class="line">    mitt.<span class="title function_">on</span>(<span class="string">&#x27;symbol_change&#x27;</span>, <span class="function">() =&gt;</span> &#123;</span><br><span class="line">      <span class="title function_">onResetCacheNeededCallback</span>()</span><br><span class="line">    &#125;)</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 取消订阅</span></span><br><span class="line">  <span class="title function_">unsubscribeBars</span>(<span class="params">subscriberUID</span>) &#123;</span><br><span class="line">    <span class="variable language_">this</span>.<span class="title function_">removeActiveSymbol</span>(subscriberUID)</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="title class_">DataFeedBase</span></span><br></pre></td></tr></table></figure><h3 id="2-3-图表配置选项"><a href="#2-3-图表配置选项" class="headerlink" title="2.3 图表配置选项"></a>2.3 图表配置选项</h3><figure class="highlight typescript"><table><tr><td class="code"><pre><span class="line"><span class="comment">// src/components/Tradingview/widgetOpts.tsx</span></span><br><span class="line"><span class="keyword">import</span> ma <span class="keyword">from</span> <span class="string">&#x27;./customIndicators/ma&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="keyword">function</span> <span class="title function_">getWidgetOpts</span>(<span class="params"></span></span><br><span class="line"><span class="params">  props,</span></span><br><span class="line"><span class="params">  <span class="attr">containerRef</span>: <span class="built_in">any</span>,</span></span><br><span class="line"><span class="params">  <span class="attr">datafeedParams</span>: <span class="built_in">any</span></span></span><br><span class="line"><span class="params"></span>): <span class="title class_">ChartingLibraryWidgetOptions</span> &#123;</span><br><span class="line">  <span class="keyword">const</span> <span class="variable constant_">ENV</span> = <span class="title function_">getEnv</span>()</span><br><span class="line">  <span class="keyword">const</span> theme = props.<span class="property">theme</span></span><br><span class="line">  <span class="keyword">const</span> bgColor = theme === <span class="string">&#x27;dark&#x27;</span> ? <span class="title class_">ThemeConst</span>.<span class="property">black</span> : <span class="title class_">ThemeConst</span>.<span class="property">white</span></span><br><span class="line">  <span class="keyword">const</span> toolbar_bg = theme === <span class="string">&#x27;dark&#x27;</span> ? <span class="title class_">ThemeConst</span>.<span class="property">black</span> : <span class="string">&#x27;#fff&#x27;</span></span><br><span class="line"></span><br><span class="line">  <span class="comment">// 禁用的功能</span></span><br><span class="line">  <span class="keyword">const</span> <span class="attr">disabled_features</span>: <span class="title class_">ChartingLibraryFeatureset</span>[] = [</span><br><span class="line">    <span class="string">&#x27;header_compare&#x27;</span>,</span><br><span class="line">    <span class="string">&#x27;symbol_search_hot_key&#x27;</span>,</span><br><span class="line">    <span class="string">&#x27;study_templates&#x27;</span>,</span><br><span class="line">    <span class="string">&#x27;header_saveload&#x27;</span>,</span><br><span class="line">    <span class="string">&#x27;save_shortcut&#x27;</span>,</span><br><span class="line">    <span class="string">&#x27;header_undo_redo&#x27;</span>,</span><br><span class="line">    <span class="string">&#x27;symbol_info&#x27;</span>,</span><br><span class="line">    <span class="string">&#x27;timeframes_toolbar&#x27;</span>,</span><br><span class="line">    <span class="string">&#x27;scales_date_format&#x27;</span>,</span><br><span class="line">    <span class="string">&#x27;header_fullscreen_button&#x27;</span>,</span><br><span class="line">    <span class="string">&#x27;display_market_status&#x27;</span></span><br><span class="line">  ]</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 移动端额外禁用</span></span><br><span class="line">  <span class="keyword">if</span> (props.<span class="property">isMobile</span>) &#123;</span><br><span class="line">    disabled_features.<span class="title function_">push</span>(</span><br><span class="line">      <span class="string">&#x27;header_symbol_search&#x27;</span>,</span><br><span class="line">      <span class="string">&#x27;context_menus&#x27;</span>,</span><br><span class="line">      <span class="string">&#x27;show_chart_property_page&#x27;</span>,</span><br><span class="line">      <span class="string">&#x27;header_screenshot&#x27;</span>,</span><br><span class="line">      <span class="string">&#x27;adaptive_logo&#x27;</span>,</span><br><span class="line">      <span class="string">&#x27;left_toolbar&#x27;</span></span><br><span class="line">    )</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> <span class="attr">widgetOptions</span>: <span class="title class_">ChartingLibraryWidgetOptions</span> = &#123;</span><br><span class="line">    <span class="attr">fullscreen</span>: <span class="literal">true</span>,</span><br><span class="line">    <span class="attr">autosize</span>: <span class="literal">true</span>,</span><br><span class="line">    <span class="attr">timezone</span>: <span class="string">&#x27;exchange&#x27;</span>,</span><br><span class="line">    <span class="attr">library_path</span>: <span class="string">`<span class="subst">$&#123;ENV.isApp ? <span class="string">&#x27;.&#x27;</span> : <span class="string">&#x27;&#x27;</span>&#125;</span>/static/charting_library/`</span>,</span><br><span class="line">    <span class="attr">datafeed</span>: <span class="keyword">new</span> <span class="title class_">DataFeedBase</span>(datafeedParams),</span><br><span class="line">    <span class="attr">symbol</span>: props.<span class="property">symbol</span>,</span><br><span class="line">    <span class="attr">client_id</span>: <span class="string">&#x27;tradingview.com&#x27;</span>,</span><br><span class="line">    <span class="attr">user_id</span>: <span class="string">&#x27;public_user_id&#x27;</span>,</span><br><span class="line">    <span class="attr">locale</span>: props.<span class="property">locale</span> <span class="keyword">as</span> <span class="title class_">LanguageCode</span>,</span><br><span class="line">    <span class="attr">interval</span>: <span class="title function_">isPC</span>() ? <span class="string">&#x27;15&#x27;</span> : <span class="string">&#x27;1&#x27;</span>,</span><br><span class="line">    theme,</span><br><span class="line">    toolbar_bg,</span><br><span class="line">    <span class="attr">container</span>: containerRef,</span><br><span class="line">    <span class="attr">symbol_search_request_delay</span>: <span class="number">1000</span>,</span><br><span class="line">    <span class="attr">auto_save_delay</span>: <span class="number">5</span>,</span><br><span class="line">    <span class="attr">study_count_limit</span>: <span class="number">5</span>,</span><br><span class="line">    <span class="attr">allow_symbol_change</span>: <span class="literal">true</span>,</span><br><span class="line">    <span class="attr">overrides</span>: &#123;</span><br><span class="line">      <span class="string">&#x27;paneProperties.background&#x27;</span>: <span class="string">`<span class="subst">$&#123;bgColor&#125;</span>`</span></span><br><span class="line">    &#125;,</span><br><span class="line">    disabled_features,</span><br><span class="line">    <span class="attr">enabled_features</span>: [</span><br><span class="line">      <span class="string">&#x27;hide_resolution_in_legend&#x27;</span>,</span><br><span class="line">      <span class="string">&#x27;display_legend_on_all_charts&#x27;</span></span><br><span class="line">    ],</span><br><span class="line">    <span class="attr">custom_css_url</span>: <span class="variable constant_">ENV</span>.<span class="property">isApp</span> ? <span class="string">`./styles/index.css`</span> : <span class="string">`/static/styles/index.css`</span>,</span><br><span class="line">    <span class="attr">favorites</span>: &#123;</span><br><span class="line">      <span class="attr">intervals</span>: [<span class="string">&#x27;1&#x27;</span>, <span class="string">&#x27;5&#x27;</span>, <span class="string">&#x27;15&#x27;</span>, <span class="string">&#x27;30&#x27;</span>, <span class="string">&#x27;60&#x27;</span>]</span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="attr">custom_indicators_getter</span>: <span class="keyword">function</span> (<span class="params"><span class="title class_">PineJS</span></span>) &#123;</span><br><span class="line">      <span class="keyword">return</span> <span class="title class_">Promise</span>.<span class="title function_">resolve</span>([<span class="title function_">ma</span>(<span class="title class_">PineJS</span>)])</span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="attr">loading_screen</span>: &#123;</span><br><span class="line">      <span class="attr">backgroundColor</span>: <span class="string">&#x27;transparent&#x27;</span>,</span><br><span class="line">      <span class="attr">foregroundColor</span>: <span class="string">&#x27;transparent&#x27;</span></span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> widgetOptions</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="三、K线数据与WebSocket实时更新"><a href="#三、K线数据与WebSocket实时更新" class="headerlink" title="三、K线数据与WebSocket实时更新"></a>三、K线数据与WebSocket实时更新</h2><h3 id="3-1-WebSocket-Store-实现"><a href="#3-1-WebSocket-Store-实现" class="headerlink" title="3.1 WebSocket Store 实现"></a>3.1 WebSocket Store 实现</h3><figure class="highlight typescript"><table><tr><td class="code"><pre><span class="line"><span class="comment">// src/stores/ws.ts</span></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">WsStore</span> &#123;</span><br><span class="line">  tvWidget = <span class="literal">null</span></span><br><span class="line">  <span class="meta">@observable</span> lastbar = &#123;&#125;</span><br><span class="line">  <span class="meta">@observable</span> activeSymbolInfo = &#123;&#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// HTTP 获取历史 K 线数据</span></span><br><span class="line">  getHttpHistoryBars = <span class="title function_">async</span> (symbolInfo, resolution, <span class="keyword">from</span>, to, countBack, firstDataRequest) =&gt; &#123;</span><br><span class="line">    <span class="keyword">const</span> klineType = &#123;</span><br><span class="line">      <span class="number">1</span>: <span class="string">&#x27;1min&#x27;</span>,</span><br><span class="line">      <span class="number">5</span>: <span class="string">&#x27;5min&#x27;</span>,</span><br><span class="line">      <span class="number">15</span>: <span class="string">&#x27;15min&#x27;</span>,</span><br><span class="line">      <span class="number">30</span>: <span class="string">&#x27;30min&#x27;</span>,</span><br><span class="line">      <span class="number">60</span>: <span class="string">&#x27;60min&#x27;</span>,</span><br><span class="line">      <span class="number">240</span>: <span class="string">&#x27;4hour&#x27;</span>,</span><br><span class="line">      <span class="string">&#x27;1D&#x27;</span>: <span class="string">&#x27;1day&#x27;</span>,</span><br><span class="line">      <span class="string">&#x27;1W&#x27;</span>: <span class="string">&#x27;1week&#x27;</span>,</span><br><span class="line">      <span class="string">&#x27;1M&#x27;</span>: <span class="string">&#x27;1mon&#x27;</span></span><br><span class="line">    &#125;[resolution] || <span class="string">&#x27;1min&#x27;</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">const</span> res = <span class="keyword">await</span> request.<span class="title function_">get</span>(<span class="string">`<span class="subst">$&#123;url&#125;</span>/api/trade-market/marketApi/kline/symbol/klineList`</span>, &#123;</span><br><span class="line">      <span class="attr">params</span>: &#123;</span><br><span class="line">        <span class="attr">symbol</span>: symbolInfo.<span class="property">symbol</span>,</span><br><span class="line">        <span class="attr">first</span>: firstDataRequest,</span><br><span class="line">        <span class="attr">current</span>: <span class="number">1</span>,</span><br><span class="line">        <span class="attr">size</span>: <span class="variable language_">document</span>.<span class="property">documentElement</span>.<span class="property">clientWidth</span> &gt;= <span class="number">1200</span> ? <span class="number">500</span> : <span class="number">200</span>,</span><br><span class="line">        klineType,</span><br><span class="line">        <span class="attr">klineTime</span>: to * <span class="number">1000</span></span><br><span class="line">      &#125;</span><br><span class="line">    &#125;)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">const</span> list = res?.<span class="property">data</span> || []</span><br><span class="line">    <span class="keyword">return</span> list.<span class="title function_">map</span>(<span class="function">(<span class="params">item</span>) =&gt;</span> &#123;</span><br><span class="line">      <span class="keyword">const</span> [klineTime, open, high, low, close] = (item || <span class="string">&#x27;&#x27;</span>).<span class="title function_">split</span>(<span class="string">&#x27;,&#x27;</span>)</span><br><span class="line">      <span class="keyword">return</span> &#123;</span><br><span class="line">        <span class="attr">open</span>: <span class="title class_">Number</span>(open),</span><br><span class="line">        <span class="attr">close</span>: <span class="title class_">Number</span>(close),</span><br><span class="line">        <span class="attr">high</span>: <span class="title class_">Number</span>(high),</span><br><span class="line">        <span class="attr">low</span>: <span class="title class_">Number</span>(low),</span><br><span class="line">        <span class="attr">time</span>: resolution.<span class="title function_">includes</span>(<span class="string">&#x27;M&#x27;</span>)</span><br><span class="line">          ? <span class="title class_">Number</span>(klineTime) + <span class="number">8</span> * <span class="number">60</span> * <span class="number">60</span> * <span class="number">1000</span></span><br><span class="line">          : <span class="title class_">Number</span>(klineTime)</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;).<span class="title function_">reverse</span>()</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 更新最后一条 K 线</span></span><br><span class="line">  updateBar = <span class="function">(<span class="params">socketData, currentSymbol</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">const</span> precision = currentSymbol.<span class="property">precision</span></span><br><span class="line">    <span class="keyword">const</span> lastBar = <span class="variable language_">this</span>.<span class="property">lastbar</span></span><br><span class="line">    <span class="keyword">const</span> resolution = currentSymbol.<span class="property">resolution</span></span><br><span class="line">    <span class="keyword">const</span> serverTime = socketData?.<span class="property">priceData</span>?.<span class="property">id</span> / <span class="number">1000</span></span><br><span class="line">    <span class="keyword">const</span> bid = socketData?.<span class="property">priceData</span>?.<span class="property">buy</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">let</span> rounded = serverTime</span><br><span class="line">    <span class="keyword">if</span> (!<span class="built_in">isNaN</span>(resolution) || resolution.<span class="title function_">includes</span>(<span class="string">&#x27;D&#x27;</span>)) &#123;</span><br><span class="line">      <span class="keyword">const</span> coeff = (resolution.<span class="title function_">includes</span>(<span class="string">&#x27;D&#x27;</span>) ? <span class="number">1440</span> : <span class="title class_">Number</span>(resolution)) * <span class="number">60</span></span><br><span class="line">      rounded = <span class="title class_">Math</span>.<span class="title function_">floor</span>(serverTime / coeff) * coeff</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">const</span> lastBarSec = lastBar?.<span class="property">time</span> / <span class="number">1000</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (rounded &gt; lastBarSec) &#123;</span><br><span class="line">      <span class="comment">// 新建 K 线</span></span><br><span class="line">      <span class="keyword">return</span> &#123;</span><br><span class="line">        <span class="attr">time</span>: rounded * <span class="number">1000</span>,</span><br><span class="line">        <span class="attr">open</span>: <span class="title class_">Number</span>(bid),</span><br><span class="line">        <span class="attr">high</span>: <span class="title class_">Number</span>(bid),</span><br><span class="line">        <span class="attr">low</span>: <span class="title class_">Number</span>(bid),</span><br><span class="line">        <span class="attr">close</span>: <span class="title class_">Number</span>(bid)</span><br><span class="line">      &#125;</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">      <span class="comment">// 更新当前 K 线</span></span><br><span class="line">      <span class="keyword">return</span> &#123;</span><br><span class="line">        <span class="attr">time</span>: lastBar.<span class="property">time</span>,</span><br><span class="line">        <span class="attr">open</span>: lastBar.<span class="property">open</span>,</span><br><span class="line">        <span class="attr">high</span>: <span class="title class_">Math</span>.<span class="title function_">max</span>(lastBar.<span class="property">high</span>, <span class="title class_">Number</span>(bid)),</span><br><span class="line">        <span class="attr">low</span>: <span class="title class_">Math</span>.<span class="title function_">min</span>(lastBar.<span class="property">low</span>, <span class="title class_">Number</span>(bid)),</span><br><span class="line">        <span class="attr">close</span>: <span class="title class_">Number</span>(bid)</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 处理 WebSocket 消息</span></span><br><span class="line">  <span class="meta">@action</span></span><br><span class="line">  <span class="title function_">message</span>(<span class="params">res</span>) &#123;</span><br><span class="line">    <span class="keyword">if</span> (res?.<span class="property">header</span>?.<span class="property">msgId</span> === <span class="string">&#x27;symbol&#x27;</span>) &#123;</span><br><span class="line">      <span class="keyword">const</span> quoteBody = <span class="variable language_">this</span>.<span class="title function_">parseQuoteBodyData</span>(res?.<span class="property">body</span>)</span><br><span class="line">      <span class="keyword">if</span> (quoteBody?.<span class="property">symbol</span> === <span class="variable language_">this</span>.<span class="property">activeSymbolInfo</span>?.<span class="property">symbolInfo</span>?.<span class="property">name</span>) &#123;</span><br><span class="line">        <span class="keyword">const</span> newLastBar = <span class="variable language_">this</span>.<span class="title function_">updateBar</span>(quoteBody, &#123;</span><br><span class="line">          <span class="attr">resolution</span>: <span class="variable language_">this</span>.<span class="property">activeSymbolInfo</span>.<span class="property">resolution</span>,</span><br><span class="line">          <span class="attr">precision</span>: <span class="variable language_">this</span>.<span class="property">activeSymbolInfo</span>.<span class="property">symbolInfo</span>.<span class="property">precision</span>,</span><br><span class="line">          <span class="attr">symbolInfo</span>: <span class="variable language_">this</span>.<span class="property">activeSymbolInfo</span>.<span class="property">symbolInfo</span></span><br><span class="line">        &#125;)</span><br><span class="line">        <span class="keyword">if</span> (newLastBar) &#123;</span><br><span class="line">          <span class="variable language_">this</span>.<span class="property">activeSymbolInfo</span>.<span class="property">onRealtimeCallback</span>?.(newLastBar)</span><br><span class="line">          <span class="variable language_">this</span>.<span class="property">lastbar</span> = newLastBar</span><br><span class="line">        &#125;</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// Datafeed 回调</span></span><br><span class="line">  <span class="meta">@action</span></span><br><span class="line">  getDataFeedBarCallback = <span class="function">(<span class="params">obj = &#123;&#125;</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">const</span> &#123; symbolInfo, resolution, firstDataRequest, <span class="keyword">from</span>, to, countBack, onHistoryCallback &#125; = obj</span><br><span class="line">    <span class="variable language_">this</span>.<span class="title function_">getHttpHistoryBars</span>(symbolInfo, resolution, <span class="keyword">from</span>, to, countBack, firstDataRequest)</span><br><span class="line">      .<span class="title function_">then</span>(<span class="function">(<span class="params">bars</span>) =&gt;</span> &#123;</span><br><span class="line">        <span class="keyword">if</span> (bars?.<span class="property">length</span>) &#123;</span><br><span class="line">          <span class="title function_">onHistoryCallback</span>(bars, &#123; <span class="attr">noData</span>: <span class="literal">false</span> &#125;)</span><br><span class="line">          <span class="variable language_">this</span>.<span class="property">lastbar</span> = bars.<span class="title function_">at</span>(-<span class="number">1</span>)</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">          <span class="title function_">onHistoryCallback</span>(bars, &#123; <span class="attr">noData</span>: <span class="literal">true</span> &#125;)</span><br><span class="line">        &#125;</span><br><span class="line">      &#125;)</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> wsStore</span><br></pre></td></tr></table></figure><h2 id="四、自定义指标开发"><a href="#四、自定义指标开发" class="headerlink" title="四、自定义指标开发"></a>四、自定义指标开发</h2><h3 id="4-1-自定义-MA-指标示例"><a href="#4-1-自定义-MA-指标示例" class="headerlink" title="4.1 自定义 MA 指标示例"></a>4.1 自定义 MA 指标示例</h3><figure class="highlight typescript"><table><tr><td class="code"><pre><span class="line"><span class="comment">// src/components/Tradingview/customIndicators/ma.ts</span></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">customerMovingAverage</span> = (<span class="params"><span class="title class_">PineJS</span>: <span class="title class_">PineJS</span></span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> <span class="attr">indicators</span>: <span class="title class_">CustomIndicator</span> = &#123;</span><br><span class="line">    <span class="attr">name</span>: <span class="string">&#x27;Customer Moving Average&#x27;</span>,</span><br><span class="line">    <span class="attr">metainfo</span>: &#123;</span><br><span class="line">      <span class="attr">_metainfoVersion</span>: <span class="number">51</span>,</span><br><span class="line">      <span class="attr">id</span>: <span class="string">&#x27;Customer Moving Average@tv-basicstudies-1&#x27;</span>,</span><br><span class="line">      <span class="attr">name</span>: <span class="string">&#x27;Customer Moving Average&#x27;</span>,</span><br><span class="line">      <span class="attr">description</span>: <span class="string">&#x27;Customer Moving Average&#x27;</span>,</span><br><span class="line">      <span class="attr">shortDescription</span>: <span class="string">&#x27;MA&#x27;</span>,</span><br><span class="line">      <span class="attr">is_price_study</span>: <span class="literal">true</span>,</span><br><span class="line">      <span class="attr">isCustomIndicator</span>: <span class="literal">true</span>,</span><br><span class="line">      <span class="attr">format</span>: &#123; <span class="attr">type</span>: <span class="string">&#x27;price&#x27;</span> &#125;,</span><br><span class="line">      <span class="attr">defaults</span>: &#123;</span><br><span class="line">        <span class="attr">styles</span>: &#123;</span><br><span class="line">          <span class="attr">plot_0</span>: &#123; <span class="attr">linestyle</span>: <span class="number">0</span>, <span class="attr">linewidth</span>: <span class="number">1</span>, <span class="attr">plottype</span>: <span class="number">0</span>, <span class="attr">trackPrice</span>: <span class="literal">false</span>, <span class="attr">transparency</span>: <span class="number">35</span>, <span class="attr">visible</span>: <span class="literal">true</span>, <span class="attr">color</span>: <span class="string">&#x27;#FF0000&#x27;</span> &#125;,</span><br><span class="line">          <span class="attr">plot_1</span>: &#123; <span class="attr">linestyle</span>: <span class="number">0</span>, <span class="attr">linewidth</span>: <span class="number">1</span>, <span class="attr">plottype</span>: <span class="number">0</span>, <span class="attr">trackPrice</span>: <span class="literal">false</span>, <span class="attr">transparency</span>: <span class="number">35</span>, <span class="attr">visible</span>: <span class="literal">true</span>, <span class="attr">color</span>: <span class="string">&#x27;#00FF00&#x27;</span> &#125;,</span><br><span class="line">          <span class="attr">plot_2</span>: &#123; <span class="attr">linestyle</span>: <span class="number">0</span>, <span class="attr">linewidth</span>: <span class="number">1</span>, <span class="attr">plottype</span>: <span class="number">0</span>, <span class="attr">trackPrice</span>: <span class="literal">false</span>, <span class="attr">transparency</span>: <span class="number">35</span>, <span class="attr">visible</span>: <span class="literal">true</span>, <span class="attr">color</span>: <span class="string">&#x27;#00FFFF&#x27;</span> &#125;</span><br><span class="line">        &#125;,</span><br><span class="line">        <span class="attr">inputs</span>: &#123; <span class="attr">in_0</span>: <span class="number">5</span>, <span class="attr">in_1</span>: <span class="number">10</span>, <span class="attr">in_2</span>: <span class="number">30</span> &#125;,</span><br><span class="line">        <span class="attr">precision</span>: <span class="number">4</span></span><br><span class="line">      &#125;,</span><br><span class="line">      <span class="attr">plots</span>: [</span><br><span class="line">        &#123; <span class="attr">id</span>: <span class="string">&#x27;plot_0&#x27;</span>, <span class="attr">type</span>: <span class="string">&#x27;line&#x27;</span> &#125;,</span><br><span class="line">        &#123; <span class="attr">id</span>: <span class="string">&#x27;plot_1&#x27;</span>, <span class="attr">type</span>: <span class="string">&#x27;line&#x27;</span> &#125;,</span><br><span class="line">        &#123; <span class="attr">id</span>: <span class="string">&#x27;plot_2&#x27;</span>, <span class="attr">type</span>: <span class="string">&#x27;line&#x27;</span> &#125;</span><br><span class="line">      ],</span><br><span class="line">      <span class="attr">inputs</span>: [</span><br><span class="line">        &#123; <span class="attr">id</span>: <span class="string">&#x27;in_0&#x27;</span>, <span class="attr">name</span>: <span class="string">&#x27;Length&#x27;</span>, <span class="attr">defval</span>: <span class="number">9</span>, <span class="attr">type</span>: <span class="string">&#x27;integer&#x27;</span>, <span class="attr">min</span>: <span class="number">1</span>, <span class="attr">max</span>: <span class="number">1e4</span> &#125;,</span><br><span class="line">        &#123; <span class="attr">id</span>: <span class="string">&#x27;in_1&#x27;</span>, <span class="attr">name</span>: <span class="string">&#x27;Length1&#x27;</span>, <span class="attr">defval</span>: <span class="number">10</span>, <span class="attr">type</span>: <span class="string">&#x27;integer&#x27;</span>, <span class="attr">min</span>: <span class="number">1</span>, <span class="attr">max</span>: <span class="number">1e4</span> &#125;,</span><br><span class="line">        &#123; <span class="attr">id</span>: <span class="string">&#x27;in_2&#x27;</span>, <span class="attr">name</span>: <span class="string">&#x27;Length2&#x27;</span>, <span class="attr">defval</span>: <span class="number">30</span>, <span class="attr">type</span>: <span class="string">&#x27;integer&#x27;</span>, <span class="attr">min</span>: <span class="number">1</span>, <span class="attr">max</span>: <span class="number">1e4</span> &#125;</span><br><span class="line">      ]</span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="attr">constructor</span>: <span class="keyword">function</span> (<span class="params"><span class="attr">this</span>: <span class="title class_">LibraryPineStudy</span>&lt;<span class="title class_">IPineStudyResult</span>&gt;</span>) &#123;</span><br><span class="line">      <span class="variable language_">this</span>.<span class="property">main</span> = <span class="keyword">function</span> (<span class="params">context, inputCallback</span>) &#123;</span><br><span class="line">        <span class="keyword">const</span> close = <span class="title class_">PineJS</span>.<span class="property">Std</span>.<span class="title function_">close</span>(context)</span><br><span class="line">        <span class="keyword">const</span> len1 = <span class="title function_">inputCallback</span>(<span class="number">0</span>)</span><br><span class="line">        <span class="keyword">const</span> len2 = <span class="title function_">inputCallback</span>(<span class="number">1</span>)</span><br><span class="line">        <span class="keyword">const</span> len3 = <span class="title function_">inputCallback</span>(<span class="number">2</span>)</span><br><span class="line"></span><br><span class="line">        <span class="keyword">const</span> value1 = <span class="title class_">PineJS</span>.<span class="property">Std</span>.<span class="title function_">sma</span>(close, len1, context)</span><br><span class="line">        <span class="keyword">const</span> value2 = <span class="title class_">PineJS</span>.<span class="property">Std</span>.<span class="title function_">sma</span>(close, len2, context)</span><br><span class="line">        <span class="keyword">const</span> value3 = <span class="title class_">PineJS</span>.<span class="property">Std</span>.<span class="title function_">sma</span>(close, len3, context)</span><br><span class="line"></span><br><span class="line">        <span class="keyword">return</span> [</span><br><span class="line">          &#123; <span class="attr">value</span>: value1, <span class="attr">offset</span>: <span class="number">0</span> &#125;,</span><br><span class="line">          &#123; <span class="attr">value</span>: value2, <span class="attr">offset</span>: <span class="number">0</span> &#125;,</span><br><span class="line">          &#123; <span class="attr">value</span>: value3, <span class="attr">offset</span>: <span class="number">0</span> &#125;</span><br><span class="line">        ]</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">return</span> indicators</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> customerMovingAverage</span><br></pre></td></tr></table></figure><h2 id="五、主题与样式定制"><a href="#五、主题与样式定制" class="headerlink" title="五、主题与样式定制"></a>五、主题与样式定制</h2><h3 id="5-1-主题配置"><a href="#5-1-主题配置" class="headerlink" title="5.1 主题配置"></a>5.1 主题配置</h3><figure class="highlight typescript"><table><tr><td class="code"><pre><span class="line"><span class="comment">// src/components/Tradingview/theme.ts</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> <span class="title function_">getTradingviewThemeCssVar</span> = (<span class="params"><span class="attr">theme</span>: <span class="title class_">ThemeName</span></span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> primary = <span class="title class_">ThemeConst</span>.<span class="property">primary</span></span><br><span class="line">  <span class="keyword">const</span> textPrimary = <span class="title class_">ThemeConst</span>.<span class="property">textPrimary</span></span><br><span class="line">  <span class="keyword">const</span> isDark = theme === <span class="string">&#x27;dark&#x27;</span></span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> &#123;</span><br><span class="line">    <span class="string">&#x27;--tv-color-toolbar-button-text&#x27;</span>: <span class="string">&#x27;#7B7E80&#x27;</span>,</span><br><span class="line">    <span class="string">&#x27;--tv-color-toolbar-button-text-active&#x27;</span>: textPrimary,</span><br><span class="line">    <span class="string">&#x27;--tv-color-toolbar-button-text-active-hover&#x27;</span>: textPrimary,</span><br><span class="line">    <span class="string">&#x27;--tv-color-toolbar-toggle-button-background-active&#x27;</span>: primary,</span><br><span class="line">    <span class="string">&#x27;--tv-color-toolbar-toggle-button-background-active-hover&#x27;</span>: primary,</span><br><span class="line">    <span class="string">&#x27;--tv-color-popup-element-text-active&#x27;</span>: <span class="string">&#x27;#131722&#x27;</span>,</span><br><span class="line">    <span class="string">&#x27;--tv-color-popup-element-background-active&#x27;</span>: <span class="string">&#x27;#f0f3fa&#x27;</span>,</span><br><span class="line">    ...(isDark ? &#123; <span class="string">&#x27;--tv-color-pane-background&#x27;</span>: <span class="title class_">ThemeConst</span>.<span class="property">black</span> &#125; : &#123;&#125;)</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="5-2-K线颜色与涨跌色设置"><a href="#5-2-K线颜色与涨跌色设置" class="headerlink" title="5.2 K线颜色与涨跌色设置"></a>5.2 K线颜色与涨跌色设置</h3><figure class="highlight typescript"><table><tr><td class="code"><pre><span class="line"><span class="comment">// src/components/Tradingview/widgetMethods.ts</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">type</span> <span class="title class_">ColorType</span> = <span class="number">1</span> | <span class="number">2</span> <span class="comment">// 1绿涨红跌 2红涨绿跌</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">function</span> <span class="title function_">setChartStyleProperties</span>(<span class="params"><span class="attr">props</span>: &#123; colorType: ColorType; tvWidget: IChartingLibraryWidget &#125;</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> &#123; colorType, tvWidget &#125; = props</span><br><span class="line">  <span class="keyword">const</span> red = <span class="title class_">ThemeConst</span>.<span class="property">red</span>   <span class="comment">// #C54747</span></span><br><span class="line">  <span class="keyword">const</span> green = <span class="title class_">ThemeConst</span>.<span class="property">green</span> <span class="comment">// #45A48A</span></span><br><span class="line"></span><br><span class="line">  <span class="keyword">let</span> upColor = <span class="title class_">Number</span>(colorType) === <span class="number">2</span> ? red : green</span><br><span class="line">  <span class="keyword">let</span> downColor = <span class="title class_">Number</span>(colorType) === <span class="number">2</span> ? green : red</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 蜡烛图样式</span></span><br><span class="line">  tvWidget.<span class="title function_">chart</span>().<span class="title function_">getSeries</span>().<span class="title function_">setChartStyleProperties</span>(<span class="number">1</span>, &#123;</span><br><span class="line">    upColor,</span><br><span class="line">    downColor,</span><br><span class="line">    <span class="attr">wickUpColor</span>: upColor,</span><br><span class="line">    <span class="attr">wickDownColor</span>: downColor,</span><br><span class="line">    <span class="attr">borderUpColor</span>: upColor,</span><br><span class="line">    <span class="attr">borderDownColor</span>: downColor</span><br><span class="line">  &#125;)</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 空心蜡烛图样式</span></span><br><span class="line">  tvWidget.<span class="title function_">chart</span>().<span class="title function_">getSeries</span>().<span class="title function_">setChartStyleProperties</span>(<span class="number">9</span>, &#123;</span><br><span class="line">    upColor,</span><br><span class="line">    downColor,</span><br><span class="line">    <span class="attr">wickUpColor</span>: upColor,</span><br><span class="line">    <span class="attr">wickDownColor</span>: downColor,</span><br><span class="line">    <span class="attr">borderUpColor</span>: upColor,</span><br><span class="line">    <span class="attr">borderDownColor</span>: downColor</span><br><span class="line">  &#125;)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="六、性能优化策略"><a href="#六、性能优化策略" class="headerlink" title="六、性能优化策略"></a>六、性能优化策略</h2><h3 id="6-1-数据加载优化"><a href="#6-1-数据加载优化" class="headerlink" title="6.1 数据加载优化"></a>6.1 数据加载优化</h3><figure class="highlight typescript"><table><tr><td class="code"><pre><span class="line"><span class="comment">// 1. 按需加载历史数据</span></span><br><span class="line">getHttpHistoryBars = <span class="title function_">async</span> (symbolInfo, resolution, <span class="keyword">from</span>, to, countBack, firstDataRequest) =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> size = <span class="variable language_">document</span>.<span class="property">documentElement</span>.<span class="property">clientWidth</span> &gt;= <span class="number">1200</span> ? <span class="number">500</span> : <span class="number">200</span></span><br><span class="line">  <span class="comment">// 根据屏幕宽度调整加载数量，移动端减少请求数据量</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 2. 数据缓存策略</span></span><br><span class="line"><span class="meta">@action</span></span><br><span class="line">getDataFeedBarCallback = <span class="function">(<span class="params">obj = &#123;&#125;</span>) =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">const</span> &#123; firstDataRequest &#125; = obj</span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (firstDataRequest) &#123;</span><br><span class="line">    <span class="comment">// 首次请求完整数据</span></span><br><span class="line">    <span class="variable language_">this</span>.<span class="title function_">getHttpHistoryBars</span>(symbolInfo, resolution, <span class="keyword">from</span>, to, countBack, <span class="literal">true</span>)</span><br><span class="line">  &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    <span class="comment">// 后续请求只获取增量数据</span></span><br><span class="line">    <span class="variable language_">this</span>.<span class="title function_">getHttpHistoryBars</span>(symbolInfo, resolution, <span class="keyword">from</span>, <span class="variable language_">this</span>.<span class="property">lastBarTime</span>, countBack, <span class="literal">false</span>)</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="6-2-WebSocket-连接优化"><a href="#6-2-WebSocket-连接优化" class="headerlink" title="6.2 WebSocket 连接优化"></a>6.2 WebSocket 连接优化</h3><figure class="highlight typescript"><table><tr><td class="code"><pre><span class="line"><span class="comment">// 使用 reconnecting-websocket 实现自动重连</span></span><br><span class="line"><span class="variable language_">this</span>.<span class="property">socket</span> = <span class="keyword">new</span> <span class="title class_">ReconnectingWebSocket</span>(wsUrl, [<span class="string">&#x27;WebSocket&#x27;</span>, token], &#123;</span><br><span class="line">  <span class="attr">minReconnectionDelay</span>: <span class="number">1</span>,</span><br><span class="line">  <span class="attr">connectionTimeout</span>: <span class="number">3000</span>,</span><br><span class="line">  <span class="attr">maxEnqueuedMessages</span>: <span class="number">0</span>,</span><br><span class="line">  <span class="attr">maxRetries</span>: <span class="number">10000</span></span><br><span class="line">&#125;)</span><br><span class="line"></span><br><span class="line"><span class="comment">// 心跳保活</span></span><br><span class="line"><span class="title function_">startHeartbeat</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="variable language_">this</span>.<span class="property">heartbeatInterval</span> = <span class="built_in">setInterval</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">    <span class="variable language_">this</span>.<span class="title function_">send</span>(&#123;&#125;, &#123; <span class="attr">msgId</span>: <span class="string">&#x27;heartbeat&#x27;</span> &#125;)</span><br><span class="line">  &#125;, <span class="number">20000</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="6-3-图表渲染优化"><a href="#6-3-图表渲染优化" class="headerlink" title="6.3 图表渲染优化"></a>6.3 图表渲染优化</h3><figure class="highlight typescript"><table><tr><td class="code"><pre><span class="line"><span class="comment">// 1. 使用 loading 状态避免闪烁</span></span><br><span class="line"><span class="keyword">const</span> [loading, setLoading] = <span class="title function_">useState</span>(<span class="literal">true</span>)</span><br><span class="line"><span class="built_in">setTimeout</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">  <span class="title function_">setLoading</span>(<span class="literal">false</span>)</span><br><span class="line">&#125;, <span class="number">200</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment">// 2. 延迟初始化避免阻塞</span></span><br><span class="line"><span class="title function_">useEffect</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">  <span class="comment">// 延迟加载图表</span></span><br><span class="line">  <span class="built_in">setTimeout</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">const</span> tvWidget = <span class="keyword">new</span> <span class="title function_">widget</span>(widgetOptions)</span><br><span class="line">  &#125;, <span class="number">100</span>)</span><br><span class="line">&#125;, [])</span><br><span class="line"></span><br><span class="line"><span class="comment">// 3. 缓存主题配置</span></span><br><span class="line"><span class="keyword">const</span> defaultBgColor = theme === <span class="string">&#x27;dark&#x27;</span> ? <span class="title class_">ThemeConst</span>.<span class="property">black</span> : <span class="title class_">ThemeConst</span>.<span class="property">white</span></span><br><span class="line"><span class="keyword">if</span> (theme &amp;&amp; defaultBgColor !== <span class="title function_">STORAGE_GET_CHART_PROPS</span>(<span class="string">&#x27;paneProperties.background&#x27;</span>)) &#123;</span><br><span class="line">  <span class="title function_">STORAGE_REMOVE_CHART_PROPS</span>()</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="6-4-内存管理与清理"><a href="#6-4-内存管理与清理" class="headerlink" title="6.4 内存管理与清理"></a>6.4 内存管理与清理</h3><figure class="highlight typescript"><table><tr><td class="code"><pre><span class="line"><span class="title function_">useEffect</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="function">() =&gt;</span> &#123;</span><br><span class="line">    <span class="comment">// 组件卸载时清理</span></span><br><span class="line">    tvWidget.<span class="title function_">remove</span>()  <span class="comment">// 销毁图表实例</span></span><br><span class="line">    mitt.<span class="title function_">off</span>(<span class="string">&#x27;symbol_change&#x27;</span>)  <span class="comment">// 取消事件订阅</span></span><br><span class="line">    <span class="variable language_">this</span>.<span class="title function_">stopHeartbeat</span>()  <span class="comment">// 停止心跳</span></span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">socket</span>?.<span class="title function_">close</span>()  <span class="comment">// 关闭 WebSocket</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;, [])</span><br></pre></td></tr></table></figure><h2 id="七、常见问题与解决方案"><a href="#七、常见问题与解决方案" class="headerlink" title="七、常见问题与解决方案"></a>七、常见问题与解决方案</h2><h3 id="7-1-主题切换不生效"><a href="#7-1-主题切换不生效" class="headerlink" title="7.1 主题切换不生效"></a>7.1 主题切换不生效</h3><figure class="highlight typescript"><table><tr><td class="code"><pre><span class="line"><span class="comment">// 问题：切换主题后图表颜色不变</span></span><br><span class="line"><span class="comment">// 解决：清除本地缓存 + 动态调用 changeTheme</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 1. 切换主题时清除缓存</span></span><br><span class="line"><span class="title function_">STORAGE_REMOVE_CHART_PROPS</span>()</span><br><span class="line"></span><br><span class="line"><span class="comment">// 2. 动态切换主题</span></span><br><span class="line">tvWidget.<span class="title function_">changeTheme</span>(theme)</span><br><span class="line"></span><br><span class="line"><span class="comment">// 3. 设置 CSS 变量</span></span><br><span class="line"><span class="title function_">setCSSCustomProperty</span>(&#123; tvWidget, theme &#125;)</span><br></pre></td></tr></table></figure><h3 id="7-2-数据请求重复"><a href="#7-2-数据请求重复" class="headerlink" title="7.2 数据请求重复"></a>7.2 数据请求重复</h3><figure class="highlight typescript"><table><tr><td class="code"><pre><span class="line"><span class="comment">// 问题：多次调用 getBars</span></span><br><span class="line"><span class="comment">// 解决：使用 lastBarTime 缓存截止时间</span></span><br><span class="line"></span><br><span class="line"><span class="variable language_">this</span>.<span class="property">lastBarTime</span> = bars[<span class="number">0</span>]?.<span class="property">time</span> / <span class="number">1000</span></span><br><span class="line"><span class="keyword">if</span> (<span class="variable language_">this</span>.<span class="property">lastBarTime</span> === bars[<span class="number">0</span>]?.<span class="property">time</span> / <span class="number">1000</span>) &#123;</span><br><span class="line">  <span class="variable language_">this</span>.<span class="property">datafeedBarCallbackObj</span>.<span class="title function_">onHistoryCallback</span>([], &#123; <span class="attr">noData</span>: <span class="literal">true</span> &#125;)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="7-3-移动端适配"><a href="#7-3-移动端适配" class="headerlink" title="7.3 移动端适配"></a>7.3 移动端适配</h3><figure class="highlight typescript"><table><tr><td class="code"><pre><span class="line"><span class="comment">// 移动端禁用多余功能</span></span><br><span class="line"><span class="keyword">if</span> (props.<span class="property">isMobile</span>) &#123;</span><br><span class="line">  disabled_features.<span class="title function_">push</span>(</span><br><span class="line">    <span class="string">&#x27;header_symbol_search&#x27;</span>,</span><br><span class="line">    <span class="string">&#x27;context_menus&#x27;</span>,</span><br><span class="line">    <span class="string">&#x27;show_chart_property_page&#x27;</span>,</span><br><span class="line">    <span class="string">&#x27;header_screenshot&#x27;</span>,</span><br><span class="line">    <span class="string">&#x27;left_toolbar&#x27;</span></span><br><span class="line">  )</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 禁止双指缩放</span></span><br><span class="line"><span class="variable language_">document</span>.<span class="property">body</span>.<span class="title function_">addEventListener</span>(<span class="string">&#x27;touchstart&#x27;</span>, <span class="function">(<span class="params">e</span>) =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">if</span> (e.<span class="property">touches</span>.<span class="property">length</span> &gt; <span class="number">1</span>) &#123;</span><br><span class="line">    e.<span class="title function_">preventDefault</span>()</span><br><span class="line">  &#125;</span><br><span class="line">&#125;, &#123; <span class="attr">passive</span>: <span class="literal">false</span> &#125;)</span><br></pre></td></tr></table></figure><h2 id="八、完整调用示例"><a href="#八、完整调用示例" class="headerlink" title="八、完整调用示例"></a>八、完整调用示例</h2><figure class="highlight tsx"><table><tr><td class="code"><pre><span class="line"><span class="comment">// src/pages/index.tsx</span></span><br><span class="line"><span class="keyword">import</span> <span class="title class_">Tradingview</span> <span class="keyword">from</span> <span class="string">&#x27;@/components/Tradingview&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="keyword">function</span> <span class="title function_">ChartPage</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">Tradingview</span> /&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line">  )</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>URL 参数说明：</p><ul><li><code>symbolName</code>: 交易品种，如 BTCUSDT</li><li><code>theme</code>: 主题，light 或 dark</li><li><code>locale</code>: 语言，如 en、zh_TW</li><li><code>colorType</code>: 涨跌颜色，1 绿涨红跌，2 红涨绿跌</li><li><code>chartType</code>: 图表类型，1 蜡烛图、2 折线图等</li></ul><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>本文详细讲解了 Next.js 项目中接入 TradingView 图表的完整方案，涵盖了：</p><ol><li><strong>环境配置</strong>：类型定义、目录结构</li><li><strong>核心实现</strong>：主组件、Datafeed、配置选项</li><li><strong>数据交互</strong>：HTTP 历史数据 + WebSocket 实时更新</li><li><strong>自定义开发</strong>：自定义指标、主题定制</li><li><strong>性能优化</strong>：数据加载、WebSocket、渲染优化、内存管理</li><li><strong>常见问题</strong>：主题切换、数据重复、移动端适配</li></ol><p>通过以上方案，可以在 Next.js 项目中快速构建专业的金融图表应用。如需更高级的功能（如图表保存加载、自定义交易品种等），可以参考 <a href="https://www.tradingview.com/charting-library-docs/latest/getting_started/">TradingView 官方文档</a>。</p>]]>
    </content>
    <id>http://fe.poetries.top/2026/02/09/nextjs-tradingview-integration/</id>
    <link href="http://fe.poetries.top/2026/02/09/nextjs-tradingview-integration/"/>
    <published>2026-02-09T09:30:00.000Z</published>
    <summary>详细讲解如何在Next.js项目中接入TradingView Charts，包括环境配置、数据馈送实现、自定义指标、主题定制、性能优化等完整流程。</summary>
    <title>在Next.js中接入TradingView图表实践总结</title>
    <updated>2026-03-08T10:22:42.089Z</updated>
  </entry>
  <entry>
    <author>
      <name>Poetry</name>
    </author>
    <category term="DevOps" scheme="http://fe.poetries.top/categories/DevOps/"/>
    <category term="Docker" scheme="http://fe.poetries.top/tags/Docker/"/>
    <category term="GitHub Actions" scheme="http://fe.poetries.top/tags/GitHub-Actions/"/>
    <category term="腾讯云" scheme="http://fe.poetries.top/tags/%E8%85%BE%E8%AE%AF%E4%BA%91/"/>
    <category term="CI/CD" scheme="http://fe.poetries.top/tags/CI-CD/"/>
    <category term="容器化部署" scheme="http://fe.poetries.top/tags/%E5%AE%B9%E5%99%A8%E5%8C%96%E9%83%A8%E7%BD%B2/"/>
    <content>
      <![CDATA[<p>在实际项目中，你是否每次发布都需要手动登录服务器执行一系列繁琐的操作？手动构建镜像、手动登录仓库、手动拉取镜像、手动启动容器…这些重复性工作不仅耗时，还容易因为人为疏忽导致部署失败。</p><p>本文将以部署Next.js应用为例，手把手教你搭建完整的CI&#x2F;CD流水线：通过GitHub Actions实现代码推送后自动构建Docker镜像，一键推送到腾讯云私有仓库，并在服务器上自动完成部署。整个过程完全自动化，开发者只需专注代码开发，部署全部由流水线自动完成。</p><h2 id="准备工作"><a href="#准备工作" class="headerlink" title="准备工作"></a>准备工作</h2><p>在开始配置之前，你需要准备以下内容：</p><ol><li><strong>GitHub仓库</strong>：用于托管代码和配置Actions工作流</li><li><strong>腾讯云账号</strong>：需要开通容器镜像服务TCR</li><li><strong>服务器</strong>：用于运行Docker容器（需安装Docker）</li></ol><h2 id="GitHub-Actions工作流配置"><a href="#GitHub-Actions工作流配置" class="headerlink" title="GitHub Actions工作流配置"></a>GitHub Actions工作流配置</h2><h3 id="创建工作流文件"><a href="#创建工作流文件" class="headerlink" title="创建工作流文件"></a>创建工作流文件</h3><p>在GitHub仓库的<code>.github/workflows</code>目录下创建<code>build-docker.yml</code>文件：</p><figure class="highlight yaml"><table><tr><td class="code"><pre><span class="line"><span class="comment"># build-docker.yml</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 填入到github的secrets中 https://github.com/test/repo/settings/secrets/actions</span></span><br><span class="line"></span><br><span class="line"><span class="attr">name:</span> <span class="string">构建和上传镜像到腾讯云私有镜像仓库</span></span><br><span class="line"></span><br><span class="line"><span class="attr">on:</span></span><br><span class="line">  <span class="attr">push:</span></span><br><span class="line">    <span class="attr">branches:</span> [<span class="string">master</span>]</span><br><span class="line"></span><br><span class="line"><span class="attr">jobs:</span></span><br><span class="line">  <span class="attr">build-and-push:</span></span><br><span class="line">    <span class="attr">runs-on:</span> <span class="string">ubuntu-latest</span></span><br><span class="line">    <span class="attr">env:</span></span><br><span class="line">      <span class="comment"># 设置镜像名称，请根据你的实际情况修改</span></span><br><span class="line">      <span class="attr">IMAGE_NAME:</span> <span class="string">my-app</span></span><br><span class="line">      <span class="comment"># 腾讯云镜像仓库命名空间，请替换为你的命名空间</span></span><br><span class="line">      <span class="attr">TCR_NAMESPACE:</span> <span class="string">test-hk</span></span><br><span class="line">      <span class="attr">TZ:</span> <span class="string">Asia/Shanghai</span> <span class="comment"># 在这里设置时区</span></span><br><span class="line">      <span class="attr">REGISTRY:</span> <span class="string">hkccr.ccs.tencentyun.com</span> <span class="comment"># 腾讯云镜像仓库地址</span></span><br><span class="line">    <span class="attr">steps:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Checkout</span> <span class="string">code</span></span><br><span class="line">        <span class="attr">uses:</span> <span class="string">actions/checkout@v4</span></span><br><span class="line"></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Generate</span> <span class="string">Docker</span> <span class="string">image</span> <span class="string">tag</span> <span class="string">with</span> <span class="string">timestamp</span></span><br><span class="line">        <span class="attr">id:</span> <span class="string">tag</span></span><br><span class="line">        <span class="attr">run:</span> <span class="string">|</span></span><br><span class="line"><span class="string">          # 验证时区设置</span></span><br><span class="line"><span class="string">          echo &quot;当前时区:&quot;</span></span><br><span class="line"><span class="string">          date</span></span><br><span class="line"><span class="string">          # 生成时间戳标签，格式为 2025-10-10-12-00-00(精确到秒)</span></span><br><span class="line"><span class="string">          TIMESTAMP=$(date +&#x27;%Y-%m-%d-%H-%M-%S&#x27;)</span></span><br><span class="line"><span class="string">          echo &quot;生成的时间戳: $TIMESTAMP&quot;</span></span><br><span class="line"><span class="string">          echo &quot;TIMESTAMP=$TIMESTAMP&quot; &gt;&gt; $GITHUB_ENV</span></span><br><span class="line"><span class="string">          # 设置镜像标签，格式为 hkccr.ccs.tencentyun.com/命名空间/镜像名:时间戳</span></span><br><span class="line"><span class="string">          echo &quot;IMAGE_TAG=$&#123;&#123; env.REGISTRY &#125;&#125;/$&#123;&#123; env.TCR_NAMESPACE &#125;&#125;/$&#123;&#123; env.IMAGE_NAME &#125;&#125;:$TIMESTAMP&quot; &gt;&gt; $GITHUB_ENV</span></span><br><span class="line"><span class="string"></span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Log</span> <span class="string">in</span> <span class="string">to</span> <span class="string">Tencent</span> <span class="string">Cloud</span> <span class="string">Container</span> <span class="string">Registry</span></span><br><span class="line">        <span class="attr">uses:</span> <span class="string">docker/login-action@v3</span></span><br><span class="line">        <span class="attr">with:</span></span><br><span class="line">          <span class="comment"># https://github.com/test/repo/settings/secrets/actions</span></span><br><span class="line">          <span class="comment"># https://github.com/docker/login-action</span></span><br><span class="line">          <span class="comment"># 腾讯云镜像仓库地址 https://console.cloud.tencent.com/tcr/repository/</span></span><br><span class="line">          <span class="attr">registry:</span> <span class="string">$&#123;&#123;</span> <span class="string">env.REGISTRY</span> <span class="string">&#125;&#125;</span></span><br><span class="line">          <span class="attr">username:</span> <span class="string">$&#123;&#123;</span> <span class="string">secrets.TENCENT_USERNAME</span> <span class="string">&#125;&#125;</span></span><br><span class="line">          <span class="attr">password:</span> <span class="string">$&#123;&#123;</span> <span class="string">secrets.TENCENT_PASSWORD</span> <span class="string">&#125;&#125;</span></span><br><span class="line"></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Set</span> <span class="string">up</span> <span class="string">Docker</span> <span class="string">Buildx</span></span><br><span class="line">        <span class="attr">uses:</span> <span class="string">docker/setup-buildx-action@v3</span></span><br><span class="line"></span><br><span class="line">      <span class="comment"># https://github.com/docker/build-push-action</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Build</span> <span class="string">and</span> <span class="string">push</span> <span class="string">Docker</span> <span class="string">image</span></span><br><span class="line">        <span class="attr">uses:</span> <span class="string">docker/build-push-action@v5</span></span><br><span class="line">        <span class="attr">with:</span></span><br><span class="line">          <span class="attr">context:</span> <span class="string">.</span></span><br><span class="line">          <span class="attr">file:</span> <span class="string">Dockerfile</span></span><br><span class="line">          <span class="attr">platforms:</span> <span class="string">linux/amd64</span></span><br><span class="line">          <span class="attr">push:</span> <span class="literal">true</span></span><br><span class="line">          <span class="attr">tags:</span> <span class="string">$&#123;&#123;</span> <span class="string">env.IMAGE_TAG</span> <span class="string">&#125;&#125;</span></span><br><span class="line"></span><br><span class="line">  <span class="comment"># https://github.com/appleboy/ssh-action/blob/master/README.zh-cn.md</span></span><br><span class="line">  <span class="attr">deploy:</span></span><br><span class="line">    <span class="attr">runs-on:</span> <span class="string">ubuntu-latest</span></span><br><span class="line">    <span class="attr">needs:</span> <span class="string">build-and-push</span> <span class="comment"># 明确依赖构建任务，等构建完成在执行部署</span></span><br><span class="line">    <span class="attr">steps:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Execute</span> <span class="string">deployment</span> <span class="string">script</span> <span class="string">on</span> <span class="string">server</span></span><br><span class="line">        <span class="attr">uses:</span> <span class="string">appleboy/ssh-action@v1</span></span><br><span class="line">        <span class="attr">with:</span></span><br><span class="line">          <span class="attr">host:</span> <span class="string">$&#123;&#123;</span> <span class="string">secrets.SERVER_IP</span> <span class="string">&#125;&#125;</span> <span class="comment"># 服务器 IP 或域名</span></span><br><span class="line">          <span class="attr">username:</span> <span class="string">$&#123;&#123;</span> <span class="string">secrets.SERVER_USERNAME</span> <span class="string">&#125;&#125;</span> <span class="comment"># SSH 用户名</span></span><br><span class="line">          <span class="attr">password:</span> <span class="string">$&#123;&#123;</span> <span class="string">secrets.SERVER_PASSWORD</span> <span class="string">&#125;&#125;</span> <span class="comment"># SSH 密码</span></span><br><span class="line">          <span class="comment"># 部署成功后自动执行服务端脚本文件</span></span><br><span class="line">          <span class="attr">script:</span> <span class="string">&#x27;sh /www/docker-deploy.sh&#x27;</span></span><br></pre></td></tr></table></figure><h3 id="配置GitHub-Secrets"><a href="#配置GitHub-Secrets" class="headerlink" title="配置GitHub Secrets"></a>配置GitHub Secrets</h3><p>在GitHub仓库的<code>Settings → Secrets and variables → Actions</code>中配置以下敏感信息。这些信息会以环境变量的形式注入到工作流中，确保敏感数据的安全：</p><table><thead><tr><th>变量名</th><th>说明</th></tr></thead><tbody><tr><td>SERVER_USERNAME</td><td>服务器SSH登录用户名</td></tr><tr><td>SERVER_PASSWORD</td><td>服务器SSH登录密码</td></tr><tr><td>SERVER_IP</td><td>服务器公网IP地址</td></tr><tr><td>TENCENT_USERNAME</td><td>腾讯云账户名</td></tr><tr><td>TENCENT_PASSWORD</td><td>腾讯云镜像仓库的密码</td></tr></tbody></table><p><strong>安全建议</strong>：生产环境建议使用SSH密钥而非密码认证，并将敏感信息的访问权限限制为最小必要范围。</p><h2 id="腾讯云私有仓库配置"><a href="#腾讯云私有仓库配置" class="headerlink" title="腾讯云私有仓库配置"></a>腾讯云私有仓库配置</h2><h3 id="创建私有镜像仓库"><a href="#创建私有镜像仓库" class="headerlink" title="创建私有镜像仓库"></a>创建私有镜像仓库</h3><p>首先登录<a href="https://console.cloud.tencent.com/tcr/repository">腾讯云容器镜像控制台</a>，创建私有镜像仓库：</p><ol><li><p>创建命名空间：点击「命名空间」→「创建」，输入命名空间名称（如<code>test-hk</code>）</p></li><li><p>创建镜像仓库：点击「镜像仓库」→「创建」，填写仓库名称和描述</p></li></ol><p><img src="https://s.poetries.top/uploads/2026/03/4318219148ce5012.png" alt="创建私有镜像仓库"></p><h3 id="获取镜像仓库地址"><a href="#获取镜像仓库地址" class="headerlink" title="获取镜像仓库地址"></a>获取镜像仓库地址</h3><p>在镜像仓库详情页面可以查看仓库的访问地址：</p><p><img src="https://s.poetries.top/uploads/2026/03/e7bd82653a736bdb.png" alt="获取镜像仓库地址"></p><h3 id="设置镜像仓库登录密码"><a href="#设置镜像仓库登录密码" class="headerlink" title="设置镜像仓库登录密码"></a>设置镜像仓库登录密码</h3><p>首次使用需要设置登录密码，用于Docker login认证：</p><ol><li>进入「访问凭证」页面</li><li>点击「设置密码」</li><li>设置完成后，该密码即用于后续Docker登录认证</li></ol><p><img src="https://s.poetries.top/uploads/2026/03/cdd08d3cc8bd08c4.png" alt="重置镜像仓库登录密码"></p><h2 id="服务器端配置"><a href="#服务器端配置" class="headerlink" title="服务器端配置"></a>服务器端配置</h2><h3 id="安装腾讯云CLI"><a href="#安装腾讯云CLI" class="headerlink" title="安装腾讯云CLI"></a>安装腾讯云CLI</h3><p>在服务器上安装腾讯云CLI工具，用于查询镜像版本：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 安装腾讯云CLI</span></span><br><span class="line">pip3 install tccli</span><br><span class="line"></span><br><span class="line"><span class="comment"># 配置访问凭证</span></span><br><span class="line">tccli configure</span><br><span class="line"><span class="comment"># 依次输入：</span></span><br><span class="line"><span class="comment"># SecretId: &lt;你的SecretId&gt;</span></span><br><span class="line"><span class="comment"># SecretKey: &lt;你的SecretKey&gt;</span></span><br><span class="line"><span class="comment"># Region: ap-guangzhou</span></span><br><span class="line"><span class="comment"># Output format: json</span></span><br></pre></td></tr></table></figure><h3 id="创建部署脚本"><a href="#创建部署脚本" class="headerlink" title="创建部署脚本"></a>创建部署脚本</h3><p>将一键部署脚本放到服务器目录 <code>/www/deploy-scripts/docker-deploy.sh</code>：</p><h2 id="部署应用"><a href="#部署应用" class="headerlink" title="部署应用"></a>部署应用</h2><p>当GitHub Action构建成功后，会自动推送镜像到私有仓库：</p><p><img src="https://s.poetries.top/uploads/2026/03/21fd2ac5d06f7c4b.png" alt="GitHub Actions构建结果"></p><p>然后我们在服务器端拉取镜像并启动容器即可完成部署：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 查询指定镜像仓库的版本：然后获取最新版本的TagName用于拉取镜像</span></span><br><span class="line">tccli tcr DescribeImagePersonal --cli-unfold-argument --region ap-guangzhou --RepoName tcb-8121221-mope/my-app --Offset 0 --Limit 2</span><br></pre></td></tr></table></figure><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 登录镜像仓库</span></span><br><span class="line">docker login hkccr.ccs.tencentyun.com --username=12312 --password=<span class="string">&quot;123132123&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 拉取镜像对应的版本</span></span><br><span class="line">docker pull hkccr.ccs.tencentyun.com/tcb-test-mope/my-app:202510281002</span><br><span class="line"><span class="comment"># 获取版本号 https://console.cloud.tencent.com/tcr/repository/</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 删除当前运行的服务</span></span><br><span class="line">docker ps</span><br><span class="line">docker <span class="built_in">rm</span> xx -f</span><br><span class="line"></span><br><span class="line"><span class="comment"># 先暂停之前的容器，防止部署出问题可以回滚</span></span><br><span class="line">docker stop xx</span><br><span class="line"></span><br><span class="line"><span class="comment"># 启动镜像</span></span><br><span class="line">docker run -d -p 3000:3000 --name my-app-202510281002 hkccr.ccs.tencentyun.com/tcb-100008440496-mope/my-app:202510281002</span><br></pre></td></tr></table></figure><h2 id="完整部署流程总结"><a href="#完整部署流程总结" class="headerlink" title="完整部署流程总结"></a>完整部署流程总结</h2><p>整个自动化部署流程如下：</p><ol><li><p><strong>代码推送</strong>：开发人员将代码推送到GitHub master分支</p></li><li><p><strong>自动触发</strong>：GitHub Actions检测到push事件，自动开始执行工作流</p></li><li><p><strong>构建镜像</strong>：基于Dockerfile构建Docker镜像，使用时间戳作为版本标签</p></li><li><p><strong>推送仓库</strong>：登录腾讯云私有仓库，将构建好的镜像推送到仓库</p></li><li><p><strong>自动部署</strong>：通过SSH连接到服务器，执行部署脚本拉取最新镜像并启动容器</p></li><li><p><strong>验证服务</strong>：确认服务正常运行，部署完成</p></li></ol><p>整个过程完全自动化，无需人工干预，大大提高了部署效率和可靠性。</p>]]>
    </content>
    <id>http://fe.poetries.top/2026/01/15/github-actions-tencent-docker-registry/</id>
    <link href="http://fe.poetries.top/2026/01/15/github-actions-tencent-docker-registry/"/>
    <published>2026-01-15T06:40:12.000Z</published>
    <summary>详解如何使用GitHub Actions自动化构建Docker镜像并推送到腾讯云私有仓库，从零开始搭建CI/CD流水线，实现代码提交自动部署。</summary>
    <title>把手把手带你基于Github Actions构建docker镜像部署到腾讯云私有仓库</title>
    <updated>2026-03-08T10:22:42.070Z</updated>
  </entry>
  <entry>
    <author>
      <name>Poetry</name>
    </author>
    <category term="Front-End" scheme="http://fe.poetries.top/categories/Front-End/"/>
    <category term="RN" scheme="http://fe.poetries.top/tags/RN/"/>
    <category term="TailwindCSS" scheme="http://fe.poetries.top/tags/TailwindCSS/"/>
    <content>
      <![CDATA[<p>最近在公司接手了一个React Native项目，打开代码的那一刻我整个人都不好了。上千行的StyleSheet，命名从<code>container1</code>到<code>container27</code>，找个样式比找对象还难。更要命的是，改个颜色要翻三个文件，调个间距得祈祷别影响其他页面。那一刻我就在想：2025年了，咱们真的还要这么写样式吗？</p><p>后来偶然接触到了Tailwind CSS在React Native上的实现方案twrnc，说实话，刚开始我是拒绝的。又是一个新轮子？学习成本会不会很高？但用了两周之后，我真香了。今天就来聊聊，为什么说Tailwind CSS能重塑React Native的开发效率。</p><h2 id="传统React-Native样式开发的痛点"><a href="#传统React-Native样式开发的痛点" class="headerlink" title="传统React Native样式开发的痛点"></a>传统React Native样式开发的痛点</h2><p>先说说传统开发模式到底痛在哪。不吐不快。</p><h3 id="样式和组件分离带来的心智负担"><a href="#样式和组件分离带来的心智负担" class="headerlink" title="样式和组件分离带来的心智负担"></a>样式和组件分离带来的心智负担</h3><p>在传统的React Native开发中，我们通常会这样写：</p><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">StyleSheet</span>, <span class="title class_">View</span>, <span class="title class_">Text</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;react-native&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">UserCard</span>(<span class="params">&#123; name, bio &#125;</span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">View</span> <span class="attr">style</span>=<span class="string">&#123;styles.container&#125;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">View</span> <span class="attr">style</span>=<span class="string">&#123;styles.header&#125;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">Text</span> <span class="attr">style</span>=<span class="string">&#123;styles.name&#125;</span>&gt;</span>&#123;name&#125;<span class="tag">&lt;/<span class="name">Text</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;/<span class="name">View</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">Text</span> <span class="attr">style</span>=<span class="string">&#123;styles.bio&#125;</span>&gt;</span>&#123;bio&#125;<span class="tag">&lt;/<span class="name">Text</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">View</span>&gt;</span></span></span><br><span class="line">  );</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> styles = <span class="title class_">StyleSheet</span>.<span class="title function_">create</span>(&#123;</span><br><span class="line">  <span class="attr">container</span>: &#123;</span><br><span class="line">    <span class="attr">backgroundColor</span>: <span class="string">&#x27;#ffffff&#x27;</span>,</span><br><span class="line">    <span class="attr">padding</span>: <span class="number">16</span>,</span><br><span class="line">    <span class="attr">borderRadius</span>: <span class="number">8</span>,</span><br><span class="line">    <span class="attr">marginBottom</span>: <span class="number">12</span>,</span><br><span class="line">    <span class="attr">shadowColor</span>: <span class="string">&#x27;#000&#x27;</span>,</span><br><span class="line">    <span class="attr">shadowOffset</span>: &#123; <span class="attr">width</span>: <span class="number">0</span>, <span class="attr">height</span>: <span class="number">2</span> &#125;,</span><br><span class="line">    <span class="attr">shadowOpacity</span>: <span class="number">0.1</span>,</span><br><span class="line">    <span class="attr">shadowRadius</span>: <span class="number">4</span>,</span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="attr">header</span>: &#123;</span><br><span class="line">    <span class="attr">marginBottom</span>: <span class="number">8</span>,</span><br><span class="line">    <span class="attr">borderBottomWidth</span>: <span class="number">1</span>,</span><br><span class="line">    <span class="attr">borderBottomColor</span>: <span class="string">&#x27;#e5e5e5&#x27;</span>,</span><br><span class="line">    <span class="attr">paddingBottom</span>: <span class="number">8</span>,</span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="attr">name</span>: &#123;</span><br><span class="line">    <span class="attr">fontSize</span>: <span class="number">18</span>,</span><br><span class="line">    <span class="attr">fontWeight</span>: <span class="string">&#x27;bold&#x27;</span>,</span><br><span class="line">    <span class="attr">color</span>: <span class="string">&#x27;#333333&#x27;</span>,</span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="attr">bio</span>: &#123;</span><br><span class="line">    <span class="attr">fontSize</span>: <span class="number">14</span>,</span><br><span class="line">    <span class="attr">color</span>: <span class="string">&#x27;#666666&#x27;</span>,</span><br><span class="line">    <span class="attr">lineHeight</span>: <span class="number">20</span>,</span><br><span class="line">  &#125;,</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p>看起来挺规范的对吧？但问题来了：</p><ol><li><p><strong>上下反复横跳</strong>：写组件的时候要不停地在顶部和底部来回滚动，看看<code>styles.container</code>到底定义了啥。写着写着就忘了自己要改哪个样式。</p></li><li><p><strong>命名焦虑症</strong>：<code>container</code>、<code>wrapper</code>、<code>inner</code>、<code>content</code>……到底该叫啥？每次起名字都要纠结半天。最后项目里出现<code>container2</code>、<code>containerNew</code>、<code>containerFinal</code>这种鬼名字。</p></li><li><p><strong>样式复用困难</strong>：想复用一个样式？要么把它提取到单独的文件，要么就复制粘贴。提取文件吧，感觉小题大做；复制粘贴吧，后面维护要哭。</p></li></ol><h3 id="响应式设计的噩梦"><a href="#响应式设计的噩梦" class="headerlink" title="响应式设计的噩梦"></a>响应式设计的噩梦</h3><p>移动端适配是另一个大坑。假设你要根据屏幕尺寸调整布局：</p><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">Dimensions</span>, <span class="title class_">Platform</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;react-native&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> &#123; width &#125; = <span class="title class_">Dimensions</span>.<span class="title function_">get</span>(<span class="string">&#x27;window&#x27;</span>);</span><br><span class="line"><span class="keyword">const</span> isSmallScreen = width &lt; <span class="number">375</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> styles = <span class="title class_">StyleSheet</span>.<span class="title function_">create</span>(&#123;</span><br><span class="line">  <span class="attr">container</span>: &#123;</span><br><span class="line">    <span class="attr">padding</span>: isSmallScreen ? <span class="number">12</span> : <span class="number">16</span>,</span><br><span class="line">    <span class="attr">fontSize</span>: isSmallScreen ? <span class="number">14</span> : <span class="number">16</span>,</span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="comment">// 还得监听屏幕旋转...</span></span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p>这还只是简单的场景。如果要处理横竖屏切换、平板适配，代码量会指数级增长。而且这种动态计算的样式，每次组件重新渲染都得算一遍，性能也是个问题。</p><h3 id="主题切换的灾难现场"><a href="#主题切换的灾难现场" class="headerlink" title="主题切换的灾难现场"></a>主题切换的灾难现场</h3><p>现在App没个深色模式都不好意思上架。传统方案是这样的：</p><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; useColorScheme &#125; <span class="keyword">from</span> <span class="string">&#x27;react-native&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">MyComponent</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> colorScheme = <span class="title function_">useColorScheme</span>();</span><br><span class="line">  </span><br><span class="line">  <span class="keyword">const</span> styles = <span class="title class_">StyleSheet</span>.<span class="title function_">create</span>(&#123;</span><br><span class="line">    <span class="attr">container</span>: &#123;</span><br><span class="line">      <span class="attr">backgroundColor</span>: colorScheme === <span class="string">&#x27;dark&#x27;</span> ? <span class="string">&#x27;#1a1a1a&#x27;</span> : <span class="string">&#x27;#ffffff&#x27;</span>,</span><br><span class="line">      <span class="attr">borderColor</span>: colorScheme === <span class="string">&#x27;dark&#x27;</span> ? <span class="string">&#x27;#333333&#x27;</span> : <span class="string">&#x27;#e5e5e5&#x27;</span>,</span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="attr">text</span>: &#123;</span><br><span class="line">      <span class="attr">color</span>: colorScheme === <span class="string">&#x27;dark&#x27;</span> ? <span class="string">&#x27;#ffffff&#x27;</span> : <span class="string">&#x27;#333333&#x27;</span>,</span><br><span class="line">    &#125;,</span><br><span class="line">  &#125;);</span><br><span class="line">  </span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">View</span> <span class="attr">style</span>=<span class="string">&#123;styles.container&#125;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">Text</span> <span class="attr">style</span>=<span class="string">&#123;styles.text&#125;</span>&gt;</span>Hello<span class="tag">&lt;/<span class="name">Text</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">View</span>&gt;</span></span></span><br><span class="line">  );</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>看着就头大。每个组件都要这么写，每个颜色都要判断一遍。更恐怖的是，如果产品经理说：”我们要支持自定义主题色，让用户可以选10种颜色”，那基本就是重写整个项目的节奏。</p><h3 id="团队协作的混乱"><a href="#团队协作的混乱" class="headerlink" title="团队协作的混乱"></a>团队协作的混乱</h3><p>多人协作的时候更乱。小王定义了一个<code>#3b82f6</code>的蓝色，小李又定义了一个<code>#2563eb</code>，结果项目里出现了十几种蓝色。想统一？得一个个文件去改。</p><p>还有间距问题。有人喜欢用8的倍数，有人喜欢10的倍数，最后整个App看起来参差不齐，强迫症看了想摔手机。</p><h2 id="Tailwind-CSS-twrnc：一剂良药"><a href="#Tailwind-CSS-twrnc：一剂良药" class="headerlink" title="Tailwind CSS + twrnc：一剂良药"></a>Tailwind CSS + twrnc：一剂良药</h2><p>说了这么多痛点，那Tailwind CSS是怎么解决这些问题的呢？</p><h3 id="Tailwind-CSS的核心思想"><a href="#Tailwind-CSS的核心思想" class="headerlink" title="Tailwind CSS的核心思想"></a>Tailwind CSS的核心思想</h3><p>Tailwind CSS的理念很简单：<strong>用原子化的工具类来构建界面</strong>。什么是原子化？就是把样式拆成最小的单元，每个类只做一件事。</p><p>比如：</p><ul><li><code>p-4</code>：padding为16px</li><li><code>bg-blue-500</code>：背景色为蓝色</li><li><code>rounded-lg</code>：圆角为8px</li><li><code>shadow-md</code>：中等阴影</li></ul><p>用这些小积木，你可以搭建出任何界面。就像搭乐高一样，每块积木功能单一，但组合起来能创造无限可能。</p><h3 id="为什么Tailwind适合React-Native"><a href="#为什么Tailwind适合React-Native" class="headerlink" title="为什么Tailwind适合React Native"></a>为什么Tailwind适合React Native</h3><p>有人会问：Tailwind不是给Web用的吗？确实，Tailwind最初是为Web设计的，但它的思想完美适配移动端开发：</p><ol><li><strong>快速原型</strong>：不用起名字，不用来回跳转，看着组件就能写样式</li><li><strong>一致性</strong>：设计系统内置，团队自然会用统一的间距和颜色</li><li><strong>响应式</strong>：内置断点系统，适配不同屏幕很简单</li><li><strong>主题切换</strong>：天生支持，切换主题就是改个配置的事</li></ol><p>关键是，有了twrnc这个库，我们可以在React Native中无缝使用Tailwind的语法。</p><h2 id="twrnc快速上手：让代码飞起来"><a href="#twrnc快速上手：让代码飞起来" class="headerlink" title="twrnc快速上手：让代码飞起来"></a>twrnc快速上手：让代码飞起来</h2><h3 id="安装和基础配置"><a href="#安装和基础配置" class="headerlink" title="安装和基础配置"></a>安装和基础配置</h3><p>安装非常简单，一行命令搞定：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">yarn add twrnc</span><br></pre></td></tr></table></figure><figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="comment">// 全局配置 </span></span><br><span class="line"><span class="comment">// libs/tailwind.ts</span></span><br><span class="line"><span class="keyword">import</span> &#123; create &#125; <span class="keyword">from</span> <span class="string">&#x27;twrnc&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// create the customized version...</span></span><br><span class="line"><span class="keyword">const</span> tw = <span class="title function_">create</span>(<span class="built_in">require</span>(<span class="string">`../../tailwind.config.js`</span>)) <span class="comment">// &lt;- your path may differ</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// ... and then this becomes the main function your app uses</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> tw</span><br></pre></td></tr></table></figure><p>然后在项目中引入：</p><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> tw <span class="keyword">from</span> <span class="string">&#x27;@/libs/tailwind.ts&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">MyComponent</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">View</span> <span class="attr">style</span>=<span class="string">&#123;tw</span>`<span class="attr">bg-white</span> <span class="attr">p-4</span> <span class="attr">rounded-lg</span> <span class="attr">shadow-md</span> <span class="attr">mb-3</span>`&#125;&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">Text</span> <span class="attr">style</span>=<span class="string">&#123;tw</span>`<span class="attr">text-lg</span> <span class="attr">font-bold</span> <span class="attr">text-gray-800</span>`&#125;&gt;</span></span></span><br><span class="line"><span class="language-xml">        你好，世界</span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;/<span class="name">Text</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">View</span>&gt;</span></span></span><br><span class="line">  );</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>就这么简单！不需要任何配置，开箱即用。</p><h3 id="实战：重构一个用户卡片组件"><a href="#实战：重构一个用户卡片组件" class="headerlink" title="实战：重构一个用户卡片组件"></a>实战：重构一个用户卡片组件</h3><p>让我们用实际例子感受一下差异。这是传统写法和Tailwind写法的对比：</p><p><strong>传统写法（30行+）：</strong></p><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">UserCard</span>(<span class="params">&#123; avatar, name, role, followers &#125;</span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">View</span> <span class="attr">style</span>=<span class="string">&#123;styles.card&#125;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">Image</span> <span class="attr">source</span>=<span class="string">&#123;&#123;</span> <span class="attr">uri:</span> <span class="attr">avatar</span> &#125;&#125; <span class="attr">style</span>=<span class="string">&#123;styles.avatar&#125;</span> /&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">View</span> <span class="attr">style</span>=<span class="string">&#123;styles.info&#125;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">Text</span> <span class="attr">style</span>=<span class="string">&#123;styles.name&#125;</span>&gt;</span>&#123;name&#125;<span class="tag">&lt;/<span class="name">Text</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">Text</span> <span class="attr">style</span>=<span class="string">&#123;styles.role&#125;</span>&gt;</span>&#123;role&#125;<span class="tag">&lt;/<span class="name">Text</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">View</span> <span class="attr">style</span>=<span class="string">&#123;styles.stats&#125;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">          <span class="tag">&lt;<span class="name">Text</span> <span class="attr">style</span>=<span class="string">&#123;styles.followers&#125;</span>&gt;</span>&#123;followers&#125; 关注者<span class="tag">&lt;/<span class="name">Text</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;/<span class="name">View</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;/<span class="name">View</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">View</span>&gt;</span></span></span><br><span class="line">  );</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> styles = <span class="title class_">StyleSheet</span>.<span class="title function_">create</span>(&#123;</span><br><span class="line">  <span class="attr">card</span>: &#123;</span><br><span class="line">    <span class="attr">flexDirection</span>: <span class="string">&#x27;row&#x27;</span>,</span><br><span class="line">    <span class="attr">backgroundColor</span>: <span class="string">&#x27;#fff&#x27;</span>,</span><br><span class="line">    <span class="attr">padding</span>: <span class="number">16</span>,</span><br><span class="line">    <span class="attr">borderRadius</span>: <span class="number">12</span>,</span><br><span class="line">    <span class="attr">marginBottom</span>: <span class="number">12</span>,</span><br><span class="line">    <span class="attr">shadowColor</span>: <span class="string">&#x27;#000&#x27;</span>,</span><br><span class="line">    <span class="attr">shadowOffset</span>: &#123; <span class="attr">width</span>: <span class="number">0</span>, <span class="attr">height</span>: <span class="number">2</span> &#125;,</span><br><span class="line">    <span class="attr">shadowOpacity</span>: <span class="number">0.1</span>,</span><br><span class="line">    <span class="attr">shadowRadius</span>: <span class="number">8</span>,</span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="attr">avatar</span>: &#123;</span><br><span class="line">    <span class="attr">width</span>: <span class="number">60</span>,</span><br><span class="line">    <span class="attr">height</span>: <span class="number">60</span>,</span><br><span class="line">    <span class="attr">borderRadius</span>: <span class="number">30</span>,</span><br><span class="line">    <span class="attr">marginRight</span>: <span class="number">12</span>,</span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="attr">info</span>: &#123;</span><br><span class="line">    <span class="attr">flex</span>: <span class="number">1</span>,</span><br><span class="line">    <span class="attr">justifyContent</span>: <span class="string">&#x27;center&#x27;</span>,</span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="attr">name</span>: &#123;</span><br><span class="line">    <span class="attr">fontSize</span>: <span class="number">18</span>,</span><br><span class="line">    <span class="attr">fontWeight</span>: <span class="string">&#x27;600&#x27;</span>,</span><br><span class="line">    <span class="attr">color</span>: <span class="string">&#x27;#1a1a1a&#x27;</span>,</span><br><span class="line">    <span class="attr">marginBottom</span>: <span class="number">4</span>,</span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="attr">role</span>: &#123;</span><br><span class="line">    <span class="attr">fontSize</span>: <span class="number">14</span>,</span><br><span class="line">    <span class="attr">color</span>: <span class="string">&#x27;#666&#x27;</span>,</span><br><span class="line">    <span class="attr">marginBottom</span>: <span class="number">8</span>,</span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="attr">stats</span>: &#123;</span><br><span class="line">    <span class="attr">flexDirection</span>: <span class="string">&#x27;row&#x27;</span>,</span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="attr">followers</span>: &#123;</span><br><span class="line">    <span class="attr">fontSize</span>: <span class="number">12</span>,</span><br><span class="line">    <span class="attr">color</span>: <span class="string">&#x27;#999&#x27;</span>,</span><br><span class="line">  &#125;,</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p><strong>Tailwind写法（14行）：</strong></p><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> tw <span class="keyword">from</span> <span class="string">&#x27;twrnc&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">UserCard</span>(<span class="params">&#123; avatar, name, role, followers &#125;</span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">View</span> <span class="attr">style</span>=<span class="string">&#123;tw</span>`<span class="attr">flex-row</span> <span class="attr">bg-white</span> <span class="attr">p-4</span> <span class="attr">rounded-xl</span> <span class="attr">mb-3</span> <span class="attr">shadow-lg</span>`&#125;&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">Image</span> </span></span></span><br><span class="line"><span class="tag"><span class="language-xml">        <span class="attr">source</span>=<span class="string">&#123;&#123;</span> <span class="attr">uri:</span> <span class="attr">avatar</span> &#125;&#125; </span></span></span><br><span class="line"><span class="tag"><span class="language-xml">        <span class="attr">style</span>=<span class="string">&#123;tw</span>`<span class="attr">w-15</span> <span class="attr">h-15</span> <span class="attr">rounded-full</span> <span class="attr">mr-3</span>`&#125; </span></span></span><br><span class="line"><span class="tag"><span class="language-xml">      /&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">View</span> <span class="attr">style</span>=<span class="string">&#123;tw</span>`<span class="attr">flex-1</span> <span class="attr">justify-center</span>`&#125;&gt;</span></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">Text</span> <span class="attr">style</span>=<span class="string">&#123;tw</span>`<span class="attr">text-lg</span> <span class="attr">font-semibold</span> <span class="attr">text-gray-900</span> <span class="attr">mb-1</span>`&#125;&gt;</span></span></span><br><span class="line"><span class="language-xml">          &#123;name&#125;</span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;/<span class="name">Text</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">Text</span> <span class="attr">style</span>=<span class="string">&#123;tw</span>`<span class="attr">text-sm</span> <span class="attr">text-gray-600</span> <span class="attr">mb-2</span>`&#125;&gt;</span>&#123;role&#125;<span class="tag">&lt;/<span class="name">Text</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">Text</span> <span class="attr">style</span>=<span class="string">&#123;tw</span>`<span class="attr">text-xs</span> <span class="attr">text-gray-400</span>`&#125;&gt;</span>&#123;followers&#125; 关注者<span class="tag">&lt;/<span class="name">Text</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;/<span class="name">View</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">View</span>&gt;</span></span></span><br><span class="line">  );</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>看到没？代码量直接砍半，而且一眼就能看出这个组件长什么样。不用上下翻滚，不用猜<code>styles.info</code>到底定义了啥。</p><h3 id="条件样式的优雅处理"><a href="#条件样式的优雅处理" class="headerlink" title="条件样式的优雅处理"></a>条件样式的优雅处理</h3><p>真实项目中，样式经常要根据状态变化。<code>twrnc</code>处理起来也很舒服：</p><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">Button</span>(<span class="params">&#123; text, disabled, variant = <span class="string">&#x27;primary&#x27;</span> &#125;</span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">TouchableOpacity</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">      <span class="attr">style</span>=<span class="string">&#123;tw</span>`</span></span></span><br><span class="line"><span class="tag"><span class="language-xml">        <span class="attr">px-6</span> <span class="attr">py-3</span> <span class="attr">rounded-lg</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">        $&#123;<span class="attr">variant</span> === <span class="string">&#x27;primary&#x27;</span> ? &#x27;<span class="attr">bg-blue-500</span>&#x27; <span class="attr">:</span> &#x27;<span class="attr">bg-gray-500</span>&#x27;&#125;</span></span></span><br><span class="line"><span class="tag"><span class="language-xml">        $&#123;<span class="attr">disabled</span> ? &#x27;<span class="attr">opacity-50</span>&#x27; <span class="attr">:</span> &#x27;<span class="attr">opacity-100</span>&#x27;&#125;</span></span></span><br><span class="line"><span class="tag"><span class="language-xml">      `&#125;</span></span></span><br><span class="line"><span class="tag"><span class="language-xml">      <span class="attr">disabled</span>=<span class="string">&#123;disabled&#125;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">    &gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">Text</span> <span class="attr">style</span>=<span class="string">&#123;tw</span>`<span class="attr">text-white</span> <span class="attr">font-medium</span> <span class="attr">text-center</span>`&#125;&gt;</span></span></span><br><span class="line"><span class="language-xml">        &#123;text&#125;</span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;/<span class="name">Text</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">TouchableOpacity</span>&gt;</span></span></span><br><span class="line">  );</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>用模板字符串配合三元运算符，逻辑清晰，改起来也方便。</p><h3 id="响应式布局变得简单"><a href="#响应式布局变得简单" class="headerlink" title="响应式布局变得简单"></a>响应式布局变得简单</h3><p>twrnc虽然不像Web版Tailwind那样有<code>sm:</code>、<code>md:</code>这些前缀，但我们可以用自己的方式实现响应式：</p><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; useWindowDimensions &#125; <span class="keyword">from</span> <span class="string">&#x27;react-native&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> tw <span class="keyword">from</span> <span class="string">&#x27;twrnc&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">ResponsiveGrid</span>(<span class="params">&#123; children &#125;</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> &#123; width &#125; = <span class="title function_">useWindowDimensions</span>();</span><br><span class="line">  <span class="keyword">const</span> isSmall = width &lt; <span class="number">375</span>;</span><br><span class="line">  <span class="keyword">const</span> isMedium = width &gt;= <span class="number">375</span> &amp;&amp; width &lt; <span class="number">768</span>;</span><br><span class="line">  </span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">View</span> <span class="attr">style</span>=<span class="string">&#123;tw</span>`</span></span></span><br><span class="line"><span class="tag"><span class="language-xml">      $&#123;<span class="attr">isSmall</span> ? &#x27;<span class="attr">grid-cols-2</span>&#x27; <span class="attr">:</span> &#x27;&#x27;&#125;</span></span></span><br><span class="line"><span class="tag"><span class="language-xml">      $&#123;<span class="attr">isMedium</span> ? &#x27;<span class="attr">grid-cols-3</span>&#x27; <span class="attr">:</span> &#x27;&#x27;&#125;</span></span></span><br><span class="line"><span class="tag"><span class="language-xml">      $&#123;<span class="attr">width</span> &gt;</span>= 768 ? &#x27;grid-cols-4&#x27; : &#x27;&#x27;&#125;</span></span><br><span class="line"><span class="language-xml">      gap-4 p-4</span></span><br><span class="line"><span class="language-xml">    `&#125;&gt;</span></span><br><span class="line"><span class="language-xml">      &#123;children&#125;</span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">View</span>&gt;</span></span></span><br><span class="line">  );</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>或者更进阶的，可以自定义样式：</p><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> tw <span class="keyword">from</span> <span class="string">&#x27;twrnc&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 根据屏幕宽度动态计算</span></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">getResponsiveStyle</span> = (<span class="params">width</span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">if</span> (width &lt; <span class="number">375</span>) <span class="keyword">return</span> tw<span class="string">`p-2 text-sm`</span>;</span><br><span class="line">  <span class="keyword">if</span> (width &lt; <span class="number">768</span>) <span class="keyword">return</span> tw<span class="string">`p-4 text-base`</span>;</span><br><span class="line">  <span class="keyword">return</span> tw<span class="string">`p-6 text-lg`</span>;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">MyComponent</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> &#123; width &#125; = <span class="title function_">useWindowDimensions</span>();</span><br><span class="line">  </span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">View</span> <span class="attr">style</span>=<span class="string">&#123;getResponsiveStyle(width)&#125;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">Text</span>&gt;</span>响应式内容<span class="tag">&lt;/<span class="name">Text</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">View</span>&gt;</span></span></span><br><span class="line">  );</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="主题切换：从噩梦到美梦"><a href="#主题切换：从噩梦到美梦" class="headerlink" title="主题切换：从噩梦到美梦"></a>主题切换：从噩梦到美梦</h2><p>主题切换是最让人头疼的需求之一，但有了<code>twrnc</code>，这变成了一件轻松的事。</p><h3 id="基础的深色模式实现"><a href="#基础的深色模式实现" class="headerlink" title="基础的深色模式实现"></a>基础的深色模式实现</h3><p>twrnc内置了对<code>useColorScheme</code>的支持，可以直接用<code>dark:</code>前缀：</p><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; useColorScheme &#125; <span class="keyword">from</span> <span class="string">&#x27;react-native&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> tw <span class="keyword">from</span> <span class="string">&#x27;twrnc&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">ThemedCard</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> colorScheme = <span class="title function_">useColorScheme</span>();</span><br><span class="line">  </span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">View</span> <span class="attr">style</span>=<span class="string">&#123;tw</span>`</span></span></span><br><span class="line"><span class="tag"><span class="language-xml">      <span class="attr">bg-white</span> <span class="attr">dark:bg-gray-800</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">      <span class="attr">border</span> <span class="attr">border-gray-200</span> <span class="attr">dark:border-gray-700</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">      <span class="attr">p-4</span> <span class="attr">rounded-lg</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">    `&#125;&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">Text</span> <span class="attr">style</span>=<span class="string">&#123;tw</span>`</span></span></span><br><span class="line"><span class="tag"><span class="language-xml">        <span class="attr">text-gray-900</span> <span class="attr">dark:text-gray-100</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">        <span class="attr">text-lg</span> <span class="attr">font-semibold</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">      `&#125;&gt;</span></span></span><br><span class="line"><span class="language-xml">        自动适配的主题</span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;/<span class="name">Text</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">Text</span> <span class="attr">style</span>=<span class="string">&#123;tw</span>`<span class="attr">text-gray-600</span> <span class="attr">dark:text-gray-400</span> <span class="attr">mt-2</span>`&#125;&gt;</span></span></span><br><span class="line"><span class="language-xml">        系统切换深色模式时，这里会自动变化</span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;/<span class="name">Text</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">View</span>&gt;</span></span></span><br><span class="line">  );</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>但这里有个小技巧。twrnc默认的<code>dark:</code>支持需要手动开启。我们需要配置一下：</p><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="comment">// context/themeProvider.tsx</span></span><br><span class="line"><span class="keyword">import</span> <span class="title class_">React</span>, &#123; createContext, useCallback, useContext, useEffect, useMemo &#125; <span class="keyword">from</span> <span class="string">&#x27;react&#x27;</span></span><br><span class="line"><span class="keyword">import</span> &#123; useColorScheme &#125; <span class="keyword">from</span> <span class="string">&#x27;react-native&#x27;</span></span><br><span class="line"><span class="keyword">import</span> type &#123; <span class="title class_">ClassInput</span>, <span class="title class_">RnColorScheme</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;twrnc&#x27;</span></span><br><span class="line"><span class="keyword">import</span> &#123; useAppColorScheme, useDeviceContext &#125; <span class="keyword">from</span> <span class="string">&#x27;twrnc&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> &#123; <span class="variable constant_">KEY_DIRECTION</span>, <span class="variable constant_">KEY_THEME</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;@/constants&#x27;</span></span><br><span class="line"><span class="keyword">import</span> &#123; useAsyncStorage &#125; <span class="keyword">from</span> <span class="string">&#x27;@/hooks/useLocalStorage&#x27;</span></span><br><span class="line"><span class="keyword">import</span> tw <span class="keyword">from</span> <span class="string">&#x27;@/libs/tailwind&#x27;</span></span><br><span class="line"><span class="keyword">import</span> &#123; lightTheme &#125; <span class="keyword">from</span> <span class="string">&#x27;@/theme/theme.config&#x27;</span></span><br><span class="line"><span class="keyword">import</span> &#123; darkTheme &#125; <span class="keyword">from</span> <span class="string">&#x27;@/theme/theme.config.dark&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// export type IThemeMode =</span></span><br><span class="line"><span class="comment">//   /** 亮 */</span></span><br><span class="line"><span class="comment">//   | &#x27;light&#x27;</span></span><br><span class="line"><span class="comment">//   /** 夜间 */</span></span><br><span class="line"><span class="comment">//   | &#x27;dark&#x27;</span></span><br><span class="line"><span class="comment">//   /** 跟随系统 */</span></span><br><span class="line"><span class="comment">//   | &#x27;device&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> type <span class="title class_">IDirection</span> =</span><br><span class="line">  <span class="comment">/** 0绿涨红跌 */</span></span><br><span class="line">  | <span class="number">0</span></span><br><span class="line">  <span class="comment">/** 红涨绿跌 */</span></span><br><span class="line">  | <span class="number">1</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> interface <span class="title class_">IThemeProps</span> &#123;</span><br><span class="line">  <span class="comment">/** 主题变量 */</span></span><br><span class="line">  <span class="attr">theme</span>: &#123;</span><br><span class="line">    <span class="comment">/** 主题颜色 */</span></span><br><span class="line">    <span class="attr">colors</span>: <span class="keyword">typeof</span> lightTheme</span><br><span class="line">    <span class="comment">/** 0绿涨红跌 1 红涨绿跌 */</span></span><br><span class="line">    <span class="attr">direction</span>: <span class="title class_">IDirection</span></span><br><span class="line">    <span class="comment">/** 涨 颜色 */</span></span><br><span class="line">    <span class="attr">up</span>: string</span><br><span class="line">    <span class="comment">/** 跌 颜色 */</span></span><br><span class="line">    <span class="attr">down</span>: string</span><br><span class="line">    <span class="comment">/** 用戶配置的主题模式 */</span></span><br><span class="line">    <span class="attr">mode</span>: <span class="title class_">RnColorScheme</span></span><br><span class="line">    <span class="comment">/** 是否是黑色主题 */</span></span><br><span class="line">    <span class="attr">isDark</span>: boolean</span><br><span class="line">    <span class="comment">/** 實際在用的主题模式 实际在用的主题模式: mode ?? useColorScheme() */</span></span><br><span class="line">    <span class="attr">colorScheme</span>: <span class="title class_">RnColorScheme</span></span><br><span class="line">  &#125;</span><br><span class="line">  <span class="comment">/** 设置主题 */</span></span><br><span class="line">  <span class="attr">setMode</span>: <span class="function">(<span class="params">key: RnColorScheme</span>) =&gt;</span> <span class="keyword">void</span></span><br><span class="line">  <span class="comment">/** 切换主题 */</span></span><br><span class="line">  <span class="attr">toggleTheme</span>: <span class="function">() =&gt;</span> <span class="keyword">void</span></span><br><span class="line">  <span class="attr">cn</span>: <span class="function">(<span class="params">...inputs: ClassInput[]</span>) =&gt;</span> any</span><br><span class="line">  <span class="comment">/** 设置绿涨红跌、红涨绿跌 */</span></span><br><span class="line">  <span class="attr">setDirection</span>: <span class="function">(<span class="params">key: IDirection</span>) =&gt;</span> <span class="keyword">void</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 主题上下文</span></span><br><span class="line"><span class="keyword">const</span> <span class="title class_">ThemeContext</span> = createContext&lt;<span class="title class_">IThemeProps</span>&gt;(&#123;&#125; <span class="keyword">as</span> <span class="title class_">IThemeProps</span>)</span><br><span class="line"></span><br><span class="line">interface iProps &#123;</span><br><span class="line">  <span class="attr">children</span>: <span class="title class_">React</span>.<span class="property">ReactNode</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> <span class="title function_">ThemeProvider</span> = (<span class="params">&#123; children &#125;: iProps</span>) =&gt; &#123;</span><br><span class="line">  <span class="comment">// 当前主题</span></span><br><span class="line">  <span class="keyword">const</span> [mode, setMode] = <span class="title function_">useAsyncStorage</span>(<span class="variable constant_">KEY_THEME</span>, <span class="string">&#x27;light&#x27;</span>) <span class="keyword">as</span> [<span class="title class_">RnColorScheme</span>, <span class="title class_">React</span>.<span class="property">Dispatch</span>&lt;<span class="title class_">React</span>.<span class="property">SetStateAction</span>&lt;<span class="title class_">RnColorScheme</span>&gt;&gt;]</span><br><span class="line">  <span class="keyword">const</span> [direction, setDirection] = <span class="title function_">useAsyncStorage</span>(<span class="variable constant_">KEY_DIRECTION</span>, <span class="number">0</span>) <span class="keyword">as</span> [<span class="title class_">IDirection</span>, <span class="title class_">React</span>.<span class="property">Dispatch</span>&lt;<span class="title class_">React</span>.<span class="property">SetStateAction</span>&lt;<span class="title class_">IDirection</span>&gt;&gt;]</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 要启用需要运行时设备数据的前缀，例如暗模式和屏幕尺寸断点等，您需要将 tw 函数与设备上下文信息的动态源连接。该库导出一个名为 useDeviceContext 的 React hook 来为您处理这个问题。它应该包含在组件层次结构的根部一次</span></span><br><span class="line">  <span class="comment">// &lt;View style=&#123;tw`bg-white dark:bg-black`&#125;&gt;</span></span><br><span class="line">  <span class="comment">// 默认情况下，如果您如上所述使用 useDeviceContext() ，您的应用程序将响应设备配色方案中的环境变化（在系统首选项中设置）。如果您希望通过某些应用内机制显式控制应用的配色方案，则需要稍微不同的配置：</span></span><br><span class="line">  <span class="title function_">useDeviceContext</span>(tw, &#123;</span><br><span class="line">    <span class="comment">// 1️⃣  opt OUT of listening to DEVICE color scheme events</span></span><br><span class="line">    <span class="attr">observeDeviceColorSchemeChanges</span>: <span class="literal">false</span>,</span><br><span class="line">    <span class="comment">// 2️⃣  and supply an initial color scheme</span></span><br><span class="line">    <span class="attr">initialColorScheme</span>: <span class="string">&#x27;light&#x27;</span> <span class="comment">// &#x27;light&#x27; | &#x27;dark&#x27; | &#x27;device&#x27;</span></span><br><span class="line">  &#125;)</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 3️⃣  use the `useAppColorScheme` hook anywhere to get a reference to the current</span></span><br><span class="line">  <span class="comment">// colorscheme, with functions to modify it (triggering re-renders) when you need</span></span><br><span class="line">  <span class="keyword">const</span> [colorScheme, toggleColorScheme, setColorScheme] = <span class="title function_">useAppColorScheme</span>(tw)</span><br><span class="line"></span><br><span class="line">  <span class="comment">//  4️⃣ use one of the setter functions, like `toggleColorScheme` in your app</span></span><br><span class="line">  <span class="comment">// &lt;TouchableOpacity onPress=&#123;toggleColorScheme&#125;&gt;</span></span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> theme = <span class="title function_">useMemo</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">const</span> colors = colorScheme === <span class="string">&#x27;dark&#x27;</span> ? darkTheme : lightTheme</span><br><span class="line">    <span class="keyword">return</span> &#123;</span><br><span class="line">      colors, <span class="comment">// 全部动态主题颜色，根据colorScheme变化</span></span><br><span class="line">      direction, <span class="comment">// 0绿涨红跌 1红涨绿跌</span></span><br><span class="line">      <span class="attr">up</span>: direction === <span class="number">1</span> ? colors.<span class="property">red</span>.<span class="property">DEFAULT</span> : colors.<span class="property">green</span>.<span class="property">DEFAULT</span>, <span class="comment">// 涨的颜色</span></span><br><span class="line">      <span class="attr">down</span>: direction === <span class="number">1</span> ? colors.<span class="property">green</span>.<span class="property">DEFAULT</span> : colors.<span class="property">red</span>.<span class="property">DEFAULT</span>, <span class="comment">// 跌的颜色</span></span><br><span class="line">      mode, <span class="comment">// 主题模式</span></span><br><span class="line">      <span class="attr">isDark</span>: mode === <span class="string">&#x27;dark&#x27;</span>, <span class="comment">// 是否暗色模式</span></span><br><span class="line">      colorScheme</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;, [direction, mode, colorScheme])</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> <span class="title function_">setTheme</span> = (<span class="params">mode: RnColorScheme</span>) =&gt; &#123;</span><br><span class="line">    <span class="title function_">setMode</span>(mode)</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> nativeColorScheme = <span class="title function_">useColorScheme</span>()</span><br><span class="line">  <span class="title function_">useEffect</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">    <span class="title function_">setColorScheme</span>(mode ?? nativeColorScheme)</span><br><span class="line">  &#125;, [nativeColorScheme, mode])</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 动态设置tailwindcss主题变量</span></span><br><span class="line">  <span class="keyword">const</span> cn = <span class="title function_">useCallback</span>(</span><br><span class="line">    <span class="function">(<span class="params">...inputs: ClassInput[]</span>) =&gt;</span> &#123;</span><br><span class="line">      <span class="keyword">return</span> tw.<span class="title function_">style</span>(...inputs)</span><br><span class="line">    &#125;,</span><br><span class="line">    [colorScheme, tw, theme]</span><br><span class="line">  )</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> values = &#123;</span><br><span class="line">    <span class="comment">/** 当前主题的所有配置项 */</span></span><br><span class="line">    theme,</span><br><span class="line">    cn,</span><br><span class="line">    <span class="attr">toggleTheme</span>: <span class="function">() =&gt;</span> &#123;</span><br><span class="line">      <span class="title function_">setTheme</span>(mode === <span class="string">&#x27;light&#x27;</span> ? <span class="string">&#x27;dark&#x27;</span> : mode === <span class="string">&#x27;dark&#x27;</span> ? <span class="literal">null</span> : <span class="string">&#x27;light&#x27;</span>)</span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="comment">/** 主题模式 */</span></span><br><span class="line">    <span class="attr">setMode</span>: <span class="function">(<span class="params">mode: RnColorScheme</span>) =&gt;</span> &#123;</span><br><span class="line">      <span class="title function_">setTheme</span>(mode)</span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="comment">/** 红涨绿跌方向 */</span></span><br><span class="line">    <span class="attr">setDirection</span>: <span class="function">(<span class="params">direction: IDirection</span>) =&gt;</span> &#123;</span><br><span class="line">      <span class="title function_">setDirection</span>(direction)</span><br><span class="line">    &#125;</span><br><span class="line">  &#125; <span class="keyword">as</span> <span class="title class_">IThemeProps</span></span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> <span class="language-xml"><span class="tag">&lt;<span class="name">ThemeContext.Provider</span> <span class="attr">value</span>=<span class="string">&#123;values&#125;</span>&gt;</span>&#123;children&#125;<span class="tag">&lt;/<span class="name">ThemeContext.Provider</span>&gt;</span></span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 获取主题</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> <span class="title function_">useTheme</span> = (<span class="params"></span>) =&gt; <span class="title function_">useContext</span>(<span class="title class_">ThemeContext</span>)</span><br></pre></td></tr></table></figure><h3 id="自定义主题颜色"><a href="#自定义主题颜色" class="headerlink" title="自定义主题颜色"></a>自定义主题颜色</h3><p>更强大的是，我们可以自定义主题配置。创建一个<code>tailwind.config.js</code>：</p><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="comment">// tailwind.config.js</span></span><br><span class="line"><span class="variable language_">module</span>.<span class="property">exports</span> = &#123;</span><br><span class="line">  <span class="attr">theme</span>: &#123;</span><br><span class="line">    <span class="attr">extend</span>: &#123;</span><br><span class="line">      <span class="attr">colors</span>: &#123;</span><br><span class="line">        <span class="attr">primary</span>: &#123;</span><br><span class="line">          <span class="attr">light</span>: <span class="string">&#x27;#60a5fa&#x27;</span>,</span><br><span class="line">          <span class="attr">DEFAULT</span>: <span class="string">&#x27;#3b82f6&#x27;</span>,</span><br><span class="line">          <span class="attr">dark</span>: <span class="string">&#x27;#2563eb&#x27;</span>,</span><br><span class="line">        &#125;,</span><br><span class="line">        <span class="attr">secondary</span>: &#123;</span><br><span class="line">          <span class="attr">light</span>: <span class="string">&#x27;#a78bfa&#x27;</span>,</span><br><span class="line">          <span class="attr">DEFAULT</span>: <span class="string">&#x27;#8b5cf6&#x27;</span>,</span><br><span class="line">          <span class="attr">dark</span>: <span class="string">&#x27;#7c3aed&#x27;</span>,</span><br><span class="line">        &#125;,</span><br><span class="line">        <span class="attr">background</span>: &#123;</span><br><span class="line">          <span class="attr">light</span>: <span class="string">&#x27;#ffffff&#x27;</span>,</span><br><span class="line">          <span class="attr">dark</span>: <span class="string">&#x27;#1a1a1a&#x27;</span>,</span><br><span class="line">        &#125;,</span><br><span class="line">        <span class="attr">text</span>: &#123;</span><br><span class="line">          <span class="attr">light</span>: <span class="string">&#x27;#1f2937&#x27;</span>,</span><br><span class="line">          <span class="attr">dark</span>: <span class="string">&#x27;#f9fafb&#x27;</span>,</span><br><span class="line">        &#125;,</span><br><span class="line">      &#125;,</span><br><span class="line">    &#125;,</span><br><span class="line">  &#125;,</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>然后在代码中使用：</p><figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="comment">// libs/tailwind.ts</span></span><br><span class="line"><span class="keyword">import</span> &#123; create &#125; <span class="keyword">from</span> <span class="string">&#x27;twrnc&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 加载自定义配置</span></span><br><span class="line"><span class="keyword">const</span> tw = <span class="title function_">create</span>(<span class="built_in">require</span>(<span class="string">`../../tailwind.config.js`</span>)) <span class="comment">// &lt;- your path may differ</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// ... and then this becomes the main function your app uses</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> tw</span><br></pre></td></tr></table></figure><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> tw <span class="keyword">from</span> <span class="string">&#x27;@/libs/tailwind&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">CustomThemedButton</span>(<span class="params">&#123; text &#125;</span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">TouchableOpacity</span> <span class="attr">style</span>=<span class="string">&#123;tw</span>`</span></span></span><br><span class="line"><span class="tag"><span class="language-xml">      <span class="attr">bg-primary</span> <span class="attr">dark:bg-primary-dark</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">      <span class="attr">px-6</span> <span class="attr">py-3</span> <span class="attr">rounded-full</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">    `&#125;&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">Text</span> <span class="attr">style</span>=<span class="string">&#123;tw</span>`</span></span></span><br><span class="line"><span class="tag"><span class="language-xml">        <span class="attr">text-white</span> <span class="attr">dark:text-gray-100</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">        <span class="attr">font-bold</span> <span class="attr">text-center</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">      `&#125;&gt;</span></span></span><br><span class="line"><span class="language-xml">        &#123;text&#125;</span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;/<span class="name">Text</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">TouchableOpacity</span>&gt;</span></span></span><br><span class="line">  );</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="实现多主题切换"><a href="#实现多主题切换" class="headerlink" title="实现多主题切换"></a>实现多主题切换</h3><p>如果要支持用户自选主题（不只是深浅色），可以结合Context实现：</p><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">React</span>, &#123; createContext, useContext, useState &#125; <span class="keyword">from</span> <span class="string">&#x27;react&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> tw <span class="keyword">from</span> <span class="string">&#x27;twrnc&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="title class_">ThemeContext</span> = <span class="title function_">createContext</span>();</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> themes = &#123;</span><br><span class="line">  <span class="attr">blue</span>: &#123;</span><br><span class="line">    <span class="attr">primary</span>: <span class="string">&#x27;#3b82f6&#x27;</span>,</span><br><span class="line">    <span class="attr">secondary</span>: <span class="string">&#x27;#8b5cf6&#x27;</span>,</span><br><span class="line">    <span class="attr">accent</span>: <span class="string">&#x27;#06b6d4&#x27;</span>,</span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="attr">green</span>: &#123;</span><br><span class="line">    <span class="attr">primary</span>: <span class="string">&#x27;#10b981&#x27;</span>,</span><br><span class="line">    <span class="attr">secondary</span>: <span class="string">&#x27;#059669&#x27;</span>,</span><br><span class="line">    <span class="attr">accent</span>: <span class="string">&#x27;#14b8a6&#x27;</span>,</span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="attr">purple</span>: &#123;</span><br><span class="line">    <span class="attr">primary</span>: <span class="string">&#x27;#a855f7&#x27;</span>,</span><br><span class="line">    <span class="attr">secondary</span>: <span class="string">&#x27;#9333ea&#x27;</span>,</span><br><span class="line">    <span class="attr">accent</span>: <span class="string">&#x27;#c084fc&#x27;</span>,</span><br><span class="line">  &#125;,</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">function</span> <span class="title function_">ThemeProvider</span>(<span class="params">&#123; children &#125;</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> [currentTheme, setCurrentTheme] = <span class="title function_">useState</span>(<span class="string">&#x27;blue&#x27;</span>);</span><br><span class="line">  </span><br><span class="line">  <span class="keyword">const</span> theme = themes[currentTheme];</span><br><span class="line">  </span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">ThemeContext.Provider</span> <span class="attr">value</span>=<span class="string">&#123;&#123;</span> <span class="attr">theme</span>, <span class="attr">setCurrentTheme</span>, <span class="attr">currentTheme</span> &#125;&#125;&gt;</span></span></span><br><span class="line"><span class="language-xml">      &#123;children&#125;</span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">ThemeContext.Provider</span>&gt;</span></span></span><br><span class="line">  );</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">function</span> <span class="title function_">useTheme</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="title function_">useContext</span>(<span class="title class_">ThemeContext</span>);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 使用</span></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">ThemedComponent</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> &#123; theme, setCurrentTheme, currentTheme &#125; = <span class="title function_">useTheme</span>();</span><br><span class="line">  </span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">View</span> <span class="attr">style</span>=<span class="string">&#123;tw</span>`<span class="attr">flex-1</span> <span class="attr">p-4</span>`&#125;&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">View</span> <span class="attr">style</span>=<span class="string">&#123;[tw</span>`<span class="attr">p-4</span> <span class="attr">rounded-lg</span>`, &#123; <span class="attr">backgroundColor:</span> <span class="attr">theme.primary</span> &#125;]&#125;&gt;</span></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">Text</span> <span class="attr">style</span>=<span class="string">&#123;tw</span>`<span class="attr">text-white</span> <span class="attr">text-lg</span> <span class="attr">font-bold</span>`&#125;&gt;</span></span></span><br><span class="line"><span class="language-xml">          当前主题：&#123;currentTheme&#125;</span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;/<span class="name">Text</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;/<span class="name">View</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      </span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">View</span> <span class="attr">style</span>=<span class="string">&#123;tw</span>`<span class="attr">flex-row</span> <span class="attr">gap-2</span> <span class="attr">mt-4</span>`&#125;&gt;</span></span></span><br><span class="line"><span class="language-xml">        &#123;Object.keys(themes).map(themeName =&gt; (</span></span><br><span class="line"><span class="language-xml">          <span class="tag">&lt;<span class="name">TouchableOpacity</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">            <span class="attr">key</span>=<span class="string">&#123;themeName&#125;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">            <span class="attr">style</span>=<span class="string">&#123;[</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">              <span class="attr">tw</span>`<span class="attr">px-4</span> <span class="attr">py-2</span> <span class="attr">rounded-full</span>`,</span></span></span><br><span class="line"><span class="tag"><span class="language-xml">              &#123; <span class="attr">backgroundColor:</span> <span class="attr">themes</span>[<span class="attr">themeName</span>]<span class="attr">.primary</span> &#125;</span></span></span><br><span class="line"><span class="tag"><span class="language-xml">            ]&#125;</span></span></span><br><span class="line"><span class="tag"><span class="language-xml">            <span class="attr">onPress</span>=<span class="string">&#123;()</span> =&gt;</span> setCurrentTheme(themeName)&#125;</span></span><br><span class="line"><span class="language-xml">          &gt;</span></span><br><span class="line"><span class="language-xml">            <span class="tag">&lt;<span class="name">Text</span> <span class="attr">style</span>=<span class="string">&#123;tw</span>`<span class="attr">text-white</span>`&#125;&gt;</span>&#123;themeName&#125;<span class="tag">&lt;/<span class="name">Text</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">          <span class="tag">&lt;/<span class="name">TouchableOpacity</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">        ))&#125;</span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;/<span class="name">View</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">View</span>&gt;</span></span></span><br><span class="line">  );</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这样就实现了灵活的多主题切换，而且代码结构清晰，维护起来也方便。</p><h2 id="深入浅出：twrnc的核心原理"><a href="#深入浅出：twrnc的核心原理" class="headerlink" title="深入浅出：twrnc的核心原理"></a>深入浅出：twrnc的核心原理</h2><p>讲了这么多用法，咱们来聊聊twrnc到底是怎么工作的。理解原理之后，用起来会更有底气。</p><h3 id="Tailwind类名到RN样式的转换"><a href="#Tailwind类名到RN样式的转换" class="headerlink" title="Tailwind类名到RN样式的转换"></a>Tailwind类名到RN样式的转换</h3><p>twrnc的核心任务就是把Tailwind的类名转成React Native能理解的样式对象。比如：</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">&#x27;bg-blue-500 p-4 rounded-lg&#x27;</span><br></pre></td></tr></table></figure><p>要转换成：</p><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">backgroundColor</span>: <span class="string">&#x27;#3b82f6&#x27;</span>,</span><br><span class="line">  <span class="attr">padding</span>: <span class="number">16</span>,</span><br><span class="line">  <span class="attr">borderRadius</span>: <span class="number">8</span>,</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这个转换过程是怎么实现的呢？</p><p><strong>第一步：解析类名</strong></p><p><code>twrnc</code>会把字符串按空格分割，得到一个类名数组：</p><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="keyword">const</span> classNames = <span class="string">&#x27;bg-blue-500 p-4 rounded-lg&#x27;</span>.<span class="title function_">split</span>(<span class="regexp">/\s+/</span>);</span><br><span class="line"><span class="comment">// [&#x27;bg-blue-500&#x27;, &#x27;p-4&#x27;, &#x27;rounded-lg&#x27;]</span></span><br></pre></td></tr></table></figure><p><strong>第二步：查表转换</strong></p><p>twrnc内部维护了一个映射表，把每个Tailwind类对应到RN的样式：</p><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="keyword">const</span> styleMap = &#123;</span><br><span class="line">  <span class="string">&#x27;bg-blue-500&#x27;</span>: &#123; <span class="attr">backgroundColor</span>: <span class="string">&#x27;#3b82f6&#x27;</span> &#125;,</span><br><span class="line">  <span class="string">&#x27;p-4&#x27;</span>: &#123; <span class="attr">padding</span>: <span class="number">16</span> &#125;,</span><br><span class="line">  <span class="string">&#x27;rounded-lg&#x27;</span>: &#123; <span class="attr">borderRadius</span>: <span class="number">8</span> &#125;,</span><br><span class="line">  <span class="comment">// 还有几千个...</span></span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>实际上这个映射表是动态生成的，不是写死的。twrnc会根据类名的模式来计算对应的样式值。</p><p><strong>第三步：合并样式</strong></p><p>最后把所有样式对象合并成一个：</p><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="keyword">const</span> finalStyle = <span class="title class_">Object</span>.<span class="title function_">assign</span>(</span><br><span class="line">  &#123;&#125;,</span><br><span class="line">  styleMap[<span class="string">&#x27;bg-blue-500&#x27;</span>],</span><br><span class="line">  styleMap[<span class="string">&#x27;p-4&#x27;</span>],</span><br><span class="line">  styleMap[<span class="string">&#x27;rounded-lg&#x27;</span>]</span><br><span class="line">);</span><br></pre></td></tr></table></figure><h3 id="样式缓存机制"><a href="#样式缓存机制" class="headerlink" title="样式缓存机制"></a>样式缓存机制</h3><p>你可能会担心：每次渲染都要解析类名，性能会不会有问题？</p><p><code>twrnc</code>的作者想到了这一点，内部实现了缓存机制。第一次遇到某个类名组合时，会解析并缓存结果。后续再遇到相同的类名，直接返回缓存的样式对象。</p><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="comment">// 简化版的缓存逻辑</span></span><br><span class="line"><span class="keyword">const</span> cache = <span class="keyword">new</span> <span class="title class_">Map</span>();</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">tw</span>(<span class="params">classNames</span>) &#123;</span><br><span class="line">  <span class="comment">// 检查缓存</span></span><br><span class="line">  <span class="keyword">if</span> (cache.<span class="title function_">has</span>(classNames)) &#123;</span><br><span class="line">    <span class="keyword">return</span> cache.<span class="title function_">get</span>(classNames);</span><br><span class="line">  &#125;</span><br><span class="line">  </span><br><span class="line">  <span class="comment">// 解析样式</span></span><br><span class="line">  <span class="keyword">const</span> style = <span class="title function_">parseClassNames</span>(classNames);</span><br><span class="line">  </span><br><span class="line">  <span class="comment">// 存入缓存</span></span><br><span class="line">  cache.<span class="title function_">set</span>(classNames, style);</span><br><span class="line">  </span><br><span class="line">  <span class="keyword">return</span> style;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这样的设计保证了性能。即使在列表中渲染几百个item，每个item使用相同的类名，也只需要解析一次。</p><h3 id="响应式和条件样式的处理"><a href="#响应式和条件样式的处理" class="headerlink" title="响应式和条件样式的处理"></a>响应式和条件样式的处理</h3><p>对于条件样式，比如：</p><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line">tw<span class="string">`bg-white <span class="subst">$&#123;isActive ? <span class="string">&#x27;bg-blue-500&#x27;</span> : <span class="string">&#x27;bg-gray-200&#x27;</span>&#125;</span>`</span></span><br></pre></td></tr></table></figure><p>twrnc会先计算出完整的类名字符串，然后再解析。这是利用了JavaScript模板字符串的特性。</p><p>但这里有个小细节：因为条件可能变化，所以这类样式的缓存key会包含动态部分。twrnc会智能地判断哪些部分是静态的（可以缓存），哪些是动态的（需要重新计算）。</p><h3 id="自定义配置的实现"><a href="#自定义配置的实现" class="headerlink" title="自定义配置的实现"></a>自定义配置的实现</h3><p>当你提供<code>tailwind.config.js</code>时，twrnc会读取这个配置并生成对应的映射表。比如你定义了：</p><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="attr">colors</span>: &#123;</span><br><span class="line">  <span class="attr">brand</span>: <span class="string">&#x27;#ff6b6b&#x27;</span>,</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>twrnc会自动生成：</p><ul><li><code>bg-brand</code>: <code>&#123; backgroundColor: &#39;#ff6b6b&#39; &#125;</code></li><li><code>text-brand</code>: <code>&#123; color: &#39;#ff6b6b&#39; &#125;</code></li><li><code>border-brand</code>: <code>&#123; borderColor: &#39;#ff6b6b&#39; &#125;</code></li></ul><p>甚至还会生成不同透明度的变体：</p><ul><li><code>bg-brand/50</code>: <code>&#123; backgroundColor: &#39;rgba(255, 107, 107, 0.5)&#39; &#125;</code></li></ul><p>这些都是在库初始化时预计算好的，不会影响运行时性能。</p><h3 id="单位转换的小秘密"><a href="#单位转换的小秘密" class="headerlink" title="单位转换的小秘密"></a>单位转换的小秘密</h3><p>Web端的Tailwind使用rem和px，但React Native只支持数字（代表dp&#x2F;pt）。twrnc是怎么处理的？</p><p>它有一套单位转换规则：</p><ul><li><code>p-4</code>：padding为 4 * 4 &#x3D; 16（默认1单位&#x3D;4dp）</li><li><code>text-base</code>：fontSize为16</li><li><code>w-1/2</code>：width为’50%’</li></ul><p>你也可以自定义这个比例：</p><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line">tw.<span class="property">config</span> = &#123;</span><br><span class="line">  <span class="attr">theme</span>: &#123;</span><br><span class="line">    <span class="attr">spacing</span>: &#123;</span><br><span class="line">      <span class="number">1</span>: <span class="number">8</span>,  <span class="comment">// 现在1单位=8dp</span></span><br><span class="line">      <span class="number">2</span>: <span class="number">16</span>,</span><br><span class="line">      <span class="comment">// ...</span></span><br><span class="line">    &#125;,</span><br><span class="line">  &#125;,</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><h2 id="最佳实践和踩坑指南"><a href="#最佳实践和踩坑指南" class="headerlink" title="最佳实践和踩坑指南"></a>最佳实践和踩坑指南</h2><p>用了一段时间twrnc，也踩了不少坑。分享一些经验。</p><h3 id="保持类名简洁"><a href="#保持类名简洁" class="headerlink" title="保持类名简洁"></a>保持类名简洁</h3><p>虽然Tailwind让我们可以在标签里写很多类，但不要滥用。如果一个组件的类名超过10个，就该考虑拆分组件或者提取样式了。</p><p><strong>不好的做法：</strong></p><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line">&lt;<span class="title class_">View</span> style=&#123;tw<span class="string">`flex-row items-center justify-between bg-white p-4 mx-4 my-2 rounded-xl shadow-lg border border-gray-200 w-full`</span>&#125;&gt;</span><br></pre></td></tr></table></figure><p><strong>好的做法：</strong></p><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="keyword">const</span> cardStyle = tw<span class="string">`flex-row items-center justify-between bg-white p-4 mx-4 my-2 rounded-xl shadow-lg border border-gray-200`</span>;</span><br><span class="line"></span><br><span class="line"><span class="language-xml"><span class="tag">&lt;<span class="name">View</span> <span class="attr">style</span>=<span class="string">&#123;cardStyle&#125;</span>&gt;</span></span></span><br></pre></td></tr></table></figure><p>或者更好的，拆分成子组件。</p><h3 id="性能优化技巧"><a href="#性能优化技巧" class="headerlink" title="性能优化技巧"></a>性能优化技巧</h3><p>虽然twrnc有缓存，但在长列表中还是要注意：</p><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="comment">// 不好：每次渲染都创建新对象</span></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">ListItem</span>(<span class="params">&#123; item &#125;</span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">View</span> <span class="attr">style</span>=<span class="string">&#123;tw</span>`<span class="attr">p-4</span> $&#123;<span class="attr">item.isActive</span> ? &#x27;<span class="attr">bg-blue-500</span>&#x27; <span class="attr">:</span> &#x27;<span class="attr">bg-white</span>&#x27;&#125;`&#125;&gt;</span></span></span><br><span class="line"><span class="language-xml">      &#123;/* ... */&#125;</span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">View</span>&gt;</span></span></span><br><span class="line">  );</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 好：预定义样式</span></span><br><span class="line"><span class="keyword">const</span> baseStyle = tw<span class="string">`p-4`</span>;</span><br><span class="line"><span class="keyword">const</span> activeStyle = tw<span class="string">`p-4 bg-blue-500`</span>;</span><br><span class="line"><span class="keyword">const</span> inactiveStyle = tw<span class="string">`p-4 bg-white`</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">ListItem</span>(<span class="params">&#123; item &#125;</span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">View</span> <span class="attr">style</span>=<span class="string">&#123;item.isActive</span> ? <span class="attr">activeStyle</span> <span class="attr">:</span> <span class="attr">inactiveStyle</span>&#125;&gt;</span></span></span><br><span class="line"><span class="language-xml">      &#123;/* ... */&#125;</span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">View</span>&gt;</span></span></span><br><span class="line">  );</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="与第三方库的配合"><a href="#与第三方库的配合" class="headerlink" title="与第三方库的配合"></a>与第三方库的配合</h3><p>有些第三方库（比如react-native-paper）有自己的样式系统。可以混用：</p><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">Button</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;react-native-paper&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> tw <span class="keyword">from</span> <span class="string">&#x27;twrnc&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="language-xml"><span class="tag">&lt;<span class="name">Button</span> </span></span></span><br><span class="line"><span class="tag"><span class="language-xml">  <span class="attr">mode</span>=<span class="string">&quot;contained&quot;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">  <span class="attr">style</span>=<span class="string">&#123;tw</span>`<span class="attr">mt-4</span>`&#125;  // <span class="attr">twrnc处理外边距</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">  <span class="attr">contentStyle</span>=<span class="string">&#123;tw</span>`<span class="attr">py-2</span>`&#125;  // <span class="attr">内部样式</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">&gt;</span></span></span><br><span class="line"><span class="language-xml">  点击我</span></span><br><span class="line"><span class="language-xml"><span class="tag">&lt;/<span class="name">Button</span>&gt;</span></span></span><br></pre></td></tr></table></figure><h3 id="类型安全（如果用TypeScript）"><a href="#类型安全（如果用TypeScript）" class="headerlink" title="类型安全（如果用TypeScript）"></a>类型安全（如果用TypeScript）</h3><p>twrnc支持TypeScript，但智能提示有限。可以结合一些类型定义来增强体验：</p><figure class="highlight typescript"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> tw <span class="keyword">from</span> <span class="string">&#x27;twrnc&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> <span class="title class_">TailwindStyle</span> = <span class="title class_">ReturnType</span>&lt;<span class="keyword">typeof</span> tw&gt;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">interface</span> <span class="title class_">CardProps</span> &#123;</span><br><span class="line">  <span class="attr">style</span>?: <span class="title class_">TailwindStyle</span>;</span><br><span class="line">  <span class="attr">children</span>: <span class="title class_">React</span>.<span class="property">ReactNode</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">Card</span>(<span class="params">&#123; style, children &#125;: <span class="title class_">CardProps</span></span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">View</span> <span class="attr">style</span>=<span class="string">&#123;[tw</span>`<span class="attr">bg-white</span> <span class="attr">p-4</span> <span class="attr">rounded-lg</span>`, <span class="attr">style</span>]&#125;&gt;</span></span></span><br><span class="line"><span class="language-xml">      &#123;children&#125;</span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">View</span>&gt;</span></span></span><br><span class="line">  );</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="总结：开发效率的质变"><a href="#总结：开发效率的质变" class="headerlink" title="总结：开发效率的质变"></a>总结：开发效率的质变</h2><p>回顾这一路，从传统的StyleSheet到Tailwind+twrnc，这不仅仅是工具的升级，更是开发思维的转变。</p><h3 id="我们获得了什么"><a href="#我们获得了什么" class="headerlink" title="我们获得了什么"></a>我们获得了什么</h3><p><strong>1. 开发速度的提升</strong><br>不用再纠结命名，不用来回跳转文件，组件和样式融为一体。原来要半小时的界面，现在10分钟就能搞定。而且改起来更快，不用担心牵一发动全身。</p><p><strong>2. 代码质量的改善</strong><br>样式一致性自然就有了，因为大家用的是同一套工具类。团队新人上手也快，看看别人怎么写，照着写就行。代码审查的时候，样式问题也少了很多。</p><p><strong>3. 维护成本的降低</strong><br>想改个颜色？搜索替换就行。想调整间距？批量改类名。主题切换？加个<code>dark:</code>前缀就完事。再也不用在上千行的StyleSheet里找bug了。</p><h3 id="还有哪些局限"><a href="#还有哪些局限" class="headerlink" title="还有哪些局限"></a>还有哪些局限</h3><p>当然，twrnc不是银弹，也有一些局限：</p><ul><li><strong>学习成本</strong>：团队成员需要熟悉Tailwind的类名，刚开始可能要查文档</li><li><strong>样式复杂度</strong>：某些特别复杂的样式（比如复杂动画），还是得用StyleSheet</li><li><strong>包体积</strong>：虽然不大，但毕竟是额外的依赖</li></ul><p>但这些问题相比带来的好处，都可以接受。</p><h3 id="给初学者的建议"><a href="#给初学者的建议" class="headerlink" title="给初学者的建议"></a>给初学者的建议</h3><p>如果你还在犹豫要不要尝试，我的建议是：</p><ol><li><strong>新项目直接上</strong>：别犹豫，直接用twrnc开发，你会爱上这种感觉</li><li><strong>老项目渐进式迁移</strong>：别一口气重写，从新页面开始，慢慢替换</li><li><strong>保持开放心态</strong>：一开始可能不习惯，但坚持两周，你就回不去了</li></ol><h3 id="最后"><a href="#最后" class="headerlink" title="最后"></a>最后</h3><p>写样式本应该是一件快乐的事，而不是折磨。Tailwind CSS和twrnc让我们重新找回了这种快乐。不用为命名发愁，不用为主题切换头疼，不用为团队协作吵架。</p><p>2025年了，是时候告别那些让我们抓狂的样式写法了。用twrnc，让代码更简洁，让开发更高效，让自己更快乐。</p><p>最重要的是，省下的时间可以多摸会儿鱼，多喝几杯咖啡，多想想产品经理下一个需求该怎么怼回去（误）。</p><p>好了，我要去把手上的项目重构一遍了。各位，代码见！</p>]]>
    </content>
    <id>http://fe.poetries.top/2026/01/02/tailwind-react-native-guide/</id>
    <link href="http://fe.poetries.top/2026/01/02/tailwind-react-native-guide/"/>
    <published>2026-01-02T06:40:12.000Z</published>
    <summary>
      <![CDATA[<p>最近在公司接手了一个React Native项目，打开代码的那一刻我整个人都不好了。上千行的StyleSheet，命名从<code>container1</code>到<code>container27</code>，找个样式比找对象还难。更要命的是，改个颜色要翻三个文件，]]>
    </summary>
    <title>告别样式混乱-用Tailwind CSS重塑React Native开发效率</title>
    <updated>2026-03-08T10:22:42.100Z</updated>
  </entry>
  <entry>
    <author>
      <name>Poetry</name>
    </author>
    <category term="Front-End" scheme="http://fe.poetries.top/categories/Front-End/"/>
    <category term="Docker" scheme="http://fe.poetries.top/tags/Docker/"/>
    <content>
      <![CDATA[<blockquote><p>本文记录了使用ecosystem部署应用，并且处理因PM2日志文件过大导致服务器磁盘空间耗尽的问题解决过程。通过安装并配置pm2-logrotate插件实现了日志文件的自动化管理，确保了服务器的稳定运行。</p></blockquote><h2 id="ecosystem配置"><a href="#ecosystem配置" class="headerlink" title="ecosystem配置"></a>ecosystem配置</h2><figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="comment">// 在nodejs/nextjs项目根目录创建 ecosystem.config.js</span></span><br><span class="line"><span class="variable language_">module</span>.<span class="property">exports</span> = &#123;</span><br><span class="line">  <span class="attr">apps</span>: [</span><br><span class="line">    &#123;</span><br><span class="line">      <span class="attr">name</span>: <span class="string">&#x27;my-app-name&#x27;</span>,</span><br><span class="line">      <span class="attr">script</span>: <span class="string">&#x27;./server.js&#x27;</span>, <span class="comment">// 服务启动脚本</span></span><br><span class="line">      <span class="attr">watch</span>: <span class="literal">false</span>, <span class="comment">// 禁用文件监视，提高性能</span></span><br><span class="line">      <span class="attr">instances</span>: <span class="number">2</span>, <span class="comment">// 指定要启动的实例数量 // 0 和 max 同义</span></span><br><span class="line">      <span class="attr">exec_mode</span>: <span class="string">&#x27;cluster&#x27;</span>, <span class="comment">// 启用集群模式，指定要启动的实例数量</span></span><br><span class="line">      <span class="comment">// 启用文件日志记录</span></span><br><span class="line">      <span class="attr">output</span>: <span class="string">&#x27;./logs/app-out.log&#x27;</span>, <span class="comment">// 标准输出日志文件</span></span><br><span class="line">      <span class="attr">error</span>: <span class="string">&#x27;./logs/app-error.log&#x27;</span>, <span class="comment">// 错误日志文件</span></span><br><span class="line">      <span class="comment">// log: &#x27;./logs/combined.log&#x27;,        // 合并日志文件（可选）</span></span><br><span class="line">      <span class="comment">// 日志配置</span></span><br><span class="line">      <span class="attr">log_date_format</span>: <span class="string">&#x27;YYYY-MM-DD HH:mm Z&#x27;</span>,</span><br><span class="line">      <span class="attr">merge_logs</span>: <span class="literal">false</span>, <span class="comment">// 为每个实例单独日志</span></span><br><span class="line">      <span class="attr">time</span>: <span class="literal">true</span>, <span class="comment">// 在日志中添加时间戳</span></span><br><span class="line">      <span class="attr">env</span>: &#123;&#125;</span><br><span class="line">    &#125;</span><br><span class="line">  ]</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>一级部署应用</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">pm2-runtime ecosystem.config.js</span><br></pre></td></tr></table></figure><h2 id="pm2日志管理logrotate"><a href="#pm2日志管理logrotate" class="headerlink" title="pm2日志管理logrotate"></a>pm2日志管理logrotate</h2><p>永久解决日志文件过大，使用日志管理用的插件 <code>pm2-logrotate</code></p><p><strong>安装插件pm2-logrotate</strong></p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">pm2 install pm2-logrotate</span><br></pre></td></tr></table></figure><p>查看日志配置</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">$ pm2 conf pm2-logrotate</span><br><span class="line"></span><br><span class="line">Module: pm2-logrotate</span><br><span class="line">$ pm2 <span class="built_in">set</span> pm2-logrotate:max_size 10M</span><br><span class="line">$ pm2 <span class="built_in">set</span> pm2-logrotate:retain 50</span><br><span class="line">$ pm2 <span class="built_in">set</span> pm2-logrotate:compress <span class="literal">false</span></span><br><span class="line">$ pm2 <span class="built_in">set</span> pm2-logrotate:dateFormat YYYY-MM-DD_HH-mm-ss</span><br><span class="line">$ pm2 <span class="built_in">set</span> pm2-logrotate:workerInterval 1</span><br><span class="line">$ pm2 <span class="built_in">set</span> pm2-logrotate:rotateInterval 0 0 * * *</span><br><span class="line">$ pm2 <span class="built_in">set</span> pm2-logrotate:rotateModule <span class="literal">true</span></span><br><span class="line">Module: module-db-v2</span><br><span class="line">$ pm2 <span class="built_in">set</span> module-db-v2:pm2-logrotate [object Object]</span><br></pre></td></tr></table></figure><p><strong>pm2-logrotate具体参数配置</strong></p><table><thead><tr><th align="left">配置项</th><th align="left">简介</th></tr></thead><tbody><tr><td align="left">Compress</td><td align="left">是否通过gzip压缩日志</td></tr><tr><td align="left">max_size</td><td align="left">单个日志文件的大小，比如上图中设置为1K（这个其实太小了，实际文件大小并不会严格分为1K）</td></tr><tr><td align="left">retain</td><td align="left">保留的日志文件个数，比如设置为10,那么在日志文件达到10个后会将最早的日志文件删除掉</td></tr><tr><td align="left">dateFormat</td><td align="left">日志文件名中的日期格式，默认是YYYY-MM-DD_HH-mm-ss，注意是设置的日志名+这个格式，如设置的日志名为abc.log，那就会生成abc_YYYY-MM-DD_HH-mm-ss.log名字的日志文件</td></tr><tr><td align="left">rotateModule</td><td align="left">把pm2本身的日志也进行分割</td></tr><tr><td align="left">workerInterval</td><td align="left">设置启动几个工作进程监控日志尺寸，最小为1</td></tr><tr><td align="left">rotateInterval</td><td align="left">设置强制分割，默认值是0 0 * * *，意思是每天晚上0点分割，这个足够了个人觉得</td></tr></tbody></table><p><strong>设置pm2-logrotat</strong></p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 设置单个文件的大小</span></span><br><span class="line">pm2 <span class="built_in">set</span> pm2-logrotate:max_size 10M</span><br><span class="line"><span class="comment"># 保留的日志文件个数，比如设置为10,那么在日志文件达到10个后会将最早的日志文件删除掉</span></span><br><span class="line">pm2 <span class="built_in">set</span> pm2-logrotate:retain 50</span><br><span class="line"><span class="comment"># dateFormat日志文件名中的日期格式，默认是YYYY-MM-DD_HH-mm-ss，注意是设置的日志名+这个格式，如设置的日志名为abc.log，那就会生成abc_YYYY-MM-DD_HH-mm-ss.log名字的日志文件</span></span><br><span class="line">pm2 <span class="built_in">set</span> pm2-logrotate:dateFormat YYYY-MM-DD_HH-mm-ss</span><br><span class="line"><span class="comment"># 是否通过gzip压缩日志</span></span><br><span class="line">pm2 <span class="built_in">set</span> pm2-logrotate:compress <span class="literal">true</span></span><br><span class="line"><span class="comment"># workerInterval 设置启动几个工作进程监控日志尺寸，最小为1</span></span><br><span class="line">pm2 <span class="built_in">set</span> pm2-logrotate:workerInterval 1</span><br><span class="line"><span class="comment"># rotateInterval设置强制分割，默认值是0 0 * * *，意思是每天晚上0点分割，这个足够了个人觉得</span></span><br><span class="line">pm2 <span class="built_in">set</span> pm2-logrotate:rotateInterval 0 0 * * *</span><br></pre></td></tr></table></figure><blockquote><p>如果想后面直接看配置，也可以通过指令<code>pm2 conf pm2-logrotate</code>来查看详细的配置</p></blockquote><h2 id="结合Dockerfile部署Nextjs"><a href="#结合Dockerfile部署Nextjs" class="headerlink" title="结合Dockerfile部署Nextjs"></a>结合Dockerfile部署Nextjs</h2><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">FROM node:22-alpine AS base</span><br><span class="line"></span><br><span class="line"><span class="comment"># Install dependencies only when needed</span></span><br><span class="line">FROM base AS deps</span><br><span class="line"><span class="comment"># Install build dependencies for native modules including USB support</span></span><br><span class="line">RUN apk add --no-cache \</span><br><span class="line">    libc6-compat \</span><br><span class="line">    python3 \</span><br><span class="line">    make \</span><br><span class="line">    g++ \</span><br><span class="line">    linux-headers \</span><br><span class="line">    eudev-dev \</span><br><span class="line">    libusb-dev</span><br><span class="line"></span><br><span class="line"><span class="comment"># 设置时区为北京时间</span></span><br><span class="line">RUN apk add --no-cache tzdata &amp;&amp; \</span><br><span class="line"><span class="built_in">cp</span> /usr/share/zoneinfo/Asia/Shanghai /etc/localtime &amp;&amp; \</span><br><span class="line"><span class="built_in">echo</span> <span class="string">&quot;Asia/Shanghai&quot;</span> &gt; /etc/timezone</span><br><span class="line"></span><br><span class="line">RUN yarn config <span class="built_in">set</span> registry https://registry.npmjs.org/</span><br><span class="line"></span><br><span class="line">WORKDIR /app</span><br><span class="line"></span><br><span class="line"><span class="comment"># Install dependencies based on the preferred package manager</span></span><br><span class="line">COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./</span><br><span class="line">RUN \</span><br><span class="line">  <span class="keyword">if</span> [ -f yarn.lock ]; <span class="keyword">then</span> yarn --frozen-lockfile; \</span><br><span class="line">  <span class="keyword">elif</span> [ -f package-lock.json ]; <span class="keyword">then</span> npm ci; \</span><br><span class="line">  <span class="keyword">elif</span> [ -f pnpm-lock.yaml ]; <span class="keyword">then</span> corepack <span class="built_in">enable</span> pnpm &amp;&amp; pnpm i --frozen-lockfile; \</span><br><span class="line">  <span class="keyword">else</span> <span class="built_in">echo</span> <span class="string">&quot;Lockfile not found.&quot;</span> &amp;&amp; <span class="built_in">exit</span> 1; \</span><br><span class="line">  <span class="keyword">fi</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># Rebuild the source code only when needed</span></span><br><span class="line">FROM base AS builder</span><br><span class="line">WORKDIR /app</span><br><span class="line">COPY --from=deps /app/node_modules ./node_modules/</span><br><span class="line">COPY . .</span><br><span class="line"></span><br><span class="line"><span class="comment"># Add memory limit and disable telemetry</span></span><br><span class="line">ENV NEXT_TELEMETRY_DISABLED=1 \</span><br><span class="line">    NODE_OPTIONS=<span class="string">&quot;--max-old-space-size=4096&quot;</span></span><br><span class="line"></span><br><span class="line">RUN \</span><br><span class="line">  <span class="keyword">if</span> [ -f yarn.lock ]; <span class="keyword">then</span> yarn build:feinterview-poetries-top; \</span><br><span class="line">  <span class="keyword">elif</span> [ -f package-lock.json ]; <span class="keyword">then</span> npm run build:feinterview-poetries-top; \</span><br><span class="line">  <span class="keyword">elif</span> [ -f pnpm-lock.yaml ]; <span class="keyword">then</span> corepack <span class="built_in">enable</span> pnpm &amp;&amp; pnpm run build:feinterview-poetries-top; \</span><br><span class="line">  <span class="keyword">else</span> <span class="built_in">echo</span> <span class="string">&quot;Lockfile not found.&quot;</span> &amp;&amp; <span class="built_in">exit</span> 1; \</span><br><span class="line">  <span class="keyword">fi</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># Production image, copy all the files and run next</span></span><br><span class="line">FROM base AS runner</span><br><span class="line">WORKDIR /app</span><br><span class="line"></span><br><span class="line">ENV NODE_ENV=production \</span><br><span class="line">    NEXT_PUBLIC_NAMESPACE=prod \</span><br><span class="line">    PORT=3000 \</span><br><span class="line">    NEXT_TELEMETRY_DISABLED=1 \</span><br><span class="line">    HOSTNAME=<span class="string">&quot;0.0.0.0&quot;</span></span><br><span class="line"></span><br><span class="line">COPY --from=builder /app/public ./public</span><br><span class="line"></span><br><span class="line">RUN <span class="built_in">mkdir</span> .next</span><br><span class="line"></span><br><span class="line"><span class="comment"># 复制构建产物</span></span><br><span class="line">COPY --from=builder /app/.next/standalone ./</span><br><span class="line">COPY --from=builder /app/.next/static ./.next/static</span><br><span class="line"><span class="comment"># 拷贝pm2部署文件</span></span><br><span class="line">COPY ecosystem.config.js ./</span><br><span class="line"></span><br><span class="line"><span class="comment"># 安装 PM2</span></span><br><span class="line">RUN npm install -g pm2</span><br><span class="line">RUN pm2 install pm2-logrotate</span><br><span class="line"></span><br><span class="line"><span class="comment"># 设置pm2-logrotate配置 后面直接看配置，也可以通过指令pm2 conf pm2-logrotate来查看详细的配置</span></span><br><span class="line"><span class="comment"># 单个日志文件的大小</span></span><br><span class="line">RUN pm2 <span class="built_in">set</span> pm2-logrotate:max_size 10M</span><br><span class="line"><span class="comment"># 保留的日志文件个数，比如设置为10,那么在日志文件达到10个后会将最早的日志文件删除掉</span></span><br><span class="line">RUN pm2 <span class="built_in">set</span> pm2-logrotate:retain 50</span><br><span class="line"><span class="comment"># dateFormat日志文件名中的日期格式，默认是YYYY-MM-DD_HH-mm-ss，注意是设置的日志名+这个格式，如设置的日志名为abc.log，那就会生成abc_YYYY-MM-DD_HH-mm-ss.log名字的日志文件</span></span><br><span class="line"><span class="comment"># RUN pm2 set pm2-logrotate:dateFormat YYYY-MM-DD_HH-mm-ss</span></span><br><span class="line"><span class="comment"># 是否通过gzip压缩日志</span></span><br><span class="line"><span class="comment"># RUN pm2 set pm2-logrotate:compress true</span></span><br><span class="line"><span class="comment"># workerInterval 设置启动几个工作进程监控日志尺寸，最小为1</span></span><br><span class="line"><span class="comment"># RUN pm2 set pm2-logrotate:workerInterval 1</span></span><br><span class="line"><span class="comment"># rotateInterval设置强制分割，默认值是0 0 * * *，意思是每天晚上0点分割，这个足够了个人觉得</span></span><br><span class="line"><span class="comment"># RUN pm2 set pm2-logrotate:rotateInterval 0 0 * * *</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 服务运行在3000端口</span></span><br><span class="line">EXPOSE 3000</span><br><span class="line"></span><br><span class="line"><span class="comment"># pm2-runtime 是 PM2 的一个命令，专门用于在生产环境中运行应用程序</span></span><br><span class="line">CMD [<span class="string">&quot;pm2-runtime&quot;</span>, <span class="string">&quot;ecosystem.config.js&quot;</span>]</span><br></pre></td></tr></table></figure><p>部署后查看日志形式以指定的方式分割存储，再也不用担心日志文件过大导致磁盘空间不足</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">/app/logs <span class="comment"># ls -lh</span></span><br><span class="line">total 240K   </span><br><span class="line">-rw-r--r--    1 root     root        2.8K Mar  1 01:53 app-error-1.log</span><br><span class="line">-rw-r--r--    1 root     root       41.5K Feb 28 00:00 app-error-1__2026-02-28_00-00-00.log</span><br><span class="line">-rw-r--r--    1 root     root       48.2K Mar  1 00:00 app-error-1__2026-03-01_00-00-00.log</span><br><span class="line">-rw-r--r--    1 root     root      113.3K Mar  1 00:43 app-error-2.log</span><br><span class="line">-rw-r--r--    1 root     root        1.3K Mar  1 02:37 app-out-1.log</span><br><span class="line">-rw-r--r--    1 root     root        1.4K Feb 28 00:00 app-out-1__2026-02-28_00-00-00.log</span><br><span class="line">-rw-r--r--    1 root     root        2.5K Mar  1 00:00 app-out-1__2026-03-01_00-00-00.log</span><br><span class="line">-rw-r--r--    1 root     root        4.4K Mar  1 02:37 app-out-2.log</span><br></pre></td></tr></table></figure><p>可以输入<code>pm2 flush</code>删除日志</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">$ pm2 flush</span><br><span class="line">[PM2] Flushing /root/.pm2/pm2.<span class="built_in">log</span></span><br><span class="line">[PM2] Flushing:</span><br><span class="line">[PM2] /root/.pm2/logs/pm2-logrotate-out.log</span><br><span class="line">[PM2] /root/.pm2/logs/pm2-logrotate-error.log</span><br><span class="line">[PM2] Flushing:</span><br><span class="line">[PM2] /app/logs/app-out-1.<span class="built_in">log</span></span><br><span class="line">[PM2] /app/logs/app-error-1.<span class="built_in">log</span></span><br><span class="line">[PM2] Flushing:</span><br><span class="line">[PM2] /app/logs/app-out-2.<span class="built_in">log</span></span><br><span class="line">[PM2] /app/logs/app-error-2.<span class="built_in">log</span></span><br><span class="line">[PM2] Logs flushed</span><br></pre></td></tr></table></figure>]]>
    </content>
    <id>http://fe.poetries.top/2025/11/12/pm2-docker-node-deploy/</id>
    <link href="http://fe.poetries.top/2025/11/12/pm2-docker-node-deploy/"/>
    <published>2025-11-12T06:40:12.000Z</published>
    <summary>
      <![CDATA[<blockquote>
<p>本文记录了使用ecosystem部署应用，并且处理因PM2日志文件过大导致服务器磁盘空间耗尽的问题解决过程。通过安装并配置pm2-logrotate插件实现了日志文件的自动化管理，确保了服务器的稳定运行。</p>
</blockquote>
<h2]]>
    </summary>
    <title>pm2 ecosystem部署应用以及日志管理pm2-logrotate</title>
    <updated>2026-03-08T10:22:42.091Z</updated>
  </entry>
  <entry>
    <author>
      <name>Poetry</name>
    </author>
    <category term="Front-End" scheme="http://fe.poetries.top/categories/Front-End/"/>
    <category term="React" scheme="http://fe.poetries.top/tags/React/"/>
    <category term="前端开发" scheme="http://fe.poetries.top/tags/%E5%89%8D%E7%AB%AF%E5%BC%80%E5%8F%91/"/>
    <category term="Next.js" scheme="http://fe.poetries.top/tags/Next-js/"/>
    <category term="升级指南" scheme="http://fe.poetries.top/tags/%E5%8D%87%E7%BA%A7%E6%8C%87%E5%8D%97/"/>
    <category term="Turbopack" scheme="http://fe.poetries.top/tags/Turbopack/"/>
    <category term="React Compiler" scheme="http://fe.poetries.top/tags/React-Compiler/"/>
    <content>
      <![CDATA[<p>作为React生态中最强大的全栈框架，Next.js的每一次更新都牵动着无数开发者的心。Next.js 16带来了自App Router推出以来最重大的一次版本迭代：Turbopack正式取代Webpack成为默认构建工具，整个路由和导航系统得到全面优化，React Compiler也终于稳定可用。</p><p>本文将基于官方文档，系统性地解析Next.js 16的所有重要变化。无论你是正在考虑升级的老用户，还是准备深入学习Next.js的新人，这篇文章都将帮助你全面理解新版本的特性和升级策略。</p><h2 id="升级准备工作"><a href="#升级准备工作" class="headerlink" title="升级准备工作"></a>升级准备工作</h2><h3 id="环境要求变化"><a href="#环境要求变化" class="headerlink" title="环境要求变化"></a>环境要求变化</h3><p>Next.js 16对运行环境提出了更高的要求：</p><table><thead><tr><th>要求</th><th>变化详情</th></tr></thead><tbody><tr><td>Node.js</td><td>最低版本20.9.0（必须为LTS），Node.js 18不再支持</td></tr><tr><td>TypeScript</td><td>最低版本5.1.0</td></tr><tr><td>浏览器</td><td>Chrome 111+、Edge 111+、Firefox 111+、Safari 16.4+</td></tr></tbody></table><h3 id="一键升级"><a href="#一键升级" class="headerlink" title="一键升级"></a>一键升级</h3><p>Next.js官方提供了强大的Codemod工具，可以自动完成大部分迁移工作：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="comment"># pnpm</span></span><br><span class="line">pnpm dlx @next/codemod@canary upgrade latest</span><br><span class="line"></span><br><span class="line"><span class="comment"># npm</span></span><br><span class="line">npx @next/codemod@canary upgrade latest</span><br><span class="line"></span><br><span class="line"><span class="comment"># yarn</span></span><br><span class="line">yarn dlx @next/codemod@canary upgrade latest</span><br><span class="line"></span><br><span class="line"><span class="comment"># bun</span></span><br><span class="line">bunx @next/codemod@canary upgrade latest</span><br></pre></td></tr></table></figure><p>Codemod能够自动完成以下工作：</p><ul><li>更新next.config.js使用新的turbopack配置</li><li>从next lint迁移到ESLint CLI</li><li>将废弃的middleware convention迁移到proxy</li><li>移除stable APIs的unstable_前缀</li><li>移除pages和layouts中的experimental_ppr配置</li></ul><h3 id="AI辅助升级"><a href="#AI辅助升级" class="headerlink" title="AI辅助升级"></a>AI辅助升级</h3><p>如果你使用支持MCP（Model Context Protocol）的AI编码助手，还可以使用Next.js DevTools MCP来自动化升级过程：</p><figure class="highlight json"><figcaption><span>filename</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;mcpServers&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;next-devtools&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">      <span class="attr">&quot;command&quot;</span><span class="punctuation">:</span> <span class="string">&quot;npx&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;args&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span><span class="string">&quot;-y&quot;</span><span class="punctuation">,</span> <span class="string">&quot;next-devtools-mcp@latest&quot;</span><span class="punctuation">]</span></span><br><span class="line">    <span class="punctuation">&#125;</span></span><br><span class="line">  <span class="punctuation">&#125;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p>配置完成后，只需告诉AI助手”帮助我升级到Next.js 16”即可自动完成升级。</p><h3 id="手动升级"><a href="#手动升级" class="headerlink" title="手动升级"></a>手动升级</h3><p>如果 prefer 手动升级，需要安装最新版本：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="comment"># pnpm</span></span><br><span class="line">pnpm add next@latest react@latest react-dom@latest</span><br><span class="line"></span><br><span class="line"><span class="comment"># npm</span></span><br><span class="line">npm install next@latest react@latest react-dom@latest</span><br><span class="line"></span><br><span class="line"><span class="comment"># yarn</span></span><br><span class="line">yarn add next@latest react@latest react-dom@latest</span><br></pre></td></tr></table></figure><blockquote><p><strong>注意</strong>：如果使用TypeScript，记得同时升级<code>@types/react</code>和<code>@types/react-dom</code>。</p></blockquote><h2 id="Turbopack默认启用"><a href="#Turbopack默认启用" class="headerlink" title="Turbopack默认启用"></a>Turbopack默认启用</h2><h3 id="重大变化"><a href="#重大变化" class="headerlink" title="重大变化"></a>重大变化</h3><p>Next.js 16最重要的变化之一是Turbopack正式稳定，并在<code>next dev</code>和<code>next build</code>中默认使用。在此之前，你需要通过<code>--turbopack</code>或<code>--turbo</code>标志手动启用。</p><figure class="highlight json"><figcaption><span>filename</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;scripts&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;dev&quot;</span><span class="punctuation">:</span> <span class="string">&quot;next dev&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;build&quot;</span><span class="punctuation">:</span> <span class="string">&quot;next build&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;start&quot;</span><span class="punctuation">:</span> <span class="string">&quot;next start&quot;</span></span><br><span class="line">  <span class="punctuation">&#125;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p>不再需要添加<code>--turbopack</code>标志。</p><h3 id="解决Webpack配置冲突"><a href="#解决Webpack配置冲突" class="headerlink" title="解决Webpack配置冲突"></a>解决Webpack配置冲突</h3><p>如果你的项目有自定义Webpack配置，运行<code>next build</code>（现在默认使用Turbopack）将会失败，以防止配置错误带来的问题。</p><p>有以下几种解决方案：</p><ol><li><strong>继续使用Turbopack</strong>：运行<code>next build --turbopack</code>，忽略你的webpack配置</li><li><strong>完全迁移到Turbopack</strong>：将webpack配置迁移为Turbopack兼容选项</li><li><strong>继续使用Webpack</strong>：使用<code>--webpack</code>标志来退出Turbopack</li></ol><figure class="highlight json"><figcaption><span>filename</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;scripts&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;dev&quot;</span><span class="punctuation">:</span> <span class="string">&quot;next dev&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;build&quot;</span><span class="punctuation">:</span> <span class="string">&quot;next build --webpack&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;start&quot;</span><span class="punctuation">:</span> <span class="string">&quot;next start&quot;</span></span><br><span class="line">  <span class="punctuation">&#125;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><h3 id="Turbopack配置位置变化"><a href="#Turbopack配置位置变化" class="headerlink" title="Turbopack配置位置变化"></a>Turbopack配置位置变化</h3><p><code>experimental.turbopack</code>配置已经移出experimental，现在是顶层配置项：</p><figure class="highlight ts"><figcaption><span>filename</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="keyword">type</span> &#123; <span class="title class_">NextConfig</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;next&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// Next.js 15 - experimental.turbopack</span></span><br><span class="line"><span class="keyword">const</span> <span class="attr">nextConfig</span>: <span class="title class_">NextConfig</span> = &#123;</span><br><span class="line">  <span class="attr">experimental</span>: &#123;</span><br><span class="line">    <span class="attr">turbopack</span>: &#123;&#125;,</span><br><span class="line">  &#125;,</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// Next.js 16 - 顶层turbopack</span></span><br><span class="line"><span class="keyword">const</span> <span class="attr">nextConfig</span>: <span class="title class_">NextConfig</span> = &#123;</span><br><span class="line">  <span class="attr">turbopack</span>: &#123;&#125;,</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="Turbopack文件系统缓存（Beta）"><a href="#Turbopack文件系统缓存（Beta）" class="headerlink" title="Turbopack文件系统缓存（Beta）"></a>Turbopack文件系统缓存（Beta）</h3><p>Turbopack现在支持开发模式下的文件系统缓存，可以在重启之间存储编译产物，显著加快编译速度：</p><figure class="highlight ts"><figcaption><span>filename</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="keyword">type</span> &#123; <span class="title class_">NextConfig</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;next&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="attr">nextConfig</span>: <span class="title class_">NextConfig</span> = &#123;</span><br><span class="line">  <span class="attr">experimental</span>: &#123;</span><br><span class="line">    <span class="attr">turbopackFileSystemCacheForDev</span>: <span class="literal">true</span>,</span><br><span class="line">  &#125;,</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="Sass导入语法变化"><a href="#Sass导入语法变化" class="headerlink" title="Sass导入语法变化"></a>Sass导入语法变化</h3><p>Turbopack完全支持从node_modules导入Sass文件。但需要注意，Webpack允许的波浪号（~）前缀语法不再支持：</p><figure class="highlight scss"><table><tr><td class="code"><pre><span class="line"><span class="comment">// Webpack写法（不再支持）</span></span><br><span class="line"><span class="keyword">@import</span> <span class="string">&#x27;~bootstrap/dist/css/bootstrap.min.css&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// Turbopack写法</span></span><br><span class="line"><span class="keyword">@import</span> <span class="string">&#x27;bootstrap/dist/css/bootstrap.min.css&#x27;</span>;</span><br></pre></td></tr></table></figure><h2 id="异步Request-APIs：完全异步化"><a href="#异步Request-APIs：完全异步化" class="headerlink" title="异步Request APIs：完全异步化"></a>异步Request APIs：完全异步化</h2><h3 id="重要变化"><a href="#重要变化" class="headerlink" title="重要变化"></a>重要变化</h3><p>Next.js 15引入了异步Request APIs作为重大变化，并提供了临时的同步兼容性。从Next.js 16开始，同步访问已完全移除，这些API只能异步访问。</p><p>需要异步访问的API包括：</p><ul><li><code>cookies</code></li><li><code>headers</code></li><li><code>draftMode</code></li><li><code>layout.js</code>、<code>page.js</code>、<code>route.js</code>等文件中的<code>params</code></li><li><code>page.js</code>中的<code>searchParams</code></li></ul><figure class="highlight tsx"><table><tr><td class="code"><pre><span class="line"><span class="comment">// Next.js 15（过渡期兼容）</span></span><br><span class="line"><span class="keyword">const</span> cookieStore = <span class="title function_">cookies</span>()</span><br><span class="line"></span><br><span class="line"><span class="comment">// Next.js 16（必须异步）</span></span><br><span class="line"><span class="keyword">const</span> cookieStore = <span class="keyword">await</span> <span class="title function_">cookies</span>()</span><br></pre></td></tr></table></figure><p>建议使用codemod来自动迁移到异步API。</p><h3 id="类型迁移助手"><a href="#类型迁移助手" class="headerlink" title="类型迁移助手"></a>类型迁移助手</h3><p>为了帮助迁移异步params和searchParams，可以运行<code>npx next typegen</code>自动生成全局可用的类型助手：</p><ul><li><code>PageProps</code>：页面组件属性</li><li><code>LayoutProps</code>：布局组件属性</li><li><code>RouteContext</code>：路由上下文</li></ul><figure class="highlight tsx"><figcaption><span>filename</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="keyword">async</span> <span class="keyword">function</span> <span class="title function_">Page</span>(<span class="params"><span class="attr">props</span>: <span class="title class_">PageProps</span>&lt;<span class="string">&#x27;/blog/[slug]&#x27;</span>&gt;</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> &#123; slug &#125; = <span class="keyword">await</span> props.<span class="property">params</span></span><br><span class="line">  <span class="keyword">const</span> query = <span class="keyword">await</span> props.<span class="property">searchParams</span></span><br><span class="line">  <span class="keyword">return</span> <span class="language-xml"><span class="tag">&lt;<span class="name">h1</span>&gt;</span>Blog Post: &#123;slug&#125;<span class="tag">&lt;/<span class="name">h1</span>&gt;</span></span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="icon和opengraph-image的异步参数"><a href="#icon和opengraph-image的异步参数" class="headerlink" title="icon和opengraph-image的异步参数"></a>icon和opengraph-image的异步参数</h3><p>传递到<code>opengraph-image</code>、<code>twitter-image</code>、<code>icon</code>和<code>apple-icon</code>中的props现在都是Promise：</p><figure class="highlight js"><figcaption><span>filename</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">async</span> <span class="keyword">function</span> <span class="title function_">generateImageMetadata</span>(<span class="params">&#123; params &#125;</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> &#123; slug &#125; = params</span><br><span class="line">  <span class="keyword">return</span> [&#123; <span class="attr">id</span>: <span class="string">&#x27;1&#x27;</span> &#125;, &#123; <span class="attr">id</span>: <span class="string">&#x27;2&#x27;</span> &#125;]</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// Next.js 16 - 异步params和id访问</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="keyword">async</span> <span class="keyword">function</span> <span class="title function_">Image</span>(<span class="params">&#123; params, id &#125;</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> &#123; slug &#125; = <span class="keyword">await</span> params</span><br><span class="line">  <span class="keyword">const</span> imageId = <span class="keyword">await</span> id</span><br><span class="line">  <span class="comment">// ...</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="sitemap的异步id参数"><a href="#sitemap的异步id参数" class="headerlink" title="sitemap的异步id参数"></a>sitemap的异步id参数</h3><figure class="highlight js"><figcaption><span>filename</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">async</span> <span class="keyword">function</span> <span class="title function_">generateSitemaps</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> [&#123; <span class="attr">id</span>: <span class="number">0</span> &#125;, &#123; <span class="attr">id</span>: <span class="number">1</span> &#125;, &#123; <span class="attr">id</span>: <span class="number">2</span> &#125;, &#123; <span class="attr">id</span>: <span class="number">3</span> &#125;]</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// Next.js 16 - 异步id访问</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="keyword">async</span> <span class="keyword">function</span> <span class="title function_">sitemap</span>(<span class="params">&#123; id &#125;</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> resolvedId = <span class="keyword">await</span> id</span><br><span class="line">  <span class="keyword">const</span> start = <span class="title class_">Number</span>(resolvedId) * <span class="number">50000</span></span><br><span class="line">  <span class="comment">// ...</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="React-19-2与React-Compiler"><a href="#React-19-2与React-Compiler" class="headerlink" title="React 19.2与React Compiler"></a>React 19.2与React Compiler</h2><h3 id="React-19-2新特性"><a href="#React-19-2新特性" class="headerlink" title="React 19.2新特性"></a>React 19.2新特性</h3><p>Next.js 16使用最新的React Canary版本，包含React 19.2的新特性：</p><ul><li><strong>View Transitions</strong>：在Transition或导航中更新元素时添加动画</li><li><strong>useEffectEvent</strong>：将非响应式逻辑从Effect提取到可重用的Effect Event函数中</li><li><strong>Activity</strong>：通过<code>display: none</code>隐藏UI同时保持状态和清理Effect来渲染”后台活动”</li></ul><h3 id="React-Compiler稳定支持"><a href="#React-Compiler稳定支持" class="headerlink" title="React Compiler稳定支持"></a>React Compiler稳定支持</h3><p>React Compiler的内置支持在Next.js 16中正式稳定。React Compiler可以自动memoize组件，减少不必要的重渲染，无需手动修改代码。</p><p><code>reactCompiler</code>配置项已从experimental升级为稳定版：</p><figure class="highlight ts"><figcaption><span>filename</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="keyword">type</span> &#123; <span class="title class_">NextConfig</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;next&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="attr">nextConfig</span>: <span class="title class_">NextConfig</span> = &#123;</span><br><span class="line">  <span class="attr">reactCompiler</span>: <span class="literal">true</span>,</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>安装最新版本的React Compiler插件：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">npm install -D babel-plugin-react-compiler</span><br></pre></td></tr></table></figure><blockquote><p><strong>注意</strong>：启用此选项后，开发和构建时的编译时间可能会更长，因为React Compiler依赖Babel。</p></blockquote><h2 id="缓存API更新"><a href="#缓存API更新" class="headerlink" title="缓存API更新"></a>缓存API更新</h2><h3 id="revalidateTag新增签名"><a href="#revalidateTag新增签名" class="headerlink" title="revalidateTag新增签名"></a>revalidateTag新增签名</h3><p><code>revalidateTag</code>有新函数签名，可以传递<code>cacheLife</code>配置文件作为第二个参数：</p><figure class="highlight ts"><figcaption><span>filename</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="string">&#x27;use server&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> &#123; revalidateTag &#125; <span class="keyword">from</span> <span class="string">&#x27;next/cache&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">async</span> <span class="keyword">function</span> <span class="title function_">updateArticle</span>(<span class="params"><span class="attr">articleId</span>: <span class="built_in">string</span></span>) &#123;</span><br><span class="line">  <span class="comment">// 标记文章数据为过时</span></span><br><span class="line">  <span class="title function_">revalidateTag</span>(<span class="string">`article-<span class="subst">$&#123;articleId&#125;</span>`</span>, <span class="string">&#x27;max&#x27;</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="updateTag：立即读取写入"><a href="#updateTag：立即读取写入" class="headerlink" title="updateTag：立即读取写入"></a>updateTag：立即读取写入</h3><p><code>updateTag</code>是一个新的Server Actions专用API，提供”读取即写入”语义——用户做出更改后，UI立即显示更改，而不是显示过时数据。</p><figure class="highlight ts"><figcaption><span>filename</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="string">&#x27;use server&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> &#123; updateTag &#125; <span class="keyword">from</span> <span class="string">&#x27;next/cache&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">async</span> <span class="keyword">function</span> <span class="title function_">updateUserProfile</span>(<span class="params"><span class="attr">userId</span>: <span class="built_in">string</span>, <span class="attr">profile</span>: <span class="title class_">Profile</span></span>) &#123;</span><br><span class="line">  <span class="keyword">await</span> db.<span class="property">users</span>.<span class="title function_">update</span>(userId, profile)</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 立即过期并刷新缓存 - 用户立即看到更改</span></span><br><span class="line">  <span class="title function_">updateTag</span>(<span class="string">`user-<span class="subst">$&#123;userId&#125;</span>`</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这确保交互功能立即反映更改，非常适合表单、用户设置等场景。</p><h3 id="refresh：刷新客户端路由"><a href="#refresh：刷新客户端路由" class="headerlink" title="refresh：刷新客户端路由"></a>refresh：刷新客户端路由</h3><p><code>refresh</code>允许从Server Action内部刷新客户端路由：</p><figure class="highlight ts"><figcaption><span>filename</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="string">&#x27;use server&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> &#123; refresh &#125; <span class="keyword">from</span> <span class="string">&#x27;next/cache&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">async</span> <span class="keyword">function</span> <span class="title function_">markNotificationAsRead</span>(<span class="params"><span class="attr">notificationId</span>: <span class="built_in">string</span></span>) &#123;</span><br><span class="line">  <span class="keyword">await</span> db.<span class="property">notifications</span>.<span class="title function_">markAsRead</span>(notificationId)</span><br><span class="line">  <span class="title function_">refresh</span>()</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="cacheLife和cacheTag稳定"><a href="#cacheLife和cacheTag稳定" class="headerlink" title="cacheLife和cacheTag稳定"></a>cacheLife和cacheTag稳定</h3><p><code>cacheLife</code>和<code>cacheTag</code>现在稳定了，不再需要<code>unstable_</code>前缀：</p><figure class="highlight ts"><table><tr><td class="code"><pre><span class="line"><span class="comment">// 之前</span></span><br><span class="line"><span class="keyword">import</span> &#123;</span><br><span class="line">  unstable_cacheLife <span class="keyword">as</span> cacheLife,</span><br><span class="line">  unstable_cacheTag <span class="keyword">as</span> cacheTag,</span><br><span class="line">&#125; <span class="keyword">from</span> <span class="string">&#x27;now/cache&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 现在</span></span><br><span class="line"><span class="keyword">import</span> &#123; cacheLife, cacheTag &#125; <span class="keyword">from</span> <span class="string">&#x27;next/cache&#x27;</span></span><br></pre></td></tr></table></figure><h2 id="路由和导航优化"><a href="#路由和导航优化" class="headerlink" title="路由和导航优化"></a>路由和导航优化</h2><p>Next.js 16包含路由和导航系统的全面改革，使页面转换更精简、更快速：</p><ul><li><strong>布局去重</strong>：当预取多个共享布局的URL时，布局只下载一次</li><li><strong>增量预取</strong>：Next.js只预取缓存中不存在的部分，而不是整个页面</li></ul><p>这些变化不需要代码修改，旨在提高所有应用的性能。</p><h2 id="middleware更名proxy"><a href="#middleware更名proxy" class="headerlink" title="middleware更名proxy"></a>middleware更名proxy</h2><h3 id="重要变化-1"><a href="#重要变化-1" class="headerlink" title="重要变化"></a>重要变化</h3><p><code>middleware</code>文件名已废弃，重命名为<code>proxy</code>，以明确网络边界和路由焦点。</p><p><code>edge</code>运行时在<code>proxy</code>中不支持。<code>proxy</code>运行时是<code>nodejs</code>，无法配置。如果想继续使用<code>edge</code>运行时，请继续使用<code>middleware</code>。</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 重命名middleware文件</span></span><br><span class="line"><span class="built_in">mv</span> middleware.ts proxy.ts</span><br><span class="line"><span class="comment"># 或</span></span><br><span class="line"><span class="built_in">mv</span> middleware.js proxy.js</span><br></pre></td></tr></table></figure><p>命名导出<code>middleware</code>也已废弃，将函数重命名为<code>proxy</code>：</p><figure class="highlight ts"><figcaption><span>filename</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">function</span> <span class="title function_">proxy</span>(<span class="params"><span class="attr">request</span>: <span class="title class_">Request</span></span>) &#123;&#125;</span><br></pre></td></tr></table></figure><p>包含<code>middleware</code>名称的配置标志也已重命名，例如<code>skipMiddlewareUrlNormalize</code>现在是<code>skipProxyUrlNormalize</code>。</p><h2 id="next-image重要变化"><a href="#next-image重要变化" class="headerlink" title="next&#x2F;image重要变化"></a>next&#x2F;image重要变化</h2><h3 id="带查询字符串的本地图片"><a href="#带查询字符串的本地图片" class="headerlink" title="带查询字符串的本地图片"></a>带查询字符串的本地图片</h3><p>带查询字符串的本地图片现在需要<code>images.localPatterns.search</code>配置来防止枚举攻击：</p><figure class="highlight ts"><figcaption><span>filename</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="attr">nextConfig</span>: <span class="title class_">NextConfig</span> = &#123;</span><br><span class="line">  <span class="attr">images</span>: &#123;</span><br><span class="line">    <span class="attr">localPatterns</span>: [</span><br><span class="line">      &#123;</span><br><span class="line">        <span class="attr">pathname</span>: <span class="string">&#x27;/assets/**&#x27;</span>,</span><br><span class="line">        <span class="attr">search</span>: <span class="string">&#x27;?v=1&#x27;</span>,</span><br><span class="line">      &#125;,</span><br><span class="line">    ],</span><br><span class="line">  &#125;,</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="minimumCacheTTL默认值变化"><a href="#minimumCacheTTL默认值变化" class="headerlink" title="minimumCacheTTL默认值变化"></a>minimumCacheTTL默认值变化</h3><p><code>images.minimumCacheTTL</code>的默认值从60秒变为4小时（14400秒）。这减少了没有cache-control头的图片的重新验证成本。</p><p>如果需要以前的行为，可以将<code>minimumCacheTTL</code>设置回60秒。</p><h3 id="imageSizes默认值变化"><a href="#imageSizes默认值变化" class="headerlink" title="imageSizes默认值变化"></a>imageSizes默认值变化</h3><p>默认<code>images.imageSizes</code>数组中的值16已被移除。很少有项目会提供16像素宽的图片，移除这个设置可以减少next&#x2F;image发送到浏览器的srcset属性大小。</p><h3 id="qualities默认值变化"><a href="#qualities默认值变化" class="headerlink" title="qualities默认值变化"></a>qualities默认值变化</h3><p><code>images.qualities</code>的默认值从允许所有质量变为只允许<code>[75]</code>。</p><h3 id="本地IP限制"><a href="#本地IP限制" class="headerlink" title="本地IP限制"></a>本地IP限制</h3><p>新的安全限制默认阻止本地IP优化。只能为私有网络设置<code>images.dangerouslyAllowLocalIP</code>为<code>true</code>。</p><h3 id="最大重定向数"><a href="#最大重定向数" class="headerlink" title="最大重定向数"></a>最大重定向数</h3><p><code>images.maximumRedirects</code>的默认值从无限制变为最多3次重定向。</p><h3 id="next-legacy-image废弃"><a href="#next-legacy-image废弃" class="headerlink" title="next&#x2F;legacy&#x2F;image废弃"></a>next&#x2F;legacy&#x2F;image废弃</h3><p><code>next/legacy/image</code>组件已废弃，使用<code>next/image</code>代替。</p><h3 id="images-domains配置废弃"><a href="#images-domains配置废弃" class="headerlink" title="images.domains配置废弃"></a>images.domains配置废弃</h3><p><code>images.domains</code>配置已废弃，使用<code>images.remotePatterns</code>代替以提高安全性。</p><h2 id="并行路由default-js要求"><a href="#并行路由default-js要求" class="headerlink" title="并行路由default.js要求"></a>并行路由default.js要求</h2><p>所有并行路由槽位现在需要显式的<code>default.js</code>文件。没有它们构建将失败。</p><figure class="highlight tsx"><figcaption><span>filename</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; notFound &#125; <span class="keyword">from</span> <span class="string">&#x27;next/navigation&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="keyword">function</span> <span class="title function_">Default</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="title function_">notFound</span>()</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>或返回null：</p><figure class="highlight tsx"><figcaption><span>filename</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="keyword">function</span> <span class="title function_">Default</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="literal">null</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="部分预渲染（PPR）变化"><a href="#部分预渲染（PPR）变化" class="headerlink" title="部分预渲染（PPR）变化"></a>部分预渲染（PPR）变化</h2><p>Next.js 16移除了实验性PPR标志和配置选项，包括路由级别的<code>experimental_ppr</code>。</p><p>从Next.js 16开始，可以通过<code>cacheComponents</code>配置来选择使用PPR：</p><figure class="highlight js"><figcaption><span>filename</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">const</span> nextConfig = &#123;</span><br><span class="line">  <span class="attr">cacheComponents</span>: <span class="literal">true</span>,</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><blockquote><p><strong>注意</strong>：Next.js 16的PPR与Next.js 15 canaries中的PPR工作方式不同。如果现在正在使用PPR，建议保持在当前使用的Next.js 15 canary版本。</p></blockquote><h2 id="其他重要变化"><a href="#其他重要变化" class="headerlink" title="其他重要变化"></a>其他重要变化</h2><h3 id="ESLint-Flat-Config"><a href="#ESLint-Flat-Config" class="headerlink" title="ESLint Flat Config"></a>ESLint Flat Config</h3><p><code>@next/eslint-plugin-next</code>现在默认使用ESLint Flat Config格式，与ESLint v10对齐。</p><h3 id="滚动行为覆盖"><a href="#滚动行为覆盖" class="headerlink" title="滚动行为覆盖"></a>滚动行为覆盖</h3><p>在Previous版本的Next.js中，如果全局在HTML元素上设置了<code>scroll-behavior: smooth</code>，Next.js会在SPA路由转换期间覆盖这个设置。</p><p>在Next.js 16中，默认情况下Next.js不再覆盖你的滚动行为设置。如果希望Next.js执行覆盖（之前的默认行为），在HTML元素上添加<code>data-scroll-behavior=&quot;smooth&quot;</code>属性：</p><figure class="highlight tsx"><figcaption><span>filename</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="keyword">function</span> <span class="title function_">RootLayout</span>(<span class="params">&#123; children &#125;</span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">html</span> <span class="attr">lang</span>=<span class="string">&quot;en&quot;</span> <span class="attr">data-scroll-behavior</span>=<span class="string">&quot;smooth&quot;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">body</span>&gt;</span>&#123;children&#125;<span class="tag">&lt;/<span class="name">body</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">html</span>&gt;</span></span></span><br><span class="line">  )</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="并发dev和build"><a href="#并发dev和build" class="headerlink" title="并发dev和build"></a>并发dev和build</h3><p><code>next dev</code>和<code>next build</code>现在使用不同的输出目录，启用并发执行。<code>next dev</code>命令输出到<code>.next/dev</code>。</p><h3 id="AMP支持移除"><a href="#AMP支持移除" class="headerlink" title="AMP支持移除"></a>AMP支持移除</h3><p>AMP的采用已显著下降，维护这个功能增加了框架的复杂性。所有AMP API和配置都已移除。</p><h3 id="next-lint命令移除"><a href="#next-lint命令移除" class="headerlink" title="next lint命令移除"></a>next lint命令移除</h3><p><code>next lint</code>命令已被移除，使用Biome或直接使用ESLint。<code>next build</code>不再运行linting。</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">npx @next/codemod@canary next-lint-to-eslint-cli .</span><br></pre></td></tr></table></figure><h3 id="运行时配置移除"><a href="#运行时配置移除" class="headerlink" title="运行时配置移除"></a>运行时配置移除</h3><p><code>serverRuntimeConfig</code>和<code>publicRuntimeConfig</code>已被移除，使用环境变量代替。</p><p>之前通过运行时配置的值，现在应该：</p><ul><li>服务器端值：直接在Server Components中访问环境变量</li><li>客户端可用值：使用<code>NEXT_PUBLIC_</code>前缀</li></ul><h3 id="devIndicators选项移除"><a href="#devIndicators选项移除" class="headerlink" title="devIndicators选项移除"></a>devIndicators选项移除</h3><p>以下选项已从devIndicators中移除：</p><ul><li><code>appIsrStatus</code></li><li><code>buildActivity</code></li><li><code>buildActivityPosition</code></li></ul><p>指标本身仍然可用。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>Next.js 16是一次里程碑式的版本更新，带来了以下核心变化：</p><ul><li><strong>Turbopack默认启用</strong>：Webpack不再是默认构建工具</li><li><strong>异步API完全化</strong>：cookies、headers、params等必须异步访问</li><li><strong>React Compiler稳定</strong>：自动优化组件渲染性能</li><li><strong>缓存API增强</strong>：updateTag实现立即读取写入</li><li><strong>middleware更名proxy</strong>：明确网络边界概念</li><li><strong>next&#x2F;image配置调整</strong>：安全性和性能优化</li><li><strong>多项功能移除</strong>：AMP、next lint、运行时配置等</li></ul><p>虽然升级需要一定工作量，但这些变化都将大幅提升应用性能和开发体验。建议尽快规划升级计划，享受新版本带来的改进。</p><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><ul><li><a href="https://nextjs.org/docs/app/guides/upgrading/version-16">Next.js 16 Official Upgrade Guide</a></li><li><a href="https://nextjs.org/blog/next-16">next-16博客</a></li><li><a href="https://react.dev/blog/2025/10/01/react-19-2">React 19.2 Announcement</a></li><li><a href="https://nextjs.org/docs/app/api-reference/config/next-config-js/turbopack">Turbopack Documentation</a></li><li><a href="https://react.dev/reference/react/Compiler">React Compiler</a></li></ul>]]>
    </content>
    <id>http://fe.poetries.top/2025/11/03/nextjs16-changes-overview/</id>
    <link href="http://fe.poetries.top/2025/11/03/nextjs16-changes-overview/"/>
    <published>2025-11-03T12:40:12.000Z</published>
    <summary>全面解析Next.js 16新特性：Turbopack默认启用、异步API全面化、React Compiler稳定版、缓存策略更新、middleware更名proxy等重大变化，附详细升级指南。</summary>
    <title>Next.js 16带来哪些变革？深度解析新版本核心特性与升级指南</title>
    <updated>2026-03-08T10:22:42.089Z</updated>
  </entry>
  <entry>
    <author>
      <name>Poetry</name>
    </author>
    <category term="Front-End" scheme="http://fe.poetries.top/categories/Front-End/"/>
    <category term="前端工程化" scheme="http://fe.poetries.top/tags/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/"/>
    <category term="状态管理" scheme="http://fe.poetries.top/tags/%E7%8A%B6%E6%80%81%E7%AE%A1%E7%90%86/"/>
    <category term="React Query" scheme="http://fe.poetries.top/tags/React-Query/"/>
    <category term="TanStack Query" scheme="http://fe.poetries.top/tags/TanStack-Query/"/>
    <category term="数据获取" scheme="http://fe.poetries.top/tags/%E6%95%B0%E6%8D%AE%E8%8E%B7%E5%8F%96/"/>
    <content>
      <![CDATA[<p>在前端开发中，数据获取与状态管理一直是核心难题。当你的React应用需要从后端API获取数据时，你是否曾被这些问题困扰过：重复请求导致性能浪费、缓存数据难以维护、加载状态处理繁琐、乐观更新实现困难？如果你正在寻找解决方案，那么React Query（现更名为TanStack Query）正是为你而设计的。</p><p>本文将深度解读React Query的核心价值，通过大量实战代码帮助读者全面掌握这个现代React应用不可或缺的数据获取库。</p><h2 id="一、为什么React应用需要React-Query"><a href="#一、为什么React应用需要React-Query" class="headerlink" title="一、为什么React应用需要React Query"></a>一、为什么React应用需要React Query</h2><h3 id="1-1-服务端状态的特殊性"><a href="#1-1-服务端状态的特殊性" class="headerlink" title="1.1 服务端状态的特殊性"></a>1.1 服务端状态的特殊性</h3><p>在理解React Query之前，我们需要先认识一个核心概念：服务端状态（Server State）与客户端状态（Client State）的本质区别。大多数传统状态管理库（如Redux、Zustand）在处理客户端状态时表现出色，但在处理服务端状态时却显得力不从心。这是因为服务端状态具有以下独特特性：</p><p>首先，服务端状态是远程持久化的，数据存储在你不一定拥有或控制的服务器上。其次，获取和更新数据需要异步API调用，无法像本地状态那样即时获取。第三，服务端状态是共享的，可能被其他人悄然改变。第四，如果不主动管理，服务端数据很容易变得过时。</p><p>正是这些特性，使得服务端状态管理成为前端开发中最具挑战性的领域之一。</p><h3 id="1-2-传统方案的时代局限"><a href="#1-2-传统方案的时代局限" class="headerlink" title="1.2 传统方案的时代局限"></a>1.2 传统方案的时代局限</h3><p>在没有专门的数据获取库时，开发者通常采用以下几种方式管理服务端状态：第一种是直接在组件中useEffect配合useState，第二种是使用Redux等通用状态管理库存储异步数据，第三种是借助SWR等轻量级数据获取工具。</p><p>这些方案虽然可行，但都存在明显缺陷。手动管理数据获取意味着你需要自己处理加载状态、错误处理、缓存逻辑、重试机制等大量重复性代码。Redux虽然功能强大，但为服务端状态编写异步逻辑过于繁琐，且性能开销较大。即便是相对轻量的SWR，在复杂场景下也缺乏React Query的灵活性。</p><p>React Query的出现彻底改变了这一局面。它专门为服务端状态设计，开箱即用，拥有零配置即可使用的默认行为，同时支持高度定制以适应项目增长。</p><h2 id="二、React-Query核心概念解析"><a href="#二、React-Query核心概念解析" class="headerlink" title="二、React Query核心概念解析"></a>二、React Query核心概念解析</h2><h3 id="2-1-QueryKey查询键的重要性"><a href="#2-1-QueryKey查询键的重要性" class="headerlink" title="2.1 QueryKey查询键的重要性"></a>2.1 QueryKey查询键的重要性</h3><p>QueryKey是React Query的核心理念之一。每个查询都需要一个唯一的键来标识数据，这个键不仅用于缓存管理，还决定了数据的依赖关系和自动刷新时机。</p><p>基础查询键的写法简单直接，例如获取待办事项列表可以使用<code>[&#39;todos&#39;]</code>，获取某个具体用户可以用<code>[&#39;user&#39;, userId]</code>。更复杂的查询可以包含多个参数，如<code>[&#39;todos&#39;, &#123; status: &#39;done&#39;, page: 1 &#125;]</code>。React Query会自动对查询键进行哈希处理，确保相同键的查询共享同一份缓存数据。</p><p>值得注意的是，查询键的顺序是敏感的。<code>[&#39;todos&#39;, status, page]</code>与<code>[&#39;todos&#39;, page, status]</code>会被视为不同的查询，因为数组元素的顺序会影响最终的哈希值。</p><h3 id="2-2-查询状态与获取状态"><a href="#2-2-查询状态与获取状态" class="headerlink" title="2.2 查询状态与获取状态"></a>2.2 查询状态与获取状态</h3><p>理解React Query返回的状态是正确使用库的关键。<code>useQuery</code>返回的结果对象包含两个维度的状态信息。</p><p>第一个维度是查询状态（status），反映数据是否存在或是否成功获取：<code>isPending</code>表示数据仍在加载中，<code>isError</code>表示查询失败并可通过<code>error</code>属性获取错误信息，<code>isSuccess</code>表示查询成功数据可通过<code>data</code>属性获取。</p><p>第二个维度是获取状态（fetchStatus），反映查询函数是否正在执行：<code>fetching</code>表示正在发起网络请求，<code>paused</code>表示请求因网络中断等原因暂停，<code>idle</code>表示当前没有进行任何请求。</p><p>这两个维度可以组合出多种状态，例如一个处于<code>success</code>状态且<code>fetchStatus</code>为<code>fetching</code>的查询，表示当前既有缓存数据可用，又在后台进行刷新请求。这正是React Query强大的stale-while-revalidate机制的体现。</p><h2 id="三、快速上手与基础用法"><a href="#三、快速上手与基础用法" class="headerlink" title="三、快速上手与基础用法"></a>三、快速上手与基础用法</h2><h3 id="3-1-环境安装配置"><a href="#3-1-环境安装配置" class="headerlink" title="3.1 环境安装配置"></a>3.1 环境安装配置</h3><p>React Query的安装非常简单，通过npm、pnpm或yarn均可完成：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">npm install @tanstack/react-query</span><br><span class="line"><span class="comment"># 或</span></span><br><span class="line">pnpm add @tanstack/react-query</span><br><span class="line"><span class="comment"># 或</span></span><br><span class="line">yarn add @tanstack/react-query</span><br></pre></td></tr></table></figure><p>安装完成后，需要在应用根组件中包裹<code>QueryClientProvider</code>并传入<code>QueryClient</code>实例：</p><figure class="highlight tsx"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">QueryClient</span>, <span class="title class_">QueryClientProvider</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;@tanstack/react-query&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> queryClient = <span class="keyword">new</span> <span class="title class_">QueryClient</span>()</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="keyword">function</span> <span class="title function_">App</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">QueryClientProvider</span> <span class="attr">client</span>=<span class="string">&#123;queryClient&#125;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">YourApp</span> /&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">QueryClientProvider</span>&gt;</span></span></span><br><span class="line">  )</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这一步骤只需执行一次，之后整个应用中的组件都可以使用useQuery和useMutationHooks。</p><h3 id="3-2-第一个查询用例"><a href="#3-2-第一个查询用例" class="headerlink" title="3.2 第一个查询用例"></a>3.2 第一个查询用例</h3><p>让我们看一个完整的查询示例，获取GitHub仓库信息：</p><figure class="highlight tsx"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; useQuery &#125; <span class="keyword">from</span> <span class="string">&#x27;@tanstack/react-query&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">RepoInfo</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> &#123; isPending, isError, data, error &#125; = <span class="title function_">useQuery</span>(&#123;</span><br><span class="line">    <span class="attr">queryKey</span>: [<span class="string">&#x27;repoData&#x27;</span>],</span><br><span class="line">    <span class="attr">queryFn</span>: <span class="function">() =&gt;</span></span><br><span class="line">      <span class="title function_">fetch</span>(<span class="string">&#x27;https://api.github.com/repos/TanStack/query&#x27;</span>)</span><br><span class="line">        .<span class="title function_">then</span>(<span class="function">(<span class="params">res</span>) =&gt;</span> res.<span class="title function_">json</span>())</span><br><span class="line">  &#125;)</span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (isPending) <span class="keyword">return</span> <span class="language-xml"><span class="tag">&lt;<span class="name">div</span>&gt;</span>加载中...<span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (isError) &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="language-xml"><span class="tag">&lt;<span class="name">div</span>&gt;</span>错误: &#123;error.message&#125;<span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">h1</span>&gt;</span>&#123;data.name&#125;<span class="tag">&lt;/<span class="name">h1</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">p</span>&gt;</span>&#123;data.description&#125;<span class="tag">&lt;/<span class="name">p</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">div</span>&gt;</span>⭐ &#123;data.stargazers_count&#125; Stars<span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">div</span>&gt;</span>🍴 &#123;data.forks_count&#125; Forks<span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line">  )</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这个示例展示了useQuery的基本用法：通过<code>queryKey</code>指定查询标识，通过<code>queryFn</code>定义获取数据的异步函数。React Query会自动处理加载状态、错误处理和数据缓存。</p><h3 id="3-3-重要默认配置项"><a href="#3-3-重要默认配置项" class="headerlink" title="3.3 重要默认配置项"></a>3.3 重要默认配置项</h3><p>React Query采用激进但合理的默认配置，在深入使用前理解这些默认值至关重要。</p><p><code>staleTime</code>默认为0，意味着数据一旦获取就被视为过时。这会导致组件挂载时自动重新获取数据。若想避免频繁请求，可将<code>staleTime</code>设置为较长的时间，例如5分钟：<code>staleTime: 5 * 60 * 1000</code>。</p><p><code>gcTime</code>默认为5分钟，用于控制没有活跃观察者时缓存数据的存活时间。超过这个时间，数据将被垃圾回收。</p><p><code>retry</code>默认为3次，失败的请求会自动以指数退避策略重试。这对于临时性网络错误非常有用。</p><p><code>refetchOnWindowFocus</code>默认为true，当用户切换回应用窗口时会自动重新获取数据，确保展示最新的服务器状态。</p><h2 id="四、useMutation与数据修改"><a href="#四、useMutation与数据修改" class="headerlink" title="四、useMutation与数据修改"></a>四、useMutation与数据修改</h2><h3 id="4-1-基础Mutations用法"><a href="#4-1-基础Mutations用法" class="headerlink" title="4.1 基础Mutations用法"></a>4.1 基础Mutations用法</h3><p>与查询不同，数据的创建、更新、删除操作应该使用<code>useMutation</code>。它提供了专门的状态管理来处理服务端修改：</p><figure class="highlight tsx"><table><tr><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">CreateTodo</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> mutation = <span class="title function_">useMutation</span>(&#123;</span><br><span class="line">    <span class="attr">mutationFn</span>: <span class="function">(<span class="params">newTodo</span>) =&gt;</span></span><br><span class="line">      axios.<span class="title function_">post</span>(<span class="string">&#x27;/api/todos&#x27;</span>, newTodo)</span><br><span class="line">  &#125;)</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">button</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">      <span class="attr">onClick</span>=<span class="string">&#123;()</span> =&gt;</span> &#123;</span></span><br><span class="line"><span class="language-xml">        mutation.mutate(&#123; title: &#x27;学习React Query&#x27;, completed: false &#125;)</span></span><br><span class="line"><span class="language-xml">      &#125;&#125;</span></span><br><span class="line"><span class="language-xml">      disabled=&#123;mutation.isPending&#125;</span></span><br><span class="line"><span class="language-xml">    &gt;</span></span><br><span class="line"><span class="language-xml">      &#123;mutation.isPending ? &#x27;创建中...&#x27; : &#x27;创建待办&#x27;&#125;</span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">button</span>&gt;</span></span></span><br><span class="line">  )</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>useMutation返回的状态包括：<code>isIdle</code>（初始状态）、<code>isPending</code>（执行中）、<code>isSuccess</code>（成功）、<code>isError</code>（失败）。你可以通过这些状态向用户展示不同的UI反馈。</p><h3 id="4-2-乐观更新实现"><a href="#4-2-乐观更新实现" class="headerlink" title="4.2 乐观更新实现"></a>4.2 乐观更新实现</h3><p>乐观更新是提升用户体验的关键技术，允许在服务器响应前就更新界面。React Query通过<code>onMutate</code>、<code>onError</code>、<code>onSettled</code>三个生命周期钩子完美支持这一模式：</p><figure class="highlight tsx"><table><tr><td class="code"><pre><span class="line"><span class="keyword">const</span> queryClient = <span class="title function_">useQueryClient</span>()</span><br><span class="line"></span><br><span class="line"><span class="title function_">useMutation</span>(&#123;</span><br><span class="line">  <span class="attr">mutationFn</span>: updateTodo,</span><br><span class="line">  <span class="attr">onMutate</span>: <span class="title function_">async</span> (newTodo) =&gt; &#123;</span><br><span class="line">    <span class="comment">// 取消所有正在进行的同名查询，防止覆盖乐观更新</span></span><br><span class="line">    <span class="keyword">await</span> queryClient.<span class="title function_">cancelQueries</span>(&#123; <span class="attr">queryKey</span>: [<span class="string">&#x27;todos&#x27;</span>] &#125;)</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 快照更新前的数据，用于回滚</span></span><br><span class="line">    <span class="keyword">const</span> previousTodos = queryClient.<span class="title function_">getQueryData</span>([<span class="string">&#x27;todos&#x27;</span>])</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 立即乐观更新缓存</span></span><br><span class="line">    queryClient.<span class="title function_">setQueryData</span>([<span class="string">&#x27;todos&#x27;</span>], <span class="function">(<span class="params">old</span>) =&gt;</span></span><br><span class="line">      old.<span class="title function_">map</span>(<span class="function">(<span class="params">todo</span>) =&gt;</span></span><br><span class="line">        todo.<span class="property">id</span> === newTodo.<span class="property">id</span> ? newTodo : todo</span><br><span class="line">      )</span><br><span class="line">    )</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 返回上下文对象，包含快照数据</span></span><br><span class="line">    <span class="keyword">return</span> &#123; previousTodos &#125;</span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="attr">onError</span>: <span class="function">(<span class="params">err, newTodo, context</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="comment">// 失败时回滚到之前的状态</span></span><br><span class="line">    queryClient.<span class="title function_">setQueryData</span>([<span class="string">&#x27;todos&#x27;</span>], context.<span class="property">previousTodos</span>)</span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="attr">onSettled</span>: <span class="function">() =&gt;</span> &#123;</span><br><span class="line">    <span class="comment">// 最终总是重新获取最新数据</span></span><br><span class="line">    queryClient.<span class="title function_">invalidateQueries</span>(&#123; <span class="attr">queryKey</span>: [<span class="string">&#x27;todos&#x27;</span>] &#125;)</span><br><span class="line">  &#125;</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><p>这段代码完整展示了乐观更新的流程：首先在<code>onMutate</code>中保存旧数据并更新缓存，然后如果请求失败在<code>onError</code>中回滚，最后无论成功失败都在<code>onSettled</code>中确保数据同步。</p><h3 id="4-3-失效查询与数据同步"><a href="#4-3-失效查询与数据同步" class="headerlink" title="4.3 失效查询与数据同步"></a>4.3 失效查询与数据同步</h3><p>mutation完成后，通常需要使相关查询失效以触发数据刷新。最简单的方式是使用<code>onSettled</code>回调：</p><figure class="highlight tsx"><table><tr><td class="code"><pre><span class="line"><span class="keyword">const</span> mutation = <span class="title function_">useMutation</span>(&#123;</span><br><span class="line">  <span class="attr">mutationFn</span>: addTodo,</span><br><span class="line">  <span class="attr">onSettled</span>: <span class="function">() =&gt;</span> &#123;</span><br><span class="line">    <span class="comment">// 添加完成后使todos查询失效，自动触发重新获取</span></span><br><span class="line">    queryClient.<span class="title function_">invalidateQueries</span>(&#123; <span class="attr">queryKey</span>: [<span class="string">&#x27;todos&#x27;</span>] &#125;)</span><br><span class="line">  &#125;</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><p>这种方式的优点是代码简洁，且能确保界面显示最新的服务端数据。</p><h2 id="五、高级特性与最佳实践"><a href="#五、高级特性与最佳实践" class="headerlink" title="五、高级特性与最佳实践"></a>五、高级特性与最佳实践</h2><h3 id="5-1-查询依赖与并行查询"><a href="#5-1-查询依赖与并行查询" class="headerlink" title="5.1 查询依赖与并行查询"></a>5.1 查询依赖与并行查询</h3><p>当某个查询需要依赖另一个查询的结果时，可以在查询函数中直接使用await获取依赖数据：</p><figure class="highlight tsx"><table><tr><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">UserProfile</span>(<span class="params">&#123; userId &#125;</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> &#123; <span class="attr">data</span>: user &#125; = <span class="title function_">useQuery</span>(&#123;</span><br><span class="line">    <span class="attr">queryKey</span>: [<span class="string">&#x27;user&#x27;</span>, userId],</span><br><span class="line">    <span class="attr">queryFn</span>: <span class="function">() =&gt;</span> <span class="title function_">fetchUser</span>(userId)</span><br><span class="line">  &#125;)</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> &#123; <span class="attr">data</span>: posts &#125; = <span class="title function_">useQuery</span>(&#123;</span><br><span class="line">    <span class="attr">queryKey</span>: [<span class="string">&#x27;posts&#x27;</span>, userId],</span><br><span class="line">    <span class="attr">queryFn</span>: <span class="function">() =&gt;</span> <span class="title function_">fetchUserPosts</span>(user?.<span class="property">id</span>),</span><br><span class="line">    <span class="attr">enabled</span>: !!user <span class="comment">// 只有当user数据存在时才执行</span></span><br><span class="line">  &#125;)</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 另一种方式是在queryFn中等待</span></span><br><span class="line">  <span class="keyword">const</span> &#123; <span class="attr">data</span>: userPosts &#125; = <span class="title function_">useQuery</span>(&#123;</span><br><span class="line">    <span class="attr">queryKey</span>: [<span class="string">&#x27;posts&#x27;</span>, userId],</span><br><span class="line">    <span class="attr">queryFn</span>: <span class="title function_">async</span> () =&gt; &#123;</span><br><span class="line">      <span class="keyword">const</span> userData = <span class="keyword">await</span> <span class="title function_">fetchUser</span>(userId)</span><br><span class="line">      <span class="keyword">return</span> <span class="title function_">fetchUserPosts</span>(userData.<span class="property">id</span>)</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>对于需要同时发起多个无关查询的场景，可以使用<code>useQueries</code>批量处理：</p><figure class="highlight tsx"><table><tr><td class="code"><pre><span class="line"><span class="keyword">const</span> results = <span class="title function_">useQueries</span>(&#123;</span><br><span class="line">  <span class="attr">queries</span>: [</span><br><span class="line">    &#123; <span class="attr">queryKey</span>: [<span class="string">&#x27;users&#x27;</span>], <span class="attr">queryFn</span>: fetchUsers &#125;,</span><br><span class="line">    &#123; <span class="attr">queryKey</span>: [<span class="string">&#x27;posts&#x27;</span>], <span class="attr">queryFn</span>: fetchPosts &#125;,</span><br><span class="line">    &#123; <span class="attr">queryKey</span>: [<span class="string">&#x27;comments&#x27;</span>], <span class="attr">queryFn</span>: fetchComments &#125;</span><br><span class="line">  ]</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><h3 id="5-2-分页与无限滚动"><a href="#5-2-分页与无限滚动" class="headerlink" title="5.2 分页与无限滚动"></a>5.2 分页与无限滚动</h3><p>React Query对分页和无限滚动提供了原生支持。分页查询的核心是将页码作为查询键的一部分：</p><figure class="highlight tsx"><table><tr><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">PaginatedList</span>(<span class="params">&#123; page &#125;</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> &#123; data, isLoading &#125; = <span class="title function_">useQuery</span>(&#123;</span><br><span class="line">    <span class="attr">queryKey</span>: [<span class="string">&#x27;posts&#x27;</span>, <span class="string">&#x27;list&#x27;</span>, page],</span><br><span class="line">    <span class="attr">queryFn</span>: <span class="function">() =&gt;</span> <span class="title function_">fetchPosts</span>(&#123; page, <span class="attr">limit</span>: <span class="number">10</span> &#125;)</span><br><span class="line">  &#125;)</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 渲染逻辑...</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>对于无限滚动，React Query提供了专门的<code>useInfiniteQuery</code>Hook：</p><figure class="highlight tsx"><table><tr><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">InfinitePosts</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> &#123;</span><br><span class="line">    data,</span><br><span class="line">    fetchNextPage,</span><br><span class="line">    hasNextPage,</span><br><span class="line">    isFetchingNextPage</span><br><span class="line">  &#125; = <span class="title function_">useInfiniteQuery</span>(&#123;</span><br><span class="line">    <span class="attr">queryKey</span>: [<span class="string">&#x27;posts&#x27;</span>, <span class="string">&#x27;infinite&#x27;</span>],</span><br><span class="line">    <span class="attr">queryFn</span>: <span class="function">(<span class="params">&#123; pageParam = <span class="number">1</span> &#125;</span>) =&gt;</span> <span class="title function_">fetchPosts</span>(&#123; <span class="attr">page</span>: pageParam &#125;),</span><br><span class="line">    <span class="attr">getNextPageParam</span>: <span class="function">(<span class="params">lastPage</span>) =&gt;</span> lastPage.<span class="property">nextPage</span></span><br><span class="line">  &#125;)</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      &#123;data.pages.map((page) =&gt;</span></span><br><span class="line"><span class="language-xml">        page.posts.map((post) =&gt; <span class="tag">&lt;<span class="name">PostCard</span> <span class="attr">key</span>=<span class="string">&#123;post.id&#125;</span> <span class="attr">post</span>=<span class="string">&#123;post&#125;</span> /&gt;</span>)</span></span><br><span class="line"><span class="language-xml">      )&#125;</span></span><br><span class="line"><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">button</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">        <span class="attr">onClick</span>=<span class="string">&#123;()</span> =&gt;</span> fetchNextPage()&#125;</span></span><br><span class="line"><span class="language-xml">        disabled=&#123;!hasNextPage || isFetchingNextPage&#125;</span></span><br><span class="line"><span class="language-xml">      &gt;</span></span><br><span class="line"><span class="language-xml">        &#123;isFetchingNextPage ? &#x27;加载中...&#x27; : &#x27;加载更多&#x27;&#125;</span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;/<span class="name">button</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line">  )</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="5-3-SSR服务端渲染支持"><a href="#5-3-SSR服务端渲染支持" class="headerlink" title="5.3 SSR服务端渲染支持"></a>5.3 SSR服务端渲染支持</h3><p>在Next.js等SSR框架中使用React Query时，需要处理服务端数据预取和客户端水合。React Query提供了<code>HydrationBoundary</code>和<code>dehydrate</code>工具：</p><figure class="highlight tsx"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; dehydrate, <span class="title class_">HydrationBoundary</span>, <span class="title class_">QueryClient</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;@tanstack/react-query&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">async</span> <span class="keyword">function</span> <span class="title function_">getServerSideProps</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> queryClient = <span class="keyword">new</span> <span class="title class_">QueryClient</span>()</span><br><span class="line"></span><br><span class="line">  <span class="keyword">await</span> queryClient.<span class="title function_">prefetchQuery</span>(&#123;</span><br><span class="line">    <span class="attr">queryKey</span>: [<span class="string">&#x27;posts&#x27;</span>],</span><br><span class="line">    <span class="attr">queryFn</span>: fetchPosts</span><br><span class="line">  &#125;)</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> &#123;</span><br><span class="line">    <span class="attr">props</span>: &#123;</span><br><span class="line">      <span class="attr">dehydratedState</span>: <span class="title function_">dehydrate</span>(queryClient)</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="keyword">function</span> <span class="title function_">PostsPage</span>(<span class="params">&#123; dehydratedState &#125;</span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">HydrationBoundary</span> <span class="attr">state</span>=<span class="string">&#123;dehydratedState&#125;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">PostsList</span> /&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">HydrationBoundary</span>&gt;</span></span></span><br><span class="line">  )</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这种方案确保服务端预取的数据能够传递到客户端，避免页面加载时的闪烁问题。</p><h3 id="5-4-React-Query结合Next-js-16最佳实践"><a href="#5-4-React-Query结合Next-js-16最佳实践" class="headerlink" title="5.4 React Query结合Next.js 16最佳实践"></a>5.4 React Query结合Next.js 16最佳实践</h3><p>Next.js 16引入了App Router架构，React Query在其中的使用方式与传统Pages Router有所不同。下面详细讲解如何在Next.js 16中最佳实践React Query。</p><h4 id="5-4-1-QueryClientProvider全局配置"><a href="#5-4-1-QueryClientProvider全局配置" class="headerlink" title="5.4.1 QueryClientProvider全局配置"></a>5.4.1 QueryClientProvider全局配置</h4><p>首先需要在应用根布局中配置QueryClientProvider。为了避免服务端和客户端使用不同的实例，我们需要使用React的useState来确保客户端只创建一个QueryClient：</p><figure class="highlight tsx"><table><tr><td class="code"><pre><span class="line"><span class="comment">// app/providers.tsx</span></span><br><span class="line"><span class="string">&#x27;use client&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">QueryClient</span>, <span class="title class_">QueryClientProvider</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;@tanstack/react-query&#x27;</span></span><br><span class="line"><span class="keyword">import</span> &#123; useState, <span class="keyword">type</span> <span class="title class_">ReactNode</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;react&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="keyword">function</span> <span class="title function_">Providers</span>(<span class="params">&#123; children &#125;: &#123; children: ReactNode &#125;</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> [queryClient] = <span class="title function_">useState</span>(</span><br><span class="line">    <span class="function">() =&gt;</span></span><br><span class="line">      <span class="keyword">new</span> <span class="title class_">QueryClient</span>(&#123;</span><br><span class="line">        <span class="attr">defaultOptions</span>: &#123;</span><br><span class="line">          <span class="attr">queries</span>: &#123;</span><br><span class="line">            <span class="comment">// SSR场景下，默认 staleTime 设置更长避免额外请求</span></span><br><span class="line">            <span class="attr">staleTime</span>: <span class="number">60</span> * <span class="number">1000</span>,</span><br><span class="line">          &#125;,</span><br><span class="line">        &#125;,</span><br><span class="line">      &#125;)</span><br><span class="line">  )</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">QueryClientProvider</span> <span class="attr">client</span>=<span class="string">&#123;queryClient&#125;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      &#123;children&#125;</span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">QueryClientProvider</span>&gt;</span></span></span><br><span class="line">  )</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>然后在根布局中使用：</p><figure class="highlight tsx"><table><tr><td class="code"><pre><span class="line"><span class="comment">// app/layout.tsx</span></span><br><span class="line"><span class="keyword">import</span> <span class="title class_">Providers</span> <span class="keyword">from</span> <span class="string">&#x27;./providers&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="keyword">function</span> <span class="title function_">RootLayout</span>(<span class="params">&#123;</span></span><br><span class="line"><span class="params">  children,</span></span><br><span class="line"><span class="params">&#125;: &#123;</span></span><br><span class="line"><span class="params">  children: React.ReactNode</span></span><br><span class="line"><span class="params">&#125;</span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">html</span> <span class="attr">lang</span>=<span class="string">&quot;zh&quot;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">body</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">Providers</span>&gt;</span>&#123;children&#125;<span class="tag">&lt;/<span class="name">Providers</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;/<span class="name">body</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">html</span>&gt;</span></span></span><br><span class="line">  )</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="5-4-2-服务端组件预取-客户端水合"><a href="#5-4-2-服务端组件预取-客户端水合" class="headerlink" title="5.4.2 服务端组件预取 + 客户端水合"></a>5.4.2 服务端组件预取 + 客户端水合</h4><p>在App Router中，我们可以在服务端组件中预取数据，然后传递给客户端组件进行水合：</p><figure class="highlight tsx"><table><tr><td class="code"><pre><span class="line"><span class="comment">// app/posts/page.tsx (服务端组件)</span></span><br><span class="line"><span class="keyword">import</span> &#123; dehydrate, <span class="title class_">HydrationBoundary</span>, <span class="title class_">QueryClient</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;@tanstack/react-query&#x27;</span></span><br><span class="line"><span class="keyword">import</span> <span class="title class_">PostsList</span> <span class="keyword">from</span> <span class="string">&#x27;./PostsList&#x27;</span></span><br><span class="line"><span class="keyword">import</span> &#123; getPosts &#125; <span class="keyword">from</span> <span class="string">&#x27;@/api/posts&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="keyword">async</span> <span class="keyword">function</span> <span class="title function_">PostsPage</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> queryClient = <span class="keyword">new</span> <span class="title class_">QueryClient</span>()</span><br><span class="line"></span><br><span class="line">  <span class="keyword">await</span> queryClient.<span class="title function_">prefetchQuery</span>(&#123;</span><br><span class="line">    <span class="attr">queryKey</span>: [<span class="string">&#x27;posts&#x27;</span>],</span><br><span class="line">    <span class="attr">queryFn</span>: getPosts,</span><br><span class="line">  &#125;)</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="comment">// 将预取的数据传递给客户端组件</span></span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">HydrationBoundary</span> <span class="attr">state</span>=<span class="string">&#123;dehydrate(queryClient)&#125;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">PostsList</span> /&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">HydrationBoundary</span>&gt;</span></span></span><br><span class="line">  )</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><figure class="highlight tsx"><table><tr><td class="code"><pre><span class="line"><span class="comment">// app/posts/PostsList.tsx (客户端组件)</span></span><br><span class="line"><span class="string">&#x27;use client&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> &#123; useQuery &#125; <span class="keyword">from</span> <span class="string">&#x27;@tanstack/react-query&#x27;</span></span><br><span class="line"><span class="keyword">import</span> &#123; getPosts &#125; <span class="keyword">from</span> <span class="string">&#x27;@/api/posts&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="keyword">function</span> <span class="title function_">PostsList</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> &#123; data, isLoading, error &#125; = <span class="title function_">useQuery</span>(&#123;</span><br><span class="line">    <span class="attr">queryKey</span>: [<span class="string">&#x27;posts&#x27;</span>],</span><br><span class="line">    <span class="attr">queryFn</span>: getPosts,</span><br><span class="line">  &#125;)</span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (isLoading) <span class="keyword">return</span> <span class="language-xml"><span class="tag">&lt;<span class="name">div</span>&gt;</span>加载中...<span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line">  <span class="keyword">if</span> (error) <span class="keyword">return</span> <span class="language-xml"><span class="tag">&lt;<span class="name">div</span>&gt;</span>错误: &#123;error.message&#125;<span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">ul</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      &#123;data?.map((post) =&gt; (</span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">li</span> <span class="attr">key</span>=<span class="string">&#123;post.id&#125;</span>&gt;</span>&#123;post.title&#125;<span class="tag">&lt;/<span class="name">li</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      ))&#125;</span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">ul</span>&gt;</span></span></span><br><span class="line">  )</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="5-4-3-静态页面预取优化"><a href="#5-4-3-静态页面预取优化" class="headerlink" title="5.4.3 静态页面预取优化"></a>5.4.3 静态页面预取优化</h4><p>对于博客、文档等静态内容页面，可以利用React Query的缓存策略减少服务端请求：</p><figure class="highlight tsx"><table><tr><td class="code"><pre><span class="line"><span class="comment">// app/blog/[slug]/page.tsx</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="keyword">async</span> <span class="keyword">function</span> <span class="title function_">BlogPost</span>(<span class="params">&#123; params &#125;: &#123; params: &#123; slug: <span class="built_in">string</span> &#125; &#125;</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> queryClient = <span class="keyword">new</span> <span class="title class_">QueryClient</span>()</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 针对静态内容，可以设置较长的 staleTime</span></span><br><span class="line">  <span class="keyword">await</span> queryClient.<span class="title function_">prefetchQuery</span>(&#123;</span><br><span class="line">    <span class="attr">queryKey</span>: [<span class="string">&#x27;blog&#x27;</span>, params.<span class="property">slug</span>],</span><br><span class="line">    <span class="attr">queryFn</span>: <span class="function">() =&gt;</span> <span class="title function_">getBlogPost</span>(params.<span class="property">slug</span>),</span><br><span class="line">    <span class="attr">queryOptions</span>: &#123;</span><br><span class="line">      <span class="attr">staleTime</span>: <span class="number">60</span> * <span class="number">60</span> * <span class="number">1000</span>, <span class="comment">// 1小时内视为新鲜</span></span><br><span class="line">    &#125;,</span><br><span class="line">  &#125;)</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">HydrationBoundary</span> <span class="attr">state</span>=<span class="string">&#123;dehydrate(queryClient)&#125;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">BlogPostContent</span> <span class="attr">slug</span>=<span class="string">&#123;params.slug&#125;</span> /&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">HydrationBoundary</span>&gt;</span></span></span><br><span class="line">  )</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="5-4-4-结合Server-Actions使用Mutation"><a href="#5-4-4-结合Server-Actions使用Mutation" class="headerlink" title="5.4.4 结合Server Actions使用Mutation"></a>5.4.4 结合Server Actions使用Mutation</h4><p>Next.js 16的Server Actions是处理数据修改的强大工具，React Query可以与其完美配合：</p><figure class="highlight tsx"><table><tr><td class="code"><pre><span class="line"><span class="comment">// app/actions.ts</span></span><br><span class="line"><span class="string">&#x27;use server&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> &#123; revalidatePath &#125; <span class="keyword">from</span> <span class="string">&#x27;next/cache&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">async</span> <span class="keyword">function</span> <span class="title function_">createPost</span>(<span class="params"><span class="attr">formData</span>: <span class="title class_">FormData</span></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> title = formData.<span class="title function_">get</span>(<span class="string">&#x27;title&#x27;</span>)</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> res = <span class="keyword">await</span> <span class="title function_">fetch</span>(<span class="string">&#x27;/api/posts&#x27;</span>, &#123;</span><br><span class="line">    <span class="attr">method</span>: <span class="string">&#x27;POST&#x27;</span>,</span><br><span class="line">    <span class="attr">body</span>: <span class="title class_">JSON</span>.<span class="title function_">stringify</span>(&#123; title &#125;),</span><br><span class="line">  &#125;)</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 使posts查询失效，触发重新获取</span></span><br><span class="line">  <span class="title function_">revalidatePath</span>(<span class="string">&#x27;/posts&#x27;</span>)</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> res.<span class="title function_">json</span>()</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><figure class="highlight tsx"><table><tr><td class="code"><pre><span class="line"><span class="comment">// app/posts/CreatePost.tsx</span></span><br><span class="line"><span class="string">&#x27;use client&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> &#123; useMutation, useQueryClient &#125; <span class="keyword">from</span> <span class="string">&#x27;@tanstack/react-query&#x27;</span></span><br><span class="line"><span class="keyword">import</span> &#123; createPost &#125; <span class="keyword">from</span> <span class="string">&#x27;@/app/actions&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">function</span> <span class="title function_">CreatePost</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> queryClient = <span class="title function_">useQueryClient</span>()</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> mutation = <span class="title function_">useMutation</span>(&#123;</span><br><span class="line">    <span class="attr">mutationFn</span>: createPost,</span><br><span class="line">    <span class="attr">onSuccess</span>: <span class="function">() =&gt;</span> &#123;</span><br><span class="line">      <span class="comment">// 手动使缓存失效，确保列表更新</span></span><br><span class="line">      queryClient.<span class="title function_">invalidateQueries</span>(&#123; <span class="attr">queryKey</span>: [<span class="string">&#x27;posts&#x27;</span>] &#125;)</span><br><span class="line">    &#125;,</span><br><span class="line">  &#125;)</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">form</span> <span class="attr">action</span>=<span class="string">&#123;mutation.mutate&#125;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">input</span> <span class="attr">name</span>=<span class="string">&quot;title&quot;</span> <span class="attr">placeholder</span>=<span class="string">&quot;输入标题&quot;</span> /&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">button</span> <span class="attr">type</span>=<span class="string">&quot;submit&quot;</span> <span class="attr">disabled</span>=<span class="string">&#123;mutation.isPending&#125;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">        &#123;mutation.isPending ? &#x27;提交中...&#x27; : &#x27;创建&#x27;&#125;</span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;/<span class="name">button</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">form</span>&gt;</span></span></span><br><span class="line">  )</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="5-4-5-路由切换时的自动刷新"><a href="#5-4-5-路由切换时的自动刷新" class="headerlink" title="5.4.5 路由切换时的自动刷新"></a>5.4.5 路由切换时的自动刷新</h4><p>在Next.js App Router中，结合React Query可以轻松实现路由切换时的数据刷新：</p><figure class="highlight tsx"><table><tr><td class="code"><pre><span class="line"><span class="comment">// app/posts/[id]/page.tsx</span></span><br><span class="line"><span class="keyword">import</span> &#123; getPost &#125; <span class="keyword">from</span> <span class="string">&#x27;@/api/posts&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="keyword">async</span> <span class="keyword">function</span> <span class="title function_">PostPage</span>(<span class="params">&#123; params &#125;: &#123; params: &#123; id: <span class="built_in">string</span> &#125; &#125;</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> queryClient = <span class="keyword">new</span> <span class="title class_">QueryClient</span>()</span><br><span class="line"></span><br><span class="line">  <span class="keyword">await</span> queryClient.<span class="title function_">prefetchQuery</span>(&#123;</span><br><span class="line">    <span class="attr">queryKey</span>: [<span class="string">&#x27;post&#x27;</span>, params.<span class="property">id</span>],</span><br><span class="line">    <span class="attr">queryFn</span>: <span class="function">() =&gt;</span> <span class="title function_">getPost</span>(params.<span class="property">id</span>),</span><br><span class="line">  &#125;)</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">HydrationBoundary</span> <span class="attr">state</span>=<span class="string">&#123;dehydrate(queryClient)&#125;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">PostDetail</span> <span class="attr">postId</span>=<span class="string">&#123;params.id&#125;</span> /&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">HydrationBoundary</span>&gt;</span></span></span><br><span class="line">  )</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><figure class="highlight tsx"><table><tr><td class="code"><pre><span class="line"><span class="comment">// app/posts/[id]/PostDetail.tsx</span></span><br><span class="line"><span class="string">&#x27;use client&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> &#123; useQuery &#125; <span class="keyword">from</span> <span class="string">&#x27;@tanstack/react-query&#x27;</span></span><br><span class="line"><span class="keyword">import</span> &#123; getPost &#125; <span class="keyword">from</span> <span class="string">&#x27;@/api/posts&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="keyword">function</span> <span class="title function_">PostDetail</span>(<span class="params">&#123; postId &#125;: &#123; postId: <span class="built_in">string</span> &#125;</span>) &#123;</span><br><span class="line">  <span class="comment">// 当postId变化时，React Query会自动触发新的请求</span></span><br><span class="line">  <span class="keyword">const</span> &#123; data, isLoading &#125; = <span class="title function_">useQuery</span>(&#123;</span><br><span class="line">    <span class="attr">queryKey</span>: [<span class="string">&#x27;post&#x27;</span>, postId],</span><br><span class="line">    <span class="attr">queryFn</span>: <span class="function">() =&gt;</span> <span class="title function_">getPost</span>(postId),</span><br><span class="line">  &#125;)</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 渲染逻辑...</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>Next.js 16与React Query的结合，使得服务端预取、客户端水合、数据变更都变得非常优雅。这种方案既保留了服务端渲染的SEO优势和首屏加载速度，又充分利用了React Query强大的客户端状态管理能力。</p><h2 id="六、性能优化策略"><a href="#六、性能优化策略" class="headerlink" title="六、性能优化策略"></a>六、性能优化策略</h2><h3 id="6-1-结构化共享与引用稳定性"><a href="#6-1-结构化共享与引用稳定性" class="headerlink" title="6.1 结构化共享与引用稳定性"></a>6.1 结构化共享与引用稳定性</h3><p>React Query默认启用结构化共享（Structural Sharing）来优化性能。当服务端返回的数据与缓存相同时，Query会保持原有引用不变，这使得配合useMemo和useCallback使用时能有效避免不必要的重渲染：</p><figure class="highlight tsx"><table><tr><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">PostList</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> &#123; data &#125; = <span class="title function_">useQuery</span>(&#123;</span><br><span class="line">    <span class="attr">queryKey</span>: [<span class="string">&#x27;posts&#x27;</span>],</span><br><span class="line">    <span class="attr">queryFn</span>: fetchPosts</span><br><span class="line">  &#125;)</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 依赖data的回调函数</span></span><br><span class="line">  <span class="keyword">const</span> handleClick = <span class="title function_">useCallback</span>(<span class="function">(<span class="params">id</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">&#x27;Clicked:&#x27;</span>, id)</span><br><span class="line">  &#125;, [])</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">ul</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      &#123;data?.map((post) =&gt; (</span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">PostItem</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">          <span class="attr">key</span>=<span class="string">&#123;post.id&#125;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">          <span class="attr">post</span>=<span class="string">&#123;post&#125;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">          <span class="attr">onClick</span>=<span class="string">&#123;handleClick&#125;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">        /&gt;</span></span></span><br><span class="line"><span class="language-xml">      ))&#125;</span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">ul</span>&gt;</span></span></span><br><span class="line">  )</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在大多数场景下，默认的结构化共享已经足够高效。如果确实需要禁用此特性，可以将<code>structuralSharing</code>设置为false。</p><h3 id="6-2-保持查询活跃"><a href="#6-2-保持查询活跃" class="headerlink" title="6.2 保持查询活跃"></a>6.2 保持查询活跃</h3><p>默认情况下，当所有使用该查询的组件都卸载后，查询会进入非活跃状态并在5分钟后被垃圾回收。但在某些场景下，我们希望查询数据继续保持活跃状态：</p><figure class="highlight tsx"><table><tr><td class="code"><pre><span class="line"><span class="comment">// 方式一：使用keepPreviousData保持上一页数据</span></span><br><span class="line"><span class="keyword">const</span> &#123; data &#125; = <span class="title function_">useQuery</span>(&#123;</span><br><span class="line">  <span class="attr">queryKey</span>: [<span class="string">&#x27;posts&#x27;</span>, page],</span><br><span class="line">  <span class="attr">queryFn</span>: <span class="function">() =&gt;</span> <span class="title function_">fetchPosts</span>(&#123; page &#125;),</span><br><span class="line">  <span class="attr">placeholderData</span>: keepPreviousData</span><br><span class="line">&#125;)</span><br><span class="line"></span><br><span class="line"><span class="comment">// 方式二：设置较长的staleTime避免自动失效</span></span><br><span class="line"><span class="keyword">const</span> &#123; data &#125; = <span class="title function_">useQuery</span>(&#123;</span><br><span class="line">  <span class="attr">queryKey</span>: [<span class="string">&#x27;posts&#x27;</span>],</span><br><span class="line">  <span class="attr">queryFn</span>: fetchPosts,</span><br><span class="line">  <span class="attr">staleTime</span>: <span class="title class_">Infinity</span> <span class="comment">// 数据永不过期，需要手动invalidate</span></span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><h3 id="6-3-窗口焦点重新获取"><a href="#6-3-窗口焦点重新获取" class="headerlink" title="6.3 窗口焦点重新获取"></a>6.3 窗口焦点重新获取</h3><p>用户切换浏览器标签页后再返回时，自动刷新数据是一个常见需求。React Query默认启用这一行为，但可以根据场景调整：</p><figure class="highlight tsx"><table><tr><td class="code"><pre><span class="line"><span class="keyword">const</span> &#123; data &#125; = <span class="title function_">useQuery</span>(&#123;</span><br><span class="line">  <span class="attr">queryKey</span>: [<span class="string">&#x27;posts&#x27;</span>],</span><br><span class="line">  <span class="attr">queryFn</span>: fetchPosts,</span><br><span class="line">  <span class="comment">// 禁用窗口焦点刷新</span></span><br><span class="line">  <span class="attr">refetchOnWindowFocus</span>: <span class="literal">false</span>,</span><br><span class="line">  <span class="comment">// 或者只在数据过时时才刷新</span></span><br><span class="line">  <span class="attr">refetchOnWindowFocus</span>: <span class="function">(<span class="params">query</span>) =&gt;</span> query.<span class="property">state</span>.<span class="property">dataUpdatedAt</span> &gt; <span class="number">0</span></span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><h2 id="七、常见应用场景总结"><a href="#七、常见应用场景总结" class="headerlink" title="七、常见应用场景总结"></a>七、常见应用场景总结</h2><p>React Query几乎能解决现代React应用中的所有数据获取需求。在用户个人资料展示场景中，可以用useQuery获取用户信息并设置较长的缓存时间减少请求。在待办事项管理场景中，create、update、delete操作配合乐观更新能带来流畅的用户体验。在实时数据看板场景中，配置合适的refetchInterval实现数据定时刷新。在搜索功能实现中，结合debounce和enabled选项避免不必要的请求。</p><p>总的来说，React Query已经成为React生态中数据获取的事实标准。它不仅大幅简化了数据获取的代码量，更重要的是提供了专业级的缓存管理、错误处理和性能优化能力。无论是小型项目还是大型企业应用，React Query都能提供显著的开发体验提升和性能改进。</p><p>掌握React Query，将让你在处理服务端状态时更加得心应手，构建出更优质的React应用。</p>]]>
    </content>
    <id>http://fe.poetries.top/2025/10/06/tanstack-query-guide/</id>
    <link href="http://fe.poetries.top/2025/10/06/tanstack-query-guide/"/>
    <published>2025-10-06T08:20:00.000Z</published>
    <summary>深度解读React Query数据获取库的核心价值，详解useQuery/useMutationHooks、缓存策略、乐观更新等高级特性，带你全面掌握现代React应用数据管理。</summary>
    <title>深入理解TanStack Query核心价值与实战技巧</title>
    <updated>2026-03-08T10:22:42.100Z</updated>
  </entry>
  <entry>
    <author>
      <name>Poetry</name>
    </author>
    <category term="Front-End" scheme="http://fe.poetries.top/categories/Front-End/"/>
    <category term="React" scheme="http://fe.poetries.top/tags/React/"/>
    <category term="状态管理" scheme="http://fe.poetries.top/tags/%E7%8A%B6%E6%80%81%E7%AE%A1%E7%90%86/"/>
    <category term="Redux" scheme="http://fe.poetries.top/tags/Redux/"/>
    <category term="Zustand" scheme="http://fe.poetries.top/tags/Zustand/"/>
    <category term="前端性能优化" scheme="http://fe.poetries.top/tags/%E5%89%8D%E7%AB%AF%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/"/>
    <content>
      <![CDATA[<p>在React项目中，状态管理一直是核心议题。随着应用规模增长，如何在组件之间高效共享状态、如何避免不必要的渲染、如何优雅地管理复杂数据…这些问题直接影响着开发体验和应用性能。</p><p>市面上的状态管理方案繁多，从Redux到Zustand，从Jotai到Valtio，每个方案都有其独特的设计理念和适用场景。本文将从实际开发需求出发，系统性地对比分析主流状态管理库，帮助你做出明智的技术选型。</p><h2 id="代码写法对比"><a href="#代码写法对比" class="headerlink" title="代码写法对比"></a>代码写法对比</h2><p>为了更直观地理解各状态管理库的差异，先看一个简单场景的写法对比：</p><table><thead><tr><th>特性</th><th>Zustand</th><th>Jotai</th><th>Valtio</th><th>Redux</th></tr></thead><tbody><tr><td><strong>定义方式</strong></td><td>create() 创建</td><td>atom() 原子</td><td>proxy() 代理</td><td>createStore()</td></tr><tr><td><strong>读取状态</strong></td><td>useStore()</td><td>useAtom()</td><td>useSnapshot()</td><td>useSelector()</td></tr><tr><td><strong>更新状态</strong></td><td>set()</td><td>setAtom()</td><td>直接赋值</td><td>dispatch()</td></tr><tr><td><strong>代码量</strong></td><td>少</td><td>中</td><td>最少</td><td>多</td></tr><tr><td><strong>样板代码</strong></td><td>无</td><td>无</td><td>无</td><td>需要action&#x2F;reducer</td></tr></tbody></table><h3 id="基础用法对比"><a href="#基础用法对比" class="headerlink" title="基础用法对比"></a>基础用法对比</h3><figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="comment">// Zustand - 最简洁</span></span><br><span class="line"><span class="keyword">import</span> &#123; create &#125; <span class="keyword">from</span> <span class="string">&#x27;zustand&#x27;</span></span><br><span class="line"><span class="keyword">const</span> useStore = <span class="title function_">create</span>(<span class="function"><span class="params">set</span> =&gt;</span> (&#123;</span><br><span class="line">  <span class="attr">count</span>: <span class="number">0</span>,</span><br><span class="line">  <span class="attr">inc</span>: <span class="function">() =&gt;</span> <span class="title function_">set</span>(<span class="function"><span class="params">s</span> =&gt;</span> (&#123; <span class="attr">count</span>: s.<span class="property">count</span> + <span class="number">1</span> &#125;))</span><br><span class="line">&#125;))</span><br><span class="line"><span class="comment">// 使用</span></span><br><span class="line"><span class="keyword">const</span> &#123; count, inc &#125; = <span class="title function_">useStore</span>()</span><br><span class="line"></span><br><span class="line"><span class="comment">// Jotai - 原子化</span></span><br><span class="line"><span class="keyword">import</span> &#123; atom, useAtom &#125; <span class="keyword">from</span> <span class="string">&#x27;jotai&#x27;</span></span><br><span class="line"><span class="keyword">const</span> countAtom = <span class="title function_">atom</span>(<span class="number">0</span>)</span><br><span class="line"><span class="keyword">const</span> countAtom2 = <span class="title function_">atom</span>(<span class="function"><span class="params">get</span> =&gt;</span> <span class="title function_">get</span>(countAtom) * <span class="number">2</span>) <span class="comment">// 派生</span></span><br><span class="line"><span class="comment">// 使用</span></span><br><span class="line"><span class="keyword">const</span> [count, setCount] = <span class="title function_">useAtom</span>(countAtom)</span><br><span class="line"></span><br><span class="line"><span class="comment">// Valtio - 响应式代理</span></span><br><span class="line"><span class="keyword">import</span> &#123; proxy, useSnapshot &#125; <span class="keyword">from</span> <span class="string">&#x27;valtio&#x27;</span></span><br><span class="line"><span class="keyword">const</span> state = <span class="title function_">proxy</span>(&#123; <span class="attr">count</span>: <span class="number">0</span> &#125;)</span><br><span class="line"><span class="comment">// 使用</span></span><br><span class="line"><span class="keyword">const</span> snap = <span class="title function_">useSnapshot</span>(state)</span><br><span class="line">state.<span class="property">count</span>++ <span class="comment">// 直接修改</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// Redux - 样板代码多</span></span><br><span class="line"><span class="keyword">const</span> <span class="variable constant_">INCREMENT</span> = <span class="string">&#x27;INCREMENT&#x27;</span></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">counterReducer</span> = (<span class="params">state = &#123; count: <span class="number">0</span> &#125;, action</span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">switch</span> (action.<span class="property">type</span>) &#123;</span><br><span class="line">    <span class="keyword">case</span> <span class="attr">INCREMENT</span>: <span class="keyword">return</span> &#123; <span class="attr">count</span>: state.<span class="property">count</span> + <span class="number">1</span> &#125;</span><br><span class="line">    <span class="attr">default</span>: <span class="keyword">return</span> state</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">// 使用</span></span><br><span class="line"><span class="keyword">const</span> dispatch = <span class="title function_">useDispatch</span>()</span><br><span class="line"><span class="keyword">const</span> count = <span class="title function_">useSelector</span>(<span class="function"><span class="params">state</span> =&gt;</span> state.<span class="property">counter</span>.<span class="property">count</span>)</span><br></pre></td></tr></table></figure><h3 id="状态隔离对比"><a href="#状态隔离对比" class="headerlink" title="状态隔离对比"></a>状态隔离对比</h3><table><thead><tr><th>方案</th><th>隔离方式</th><th>适用场景</th></tr></thead><tbody><tr><td>Zustand</td><td>每个模块独立文件</td><td>推荐，按页面&#x2F;功能拆分</td></tr><tr><td>Jotai</td><td>atom key + Provider</td><td>需要运行时动态创建</td></tr><tr><td>Valtio</td><td>proxy实例</td><td>多实例编辑器</td></tr><tr><td>Redux</td><td>reducer拆分 + combineReducers</td><td>大型项目</td></tr></tbody></table><h3 id="避免无效渲染对比"><a href="#避免无效渲染对比" class="headerlink" title="避免无效渲染对比"></a>避免无效渲染对比</h3><table><thead><tr><th>方案</th><th>精准更新方式</th><th>复杂度</th></tr></thead><tbody><tr><td>Zustand</td><td>Selector选择</td><td>需要手动写</td></tr><tr><td>Jotai</td><td>原子订阅</td><td>自动</td></tr><tr><td>Valtio</td><td>Proxy追踪</td><td>自动</td></tr><tr><td>Redux</td><td>useSelector</td><td>需要手动写</td></tr></tbody></table><figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="comment">// Zustand - 需要Selector</span></span><br><span class="line"><span class="keyword">const</span> count = <span class="title function_">useStore</span>(<span class="function"><span class="params">s</span> =&gt;</span> s.<span class="property">count</span>) <span class="comment">// 只订阅count</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// Jotai - 自动精准</span></span><br><span class="line"><span class="keyword">const</span> [count] = <span class="title function_">useAtom</span>(countAtom) <span class="comment">// 只订阅该原子</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// Valtio - 自动精准</span></span><br><span class="line"><span class="keyword">const</span> snap = <span class="title function_">useSnapshot</span>(state) <span class="comment">// 用了哪个属性就订阅哪个</span></span><br></pre></td></tr></table></figure><h2 id="核心需求维度"><a href="#核心需求维度" class="headerlink" title="核心需求维度"></a>核心需求维度</h2><p>在选择状态管理库之前，我们需要明确四个核心需求：</p><h3 id="全局状态共享"><a href="#全局状态共享" class="headerlink" title="全局状态共享"></a>全局状态共享</h3><p>最基本的需求是：任意两个或多个组件之间能够利用状态管理工具互相通信，不需要通过props层层传递，实现真正的全局通信。</p><p>主流方案在这个维度上都能满足需求，包括：Context API（React内置）、Redux（Flux模式单向数据流）、Zustand（轻量级Hook流派）、Recoil&#x2F;Jotai（原子化）、Mobx&#x2F;Valtio（响应式）、dva（Redux增强版）。</p><h3 id="状态隔离与模块化"><a href="#状态隔离与模块化" class="headerlink" title="状态隔离与模块化"></a>状态隔离与模块化</h3><p>当项目规模变大，全局状态的合理分区变得尤为重要。好的状态管理器应该支持数据隔离，在做到全局共享的同时，避免不同业务模块之间的状态冲突。</p><p>Redux通过定义独立的reducer来区分不同模块的状态；Zustand推荐为每个页面创建独立的store文件，实现物理层面的隔离；Recoil通过atom的key值确保全局唯一性；Jotai支持为每个页面使用独立的Provider包裹；Mobx为每个模块创建Store实例后在RootStore中合并；Valtio则为每个模块创建独立的proxy实例。</p><h3 id="避免无效渲染"><a href="#避免无效渲染" class="headerlink" title="避免无效渲染"></a>避免无效渲染</h3><p>这是React性能优化的关键。状态管理库应该能够帮助组件只在状态真正变化时才重新渲染，而不是任何状态变化都触发全量更新。</p><p>Context API在这方面表现最差，它几乎无法避免冗余渲染，甚至本身就是问题来源之一。Redustand可以通过ux和ZSelector模式避免无效渲染，但需要开发者注意写法。Mobx和Valtio在响应式更新方面表现优异，能够做到属性级别的细粒度更新，轻松处理上万条数据。Jotai通过原子化实现按需收集依赖，避免初始化时的高性能消耗。</p><h3 id="多实例支持"><a href="#多实例支持" class="headerlink" title="多实例支持"></a>多实例支持</h3><p>除了全局状态，有时我们只需要部分组件共享状态，并且这些组件会在项目中创建多个实例。这时单例模式就无法满足需求，需要状态库支持多实例（沙箱隔离）能力。</p><p>Context API、Redux、Recoil、Jotai、Mobx、Valtio都原生支持多实例。Zustand需要结合Context API，将单例store存储在Context的useRef中实现多实例，这是一个大厂面试的常见考点。</p><h2 id="三种设计范式"><a href="#三种设计范式" class="headerlink" title="三种设计范式"></a>三种设计范式</h2><p>从底层设计来看，主流状态管理库可以分为三大类：</p><table><thead><tr><th>设计范式</th><th>代表方案</th></tr></thead><tbody><tr><td>单向数据流</td><td>Redux、Zustand</td></tr><tr><td>原子化</td><td>Recoil、Jotai</td></tr><tr><td>Proxy代理</td><td>Mobx、Valtio</td></tr></tbody></table><p><strong>单向数据流</strong>的优势在于数据流动清晰、可预测性强。但当数据结构非常复杂时，通常需要结合Immer.js等不可变数据工具才能达到最佳性能表现。</p><p><strong>原子化</strong>和<strong>Proxy代理</strong>的核心理念一致——都是建立数据与UI的绑定关系，当数据变化时UI自动更新。二者的区别在于：原子化需要先定义原子，再通过原子管理数据；Proxy则是先定义一个大对象，通过劫持属性来实现绑定。从性能角度看，原子化略胜一筹，因为省去了劫持过程。但当数据复杂度提升，原子化的写法会变得繁琐。</p><h2 id="性能深度对比"><a href="#性能深度对比" class="headerlink" title="性能深度对比"></a>性能深度对比</h2><h3 id="大型列表场景"><a href="#大型列表场景" class="headerlink" title="大型列表场景"></a>大型列表场景</h3><p>处理复杂列表数据时，三种主流方案的差异明显：</p><table><thead><tr><th>方案</th><th>初始化速度</th><th>更新性能</th><th>内存占用</th><th>开发复杂度</th></tr></thead><tbody><tr><td>Zustand</td><td>极快</td><td>快</td><td>极低（原生对象）</td><td>较高</td></tr><tr><td>Jotai</td><td>较慢</td><td>精准、快</td><td>最高（原子实例多）</td><td>偏高</td></tr><tr><td>Valtio</td><td>中等偏慢</td><td>精准、快</td><td>偏高（Proxy开销）</td><td>低</td></tr></tbody></table><h3 id="更新机制解析"><a href="#更新机制解析" class="headerlink" title="更新机制解析"></a>更新机制解析</h3><p><strong>Zustand</strong>采用O(N)的通知复杂度，通过线性遍历订阅列表配合Selector比对实现更新。这种方式在数据量较大时会有一定性能开销，但由于使用原生JavaScript对象，内存占用极低。</p><p><strong>Jotai</strong>凭借依赖图实现O(1)的通知和渲染复杂度。当某个原子变化时，直接定位到订阅了该原子的组件，精准更新。不过在初始化阶段，Jotai需要为每个列表项创建Atom对象，这在超长列表场景下会导致内存急剧增长。</p><p><strong>Valtio</strong>通过Proxy追踪，同样实现O(1)的更新效率。属性变化时，直接通知访问过该属性的组件。但Proxy对象的内存开销较大，且需要维护额外的映射关系。</p><h3 id="内存占用详解"><a href="#内存占用详解" class="headerlink" title="内存占用详解"></a>内存占用详解</h3><p><strong>Zustand</strong>是内存利用率的王者。其store本质上是闭包中的普通JavaScript对象，一万条数据几乎只占用一万条原始JSON数据的内存。额外开销仅有一个微小的订阅列表。在内存受限或数据量巨大的场景下，Zustand是首选。</p><p><strong>Jotai</strong>在长列表场景下是内存消耗重灾区。为实现精准更新，splitAtom模式会为数组中每个元素创建Atom对象。配合WeakMap维护的原子状态映射，内存占用呈线性爆发式增长，可能导致浏览器频繁GC引发掉帧。</p><p><strong>Valtio</strong>的Proxy机制带来额外负荷。嵌套对象和数组都会被转化为Proxy实例，useSnapshot会创建状态快照。虽然使用结构共享复用未变动部分，但渲染瞬间仍会产生临时对象。不过在中等规模（千级数据）应用中完全可接受。</p><h2 id="场景化选型建议"><a href="#场景化选型建议" class="headerlink" title="场景化选型建议"></a>场景化选型建议</h2><h3 id="大型数据量场景"><a href="#大型数据量场景" class="headerlink" title="大型数据量场景"></a>大型数据量场景</h3><p>如果你的应用涉及在线Excel、大型看板、轨迹数据等万级数据量场景，<strong>Zustand</strong>是最佳选择。此时内存和初始化速度是生死线，无法承受为每个数据点创建Proxy或Atom的开销，需要最原始的JS对象和手动优化的订阅逻辑。</p><h3 id="中等规模复杂交互"><a href="#中等规模复杂交互" class="headerlink" title="中等规模复杂交互"></a>中等规模复杂交互</h3><p>对于多列配置列表、复杂逻辑购物车等中等规模但交互极复杂的场景，<strong>Valtio</strong>更为合适。虽然内存占用略高于Zustand，但自动追踪功能能省去大量Selector代码，开发效率提升显著。</p><h3 id="动态增减频繁场景"><a href="#动态增减频繁场景" class="headerlink" title="动态增减频繁场景"></a>动态增减频繁场景</h3><p>多页签编辑器、独立任务卡片等需要动态创建销毁的场景，<strong>Jotai</strong>是更好的选择。其生命周期管理是独特优势——组件卸载时对应的Atom状态会被自动垃圾回收，保持内存健康。动态列表场景下能帮你维持内存的”新鲜度”。</p><h3 id="简单场景"><a href="#简单场景" class="headerlink" title="简单场景"></a>简单场景</h3><p>如果项目没有特别复杂的数据结构，那么选择哪个都可以，此时更多考虑的是编码偏好：</p><p>追求极致轻量（省内存、省CPU）选择<strong>Zustand</strong>；追求逻辑严密（原子组合、按需销毁）选择<strong>Jotai</strong>；追求开发爽感（自动优化、代码最少）选择<strong>Valtio</strong>。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>选择状态管理库需要综合考虑项目规模、数据复杂度、交互频率和团队偏好。Zustand适合追求性能和轻量的团队，Valtio适合追求开发效率的场景，Jotai则在动态生命周期管理上有独特优势。</p><p>没有绝对的最佳方案，只有最适合当前业务场景的选择。希望本文的对比分析能帮助你在技术选型时做出更明智的决策。</p>]]>
    </content>
    <id>http://fe.poetries.top/2025/09/21/react-state-management-comparison/</id>
    <link href="http://fe.poetries.top/2025/09/21/react-state-management-comparison/"/>
    <published>2025-09-21T06:40:12.000Z</published>
    <summary>深度对比Zustand、Jotai、Valtio、Redux等主流React状态管理库，从性能、内存占用、开发体验等维度分析，帮你找到最适合项目的方案。</summary>
    <title>React状态管理库选型指南：主流方案对比与实战推荐</title>
    <updated>2026-03-08T10:22:42.095Z</updated>
  </entry>
  <entry>
    <author>
      <name>Poetry</name>
    </author>
    <category term="Front-End" scheme="http://fe.poetries.top/categories/Front-End/"/>
    <category term="ESLint" scheme="http://fe.poetries.top/tags/ESLint/"/>
    <category term="Prettier" scheme="http://fe.poetries.top/tags/Prettier/"/>
    <category term="Husky" scheme="http://fe.poetries.top/tags/Husky/"/>
    <category term="Commitlint" scheme="http://fe.poetries.top/tags/Commitlint/"/>
    <category term="Lint-staged" scheme="http://fe.poetries.top/tags/Lint-staged/"/>
    <category term="前端工程化" scheme="http://fe.poetries.top/tags/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/"/>
    <content>
      <![CDATA[<p>在前端项目开发中，代码规范是保证代码质量和团队协作效率的基础。一个完善的代码规范体系不仅能统一团队成员的编码风格，还能在代码提交前自动检查问题，减少代码review的成本。</p><p>本文将手把手教你搭建一套完整的前端开发规范体系，基于ESLint 9新配置系统，配合Husky、Prettier、Commitlint、Lint-staged等工具，实现代码提交前的自动检查和格式化。</p><h2 id="一、项目初始化与依赖安装"><a href="#一、项目初始化与依赖安装" class="headerlink" title="一、项目初始化与依赖安装"></a>一、项目初始化与依赖安装</h2><h3 id="创建Next-js-16项目"><a href="#创建Next-js-16项目" class="headerlink" title="创建Next.js 16项目"></a>创建Next.js 16项目</h3><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">npx create-next-app@latest my-app --typescript --tailwind --eslint</span><br><span class="line"><span class="built_in">cd</span> my-app</span><br></pre></td></tr></table></figure><h3 id="安装ESLint-9相关依赖"><a href="#安装ESLint-9相关依赖" class="headerlink" title="安装ESLint 9相关依赖"></a>安装ESLint 9相关依赖</h3><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">npm install eslint@9 @eslint/js@9 --save-dev</span><br><span class="line">npm install typescript-eslint@8 --save-dev</span><br><span class="line">npm install eslint-config-next@16 --save-dev</span><br><span class="line">npm install eslint-plugin-simple-import-sort@12 eslint-plugin-unused-imports@4 --save-dev</span><br><span class="line">npm install @eslint/compat@1 --save-dev</span><br></pre></td></tr></table></figure><h3 id="安装Prettier相关依赖"><a href="#安装Prettier相关依赖" class="headerlink" title="安装Prettier相关依赖"></a>安装Prettier相关依赖</h3><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">npm install prettier@3 prettier-plugin-tailwindcss@0.7 --save-dev</span><br><span class="line">npm install eslint-config-prettier@10 --save-dev</span><br></pre></td></tr></table></figure><h3 id="安装Git钩子相关依赖"><a href="#安装Git钩子相关依赖" class="headerlink" title="安装Git钩子相关依赖"></a>安装Git钩子相关依赖</h3><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">npm install husky@9 lint-staged@16 --save-dev</span><br><span class="line">npm install @commitlint/cli@17 @commitlint/config-conventional@17 --save-dev</span><br></pre></td></tr></table></figure><h3 id="完整依赖清单"><a href="#完整依赖清单" class="headerlink" title="完整依赖清单"></a>完整依赖清单</h3><figure class="highlight json"><table><tr><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;devDependencies&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;@commitlint/cli&quot;</span><span class="punctuation">:</span> <span class="string">&quot;^17.7.2&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;@commitlint/config-conventional&quot;</span><span class="punctuation">:</span> <span class="string">&quot;^17.7.0&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;@eslint/compat&quot;</span><span class="punctuation">:</span> <span class="string">&quot;^1.4.1&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;@eslint/eslintrc&quot;</span><span class="punctuation">:</span> <span class="string">&quot;^3&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;eslint&quot;</span><span class="punctuation">:</span> <span class="string">&quot;^9.39.0&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;eslint-config-next&quot;</span><span class="punctuation">:</span> <span class="string">&quot;^16.0.1&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;eslint-config-prettier&quot;</span><span class="punctuation">:</span> <span class="string">&quot;^10.1.8&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;eslint-plugin-prettier&quot;</span><span class="punctuation">:</span> <span class="string">&quot;^5.5.4&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;eslint-plugin-react-hooks&quot;</span><span class="punctuation">:</span> <span class="string">&quot;^7.0.1&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;eslint-plugin-simple-import-sort&quot;</span><span class="punctuation">:</span> <span class="string">&quot;^12.1.1&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;eslint-plugin-unused-imports&quot;</span><span class="punctuation">:</span> <span class="string">&quot;^4.3.0&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;husky&quot;</span><span class="punctuation">:</span> <span class="string">&quot;^9.1.7&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;lint-staged&quot;</span><span class="punctuation">:</span> <span class="string">&quot;^16.2.6&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;prettier&quot;</span><span class="punctuation">:</span> <span class="string">&quot;^3.6.2&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;prettier-plugin-tailwindcss&quot;</span><span class="punctuation">:</span> <span class="string">&quot;^0.7.1&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;typescript&quot;</span><span class="punctuation">:</span> <span class="string">&quot;^5.9.3&quot;</span></span><br><span class="line">  <span class="punctuation">&#125;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><h2 id="二、ESLint-9扁平化配置"><a href="#二、ESLint-9扁平化配置" class="headerlink" title="二、ESLint 9扁平化配置"></a>二、ESLint 9扁平化配置</h2><p>ESLint 9采用了全新的Flat Config（扁平化配置）系统，配置文件格式从<code>.eslintrc.json</code>变为<code>eslint.config.mjs</code>。</p><h3 id="创建ESLint配置文件"><a href="#创建ESLint配置文件" class="headerlink" title="创建ESLint配置文件"></a>创建ESLint配置文件</h3><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="comment">// eslint.config.mjs</span></span><br><span class="line"><span class="keyword">import</span> &#123; defineConfig, globalIgnores &#125; <span class="keyword">from</span> <span class="string">&#x27;eslint/config&#x27;</span></span><br><span class="line"><span class="keyword">import</span> nextVitals <span class="keyword">from</span> <span class="string">&#x27;eslint-config-next/core-web-vitals&#x27;</span></span><br><span class="line"><span class="keyword">import</span> nextTs <span class="keyword">from</span> <span class="string">&#x27;eslint-config-next/typescript&#x27;</span></span><br><span class="line"><span class="keyword">import</span> tseslint <span class="keyword">from</span> <span class="string">&#x27;typescript-eslint&#x27;</span></span><br><span class="line"><span class="keyword">import</span> simpleImportSort <span class="keyword">from</span> <span class="string">&#x27;eslint-plugin-simple-import-sort&#x27;</span></span><br><span class="line"><span class="keyword">import</span> unusedImports <span class="keyword">from</span> <span class="string">&#x27;eslint-plugin-unused-imports&#x27;</span></span><br><span class="line"><span class="keyword">import</span> &#123; fixupPluginRules &#125; <span class="keyword">from</span> <span class="string">&#x27;@eslint/compat&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// ESLint 9扁平化配置</span></span><br><span class="line"><span class="keyword">const</span> eslintConfig = <span class="title function_">defineConfig</span>([</span><br><span class="line">  ...nextVitals,</span><br><span class="line">  ...nextTs,</span><br><span class="line">  tseslint.<span class="property">configs</span>.<span class="property">recommendedTypeChecked</span>,</span><br><span class="line">  &#123;</span><br><span class="line">    <span class="attr">files</span>: [<span class="string">&#x27;src/**/*.&#123;js,jsx,ts,tsx&#125;&#x27;</span>],</span><br><span class="line">    <span class="attr">plugins</span>: &#123;</span><br><span class="line">      <span class="string">&#x27;simple-import-sort&#x27;</span>: <span class="title function_">fixupPluginRules</span>(simpleImportSort),</span><br><span class="line">      <span class="string">&#x27;unused-imports&#x27;</span>: <span class="title function_">fixupPluginRules</span>(unusedImports)</span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="attr">extends</span>: [],</span><br><span class="line">    <span class="attr">languageOptions</span>: &#123;</span><br><span class="line">      <span class="attr">parserOptions</span>: &#123;</span><br><span class="line">        <span class="attr">projectService</span>: <span class="literal">true</span></span><br><span class="line">      &#125;,</span><br><span class="line">      <span class="attr">globals</span>: &#123;</span><br><span class="line">        <span class="attr">JSX</span>: <span class="literal">true</span></span><br><span class="line">      &#125;</span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="attr">rules</span>: &#123;</span><br><span class="line">      <span class="comment">// 关闭部分严格规则，适应项目需求</span></span><br><span class="line">      <span class="attr">semi</span>: <span class="string">&#x27;off&#x27;</span>,</span><br><span class="line">      <span class="string">&#x27;@typescript-eslint/explicit-member-accessibility&#x27;</span>: <span class="string">&#x27;off&#x27;</span>,</span><br><span class="line">      <span class="string">&#x27;trailing-comma&#x27;</span>: <span class="string">&#x27;off&#x27;</span>,</span><br><span class="line">      <span class="string">&#x27;simple-import-sort/imports&#x27;</span>: <span class="string">&#x27;warn&#x27;</span>,</span><br><span class="line">      <span class="string">&#x27;simple-import-sort/exports&#x27;</span>: <span class="string">&#x27;warn&#x27;</span>,</span><br><span class="line">      <span class="string">&#x27;@typescript-eslint/no-explicit-any&#x27;</span>: <span class="string">&#x27;off&#x27;</span>,</span><br><span class="line">      <span class="string">&#x27;@typescript-eslint/ban-ts-comment&#x27;</span>: <span class="string">&#x27;off&#x27;</span>,</span><br><span class="line">      <span class="string">&#x27;@typescript-eslint/no-var-requires&#x27;</span>: <span class="string">&#x27;off&#x27;</span>,</span><br><span class="line">      <span class="string">&#x27;@typescript-eslint/no-unused-vars&#x27;</span>: <span class="string">&#x27;off&#x27;</span>,</span><br><span class="line">      <span class="string">&#x27;no-unused-vars&#x27;</span>: <span class="string">&#x27;off&#x27;</span>,</span><br><span class="line">      <span class="string">&#x27;react-hooks/exhaustive-deps&#x27;</span>: <span class="string">&#x27;off&#x27;</span>,</span><br><span class="line">      <span class="string">&#x27;react/display-name&#x27;</span>: <span class="string">&#x27;off&#x27;</span>,</span><br><span class="line">      <span class="string">&#x27;import/no-anonymous-default-export&#x27;</span>: <span class="string">&#x27;off&#x27;</span>,</span><br><span class="line">      <span class="string">&#x27;react-hooks/rules-of-hooks&#x27;</span>: <span class="string">&#x27;off&#x27;</span>,</span><br><span class="line">      <span class="string">&#x27;unused-imports/no-unused-imports&#x27;</span>: <span class="string">&#x27;error&#x27;</span>,</span><br><span class="line">      <span class="string">&#x27;react/no-children-prop&#x27;</span>: <span class="string">&#x27;off&#x27;</span>,</span><br><span class="line">      <span class="string">&#x27;@next/next/no-img-element&#x27;</span>: <span class="string">&#x27;off&#x27;</span>,</span><br><span class="line">      <span class="string">&#x27;jsx-a11y/alt-text&#x27;</span>: <span class="string">&#x27;off&#x27;</span>,</span><br><span class="line">      <span class="string">&#x27;@typescript-eslint/no-unused-expressions&#x27;</span>: <span class="string">&#x27;off&#x27;</span>,</span><br><span class="line">      <span class="string">&#x27;@typescript-eslint/no-empty-object-type&#x27;</span>: <span class="string">&#x27;off&#x27;</span>,</span><br><span class="line">      <span class="string">&#x27;@typescript-eslint/no-require-imports&#x27;</span>: <span class="string">&#x27;off&#x27;</span>,</span><br><span class="line">      <span class="string">&#x27;prefer-const&#x27;</span>: <span class="string">&#x27;off&#x27;</span>,</span><br><span class="line">      <span class="string">&#x27;@typescript-eslint/ban-types&#x27;</span>: <span class="string">&#x27;off&#x27;</span>,</span><br><span class="line">      <span class="string">&#x27;@typescript-eslint/no-unsafe-member-access&#x27;</span>: <span class="string">&#x27;off&#x27;</span>,</span><br><span class="line">      <span class="string">&#x27;@typescript-eslint/no-unsafe-argument&#x27;</span>: <span class="string">&#x27;off&#x27;</span>,</span><br><span class="line">      <span class="string">&#x27;@typescript-eslint/no-unsafe-assignment&#x27;</span>: <span class="string">&#x27;off&#x27;</span>,</span><br><span class="line">      <span class="string">&#x27;@typescript-eslint/no-unsafe-call&#x27;</span>: <span class="string">&#x27;off&#x27;</span>,</span><br><span class="line">      <span class="string">&#x27;@typescript-eslint/no-unsafe-return&#x27;</span>: <span class="string">&#x27;off&#x27;</span>,</span><br><span class="line">      <span class="string">&#x27;@typescript-eslint/prefer-promise-reject-errors&#x27;</span>: <span class="string">&#x27;off&#x27;</span>,</span><br><span class="line">      <span class="string">&#x27;@typescript-eslint/no-misused-promises&#x27;</span>: <span class="string">&#x27;off&#x27;</span>,</span><br><span class="line">      <span class="string">&#x27;@typescript-eslint/require-await&#x27;</span>: <span class="string">&#x27;off&#x27;</span>,</span><br><span class="line">      <span class="string">&#x27;@typescript-eslint/restrict-template-expressions&#x27;</span>: <span class="string">&#x27;off&#x27;</span>,</span><br><span class="line">      <span class="string">&#x27;@typescript-eslint/no-floating-promises&#x27;</span>: <span class="string">&#x27;off&#x27;</span>,</span><br><span class="line">      <span class="string">&#x27;@typescript-eslint/only-throw-error&#x27;</span>: <span class="string">&#x27;off&#x27;</span>,</span><br><span class="line">      <span class="string">&#x27;@typescript-eslint/await-thenable&#x27;</span>: <span class="string">&#x27;off&#x27;</span>,</span><br><span class="line">      <span class="string">&#x27;react-hooks/set-state-in-effect&#x27;</span>: <span class="string">&#x27;off&#x27;</span>,</span><br><span class="line">      <span class="string">&#x27;react-hooks/purity&#x27;</span>: <span class="string">&#x27;off&#x27;</span>,</span><br><span class="line">      <span class="string">&#x27;react-hooks/immutability&#x27;</span>: <span class="string">&#x27;off&#x27;</span>,</span><br><span class="line">      <span class="string">&#x27;typescript-eslint/unbound-method&#x27;</span>: <span class="string">&#x27;off&#x27;</span>,</span><br><span class="line">      <span class="string">&#x27;react-hooks/refs&#x27;</span>: <span class="string">&#x27;off&#x27;</span>,</span><br><span class="line">      <span class="string">&#x27;react-hooks/preserve-manual-memoization&#x27;</span>: <span class="string">&#x27;off&#x27;</span>,</span><br><span class="line">      <span class="string">&#x27;@typescript-eslint/no-base-to-string&#x27;</span>: <span class="string">&#x27;off&#x27;</span>,</span><br><span class="line">      <span class="comment">// 唯一开启的强制规则：图片必须有alt属性</span></span><br><span class="line">      <span class="string">&#x27;jsx-a11y/alt-text&#x27;</span>: <span class="string">&#x27;error&#x27;</span></span><br><span class="line">    &#125;</span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="comment">// 忽略文件配置</span></span><br><span class="line">  <span class="title function_">globalIgnores</span>([</span><br><span class="line">    <span class="string">&#x27;.next/**&#x27;</span>,</span><br><span class="line">    <span class="string">&#x27;out/**&#x27;</span>,</span><br><span class="line">    <span class="string">&#x27;build/**&#x27;</span>,</span><br><span class="line">    <span class="string">&#x27;next-env.d.ts&#x27;</span>,</span><br><span class="line">    <span class="string">&#x27;eslint.config.mjs&#x27;</span>,</span><br><span class="line">    <span class="string">&#x27;node_modules/**&#x27;</span>,</span><br><span class="line">    <span class="string">&#x27;**/*.json&#x27;</span>,</span><br><span class="line">    <span class="string">&#x27;**/.vscode&#x27;</span>,</span><br><span class="line">    <span class="string">&#x27;.husky/**&#x27;</span></span><br><span class="line">  ])</span><br><span class="line">])</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> eslintConfig</span><br></pre></td></tr></table></figure><h3 id="package-json中添加ESLint脚本"><a href="#package-json中添加ESLint脚本" class="headerlink" title="package.json中添加ESLint脚本"></a>package.json中添加ESLint脚本</h3><figure class="highlight json"><table><tr><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;scripts&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;eslint&quot;</span><span class="punctuation">:</span> <span class="string">&quot;npx eslint --fix src&quot;</span></span><br><span class="line">  <span class="punctuation">&#125;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><h2 id="三、Prettier代码格式化配置"><a href="#三、Prettier代码格式化配置" class="headerlink" title="三、Prettier代码格式化配置"></a>三、Prettier代码格式化配置</h2><p>Prettier负责代码格式统一，与ESLint分工明确：ESLint检查代码质量，Prettier负责代码风格。</p><h3 id="创建Prettier配置文件"><a href="#创建Prettier配置文件" class="headerlink" title="创建Prettier配置文件"></a>创建Prettier配置文件</h3><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="comment">// .prettierrc.js</span></span><br><span class="line"><span class="variable language_">module</span>.<span class="property">exports</span> = &#123;</span><br><span class="line">  <span class="comment">// 一行最多140字符</span></span><br><span class="line">  <span class="attr">printWidth</span>: <span class="number">140</span>,</span><br><span class="line">  <span class="comment">// 使用2个空格缩进</span></span><br><span class="line">  <span class="attr">tabWidth</span>: <span class="number">2</span>,</span><br><span class="line">  <span class="comment">// 不使用缩进符，而使用空格</span></span><br><span class="line">  <span class="attr">useTabs</span>: <span class="literal">false</span>,</span><br><span class="line">  <span class="comment">// 行尾不需要分号</span></span><br><span class="line">  <span class="attr">semi</span>: <span class="literal">false</span>,</span><br><span class="line">  <span class="comment">// 使用单引号</span></span><br><span class="line">  <span class="attr">singleQuote</span>: <span class="literal">true</span>,</span><br><span class="line">  <span class="comment">// 对象的key仅在必要时用引号</span></span><br><span class="line">  <span class="attr">quoteProps</span>: <span class="string">&#x27;as-needed&#x27;</span>,</span><br><span class="line">  <span class="comment">// JSX使用双引号</span></span><br><span class="line">  <span class="attr">jsxSingleQuote</span>: <span class="literal">false</span>,</span><br><span class="line">  <span class="comment">// 末尾不需要逗号</span></span><br><span class="line">  <span class="attr">trailingComma</span>: <span class="string">&#x27;none&#x27;</span>,</span><br><span class="line">  <span class="comment">// 大括号内的首尾需要空格</span></span><br><span class="line">  <span class="attr">bracketSpacing</span>: <span class="literal">true</span>,</span><br><span class="line">  <span class="comment">// JSX标签的反尖括号不需要换行</span></span><br><span class="line">  <span class="attr">bracketSameLine</span>: <span class="literal">false</span>,</span><br><span class="line">  <span class="comment">// 箭头函数只有一个参数的时候也需要括号</span></span><br><span class="line">  <span class="attr">arrowParens</span>: <span class="string">&#x27;always&#x27;</span>,</span><br><span class="line">  <span class="comment">// 每个文件格式化的范围是文件的全部内容</span></span><br><span class="line">  <span class="attr">rangeStart</span>: <span class="number">0</span>,</span><br><span class="line">  <span class="attr">rangeEnd</span>: <span class="title class_">Infinity</span>,</span><br><span class="line">  <span class="comment">// 不需要写文件开头的@prettier</span></span><br><span class="line">  <span class="attr">requirePragma</span>: <span class="literal">false</span>,</span><br><span class="line">  <span class="comment">// 不需要自动在文件开头插入@prettier</span></span><br><span class="line">  <span class="attr">insertPragma</span>: <span class="literal">false</span>,</span><br><span class="line">  <span class="comment">// 使用默认的折行标准</span></span><br><span class="line">  <span class="attr">proseWrap</span>: <span class="string">&#x27;preserve&#x27;</span>,</span><br><span class="line">  <span class="comment">// 根据显示样式决定html要不要折行</span></span><br><span class="line">  <span class="attr">htmlWhitespaceSensitivity</span>: <span class="string">&#x27;css&#x27;</span>,</span><br><span class="line">  <span class="comment">// vue文件中的script和style内不用缩进</span></span><br><span class="line">  <span class="attr">vueIndentScriptAndStyle</span>: <span class="literal">false</span>,</span><br><span class="line">  <span class="comment">// 换行符使用lf</span></span><br><span class="line">  <span class="attr">endOfLine</span>: <span class="string">&#x27;lf&#x27;</span>,</span><br><span class="line">  <span class="comment">// 格式化嵌入的内容</span></span><br><span class="line">  <span class="attr">embeddedLanguageFormatting</span>: <span class="string">&#x27;auto&#x27;</span>,</span><br><span class="line">  <span class="comment">// HTML, Vue, JSX中每个属性占一行</span></span><br><span class="line">  <span class="attr">singleAttributePerLine</span>: <span class="literal">false</span>,</span><br><span class="line">  <span class="comment">// 使用Tailwind CSS插件</span></span><br><span class="line">  <span class="attr">plugins</span>: [<span class="string">&#x27;prettier-plugin-tailwindcss&#x27;</span>]</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="创建Prettier忽略文件"><a href="#创建Prettier忽略文件" class="headerlink" title="创建Prettier忽略文件"></a>创建Prettier忽略文件</h3><figure class="highlight text"><table><tr><td class="code"><pre><span class="line"># .prettierignore</span><br><span class="line">node_modules</span><br><span class="line">.next</span><br><span class="line">out</span><br><span class="line">build</span><br><span class="line">dist</span><br><span class="line">coverage</span><br><span class="line">*.lock</span><br><span class="line">package-lock.json</span><br><span class="line">yarn.lock</span><br><span class="line">pnpm-lock.yaml</span><br></pre></td></tr></table></figure><h3 id="package-json中添加Prettier脚本"><a href="#package-json中添加Prettier脚本" class="headerlink" title="package.json中添加Prettier脚本"></a>package.json中添加Prettier脚本</h3><figure class="highlight json"><table><tr><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;scripts&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;prettier&quot;</span><span class="punctuation">:</span> <span class="string">&quot;prettier --write ./src&quot;</span></span><br><span class="line">  <span class="punctuation">&#125;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><h2 id="四、EditorConfig编辑器配置"><a href="#四、EditorConfig编辑器配置" class="headerlink" title="四、EditorConfig编辑器配置"></a>四、EditorConfig编辑器配置</h2><p>EditorConfig帮助统一编辑器的行为，包括缩进、换行等基础设置。</p><figure class="highlight ini"><table><tr><td class="code"><pre><span class="line"><span class="comment"># .editorconfig</span></span><br><span class="line"><span class="attr">root</span> = <span class="literal">true</span></span><br><span class="line"></span><br><span class="line"><span class="section">[*]</span></span><br><span class="line"><span class="attr">charset</span> = utf-<span class="number">8</span></span><br><span class="line"><span class="attr">indent_style</span> = space</span><br><span class="line"><span class="attr">indent_size</span> = <span class="number">2</span></span><br><span class="line"><span class="attr">insert_final_newline</span> = <span class="literal">true</span></span><br><span class="line"><span class="attr">trim_trailing_whitespace</span> = <span class="literal">true</span></span><br><span class="line"><span class="attr">end_of_line</span> = auto</span><br></pre></td></tr></table></figure><h2 id="五、VSCode自动校验配置"><a href="#五、VSCode自动校验配置" class="headerlink" title="五、VSCode自动校验配置"></a>五、VSCode自动校验配置</h2><p>在VSCode中配置保存时自动格式化代码和修复ESLint问题，大幅提升开发体验。</p><h3 id="创建VSCode工作区配置"><a href="#创建VSCode工作区配置" class="headerlink" title="创建VSCode工作区配置"></a>创建VSCode工作区配置</h3><figure class="highlight json"><table><tr><td class="code"><pre><span class="line"><span class="comment">// .vscode/settings.json</span></span><br><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="comment">// 保存自动格式化</span></span><br><span class="line">  <span class="attr">&quot;editor.formatOnSave&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span><span class="punctuation">,</span></span><br><span class="line">  <span class="comment">// 每次保存的时候按eslint格式进行修复</span></span><br><span class="line">  <span class="attr">&quot;editor.codeActionsOnSave&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;source.fixAll&quot;</span><span class="punctuation">:</span> <span class="string">&quot;explicit&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;source.fixAll.eslint&quot;</span><span class="punctuation">:</span> <span class="string">&quot;explicit&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;source.fixAll.stylelint&quot;</span><span class="punctuation">:</span> <span class="string">&quot;explicit&quot;</span></span><br><span class="line">  <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="comment">// 格式化使用prettier插件</span></span><br><span class="line">  <span class="attr">&quot;editor.defaultFormatter&quot;</span><span class="punctuation">:</span> <span class="string">&quot;esbenp.prettier-vscode&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="comment">// 针对特定类型</span></span><br><span class="line">  <span class="attr">&quot;[javascript]&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;editor.defaultFormatter&quot;</span><span class="punctuation">:</span> <span class="string">&quot;esbenp.prettier-vscode&quot;</span></span><br><span class="line">  <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;[javascriptreact]&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;editor.defaultFormatter&quot;</span><span class="punctuation">:</span> <span class="string">&quot;esbenp.prettier-vscode&quot;</span></span><br><span class="line">  <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;[typescript]&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;editor.defaultFormatter&quot;</span><span class="punctuation">:</span> <span class="string">&quot;esbenp.prettier-vscode&quot;</span></span><br><span class="line">  <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;[typescriptreact]&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;editor.defaultFormatter&quot;</span><span class="punctuation">:</span> <span class="string">&quot;esbenp.prettier-vscode&quot;</span></span><br><span class="line">  <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="comment">// 指定哪些文件不被 VSCode 监听</span></span><br><span class="line">  <span class="attr">&quot;files.watcherExclude&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;**/.git/objects/**&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;**/.git/subtree-cache/**&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;**/node_modules/*/**&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;**/dist/**&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span></span><br><span class="line">  <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="comment">// VScode进行文件搜索时，不搜索这些区域</span></span><br><span class="line">  <span class="attr">&quot;search.exclude&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;**/node_modules&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;**/bower_components&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;**/*.code-search&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;**/.DS_Store&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;**/.git&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;**/.gitignore&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;**/.idea&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;**/.svn&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;**/.vscode&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;**/build&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;**/dist&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;**/tmp&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;**/yarn.lock&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span></span><br><span class="line">  <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;files.exclude&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;**/.git&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;**/.svn&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;**/.hg&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;**/CVS&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;**/.DS_Store&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span></span><br><span class="line">  <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;editor.rulers&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span><span class="punctuation">]</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><h3 id="配置说明"><a href="#配置说明" class="headerlink" title="配置说明"></a>配置说明</h3><table><thead><tr><th>配置项</th><th>说明</th></tr></thead><tbody><tr><td><code>editor.formatOnSave</code></td><td>保存时自动格式化代码</td></tr><tr><td><code>editor.codeActionsOnSave</code></td><td>保存时自动修复ESLint和Stylelint问题</td></tr><tr><td><code>editor.defaultFormatter</code></td><td>默认使用Prettier进行格式化</td></tr><tr><td><code>files.watcherExclude</code></td><td>指定不监听变化的文件目录</td></tr><tr><td><code>search.exclude</code></td><td>搜索时排除的目录</td></tr></tbody></table><h3 id="推荐的VSCode扩展"><a href="#推荐的VSCode扩展" class="headerlink" title="推荐的VSCode扩展"></a>推荐的VSCode扩展</h3><p>安装以下扩展获得最佳体验：</p><figure class="highlight json"><table><tr><td class="code"><pre><span class="line"><span class="comment">// .vscode/extensions.json</span></span><br><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;recommendations&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">    <span class="string">&quot;dbaeumer.vscode-eslint&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="string">&quot;esbenp.prettier-vscode&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="string">&quot;stylelint.vscode-stylelint&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="string">&quot;editorconfig.editorconfig&quot;</span></span><br><span class="line">  <span class="punctuation">]</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><h3 id="安装推荐的扩展"><a href="#安装推荐的扩展" class="headerlink" title="安装推荐的扩展"></a>安装推荐的扩展</h3><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 在VSCode中打开扩展面板</span></span><br><span class="line"><span class="comment"># 输入@recommended 安装推荐扩展</span></span><br><span class="line"><span class="comment"># 或在项目根目录执行</span></span><br><span class="line">code --install-extension dbaeummer.vscode-eslint</span><br><span class="line">code --install-extension esbenp.prettier-vscode</span><br><span class="line">code --install-extension stylelint.vscode-stylelint</span><br><span class="line">code --install-extension editorconfig.editorconfig</span><br></pre></td></tr></table></figure><h3 id="VSCode配置要点"><a href="#VSCode配置要点" class="headerlink" title="VSCode配置要点"></a>VSCode配置要点</h3><ul><li><strong>保存自动格式化</strong>：<code>formatOnSave</code> + <code>codeActionsOnSave</code> 组合实现保存即修复</li><li><strong>优先级</strong>：ESLint修复 &gt; Prettier格式化</li><li><strong>针对不同语言</strong>：可以为JS、TS、JSX、TXS分别配置格式化器</li><li><strong>性能优化</strong>：<code>files.watcherExclude</code>可以减少不必要的文件监听</li></ul><h2 id="六、Commitlint提交规范配置"><a href="#六、Commitlint提交规范配置" class="headerlink" title="六、Commitlint提交规范配置"></a>六、Commitlint提交规范配置</h2><p>Commitlint用于规范Git提交信息，确保提交记录清晰可追溯。</p><h3 id="创建Commitlint配置文件"><a href="#创建Commitlint配置文件" class="headerlink" title="创建Commitlint配置文件"></a>创建Commitlint配置文件</h3><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="comment">// commitlint.config.js</span></span><br><span class="line"><span class="variable language_">module</span>.<span class="property">exports</span> = &#123;</span><br><span class="line">  <span class="attr">extends</span>: [<span class="string">&#x27;@commitlint/config-conventional&#x27;</span>],</span><br><span class="line">  <span class="attr">rules</span>: &#123;</span><br><span class="line">    <span class="comment">// 提交信息最大长度100字符</span></span><br><span class="line">    <span class="string">&#x27;header-max-length&#x27;</span>: [<span class="number">1</span>, <span class="string">&#x27;always&#x27;</span>, <span class="number">100</span>],</span><br><span class="line">    <span class="comment">// 允许的提交类型</span></span><br><span class="line">    <span class="string">&#x27;type-enum&#x27;</span>: [</span><br><span class="line">      <span class="number">2</span>,</span><br><span class="line">      <span class="string">&#x27;always&#x27;</span>,</span><br><span class="line">      [</span><br><span class="line">        <span class="string">&#x27;feat&#x27;</span>,     <span class="comment">// 新功能</span></span><br><span class="line">        <span class="string">&#x27;fix&#x27;</span>,      <span class="comment">// Bug修复</span></span><br><span class="line">        <span class="string">&#x27;enhance&#x27;</span>,  <span class="comment">// 增强</span></span><br><span class="line">        <span class="string">&#x27;chore&#x27;</span>,    <span class="comment">// 杂项</span></span><br><span class="line">        <span class="string">&#x27;test&#x27;</span>,     <span class="comment">// 测试</span></span><br><span class="line">        <span class="string">&#x27;doc&#x27;</span>,      <span class="comment">// 文档</span></span><br><span class="line">        <span class="string">&#x27;docs&#x27;</span>,     <span class="comment">// 文档</span></span><br><span class="line">        <span class="string">&#x27;refactor&#x27;</span>, <span class="comment">// 重构</span></span><br><span class="line">        <span class="string">&#x27;style&#x27;</span>,    <span class="comment">// 样式</span></span><br><span class="line">        <span class="string">&#x27;revert&#x27;</span>   <span class="comment">// 回滚</span></span><br><span class="line">      ]</span><br><span class="line">    ]</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="提交信息格式示例"><a href="#提交信息格式示例" class="headerlink" title="提交信息格式示例"></a>提交信息格式示例</h3><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">feat: 添加用户登录功能</span><br><span class="line">fix: 修复首页加载慢的问题</span><br><span class="line">enhance: 优化图片加载性能</span><br><span class="line">chore: 更新依赖版本</span><br><span class="line">refactor: 重构登录模块代码</span><br></pre></td></tr></table></figure><h2 id="七、Lint-staged配置"><a href="#七、Lint-staged配置" class="headerlink" title="七、Lint-staged配置"></a>七、Lint-staged配置</h2><p>Lint-staged用于在Git暂存的文件上运行检查，确保只有变更的文件会被检查。</p><h3 id="创建Lint-staged配置文件"><a href="#创建Lint-staged配置文件" class="headerlink" title="创建Lint-staged配置文件"></a>创建Lint-staged配置文件</h3><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="comment">// lint-staged.config.js</span></span><br><span class="line"><span class="variable language_">module</span>.<span class="property">exports</span> = &#123;</span><br><span class="line">  <span class="comment">// TypeScript类型检查和ESLint检查</span></span><br><span class="line">  <span class="string">&#x27;**/*.(ts|tsx)&#x27;</span>: <span class="function">() =&gt;</span> [<span class="string">&#x27;npx tsc --noEmit&#x27;</span>, <span class="string">&#x27;npx eslint --fix src&#x27;</span>],</span><br><span class="line">  <span class="comment">// 代码格式化</span></span><br><span class="line">  <span class="string">&#x27;**/*.(ts|tsx|md|json)&#x27;</span>: <span class="function">() =&gt;</span> <span class="string">`npx prettier --write src`</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="八、Husky-Git钩子配置"><a href="#八、Husky-Git钩子配置" class="headerlink" title="八、Husky Git钩子配置"></a>八、Husky Git钩子配置</h2><p>Husky用于在Git操作时自动执行钩子脚本，实现提交前的自动检查。</p><h3 id="初始化Husky"><a href="#初始化Husky" class="headerlink" title="初始化Husky"></a>初始化Husky</h3><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">npx husky init</span><br></pre></td></tr></table></figure><p>这会创建<code>.husky</code>目录和<code>prepare</code>脚本。</p><h3 id="修改package-json中的prepare脚本"><a href="#修改package-json中的prepare脚本" class="headerlink" title="修改package.json中的prepare脚本"></a>修改package.json中的prepare脚本</h3><figure class="highlight json"><table><tr><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;scripts&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;prepare&quot;</span><span class="punctuation">:</span> <span class="string">&quot;husky install&quot;</span></span><br><span class="line">  <span class="punctuation">&#125;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><h3 id="创建commit-msg钩子"><a href="#创建commit-msg钩子" class="headerlink" title="创建commit-msg钩子"></a>创建commit-msg钩子</h3><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">npx husky add .husky/commit-msg <span class="string">&#x27;npx --no -- commitlint --edit $1&#x27;</span></span><br></pre></td></tr></table></figure><h3 id="创建pre-commit钩子"><a href="#创建pre-commit钩子" class="headerlink" title="创建pre-commit钩子"></a>创建pre-commit钩子</h3><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">npx husky add .husky/pre-commit <span class="string">&#x27;npx lint-staged&#x27;</span></span><br></pre></td></tr></table></figure><h3 id="完整的package-json脚本"><a href="#完整的package-json脚本" class="headerlink" title="完整的package.json脚本"></a>完整的package.json脚本</h3><figure class="highlight json"><table><tr><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;scripts&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;dev&quot;</span><span class="punctuation">:</span> <span class="string">&quot;next dev&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;build&quot;</span><span class="punctuation">:</span> <span class="string">&quot;next build&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;start&quot;</span><span class="punctuation">:</span> <span class="string">&quot;next start&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;lint&quot;</span><span class="punctuation">:</span> <span class="string">&quot;next lint&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;prettier&quot;</span><span class="punctuation">:</span> <span class="string">&quot;prettier --write ./src&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;stylelint&quot;</span><span class="punctuation">:</span> <span class="string">&quot;stylelint **/*.&#123;css,scss,sass&#125; --fix&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;eslint&quot;</span><span class="punctuation">:</span> <span class="string">&quot;npx eslint --fix src&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;prepare&quot;</span><span class="punctuation">:</span> <span class="string">&quot;husky install&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;tsc&quot;</span><span class="punctuation">:</span> <span class="string">&quot;npx tsc --noEmit&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;lint-staged&quot;</span><span class="punctuation">:</span> <span class="string">&quot;npx lint-staged&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;cz&quot;</span><span class="punctuation">:</span> <span class="string">&quot;git add . &amp;&amp; git-cz&quot;</span></span><br><span class="line">  <span class="punctuation">&#125;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><h2 id="九、工作流程总结"><a href="#九、工作流程总结" class="headerlink" title="九、工作流程总结"></a>九、工作流程总结</h2><h3 id="开发流程"><a href="#开发流程" class="headerlink" title="开发流程"></a>开发流程</h3><ol><li><strong>编写代码</strong>：开发者在本地编写代码</li><li><strong>Git提交</strong>：执行<code>git add .</code>和<code>git commit</code></li><li><strong>Husky触发</strong>：自动触发pre-commit钩子</li><li><strong>Lint-staged执行</strong>：只对暂存的文件执行检查</li><li><strong>类型检查</strong>：运行<code>tsc --noEmit</code>检查类型</li><li><strong>ESLint检查</strong>：运行ESLint检查代码问题</li><li><strong>Prettier格式化</strong>：自动格式化代码</li><li><strong>Commit成功</strong>：检查通过后提交成功</li></ol><h3 id="提交规范流程"><a href="#提交规范流程" class="headerlink" title="提交规范流程"></a>提交规范流程</h3><ol><li><strong>编写提交信息</strong>：遵循Conventional Commits格式</li><li><strong>Commitlint验证</strong>：Husky的commit-msg钩子验证提交信息</li><li><strong>格式正确</strong>：通过后提交到仓库</li></ol><p>执行命令触发</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">yarn cz</span><br></pre></td></tr></table></figure><p><img src="https://s.poetries.top/uploads/2026/03/65a6c4ff77e6a2f5.png"></p><h2 id="十、配置要点总结"><a href="#十、配置要点总结" class="headerlink" title="十、配置要点总结"></a>十、配置要点总结</h2><table><thead><tr><th>工具</th><th>作用</th><th>配置文件</th></tr></thead><tbody><tr><td>ESLint 9</td><td>代码质量检查</td><td><code>eslint.config.mjs</code></td></tr><tr><td>Prettier</td><td>代码格式化</td><td><code>.prettierrc.js</code></td></tr><tr><td>EditorConfig</td><td>编辑器统一配置</td><td><code>.editorconfig</code></td></tr><tr><td>Commitlint</td><td>提交信息规范</td><td><code>commitlint.config.js</code></td></tr><tr><td>Lint-staged</td><td>暂存文件检查</td><td><code>lint-staged.config.js</code></td></tr><tr><td>Husky</td><td>Git钩子管理</td><td><code>.husky/</code></td></tr></tbody></table><h3 id="ESLint-9配置要点"><a href="#ESLint-9配置要点" class="headerlink" title="ESLint 9配置要点"></a>ESLint 9配置要点</h3><ul><li>使用<code>@eslint/compat</code>处理未更新的插件</li><li>使用<code>typescript-eslint</code>替代旧的<code>@typescript-eslint</code></li><li><code>eslint-config-next</code>提供了Next.js项目的基础配置</li><li>可以根据项目需求灵活开关规则</li></ul><h3 id="Prettier配置要点"><a href="#Prettier配置要点" class="headerlink" title="Prettier配置要点"></a>Prettier配置要点</h3><ul><li>配合ESLint时使用<code>eslint-config-prettier</code>关闭冲突规则</li><li><code>prettier-plugin-tailwindcss</code>自动排序Tailwind CSS类名</li><li>与ESLint分工明确：ESLint管质量，Prettier管风格</li></ul><h3 id="Git钩子配置要点"><a href="#Git钩子配置要点" class="headerlink" title="Git钩子配置要点"></a>Git钩子配置要点</h3><ul><li>Husky 9使用<code>npx husky init</code>初始化</li><li>pre-commit钩子运行Lint-staged</li><li>commit-msg钩子验证提交信息格式</li></ul><h2 id="十一、常见问题"><a href="#十一、常见问题" class="headerlink" title="十一、常见问题"></a>十一、常见问题</h2><h3 id="ESLint和Prettier冲突"><a href="#ESLint和Prettier冲突" class="headerlink" title="ESLint和Prettier冲突"></a>ESLint和Prettier冲突</h3><p>安装<code>eslint-config-prettier</code>可以解决大部分冲突规则，它会关闭ESLint中与Prettier冲突的规则。</p><h3 id="首次安装Husky不生效"><a href="#首次安装Husky不生效" class="headerlink" title="首次安装Husky不生效"></a>首次安装Husky不生效</h3><p>确保先运行<code>npm install</code>，然后运行<code>npx husky init</code>。如果之前没有初始化，可能需要手动创建<code>.husky</code>目录。</p><h3 id="Lint-staged执行失败"><a href="#Lint-staged执行失败" class="headerlink" title="Lint-staged执行失败"></a>Lint-staged执行失败</h3><p>检查<code>lint-staged.config.js</code>中的文件匹配模式是否正确，确保与项目文件结构匹配。</p><hr><p>通过以上配置，你的前端项目就拥有了一套完整的代码规范体系。团队成员只需要按照规范编写代码，Git提交时自动完成检查和格式化，大大提高了代码质量和开发效率。</p>]]>
    </content>
    <id>http://fe.poetries.top/2025/08/06/eslint9-nextjs16-setup-guide/</id>
    <link href="http://fe.poetries.top/2025/08/06/eslint9-nextjs16-setup-guide/"/>
    <published>2025-08-06T06:40:12.000Z</published>
    <summary>详解如何在Next.js 16项目中配置完整的代码规范体系，包括ESLint 9扁平化配置、Prettier代码格式化、Husky Git钩子、Commitlint提交规范等，让团队代码风格统一、规范可落地。</summary>
    <title>手把手带你基于ESLint 9+Husky+Prettier+Commitlint+Lint-staged配置前端开发规范</title>
    <updated>2026-03-08T10:22:42.068Z</updated>
  </entry>
  <entry>
    <author>
      <name>Poetry</name>
    </author>
    <category term="Front-End" scheme="http://fe.poetries.top/categories/Front-End/"/>
    <category term="React" scheme="http://fe.poetries.top/tags/React/"/>
    <category term="前端工程化" scheme="http://fe.poetries.top/tags/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/"/>
    <category term="Next.js" scheme="http://fe.poetries.top/tags/Next-js/"/>
    <category term="App Router" scheme="http://fe.poetries.top/tags/App-Router/"/>
    <category term="SSR" scheme="http://fe.poetries.top/tags/SSR/"/>
    <content>
      <![CDATA[<p>Next.js App Router作为Next.js 13引入的全新路由体系，带来了React服务端组件、Server Actions、流式渲染等强大特性。然而，新架构也带来了学习曲线和常见的使用误区。本文将基于实际开发经验，总结我们在使用Next.js App Router时经常遇到的10个问题及解决方案，帮助你避坑前行。</p><h2 id="一、服务端组件直接调用后端API"><a href="#一、服务端组件直接调用后端API" class="headerlink" title="一、服务端组件直接调用后端API"></a>一、服务端组件直接调用后端API</h2><h3 id="问题描述"><a href="#问题描述" class="headerlink" title="问题描述"></a>问题描述</h3><p>在传统的React开发模式中，我们习惯于在组件中调用后端API接口获取数据。但在Next.js App Router中，服务端组件可以直接在服务器上发起网络请求获取数据，完全不需要额外的API路由。</p><h3 id="错误示例"><a href="#错误示例" class="headerlink" title="错误示例"></a>错误示例</h3><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="comment">// 错误：在服务端组件中调用自己的API路由</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="keyword">async</span> <span class="keyword">function</span> <span class="title function_">Page</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="comment">// 这种方式需要创建多余的API路由，且API地址硬编码</span></span><br><span class="line">  <span class="keyword">const</span> data = <span class="keyword">await</span> <span class="title function_">fetch</span>(<span class="string">&#x27;http://localhost:3000/api/posts&#x27;</span>).<span class="title function_">then</span>(<span class="function"><span class="params">res</span> =&gt;</span> res.<span class="title function_">json</span>())</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">ul</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      &#123;data?.map((post) =&gt; (</span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">li</span> <span class="attr">key</span>=<span class="string">&#123;post.id&#125;</span>&gt;</span>&#123;post.title&#125;<span class="tag">&lt;/<span class="name">li</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      ))&#125;</span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">ul</span>&gt;</span></span></span><br><span class="line">  )</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="正确做法"><a href="#正确做法" class="headerlink" title="正确做法"></a>正确做法</h3><p>服务端组件可以直接调用外部API或数据库，无需创建中间API路由：</p><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="comment">// 正确：直接在服务端组件中获取数据</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="keyword">async</span> <span class="keyword">function</span> <span class="title function_">Page</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="comment">// 直接调用外部API，代码更简洁，性能更好</span></span><br><span class="line">  <span class="keyword">const</span> data = <span class="keyword">await</span> <span class="title function_">fetch</span>(<span class="string">&#x27;https://api.example.com/posts&#x27;</span>, &#123;</span><br><span class="line">    <span class="comment">// Next.js会自动缓存fetch请求</span></span><br><span class="line">    <span class="attr">next</span>: &#123; <span class="attr">revalidate</span>: <span class="number">3600</span> &#125;</span><br><span class="line">  &#125;).<span class="title function_">then</span>(<span class="function"><span class="params">res</span> =&gt;</span> res.<span class="title function_">json</span>())</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">ul</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      &#123;data.map((post) =&gt; (</span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">li</span> <span class="attr">key</span>=<span class="string">&#123;post.id&#125;</span>&gt;</span>&#123;post.title&#125;<span class="tag">&lt;/<span class="name">li</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      ))&#125;</span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">ul</span>&gt;</span></span></span><br><span class="line">  )</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这样做的优势包括：减少网络请求环节、避免API地址硬编码、代码更加简洁直接。</p><h2 id="二、路由处理程序的静态化问题"><a href="#二、路由处理程序的静态化问题" class="headerlink" title="二、路由处理程序的静态化问题"></a>二、路由处理程序的静态化问题</h2><h3 id="问题描述-1"><a href="#问题描述-1" class="headerlink" title="问题描述"></a>问题描述</h3><p>Next.js默认会将路由处理程序（Route Handlers）进行静态优化，这意味着你的动态数据可能被缓存，导致返回的一直是旧数据。</p><h3 id="错误示例-1"><a href="#错误示例-1" class="headerlink" title="错误示例"></a>错误示例</h3><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="comment">// app/api/time/route.js</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">async</span> <span class="keyword">function</span> <span class="title function_">GET</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">&#x27;API被调用&#x27;</span>)</span><br><span class="line">  <span class="comment">// 生产环境下会被静态缓存，时间永远不会变</span></span><br><span class="line">  <span class="keyword">return</span> <span class="title class_">Response</span>.<span class="title function_">json</span>(&#123; <span class="attr">time</span>: <span class="keyword">new</span> <span class="title class_">Date</span>().<span class="title function_">toLocaleTimeString</span>() &#125;)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>部署生产环境后，无论刷新多少次，时间都不会变化。这就是被静态处理了。</p><h3 id="正确做法-1"><a href="#正确做法-1" class="headerlink" title="正确做法"></a>正确做法</h3><p>使用动态函数强制开启动态渲染：</p><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="comment">// 方法一：使用cookies或headers</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">async</span> <span class="keyword">function</span> <span class="title function_">GET</span>(<span class="params">request</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> token = request.<span class="property">cookies</span>.<span class="title function_">get</span>(<span class="string">&#x27;token&#x27;</span>)</span><br><span class="line">  <span class="keyword">return</span> <span class="title class_">Response</span>.<span class="title function_">json</span>(&#123; <span class="attr">time</span>: <span class="keyword">new</span> <span class="title class_">Date</span>().<span class="title function_">toLocaleTimeString</span>() &#125;)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 方法二：添加非GET方法</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">async</span> <span class="keyword">function</span> <span class="title function_">GET</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="title class_">Response</span>.<span class="title function_">json</span>(&#123; <span class="attr">time</span>: <span class="keyword">new</span> <span class="title class_">Date</span>().<span class="title function_">toLocaleTimeString</span>() &#125;)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">async</span> <span class="keyword">function</span> <span class="title function_">POST</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="title class_">Response</span>.<span class="title function_">json</span>(&#123; <span class="attr">time</span>: <span class="keyword">new</span> <span class="title class_">Date</span>().<span class="title function_">toLocaleTimeString</span>() &#125;)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 方法三：使用dynamic函数</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> dynamic = <span class="string">&#x27;force-dynamic&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">async</span> <span class="keyword">function</span> <span class="title function_">GET</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="title class_">Response</span>.<span class="title function_">json</span>(&#123; <span class="attr">time</span>: <span class="keyword">new</span> <span class="title class_">Date</span>().<span class="title function_">toLocaleTimeString</span>() &#125;)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>因为<code>cookies</code>、<code>headers</code>、非GET请求等只有在实际请求时才能确定值，Next.js会自动将其转为动态处理。</p><h2 id="三、客户端组件调用API路由"><a href="#三、客户端组件调用API路由" class="headerlink" title="三、客户端组件调用API路由"></a>三、客户端组件调用API路由</h2><h3 id="问题描述-2"><a href="#问题描述-2" class="headerlink" title="问题描述"></a>问题描述</h3><p>有些开发者误以为在客户端组件中就不能直接调用外部API，其实客户端组件同样可以直接发起网络请求。</p><h3 id="错误示例-2"><a href="#错误示例-2" class="headerlink" title="错误示例"></a>错误示例</h3><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="string">&#x27;use client&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> &#123; useState &#125; <span class="keyword">from</span> <span class="string">&#x27;react&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="keyword">function</span> <span class="title function_">PostsPage</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> [posts, setPosts] = <span class="title function_">useState</span>([])</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">ul</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">        &#123;posts.map(post =&gt; (</span></span><br><span class="line"><span class="language-xml">          <span class="tag">&lt;<span class="name">li</span> <span class="attr">key</span>=<span class="string">&#123;post.id&#125;</span>&gt;</span>&#123;post.title&#125;<span class="tag">&lt;/<span class="name">li</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">        ))&#125;</span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;/<span class="name">ul</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">button</span> <span class="attr">onClick</span>=<span class="string">&#123;async</span> () =&gt;</span> &#123;</span></span><br><span class="line"><span class="language-xml">        // 错误：调用自己创建的API路由</span></span><br><span class="line"><span class="language-xml">        const res = await fetch(&#x27;/api/posts&#x27;)</span></span><br><span class="line"><span class="language-xml">        const data = await res.json()</span></span><br><span class="line"><span class="language-xml">        setPosts(data)</span></span><br><span class="line"><span class="language-xml">      &#125;&#125;&gt;</span></span><br><span class="line"><span class="language-xml">        获取文章</span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;/<span class="name">button</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/&gt;</span></span></span><br><span class="line">  )</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="正确做法-2"><a href="#正确做法-2" class="headerlink" title="正确做法"></a>正确做法</h3><p>客户端组件可以直接调用外部API，无需中间层：</p><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="string">&#x27;use client&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> &#123; useState &#125; <span class="keyword">from</span> <span class="string">&#x27;react&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="keyword">function</span> <span class="title function_">PostsPage</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> [posts, setPosts] = <span class="title function_">useState</span>([])</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">ul</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">        &#123;posts.map(post =&gt; (</span></span><br><span class="line"><span class="language-xml">          <span class="tag">&lt;<span class="name">li</span> <span class="attr">key</span>=<span class="string">&#123;post.id&#125;</span>&gt;</span>&#123;post.title&#125;<span class="tag">&lt;/<span class="name">li</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">        ))&#125;</span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;/<span class="name">ul</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">button</span> <span class="attr">onClick</span>=<span class="string">&#123;async</span> () =&gt;</span> &#123;</span></span><br><span class="line"><span class="language-xml">        // 正确：直接调用外部API</span></span><br><span class="line"><span class="language-xml">        const res = await fetch(&#x27;https://api.example.com/posts&#x27;)</span></span><br><span class="line"><span class="language-xml">        const data = await res.json()</span></span><br><span class="line"><span class="language-xml">        setPosts(data)</span></span><br><span class="line"><span class="language-xml">      &#125;&#125;&gt;</span></span><br><span class="line"><span class="language-xml">        获取文章</span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;/<span class="name">button</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/&gt;</span></span></span><br><span class="line">  )</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="四、Suspense组件的错误使用"><a href="#四、Suspense组件的错误使用" class="headerlink" title="四、Suspense组件的错误使用"></a>四、Suspense组件的错误使用</h2><h3 id="问题描述-3"><a href="#问题描述-3" class="headerlink" title="问题描述"></a>问题描述</h3><p>Suspense用于流式渲染和加载状态展示，但很多开发者把它放错了位置，导致效果适得其反。</p><h3 id="错误示例-3"><a href="#错误示例-3" class="headerlink" title="错误示例"></a>错误示例</h3><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">Suspense</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;react&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">async</span> <span class="keyword">function</span> <span class="title function_">Posts</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> data = <span class="keyword">await</span> <span class="title function_">fetchPosts</span>() <span class="comment">// 模拟2秒延迟</span></span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">ul</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      &#123;data.map(post =&gt; (</span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">li</span> <span class="attr">key</span>=<span class="string">&#123;post.id&#125;</span>&gt;</span>&#123;post.title&#125;<span class="tag">&lt;/<span class="name">li</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      ))&#125;</span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">ul</span>&gt;</span></span></span><br><span class="line">  )</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="keyword">async</span> <span class="keyword">function</span> <span class="title function_">Page</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">h1</span>&gt;</span>文章列表<span class="tag">&lt;/<span class="name">h1</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      &#123;/* 错误：Suspense放在异步组件内部 */&#125;</span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">Suspense</span> <span class="attr">fallback</span>=<span class="string">&#123;</span>&lt;<span class="attr">div</span>&gt;</span>加载中...<span class="tag">&lt;/<span class="name">div</span>&gt;</span>&#125;&gt;</span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">Posts</span> /&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;/<span class="name">Suspense</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line">  )</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这样写会导致整个页面都要等待Posts加载完成才能开始渲染。</p><h3 id="正确做法-3"><a href="#正确做法-3" class="headerlink" title="正确做法"></a>正确做法</h3><p>Suspense应该包裹异步组件，放在父组件层面：</p><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">Suspense</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;react&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">async</span> <span class="keyword">function</span> <span class="title function_">Posts</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> data = <span class="keyword">await</span> <span class="title function_">fetchPosts</span>()</span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">ul</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      &#123;data.map(post =&gt; (</span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">li</span> <span class="attr">key</span>=<span class="string">&#123;post.id&#125;</span>&gt;</span>&#123;post.title&#125;<span class="tag">&lt;/<span class="name">li</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      ))&#125;</span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">ul</span>&gt;</span></span></span><br><span class="line">  )</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="keyword">function</span> <span class="title function_">Page</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">h1</span>&gt;</span>文章列表<span class="tag">&lt;/<span class="name">h1</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      &#123;/* 正确：Suspense在父组件中包裹异步子组件 */&#125;</span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">Suspense</span> <span class="attr">fallback</span>=<span class="string">&#123;</span>&lt;<span class="attr">div</span>&gt;</span>加载中...<span class="tag">&lt;/<span class="name">div</span>&gt;</span>&#125;&gt;</span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">Posts</span> /&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;/<span class="name">Suspense</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line">  )</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这样页面骨架会立即渲染，加载状态显示在对应位置，实现真正的流式加载体验。</p><h2 id="五、Context-Providers的错误封装"><a href="#五、Context-Providers的错误封装" class="headerlink" title="五、Context Providers的错误封装"></a>五、Context Providers的错误封装</h2><h3 id="问题描述-4"><a href="#问题描述-4" class="headerlink" title="问题描述"></a>问题描述</h3><p>在App Router中，Context Providers必须放在客户端组件中。如果直接在整个页面使用Context，会导致整个页面变成客户端组件，失去服务端渲染的优势。</p><h3 id="错误示例-4"><a href="#错误示例-4" class="headerlink" title="错误示例"></a>错误示例</h3><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="string">&#x27;use client&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> &#123; createContext, useContext, useState &#125; <span class="keyword">from</span> <span class="string">&#x27;react&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="title class_">ThemeContext</span> = <span class="title function_">createContext</span>(<span class="string">&#x27;light&#x27;</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">ThemeButton</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> theme = <span class="title function_">useContext</span>(<span class="title class_">ThemeContext</span>)</span><br><span class="line">  <span class="keyword">return</span> <span class="language-xml"><span class="tag">&lt;<span class="name">button</span>&gt;</span>主题: &#123;theme&#125;<span class="tag">&lt;/<span class="name">button</span>&gt;</span></span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 错误：整个页面变成客户端组件</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="keyword">function</span> <span class="title function_">Page</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">ThemeContext.Provider</span> <span class="attr">value</span>=<span class="string">&quot;dark&quot;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">ThemeButton</span> /&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">ThemeContext.Provider</span>&gt;</span></span></span><br><span class="line">  )</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这会导致页面无法享受服务端渲染的性能优势。</p><h3 id="正确做法-4"><a href="#正确做法-4" class="headerlink" title="正确做法"></a>正确做法</h3><p>将Provider组件独立出来，放在布局中：</p><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="comment">// app/theme-provider.js</span></span><br><span class="line"><span class="string">&#x27;use client&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> &#123; createContext, useContext, useState &#125; <span class="keyword">from</span> <span class="string">&#x27;react&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="title class_">ThemeContext</span> = <span class="title function_">createContext</span>()</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">function</span> <span class="title function_">ThemeProvider</span>(<span class="params">&#123; children &#125;</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> [theme, setTheme] = <span class="title function_">useState</span>(<span class="string">&#x27;light&#x27;</span>)</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">ThemeContext.Provider</span> <span class="attr">value</span>=<span class="string">&#123;&#123;</span> <span class="attr">theme</span>, <span class="attr">setTheme</span> &#125;&#125;&gt;</span></span></span><br><span class="line"><span class="language-xml">      &#123;children&#125;</span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">ThemeContext.Provider</span>&gt;</span></span></span><br><span class="line">  )</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">function</span> <span class="title function_">useTheme</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="title function_">useContext</span>(<span class="title class_">ThemeContext</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="comment">// app/layout.js</span></span><br><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">ThemeProvider</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;./theme-provider&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="keyword">function</span> <span class="title function_">RootLayout</span>(<span class="params">&#123; children &#125;</span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">html</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">body</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">ThemeProvider</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">          &#123;children&#125;</span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;/<span class="name">ThemeProvider</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;/<span class="name">body</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">html</span>&gt;</span></span></span><br><span class="line">  )</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="comment">// app/page.js</span></span><br><span class="line"><span class="keyword">import</span> &#123; useTheme &#125; <span class="keyword">from</span> <span class="string">&#x27;./theme-provider&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">ThemeButton</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> &#123; theme &#125; = <span class="title function_">useTheme</span>()</span><br><span class="line">  <span class="keyword">return</span> <span class="language-xml"><span class="tag">&lt;<span class="name">button</span>&gt;</span>主题: &#123;theme&#125;<span class="tag">&lt;/<span class="name">button</span>&gt;</span></span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 这样页面仍然是服务端组件</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="keyword">function</span> <span class="title function_">Page</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="language-xml"><span class="tag">&lt;<span class="name">ThemeButton</span> /&gt;</span></span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="六、滥用”use-client”指令"><a href="#六、滥用”use-client”指令" class="headerlink" title="六、滥用”use client”指令"></a>六、滥用”use client”指令</h2><h3 id="问题描述-5"><a href="#问题描述-5" class="headerlink" title="问题描述"></a>问题描述</h3><p>很多开发者习惯在每个组件都加上”use client”，导致整个应用退化为纯客户端渲染，失去服务端渲染的优势。</p><h3 id="正确理解"><a href="#正确理解" class="headerlink" title="正确理解"></a>正确理解</h3><p>“use client”声明的是服务端组件和客户端组件的边界。当你在父组件中直接导入子组件时，子组件会自动继承父组件的渲染环境。</p><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="comment">// 父组件是客户端组件</span></span><br><span class="line"><span class="string">&#x27;use client&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> &#123; useState &#125; <span class="keyword">from</span> <span class="string">&#x27;react&#x27;</span></span><br><span class="line"><span class="keyword">import</span> <span class="title class_">Button</span> <span class="keyword">from</span> <span class="string">&#x27;./button&#x27;</span>  <span class="comment">// 不需要&quot;use client&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="keyword">function</span> <span class="title function_">Parent</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> [count, setCount] = <span class="title function_">useState</span>(<span class="number">0</span>)</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">span</span>&gt;</span>&#123;count&#125;<span class="tag">&lt;/<span class="name">span</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      &#123;/* Button会自动成为客户端组件的一部分 */&#125;</span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">Button</span> <span class="attr">onClick</span>=<span class="string">&#123;()</span> =&gt;</span> setCount(c =&gt; c + 1)&#125; /&gt;</span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line">  )</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>只有当组件使用了以下特性时才需要”use client”：useState&#x2F;useEffect等Hook、事件处理函数、浏览器专用API、自定义Context Provider。</p><h2 id="七、服务端组件与客户端组件的组合"><a href="#七、服务端组件与客户端组件的组合" class="headerlink" title="七、服务端组件与客户端组件的组合"></a>七、服务端组件与客户端组件的组合</h2><h3 id="问题描述-6"><a href="#问题描述-6" class="headerlink" title="问题描述"></a>问题描述</h3><p>如何在客户端组件中使用服务端组件？直接导入是不行的。</p><h3 id="错误示例-5"><a href="#错误示例-5" class="headerlink" title="错误示例"></a>错误示例</h3><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="string">&#x27;use client&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> <span class="title class_">ServerComponent</span> <span class="keyword">from</span> <span class="string">&#x27;./server-component&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="keyword">function</span> <span class="title function_">ClientComponent</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      &#123;/* 错误：服务端组件不能直接导入到客户端组件 */&#125;</span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">ServerComponent</span> /&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line">  )</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="正确做法-5"><a href="#正确做法-5" class="headerlink" title="正确做法"></a>正确做法</h3><p>通过props传递服务端组件：</p><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="comment">// app/page.js - 服务端组件</span></span><br><span class="line"><span class="keyword">import</span> <span class="title class_">ClientComponent</span> <span class="keyword">from</span> <span class="string">&#x27;./client-component&#x27;</span></span><br><span class="line"><span class="keyword">import</span> <span class="title class_">ServerComponent</span> <span class="keyword">from</span> <span class="string">&#x27;./server-component&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="keyword">function</span> <span class="title function_">Page</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">ClientComponent</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">ServerComponent</span> /&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">ClientComponent</span>&gt;</span></span></span><br><span class="line">  )</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="comment">// app/client-component.js - 客户端组件</span></span><br><span class="line"><span class="string">&#x27;use client&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="keyword">function</span> <span class="title function_">ClientComponent</span>(<span class="params">&#123; children &#125;</span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">p</span>&gt;</span>客户端组件<span class="tag">&lt;/<span class="name">p</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      &#123;/* children就是传入的服务端组件 */&#125;</span></span><br><span class="line"><span class="language-xml">      &#123;children&#125;</span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line">  )</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这是因为props传递的是渲染结果，而非组件本身，服务端组件先在服务器上渲染完成，再将结果传递给客户端组件。</p><h2 id="八、忽视数据重新验证"><a href="#八、忽视数据重新验证" class="headerlink" title="八、忽视数据重新验证"></a>八、忽视数据重新验证</h2><h3 id="问题描述-7"><a href="#问题描述-7" class="headerlink" title="问题描述"></a>问题描述</h3><p>使用Server Actions修改数据后，页面不会自动更新，需要手动触发重新验证。</p><h3 id="错误示例-6"><a href="#错误示例-6" class="headerlink" title="错误示例"></a>错误示例</h3><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="comment">// app/actions.js</span></span><br><span class="line"><span class="string">&#x27;use server&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">async</span> <span class="keyword">function</span> <span class="title function_">createPost</span>(<span class="params">formData</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> title = formData.<span class="title function_">get</span>(<span class="string">&#x27;title&#x27;</span>)</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 模拟数据库操作</span></span><br><span class="line">  <span class="keyword">await</span> db.<span class="property">posts</span>.<span class="title function_">create</span>(&#123; title &#125;)</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 错误：没有重新验证数据</span></span><br><span class="line">  <span class="keyword">return</span> &#123; <span class="attr">success</span>: <span class="literal">true</span> &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="comment">// app/page.js</span></span><br><span class="line"><span class="keyword">import</span> &#123; createPost &#125; <span class="keyword">from</span> <span class="string">&#x27;./actions&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="keyword">function</span> <span class="title function_">Page</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">form</span> <span class="attr">action</span>=<span class="string">&#123;createPost&#125;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">input</span> <span class="attr">name</span>=<span class="string">&quot;title&quot;</span> /&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">button</span> <span class="attr">type</span>=<span class="string">&quot;submit&quot;</span>&gt;</span>提交<span class="tag">&lt;/<span class="name">button</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">form</span>&gt;</span></span></span><br><span class="line">  )</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>提交数据后，页面不会自动更新，需要手动刷新。</p><h3 id="正确做法-6"><a href="#正确做法-6" class="headerlink" title="正确做法"></a>正确做法</h3><p>使用revalidatePath或revalidateTag重新验证数据：</p><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="comment">// app/actions.js</span></span><br><span class="line"><span class="string">&#x27;use server&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> &#123; revalidatePath &#125; <span class="keyword">from</span> <span class="string">&#x27;next/cache&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">async</span> <span class="keyword">function</span> <span class="title function_">createPost</span>(<span class="params">formData</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> title = formData.<span class="title function_">get</span>(<span class="string">&#x27;title&#x27;</span>)</span><br><span class="line"></span><br><span class="line">  <span class="keyword">await</span> db.<span class="property">posts</span>.<span class="title function_">create</span>(&#123; title &#125;)</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 正确：重新验证页面数据</span></span><br><span class="line">  <span class="title function_">revalidatePath</span>(<span class="string">&#x27;/posts&#x27;</span>)</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> &#123; <span class="attr">success</span>: <span class="literal">true</span> &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>或者使用revalidateTag标记：</p><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="comment">// app/actions.js</span></span><br><span class="line"><span class="string">&#x27;use server&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> &#123; revalidateTag &#125; <span class="keyword">from</span> <span class="string">&#x27;next/cache&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">async</span> <span class="keyword">function</span> <span class="title function_">createPost</span>(<span class="params">formData</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> title = formData.<span class="title function_">get</span>(<span class="string">&#x27;title&#x27;</span>)</span><br><span class="line"></span><br><span class="line">  <span class="keyword">await</span> db.<span class="property">posts</span>.<span class="title function_">create</span>(&#123; title &#125;)</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 重新验证特定标签的数据</span></span><br><span class="line">  <span class="title function_">revalidateTag</span>(<span class="string">&#x27;posts&#x27;</span>)</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> &#123; <span class="attr">success</span>: <span class="literal">true</span> &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 获取数据时标记标签</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">async</span> <span class="keyword">function</span> <span class="title function_">getPosts</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> res = <span class="keyword">await</span> <span class="title function_">fetch</span>(<span class="string">&#x27;https://api.example.com/posts&#x27;</span>, &#123;</span><br><span class="line">    <span class="attr">next</span>: &#123; <span class="attr">tags</span>: [<span class="string">&#x27;posts&#x27;</span>] &#125;</span><br><span class="line">  &#125;)</span><br><span class="line">  <span class="keyword">return</span> res.<span class="title function_">json</span>()</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="九、try-catch中使用redirect"><a href="#九、try-catch中使用redirect" class="headerlink" title="九、try&#x2F;catch中使用redirect"></a>九、try&#x2F;catch中使用redirect</h2><h3 id="问题描述-8"><a href="#问题描述-8" class="headerlink" title="问题描述"></a>问题描述</h3><p>redirect函数内部是通过抛出错误实现的，如果放在try&#x2F;catch中会导致失效。</p><h3 id="错误示例-7"><a href="#错误示例-7" class="headerlink" title="错误示例"></a>错误示例</h3><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="string">&#x27;use server&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> &#123; redirect &#125; <span class="keyword">from</span> <span class="string">&#x27;next/navigation&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">async</span> <span class="keyword">function</span> <span class="title function_">login</span>(<span class="params">formData</span>) &#123;</span><br><span class="line">  <span class="keyword">try</span> &#123;</span><br><span class="line">    <span class="keyword">const</span> email = formData.<span class="title function_">get</span>(<span class="string">&#x27;email&#x27;</span>)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (!email) &#123;</span><br><span class="line">      <span class="comment">// 错误：redirect在catch中无法生效</span></span><br><span class="line">      <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">Error</span>(<span class="string">&#x27;需要邮箱&#x27;</span>)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">await</span> <span class="title function_">doLogin</span>(email)</span><br><span class="line">    <span class="title function_">redirect</span>(<span class="string">&#x27;/dashboard&#x27;</span>)</span><br><span class="line">  &#125; <span class="keyword">catch</span> (error) &#123;</span><br><span class="line">    <span class="keyword">return</span> &#123; <span class="attr">error</span>: error.<span class="property">message</span> &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="正确做法-7"><a href="#正确做法-7" class="headerlink" title="正确做法"></a>正确做法</h3><p>将redirect放在try&#x2F;catch之外，或使用finally：</p><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="comment">// 方法一：放在try之外</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">async</span> <span class="keyword">function</span> <span class="title function_">login</span>(<span class="params">formData</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> email = formData.<span class="title function_">get</span>(<span class="string">&#x27;email&#x27;</span>)</span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (!email) &#123;</span><br><span class="line">    <span class="keyword">return</span> &#123; <span class="attr">error</span>: <span class="string">&#x27;需要邮箱&#x27;</span> &#125;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">await</span> <span class="title function_">doLogin</span>(email)</span><br><span class="line">  <span class="title function_">redirect</span>(<span class="string">&#x27;/dashboard&#x27;</span>)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 方法二：使用finally</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">async</span> <span class="keyword">function</span> <span class="title function_">login</span>(<span class="params">formData</span>) &#123;</span><br><span class="line">  <span class="keyword">try</span> &#123;</span><br><span class="line">    <span class="keyword">const</span> email = formData.<span class="title function_">get</span>(<span class="string">&#x27;email&#x27;</span>)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (!email) &#123;</span><br><span class="line">      <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">Error</span>(<span class="string">&#x27;需要邮箱&#x27;</span>)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">await</span> <span class="title function_">doLogin</span>(email)</span><br><span class="line">  &#125; <span class="keyword">catch</span> (error) &#123;</span><br><span class="line">    <span class="keyword">return</span> &#123; <span class="attr">error</span>: error.<span class="property">message</span> &#125;</span><br><span class="line">  &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">    <span class="comment">// 正确：finally中redirect可以执行</span></span><br><span class="line">    <span class="title function_">redirect</span>(<span class="string">&#x27;/dashboard&#x27;</span>)</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="十、忽视搜索引擎优化"><a href="#十、忽视搜索引擎优化" class="headerlink" title="十、忽视搜索引擎优化"></a>十、忽视搜索引擎优化</h2><h3 id="问题描述-9"><a href="#问题描述-9" class="headerlink" title="问题描述"></a>问题描述</h3><p>Next.js App Router默认不自动生成metadata，需要手动配置。</p><h3 id="正确做法-8"><a href="#正确做法-8" class="headerlink" title="正确做法"></a>正确做法</h3><p>使用Next.js提供的Metadata API：</p><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="comment">// app/page.js</span></span><br><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">Metadata</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;next&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> <span class="attr">metadata</span>: <span class="title class_">Metadata</span> = &#123;</span><br><span class="line">  <span class="attr">title</span>: <span class="string">&#x27;页面标题&#x27;</span>,</span><br><span class="line">  <span class="attr">description</span>: <span class="string">&#x27;页面描述&#x27;</span>,</span><br><span class="line">  <span class="attr">openGraph</span>: &#123;</span><br><span class="line">    <span class="attr">title</span>: <span class="string">&#x27;社交分享标题&#x27;</span>,</span><br><span class="line">    <span class="attr">description</span>: <span class="string">&#x27;社交分享描述&#x27;</span>,</span><br><span class="line">    <span class="attr">images</span>: [<span class="string">&#x27;/og-image.jpg&#x27;</span>],</span><br><span class="line">  &#125;,</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="keyword">function</span> <span class="title function_">Page</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="language-xml"><span class="tag">&lt;<span class="name">h1</span>&gt;</span>内容<span class="tag">&lt;/<span class="name">h1</span>&gt;</span></span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>对于动态路由：</p><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="comment">// app/posts/[slug]/page.js</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">async</span> <span class="keyword">function</span> <span class="title function_">generateMetadata</span>(<span class="params">&#123; params &#125;</span>): <span class="title class_">Promise</span>&lt;<span class="title class_">Metadata</span>&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> post = <span class="keyword">await</span> <span class="title function_">getPost</span>(params.<span class="property">slug</span>)</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> &#123;</span><br><span class="line">    <span class="attr">title</span>: post.<span class="property">title</span>,</span><br><span class="line">    <span class="attr">description</span>: post.<span class="property">excerpt</span>,</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>Next.js App Router带来了全新的开发范式，但也需要我们转变开发习惯：</p><ol><li><strong>服务端组件优先</strong>：尽量在服务端组件中完成数据获取和渲染</li><li><strong>正确使用Suspense</strong>：将Suspense放在父组件，包裹异步子组件</li><li><strong>合理划分客户端与服务端</strong>：只在必要时使用”use client”</li><li><strong>注意数据重新验证</strong>：修改数据后使用revalidatePath或revalidateTag</li><li><strong>避免常见陷阱</strong>：不要在try&#x2F;catch中使用redirect，正确配置Context Provider</li></ol><p>掌握这些最佳实践，能够帮助你更好地使用Next.js App Router，构建更高效、更优质的Web应用。</p>]]>
    </content>
    <id>http://fe.poetries.top/2025/06/23/nextjs-app-router-pitfalls-challenges/</id>
    <link href="http://fe.poetries.top/2025/06/23/nextjs-app-router-pitfalls-challenges/"/>
    <published>2025-06-23T06:40:12.000Z</published>
    <summary>深度总结Next.js App Router开发中常见的10个错误，包括服务端组件调用API、路由处理程序静态化、Suspense正确用法、Context Providers配置等问题，并提供最佳实践方案。</summary>
    <title>Next.js App Router开发避坑指南 常见错误与最佳实践</title>
    <updated>2026-03-08T10:22:42.089Z</updated>
  </entry>
  <entry>
    <author>
      <name>Poetry</name>
    </author>
    <category term="Front-End" scheme="http://fe.poetries.top/categories/Front-End/"/>
    <category term="React" scheme="http://fe.poetries.top/tags/React/"/>
    <category term="前端开发" scheme="http://fe.poetries.top/tags/%E5%89%8D%E7%AB%AF%E5%BC%80%E5%8F%91/"/>
    <category term="Next.js" scheme="http://fe.poetries.top/tags/Next-js/"/>
    <category term="React 19" scheme="http://fe.poetries.top/tags/React-19/"/>
    <category term="升级指南" scheme="http://fe.poetries.top/tags/%E5%8D%87%E7%BA%A7%E6%8C%87%E5%8D%97/"/>
    <content>
      <![CDATA[<p>作为React生态中最流行的全栈框架，Next.js每次版本更新都牵动着无数开发者的心。Next.js 15带来了自App Router推出以来最重要的变化——全面拥抱React 19，同时对多个核心API进行了异步化改造。这些变化不仅影响着代码的编写方式，更深刻改变了我们对服务端渲染的认知。</p><p>本文将基于官方文档，系统性地解析Next.js 15的所有重要变化。无论你是正在考虑升级的现有项目开发者，还是准备学习Next.js的新人，这篇文章都将帮助你全面理解新版本的特性和升级策略。</p><h2 id="升级准备工作"><a href="#升级准备工作" class="headerlink" title="升级准备工作"></a>升级准备工作</h2><h3 id="环境要求"><a href="#环境要求" class="headerlink" title="环境要求"></a>环境要求</h3><p>在升级到Next.js 15之前，需要确保你的开发环境满足以下要求：</p><ul><li>Node.js 18.17.0 或更高版本</li><li>如果使用TypeScript，需要安装最新的<code>@types/react</code>和<code>@types/react-dom</code></li><li>建议使用pnpm 8+、npm 10+或yarn 1.22+</li></ul><h3 id="一键升级"><a href="#一键升级" class="headerlink" title="一键升级"></a>一键升级</h3><p>Next.js官方提供了Codemod工具，可以自动完成大部分迁移工作。在项目根目录下运行以下命令：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 使用pnpm</span></span><br><span class="line">pnpm dlx @next/codemod@canary upgrade latest</span><br><span class="line"></span><br><span class="line"><span class="comment"># 使用npm</span></span><br><span class="line">npx @next/codemod@canary upgrade latest</span><br><span class="line"></span><br><span class="line"><span class="comment"># 使用yarn</span></span><br><span class="line">yarn dlx @next/codemod@canary upgrade latest</span><br><span class="line"></span><br><span class="line"><span class="comment"># 使用bun</span></span><br><span class="line">bunx @next/codemod@canary upgrade latest</span><br></pre></td></tr></table></figure><h3 id="手动升级"><a href="#手动升级" class="headerlink" title="手动升级"></a>手动升级</h3><p>如果你 prefer 手动升级，需要同时更新Next.js和React相关依赖：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="comment"># pnpm</span></span><br><span class="line">pnpm add next@latest react@latest react-dom@latest eslint-config-next@latest</span><br><span class="line"></span><br><span class="line"><span class="comment"># npm</span></span><br><span class="line">npm install next@latest react@latest react-dom@latest eslint-config-next@latest</span><br><span class="line"></span><br><span class="line"><span class="comment"># yarn</span></span><br><span class="line">yarn add next@latest react@latest react-dom@latest eslint-config-next@latest</span><br></pre></td></tr></table></figure><blockquote><p><strong>注意</strong>：如果遇到peer dependencies警告，可能需要手动指定React版本或使用<code>--force</code>&#x2F;<code>--legacy-peer-deps</code>参数。这在Next.js 15和React 19正式稳定后将不再需要。</p></blockquote><h2 id="React-19全面支持"><a href="#React-19全面支持" class="headerlink" title="React 19全面支持"></a>React 19全面支持</h2><h3 id="版本要求"><a href="#版本要求" class="headerlink" title="版本要求"></a>版本要求</h3><p>Next.js 15要求React和ReactDom的最低版本为19。这意味着你可以直接使用React 19带来的所有新特性，包括：</p><ul><li><strong>useActionState</strong>：取代原来的useFormState，提供更强大的表单状态管理能力</li><li><strong>useFormStatus增强</strong>：新增<code>data</code>、<code>method</code>、<code>action</code>等属性</li><li><strong>Actions</strong>：支持在服务端定义可调用函数</li></ul><h3 id="useFormState迁移到useActionState"><a href="#useFormState迁移到useActionState" class="headerlink" title="useFormState迁移到useActionState"></a>useFormState迁移到useActionState</h3><p><code>useFormState</code>已被<code>useActionState</code>替代，虽然前者仍然可用，但已标记为废弃。<code>useActionState</code>提供了更丰富的API，包括直接读取<code>pending</code>状态的能力：</p><figure class="highlight tsx"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; useActionState &#125; <span class="keyword">from</span> <span class="string">&#x27;react&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">async</span> <span class="keyword">function</span> <span class="title function_">submitForm</span>(<span class="params">prevState, formData</span>) &#123;</span><br><span class="line">  <span class="comment">// 表单提交逻辑</span></span><br><span class="line">  <span class="keyword">const</span> result = <span class="keyword">await</span> <span class="title function_">fetch</span>(<span class="string">&#x27;/api/submit&#x27;</span>, &#123;</span><br><span class="line">    <span class="attr">body</span>: formData</span><br><span class="line">  &#125;)</span><br><span class="line">  <span class="keyword">return</span> result.<span class="title function_">json</span>()</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="keyword">function</span> <span class="title function_">Form</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> [state, submitAction, isPending] = <span class="title function_">useActionState</span>(submitForm, <span class="literal">null</span>)</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">form</span> <span class="attr">action</span>=<span class="string">&#123;submitAction&#125;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      &#123;/* 表单字段 */&#125;</span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">button</span> <span class="attr">type</span>=<span class="string">&quot;submit&quot;</span> <span class="attr">disabled</span>=<span class="string">&#123;isPending&#125;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">        &#123;isPending ? &#x27;提交中...&#x27; : &#x27;提交&#x27;&#125;</span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;/<span class="name">button</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">form</span>&gt;</span></span></span><br><span class="line">  )</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="useFormStatus增强"><a href="#useFormStatus增强" class="headerlink" title="useFormStatus增强"></a>useFormStatus增强</h3><p>在React 19中，<code>useFormStatus</code>增加了更多有用的属性：</p><figure class="highlight tsx"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; useFormStatus &#125; <span class="keyword">from</span> <span class="string">&#x27;react-dom&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">SubmitButton</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> &#123; pending, data, method, action &#125; = <span class="title function_">useFormStatus</span>()</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">button</span> <span class="attr">type</span>=<span class="string">&quot;submit&quot;</span> <span class="attr">disabled</span>=<span class="string">&#123;pending&#125;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      &#123;pending ? &#x27;提交中...&#x27; : &#x27;提交&#x27;&#125;</span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">button</span>&gt;</span></span></span><br><span class="line">  )</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><blockquote><p><strong>提示</strong>：如果你还未升级到React 19，<code>useFormStatus</code>仍然只提供<code>pending</code>属性。</p></blockquote><h2 id="异步Request-APIs：重大架构变化"><a href="#异步Request-APIs：重大架构变化" class="headerlink" title="异步Request APIs：重大架构变化"></a>异步Request APIs：重大架构变化</h2><p>这是Next.js 15最重要的变化之一。原本同步的动态API（如cookies、headers、draftMode）现在需要异步调用。这些API依赖运行时信息，异步化后能更好地支持React 19的并发渲染能力。</p><h3 id="cookies异步化"><a href="#cookies异步化" class="headerlink" title="cookies异步化"></a>cookies异步化</h3><figure class="highlight tsx"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; cookies &#125; <span class="keyword">from</span> <span class="string">&#x27;next/headers&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// Next.js 14（同步）</span></span><br><span class="line"><span class="keyword">const</span> cookieStore = <span class="title function_">cookies</span>()</span><br><span class="line"><span class="keyword">const</span> token = cookieStore.<span class="title function_">get</span>(<span class="string">&#x27;token&#x27;</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment">// Next.js 15（异步）</span></span><br><span class="line"><span class="keyword">const</span> cookieStore = <span class="keyword">await</span> <span class="title function_">cookies</span>()</span><br><span class="line"><span class="keyword">const</span> token = cookieStore.<span class="title function_">get</span>(<span class="string">&#x27;token&#x27;</span>)</span><br></pre></td></tr></table></figure><p>如果你暂时不想修改所有代码，可以使用类型转换保持同步调用（仅作为过渡方案）：</p><figure class="highlight tsx"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; cookies, <span class="keyword">type</span> <span class="title class_">UnsafeUnwrappedCookies</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;next/headers&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 临时同步用法（不推荐，仅作过渡）</span></span><br><span class="line"><span class="keyword">const</span> cookieStore = <span class="title function_">cookies</span>() <span class="keyword">as</span> <span class="built_in">unknown</span> <span class="keyword">as</span> <span class="title class_">UnsafeUnwrappedCookies</span></span><br><span class="line"><span class="comment">// 开发模式下会收到警告</span></span><br><span class="line"><span class="keyword">const</span> token = cookieStore.<span class="title function_">get</span>(<span class="string">&#x27;token&#x27;</span>)</span><br></pre></td></tr></table></figure><h3 id="headers异步化"><a href="#headers异步化" class="headerlink" title="headers异步化"></a>headers异步化</h3><figure class="highlight tsx"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; headers &#125; <span class="keyword">from</span> <span class="string">&#x27;next/headers&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// Next.js 14（同步）</span></span><br><span class="line"><span class="keyword">const</span> headersList = <span class="title function_">headers</span>()</span><br><span class="line"><span class="keyword">const</span> userAgent = headersList.<span class="title function_">get</span>(<span class="string">&#x27;user-agent&#x27;</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment">// Next.js 15（异步）</span></span><br><span class="line"><span class="keyword">const</span> headersList = <span class="keyword">await</span> <span class="title function_">headers</span>()</span><br><span class="line"><span class="keyword">const</span> userAgent = headersList.<span class="title function_">get</span>(<span class="string">&#x27;user-agent&#x27;</span>)</span><br></pre></td></tr></table></figure><h3 id="draftMode异步化"><a href="#draftMode异步化" class="headerlink" title="draftMode异步化"></a>draftMode异步化</h3><figure class="highlight tsx"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; draftMode &#125; <span class="keyword">from</span> <span class="string">&#x27;next/headers&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// Next.js 14（同步）</span></span><br><span class="line"><span class="keyword">const</span> &#123; isEnabled &#125; = <span class="title function_">draftMode</span>()</span><br><span class="line"></span><br><span class="line"><span class="comment">// Next.js 15（异步）</span></span><br><span class="line"><span class="keyword">const</span> &#123; isEnabled &#125; = <span class="keyword">await</span> <span class="title function_">draftMode</span>()</span><br></pre></td></tr></table></figure><h3 id="params和searchParams异步化"><a href="#params和searchParams异步化" class="headerlink" title="params和searchParams异步化"></a>params和searchParams异步化</h3><p>在Next.js 15中，layout和page组件的<code>params</code>和<code>searchParams</code>都变成了Promise类型。</p><h4 id="异步Page组件"><a href="#异步Page组件" class="headerlink" title="异步Page组件"></a>异步Page组件</h4><figure class="highlight tsx"><table><tr><td class="code"><pre><span class="line"><span class="comment">// Next.js 14</span></span><br><span class="line"><span class="keyword">type</span> <span class="title class_">Params</span> = &#123; <span class="attr">slug</span>: <span class="built_in">string</span> &#125;</span><br><span class="line"><span class="keyword">type</span> <span class="title class_">SearchParams</span> = &#123; [<span class="attr">key</span>: <span class="built_in">string</span>]: <span class="built_in">string</span> | <span class="built_in">string</span>[] | <span class="literal">undefined</span> &#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="keyword">async</span> <span class="keyword">function</span> <span class="title function_">Page</span>(<span class="params">&#123;</span></span><br><span class="line"><span class="params">  params,</span></span><br><span class="line"><span class="params">  searchParams,</span></span><br><span class="line"><span class="params">&#125;: &#123;</span></span><br><span class="line"><span class="params">  params: Params</span></span><br><span class="line"><span class="params">  searchParams: SearchParams</span></span><br><span class="line"><span class="params">&#125;</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> &#123; slug &#125; = params</span><br><span class="line">  <span class="keyword">const</span> &#123; query &#125; = searchParams</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// Next.js 15</span></span><br><span class="line"><span class="keyword">type</span> <span class="title class_">Params</span> = <span class="title class_">Promise</span>&lt;&#123; <span class="attr">slug</span>: <span class="built_in">string</span> &#125;&gt;</span><br><span class="line"><span class="keyword">type</span> <span class="title class_">SearchParams</span> = <span class="title class_">Promise</span>&lt;&#123; [<span class="attr">key</span>: <span class="built_in">string</span>]: <span class="built_in">string</span> | <span class="built_in">string</span>[] | <span class="literal">undefined</span> &#125;&gt;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="keyword">async</span> <span class="keyword">function</span> <span class="title function_">Page</span>(<span class="params"><span class="attr">props</span>: &#123;</span></span><br><span class="line"><span class="params">  params: Params</span></span><br><span class="line"><span class="params">  searchParams: SearchParams</span></span><br><span class="line"><span class="params">&#125;</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> params = <span class="keyword">await</span> props.<span class="property">params</span></span><br><span class="line">  <span class="keyword">const</span> searchParams = <span class="keyword">await</span> props.<span class="property">searchParams</span></span><br><span class="line">  <span class="keyword">const</span> &#123; slug &#125; = params</span><br><span class="line">  <span class="keyword">const</span> &#123; query &#125; = searchParams</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="同步Layout组件（使用use-hook）"><a href="#同步Layout组件（使用use-hook）" class="headerlink" title="同步Layout组件（使用use hook）"></a>同步Layout组件（使用use hook）</h4><p>如果你需要保持Layout组件同步，可以使用React 19的<code>use</code> hook：</p><figure class="highlight tsx"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; use &#125; <span class="keyword">from</span> <span class="string">&#x27;react&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> <span class="title class_">Params</span> = <span class="title class_">Promise</span>&lt;&#123; <span class="attr">slug</span>: <span class="built_in">string</span> &#125;&gt;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="keyword">function</span> <span class="title function_">Layout</span>(<span class="params"><span class="attr">props</span>: &#123;</span></span><br><span class="line"><span class="params">  children: React.ReactNode</span></span><br><span class="line"><span class="params">  params: Params</span></span><br><span class="line"><span class="params">&#125;</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> params = <span class="title function_">use</span>(props.<span class="property">params</span>)</span><br><span class="line">  <span class="keyword">const</span> &#123; slug &#125; = params</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> <span class="language-xml"><span class="tag">&lt;<span class="name">div</span>&gt;</span>&#123;slug&#125;<span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="Route-Handlers中的params"><a href="#Route-Handlers中的params" class="headerlink" title="Route Handlers中的params"></a>Route Handlers中的params</h4><figure class="highlight tsx"><table><tr><td class="code"><pre><span class="line"><span class="comment">// Next.js 14</span></span><br><span class="line"><span class="keyword">type</span> <span class="title class_">Params</span> = &#123; <span class="attr">slug</span>: <span class="built_in">string</span> &#125;</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">async</span> <span class="keyword">function</span> <span class="title function_">GET</span>(<span class="params"><span class="attr">request</span>: <span class="title class_">Request</span>, <span class="attr">segmentData</span>: &#123; params: Params &#125;</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> params = segmentData.<span class="property">params</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// Next.js 15</span></span><br><span class="line"><span class="keyword">type</span> <span class="title class_">Params</span> = <span class="title class_">Promise</span>&lt;&#123; <span class="attr">slug</span>: <span class="built_in">string</span> &#125;&gt;</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">async</span> <span class="keyword">function</span> <span class="title function_">GET</span>(<span class="params"><span class="attr">request</span>: <span class="title class_">Request</span>, <span class="attr">segmentData</span>: &#123; params: Params &#125;</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> params = <span class="keyword">await</span> segmentData.<span class="property">params</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="Fetch请求默认行为变化"><a href="#Fetch请求默认行为变化" class="headerlink" title="Fetch请求默认行为变化"></a>Fetch请求默认行为变化</h2><h3 id="默认不再缓存"><a href="#默认不再缓存" class="headerlink" title="默认不再缓存"></a>默认不再缓存</h3><p>Next.js 15中，<code>fetch</code>请求默认不再自动缓存。这意味着你需要在使用时明确指定缓存策略：</p><figure class="highlight tsx"><table><tr><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="keyword">async</span> <span class="keyword">function</span> <span class="title function_">RootLayout</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="comment">// 不缓存（默认行为）</span></span><br><span class="line">  <span class="keyword">const</span> a = <span class="keyword">await</span> <span class="title function_">fetch</span>(<span class="string">&#x27;https://example.com/api/data&#x27;</span>)</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 强制缓存</span></span><br><span class="line">  <span class="keyword">const</span> b = <span class="keyword">await</span> <span class="title function_">fetch</span>(<span class="string">&#x27;https://example.com/api/data&#x27;</span>, &#123;</span><br><span class="line">    <span class="attr">cache</span>: <span class="string">&#x27;force-cache&#x27;</span></span><br><span class="line">  &#125;)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="批量启用缓存"><a href="#批量启用缓存" class="headerlink" title="批量启用缓存"></a>批量启用缓存</h3><p>如果你希望整个layout或page中的fetch请求都默认缓存，可以使用<code>fetchCache</code>配置：</p><figure class="highlight tsx"><table><tr><td class="code"><pre><span class="line"><span class="comment">// 根布局中设置默认缓存策略</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> fetchCache = <span class="string">&#x27;default-cache&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="keyword">async</span> <span class="keyword">function</span> <span class="title function_">RootLayout</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="comment">// 默认缓存</span></span><br><span class="line">  <span class="keyword">const</span> a = <span class="keyword">await</span> <span class="title function_">fetch</span>(<span class="string">&#x27;https://example.com/api/data&#x27;</span>)</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 明确不缓存</span></span><br><span class="line">  <span class="keyword">const</span> b = <span class="keyword">await</span> <span class="title function_">fetch</span>(<span class="string">&#x27;https://example.com/api/data&#x27;</span>, &#123;</span><br><span class="line">    <span class="attr">cache</span>: <span class="string">&#x27;no-store&#x27;</span></span><br><span class="line">  &#125;)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="Route-Handlers的GET请求"><a href="#Route-Handlers的GET请求" class="headerlink" title="Route Handlers的GET请求"></a>Route Handlers的GET请求</h3><p>GET请求默认不再缓存。如果需要缓存，需要显式设置：</p><figure class="highlight tsx"><table><tr><td class="code"><pre><span class="line"><span class="comment">// 强制静态</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> dynamic = <span class="string">&#x27;force-static&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">async</span> <span class="keyword">function</span> <span class="title function_">GET</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="title class_">Response</span>.<span class="title function_">json</span>(&#123; <span class="attr">data</span>: <span class="string">&#x27;Hello&#x27;</span> &#125;)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="客户端路由缓存策略调整"><a href="#客户端路由缓存策略调整" class="headerlink" title="客户端路由缓存策略调整"></a>客户端路由缓存策略调整</h2><h3 id="页面导航不再复用缓存"><a href="#页面导航不再复用缓存" class="headerlink" title="页面导航不再复用缓存"></a>页面导航不再复用缓存</h3><p>在使用<code>&lt;Link&gt;</code>或<code>useRouter</code>进行页面导航时，页面组件不再从客户端路由缓存中复用。这意味着每次导航都会获取最新数据。</p><p>不过，浏览器后退前进导航和共享布局仍然会复用缓存。</p><h3 id="配置缓存时间"><a href="#配置缓存时间" class="headerlink" title="配置缓存时间"></a>配置缓存时间</h3><p>你可以通过<code>staleTimes</code>配置来控制页面缓存时间：</p><figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="comment">/** <span class="doctag">@type</span> &#123;<span class="type">import(&#x27;next&#x27;).NextConfig</span>&#125; */</span></span><br><span class="line"><span class="keyword">const</span> nextConfig = &#123;</span><br><span class="line">  <span class="attr">experimental</span>: &#123;</span><br><span class="line">    <span class="attr">staleTimes</span>: &#123;</span><br><span class="line">      <span class="attr">dynamic</span>: <span class="number">30</span>,  <span class="comment">// 动态路由30秒后过期</span></span><br><span class="line">      <span class="attr">static</span>: <span class="number">180</span>,  <span class="comment">// 静态路由180秒后过期</span></span><br><span class="line">    &#125;,</span><br><span class="line">  &#125;,</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="variable language_">module</span>.<span class="property">exports</span> = nextConfig</span><br></pre></td></tr></table></figure><blockquote><p><strong>注意</strong>：Layouts和loading状态在导航时仍然会被复用。</p></blockquote><h2 id="其他重要API变更"><a href="#其他重要API变更" class="headerlink" title="其他重要API变更"></a>其他重要API变更</h2><h3 id="next-font包移除"><a href="#next-font包移除" class="headerlink" title="@next&#x2F;font包移除"></a>@next&#x2F;font包移除</h3><p><code>@next/font</code>包已被移除，统一使用内置的<code>next/font</code>。Codemod会自动处理迁移：</p><figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="comment">// 旧写法</span></span><br><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">Inter</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;@next/font/google&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 新写法</span></span><br><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">Inter</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;next/font/google&#x27;</span></span><br></pre></td></tr></table></figure><h3 id="runtime配置简化"><a href="#runtime配置简化" class="headerlink" title="runtime配置简化"></a>runtime配置简化</h3><p><code>experimental-edge</code>运行时已被废弃，统一使用<code>edge</code>：</p><figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="comment">// 旧写法（报错）</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> runtime = <span class="string">&#x27;experimental-edge&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 新写法</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> runtime = <span class="string">&#x27;edge&#x27;</span></span><br></pre></td></tr></table></figure><h3 id="配置项重命名"><a href="#配置项重命名" class="headerlink" title="配置项重命名"></a>配置项重命名</h3><p>两个实验性配置项已正式稳定：</p><figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="comment">// bundlePagesExternals → bundlePagesRouterDependencies</span></span><br><span class="line"><span class="keyword">const</span> nextConfig = &#123;</span><br><span class="line">  <span class="comment">// 旧写法</span></span><br><span class="line">  <span class="attr">experimental</span>: &#123;</span><br><span class="line">    <span class="attr">bundlePagesExternals</span>: <span class="literal">true</span>,</span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="comment">// 新写法</span></span><br><span class="line">  <span class="attr">bundlePagesRouterDependencies</span>: <span class="literal">true</span>,</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// serverComponentsExternalPackages → serverExternalPackages</span></span><br><span class="line"><span class="keyword">const</span> nextConfig = &#123;</span><br><span class="line">  <span class="comment">// 旧写法</span></span><br><span class="line">  <span class="attr">experimental</span>: &#123;</span><br><span class="line">    <span class="attr">serverComponentsExternalPackages</span>: [<span class="string">&#x27;package-name&#x27;</span>],</span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="comment">// 新写法</span></span><br><span class="line">  <span class="attr">serverExternalPackages</span>: [<span class="string">&#x27;package-name&#x27;</span>],</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="NextRequest地理位置移除"><a href="#NextRequest地理位置移除" class="headerlink" title="NextRequest地理位置移除"></a>NextRequest地理位置移除</h3><p><code>NextRequest</code>上的<code>geo</code>和<code>ip</code>属性已被移除，因为这些值应由托管平台提供。使用Vercel时，可以改用<code>@vercel/functions</code>包：</p><figure class="highlight ts"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; geolocation, ipAddress &#125; <span class="keyword">from</span> <span class="string">&#x27;@vercel/functions&#x27;</span></span><br><span class="line"><span class="keyword">import</span> <span class="keyword">type</span> &#123; <span class="title class_">NextRequest</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;next/server&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">function</span> <span class="title function_">middleware</span>(<span class="params"><span class="attr">request</span>: <span class="title class_">NextRequest</span></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> &#123; city &#125; = <span class="title function_">geolocation</span>(request)</span><br><span class="line">  <span class="keyword">const</span> ip = <span class="title function_">ipAddress</span>(request)</span><br><span class="line"></span><br><span class="line">  <span class="comment">// ...</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="Speed-Insights自动埋点移除"><a href="#Speed-Insights自动埋点移除" class="headerlink" title="Speed Insights自动埋点移除"></a>Speed Insights自动埋点移除</h3><p>Next.js 15移除了Speed Insights的自动埋点功能。继续使用需要遵循<a href="https://vercel.com/docs/speed-insights/quickstart">Vercel Speed Insights快速入门指南</a>进行配置。</p><h2 id="升级建议与最佳实践"><a href="#升级建议与最佳实践" class="headerlink" title="升级建议与最佳实践"></a>升级建议与最佳实践</h2><h3 id="渐进式迁移"><a href="#渐进式迁移" class="headerlink" title="渐进式迁移"></a>渐进式迁移</h3><p>由于异步API变化较大，建议采用以下策略：</p><ol><li><strong>先运行Codemod</strong>：官方提供的迁移工具可以自动处理大部分变更</li><li><strong>逐个文件修改</strong>：不要一次性修改所有文件，按需修改</li><li><strong>使用过渡方案</strong>：如<code>UnsafeUnwrappedCookies</code>类型可以在过渡期使用</li><li><strong>充分测试</strong>：特别是涉及cookies、headers的中间件和API路由</li></ol><h3 id="类型安全"><a href="#类型安全" class="headerlink" title="类型安全"></a>类型安全</h3><p>升级时特别注意TypeScript类型的变化：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 确保更新类型定义</span></span><br><span class="line">npm install @types/react@latest @types/react-dom@latest</span><br></pre></td></tr></table></figure><h3 id="运行时选择"><a href="#运行时选择" class="headerlink" title="运行时选择"></a>运行时选择</h3><p>如果你使用Edge Runtime，确保将<code>experimental-edge</code>改为<code>edge</code>，避免部署时报错。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>Next.js 15是一次重要的版本迭代，带来了以下核心变化：</p><ul><li><strong>React 19全面支持</strong>：可以使用所有React 19新特性</li><li><strong>异步API改造</strong>：cookies、headers、params等改为异步模式</li><li><strong>Fetch默认不缓存</strong>：需要显式指定缓存策略</li><li><strong>路由缓存调整</strong>：页面导航行为有所变化</li><li><strong>配置项正式化</strong>：多个实验性配置稳定可用</li></ul><p>虽然升级需要一定工作量，但这些变化都是为了更好地支持React 19的并发渲染能力，提升应用性能。建议尽快规划升级计划，享受新版本带来的改进。</p><p>如果在使用过程中遇到问题，可以查阅<a href="https://nextjs.org/docs/app/guides/upgrading/version-15">官方升级指南</a>或参与社区讨论。</p><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><ul><li><a href="https://nextjs.org/docs/app/guides/upgrading/version-15">Next.js 15 Official Upgrade Guide</a></li><li><a href="https://react.dev/blog/2024/04/25/react-19-upgrade-guide">React 19 Upgrade Guide</a></li><li><a href="https://vercel.com/docs/speed-insights/quickstart">Vercel Speed Insights</a></li></ul>]]>
    </content>
    <id>http://fe.poetries.top/2025/05/11/nextjs-15-performance-react-apps/</id>
    <link href="http://fe.poetries.top/2025/05/11/nextjs-15-performance-react-apps/"/>
    <published>2025-05-11T06:40:12.000Z</published>
    <summary>深入解析Next.js 15带来的重大更新，包括React 19支持、异步Request APIs、fetch默认不缓存等核心变化，提供详细的升级指南和代码示例。</summary>
    <title>Next.js 15新特性完全指南：升级须知与核心变化解析</title>
    <updated>2026-03-08T10:22:42.089Z</updated>
  </entry>
  <entry>
    <author>
      <name>Poetry</name>
    </author>
    <category term="Front-End" scheme="http://fe.poetries.top/categories/Front-End/"/>
    <category term="React" scheme="http://fe.poetries.top/tags/React/"/>
    <category term="前端开发" scheme="http://fe.poetries.top/tags/%E5%89%8D%E7%AB%AF%E5%BC%80%E5%8F%91/"/>
    <category term="React 19" scheme="http://fe.poetries.top/tags/React-19/"/>
    <category term="Hooks" scheme="http://fe.poetries.top/tags/Hooks/"/>
    <category term="TypeScript" scheme="http://fe.poetries.top/tags/TypeScript/"/>
    <content>
      <![CDATA[<p>React 19已正式发布，这是自React 18以来最重要的版本迭代。React 19引入了诸多革命性的新特性，包括Actions表单处理、全新的<code>use</code>API、Ref清理函数、Server Components支持等，同时清理了大量废弃API，让React更加精简高效。</p><p>本文将全面解析React 19的所有新特性和重大变化，并提供详细的代码示例，帮助你快速掌握这个最新版本。</p><h2 id="安装与环境准备"><a href="#安装与环境准备" class="headerlink" title="安装与环境准备"></a>安装与环境准备</h2><h3 id="安装React-19"><a href="#安装React-19" class="headerlink" title="安装React 19"></a>安装React 19</h3><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 使用npm安装</span></span><br><span class="line">npm install --save-exact react@^19.0.0 react-dom@^19.0.0</span><br><span class="line"></span><br><span class="line"><span class="comment"># 使用yarn安装</span></span><br><span class="line">yarn add --save-exact react@^19.0.0 react-dom@^19.0.0</span><br></pre></td></tr></table></figure><p>如果使用TypeScript，还需要更新类型定义：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">npm install --save-exact @types/react@^19.0.0 @types/react-dom@^19.0.0</span><br></pre></td></tr></table></figure><h3 id="新JSX-Transform"><a href="#新JSX-Transform" class="headerlink" title="新JSX Transform"></a>新JSX Transform</h3><p>React 19要求使用新的JSX Transform。如果未启用，会看到警告：</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">Your app (or one of its dependencies) is using an outdated JSX transform.</span><br><span class="line">Update to the modern JSX transform for faster performance.</span><br></pre></td></tr></table></figure><p>大多数项目不受影响，因为新Transform已在大多数环境中默认启用。</p><h2 id="一、Actions：表单处理的革命"><a href="#一、Actions：表单处理的革命" class="headerlink" title="一、Actions：表单处理的革命"></a>一、Actions：表单处理的革命</h2><p>Actions是React 19最核心的新特性，专门用于处理表单提交和数据变更。它能自动管理pending状态、错误处理和乐观更新。</p><h3 id="传统写法-vs-Actions写法"><a href="#传统写法-vs-Actions写法" class="headerlink" title="传统写法 vs Actions写法"></a>传统写法 vs Actions写法</h3><figure class="highlight jsx"><table><tr><td class="code"><pre><span class="line"><span class="comment">// 传统写法：手动管理所有状态</span></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">UpdateName</span>(<span class="params">&#123;&#125;</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> [name, setName] = <span class="title function_">useState</span>(<span class="string">&quot;&quot;</span>);</span><br><span class="line">  <span class="keyword">const</span> [error, setError] = <span class="title function_">useState</span>(<span class="literal">null</span>);</span><br><span class="line">  <span class="keyword">const</span> [isPending, setIsPending] = <span class="title function_">useState</span>(<span class="literal">false</span>);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> <span class="title function_">handleSubmit</span> = <span class="keyword">async</span> (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">    <span class="title function_">setIsPending</span>(<span class="literal">true</span>);</span><br><span class="line">    <span class="keyword">const</span> error = <span class="keyword">await</span> <span class="title function_">updateName</span>(name);</span><br><span class="line">    <span class="title function_">setIsPending</span>(<span class="literal">false</span>);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (error) &#123;</span><br><span class="line">      <span class="title function_">setError</span>(error);</span><br><span class="line">      <span class="keyword">return</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="title function_">redirect</span>(<span class="string">&quot;/path&quot;</span>);</span><br><span class="line">  &#125;;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">input</span> <span class="attr">value</span>=<span class="string">&#123;name&#125;</span> <span class="attr">onChange</span>=<span class="string">&#123;(e)</span> =&gt;</span> setName(e.target.value)&#125; /&gt;</span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">button</span> <span class="attr">onClick</span>=<span class="string">&#123;handleSubmit&#125;</span> <span class="attr">disabled</span>=<span class="string">&#123;isPending&#125;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">        Update</span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;/<span class="name">button</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      &#123;error &amp;&amp; <span class="tag">&lt;<span class="name">p</span>&gt;</span>&#123;error&#125;<span class="tag">&lt;/<span class="name">p</span>&gt;</span>&#125;</span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line">  );</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// React 19 Actions写法：简洁优雅</span></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">UpdateName</span>(<span class="params">&#123;&#125;</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> [name, setName] = <span class="title function_">useState</span>(<span class="string">&quot;&quot;</span>);</span><br><span class="line">  <span class="keyword">const</span> [isPending, startTransition] = <span class="title function_">useTransition</span>();</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> <span class="title function_">handleSubmit</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">    <span class="title function_">startTransition</span>(<span class="title function_">async</span> () =&gt; &#123;</span><br><span class="line">      <span class="keyword">const</span> error = <span class="keyword">await</span> <span class="title function_">updateName</span>(name);</span><br><span class="line">      <span class="keyword">if</span> (error) &#123;</span><br><span class="line">        <span class="title function_">setError</span>(error);</span><br><span class="line">        <span class="keyword">return</span>;</span><br><span class="line">      &#125;</span><br><span class="line">      <span class="title function_">redirect</span>(<span class="string">&quot;/path&quot;</span>);</span><br><span class="line">    &#125;);</span><br><span class="line">  &#125;;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">input</span> <span class="attr">value</span>=<span class="string">&#123;name&#125;</span> <span class="attr">onChange</span>=<span class="string">&#123;(e)</span> =&gt;</span> setName(e.target.value)&#125; /&gt;</span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">button</span> <span class="attr">onClick</span>=<span class="string">&#123;handleSubmit&#125;</span> <span class="attr">disabled</span>=<span class="string">&#123;isPending&#125;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">        Update</span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;/<span class="name">button</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line">  );</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>Actions会自动管理：</p><ul><li><strong>Pending状态</strong>：请求开始时自动设为true，完成后自动重置</li><li><strong>乐观更新</strong>：配合<code>useOptimistic</code>显示即时反馈</li><li><strong>错误处理</strong>：自动显示Error Boundary并回滚乐观更新</li><li><strong>表单重置</strong>：成功提交后自动重置表单</li></ul><h2 id="二、useActionState：简化Actions"><a href="#二、useActionState：简化Actions" class="headerlink" title="二、useActionState：简化Actions"></a>二、useActionState：简化Actions</h2><p><code>useActionState</code>是专门为Actions设计的Hook，简化了常见的数据提交场景：</p><figure class="highlight jsx"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; useActionState &#125; <span class="keyword">from</span> <span class="string">&#x27;react&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">ChangeName</span>(<span class="params">&#123; name, setName &#125;</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> [error, submitAction, isPending] = <span class="title function_">useActionState</span>(</span><br><span class="line">    <span class="title function_">async</span> (previousState, formData) =&gt; &#123;</span><br><span class="line">      <span class="keyword">const</span> error = <span class="keyword">await</span> <span class="title function_">updateName</span>(formData.<span class="title function_">get</span>(<span class="string">&quot;name&quot;</span>));</span><br><span class="line"></span><br><span class="line">      <span class="keyword">if</span> (error) &#123;</span><br><span class="line">        <span class="keyword">return</span> error; <span class="comment">// 返回错误信息</span></span><br><span class="line">      &#125;</span><br><span class="line"></span><br><span class="line">      <span class="title function_">redirect</span>(<span class="string">&quot;/path&quot;</span>);</span><br><span class="line">      <span class="keyword">return</span> <span class="literal">null</span>; <span class="comment">// 成功返回null</span></span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="literal">null</span> <span class="comment">// 初始状态</span></span><br><span class="line">  );</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">form</span> <span class="attr">action</span>=<span class="string">&#123;submitAction&#125;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">input</span> <span class="attr">type</span>=<span class="string">&quot;text&quot;</span> <span class="attr">name</span>=<span class="string">&quot;name&quot;</span> /&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">button</span> <span class="attr">type</span>=<span class="string">&quot;submit&quot;</span> <span class="attr">disabled</span>=<span class="string">&#123;isPending&#125;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">        &#123;isPending ? &#x27;提交中...&#x27; : &#x27;更新&#x27;&#125;</span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;/<span class="name">button</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      &#123;error &amp;&amp; <span class="tag">&lt;<span class="name">p</span> <span class="attr">style</span>=<span class="string">&#123;&#123;</span> <span class="attr">color:</span> &#x27;<span class="attr">red</span>&#x27; &#125;&#125;&gt;</span>&#123;error&#125;<span class="tag">&lt;/<span class="name">p</span>&gt;</span>&#125;</span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">form</span>&gt;</span></span></span><br><span class="line">  );</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>返回值的结构：</p><ul><li><code>error</code>：Action的返回结果（成功为null，失败为错误信息）</li><li><code>submitAction</code>：包装后的Action函数</li><li><code>isPending</code>：是否处于提交中状态</li></ul><h2 id="三、useOptimistic：乐观更新"><a href="#三、useOptimistic：乐观更新" class="headerlink" title="三、useOptimistic：乐观更新"></a>三、useOptimistic：乐观更新</h2><p>乐观更新让用户看到即时的UI反馈，无需等待服务器响应：</p><figure class="highlight jsx"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; useOptimistic &#125; <span class="keyword">from</span> <span class="string">&#x27;react&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">ChangeName</span>(<span class="params">&#123; currentName, onUpdateName &#125;</span>) &#123;</span><br><span class="line">  <span class="comment">// 立即显示新名字，同时发起请求</span></span><br><span class="line">  <span class="keyword">const</span> [optimisticName, setOptimisticName] = <span class="title function_">useOptimistic</span>(currentName);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> <span class="title function_">submitAction</span> = <span class="keyword">async</span> (<span class="params">formData</span>) =&gt; &#123;</span><br><span class="line">    <span class="keyword">const</span> newName = formData.<span class="title function_">get</span>(<span class="string">&quot;name&quot;</span>);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 立即显示乐观更新</span></span><br><span class="line">    <span class="title function_">setOptimisticName</span>(newName);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 等待服务器响应</span></span><br><span class="line">    <span class="keyword">const</span> updatedName = <span class="keyword">await</span> <span class="title function_">updateName</span>(newName);</span><br><span class="line">    <span class="title function_">onUpdateName</span>(updatedName);</span><br><span class="line">  &#125;;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">form</span> <span class="attr">action</span>=<span class="string">&#123;submitAction&#125;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">p</span>&gt;</span>当前名字: &#123;optimisticName&#125;<span class="tag">&lt;/<span class="name">p</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">input</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">        <span class="attr">type</span>=<span class="string">&quot;text&quot;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">        <span class="attr">name</span>=<span class="string">&quot;name&quot;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">        <span class="attr">disabled</span>=<span class="string">&#123;currentName</span> !== <span class="string">optimisticName&#125;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">      /&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">button</span> <span class="attr">type</span>=<span class="string">&quot;submit&quot;</span>&gt;</span>修改<span class="tag">&lt;/<span class="name">button</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">form</span>&gt;</span></span></span><br><span class="line">  );</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>当请求进行中时显示<code>optimisticName</code>，请求完成后自动切换回<code>currentName</code>。</p><h2 id="四、useFormStatus：表单状态共享"><a href="#四、useFormStatus：表单状态共享" class="headerlink" title="四、useFormStatus：表单状态共享"></a>四、useFormStatus：表单状态共享</h2><p>在设计系统中，子组件常常需要知道父表单的状态，<code>useFormStatus</code>让这变得简单：</p><figure class="highlight jsx"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; useFormStatus &#125; <span class="keyword">from</span> <span class="string">&#x27;react-dom&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">SubmitButton</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> &#123; pending, data, method, action &#125; = <span class="title function_">useFormStatus</span>();</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">button</span> <span class="attr">type</span>=<span class="string">&quot;submit&quot;</span> <span class="attr">disabled</span>=<span class="string">&#123;pending&#125;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      &#123;pending ? &#x27;提交中...&#x27; : &#x27;提交&#x27;&#125;</span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">button</span>&gt;</span></span></span><br><span class="line">  );</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">MyForm</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">form</span> <span class="attr">action</span>=<span class="string">&#123;async</span> () =&gt;</span> &#123; /* ... */ &#125;&#125;&gt;</span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">input</span> <span class="attr">type</span>=<span class="string">&quot;text&quot;</span> <span class="attr">name</span>=<span class="string">&quot;username&quot;</span> /&gt;</span></span></span><br><span class="line"><span class="language-xml">      &#123;/* 子组件可以获取表单状态，无需props传递 */&#125;</span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">SubmitButton</span> /&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">form</span>&gt;</span></span></span><br><span class="line">  );</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="五、全新use-API：渲染中读取资源"><a href="#五、全新use-API：渲染中读取资源" class="headerlink" title="五、全新use API：渲染中读取资源"></a>五、全新use API：渲染中读取资源</h2><h3 id="use-Promise"><a href="#use-Promise" class="headerlink" title="use Promise"></a>use Promise</h3><p><code>use</code>是React 19引入的全新API，可以在渲染阶段读取Promise和Context：</p><figure class="highlight jsx"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; use, <span class="title class_">Suspense</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;react&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">Comments</span>(<span class="params">&#123; commentsPromise &#125;</span>) &#123;</span><br><span class="line">  <span class="comment">// use会Suspend直到Promise resolved</span></span><br><span class="line">  <span class="keyword">const</span> comments = <span class="title function_">use</span>(commentsPromise);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> comments.<span class="title function_">map</span>(<span class="function"><span class="params">comment</span> =&gt;</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">p</span> <span class="attr">key</span>=<span class="string">&#123;comment.id&#125;</span>&gt;</span>&#123;comment.text&#125;<span class="tag">&lt;/<span class="name">p</span>&gt;</span></span></span><br><span class="line">  ));</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">Page</span>(<span class="params">&#123; commentsPromise &#125;</span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">Suspense</span> <span class="attr">fallback</span>=<span class="string">&#123;</span>&lt;<span class="attr">div</span>&gt;</span>加载中...<span class="tag">&lt;/<span class="name">div</span>&gt;</span>&#125;&gt;</span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">Comments</span> <span class="attr">commentsPromise</span>=<span class="string">&#123;commentsPromise&#125;</span> /&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">Suspense</span>&gt;</span></span></span><br><span class="line">  );</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="use-Context"><a href="#use-Context" class="headerlink" title="use Context"></a>use Context</h3><p><code>use</code>还可以读取Context，且支持在条件语句后使用（这是useContext做不到的）：</p><figure class="highlight jsx"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; use &#125; <span class="keyword">from</span> <span class="string">&#x27;react&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> <span class="title class_">ThemeContext</span> <span class="keyword">from</span> <span class="string">&#x27;./ThemeContext&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">Heading</span>(<span class="params">&#123; children &#125;</span>) &#123;</span><br><span class="line">  <span class="keyword">if</span> (children == <span class="literal">null</span>) &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">null</span>;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// useContext无法在early return后使用，use可以</span></span><br><span class="line">  <span class="keyword">const</span> theme = <span class="title function_">use</span>(<span class="title class_">ThemeContext</span>);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> <span class="language-xml"><span class="tag">&lt;<span class="name">h1</span> <span class="attr">style</span>=<span class="string">&#123;&#123;</span> <span class="attr">color:</span> <span class="attr">theme.color</span> &#125;&#125;&gt;</span>&#123;children&#125;<span class="tag">&lt;/<span class="name">h1</span>&gt;</span></span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="六、Ref清理函数"><a href="#六、Ref清理函数" class="headerlink" title="六、Ref清理函数"></a>六、Ref清理函数</h2><p>React 19支持从ref回调返回清理函数，这是社区呼吁已久的特性：</p><figure class="highlight jsx"><table><tr><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">MyInput</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> inputRef = <span class="title function_">useRef</span>(<span class="literal">null</span>);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">input</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">      <span class="attr">ref</span>=<span class="string">&#123;(ref)</span> =&gt;</span> &#123;</span></span><br><span class="line"><span class="language-xml">        // ref创建</span></span><br><span class="line"><span class="language-xml">        inputRef.current = ref;</span></span><br><span class="line"><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">        // 返回清理函数</span></span><br><span class="line"><span class="language-xml">        return () =&gt; &#123;</span></span><br><span class="line"><span class="language-xml">          // ref清理 - 元素从DOM移除时调用</span></span><br><span class="line"><span class="language-xml">          inputRef.current = null;</span></span><br><span class="line"><span class="language-xml">          console.log(&#x27;Input ref cleaned up&#x27;);</span></span><br><span class="line"><span class="language-xml">        &#125;;</span></span><br><span class="line"><span class="language-xml">      &#125;&#125;</span></span><br><span class="line"><span class="language-xml">    /&gt;</span></span><br><span class="line">  );</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>对比之前必须用<code>null</code>判断：</p><figure class="highlight jsx"><table><tr><td class="code"><pre><span class="line"><span class="comment">// React 18及之前的写法</span></span><br><span class="line">&lt;input</span><br><span class="line">  ref=&#123;<span class="function">(<span class="params">ref</span>) =&gt;</span> &#123;</span><br><span class="line">    inputRef.<span class="property">current</span> = ref;</span><br><span class="line">    <span class="keyword">return</span> <span class="function">() =&gt;</span> &#123;</span><br><span class="line">      <span class="comment">// 清理逻辑</span></span><br><span class="line">    &#125;;</span><br><span class="line">  &#125;&#125;</span><br><span class="line">/&gt;</span><br><span class="line"><span class="comment">// 或者</span></span><br><span class="line"><span class="language-xml"><span class="tag">&lt;<span class="name">input</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">  <span class="attr">ref</span>=<span class="string">&#123;(ref)</span> =&gt;</span> &#123;</span></span><br><span class="line"><span class="language-xml">    inputRef.current = ref;</span></span><br><span class="line"><span class="language-xml">  &#125;&#125;</span></span><br><span class="line"><span class="language-xml">/&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 组件卸载时React会调用ref(null)</span></span><br></pre></td></tr></table></figure><h2 id="七、ref作为普通prop"><a href="#七、ref作为普通prop" class="headerlink" title="七、ref作为普通prop"></a>七、ref作为普通prop</h2><p>之前函数组件使用ref必须用<code>forwardRef</code>，React 19可以直接把ref当作prop传递：</p><figure class="highlight jsx"><table><tr><td class="code"><pre><span class="line"><span class="comment">// React 19新写法：无需forwardRef</span></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">MyInput</span>(<span class="params">&#123; placeholder, ref &#125;</span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="language-xml"><span class="tag">&lt;<span class="name">input</span> <span class="attr">placeholder</span>=<span class="string">&#123;placeholder&#125;</span> <span class="attr">ref</span>=<span class="string">&#123;ref&#125;</span> /&gt;</span></span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 使用</span></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">App</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> inputRef = <span class="title function_">useRef</span>(<span class="literal">null</span>);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> <span class="language-xml"><span class="tag">&lt;<span class="name">MyInput</span> <span class="attr">ref</span>=<span class="string">&#123;inputRef&#125;</span> <span class="attr">placeholder</span>=<span class="string">&quot;请输入&quot;</span> /&gt;</span></span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="八、Context作为Provider"><a href="#八、Context作为Provider" class="headerlink" title="八、Context作为Provider"></a>八、Context作为Provider</h2><p>React 19简化了Context Provider的写法：</p><figure class="highlight jsx"><table><tr><td class="code"><pre><span class="line"><span class="comment">// 之前的写法</span></span><br><span class="line">&lt;<span class="title class_">ThemeContext</span>.<span class="property">Provider</span> value=<span class="string">&quot;dark&quot;</span>&gt;</span><br><span class="line">  <span class="language-xml"><span class="tag">&lt;<span class="name">App</span> /&gt;</span></span></span><br><span class="line">&lt;/<span class="title class_">ThemeContext</span>.<span class="property">Provider</span>&gt;</span><br><span class="line"></span><br><span class="line"><span class="comment">// React 19新写法：直接渲染Context</span></span><br><span class="line"><span class="language-xml"><span class="tag">&lt;<span class="name">ThemeContext</span> <span class="attr">value</span>=<span class="string">&quot;dark&quot;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">  <span class="tag">&lt;<span class="name">App</span> /&gt;</span></span></span><br><span class="line"><span class="language-xml"><span class="tag">&lt;/<span class="name">ThemeContext</span>&gt;</span></span></span><br></pre></td></tr></table></figure><h2 id="九、文档元数据支持"><a href="#九、文档元数据支持" class="headerlink" title="九、文档元数据支持"></a>九、文档元数据支持</h2><p>React 19原生支持在组件中渲染<code>&lt;title&gt;</code>、<code>&lt;link&gt;</code>、<code>&lt;meta&gt;</code>等文档元数据：</p><figure class="highlight jsx"><table><tr><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">BlogPost</span>(<span class="params">&#123; post &#125;</span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">article</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">h1</span>&gt;</span>&#123;post.title&#125;<span class="tag">&lt;/<span class="name">h1</span>&gt;</span></span></span><br><span class="line"><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">      &#123;/* 自动提升到<span class="tag">&lt;<span class="name">head</span>&gt;</span> */&#125;</span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">title</span>&gt;</span>&#123;post.title&#125;<span class="tag">&lt;/<span class="name">title</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">meta</span> <span class="attr">name</span>=<span class="string">&quot;author&quot;</span> <span class="attr">content</span>=<span class="string">&quot;作者名&quot;</span> /&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">link</span> <span class="attr">rel</span>=<span class="string">&quot;canonical&quot;</span> <span class="attr">href</span>=<span class="string">&#123;post.url&#125;</span> /&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">meta</span> <span class="attr">name</span>=<span class="string">&quot;keywords&quot;</span> <span class="attr">content</span>=<span class="string">&#123;post.keywords&#125;</span> /&gt;</span></span></span><br><span class="line"><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">div</span>&gt;</span>&#123;post.content&#125;<span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">article</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">  );</span></span><br><span class="line"><span class="language-xml">&#125;</span></span><br></pre></td></tr></table></figure><p>React会自动将这些标签提升到文档的<code>&lt;head&gt;</code>部分。</p><h2 id="十、样式表支持"><a href="#十、样式表支持" class="headerlink" title="十、样式表支持"></a>十、样式表支持</h2><p>React 19原生支持样式表，并处理加载顺序：</p><figure class="highlight jsx"><table><tr><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">ComponentOne</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">Suspense</span> <span class="attr">fallback</span>=<span class="string">&quot;加载中...&quot;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      &#123;/* 使用precedence控制优先级 */&#125;</span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">link</span> <span class="attr">rel</span>=<span class="string">&quot;stylesheet&quot;</span> <span class="attr">href</span>=<span class="string">&quot;foo.css&quot;</span> <span class="attr">precedence</span>=<span class="string">&quot;default&quot;</span> /&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">link</span> <span class="attr">rel</span>=<span class="string">&quot;stylesheet&quot;</span> <span class="attr">href</span>=<span class="string">&quot;bar.css&quot;</span> <span class="attr">precedence</span>=<span class="string">&quot;high&quot;</span> /&gt;</span></span></span><br><span class="line"><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">div</span> <span class="attr">className</span>=<span class="string">&quot;foo-class bar-class&quot;</span>&gt;</span>内容<span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">Suspense</span>&gt;</span></span></span><br><span class="line">  );</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">ComponentTwo</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">link</span> <span class="attr">rel</span>=<span class="string">&quot;stylesheet&quot;</span> <span class="attr">href</span>=<span class="string">&quot;baz.css&quot;</span> <span class="attr">precedence</span>=<span class="string">&quot;default&quot;</span> /&gt;</span></span></span><br><span class="line"><span class="language-xml">      &#123;/* 插入到foo和bar之间 */&#125;</span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line">  );</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="十一、资源预加载API"><a href="#十一、资源预加载API" class="headerlink" title="十一、资源预加载API"></a>十一、资源预加载API</h2><p>React 19提供了优化资源加载的新API：</p><figure class="highlight jsx"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; prefetchDNS, preconnect, preload, preinit &#125; <span class="keyword">from</span> <span class="string">&#x27;react-dom&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">MyComponent</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      &#123;/* 预连接DNS - 当确定需要连接某域名但不确定具体资源时 */&#125;</span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">link</span> <span class="attr">rel</span>=<span class="string">&quot;preconnect&quot;</span> <span class="attr">href</span>=<span class="string">&quot;https://fonts.googleapis.com&quot;</span> /&gt;</span></span></span><br><span class="line"><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">      &#123;/* 预获取DNS - 当可能需要某域名资源时 */&#125;</span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">link</span> <span class="attr">rel</span>=<span class="string">&quot;prefetch-dns&quot;</span> <span class="attr">href</span>=<span class="string">&quot;https://analytics.com&quot;</span> /&gt;</span></span></span><br><span class="line"><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">      &#123;/* 预加载字体 */&#125;</span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">link</span> <span class="attr">rel</span>=<span class="string">&quot;preload&quot;</span> <span class="attr">href</span>=<span class="string">&quot;/fonts.woff&quot;</span> <span class="attr">as</span>=<span class="string">&quot;font&quot;</span> /&gt;</span></span></span><br><span class="line"><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">      &#123;/* 预加载样式 */&#125;</span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">link</span> <span class="attr">rel</span>=<span class="string">&quot;preload&quot;</span> <span class="attr">href</span>=<span class="string">&quot;/styles.css&quot;</span> <span class="attr">as</span>=<span class="string">&quot;style&quot;</span> /&gt;</span></span></span><br><span class="line"><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">      &#123;/* 预初始化脚本 - 立即加载并执行 */&#125;</span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">script</span> <span class="attr">async</span> <span class="attr">src</span>=<span class="string">&quot;/analytics.js&quot;</span> /&gt;</span><span class="language-handlebars"><span class="language-xml"></span></span></span></span><br><span class="line"><span class="language-xml"><span class="language-handlebars"><span class="language-xml">    <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span></span></span><br><span class="line">  );</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="十二、错误处理改进"><a href="#十二、错误处理改进" class="headerlink" title="十二、错误处理改进"></a>十二、错误处理改进</h2><p>React 19改进了错误处理，减少了重复日志：</p><figure class="highlight jsx"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; createRoot &#125; <span class="keyword">from</span> <span class="string">&#x27;react-dom/client&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> root = <span class="title function_">createRoot</span>(container, &#123;</span><br><span class="line">  <span class="comment">// 捕获到的错误（Error Boundary处理）</span></span><br><span class="line">  <span class="attr">onCaughtError</span>: <span class="function">(<span class="params">error, errorInfo</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">error</span>(<span class="string">&#x27;捕获的错误:&#x27;</span>, error, errorInfo);</span><br><span class="line">    <span class="comment">// 上报错误监控系统</span></span><br><span class="line">  &#125;,</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 未捕获的错误</span></span><br><span class="line">  <span class="attr">onUncaughtError</span>: <span class="function">(<span class="params">error, errorInfo</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">error</span>(<span class="string">&#x27;未捕获的错误:&#x27;</span>, error);</span><br><span class="line">  &#125;,</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 可恢复的错误</span></span><br><span class="line">  <span class="attr">onRecoverableError</span>: <span class="function">(<span class="params">error, errorInfo</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">warn</span>(<span class="string">&#x27;可恢复的错误:&#x27;</span>, error);</span><br><span class="line">  &#125;,</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><h2 id="十三、hydration改进"><a href="#十三、hydration改进" class="headerlink" title="十三、hydration改进"></a>十三、hydration改进</h2><p>React 19改进了hydration错误提示，现在会显示具体的差异：</p><figure class="highlight jsx"><table><tr><td class="code"><pre><span class="line"><span class="comment">// 之前：只显示模糊的错误信息</span></span><br><span class="line"><span class="comment">// &quot;Warning: Text content did not match...&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 现在：显示具体差异</span></span><br><span class="line"><span class="comment">// &quot;Hydration failed because the server rendered HTML didn&#x27;t match the client.</span></span><br><span class="line"><span class="comment">// &lt;App&gt;</span></span><br><span class="line"><span class="comment">//   &lt;span&gt;</span></span><br><span class="line"><span class="comment">//     + Client</span></span><br><span class="line"><span class="comment">//     - Server</span></span><br><span class="line"><span class="comment">//   &lt;/span&gt;</span></span><br><span class="line"><span class="comment">// &lt;/App&gt;&quot;</span></span><br></pre></td></tr></table></figure><h2 id="十四、Server-Components与Server-Actions"><a href="#十四、Server-Components与Server-Actions" class="headerlink" title="十四、Server Components与Server Actions"></a>十四、Server Components与Server Actions</h2><h3 id="Server-Components"><a href="#Server-Components" class="headerlink" title="Server Components"></a>Server Components</h3><p>Server Components允许组件在服务器端运行，减少客户端JavaScript体积：</p><figure class="highlight jsx"><table><tr><td class="code"><pre><span class="line"><span class="comment">// 这是一个Server Component (默认)</span></span><br><span class="line"><span class="keyword">async</span> <span class="keyword">function</span> <span class="title function_">BlogPost</span>(<span class="params">&#123; id &#125;</span>) &#123;</span><br><span class="line">  <span class="comment">// 直接在服务器端读取数据库</span></span><br><span class="line">  <span class="keyword">const</span> post = <span class="keyword">await</span> db.<span class="property">posts</span>.<span class="title function_">get</span>(id);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">article</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">h1</span>&gt;</span>&#123;post.title&#125;<span class="tag">&lt;/<span class="name">h1</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">p</span>&gt;</span>&#123;post.content&#125;<span class="tag">&lt;/<span class="name">p</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">article</span>&gt;</span></span></span><br><span class="line">  );</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="Server-Actions"><a href="#Server-Actions" class="headerlink" title="Server Actions"></a>Server Actions</h3><p>Server Actions允许客户端调用服务器端函数：</p><figure class="highlight jsx"><table><tr><td class="code"><pre><span class="line"><span class="comment">// server/actions.js</span></span><br><span class="line"><span class="string">&#x27;use server&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">async</span> <span class="keyword">function</span> <span class="title function_">updateUser</span>(<span class="params">formData</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> name = formData.<span class="title function_">get</span>(<span class="string">&#x27;name&#x27;</span>);</span><br><span class="line">  <span class="keyword">await</span> db.<span class="property">users</span>.<span class="title function_">update</span>(&#123; name &#125;);</span><br><span class="line">  <span class="title function_">revalidatePath</span>(<span class="string">&#x27;/profile&#x27;</span>);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// client/Component.jsx</span></span><br><span class="line"><span class="string">&#x27;use client&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> &#123; updateUser &#125; <span class="keyword">from</span> <span class="string">&#x27;./server/actions&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">ProfileForm</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">form</span> <span class="attr">action</span>=<span class="string">&#123;updateUser&#125;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">input</span> <span class="attr">type</span>=<span class="string">&quot;text&quot;</span> <span class="attr">name</span>=<span class="string">&quot;name&quot;</span> /&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">button</span> <span class="attr">type</span>=<span class="string">&quot;submit&quot;</span>&gt;</span>更新<span class="tag">&lt;/<span class="name">button</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">form</span>&gt;</span></span></span><br><span class="line">  );</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="十五、重大变更与废弃API"><a href="#十五、重大变更与废弃API" class="headerlink" title="十五、重大变更与废弃API"></a>十五、重大变更与废弃API</h2><h3 id="已移除的API"><a href="#已移除的API" class="headerlink" title="已移除的API"></a>已移除的API</h3><table><thead><tr><th>API</th><th>替代方案</th></tr></thead><tbody><tr><td><code>propTypes</code></td><td>使用TypeScript或PropTypes库</td></tr><tr><td><code>defaultProps</code>(函数组件)</td><td>使用ES6默认参数</td></tr><tr><td>字符串ref</td><td>使用ref回调</td></tr><tr><td><code>React.createFactory</code></td><td>使用JSX</td></tr><tr><td><code>ReactDOM.render</code></td><td>使用<code>createRoot</code></td></tr><tr><td><code>ReactDOM.hydrate</code></td><td>使用<code>hydrateRoot</code></td></tr><tr><td><code>unmountComponentAtNode</code></td><td>使用<code>root.unmount()</code></td></tr><tr><td><code>findDOMNode</code></td><td>使用DOM ref</td></tr></tbody></table><h3 id="TypeScript变更"><a href="#TypeScript变更" class="headerlink" title="TypeScript变更"></a>TypeScript变更</h3><figure class="highlight tsx"><table><tr><td class="code"><pre><span class="line"><span class="comment">// useRef现在必须传入初始值</span></span><br><span class="line"><span class="keyword">const</span> ref = <span class="title function_">useRef</span>(<span class="literal">null</span>); <span class="comment">// 必须传参</span></span><br><span class="line"><span class="comment">// 或者</span></span><br><span class="line"><span class="keyword">const</span> ref = useRef&lt;<span class="title class_">HTMLInputElement</span>&gt;(<span class="literal">null</span>);</span><br><span class="line"></span><br><span class="line"><span class="comment">// ref回调的隐式返回被禁止</span></span><br><span class="line"><span class="comment">// 错误写法</span></span><br><span class="line"><span class="language-xml"><span class="tag">&lt;<span class="name">div</span> <span class="attr">ref</span>=<span class="string">&#123;current</span> =&gt;</span> instance = current&#125; /&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 正确写法</span></span><br><span class="line"><span class="language-xml"><span class="tag">&lt;<span class="name">div</span> <span class="attr">ref</span>=<span class="string">&#123;current</span> =&gt;</span> &#123; instance = current; &#125;&#125; /&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// useReducer类型推断改进</span></span><br><span class="line"><span class="comment">// 推荐不指定类型参数</span></span><br><span class="line"><span class="keyword">const</span> [state, dispatch] = <span class="title function_">useReducer</span>(reducer, initialState);</span><br><span class="line"></span><br><span class="line"><span class="comment">// JSX命名空间变更</span></span><br><span class="line"><span class="comment">// 需要在模块声明中扩展</span></span><br><span class="line"><span class="keyword">declare</span> <span class="variable language_">module</span> <span class="string">&quot;react&quot;</span> &#123;</span><br><span class="line">  <span class="keyword">namespace</span> <span class="title class_">JSX</span> &#123;</span><br><span class="line">    <span class="keyword">interface</span> <span class="title class_">IntrinsicElements</span> &#123;</span><br><span class="line">      <span class="string">&quot;my-element&quot;</span>: &#123; <span class="attr">myProp</span>: <span class="built_in">string</span> &#125;;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="升级建议"><a href="#升级建议" class="headerlink" title="升级建议"></a>升级建议</h2><h3 id="推荐升级步骤"><a href="#推荐升级步骤" class="headerlink" title="推荐升级步骤"></a>推荐升级步骤</h3><ol><li><p><strong>先升级到React 18.3</strong>：它包含React 19所需的所有警告</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">npm install react@18.3 react-dom@18.3</span><br></pre></td></tr></table></figure></li><li><p><strong>修复所有警告</strong>：确保应用在React 18.3下无警告运行</p></li><li><p><strong>运行Codemod</strong>：自动迁移大部分变更</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">npx codemod@latest react/19/migration-recipe</span><br></pre></td></tr></table></figure></li><li><p><strong>升级到React 19</strong></p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">npm install react@latest react-dom@latest</span><br></pre></td></tr></table></figure></li><li><p><strong>处理TypeScript类型</strong></p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">npx types-react-codemod@latest preset-19 ./path-to-app</span><br></pre></td></tr></table></figure></li></ol><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>React 19是一次重大版本更新，带来的核心变化包括：</p><ul><li><strong>Actions</strong>：革命性的表单处理方案</li><li><strong>useActionState</strong>：简化Actions使用</li><li><strong>useOptimistic</strong>：优雅的乐观更新</li><li><strong>use API</strong>：渲染中读取Promise&#x2F;Context</li><li><strong>Ref清理函数</strong>：更优雅的ref管理</li><li><strong>文档元数据</strong>：原生支持SEO标签</li><li><strong>样式表支持</strong>：更好的CSS管理</li><li><strong>资源预加载</strong>：性能优化利器</li><li><strong>错误处理改进</strong>：更清晰的错误信息</li><li><strong>Server Components</strong>：全栈React架构</li></ul><p>建议尽快规划升级计划，体验React 19带来的开发体验提升。</p><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><ul><li><a href="https://react.dev/blog/2024/04/25/react-19-upgrade-guide">React 19官方升级指南</a></li><li><a href="https://react.dev/blog/2024/12/05/react-19">React 19发布公告</a></li><li><a href="https://react.dev/reference/react/useActionState">useActionState文档</a></li><li><a href="https://react.dev/reference/react/useOptimistic">useOptimistic文档</a></li><li><a href="https://react.dev/reference/react/use">use API文档</a></li></ul>]]>
    </content>
    <id>http://fe.poetries.top/2025/02/11/react-19-new-features/</id>
    <link href="http://fe.poetries.top/2025/02/11/react-19-new-features/"/>
    <published>2025-02-11T02:00:00.000Z</published>
    <summary>深入解析React 19带来的所有新特性，包括Actions、useActionState、useOptimistic、use新API、Ref清理函数等，配以完整代码示例，帮你快速掌握React最新版本。</summary>
    <title>React 19完全指南：新特性、重大变化与升级攻略</title>
    <updated>2026-03-08T10:22:42.093Z</updated>
  </entry>
  <entry>
    <author>
      <name>Poetry</name>
    </author>
    <category term="Front-End" scheme="http://fe.poetries.top/categories/Front-End/"/>
    <category term="React" scheme="http://fe.poetries.top/tags/React/"/>
    <category term="状态管理" scheme="http://fe.poetries.top/tags/%E7%8A%B6%E6%80%81%E7%AE%A1%E7%90%86/"/>
    <category term="Zustand" scheme="http://fe.poetries.top/tags/Zustand/"/>
    <category term="前端性能优化" scheme="http://fe.poetries.top/tags/%E5%89%8D%E7%AB%AF%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/"/>
    <content>
      <![CDATA[<p>在React项目中，你是否受够了Redux的繁琐配置？是否厌倦了Context带来的不必要的渲染？Zustand作为一个轻量级的状态管理库，仅用一個create()函数就能搞定状态管理，无需Provider嵌套，无需编写冗余的Action和Reducer。</p><p>本文将从状态管理方案选型讲起，深入讲解Zustand的核心API、精准订阅机制、持久化与中间件扩展等实战技巧，帮助你在项目中快速落地。无论你是想简化现有状态管理架构，还是寻找更高效的解决方案，这篇文章都能给你答案。</p><h2 id="为什么选择Zustand"><a href="#为什么选择Zustand" class="headerlink" title="为什么选择Zustand"></a>为什么选择Zustand</h2><h3 id="现状痛点"><a href="#现状痛点" class="headerlink" title="现状痛点"></a>现状痛点</h3><p>在React项目中管理状态一直是开发者面临的难题。传统的Context API虽然使用简单，但存在严重的性能问题——当Provider的value变化时，所有消费该Context的组件都会无条件重新渲染。Redux虽然功能强大，但配置繁琐，样板代码过多，学习曲线陡峭。对于中小型项目来说，这些方案显得过于笨重。</p><p>Zustand正是为解决这些痛点而生的。它由React Three Fiber团队开发，设计理念是”用最小的API，实现最大的效率”。你不需要Provider，不需要定义Action和Reducer，只需要一个create()函数就能搞定状态管理。</p><h3 id="Zustand核心优势"><a href="#Zustand核心优势" class="headerlink" title="Zustand核心优势"></a>Zustand核心优势</h3><ul><li><strong>极简API</strong>：一行代码创建store，没有任何繁琐配置</li><li><strong>精准订阅</strong>：基于Selector的订阅机制，只订阅需要的状态片段，避免不必要的渲染</li><li><strong>无需Provider</strong>：不依赖Context，避免组件树嵌套地狱</li><li><strong>TypeScript友好</strong>：完美支持类型推导，IDE提示完善</li><li><strong>中间件扩展</strong>：内置persist、devtools、immer等常用中间件</li><li><strong>性能优异</strong>：初始化速度极快，内存占用极低</li></ul><h3 id="主流状态管理方案对比"><a href="#主流状态管理方案对比" class="headerlink" title="主流状态管理方案对比"></a>主流状态管理方案对比</h3><table><thead><tr><th align="left">单向数据流</th><th align="left">原子化</th><th align="left">Proxy</th></tr></thead><tbody><tr><td align="left">redux</td><td align="left">Recoil</td><td align="left">Mobx</td></tr><tr><td align="left">zustand</td><td align="left">jotai</td><td align="left">valtio</td></tr></tbody></table><p>在选择时,通常是选择 <code>zustand</code>、<code>jotai</code>、<code>valtio</code>,他们都是对应类型前者的优化版本。</p><p>单向数据流的优势是数据本身比较干净。负担轻。但是当数据结构非常复杂的时候,通常需要结合不可变数据集 <code>immutable.js</code>、<code>immer.js</code> 等才能做到最佳的性能表现。</p><p>原子化方式与 proxy 方式都是一致的,都是收集数据与 UI 的绑定关系,当数据发生变化时,UI 会自动更新。他们的区别就是,原子化在写法上,是先定义原子,然后通过原子来管理数据。</p><p>而 Proxy 是先定义一个大一点的对象,然后通过 Proxy 来劫持对象的属性,然后再将属性与 UI 进行绑定。因此在性能表象上,原子化的性能会略微好一些,他少了劫持的过程。但是当数据开始变得复杂时,原子化的写法可能也会比较繁琐。</p><p>在处理复杂数据时,初始化创建 Atom 对象的开销比较大,但是 Proxy 的包装开销也会比较大。他们在更新时的开销都比较小。</p><p>在处理大型复杂列表数据时,他们的表现如下所示</p><table><thead><tr><th align="left">库</th><th align="left">初始化速度</th><th align="left">更新速度</th><th align="left">内存占用</th><th align="left">开发复杂度</th></tr></thead><tbody><tr><td align="left">Zustand</td><td align="left"><strong>极快</strong></td><td align="left">快</td><td align="left"><strong>极低</strong>,原生对象</td><td align="left">较高</td></tr><tr><td align="left">jotai</td><td align="left">较慢</td><td align="left"><strong>精准,快</strong></td><td align="left"><strong>最高</strong>,原子实例多</td><td align="left">偏高</td></tr><tr><td align="left">valtio</td><td align="left">中等偏慢</td><td align="left"><strong>精准,快</strong></td><td align="left"><strong>偏高</strong>,Proxy 开销大</td><td align="left">低</td></tr></tbody></table><p>在更新上的具体细节表现如下</p><table><thead><tr><th align="left">库</th><th align="left">通知复杂度</th><th align="left">渲染复杂度</th><th align="left">原理</th></tr></thead><tbody><tr><td align="left"><strong>Zustand</strong></td><td align="left"><strong>O(N)</strong></td><td align="left"><strong>O(1)</strong></td><td align="left">线性遍历订阅列表 + Selector 比对</td></tr><tr><td align="left"><strong>Jotai</strong></td><td align="left"><strong>O(1)</strong></td><td align="left"><strong>O(1)</strong></td><td align="left">依赖图。原子 A 变了,直接找到订阅了 A 的那一个组件。</td></tr><tr><td align="left"><strong>Valtio</strong></td><td align="left"><strong>O(1)</strong></td><td align="left"><strong>O(1)</strong></td><td align="left">Proxy 追踪。属性 A 变了,直接精准通知访问过属性 A 的组件。</td></tr></tbody></table><h2 id="快速上手：创建第一个Store"><a href="#快速上手：创建第一个Store" class="headerlink" title="快速上手：创建第一个Store"></a>快速上手：创建第一个Store</h2><h3 id="基础用法"><a href="#基础用法" class="headerlink" title="基础用法"></a>基础用法</h3><p>Zustand的使用非常简单，只需要导入create函数并定义状态和操作方法：</p><figure class="highlight typescript"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; create &#125; <span class="keyword">from</span> <span class="string">&#x27;zustand&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">interface</span> <span class="title class_">TabItem</span> &#123;</span><br><span class="line">  <span class="attr">id</span>: <span class="built_in">string</span></span><br><span class="line">  <span class="attr">title</span>: <span class="built_in">string</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">interface</span> <span class="title class_">TabState</span> &#123;</span><br><span class="line">  <span class="attr">tabs</span>: <span class="title class_">TabItem</span>[]</span><br><span class="line">  <span class="attr">currentTabId</span>: <span class="built_in">string</span></span><br><span class="line">  <span class="attr">addTab</span>: <span class="function">(<span class="params"><span class="attr">tab</span>: <span class="title class_">TabItem</span></span>) =&gt;</span> <span class="built_in">void</span></span><br><span class="line">  <span class="attr">removeTab</span>: <span class="function">(<span class="params"><span class="attr">id</span>: <span class="built_in">string</span></span>) =&gt;</span> <span class="built_in">void</span></span><br><span class="line">  <span class="attr">setCurrentTab</span>: <span class="function">(<span class="params"><span class="attr">id</span>: <span class="built_in">string</span></span>) =&gt;</span> <span class="built_in">void</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> useTabStore = create&lt;<span class="title class_">TabState</span>&gt;(<span class="function">(<span class="params">set</span>) =&gt;</span> (&#123;</span><br><span class="line">  <span class="attr">tabs</span>: [],</span><br><span class="line">  <span class="attr">currentTabId</span>: <span class="string">&#x27;&#x27;</span>,</span><br><span class="line">  <span class="attr">addTab</span>: <span class="function">(<span class="params">tab</span>) =&gt;</span> <span class="title function_">set</span>(<span class="function">(<span class="params">state</span>) =&gt;</span> (&#123;</span><br><span class="line">    <span class="attr">tabs</span>: [...state.<span class="property">tabs</span>, tab]</span><br><span class="line">  &#125;)),</span><br><span class="line">  <span class="attr">removeTab</span>: <span class="function">(<span class="params">id</span>) =&gt;</span> <span class="title function_">set</span>(<span class="function">(<span class="params">state</span>) =&gt;</span> (&#123;</span><br><span class="line">    <span class="attr">tabs</span>: state.<span class="property">tabs</span>.<span class="title function_">filter</span>(<span class="function">(<span class="params">t</span>) =&gt;</span> t.<span class="property">id</span> !== id),</span><br><span class="line">    <span class="attr">currentTabId</span>: state.<span class="property">currentTabId</span> === id ? <span class="string">&#x27;&#x27;</span> : state.<span class="property">currentTabId</span></span><br><span class="line">  &#125;)),</span><br><span class="line">  <span class="attr">setCurrentTab</span>: <span class="function">(<span class="params">id</span>) =&gt;</span> <span class="title function_">set</span>(&#123; <span class="attr">currentTabId</span>: id &#125;)</span><br><span class="line">&#125;))</span><br></pre></td></tr></table></figure><p>在组件中使用同样直观：</p><figure class="highlight tsx"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; useTabStore &#125; <span class="keyword">from</span> <span class="string">&#x27;./store/tabStore&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="keyword">function</span> <span class="title function_">TabView</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> &#123; tabs, currentTabId, addTab, setCurrentTab &#125; = <span class="title function_">useTabStore</span>()</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">button</span> <span class="attr">onClick</span>=<span class="string">&#123;()</span> =&gt;</span> addTab(&#123; id: &#x27;settings&#x27;, title: &#x27;设置&#x27; &#125;)&#125;&gt;</span></span><br><span class="line"><span class="language-xml">        新增 Tab</span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;/<span class="name">button</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">div</span> <span class="attr">style</span>=<span class="string">&#123;&#123;</span> <span class="attr">display:</span> &#x27;<span class="attr">flex</span>&#x27;, <span class="attr">gap:</span> <span class="attr">8</span> &#125;&#125;&gt;</span></span></span><br><span class="line"><span class="language-xml">        &#123;tabs.map((tab) =&gt; (</span></span><br><span class="line"><span class="language-xml">          <span class="tag">&lt;<span class="name">div</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">            <span class="attr">key</span>=<span class="string">&#123;tab.id&#125;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">            <span class="attr">onClick</span>=<span class="string">&#123;()</span> =&gt;</span> setCurrentTab(tab.id)&#125;</span></span><br><span class="line"><span class="language-xml">            style=&#123;&#123;</span></span><br><span class="line"><span class="language-xml">              padding: 4,</span></span><br><span class="line"><span class="language-xml">              borderBottom: tab.id === currentTabId ? &#x27;2px solid blue&#x27; : &#x27;none&#x27;,</span></span><br><span class="line"><span class="language-xml">              cursor: &#x27;pointer&#x27;</span></span><br><span class="line"><span class="language-xml">            &#125;&#125;</span></span><br><span class="line"><span class="language-xml">          &gt;</span></span><br><span class="line"><span class="language-xml">            &#123;tab.title&#125;</span></span><br><span class="line"><span class="language-xml">          <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">        ))&#125;</span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line">  )</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这就是Zustand的全部——创建store，在组件中引入，直接使用。所有状态都由Zustand管理，任何组件都能随时访问。</p><h3 id="处理异步操作"><a href="#处理异步操作" class="headerlink" title="处理异步操作"></a>处理异步操作</h3><p>异步处理在Zust中同样简单，直接在set回调中执行async&#x2F;await即可：</p><figure class="highlight typescript"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; create &#125; <span class="keyword">from</span> <span class="string">&#x27;zustand&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">interface</span> <span class="title class_">Todo</span> &#123;</span><br><span class="line">  <span class="attr">id</span>: <span class="built_in">number</span></span><br><span class="line">  <span class="attr">title</span>: <span class="built_in">string</span></span><br><span class="line">  <span class="attr">completed</span>: <span class="built_in">boolean</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">interface</span> <span class="title class_">TodoState</span> &#123;</span><br><span class="line">  <span class="attr">todos</span>: <span class="title class_">Todo</span>[]</span><br><span class="line">  <span class="attr">error</span>: <span class="built_in">string</span> | <span class="literal">null</span></span><br><span class="line">  <span class="attr">isLoading</span>: <span class="built_in">boolean</span></span><br><span class="line">  <span class="attr">fetchTodos</span>: <span class="function">() =&gt;</span> <span class="title class_">Promise</span>&lt;<span class="built_in">void</span>&gt;</span><br><span class="line">  <span class="attr">toggleTodo</span>: <span class="function">(<span class="params"><span class="attr">id</span>: <span class="built_in">number</span></span>) =&gt;</span> <span class="built_in">void</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> useTodoStore = create&lt;<span class="title class_">TodoState</span>&gt;(<span class="function">(<span class="params">set</span>) =&gt;</span> (&#123;</span><br><span class="line">  <span class="attr">todos</span>: [],</span><br><span class="line">  <span class="attr">error</span>: <span class="literal">null</span>,</span><br><span class="line">  <span class="attr">isLoading</span>: <span class="literal">false</span>,</span><br><span class="line">  <span class="attr">fetchTodos</span>: <span class="title function_">async</span> () =&gt; &#123;</span><br><span class="line">    <span class="title function_">set</span>(&#123; <span class="attr">isLoading</span>: <span class="literal">true</span>, <span class="attr">error</span>: <span class="literal">null</span> &#125;)</span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">      <span class="keyword">const</span> res = <span class="keyword">await</span> <span class="title function_">fetch</span>(<span class="string">&#x27;https://jsonplaceholder.typicode.com/todos&#x27;</span>)</span><br><span class="line">      <span class="keyword">const</span> data = <span class="keyword">await</span> res.<span class="title function_">json</span>()</span><br><span class="line">      <span class="title function_">set</span>(&#123; <span class="attr">todos</span>: data.<span class="title function_">slice</span>(<span class="number">0</span>, <span class="number">10</span>), <span class="attr">isLoading</span>: <span class="literal">false</span> &#125;)</span><br><span class="line">    &#125; <span class="keyword">catch</span> (error) &#123;</span><br><span class="line">      <span class="title function_">set</span>(&#123; <span class="attr">error</span>: (error <span class="keyword">as</span> <span class="title class_">Error</span>).<span class="property">message</span>, <span class="attr">isLoading</span>: <span class="literal">false</span> &#125;)</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="attr">toggleTodo</span>: <span class="function">(<span class="params">id</span>) =&gt;</span> <span class="title function_">set</span>(<span class="function">(<span class="params">state</span>) =&gt;</span> (&#123;</span><br><span class="line">    <span class="attr">todos</span>: state.<span class="property">todos</span>.<span class="title function_">map</span>(<span class="function"><span class="params">todo</span> =&gt;</span></span><br><span class="line">      todo.<span class="property">id</span> === id ? &#123; ...todo, <span class="attr">completed</span>: !todo.<span class="property">completed</span> &#125; : todo</span><br><span class="line">    )</span><br><span class="line">  &#125;))</span><br><span class="line">&#125;))</span><br></pre></td></tr></table></figure><h2 id="精准订阅：避免无效渲染"><a href="#精准订阅：避免无效渲染" class="headerlink" title="精准订阅：避免无效渲染"></a>精准订阅：避免无效渲染</h2><h3 id="问题分析"><a href="#问题分析" class="headerlink" title="问题分析"></a>问题分析</h3><p>假设我们有这样一个store，包含用户信息和主题设置：</p><figure class="highlight typescript"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; create &#125; <span class="keyword">from</span> <span class="string">&#x27;zustand&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> useAppStore = <span class="title function_">create</span>(<span class="function">(<span class="params">set</span>) =&gt;</span> (&#123;</span><br><span class="line">  <span class="attr">user</span>: &#123;</span><br><span class="line">    <span class="attr">name</span>: <span class="string">&#x27;张三&#x27;</span>,</span><br><span class="line">    <span class="attr">age</span>: <span class="number">25</span>,</span><br><span class="line">    <span class="attr">isLogin</span>: <span class="literal">true</span></span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="attr">theme</span>: <span class="string">&#x27;light&#x27;</span>,</span><br><span class="line">  <span class="attr">setTheme</span>: <span class="function">(<span class="params"><span class="attr">newTheme</span>: <span class="built_in">string</span></span>) =&gt;</span> <span class="title function_">set</span>(&#123; <span class="attr">theme</span>: newTheme &#125;),</span><br><span class="line">  <span class="attr">setUserAge</span>: <span class="function">(<span class="params"><span class="attr">age</span>: <span class="built_in">number</span></span>) =&gt;</span> <span class="title function_">set</span>(<span class="function">(<span class="params">state</span>) =&gt;</span> (&#123;</span><br><span class="line">    <span class="attr">user</span>: &#123; ...state.<span class="property">user</span>, age &#125;</span><br><span class="line">  &#125;))</span><br><span class="line">&#125;))</span><br></pre></td></tr></table></figure><p>如果组件直接订阅整个状态对象：</p><figure class="highlight tsx"><table><tr><td class="code"><pre><span class="line"><span class="comment">// 错误示范：订阅整个状态</span></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">UserName</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> &#123; user &#125; = <span class="title function_">useAppStore</span>()</span><br><span class="line">  <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">&#x27;UserName 渲染了&#x27;</span>)</span><br><span class="line">  <span class="keyword">return</span> <span class="language-xml"><span class="tag">&lt;<span class="name">div</span>&gt;</span>用户名：&#123;user.name&#125;<span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">ThemeDisplay</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> &#123; theme &#125; = <span class="title function_">useAppStore</span>()</span><br><span class="line">  <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">&#x27;ThemeDisplay 渲染了&#x27;</span>)</span><br><span class="line">  <span class="keyword">return</span> <span class="language-xml"><span class="tag">&lt;<span class="name">div</span>&gt;</span>当前主题：&#123;theme&#125;<span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>当调用setUserAge(26)时，即使ThemeDisplay只用到theme字段（未变化），也会被迫重新渲染。这就是Context和普通订阅的典型问题。</p><h3 id="解决方案：Selector精准订阅"><a href="#解决方案：Selector精准订阅" class="headerlink" title="解决方案：Selector精准订阅"></a>解决方案：Selector精准订阅</h3><p>Zustand的精髓在于Selector——通过函数精确指定需要订阅的状态片段：</p><figure class="highlight tsx"><table><tr><td class="code"><pre><span class="line"><span class="comment">// 正确用法：使用Selector精确订阅</span></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">UserName</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> userName = <span class="title function_">useAppStore</span>(<span class="function">(<span class="params">state</span>) =&gt;</span> state.<span class="property">user</span>.<span class="property">name</span>)</span><br><span class="line">  <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">&#x27;UserName 渲染了&#x27;</span>)</span><br><span class="line">  <span class="keyword">return</span> <span class="language-xml"><span class="tag">&lt;<span class="name">div</span>&gt;</span>用户名：&#123;userName&#125;<span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">ThemeDisplay</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> theme = <span class="title function_">useAppStore</span>(<span class="function">(<span class="params">state</span>) =&gt;</span> state.<span class="property">theme</span>)</span><br><span class="line">  <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">&#x27;ThemeDisplay 渲染了&#x27;</span>)</span><br><span class="line">  <span class="keyword">return</span> <span class="language-xml"><span class="tag">&lt;<span class="name">div</span>&gt;</span>当前主题：&#123;theme&#125;<span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>效果对比：</p><ul><li>调用setUserAge(26) → 只有UserName重新渲染（user.age变化）</li><li>调用setTheme(‘dark’) → 只有ThemeDisplay重新渲染（theme变化）</li></ul><p>Selector的工作原理是：Zustand会对比上次渲染返回的值，只有值发生变化时才触发组件更新。这意味着你可以订阅任意深度的嵌套属性，而不必担心其他字段的变化会影响组件。</p><h3 id="计算属性与派生状态"><a href="#计算属性与派生状态" class="headerlink" title="计算属性与派生状态"></a>计算属性与派生状态</h3><p>对于需要基于状态计算派生数据的场景，可以在组件中组合多个Selector：</p><figure class="highlight typescript"><table><tr><td class="code"><pre><span class="line"><span class="comment">// 在store中定义计算逻辑</span></span><br><span class="line"><span class="keyword">interface</span> <span class="title class_">StoreState</span> &#123;</span><br><span class="line">  <span class="attr">items</span>: <span class="built_in">number</span>[]</span><br><span class="line">  <span class="attr">total</span>: <span class="built_in">number</span></span><br><span class="line">  <span class="attr">addItem</span>: <span class="function">(<span class="params"><span class="attr">item</span>: <span class="built_in">number</span></span>) =&gt;</span> <span class="built_in">void</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> useStore = create&lt;<span class="title class_">StoreState</span>&gt;(<span class="function">(<span class="params">set</span>) =&gt;</span> (&#123;</span><br><span class="line">  <span class="attr">items</span>: [],</span><br><span class="line">  <span class="attr">total</span>: <span class="number">0</span>,</span><br><span class="line">  <span class="attr">addItem</span>: <span class="function">(<span class="params">item</span>) =&gt;</span> <span class="title function_">set</span>(<span class="function">(<span class="params">state</span>) =&gt;</span> (&#123;</span><br><span class="line">    <span class="attr">items</span>: [...state.<span class="property">items</span>, item],</span><br><span class="line">    <span class="attr">total</span>: state.<span class="property">total</span> + item</span><br><span class="line">  &#125;))</span><br><span class="line">&#125;))</span><br><span class="line"></span><br><span class="line"><span class="comment">// 或在组件中计算</span></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">TotalDisplay</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> items = <span class="title function_">useStore</span>(<span class="function">(<span class="params">state</span>) =&gt;</span> state.<span class="property">items</span>)</span><br><span class="line">  <span class="keyword">const</span> total = items.<span class="title function_">reduce</span>(<span class="function">(<span class="params">sum, item</span>) =&gt;</span> sum + item, <span class="number">0</span>)</span><br><span class="line">  <span class="keyword">return</span> <span class="language-xml"><span class="tag">&lt;<span class="name">div</span>&gt;</span>总和：&#123;total&#125;<span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="状态持久化：自动保存到本地存储"><a href="#状态持久化：自动保存到本地存储" class="headerlink" title="状态持久化：自动保存到本地存储"></a>状态持久化：自动保存到本地存储</h2><h3 id="基础持久化"><a href="#基础持久化" class="headerlink" title="基础持久化"></a>基础持久化</h3><p>Zustand内置的persist中间件让状态持久化变得极其简单：</p><figure class="highlight typescript"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; create &#125; <span class="keyword">from</span> <span class="string">&#x27;zustand&#x27;</span></span><br><span class="line"><span class="keyword">import</span> &#123; persist &#125; <span class="keyword">from</span> <span class="string">&#x27;zustand/middleware&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">interface</span> <span class="title class_">UserState</span> &#123;</span><br><span class="line">  <span class="attr">token</span>: <span class="built_in">string</span></span><br><span class="line">  <span class="attr">userInfo</span>: <span class="built_in">any</span></span><br><span class="line">  <span class="attr">setToken</span>: <span class="function">(<span class="params"><span class="attr">token</span>: <span class="built_in">string</span></span>) =&gt;</span> <span class="built_in">void</span></span><br><span class="line">  <span class="attr">setUserInfo</span>: <span class="function">(<span class="params"><span class="attr">info</span>: <span class="built_in">any</span></span>) =&gt;</span> <span class="built_in">void</span></span><br><span class="line">  <span class="attr">logout</span>: <span class="function">() =&gt;</span> <span class="built_in">void</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> useUserStore = create&lt;<span class="title class_">UserState</span>&gt;()(</span><br><span class="line">  <span class="title function_">persist</span>(</span><br><span class="line">    <span class="function">(<span class="params">set</span>) =&gt;</span> (&#123;</span><br><span class="line">      <span class="attr">token</span>: <span class="string">&#x27;&#x27;</span>,</span><br><span class="line">      <span class="attr">userInfo</span>: <span class="literal">null</span>,</span><br><span class="line">      <span class="attr">setToken</span>: <span class="function">(<span class="params">token</span>) =&gt;</span> <span class="title function_">set</span>(&#123; token &#125;),</span><br><span class="line">      <span class="attr">setUserInfo</span>: <span class="function">(<span class="params">userInfo</span>) =&gt;</span> <span class="title function_">set</span>(&#123; userInfo &#125;),</span><br><span class="line">      <span class="attr">logout</span>: <span class="function">() =&gt;</span> <span class="title function_">set</span>(&#123; <span class="attr">token</span>: <span class="string">&#x27;&#x27;</span>, <span class="attr">userInfo</span>: <span class="literal">null</span> &#125;)</span><br><span class="line">    &#125;),</span><br><span class="line">    &#123;</span><br><span class="line">      <span class="attr">name</span>: <span class="string">&#x27;user-storage&#x27;</span> <span class="comment">// localStorage中的key</span></span><br><span class="line">    &#125;</span><br><span class="line">  )</span><br><span class="line">)</span><br></pre></td></tr></table></figure><p>刷新页面后，状态会自动从localStorage恢复，无需手动处理。</p><h3 id="高级配置"><a href="#高级配置" class="headerlink" title="高级配置"></a>高级配置</h3><p>persist中间件支持丰富的配置选项：</p><figure class="highlight typescript"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; persist, createJSONStorage &#125; <span class="keyword">from</span> <span class="string">&#x27;zustand/middleware&#x27;</span></span><br><span class="line"><span class="keyword">import</span> &#123; devtools &#125; <span class="keyword">from</span> <span class="string">&#x27;zustand/middleware&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> useStore = create&lt;<span class="title class_">UserState</span>&gt;()(</span><br><span class="line">  <span class="title function_">persist</span>(</span><br><span class="line">    <span class="function">(<span class="params">set</span>) =&gt;</span> (&#123;</span><br><span class="line">      <span class="comment">// 状态定义</span></span><br><span class="line">    &#125;),</span><br><span class="line">    &#123;</span><br><span class="line">      <span class="attr">name</span>: <span class="string">&#x27;my-app-storage&#x27;</span>,</span><br><span class="line">      <span class="attr">storage</span>: <span class="title function_">createJSONStorage</span>(<span class="function">() =&gt;</span> <span class="variable language_">sessionStorage</span>), <span class="comment">// 使用sessionStorage</span></span><br><span class="line">      <span class="attr">partialize</span>: <span class="function">(<span class="params">state</span>) =&gt;</span> (&#123;</span><br><span class="line">        <span class="comment">// 只持久化部分字段</span></span><br><span class="line">        <span class="attr">token</span>: state.<span class="property">token</span>,</span><br><span class="line">        <span class="attr">theme</span>: state.<span class="property">theme</span></span><br><span class="line">      &#125;),</span><br><span class="line">      <span class="attr">onRehydrateStorage</span>: <span class="function">() =&gt;</span> <span class="function">(<span class="params">state</span>) =&gt;</span> &#123;</span><br><span class="line">        <span class="comment">// 持久化恢复后的回调</span></span><br><span class="line">        <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">&#x27;hydration finished&#x27;</span>, state)</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;</span><br><span class="line">  )</span><br><span class="line">)</span><br></pre></td></tr></table></figure><h2 id="开发工具：Redux-DevTools集成"><a href="#开发工具：Redux-DevTools集成" class="headerlink" title="开发工具：Redux DevTools集成"></a>开发工具：Redux DevTools集成</h2><h3 id="基础配置"><a href="#基础配置" class="headerlink" title="基础配置"></a>基础配置</h3><p>开发时配合Redux DevTools使用，只需要添加devtools中间件：</p><figure class="highlight typescript"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; create &#125; <span class="keyword">from</span> <span class="string">&#x27;zustand&#x27;</span></span><br><span class="line"><span class="keyword">import</span> &#123; devtools &#125; <span class="keyword">from</span> <span class="string">&#x27;zustand/middleware&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> useCounterStore = <span class="title function_">create</span>(</span><br><span class="line">  <span class="title function_">devtools</span>(</span><br><span class="line">    <span class="function">(<span class="params">set</span>) =&gt;</span> (&#123;</span><br><span class="line">      <span class="attr">count</span>: <span class="number">0</span>,</span><br><span class="line">      <span class="attr">increase</span>: <span class="function">() =&gt;</span> <span class="title function_">set</span>(<span class="function">(<span class="params">state</span>) =&gt;</span> (&#123; <span class="attr">count</span>: state.<span class="property">count</span> + <span class="number">1</span> &#125;)),</span><br><span class="line">      <span class="attr">decrease</span>: <span class="function">() =&gt;</span> <span class="title function_">set</span>(<span class="function">(<span class="params">state</span>) =&gt;</span> (&#123; <span class="attr">count</span>: state.<span class="property">count</span> - <span class="number">1</span> &#125;))</span><br><span class="line">    &#125;),</span><br><span class="line">    &#123; <span class="attr">name</span>: <span class="string">&#x27;counter-store&#x27;</span> &#125; <span class="comment">// DevTools中显示的名称</span></span><br><span class="line">  )</span><br><span class="line">)</span><br></pre></td></tr></table></figure><p>打开浏览器DevTools的Redux面板，你可以：</p><ul><li>查看完整的state快照</li><li>查看所有action调用记录</li><li>时间旅行调试，回退到任意状态</li><li>手动dispatch action进行测试</li></ul><h3 id="生产环境配置"><a href="#生产环境配置" class="headerlink" title="生产环境配置"></a>生产环境配置</h3><p>生产环境中建议限制devtools的使用：</p><figure class="highlight typescript"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; devtools &#125; <span class="keyword">from</span> <span class="string">&#x27;zustand/middleware&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> store = <span class="title function_">create</span>(</span><br><span class="line">  <span class="title function_">devtools</span>(</span><br><span class="line">    <span class="function">(<span class="params">set</span>) =&gt;</span> (&#123; <span class="comment">/* ... */</span> &#125;),</span><br><span class="line">    &#123;</span><br><span class="line">      <span class="attr">enabled</span>: process.<span class="property">env</span>.<span class="property">NODE_ENV</span> === <span class="string">&#x27;development&#x27;</span>,</span><br><span class="line">      <span class="attr">name</span>: <span class="string">&#x27;my-app&#x27;</span></span><br><span class="line">    &#125;</span><br><span class="line">  )</span><br><span class="line">)</span><br></pre></td></tr></table></figure><h2 id="复杂状态更新：Immer集成"><a href="#复杂状态更新：Immer集成" class="headerlink" title="复杂状态更新：Immer集成"></a>复杂状态更新：Immer集成</h2><h3 id="问题场景"><a href="#问题场景" class="headerlink" title="问题场景"></a>问题场景</h3><p>当状态嵌套较深时，传统的不可变更新写法非常冗长：</p><figure class="highlight typescript"><table><tr><td class="code"><pre><span class="line"><span class="keyword">interface</span> <span class="title class_">DeepState</span> &#123;</span><br><span class="line">  <span class="attr">nested</span>: &#123;</span><br><span class="line">    <span class="attr">object</span>: &#123;</span><br><span class="line">      <span class="attr">count</span>: <span class="built_in">number</span></span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 传统的展开写法</span></span><br><span class="line"><span class="keyword">const</span> useStore = create&lt;<span class="title class_">DeepState</span>&gt;(<span class="function">(<span class="params">set</span>) =&gt;</span> (&#123;</span><br><span class="line">  <span class="attr">nested</span>: &#123; <span class="attr">object</span>: &#123; <span class="attr">count</span>: <span class="number">0</span> &#125; &#125;,</span><br><span class="line">  <span class="attr">increment</span>: <span class="function">() =&gt;</span> <span class="title function_">set</span>(<span class="function">(<span class="params">state</span>) =&gt;</span> (&#123;</span><br><span class="line">    <span class="attr">nested</span>: &#123;</span><br><span class="line">      ...state.<span class="property">nested</span>,</span><br><span class="line">      <span class="attr">object</span>: &#123;</span><br><span class="line">        ...state.<span class="property">nested</span>.<span class="property">object</span>,</span><br><span class="line">        <span class="attr">count</span>: state.<span class="property">nested</span>.<span class="property">object</span>.<span class="property">count</span> + <span class="number">1</span></span><br><span class="line">      &#125;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;))</span><br><span class="line">&#125;))</span><br></pre></td></tr></table></figure><p>这种写法不仅繁琐，而且极易出错。</p><h3 id="Immer解决方案"><a href="#Immer解决方案" class="headerlink" title="Immer解决方案"></a>Immer解决方案</h3><p>结合Immer中间件，可以用更直观的可变式写法更新深层状态：</p><figure class="highlight typescript"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; create &#125; <span class="keyword">from</span> <span class="string">&#x27;zustand&#x27;</span></span><br><span class="line"><span class="keyword">import</span> &#123; immer &#125; <span class="keyword">from</span> <span class="string">&#x27;zustand/middleware/immer&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">interface</span> <span class="title class_">DeepState</span> &#123;</span><br><span class="line">  <span class="attr">nested</span>: &#123;</span><br><span class="line">    <span class="attr">object</span>: &#123;</span><br><span class="line">      <span class="attr">count</span>: <span class="built_in">number</span></span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="attr">increment</span>: <span class="function">() =&gt;</span> <span class="built_in">void</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> useStore = create&lt;<span class="title class_">DeepState</span>&gt;()(</span><br><span class="line">  <span class="title function_">immer</span>(<span class="function">(<span class="params">set</span>) =&gt;</span> (&#123;</span><br><span class="line">    <span class="attr">nested</span>: &#123; <span class="attr">object</span>: &#123; <span class="attr">count</span>: <span class="number">0</span> &#125; &#125;,</span><br><span class="line">    <span class="attr">increment</span>: <span class="function">() =&gt;</span> <span class="title function_">set</span>(<span class="function">(<span class="params">state</span>) =&gt;</span> &#123;</span><br><span class="line">      state.<span class="property">nested</span>.<span class="property">object</span>.<span class="property">count</span>++</span><br><span class="line">    &#125;)</span><br><span class="line">  &#125;))</span><br><span class="line">)</span><br></pre></td></tr></table></figure><p>Immer会自动处理不可变更新，让代码保持简洁。需要注意的是，SSR场景下建议慎用Immer，因为它会带来额外的CPU开销。</p><h2 id="最佳实践与注意事项"><a href="#最佳实践与注意事项" class="headerlink" title="最佳实践与注意事项"></a>最佳实践与注意事项</h2><h3 id="Store模块化拆分"><a href="#Store模块化拆分" class="headerlink" title="Store模块化拆分"></a>Store模块化拆分</h3><p>在中大型项目中，建议将不同业务域的状态拆分为独立的store文件：</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">stores/</span><br><span class="line">├── userStore.ts      // 用户认证状态</span><br><span class="line">├── cartStore.ts      // 购物车状态</span><br><span class="line">├── uiStore.ts        // UI相关状态（主题、侧边栏等）</span><br><span class="line">└── settingsStore.ts // 用户设置</span><br></pre></td></tr></table></figure><p>每个store维护自己的状态和操作逻辑，职责清晰，便于维护：</p><figure class="highlight typescript"><table><tr><td class="code"><pre><span class="line"><span class="comment">// stores/userStore.ts</span></span><br><span class="line"><span class="keyword">interface</span> <span class="title class_">UserState</span> &#123;</span><br><span class="line">  <span class="attr">isAuthenticated</span>: <span class="built_in">boolean</span></span><br><span class="line">  <span class="attr">user</span>: <span class="title class_">User</span> | <span class="literal">null</span></span><br><span class="line">  <span class="attr">login</span>: <span class="function">(<span class="params"><span class="attr">credentials</span>: <span class="title class_">Credentials</span></span>) =&gt;</span> <span class="title class_">Promise</span>&lt;<span class="built_in">void</span>&gt;</span><br><span class="line">  <span class="attr">logout</span>: <span class="function">() =&gt;</span> <span class="built_in">void</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> useUserStore = create&lt;<span class="title class_">UserState</span>&gt;(<span class="function">(<span class="params">set</span>) =&gt;</span> (&#123;</span><br><span class="line">  <span class="attr">isAuthenticated</span>: <span class="literal">false</span>,</span><br><span class="line">  <span class="attr">user</span>: <span class="literal">null</span>,</span><br><span class="line">  <span class="attr">login</span>: <span class="title function_">async</span> (credentials) =&gt; &#123;</span><br><span class="line">    <span class="keyword">const</span> user = <span class="keyword">await</span> authService.<span class="title function_">login</span>(credentials)</span><br><span class="line">    <span class="title function_">set</span>(&#123; <span class="attr">isAuthenticated</span>: <span class="literal">true</span>, user &#125;)</span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="attr">logout</span>: <span class="function">() =&gt;</span> <span class="title function_">set</span>(&#123; <span class="attr">isAuthenticated</span>: <span class="literal">false</span>, <span class="attr">user</span>: <span class="literal">null</span> &#125;)</span><br><span class="line">&#125;))</span><br></pre></td></tr></table></figure><p>模块化拆分的好处：</p><ul><li>状态职责单一，易于追踪和维护</li><li>避免单个store过于臃肿</li><li>新成员容易理解状态结构和变化来源</li><li>便于代码分割（code splitting）</li></ul><h3 id="避免常见陷阱"><a href="#避免常见陷阱" class="headerlink" title="避免常见陷阱"></a>避免常见陷阱</h3><ol><li><strong>不要在组件外直接调用getState()</strong></li></ol><figure class="highlight typescript"><table><tr><td class="code"><pre><span class="line"><span class="comment">// 错误用法</span></span><br><span class="line"><span class="keyword">const</span> store = <span class="title function_">create</span>(<span class="function">(<span class="params">set</span>) =&gt;</span> (&#123; <span class="attr">count</span>: <span class="number">0</span> &#125;))</span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(store.<span class="title function_">getState</span>().<span class="property">count</span>) <span class="comment">// 可能拿到过期值</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 正确用法</span></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">MyComponent</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="keyword">const</span> count = <span class="title function_">useStore</span>(<span class="function">(<span class="params">state</span>) =&gt;</span> state.<span class="property">count</span>)</span><br><span class="line">    <span class="comment">// 在组件内使用</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ol start="2"><li><strong>保持状态序列化的考虑</strong></li></ol><p>如果使用persist中间件，确保状态可以被序列化。避免在state中存储函数、类实例或循环引用的对象。</p><ol><li><strong>Selector中避免创建新对象</strong></li></ol><figure class="highlight typescript"><table><tr><td class="code"><pre><span class="line"><span class="comment">// 错误：每次渲染都创建新对象</span></span><br><span class="line"><span class="keyword">const</span> user = <span class="title function_">useStore</span>(<span class="function">(<span class="params">state</span>) =&gt;</span> (&#123; <span class="attr">name</span>: state.<span class="property">name</span> &#125;))</span><br><span class="line"></span><br><span class="line"><span class="comment">// 正确：直接返回原始值</span></span><br><span class="line"><span class="keyword">const</span> name = <span class="title function_">useStore</span>(<span class="function">(<span class="params">state</span>) =&gt;</span> state.<span class="property">name</span>)</span><br></pre></td></tr></table></figure><ol start="4"><li><p><strong>合理使用中间件</strong></p><p>中间件虽然强大，但不要过度使用。只添加实际需要的中间件，避免不必要的性能开销。</p></li></ol><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>Zustand以其极简的API、优异的性能和灵活的扩展性，成为React状态管理的优秀选择。它不追求大而全，而是专注于解决实际开发中的痛点。</p><p>通过本文，你应该已经掌握了：</p><ul><li>Zustand的核心概念和优势</li><li>创建和使用store的多种方式</li><li>Selector精准订阅的实现原理</li><li>持久化、DevTools、Immer等常用技巧</li><li>模块化拆分和最佳实践</li></ul><p>对于中小型项目，Zustand完全可以替代Redux，提供更简洁的开发体验。对于大型项目，它可以作为领域状态管理的轻量选择，与服务端状态管理方案（如React Query）配合使用。</p><p>如果你正在寻找一个简单、高效、现代化的React状态管理方案，Zustand绝对值得一试。</p>]]>
    </content>
    <id>http://fe.poetries.top/2025/01/14/zustand-react-state-management/</id>
    <link href="http://fe.poetries.top/2025/01/14/zustand-react-state-management/"/>
    <published>2025-01-14T06:40:12.000Z</published>
    <summary>深入解析Zustand状态管理库，对比Redux/Recoil/MobX等主流方案，详解Selector精准订阅、持久化、DevTools等核心功能，带你打造优雅高效的React应用。</summary>
    <title>极简React状态管理方案：Zustand使用指南与实战技巧</title>
    <updated>2026-03-08T10:22:42.108Z</updated>
  </entry>
  <entry>
    <author>
      <name>Poetry</name>
    </author>
    <category term="Front-End" scheme="http://fe.poetries.top/categories/Front-End/"/>
    <category term="React" scheme="http://fe.poetries.top/tags/React/"/>
    <category term="React 18" scheme="http://fe.poetries.top/tags/React-18/"/>
    <category term="并发渲染" scheme="http://fe.poetries.top/tags/%E5%B9%B6%E5%8F%91%E6%B8%B2%E6%9F%93/"/>
    <category term="前端进阶" scheme="http://fe.poetries.top/tags/%E5%89%8D%E7%AB%AF%E8%BF%9B%E9%98%B6/"/>
    <content>
      <![CDATA[<h2 id="导语"><a href="#导语" class="headerlink" title="导语"></a>导语</h2><p>React 18 最重要的更新就是引入了<strong>并发机制（Concurrent Features）</strong>，这是 React 团队多年研发的结晶。简单来说，并发机制让 React 可以<strong>同时准备多个版本的 UI</strong>，根据用户设备的性能动态调整渲染优先级，从而提供更流畅的用户体验。</p><p>本文将深入浅出地讲解 React 18 并发渲染的核心原理，包括 <strong>Lane 模型</strong>、<strong>时间切片</strong>、<strong>useTransition</strong> 等关键技术，并通过源码分析帮助大家彻底理解这一革命性的架构升级。</p><h2 id="一、React-渲染流程与问题"><a href="#一、React-渲染流程与问题" class="headerlink" title="一、React 渲染流程与问题"></a>一、React 渲染流程与问题</h2><h3 id="1-1-传统渲染流程"><a href="#1-1-传统渲染流程" class="headerlink" title="1.1 传统渲染流程"></a>1.1 传统渲染流程</h3><p>在 React 18 之前，React 的渲染过程是<strong>同步且不可中断</strong>的。假设我们有这样一个组件树：</p><figure class="highlight jsx"><table><tr><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">App</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">Header</span> /&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">Sidebar</span> /&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">Content</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">ComponentA</span> /&gt;</span></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">ComponentB</span> /&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;/<span class="name">Content</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">Footer</span> /&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line">  );</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>React 会以 <strong>DFS（深度优先搜索）</strong> 的顺序遍历整棵树：</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">App -&gt; Header -&gt; Sidebar -&gt; Content -&gt; ComponentA -&gt; ComponentB -&gt; Footer</span><br></pre></td></tr></table></figure><p>对于每个组件，React 都会创建对应的 <strong>Fiber Node</strong>（Fiber 节点），用于保存渲染所需的信息如 props、key、ref、lanes 等。这就是 React 的 <strong>Fiber 架构</strong>。</p><h3 id="1-2-渲染触发时机"><a href="#1-2-渲染触发时机" class="headerlink" title="1.2 渲染触发时机"></a>1.2 渲染触发时机</h3><p>React 会在两种情况下触发渲染：</p><ol><li><strong>mount</strong>：首次渲染，例如 <code>ReactDOM.createRoot(document.querySelector(&#39;#root&#39;)).render(&lt;App /&gt;)</code></li><li><strong>update</strong>：状态更新，例如通过 <code>useState</code>、<code>useReducer</code> 等 Hook 触发重新渲染</li></ol><h3 id="1-3-核心问题：渲染不可中断"><a href="#1-3-核心问题：渲染不可中断" class="headerlink" title="1.3 核心问题：渲染不可中断"></a>1.3 核心问题：渲染不可中断</h3><p>在 React 18 之前，<strong>整个渲染过程是不能被中断的</strong>。这意味着：</p><ul><li>如果某个组件渲染开销较大（如包含大量列表项），用户会明显感觉到页面卡顿</li><li>在渲染过程中，浏览器无法响应用户的交互操作</li><li>即使有更高优先级的任务（如用户点击），也必须等待当前渲染完成</li></ul><p>React 官方提供了一个典型例子来展示这个问题：</p><figure class="highlight jsx"><table><tr><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">App</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> [tab, setTab] = <span class="title function_">useState</span>(<span class="string">&#x27;posts&#x27;</span>);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">button</span> <span class="attr">onClick</span>=<span class="string">&#123;()</span> =&gt;</span> setTab(&#x27;posts&#x27;)&#125;&gt;Posts<span class="tag">&lt;/<span class="name">button</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">button</span> <span class="attr">onClick</span>=<span class="string">&#123;()</span> =&gt;</span> setTab(&#x27;about&#x27;)&#125;&gt;About<span class="tag">&lt;/<span class="name">button</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      &#123;tab === &#x27;posts&#x27; ? <span class="tag">&lt;<span class="name">PostsTab</span> /&gt;</span> : <span class="tag">&lt;<span class="name">AboutTab</span> /&gt;</span>&#125;</span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line">  );</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// PostsTab 包含500个渲染开销大的组件</span></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">PostsTab</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      &#123;Array(500).fill(0).map((_, i) =&gt; (</span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">SlowPost</span> <span class="attr">key</span>=<span class="string">&#123;i&#125;</span> <span class="attr">index</span>=<span class="string">&#123;i&#125;</span> /&gt;</span></span></span><br><span class="line"><span class="language-xml">      ))&#125;</span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line">  );</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 每个SlowPost组件渲染需要1ms</span></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">SlowPost</span>(<span class="params">&#123; index &#125;</span>) &#123;</span><br><span class="line">  <span class="comment">// 模拟渲染开销</span></span><br><span class="line">  <span class="keyword">let</span> startTime = performance.<span class="title function_">now</span>();</span><br><span class="line">  <span class="keyword">while</span> (performance.<span class="title function_">now</span>() - startTime &lt; <span class="number">1</span>) &#123;&#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> <span class="language-xml"><span class="tag">&lt;<span class="name">div</span>&gt;</span>Post #&#123;index&#125;<span class="tag">&lt;/<span class="name">div</span>&gt;</span></span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在这个例子中：</p><ul><li>点击 Posts 按钮后，页面会出现明显卡顿</li><li>在渲染完成前，其他按钮点击无法响应</li><li>用户体验非常糟糕</li></ul><p><strong>这就是 React 18 并发机制要解决的核心问题。</strong></p><h2 id="二、并发渲染的核心解决方案"><a href="#二、并发渲染的核心解决方案" class="headerlink" title="二、并发渲染的核心解决方案"></a>二、并发渲染的核心解决方案</h2><p>React 18 通过两个核心技术实现了并发渲染：</p><ol><li><strong>Lane 模型</strong>：为每次渲染分配优先级</li><li><strong>时间切片</strong>：将连续渲染拆分为可中断的片段</li></ol><h3 id="2-1-解决方案一：useTransition"><a href="#2-1-解决方案一：useTransition" class="headerlink" title="2.1 解决方案一：useTransition"></a>2.1 解决方案一：useTransition</h3><p>React 18 提供了 <code>useTransition</code> Hook 来解决上述问题：</p><figure class="highlight jsx"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; useState, useTransition &#125; <span class="keyword">from</span> <span class="string">&#x27;react&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">App</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> [tab, setTab] = <span class="title function_">useState</span>(<span class="string">&#x27;posts&#x27;</span>);</span><br><span class="line">  <span class="keyword">const</span> [isPending, startTransition] = <span class="title function_">useTransition</span>();</span><br><span class="line"></span><br><span class="line">  <span class="keyword">function</span> <span class="title function_">handleTabChange</span>(<span class="params">nextTab</span>) &#123;</span><br><span class="line">    <span class="comment">// 使用 startTransition 包裹低优先级更新</span></span><br><span class="line">    <span class="title function_">startTransition</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">      <span class="title function_">setTab</span>(nextTab);</span><br><span class="line">    &#125;);</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">button</span> <span class="attr">onClick</span>=<span class="string">&#123;()</span> =&gt;</span> handleTabChange(&#x27;posts&#x27;)&#125;&gt;Posts<span class="tag">&lt;/<span class="name">button</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">button</span> <span class="attr">onClick</span>=<span class="string">&#123;()</span> =&gt;</span> handleTabChange(&#x27;about&#x27;)&#125;&gt;About<span class="tag">&lt;/<span class="name">button</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      &#123;isPending ? <span class="tag">&lt;<span class="name">Loading</span> /&gt;</span> : tab === &#x27;posts&#x27; ? <span class="tag">&lt;<span class="name">PostsTab</span> /&gt;</span> : <span class="tag">&lt;<span class="name">AboutTab</span> /&gt;</span>&#125;</span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line">  );</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>使用 <code>useTransition</code> 后：</p><ul><li>点击按钮会立即响应，页面不会卡顿</li><li>低优先级的渲染任务可以被高优先级任务中断</li><li>用户可以继续与其他元素交互</li></ul><p>这就是<strong>并发更新</strong>的典型应用场景。</p><h2 id="三、-Lane-模型详解"><a href="#三、-Lane-模型详解" class="headerlink" title="三、 Lane 模型详解"></a>三、 Lane 模型详解</h2><h3 id="3-1-什么是-Lane"><a href="#3-1-什么是-Lane" class="headerlink" title="3.1 什么是 Lane"></a>3.1 什么是 Lane</h3><p><strong>Lane</strong>（中文意为”赛道”）是 React 18 引入的优先级管理机制。简单来说，Lane 模型会给每次渲染分配一个<strong>优先级</strong>，React 根据这些优先级决定哪些更新应该优先处理。</p><h3 id="3-2-二进制表示的优势"><a href="#3-2-二进制表示的优势" class="headerlink" title="3.2 二进制表示的优势"></a>3.2 二进制表示的优势</h3><p>React 使用<strong>二进制</strong>来表示不同的 Lane：</p><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="comment">// React 源码中的 Lane 定义（简化）</span></span><br><span class="line"><span class="keyword">const</span> <span class="title class_">Lane</span> = &#123;</span><br><span class="line">  <span class="title class_">NoLane</span>: <span class="number">0b0000000000000000000000000000000</span>,</span><br><span class="line">  <span class="title class_">SyncLane</span>: <span class="number">0b0000000000000000000000000000001</span>,  <span class="comment">// 最高优先级</span></span><br><span class="line">  <span class="title class_">InputContinuousLane</span>: <span class="number">0b0000000000000000000000000000100</span>,</span><br><span class="line">  <span class="title class_">DefaultLane</span>: <span class="number">0b0000000000000000000000000010000</span>,</span><br><span class="line">  <span class="title class_">IdleLane</span>: <span class="number">0b0000000000000000000001000000000</span>,   <span class="comment">// 最低优先级</span></span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>为什么采用二进制？</p><ol><li><strong>性能</strong>：计算机底层对二进制的处理效率更高</li><li><strong>位运算</strong>：可以轻松完成合并、比较等操作</li></ol><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="comment">// 合并多个 Lane</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">function</span> <span class="title function_">mergeLanes</span>(<span class="params">a, b</span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> a | b;  <span class="comment">// 位运算 OR</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 移除某个 Lane</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">function</span> <span class="title function_">removeLanes</span>(<span class="params">set, subset</span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> set &amp; ~subset;  <span class="comment">// 位运算 AND NOT</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="3-3-事件优先级映射"><a href="#3-3-事件优先级映射" class="headerlink" title="3.3 事件优先级映射"></a>3.3 事件优先级映射</h3><p>不同的浏览器事件对应不同的 Lane 优先级：</p><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">function</span> <span class="title function_">getEventPriority</span>(<span class="params">domEventName</span>) &#123;</span><br><span class="line">  <span class="keyword">switch</span> (domEventName) &#123;</span><br><span class="line">    <span class="keyword">case</span> <span class="string">&#x27;click&#x27;</span>:</span><br><span class="line">    <span class="keyword">case</span> <span class="string">&#x27;input&#x27;</span>:</span><br><span class="line">    <span class="keyword">case</span> <span class="string">&#x27;keydown&#x27;</span>:</span><br><span class="line">      <span class="keyword">return</span> <span class="title class_">DiscreteEventPriority</span>;  <span class="comment">// 最高优先级</span></span><br><span class="line">    <span class="keyword">case</span> <span class="string">&#x27;scroll&#x27;</span>:</span><br><span class="line">    <span class="keyword">case</span> <span class="string">&#x27;wheel&#x27;</span>:</span><br><span class="line">    <span class="keyword">case</span> <span class="string">&#x27;mouseenter&#x27;</span>:</span><br><span class="line">      <span class="keyword">return</span> <span class="title class_">ContinuousEventPriority</span>; <span class="comment">// 中等优先级</span></span><br><span class="line">    <span class="attr">default</span>:</span><br><span class="line">      <span class="keyword">return</span> <span class="title class_">DefaultEventPriority</span>;    <span class="comment">// 默认优先级</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>优先级顺序：<code>DiscreteEventPriority</code> &gt; <code>ContinuousEventPriority</code> &gt; <code>DefaultEventPriority</code></p><p>这意味着：</p><ul><li>用户点击输入等操作会立即响应</li><li>滚动、拖拽等连续事件次之</li><li>数据渲染等后台任务优先级最低</li></ul><h2 id="四、时间切片详解"><a href="#四、时间切片详解" class="headerlink" title="四、时间切片详解"></a>四、时间切片详解</h2><h3 id="4-1-什么是时间切片"><a href="#4-1-什么是时间切片" class="headerlink" title="4.1 什么是时间切片"></a>4.1 什么是时间切片</h3><p><strong>时间切片（Time Slicing）</strong> 是将连续不可中断的渲染过程变成可中断的、离散的渲染片段。</p><p>这样做的好处是：</p><ol><li>在渲染间隙可以判断是否有更高优先级的任务</li><li>可以及时渲染 UI 界面</li><li>可以响应用户的交互操作</li></ol><h3 id="4-2-为什么需要时间切片"><a href="#4-2-为什么需要时间切片" class="headerlink" title="4.2 为什么需要时间切片"></a>4.2 为什么需要时间切片</h3><p>我们先理解浏览器的刷新机制：</p><ul><li>常见显示器刷新率有 60Hz、120Hz、144Hz</li><li>60Hz 意味着每秒钟刷新 60 次，即每次间隔约 <strong>16.7ms</strong></li><li>浏览器需要在 16.7ms 内完成 JS 执行和 UI 渲染</li></ul><p>问题在于：React 的渲染和 JS 执行都运行在<strong>主线程</strong>上，当渲染时间过长时，会阻塞 UI 渲染导致卡顿。</p><p><strong>时间切片的解决方案</strong>：把连续的渲染过程切分成小块，每个小块执行时间不超过 5ms，执行完后让出主线程，让浏览器有机会渲染 UI。</p><h3 id="4-3-React-如何实现时间切片"><a href="#4-3-React-如何实现时间切片" class="headerlink" title="4.3 React 如何实现时间切片"></a>4.3 React 如何实现时间切片</h3><p>React 并没有直接使用 <code>requestIdleCallback</code>（因为 Safari 不兼容且浏览器执行不够积极），而是基于 <code>MessageChannel</code> 实现了自己的调度器。</p><h4 id="MessageChannel-实现原理"><a href="#MessageChannel-实现原理" class="headerlink" title="MessageChannel 实现原理"></a>MessageChannel 实现原理</h4><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="comment">// React Scheduler 源码简化版</span></span><br><span class="line"><span class="keyword">const</span> channel = <span class="keyword">new</span> <span class="title class_">MessageChannel</span>();</span><br><span class="line"><span class="keyword">const</span> port = channel.<span class="property">port2</span>;</span><br><span class="line">channel.<span class="property">port1</span>.<span class="property">onmessage</span> = performWorkUntilDeadline;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 调度函数</span></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">requestHostCallback</span>(<span class="params">callback</span>) &#123;</span><br><span class="line">  scheduledHostCallback = callback;</span><br><span class="line">  <span class="keyword">if</span> (!isMessageLoopRunning) &#123;</span><br><span class="line">    isMessageLoopRunning = <span class="literal">true</span>;</span><br><span class="line">    port.<span class="title function_">postMessage</span>(<span class="literal">null</span>);  <span class="comment">// 发送消息触发调度</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 核心工作循环</span></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">workLoop</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">let</span> currentTask = taskQueue[<span class="number">0</span>];</span><br><span class="line">  <span class="keyword">while</span> (currentTask) &#123;</span><br><span class="line">    <span class="keyword">if</span> (<span class="title function_">shouldYieldToHost</span>()) &#123;  <span class="comment">// 判断是否需要让出主线程</span></span><br><span class="line">      <span class="keyword">break</span>;  <span class="comment">// 让出主线程，等待下次调度</span></span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">const</span> callback = currentTask.<span class="property">callback</span>;</span><br><span class="line">    <span class="title function_">callback</span>();</span><br><span class="line">    taskQueue.<span class="title function_">shift</span>();</span><br><span class="line">    currentTask = taskQueue[<span class="number">0</span>];</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">return</span> currentTask !== <span class="literal">null</span>;  <span class="comment">// 是否还有任务</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 判断是否需要让出主线程</span></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">shouldYieldToHost</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> timeElapsed = <span class="title function_">getCurrentTime</span>() - startTime;</span><br><span class="line">  <span class="keyword">return</span> timeElapsed &gt;= <span class="number">5</span>;  <span class="comment">// 默认5ms时间片</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="工作流程"><a href="#工作流程" class="headerlink" title="工作流程"></a>工作流程</h4><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">1. 调用 unstable_scheduleCallback 添加任务</span><br><span class="line">2. 通过 port.postMessage 发送消息</span><br><span class="line">3. 消息被作为宏任务处理，执行 performWorkUntilDeadline</span><br><span class="line">4. 在 workLoop 中执行渲染任务</span><br><span class="line">5. 每执行 5ms 后判断 shouldYieldToHost()</span><br><span class="line">6. 如果需要让出主线程，停止渲染，等待下次调度</span><br></pre></td></tr></table></figure><h3 id="4-4-与-requestAnimationFrame-的关系"><a href="#4-4-与-requestAnimationFrame-的关系" class="headerlink" title="4.4 与 requestAnimationFrame 的关系"></a>4.4 与 requestAnimationFrame 的关系</h3><p>时间切片与浏览器渲染时机的关系：</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">1. 取出宏任务执行</span><br><span class="line">2. 处理微任务队列</span><br><span class="line">3. 执行 requestAnimationFrame 回调</span><br><span class="line">4. 浏览器渲染</span><br><span class="line">5. 执行 requestIdleCallback（空闲时）</span><br><span class="line">6. 重复...</span><br></pre></td></tr></table></figure><p>React 的时间切片就是在步骤 3-5 之间找到执行渲染任务的机会。</p><h2 id="五、并发模式下的渲染流程"><a href="#五、并发模式下的渲染流程" class="headerlink" title="五、并发模式下的渲染流程"></a>五、并发模式下的渲染流程</h2><h3 id="5-1-非并发模式-vs-并发模式"><a href="#5-1-非并发模式-vs-并发模式" class="headerlink" title="5.1 非并发模式 vs 并发模式"></a>5.1 非并发模式 vs 并发模式</h3><p><strong>非并发模式</strong>：</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">用户点击 About -&gt; 渲染 PostsTab -&gt; 渲染 AboutTab -&gt; 完成</span><br><span class="line">                (阻塞等待)     (阻塞等待)</span><br></pre></td></tr></table></figure><p><strong>并发模式</strong>：</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">用户点击 About -&gt; 渲染部分 PostsTab -&gt; 检测到高优先级任务</span><br><span class="line">                -&gt; 中断 -&gt; 渲染 AboutTab -&gt; 完成</span><br><span class="line">                -&gt; 继续渲染剩余 PostsTab</span><br></pre></td></tr></table></figure><h3 id="5-2-React-18-的并发特性"><a href="#5-2-React-18-的并发特性" class="headerlink" title="5.2 React 18 的并发特性"></a>5.2 React 18 的并发特性</h3><p>React 18 的并发机制包含以下特性：</p><ol><li><strong>自动批处理</strong>：多个状态更新自动合并为一次渲染</li><li><strong>useTransition</strong>：标记非紧急更新为”过渡”</li><li><strong>useDeferredValue</strong>：延迟非关键 UI 更新</li><li><strong>Suspense</strong>：优雅处理异步加载</li><li><strong>useId</strong>：生成稳定的唯一 ID</li></ol><h2 id="六、源码分析：完整的调度流程"><a href="#六、源码分析：完整的调度流程" class="headerlink" title="六、源码分析：完整的调度流程"></a>六、源码分析：完整的调度流程</h2><h3 id="6-1-整体架构"><a href="#6-1-整体架构" class="headerlink" title="6.1 整体架构"></a>6.1 整体架构</h3><p>React 18 的调度流程可以分为以下几个层次：</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">用户触发更新</span><br><span class="line">    ↓</span><br><span class="line">调度中心（Scheduler）← Lane 优先级</span><br><span class="line">    ↓</span><br><span class="line">Fiber 协调器（Reconciler）</span><br><span class="line">    ↓</span><br><span class="line">渲染器（Renderer）</span><br></pre></td></tr></table></figure><h3 id="6-2-核心源码解析"><a href="#6-2-核心源码解析" class="headerlink" title="6.2 核心源码解析"></a>6.2 核心源码解析</h3><h4 id="1-状态更新入口"><a href="#1-状态更新入口" class="headerlink" title="1. 状态更新入口"></a>1. 状态更新入口</h4><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="comment">// useState 内部实现简化</span></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">useState</span>(<span class="params">initialState</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> dispatcher = <span class="title function_">resolveDispatcher</span>();</span><br><span class="line">  <span class="keyword">return</span> dispatcher.<span class="title function_">useState</span>(initialState);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">updateState</span>(<span class="params">initialState</span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="title function_">dispatchAction</span>(fiber, queue, action);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="2-调度优先级计算"><a href="#2-调度优先级计算" class="headerlink" title="2. 调度优先级计算"></a>2. 调度优先级计算</h4><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">dispatchAction</span>(<span class="params">fiber, queue, action</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> lane = <span class="title function_">requestUpdateLane</span>(fiber);  <span class="comment">// 根据事件类型获取 Lane</span></span><br><span class="line">  <span class="keyword">const</span> update = &#123;</span><br><span class="line">    lane,</span><br><span class="line">    action,</span><br><span class="line">    <span class="attr">eagerReducer</span>: <span class="literal">null</span>,</span><br><span class="line">    <span class="attr">next</span>: <span class="literal">null</span>,</span><br><span class="line">  &#125;;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 将更新加入队列</span></span><br><span class="line">  <span class="keyword">const</span> root = <span class="title function_">scheduleUpdateOnFiber</span>(fiber, lane);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="3-渲染阶段的让出机制"><a href="#3-渲染阶段的让出机制" class="headerlink" title="3. 渲染阶段的让出机制"></a>3. 渲染阶段的让出机制</h4><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">workLoopConcurrent</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">while</span> (workInProgress !== <span class="literal">null</span> &amp;&amp; !<span class="title function_">shouldYield</span>()) &#123;</span><br><span class="line">    <span class="title function_">performUnitOfWork</span>(workInProgress);</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (workInProgress !== <span class="literal">null</span>) &#123;</span><br><span class="line">    <span class="comment">// 还有工作没完成，让出主线程</span></span><br><span class="line">    <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="6-3-useTransition-的实现"><a href="#6-3-useTransition-的实现" class="headerlink" title="6.3 useTransition 的实现"></a>6.3 useTransition 的实现</h3><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">useTransition</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> dispatcher = <span class="title function_">resolveDispatcher</span>();</span><br><span class="line">  <span class="keyword">return</span> dispatcher.<span class="title function_">useTransition</span>();</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">mountTransition</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> [isPending, setPending] = <span class="title function_">useState</span>(<span class="literal">false</span>);</span><br><span class="line">  <span class="keyword">const</span> <span class="title function_">startTransition</span> = (<span class="params">callback</span>) =&gt; &#123;</span><br><span class="line">    <span class="title function_">setPending</span>(<span class="literal">true</span>);</span><br><span class="line">    <span class="keyword">const</span> prevTransition = <span class="title class_">ReactCurrentBatchConfig</span>.<span class="property">transition</span>;</span><br><span class="line">    <span class="title class_">ReactCurrentBatchConfig</span>.<span class="property">transition</span> = &#123;&#125;;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">      <span class="title function_">callback</span>();  <span class="comment">// 执行低优先级更新</span></span><br><span class="line">      <span class="title function_">setPending</span>(<span class="literal">false</span>);</span><br><span class="line">    &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">      <span class="title class_">ReactCurrentBatchConfig</span>.<span class="property">transition</span> = prevTransition;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> [isPending, startTransition];</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>核心原理：将 <code>callback</code> 的执行标记为<strong>过渡优先级</strong>，允许被高优先级任务中断。</p><h2 id="七、面试简洁版本"><a href="#七、面试简洁版本" class="headerlink" title="七、面试简洁版本"></a>七、面试简洁版本</h2><h3 id="7-1-一句话概括"><a href="#7-1-一句话概括" class="headerlink" title="7.1 一句话概括"></a>7.1 一句话概括</h3><p><strong>React 18 的并发机制通过 Lane 模型分配优先级、时间切片拆分渲染，实现了可中断的渲染能力，让高优先级任务（如用户交互）能够优先响应。</strong></p><h3 id="7-2-核心概念"><a href="#7-2-核心概念" class="headerlink" title="7.2 核心概念"></a>7.2 核心概念</h3><ol><li><strong>Lane 模型</strong>：用二进制位表示渲染优先级，支持高效合并和比较</li><li><strong>时间切片</strong>：将渲染拆分为 5ms 的小片段，执行后让出主线程</li><li><strong>useTransition</strong>：将低优先级更新标记为”过渡”，可被中断</li></ol><h3 id="7-3-常见面试题"><a href="#7-3-常见面试题" class="headerlink" title="7.3 常见面试题"></a>7.3 常见面试题</h3><p><strong>Q1: React 18 并发渲染是什么？</strong></p><p>A: 并发渲染是 React 18 引入的新能力，可以让 React 同时准备多个版本的 UI。它不是并行（同时执行多个），而是可中断的渲染——当有更高优先级的任务时，会暂停当前渲染先去处理高优先级任务。</p><p><strong>Q2: 为什么需要时间切片？</strong></p><p>A: 因为 JS 执行和 UI 渲染都在主线程，之前的渲染是同步且不可中断的，会阻塞页面响应。时间切片将渲染拆分成小片段，每片段执行后让出主线程，让浏览器有机会渲染 UI 和响应用户交互。</p><p><strong>Q3: Lane 模型的优势？</strong></p><p>A: 用二进制表示优先级，可以利用位运算高效地进行合并、比较操作。React 可以根据不同事件（点击 &gt; 滚动 &gt; 渲染）分配不同优先级。</p><p><strong>Q4: useTransition 和 useDeferredValue 的区别？</strong></p><p>A: <code>useTransition</code> 用于状态更新场景，标记某次更新为低优先级；<code>useDeferredValue</code> 用于值变化场景，延迟子组件的渲染更新。两者都是处理”紧急更新”和”慢速更新”竞争的问题。</p><p><strong>Q5: React 18 自动批处理？</strong></p><p>A: React 18 之前只在事件处理函数中自动批处理，Promise、setTimeout 等场景需要手动处理。React 18 默认所有场景都自动批处理，减少不必要的渲染。</p><h3 id="7-4-代码示例"><a href="#7-4-代码示例" class="headerlink" title="7.4 代码示例"></a>7.4 代码示例</h3><figure class="highlight jsx"><table><tr><td class="code"><pre><span class="line"><span class="comment">// 优化前：卡顿</span></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">App</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> [query, setQuery] = <span class="title function_">useState</span>(<span class="string">&#x27;&#x27;</span>);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">input</span> <span class="attr">value</span>=<span class="string">&#123;query&#125;</span> <span class="attr">onChange</span>=<span class="string">&#123;e</span> =&gt;</span> setQuery(e.target.value)&#125; /&gt;</span></span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">Results</span> <span class="attr">query</span>=<span class="string">&#123;query&#125;</span> /&gt;</span></span>  <span class="comment">// 大量数据渲染</span></span><br><span class="line">  );</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 优化后：使用 useTransition</span></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">App</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> [query, setQuery] = <span class="title function_">useState</span>(<span class="string">&#x27;&#x27;</span>);</span><br><span class="line">  <span class="keyword">const</span> [isPending, startTransition] = <span class="title function_">useTransition</span>();</span><br><span class="line"></span><br><span class="line">  <span class="keyword">function</span> <span class="title function_">handleChange</span>(<span class="params">e</span>) &#123;</span><br><span class="line">    <span class="title function_">startTransition</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">      <span class="title function_">setQuery</span>(e.<span class="property">target</span>.<span class="property">value</span>);  <span class="comment">// 低优先级，可中断</span></span><br><span class="line">    &#125;);</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">input</span> <span class="attr">value</span>=<span class="string">&#123;query&#125;</span> <span class="attr">onChange</span>=<span class="string">&#123;handleChange&#125;</span> /&gt;</span></span></span><br><span class="line">    &#123;isPending ? <span class="language-xml"><span class="tag">&lt;<span class="name">Loading</span> /&gt;</span></span> : <span class="language-xml"><span class="tag">&lt;<span class="name">Results</span> <span class="attr">query</span>=<span class="string">&#123;query&#125;</span> /&gt;</span></span>&#125;</span><br><span class="line">  );</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>React 18 并发机制的核心在于：</p><ol><li><strong>Lane 模型</strong>：用二进制位运算高效管理渲染优先级</li><li><strong>时间切片</strong>：基于 MessageChannel 实现可中断渲染</li><li><strong>useTransition</strong>：让开发者控制哪些更新可以被打断</li></ol><p>这套机制解决了 React 长年被诟病的”渲染阻塞交互”问题，让应用能够根据用户设备的性能动态调整，提供更流畅的用户体验。</p><p>理解并发机制，对于深入掌握 React 架构和应对面试都至关重要。</p>]]>
    </content>
    <id>http://fe.poetries.top/2024/12/28/react-18-concurrency/</id>
    <link href="http://fe.poetries.top/2024/12/28/react-18-concurrency/"/>
    <published>2024-12-28T09:00:00.000Z</published>
    <summary>深入解析React 18并发渲染机制，包含Lane模型、时间切片、源码分析，以及面试必备的简洁版本。帮你彻底理解React 18如何实现可中断渲染。</summary>
    <title>React 18 并发机制深度解析</title>
    <updated>2026-03-08T10:22:42.093Z</updated>
  </entry>
  <entry>
    <author>
      <name>Poetry</name>
    </author>
    <category term="Front-End" scheme="http://fe.poetries.top/categories/Front-End/"/>
    <category term="react" scheme="http://fe.poetries.top/tags/react/"/>
    <category term="RN" scheme="http://fe.poetries.top/tags/RN/"/>
    <content>
      <![CDATA[<h2 id="配置development测试证书"><a href="#配置development测试证书" class="headerlink" title="配置development测试证书"></a>配置development测试证书</h2><blockquote><p>使用<code>xcode</code>来管理自动生成证书，不需要在管理后台创建</p></blockquote><h3 id="1、创建Identifiers"><a href="#1、创建Identifiers" class="headerlink" title="1、创建Identifiers"></a>1、创建Identifiers</h3><blockquote><p>登录 <a href="https://developer.apple.com/account">https://developer.apple.com/account</a></p></blockquote><p><img src="https://s.poetries.top/uploads/2023/10/d3d19228ec48dfe2.png"></p><p><img src="https://s.poetries.top/uploads/2023/10/19657900fcb37bef.png"></p><p><img src="https://s.poetries.top/uploads/2023/10/9348e21eb02eae74.png"></p><p><img src="https://s.poetries.top/uploads/2023/10/4c8cddc046e42324.png"></p><p><img src="https://s.poetries.top/uploads/2023/10/68e759bd80a6b7d0.png"></p><p><img src="https://s.poetries.top/uploads/2023/10/1c453d7171b4d71f.png"></p><h3 id="2、在Xcode端自动生成证书，不需要在管理后台添加证书"><a href="#2、在Xcode端自动生成证书，不需要在管理后台添加证书" class="headerlink" title="2、在Xcode端自动生成证书，不需要在管理后台添加证书"></a>2、在Xcode端自动生成证书，不需要在管理后台添加证书</h3><p><img src="https://s.poetries.top/uploads/2023/10/864c4d1828d36193.png"></p><p><img src="https://s.poetries.top/uploads/2023/10/7ff0853d6763d1e7.png"></p><blockquote><p>然后到后面就看到自动创建的证书了</p></blockquote><p><img src="https://s.poetries.top/uploads/2023/10/dae17eae4f107800.png"></p><h3 id="3、创建Profiles"><a href="#3、创建Profiles" class="headerlink" title="3、创建Profiles"></a>3、创建Profiles</h3><p><img src="https://s.poetries.top/uploads/2023/10/43fdac62f1c4ebea.png"></p><p><img src="https://s.poetries.top/uploads/2023/10/08dbd8f12c57cd87.png"></p><p><img src="https://s.poetries.top/uploads/2023/10/05aab8ad8141cbab.png"></p><p><img src="https://s.poetries.top/uploads/2023/10/543f027a5d7c2043.png"></p><p><img src="https://s.poetries.top/uploads/2023/10/6232eacf27532221.png"></p><p><img src="https://s.poetries.top/uploads/2023/10/31b7f6ff29a2d551.png"></p><h2 id="配置distribution正式环境证书"><a href="#配置distribution正式环境证书" class="headerlink" title="配置distribution正式环境证书"></a>配置distribution正式环境证书</h2><h3 id="1、生成IOS正式环境证书"><a href="#1、生成IOS正式环境证书" class="headerlink" title="1、生成IOS正式环境证书"></a>1、生成IOS正式环境证书</h3><p><img src="https://s.poetries.top/uploads/2023/10/25717985c486ad13.png"></p><h3 id="2、创建Profiles"><a href="#2、创建Profiles" class="headerlink" title="2、创建Profiles"></a>2、创建Profiles</h3><p><img src="https://s.poetries.top/uploads/2023/10/e7815063c626ab41.png"></p><p><img src="https://s.poetries.top/uploads/2023/10/5df2093eb4cf289f.png"></p><p><img src="https://s.poetries.top/uploads/2023/10/32a4e9928c21e29d.png"></p><p><img src="https://s.poetries.top/uploads/2023/10/09602f7d4539b5df.png"></p><h2 id="配置Xcode"><a href="#配置Xcode" class="headerlink" title="配置Xcode"></a>配置Xcode</h2><p><img src="https://s.poetries.top/uploads/2023/10/55b307b2e260aee8.png"><br><img src="https://s.poetries.top/uploads/2023/10/a8e531d89de997aa.png"></p><h2 id="打测试包发布到蒲公英测试"><a href="#打测试包发布到蒲公英测试" class="headerlink" title="打测试包发布到蒲公英测试"></a>打测试包发布到蒲公英测试</h2><p><img src="https://s.poetries.top/uploads/2023/10/7c25b7fdd2cf1a4f.png"></p><p><img src="https://s.poetries.top/uploads/2023/10/979bfb42f2d823c6.png"></p><p><img src="https://s.poetries.top/uploads/2023/10/446e877b181200e4.png"></p><p><img src="https://s.poetries.top/uploads/2023/10/c0ac19effeff23bb.png"></p><p><img src="https://s.poetries.top/uploads/2023/10/7a9d940024ec8cad.png"></p><p><img src="https://s.poetries.top/uploads/2023/10/e4b33263233d8db8.png"></p><p><img src="https://s.poetries.top/uploads/2023/10/305c22c3e7b190b9.png"></p><blockquote><p>上传到蒲公英内测 <a href="https://www.pgyer.com/">https://www.pgyer.com</a></p></blockquote><p><img src="https://s.poetries.top/uploads/2023/10/b15ca30cd01b19fd.png"></p><p><img src="https://s.poetries.top/uploads/2023/10/b2929e57f5f970ab.png"></p><p><img src="https://s.poetries.top/uploads/2023/10/a99d12324dd8a872.png"></p><p>最后把应用地址复制发给测试人员下载即可，需要注意的是我们要添加测试手机的设备UUID才可以安装到手机上测试</p><h3 id="添加手机UDID到管理后台"><a href="#添加手机UDID到管理后台" class="headerlink" title="添加手机UDID到管理后台"></a>添加手机UDID到管理后台</h3><p><img src="https://s.poetries.top/uploads/2023/10/78653128e1967b61.png"></p><p><img src="https://s.poetries.top/uploads/2023/10/6e003a3a0a8ef496.png"></p><h3 id="通过蒲公英获取UDID填入即可"><a href="#通过蒲公英获取UDID填入即可" class="headerlink" title="通过蒲公英获取UDID填入即可"></a>通过蒲公英获取UDID填入即可</h3><blockquote><p>地址：<a href="https://www.pgyer.com/tools/udid/manage">https://www.pgyer.com/tools/udid/manage</a></p></blockquote><p><img src="https://s.poetries.top/uploads/2023/10/d91b1123872f2ca9.png"></p><blockquote><p>需要注意的是：设备添加超过<code>20个</code>，需要等待<code>24小时</code>才能在iPhone手机上安装APP测试。<code>添加新的额设备udid，需要重新打包ipa包才能进行安装到对应手机上</code></p></blockquote><h2 id="打正式包上传到appstore"><a href="#打正式包上传到appstore" class="headerlink" title="打正式包上传到appstore"></a>打正式包上传到appstore</h2><h3 id="进入App-Store-Connect创建应用才可以上传"><a href="#进入App-Store-Connect创建应用才可以上传" class="headerlink" title="进入App Store Connect创建应用才可以上传"></a>进入App Store Connect创建应用才可以上传</h3><blockquote><p>入口：<a href="https://developer.apple.com/account">https://developer.apple.com/account</a></p></blockquote><p><img src="https://s.poetries.top/uploads/2023/10/098ffcfe3382883e.png"></p><p><img src="https://s.poetries.top/uploads/2023/10/ad192fc1a02051d0.png"></p><p><img src="https://s.poetries.top/uploads/2023/10/aa4f08b7d0b6b23d.png"></p><p><img src="https://s.poetries.top/uploads/2023/10/2e2f5d4a13f43e57.png"></p><h3 id="开始上传到App-Store-Connect"><a href="#开始上传到App-Store-Connect" class="headerlink" title="开始上传到App Store Connect"></a>开始上传到App Store Connect</h3><p><img src="https://s.poetries.top/uploads/2023/10/cf408e3029539dc9.png"></p><p><img src="https://s.poetries.top/uploads/2023/10/2a74082d89e946e8.png"></p><p><img src="https://s.poetries.top/uploads/2023/10/64dfe89783c717ed.png"></p><blockquote><p>上传成功后，来到app 后台</p></blockquote><p><img src="https://s.poetries.top/uploads/2023/10/cd1a10fc38b5c201.png"></p><p><img src="https://s.poetries.top/uploads/2023/10/c5d55c40962f74ec.png"></p><h3 id="如果审核失败了，重新构建版本号需要修改"><a href="#如果审核失败了，重新构建版本号需要修改" class="headerlink" title="如果审核失败了，重新构建版本号需要修改"></a>如果审核失败了，重新构建版本号需要修改</h3><blockquote><p>打包后，在app store后台选择最近的构建版本</p></blockquote><p><img src="https://s.poetries.top/uploads/2023/10/7fc6ff00ac395145.png"></p><h3 id="需要注意的是权限提示信息需要修改，否则会被拒审"><a href="#需要注意的是权限提示信息需要修改，否则会被拒审" class="headerlink" title="需要注意的是权限提示信息需要修改，否则会被拒审"></a>需要注意的是权限提示信息需要修改，否则会被拒审</h3><p><img src="https://s.poetries.top/uploads/2023/10/c4bfcbf056cf7932.png"></p>]]>
    </content>
    <id>http://fe.poetries.top/2024/12/22/ios-build/</id>
    <link href="http://fe.poetries.top/2024/12/22/ios-build/"/>
    <published>2024-12-22T10:10:12.000Z</published>
    <summary>
      <![CDATA[<h2 id="配置development测试证书"><a href="#配置development测试证书" class="headerlink" title="配置development测试证书"></a>配置development测试证书</h2><blockquote>]]>
    </summary>
    <title>RN构建iOS包发布到AppStore总结篇</title>
    <updated>2026-03-08T10:22:42.082Z</updated>
  </entry>
</feed>
