<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <author>
    <name>紫升</name>
  </author>
  <generator uri="https://hexo.io/">Hexo</generator>
  <id>https://blog.zisheng.pro/</id>
  <link href="https://blog.zisheng.pro/" rel="alternate"/>
  <link href="https://blog.zisheng.pro/atom.xml" rel="self"/>
  <rights>All rights reserved 2026, 紫升</rights>
  <subtitle>怕什么真理无穷，进一寸有一寸的欢喜。</subtitle>
  <title>紫升的博客</title>
  <updated>2026-09-18T08:15:28.629Z</updated>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="软件工程" scheme="https://blog.zisheng.pro/categories/%E8%BD%AF%E4%BB%B6%E5%B7%A5%E7%A8%8B/"/>
    <category term="HyperFrames" scheme="https://blog.zisheng.pro/tags/HyperFrames/"/>
    <category term="HTML" scheme="https://blog.zisheng.pro/tags/HTML/"/>
    <category term="Video Rendering" scheme="https://blog.zisheng.pro/tags/Video-Rendering/"/>
    <category term="GSAP" scheme="https://blog.zisheng.pro/tags/GSAP/"/>
    <category term="Deterministic Rendering" scheme="https://blog.zisheng.pro/tags/Deterministic-Rendering/"/>
    <content>
      <![CDATA[<p>最近用 HyperFrames 做视频时，我发现最容易误解它的方式，是把它看成“带时间轴的网页截图器”。这个比喻能解释结果，却解释不了关键约束：为什么媒体播放不应该交给 GSAP，为什么 <code>data-duration</code> 和动画 timeline 不是一回事，为什么同样一段 HTML 在预览里看起来正常，渲染时却可能从错误的帧开始。</p><p>HyperFrames 真正解决的问题，是把视频重新表达成一种可寻址的网页状态：HTML 声明画面由哪些 clip 组成、每个 clip 在什么时候出现、位于哪一层；动画 Runtime 根据当前时间计算 DOM 状态；框架负责把媒体 seek 到对应位置，再捕获确定的 frame。这个边界一旦清楚，很多“玄学”都会变成可以检查的 contract。</p><span id="more"></span><h2 id="一句话总结"><a href="#一句话总结" class="headerlink" title="一句话总结"></a>一句话总结</h2><p><strong>HyperFrames 的核心不是“用 HTML 画视频”，而是“用 HTML 声明时间，用 seekable Runtime 计算状态，用框架统一接管媒体播放”。</strong></p><p>一个可渲染的 composition 至少包含四类信息：</p><ol><li>画布身份与尺寸：<code>data-composition-id</code>、<code>data-width</code>、<code>data-height</code>。</li><li>时间结构：<code>data-start</code>、<code>data-duration</code>、<code>data-track-index</code>。</li><li>视觉状态：普通 DOM、SVG、Canvas 或动画 Runtime 产生的可复现状态。</li><li>媒体边界：<code>video</code>、<code>audio</code> 的源文件、起始偏移、音量和裁剪窗口。</li></ol><p>渲染器在时间 <code>t</code> 上做的事情，可以简化成：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">HTML contract + t</span><br><span class="line">  → 解析当前 composition 与 clip</span><br><span class="line">  → 将 video/audio seek 到 t 对应的媒体时间</span><br><span class="line">  → 将动画 timeline seek 到 t</span><br><span class="line">  → 捕获当前 DOM / Canvas / Audio 状态</span><br><span class="line">  → 输出一帧</span><br></pre></td></tr></table></figure><p>这条链路决定了 HyperFrames 的工程重点：每一帧都必须能够从时间和输入稳定推导出来，而不能依赖“刚才播放到了哪里”。</p><h2 id="先分清三个对象：composition、clip-和-timeline"><a href="#先分清三个对象：composition、clip-和-timeline" class="headerlink" title="先分清三个对象：composition、clip 和 timeline"></a>先分清三个对象：composition、clip 和 timeline</h2><p>HyperFrames 的概念不多，但每个概念的责任边界很窄。把它们混在一起，代码通常还能在浏览器里动起来，到了逐帧渲染就会暴露问题。</p><table><thead><tr><th>对象</th><th>负责什么</th><th>不负责什么</th></tr></thead><tbody><tr><td>composition</td><td>定义一张画布、总时长、嵌套关系和变量入口</td><td>不直接决定每个媒体的解码位置</td></tr><tr><td>clip</td><td>描述一个媒体或视觉单元何时出现、持续多久、在哪个 track</td><td>不负责动画曲线和 DOM 状态变化</td></tr><tr><td>timeline</td><td>在给定时间计算元素的属性、路径、透明度或变换</td><td>不接管媒体播放和 clip 的可见性</td></tr></tbody></table><p>composition 是可组合的时间容器。最外层通常是一个带有 <code>data-composition-id</code> 的根节点；子 composition 通过 <code>data-composition-src</code> 加载，再由框架把自己的局部时间映射到父级时间。</p><p>clip 是时间轴上的离散单元。一个最小的视频片段可以这样写：</p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">video</span></span></span><br><span class="line"><span class="tag">  <span class="attr">id</span>=<span class="string">&quot;intro&quot;</span></span></span><br><span class="line"><span class="tag">  <span class="attr">data-start</span>=<span class="string">&quot;0&quot;</span></span></span><br><span class="line"><span class="tag">  <span class="attr">data-duration</span>=<span class="string">&quot;4&quot;</span></span></span><br><span class="line"><span class="tag">  <span class="attr">data-track-index</span>=<span class="string">&quot;0&quot;</span></span></span><br><span class="line"><span class="tag">  <span class="attr">src</span>=<span class="string">&quot;./assets/intro.mp4&quot;</span></span></span><br><span class="line"><span class="tag">&gt;</span><span class="tag">&lt;/<span class="name">video</span>&gt;</span></span><br></pre></td></tr></table></figure><p><code>data-start</code> 和 <code>data-duration</code> 描述的是“它占据时间线的哪一段”，不是 CSS 动画的起止值。<code>data-track-index</code> 主要表达视觉层级和 Studio 中的轨道归属；它不是一个“同轨道绝对不能重叠”的播放锁。</p><p>timeline 则处理另一件事：在当前时间点，标题应该位于哪里，遮罩应该有多大，光晕应该有多亮。以 GSAP 为例，HyperFrames 要求 composition 注册一个 paused timeline：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="variable language_">window</span>.<span class="property">__timelines</span>[<span class="string">&#x27;intro&#x27;</span>] = gsap.<span class="title function_">timeline</span>(&#123; <span class="attr">paused</span>: <span class="literal">true</span> &#125;)</span><br><span class="line">  .<span class="title function_">fromTo</span>(<span class="string">&#x27;#title&#x27;</span>, &#123; <span class="attr">x</span>: -<span class="number">48</span>, <span class="attr">opacity</span>: <span class="number">0</span> &#125;, &#123;</span><br><span class="line">    <span class="attr">x</span>: <span class="number">0</span>,</span><br><span class="line">    <span class="attr">opacity</span>: <span class="number">1</span>,</span><br><span class="line">    <span class="attr">duration</span>: <span class="number">0.8</span>,</span><br><span class="line">    <span class="attr">ease</span>: <span class="string">&#x27;power2.out&#x27;</span>,</span><br><span class="line">  &#125;);</span><br></pre></td></tr></table></figure><p>这里的关键字是 <code>paused: true</code>。播放循环不由 GSAP 自己推进，而由框架在渲染第 <code>n</code> 帧时把 timeline 定位到对应时间。这样，渲染第 300 帧和从头播放到第 300 帧，应该得到同一个视觉状态。</p><h2 id="HTML-为什么适合做视频的源代码"><a href="#HTML-为什么适合做视频的源代码" class="headerlink" title="HTML 为什么适合做视频的源代码"></a>HTML 为什么适合做视频的源代码</h2><p>HTML 的优势不在于它比专业剪辑软件“更强”，而在于它能把视频里常被藏在时间轴 UI 里的信息，变成可检查的文本结构。</p><p>例如下面的片段同时表达了三个事实：主画面从第 4 秒开始，片尾紧接着主画面，字幕和主画面在两个视觉层级上叠加。</p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">div</span></span></span><br><span class="line"><span class="tag">  <span class="attr">data-composition-id</span>=<span class="string">&quot;demo&quot;</span></span></span><br><span class="line"><span class="tag">  <span class="attr">data-width</span>=<span class="string">&quot;1920&quot;</span></span></span><br><span class="line"><span class="tag">  <span class="attr">data-height</span>=<span class="string">&quot;1080&quot;</span></span></span><br><span class="line"><span class="tag">  <span class="attr">data-duration</span>=<span class="string">&quot;12&quot;</span></span></span><br><span class="line"><span class="tag">&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">video</span> <span class="attr">id</span>=<span class="string">&quot;main&quot;</span> <span class="attr">data-start</span>=<span class="string">&quot;0&quot;</span> <span class="attr">data-duration</span>=<span class="string">&quot;8&quot;</span> <span class="attr">data-track-index</span>=<span class="string">&quot;0&quot;</span> <span class="attr">src</span>=<span class="string">&quot;main.mp4&quot;</span>&gt;</span><span class="tag">&lt;/<span class="name">video</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">img</span> <span class="attr">id</span>=<span class="string">&quot;outro&quot;</span> <span class="attr">data-start</span>=<span class="string">&quot;main&quot;</span> <span class="attr">data-duration</span>=<span class="string">&quot;4&quot;</span> <span class="attr">data-track-index</span>=<span class="string">&quot;0&quot;</span> <span class="attr">src</span>=<span class="string">&quot;outro.png&quot;</span> /&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">div</span> <span class="attr">id</span>=<span class="string">&quot;caption&quot;</span> <span class="attr">class</span>=<span class="string">&quot;clip&quot;</span> <span class="attr">data-start</span>=<span class="string">&quot;main&quot;</span> <span class="attr">data-duration</span>=<span class="string">&quot;4&quot;</span> <span class="attr">data-track-index</span>=<span class="string">&quot;1&quot;</span>&gt;</span></span><br><span class="line">    See you next time</span><br><span class="line">  <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br></pre></td></tr></table></figure><p><code>data-start=&quot;main&quot;</code> 是相对时间引用：<code>outro</code> 在 <code>main</code> 结束时开始。它比手写 <code>8</code> 更接近编辑意图，也减少了前面片段改时长后留下的魔法数字。</p><p>但“声明式”不等于“所有东西都写成属性”。HTML 负责描述结构和媒体窗口，脚本负责画面内部的连续变化。这个分工让检查器可以分别回答两个问题：时间轴是否完整，动画是否可以被 seek。</p><h2 id="最关键的原理：seek，而不是播放"><a href="#最关键的原理：seek，而不是播放" class="headerlink" title="最关键的原理：seek，而不是播放"></a>最关键的原理：seek，而不是播放</h2><p>普通网页动画通常依赖 <code>requestAnimationFrame</code>：浏览器上一帧结束后，下一帧再推进一点时间。视频渲染需要反过来：给定目标时间，系统直接跳到目标状态。</p><p>这会带来三个约束。</p><h3 id="不能读取真实时钟"><a href="#不能读取真实时钟" class="headerlink" title="不能读取真实时钟"></a>不能读取真实时钟</h3><p>以下写法在浏览器里很自然，在逐帧渲染里却没有稳定含义：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> drift = performance.<span class="title function_">now</span>() % <span class="number">1000</span>;</span><br><span class="line">el.<span class="property">style</span>.<span class="property">transform</span> = <span class="string">`translateX(<span class="subst">$&#123;drift&#125;</span>px)`</span>;</span><br></pre></td></tr></table></figure><p>同一帧在不同机器、不同负载下会得到不同结果。渲染器需要的是 <code>state = f(input, time)</code>，而不是 <code>state = f(input, wallClock)</code>。</p><h3 id="随机数必须可复现"><a href="#随机数必须可复现" class="headerlink" title="随机数必须可复现"></a>随机数必须可复现</h3><p>粒子、噪声和装饰可以使用随机分布，但随机种子必须固定，或者在初始化时根据稳定输入生成。未设种子的 <code>Math.random()</code> 会让截图对比、失败重试和缓存失去意义。</p><h3 id="无限循环必须有边界"><a href="#无限循环必须有边界" class="headerlink" title="无限循环必须有边界"></a>无限循环必须有边界</h3><p>一个永不结束的 CSS 或 GSAP 循环无法告诉渲染器总时长。需要明确的 <code>data-duration</code> 或有限的重复次数；“看起来一直动”不等于“渲染器知道渲染到哪里”。</p><p>所以 HyperFrames 的动画不是“播放一段 tween”，而是“注册一套时间到状态的映射”。这也是为什么一个动画 timeline 可以早于 composition 结束，也可以超过 composition 时长：最终输出长度由 composition 的 duration contract 决定，timeline 超出的部分会被截断，timeline 提前结束则保持最后状态。</p><h2 id="媒体为什么必须由框架接管"><a href="#媒体为什么必须由框架接管" class="headerlink" title="媒体为什么必须由框架接管"></a>媒体为什么必须由框架接管</h2><p><code>video.currentTime</code> 和 <code>audio.currentTime</code> 看起来也是普通 DOM 属性，但媒体解码有自己的缓冲、关键帧和异步状态。若让业务脚本自行播放媒体，截图时可能出现“DOM 已经到第 5 秒，视频还停在第 4.8 秒”的竞态。</p><p>HyperFrames 的做法是把媒体当作有时间窗口的资源：</p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">video</span></span></span><br><span class="line"><span class="tag">  <span class="attr">id</span>=<span class="string">&quot;broll&quot;</span></span></span><br><span class="line"><span class="tag">  <span class="attr">data-start</span>=<span class="string">&quot;2&quot;</span></span></span><br><span class="line"><span class="tag">  <span class="attr">data-duration</span>=<span class="string">&quot;5&quot;</span></span></span><br><span class="line"><span class="tag">  <span class="attr">data-media-start</span>=<span class="string">&quot;13.5&quot;</span></span></span><br><span class="line"><span class="tag">  <span class="attr">src</span>=<span class="string">&quot;broll.mp4&quot;</span></span></span><br><span class="line"><span class="tag">&gt;</span><span class="tag">&lt;/<span class="name">video</span>&gt;</span></span><br></pre></td></tr></table></figure><p>这里的 <code>data-start=&quot;2&quot;</code> 表示它在 composition 的第 2 秒出现；<code>data-media-start=&quot;13.5&quot;</code> 表示出现时从源文件的 13.5 秒开始读。两者分别属于“时间线坐标”和“源媒体坐标”，不能混用。</p><p>这也解释了一个常见 lint 错误：不要给 <code>video</code> 和它的 timed ancestor 同时设置 <code>data-start</code>。如果外层 wrapper 已经移动了时间窗口，内层 video 再移动一次，渲染器在计算媒体起始位置和可见性时会得到两套不一致的坐标。</p><p>音频还有一个容易被忽略的 contract：每个 <code>&lt;audio&gt;</code> 都应有唯一 <code>id</code>。没有 id 的音频可能无法被 mixer 发现，最终画面正常但输出无声。这类问题不是“音频文件坏了”，而是资源没有进入框架的媒体管理边界。</p><h2 id="两层结构：声明层和表现层"><a href="#两层结构：声明层和表现层" class="headerlink" title="两层结构：声明层和表现层"></a>两层结构：声明层和表现层</h2><p>我更愿意把 HyperFrames composition 看成两层：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">声明层：HTML + data-* attributes</span><br><span class="line">  解决“有什么、何时出现、从源文件哪里开始”</span><br><span class="line"></span><br><span class="line">表现层：CSS / SVG / Canvas / GSAP / WebGL</span><br><span class="line">  解决“当前时刻长什么样”</span><br></pre></td></tr></table></figure><p>声明层应该尽量简单、稳定、可读。表现层可以复杂，但必须满足 seek-safe 和 deterministic 的要求。</p><p>例如，淡出字幕时应该动画字幕内部节点的 <code>opacity</code>，而不是用 GSAP 把 <code>.clip</code> 自己设置成 <code>visibility: hidden</code>。clip 的可见性由时间窗口负责；Runtime 需要知道它什么时候仍然存在，才能正确组合轨道和子 composition。</p><p>同理，CSS 的初始 <code>transform</code> 和 GSAP 对同一属性的 tween 也不应互相竞争。更稳妥的写法是把初始状态写进 <code>fromTo</code>，让唯一的动画 Runtime 成为状态来源：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">gsap.<span class="title function_">fromTo</span>(</span><br><span class="line">  <span class="string">&#x27;#card&#x27;</span>,</span><br><span class="line">  &#123; <span class="attr">x</span>: -<span class="number">40</span>, <span class="attr">opacity</span>: <span class="number">0</span> &#125;,</span><br><span class="line">  &#123; <span class="attr">x</span>: <span class="number">0</span>, <span class="attr">opacity</span>: <span class="number">1</span>, <span class="attr">duration</span>: <span class="number">0.6</span> &#125;,</span><br><span class="line">);</span><br></pre></td></tr></table></figure><p>这不是语法偏好，而是为了避免“浏览器首次布局值”和“时间轴首帧值”在不同阶段互相覆盖。</p><h2 id="子-composition-解决的是复用，不是逃避时间"><a href="#子-composition-解决的是复用，不是逃避时间" class="headerlink" title="子 composition 解决的是复用，不是逃避时间"></a>子 composition 解决的是复用，不是逃避时间</h2><p>当一个视频由片头、主体、解释卡片和片尾组成时，把所有 DOM 塞进一个 HTML 文件很快会失去边界。子 composition 可以把每个场景封装成独立模板，再由父级按时间组合。</p><p>但子 composition 仍然必须是完整的时间函数。父级传入实例变量、素材或起始时间后，子级的 timeline 仍然要能在任意目标时间被直接求值。不能假设“父级一定从 0 秒开始顺序播放”，否则单独预览、拖动时间轴和并行渲染都会产生差异。</p><p>一个实用的拆分标准是：如果场景有自己的素材、变量、动效和验收条件，就值得成为 composition；如果只是同一场景里的一块装饰，不要为了文件数量而拆分。</p><h2 id="从原理推导出的验证顺序"><a href="#从原理推导出的验证顺序" class="headerlink" title="从原理推导出的验证顺序"></a>从原理推导出的验证顺序</h2><p>HyperFrames 项目的验证应该沿着渲染链路进行，而不是只看页面能否打开。</p><ol><li><strong>先查结构。</strong> 根节点是否有唯一的 <code>data-composition-id</code>、尺寸和可推导的总时长？所有媒体是否有 id？</li><li><strong>再查时间。</strong> clip 的 <code>data-start</code>、<code>data-duration</code>、相对引用和嵌套偏移是否符合预期？是否给媒体套了重复的 timed wrapper？</li><li><strong>再查 Runtime。</strong> <code>window.__timelines[id]</code> 是否与 composition id 一致？timeline 是否 paused、有限、可 seek？</li><li><strong>再查视觉状态。</strong> 在 0%、50%、100% 等中间点做 snapshot，而不是只看首帧和末帧。</li><li><strong>最后查媒体与声音。</strong> 视频是否从正确的 <code>data-media-start</code> 开始，音频是否被 mixer 发现，轨道叠加和音量是否符合预期。</li></ol><p>常用命令可以概括为：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">npx hyperframes lint</span><br><span class="line">npx hyperframes check</span><br><span class="line">npx hyperframes snapshot --at 0,50%,100%</span><br><span class="line">npx hyperframes render --output output.mp4</span><br></pre></td></tr></table></figure><p><code>check</code> 通过只说明当前 contract 和审计项没有发现问题，不等于已经完成完整的观感验收。真正交付前仍应检查代表性帧、完整播放、字体加载、媒体源和输出音轨。</p><h2 id="三个看似合理、实际危险的做法"><a href="#三个看似合理、实际危险的做法" class="headerlink" title="三个看似合理、实际危险的做法"></a>三个看似合理、实际危险的做法</h2><h3 id="把-HyperFrames-当成录屏器"><a href="#把-HyperFrames-当成录屏器" class="headerlink" title="把 HyperFrames 当成录屏器"></a>把 HyperFrames 当成录屏器</h3><p>录屏只关心屏幕此刻显示什么；HyperFrames 需要知道每个元素为什么在这一帧出现。把所有动画塞进 <code>setInterval</code> 或依赖用户拖动，短期能出片，长期无法复现。</p><p><strong>修复方向：</strong> 把时间窗口写进 <code>data-*</code>，把连续变化写进可 seek 的 Runtime。</p><h3 id="用-timeline-控制视频播放"><a href="#用-timeline-控制视频播放" class="headerlink" title="用 timeline 控制视频播放"></a>用 timeline 控制视频播放</h3><p>timeline 适合控制 DOM 属性，不适合替代框架对媒体时间的统一管理。视频的源时间、关键帧和音画同步有自己的 contract。</p><p><strong>修复方向：</strong> 用 <code>data-media-start</code> 和 <code>data-duration</code> 描述媒体窗口，让 timeline 只做画面表现。</p><h3 id="只验证“浏览器里看起来对”"><a href="#只验证“浏览器里看起来对”" class="headerlink" title="只验证“浏览器里看起来对”"></a>只验证“浏览器里看起来对”</h3><p>交互式预览允许很多隐式状态存在：字体已经缓存、视频刚好播放到某个位置、上一段动画留下了 inline style。逐帧渲染会把这些隐式依赖暴露出来。</p><p><strong>修复方向：</strong> 使用 lint、check、snapshot 和最终 render 组成验证链，并保留代表性帧作为回归样本。</p><h2 id="要不要用-我的判断框架"><a href="#要不要用-我的判断框架" class="headerlink" title="要不要用 &#x2F; 我的判断框架"></a>要不要用 &#x2F; 我的判断框架</h2><p>值得上：视频画面本身就是 UI、网页、代码、图表、字幕或可复用组件；团队希望用 Git 管理时间线，并且需要精确到帧的渲染、批量生成或自动化验证。</p><p>可以再等：任务只是一次简单剪辑，素材和转场已经在成熟的 NLE 中完成，用 HTML 表达反而增加工程成本。</p><p>重点关注：不要先问“HyperFrames 能不能替代剪辑软件”，先问项目是否需要<strong>可组合、可寻址、可复现的画面状态</strong>。如果答案是肯定的，HTML 就不再只是展示层，而可以成为视频的 source of truth；如果答案是否定的，继续使用传统时间线工具通常更直接。</p><p>HyperFrames 最值得借鉴的地方，也许不是某个 CLI 命令，而是这条边界：<strong>把结构、时间、媒体和表现分别交给最适合它们的系统，再用确定性渲染把四者重新合成一帧。</strong></p><p>参考资料：</p><ul><li><a href="https://github.com/heygen-com/hyperframes/blob/main/packages/core/docs/core.md">HyperFrames Core Documentation</a></li><li><a href="https://github.com/heygen-com/hyperframes/blob/main/skills/hyperframes-core/SKILL.md">HyperFrames Core Skill</a></li><li><a href="https://github.com/heygen-com/hyperframes/blob/main/skills/hyperframes-core/references/data-attributes.md">Data Attributes Reference</a></li></ul><blockquote><p>本文使用 <a href="https://github.com/zisheng-ai/writting-skill">writting-skill</a> 辅助写作。项目已开源，欢迎在 GitHub 点个 Star。</p></blockquote>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/1c27b698d23b/</id>
    <link href="https://blog.zisheng.pro/posts/1c27b698d23b/"/>
    <published>2026-09-18T08:15:00.000Z</published>
    <summary>HyperFrames 不是把网页录成视频的截图工具，而是一套把 HTML、媒体时间、可寻址动画和确定性渲染接在一起的运行时。本文从 composition、clip、timeline 和 media playback 四个边界出发，解释它为什么能从 HTML 生成可编辑、可复现的视频，以及哪些写法看似合理却会破坏渲染结果。</summary>
    <title>HyperFrames 的原理：让 HTML 成为可寻址的视频时间线</title>
    <updated>2026-09-18T08:15:28.629Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="软件工程" scheme="https://blog.zisheng.pro/categories/%E8%BD%AF%E4%BB%B6%E5%B7%A5%E7%A8%8B/"/>
    <category term="Node.js" scheme="https://blog.zisheng.pro/tags/Node-js/"/>
    <category term="Bearer Token" scheme="https://blog.zisheng.pro/tags/Bearer-Token/"/>
    <category term="OAuth 2.0" scheme="https://blog.zisheng.pro/tags/OAuth-2-0/"/>
    <category term="HTTP" scheme="https://blog.zisheng.pro/tags/HTTP/"/>
    <category term="JWT" scheme="https://blog.zisheng.pro/tags/JWT/"/>
    <category term="API Security" scheme="https://blog.zisheng.pro/tags/API-Security/"/>
    <content>
      <![CDATA[<p>最近在给桌面 Agent 接入 API 时，我遇到一个很基础、也很容易混淆的问题：请求头里写的 <code>Bearer</code> 到底叫什么，它是在证明用户身份，还是只是在携带一段 Token？</p><p>标准写法是：</p><figure class="highlight http"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attribute">Authorization</span><span class="punctuation">: </span>Bearer &lt;access_token&gt;</span><br></pre></td></tr></table></figure><p>这里没有连字符。<code>Bearer</code> 后面必须是空格，再跟 Access Token。它对应的标准名称是 <strong>Bearer Token Authentication</strong>，更准确地说，是 HTTP <code>Authorization</code> Header 中的 Bearer Authentication Scheme。</p><span id="more"></span><h2 id="一句话总结"><a href="#一句话总结" class="headerlink" title="一句话总结"></a>一句话总结</h2><p><strong>Bearer Token 的核心规则是“谁持有，谁就能使用”。</strong> Resource Server 通常不要求调用方再证明自己掌握某个私钥，因此 Token 一旦泄露，攻击者就可能直接重放。</p><p>这也是 Bearer Token 最重要的工程边界：它很好用，但安全性高度依赖 HTTPS、短有效期、最小权限、安全存储和严格验证。</p><h2 id="Bearer-到底是什么意思"><a href="#Bearer-到底是什么意思" class="headerlink" title="Bearer 到底是什么意思"></a>Bearer 到底是什么意思</h2><p><a href="https://www.rfc-editor.org/rfc/rfc6750.html">RFC 6750</a> 对 Bearer Token 的定义很直接：任何持有 Token 的参与方，都可以用它访问 Token 对应的资源，不需要额外证明自己持有某个密码学密钥。</p><p>可以把它理解为一张临时门禁卡：</p><ol><li>Authorization Server 签发门禁卡。</li><li>Client 保存门禁卡，并在访问 API 时出示。</li><li>Resource Server 检查门禁卡是否有效、是否过期、是否属于自己、权限是否足够。</li><li>检查通过后，Resource Server 返回受保护资源。</li></ol><p>这个比喻只解释“持有即使用”。实际系统仍然要检查 Token 的签名、Issuer、Audience、有效期和 Scope，不能看到一段字符串就放行。</p><h2 id="正确的请求格式"><a href="#正确的请求格式" class="headerlink" title="正确的请求格式"></a>正确的请求格式</h2><p>客户端应优先把 Access Token 放进 <code>Authorization</code> Header：</p><figure class="highlight http"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">GET</span> <span class="string">/api/orders/123</span> <span class="meta">HTTP/1.1</span></span><br><span class="line"><span class="attribute">Host</span><span class="punctuation">: </span>api.example.com</span><br><span class="line"><span class="attribute">Authorization</span><span class="punctuation">: </span>Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IjIwMjYtMDkifQ...</span><br></pre></td></tr></table></figure><p>用 <code>curl</code> 调用时：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">curl https://api.example.com/api/orders/123 \</span><br><span class="line">  -H <span class="string">&quot;Authorization: Bearer <span class="variable">$ACCESS_TOKEN</span>&quot;</span></span><br></pre></td></tr></table></figure><p>在浏览器或 Node.js 中使用 <code>fetch</code>：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> response = <span class="keyword">await</span> <span class="title function_">fetch</span>(<span class="string">&#x27;https://api.example.com/api/orders/123&#x27;</span>, &#123;</span><br><span class="line">  <span class="attr">headers</span>: &#123;</span><br><span class="line">    <span class="title class_">Authorization</span>: <span class="string">`Bearer <span class="subst">$&#123;accessToken&#125;</span>`</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">if</span> (!response.<span class="property">ok</span>) &#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">`Request failed: <span class="subst">$&#123;response.status&#125;</span>`</span>);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> order = <span class="keyword">await</span> response.<span class="title function_">json</span>();</span><br></pre></td></tr></table></figure><p>RFC 6750 还描述了 Form Body 和 URI Query 两种传递方式，但同一个请求不能同时使用多种方式。URI Query 尤其不推荐：URL 可能进入浏览器历史、反向代理、监控系统、访问日志和 Referer，泄露面远大于 Header。</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">不推荐：GET /api/orders/123?access_token=xxx</span><br><span class="line">推荐：  Authorization: Bearer xxx</span><br></pre></td></tr></table></figure><h2 id="Bearer、JWT、OAuth-2-0-不是同一个概念"><a href="#Bearer、JWT、OAuth-2-0-不是同一个概念" class="headerlink" title="Bearer、JWT、OAuth 2.0 不是同一个概念"></a>Bearer、JWT、OAuth 2.0 不是同一个概念</h2><p>Bearer 经常和 JWT、OAuth 2.0 一起出现，但三者不在同一层。</p><table><thead><tr><th>名称</th><th>所在层次</th><th>解决的问题</th></tr></thead><tbody><tr><td>Bearer</td><td>HTTP Token 使用方式</td><td>Client 如何向 Resource Server 出示 Token</td></tr><tr><td>Access Token</td><td>授权凭证</td><td>当前 Client 被允许访问什么资源、持续多久</td></tr><tr><td>JWT</td><td>Token 编码格式</td><td>如何在一个可签名的数据结构中携带 Claims</td></tr><tr><td>OAuth 2.0</td><td>授权框架</td><td>Client 如何取得授权并获得 Access Token</td></tr><tr><td>OpenID Connect</td><td>身份协议</td><td>在 OAuth 2.0 之上表达用户登录和身份信息</td></tr></tbody></table><p>Bearer Token 可以是 JWT，也可以是一段不可读的随机字符串。后者通常称为 Opaque Token，Resource Server 需要通过 Token Introspection 或内部查询确认它的状态。</p><p>反过来也成立：JWT 不一定是 Bearer Access Token。ID Token、内部事件签名和一次性声明都可以使用 JWT 格式。Resource Server 不能因为字符串长得像 <code>xxx.yyy.zzz</code>，就把它当作合法 Access Token。</p><h2 id="Bearer-与其他鉴权方式有什么区别"><a href="#Bearer-与其他鉴权方式有什么区别" class="headerlink" title="Bearer 与其他鉴权方式有什么区别"></a>Bearer 与其他鉴权方式有什么区别</h2><table><thead><tr><th>方式</th><th>请求中携带什么</th><th>主要特点</th><th>常见场景</th></tr></thead><tbody><tr><td>Bearer Token</td><td>Access Token</td><td>持有即可使用，适合短期授权</td><td>OAuth API、移动端、Agent、服务调用</td></tr><tr><td>Basic Authentication</td><td>用户名和密码的 Base64 编码</td><td>简单，但每次请求都暴露长期凭证</td><td>内部工具、兼容旧系统</td></tr><tr><td>API Key</td><td>应用或调用方的固定 Key</td><td>易于接入，身份与权限模型通常较粗</td><td>开放 API、服务集成</td></tr><tr><td>Session Cookie</td><td>服务端 Session ID</td><td>浏览器自动携带，需要处理 CSRF</td><td>传统 Web 登录</td></tr><tr><td>mTLS &#x2F; DPoP</td><td>Token 加客户端密钥证明</td><td>被盗 Token 不一定能被单独重放</td><td>高安全 API、金融与关键基础设施</td></tr></tbody></table><p>Bearer Token Authentication 只说明 Token 怎样被呈现，不自动回答 Token 怎样签发、撤销、刷新和绑定用户。完整系统至少还需要 Authorization Server、Resource Server、权限模型和密钥管理。</p><h2 id="一次完整的-Access-Token-生命周期"><a href="#一次完整的-Access-Token-生命周期" class="headerlink" title="一次完整的 Access Token 生命周期"></a>一次完整的 Access Token 生命周期</h2><h3 id="1-Client-获取-Access-Token"><a href="#1-Client-获取-Access-Token" class="headerlink" title="1. Client 获取 Access Token"></a>1. Client 获取 Access Token</h3><p>在 OAuth 2.0 中，Client 通过 Authorization Code、Client Credentials 等流程向 Authorization Server 换取 Access Token。返回结果通常类似：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;access_token&quot;</span><span class="punctuation">:</span> <span class="string">&quot;eyJhbGciOiJSUzI1NiIs...&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;token_type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;Bearer&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;expires_in&quot;</span><span class="punctuation">:</span> <span class="number">900</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;scope&quot;</span><span class="punctuation">:</span> <span class="string">&quot;orders:read orders:write&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;refresh_token&quot;</span><span class="punctuation">:</span> <span class="string">&quot;opaque-refresh-token&quot;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p><code>access_token</code> 用于调用 Resource Server；<code>refresh_token</code> 只发给 Authorization Server 换取新的 Access Token，不应该被发送到业务 API。</p><h3 id="2-Client-调用-Resource-Server"><a href="#2-Client-调用-Resource-Server" class="headerlink" title="2. Client 调用 Resource Server"></a>2. Client 调用 Resource Server</h3><figure class="highlight http"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attribute">Authorization</span><span class="punctuation">: </span>Bearer &lt;access_token&gt;</span><br></pre></td></tr></table></figure><p>Client 不应把 Token 写进 URL、日志、错误上报或公开配置。跨服务转发时也不能默认原样透传，需要确认下游服务是否属于 Token 的 Audience。</p><h3 id="3-Resource-Server-验证-Token"><a href="#3-Resource-Server-验证-Token" class="headerlink" title="3. Resource Server 验证 Token"></a>3. Resource Server 验证 Token</h3><p>对于 JWT Access Token，至少检查：</p><table><thead><tr><th>检查项</th><th>防止什么问题</th></tr></thead><tbody><tr><td>签名与允许的算法</td><td>Token 被伪造、篡改或算法降级</td></tr><tr><td><code>iss</code></td><td>接受了不受信任的签发方</td></tr><tr><td><code>aud</code></td><td>把签给其他 API 的 Token 拿来重放</td></tr><tr><td><code>exp</code>、<code>nbf</code></td><td>使用已过期或尚未生效的 Token</td></tr><tr><td><code>scope</code> &#x2F; permission</td><td>Token 有效，但没有当前操作权限</td></tr><tr><td>Token 类型</td><td>把 ID Token、Refresh Token 当成 Access Token</td></tr></tbody></table><p>只执行 Base64 Decode 没有任何安全意义。Decode 只能读取 Claims，不能证明 Claims 没被修改。</p><h3 id="4-Resource-Server-返回标准错误"><a href="#4-Resource-Server-返回标准错误" class="headerlink" title="4. Resource Server 返回标准错误"></a>4. Resource Server 返回标准错误</h3><p>缺少或无效 Token 通常返回 <code>401 Unauthorized</code>；Token 有效但权限不足返回 <code>403 Forbidden</code>。RFC 6750 还定义了 <code>WWW-Authenticate</code> Header：</p><figure class="highlight http"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">HTTP/1.1</span> <span class="number">401</span> Unauthorized</span><br><span class="line"><span class="attribute">WWW-Authenticate</span><span class="punctuation">: </span>Bearer realm=&quot;api&quot;, error=&quot;invalid_token&quot;</span><br></pre></td></tr></table></figure><figure class="highlight http"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">HTTP/1.1</span> <span class="number">403</span> Forbidden</span><br><span class="line"><span class="attribute">WWW-Authenticate</span><span class="punctuation">: </span>Bearer realm=&quot;api&quot;, error=&quot;insufficient_scope&quot;, scope=&quot;orders:write&quot;</span><br></pre></td></tr></table></figure><p>这比只返回一段模糊的 JSON 更利于 Client 判断应该重新登录、刷新 Token，还是提示用户缺少权限。</p><h2 id="Node-js-实战：验证-JWT-Bearer-Token"><a href="#Node-js-实战：验证-JWT-Bearer-Token" class="headerlink" title="Node.js 实战：验证 JWT Bearer Token"></a>Node.js 实战：验证 JWT Bearer Token</h2><p>这个最小 Resource Server 使用 Express 和 <a href="https://github.com/panva/jose"><code>jose</code></a>，从受信任的 JWKS 地址获取公钥，并验证 JWT 的签名、Issuer 和 Audience。</p><h3 id="1-安装依赖"><a href="#1-安装依赖" class="headerlink" title="1. 安装依赖"></a>1. 安装依赖</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">npm install express jose</span><br><span class="line">npm install -D typescript tsx @types/express</span><br></pre></td></tr></table></figure><p>准备环境变量：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">export</span> AUTH_ISSUER=<span class="string">&quot;https://auth.example.com/&quot;</span></span><br><span class="line"><span class="built_in">export</span> AUTH_AUDIENCE=<span class="string">&quot;https://api.example.com&quot;</span></span><br><span class="line"><span class="built_in">export</span> AUTH_JWKS_URL=<span class="string">&quot;https://auth.example.com/.well-known/jwks.json&quot;</span></span><br></pre></td></tr></table></figure><p>JWKS 地址必须来自配置或可信的 Issuer Metadata，不能读取 Token Header 里的任意 <code>jku</code> 后直接请求。Token Header 属于攻击者可控输入。</p><h3 id="2-编写-Bearer-Middleware"><a href="#2-编写-Bearer-Middleware" class="headerlink" title="2. 编写 Bearer Middleware"></a>2. 编写 Bearer Middleware</h3><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> express, &#123; <span class="keyword">type</span> <span class="title class_">NextFunction</span>, <span class="keyword">type</span> <span class="title class_">Request</span>, <span class="keyword">type</span> <span class="title class_">Response</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;express&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; createRemoteJWKSet, jwtVerify, <span class="keyword">type</span> <span class="title class_">JWTPayload</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;jose&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> issuer = process.<span class="property">env</span>.<span class="property">AUTH_ISSUER</span>!;</span><br><span class="line"><span class="keyword">const</span> audience = process.<span class="property">env</span>.<span class="property">AUTH_AUDIENCE</span>!;</span><br><span class="line"><span class="keyword">const</span> jwksUrl = process.<span class="property">env</span>.<span class="property">AUTH_JWKS_URL</span>!;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="variable constant_">JWKS</span> = <span class="title function_">createRemoteJWKSet</span>(<span class="keyword">new</span> <span class="title function_">URL</span>(jwksUrl));</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> <span class="title class_">AuthenticatedRequest</span> = <span class="title class_">Request</span> &amp; &#123;</span><br><span class="line">  auth?: <span class="title class_">JWTPayload</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_">readBearerToken</span>(<span class="params">header: <span class="built_in">string</span> | <span class="literal">undefined</span></span>): <span class="built_in">string</span> | <span class="literal">undefined</span> &#123;</span><br><span class="line">  <span class="keyword">if</span> (!header) <span class="keyword">return</span> <span class="literal">undefined</span>;</span><br><span class="line">  <span class="keyword">const</span> match = <span class="regexp">/^Bearer +([^\s]+)$/i</span>.<span class="title function_">exec</span>(header.<span class="title function_">trim</span>());</span><br><span class="line">  <span class="keyword">return</span> match?.[<span class="number">1</span>];</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">async</span> <span class="keyword">function</span> <span class="title function_">authenticateBearer</span>(<span class="params"></span></span><br><span class="line"><span class="params">  req: Request,</span></span><br><span class="line"><span class="params">  res: Response,</span></span><br><span class="line"><span class="params">  next: NextFunction,</span></span><br><span class="line"><span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> token = <span class="title function_">readBearerToken</span>(req.<span class="title function_">header</span>(<span class="string">&#x27;authorization&#x27;</span>));</span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (!token) &#123;</span><br><span class="line">    res.<span class="title function_">setHeader</span>(<span class="string">&#x27;WWW-Authenticate&#x27;</span>, <span class="string">&#x27;Bearer realm=&quot;api&quot;&#x27;</span>);</span><br><span class="line">    <span class="keyword">return</span> res.<span class="title function_">status</span>(<span class="number">401</span>).<span class="title function_">json</span>(&#123; <span class="attr">error</span>: <span class="string">&#x27;unauthorized&#x27;</span> &#125;);</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">try</span> &#123;</span><br><span class="line">    <span class="keyword">const</span> &#123; payload &#125; = <span class="keyword">await</span> <span class="title function_">jwtVerify</span>(token, <span class="variable constant_">JWKS</span>, &#123;</span><br><span class="line">      issuer,</span><br><span class="line">      audience,</span><br><span class="line">      <span class="attr">algorithms</span>: [<span class="string">&#x27;RS256&#x27;</span>],</span><br><span class="line">      <span class="attr">requiredClaims</span>: [<span class="string">&#x27;exp&#x27;</span>],</span><br><span class="line">      <span class="attr">clockTolerance</span>: <span class="number">5</span>,</span><br><span class="line">    &#125;);</span><br><span class="line"></span><br><span class="line">    (req <span class="keyword">as</span> <span class="title class_">AuthenticatedRequest</span>).<span class="property">auth</span> = payload;</span><br><span class="line">    <span class="keyword">return</span> <span class="title function_">next</span>();</span><br><span class="line">  &#125; <span class="keyword">catch</span> &#123;</span><br><span class="line">    res.<span class="title function_">setHeader</span>(</span><br><span class="line">      <span class="string">&#x27;WWW-Authenticate&#x27;</span>,</span><br><span class="line">      <span class="string">&#x27;Bearer realm=&quot;api&quot;, error=&quot;invalid_token&quot;&#x27;</span>,</span><br><span class="line">    );</span><br><span class="line">    <span class="keyword">return</span> res.<span class="title function_">status</span>(<span class="number">401</span>).<span class="title function_">json</span>(&#123; <span class="attr">error</span>: <span class="string">&#x27;invalid_token&#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">const</span> app = <span class="title function_">express</span>();</span><br><span class="line"></span><br><span class="line">app.<span class="title function_">get</span>(<span class="string">&#x27;/api/profile&#x27;</span>, authenticateBearer, <span class="function">(<span class="params">req, res</span>) =&gt;</span> &#123;</span><br><span class="line">  res.<span class="title function_">json</span>(&#123; <span class="attr">subject</span>: (req <span class="keyword">as</span> <span class="title class_">AuthenticatedRequest</span>).<span class="property">auth</span>?.<span class="property">sub</span> &#125;);</span><br><span class="line">&#125;);</span><br><span class="line"></span><br><span class="line">app.<span class="title function_">listen</span>(<span class="number">3000</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;Resource Server listening on http://localhost:3000&#x27;</span>);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p><code>jwtVerify</code> 会完成签名验证，并根据配置检查标准 Claims。生产代码还应校验所使用 Authorization Server 对 Access Token 规定的 <code>typ</code>、Scope Claim 名称和其他约束；不同 Provider 的 Claims 契约不完全相同。如果双方采用 <a href="https://www.rfc-editor.org/rfc/rfc9068.html">RFC 9068</a> 的 JWT Access Token Profile，还应要求 <code>typ=at+jwt</code>，防止把 ID Token 当成 Access Token。</p><h3 id="3-增加-Scope-授权"><a href="#3-增加-Scope-授权" class="headerlink" title="3. 增加 Scope 授权"></a>3. 增加 Scope 授权</h3><p>身份验证通过不等于拥有所有权限。可以在同一条路由上继续检查 Scope：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">requireScope</span>(<span class="params">requiredScope: <span class="built_in">string</span></span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="function">(<span class="params">req: Request, res: Response, next: NextFunction</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">const</span> auth = (req <span class="keyword">as</span> <span class="title class_">AuthenticatedRequest</span>).<span class="property">auth</span>;</span><br><span class="line">    <span class="keyword">const</span> scopes = <span class="title class_">String</span>(auth?.<span class="property">scope</span> || <span class="string">&#x27;&#x27;</span>)</span><br><span class="line">      .<span class="title function_">split</span>(<span class="string">&#x27; &#x27;</span>)</span><br><span class="line">      .<span class="title function_">filter</span>(<span class="title class_">Boolean</span>);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (!scopes.<span class="title function_">includes</span>(requiredScope)) &#123;</span><br><span class="line">      res.<span class="title function_">setHeader</span>(</span><br><span class="line">        <span class="string">&#x27;WWW-Authenticate&#x27;</span>,</span><br><span class="line">        <span class="string">`Bearer realm=&quot;api&quot;, error=&quot;insufficient_scope&quot;, scope=&quot;<span class="subst">$&#123;requiredScope&#125;</span>&quot;`</span>,</span><br><span class="line">      );</span><br><span class="line">      <span class="keyword">return</span> res.<span class="title function_">status</span>(<span class="number">403</span>).<span class="title function_">json</span>(&#123; <span class="attr">error</span>: <span class="string">&#x27;insufficient_scope&#x27;</span> &#125;);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> <span class="title function_">next</span>();</span><br><span class="line">  &#125;;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">app.<span class="title function_">delete</span>(</span><br><span class="line">  <span class="string">&#x27;/api/orders/:id&#x27;</span>,</span><br><span class="line">  authenticateBearer,</span><br><span class="line">  requireScope(<span class="string">&#x27;orders:delete&#x27;</span>),</span><br><span class="line">  <span class="function">(<span class="params">req, res</span>) =&gt;</span> &#123;</span><br><span class="line">    res.<span class="title function_">status</span>(<span class="number">204</span>).<span class="title function_">end</span>();</span><br><span class="line">  &#125;,</span><br><span class="line">);</span><br></pre></td></tr></table></figure><p>实际项目还需要把 <code>sub</code> 映射到用户或服务主体，并在业务层检查资源归属。例如拥有 <code>orders:write</code> 不代表可以修改任意用户的订单。</p><h3 id="4-验证三条关键分支"><a href="#4-验证三条关键分支" class="headerlink" title="4. 验证三条关键分支"></a>4. 验证三条关键分支</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 缺少 Token：预期 401，并返回 WWW-Authenticate: Bearer</span></span><br><span class="line">curl -i http://localhost:3000/api/profile</span><br><span class="line"></span><br><span class="line"><span class="comment"># 无效 Token：预期 401 + invalid_token</span></span><br><span class="line">curl -i http://localhost:3000/api/profile \</span><br><span class="line">  -H <span class="string">&quot;Authorization: Bearer invalid-token&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 有效但缺少 Scope：预期 403 + insufficient_scope</span></span><br><span class="line">curl -i -X DELETE http://localhost:3000/api/orders/123 \</span><br><span class="line">  -H <span class="string">&quot;Authorization: Bearer <span class="variable">$READ_ONLY_ACCESS_TOKEN</span>&quot;</span></span><br></pre></td></tr></table></figure><p>验证时不要只看 JSON Body。HTTP Status 与 <code>WWW-Authenticate</code> Header 同样属于协议契约。</p><h2 id="生产环境最容易犯的错误"><a href="#生产环境最容易犯的错误" class="headerlink" title="生产环境最容易犯的错误"></a>生产环境最容易犯的错误</h2><h3 id="把-JWT-Decode-当成-Verify"><a href="#把-JWT-Decode-当成-Verify" class="headerlink" title="把 JWT Decode 当成 Verify"></a>把 JWT Decode 当成 Verify</h3><p>任何人都能构造 JWT Payload。Resource Server 必须验证签名，并固定可信 Issuer、Audience 与允许算法。</p><h3 id="把-Token-放进-URL"><a href="#把-Token-放进-URL" class="headerlink" title="把 Token 放进 URL"></a>把 Token 放进 URL</h3><p>URL 会穿过太多系统。即使业务日志主动脱敏，浏览器历史、代理、CDN、APM 或错误追踪仍可能留下完整 Query String。</p><h3 id="让-Access-Token-永不过期"><a href="#让-Access-Token-永不过期" class="headerlink" title="让 Access Token 永不过期"></a>让 Access Token 永不过期</h3><p>长期 Bearer Token 接近长期密码。优先使用短期 Access Token，并为 Refresh Token 配置 Rotation 或 Sender Constraint。</p><h3 id="只验证“有效”，不验证“给谁”"><a href="#只验证“有效”，不验证“给谁”" class="headerlink" title="只验证“有效”，不验证“给谁”"></a>只验证“有效”，不验证“给谁”</h3><p>签名有效只说明 Token 来自某个密钥，不说明它是签给当前 API 的。<code>aud</code> 校验用于阻止 Token Redirect 和跨服务误用。</p><h3 id="在日志里打印整个-Header"><a href="#在日志里打印整个-Header" class="headerlink" title="在日志里打印整个 Header"></a>在日志里打印整个 Header</h3><p>应用日志、网关日志和异常上下文都应脱敏 <code>Authorization</code>、Cookie、Refresh Token 等字段。排障通常只需要 Token 的哈希摘要、<code>kid</code>、Issuer、Subject 和错误类型。</p><h3 id="用设备-ID-代替-Bearer-Token"><a href="#用设备-ID-代替-Bearer-Token" class="headerlink" title="用设备 ID 代替 Bearer Token"></a>用设备 ID 代替 Bearer Token</h3><p>设备序列号、随机安装 ID 和浏览器指纹可以帮助审计来源，但它们通常不是服务端签发、可过期、可限制 Scope 的安全凭证。它们回答“可能来自哪个安装实例”，不能独立证明“调用方被授权访问什么”。</p><h2 id="2026-年还要继续用-Bearer-Token-吗"><a href="#2026-年还要继续用-Bearer-Token-吗" class="headerlink" title="2026 年还要继续用 Bearer Token 吗"></a>2026 年还要继续用 Bearer Token 吗</h2><p>Bearer Token 仍然是主流且实用的 API 访问方式。它的优势是协议简单、工具链成熟、跨语言互操作好。多数普通业务 API，只要做到 HTTPS、短有效期、Audience 限制、最小 Scope、可靠密钥轮换和日志脱敏，就能建立清晰的安全边界。</p><p>高价值数据、金融操作或泄露后影响很大的 API，需要进一步考虑 Sender-Constrained Access Token。<a href="https://www.rfc-editor.org/rfc/rfc9700.html">RFC 9700</a> 建议使用 mTLS 或 DPoP 等机制，把 Token 绑定到特定发送方，从而降低被盗 Token 单独重放的风险。</p><h2 id="要不要用：我的判断框架"><a href="#要不要用：我的判断框架" class="headerlink" title="要不要用：我的判断框架"></a>要不要用：我的判断框架</h2><h3 id="值得用"><a href="#值得用" class="headerlink" title="值得用"></a>值得用</h3><p>系统已经有 OAuth 2.0 &#x2F; OIDC Provider，需要为 Web、移动端、CLI、Agent 或服务间调用提供统一 API 授权；团队能够管理 Token 生命周期、Scope、Audience 和密钥轮换。</p><h3 id="可以简化"><a href="#可以简化" class="headerlink" title="可以简化"></a>可以简化</h3><p>单体 Web 应用只有浏览器页面和同域后端，成熟的 Server Session + <code>Secure</code>、<code>HttpOnly</code>、<code>SameSite</code> Cookie 可能更直接。不要为了“前后端分离”机械引入一套不完整的 OAuth 系统。</p><h3 id="需要升级"><a href="#需要升级" class="headerlink" title="需要升级"></a>需要升级</h3><p>Token 一旦泄露就可能造成重大损失，或 API 跨越不可信终端和网络边界时，应评估 DPoP、mTLS、BFF、Token Exchange 和更严格的 Audience 隔离。Bearer 的易用性来自“持有即可使用”，风险也来自同一个性质。</p><p>我的判断是：<strong>Bearer Token 适合做短期、最小权限、可验证的访问凭证，不适合充当永不过期的万能身份。</strong></p><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><ol><li><a href="https://www.rfc-editor.org/rfc/rfc6750.html">RFC 6750：The OAuth 2.0 Authorization Framework: Bearer Token Usage</a></li><li><a href="https://www.rfc-editor.org/rfc/rfc9700.html">RFC 9700：Best Current Practice for OAuth 2.0 Security</a></li><li><a href="https://www.rfc-editor.org/rfc/rfc9068.html">RFC 9068：JSON Web Token Profile for OAuth 2.0 Access Tokens</a></li><li><a href="https://github.com/panva/jose/blob/main/docs/jwks/remote/functions/createRemoteJWKSet.md"><code>jose</code>：JWT Verification 与 Remote JWKS</a></li></ol><blockquote><p>本文使用 <a href="https://github.com/zisheng-ai/writting-skill">writting-skill</a> 辅助写作。项目已开源，欢迎在 GitHub 点个 Star。</p></blockquote>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/cbd8d77fb880/</id>
    <link href="https://blog.zisheng.pro/posts/cbd8d77fb880/"/>
    <published>2026-09-17T07:43:17.000Z</published>
    <summary>Bearer Token 是 HTTP API 最常见的访问凭证，却经常被误认为 JWT、OAuth 2.0 或普通 API Key。本文从 RFC 6750 出发，讲清 Authorization Header、Access Token、JWT 与 Refresh Token 的边界，并给出一套可运行的 Node.js 验证、Scope 授权和错误响应实现。</summary>
    <title>Bearer Token Authentication 到底是什么：从 Authorization Header 到安全验证</title>
    <updated>2026-09-18T08:15:28.629Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="软件工程" scheme="https://blog.zisheng.pro/categories/%E8%BD%AF%E4%BB%B6%E5%B7%A5%E7%A8%8B/"/>
    <category term="Windows" scheme="https://blog.zisheng.pro/tags/Windows/"/>
    <category term="Release Engineering" scheme="https://blog.zisheng.pro/tags/Release-Engineering/"/>
    <category term="PE" scheme="https://blog.zisheng.pro/tags/PE/"/>
    <category term="Bun" scheme="https://blog.zisheng.pro/tags/Bun/"/>
    <category term="resedit" scheme="https://blog.zisheng.pro/tags/resedit/"/>
    <category term="Build Tooling" scheme="https://blog.zisheng.pro/tags/Build-Tooling/"/>
    <content>
      <![CDATA[<p>最近在精简一个 Windows 发行版时，我看到源码目录里放着一个 <code>rcedit-x64.exe</code>。它不属于产品业务，也不会在应用运行时被调用，却跟着发行版源码保存了很多年。</p><p>最初的问题只是“这个文件能不能删”。继续追下去，问题变成了三个：谁负责修改 Windows 可执行文件的图标和版本信息？这类工具应该留在发行版，还是归构建框架管理？如果迁移到官方建议的 <code>resedit</code>，它能不能安全处理 Bun 生成的单文件 EXE？</p><p>这次迁移最后没有停在替换依赖。<code>resedit</code> 确实可以取代 <code>rcedit.exe</code> 的资源编辑职责，但 Bun 在 PE 末尾增加了自己的 <code>.bun</code> section，触发了底层解析库的保护性报错。要兼容它，必须先理解 PE 的结构，再把“兼容”写成可验证的不变量。</p><p><img src="/images/from-rcedit-to-resedit-bun-pe.webp" alt="一个 Windows PE 容器中，仅资源模块被替换，Bun payload 保持封闭完整"></p><span id="more"></span><h2 id="一句话总结"><a href="#一句话总结" class="headerlink" title="一句话总结"></a>一句话总结</h2><p>Windows PE 是 <code>.exe</code>、<code>.dll</code> 等文件使用的结构化容器。版本信息和图标通常位于 <code>.rsrc</code>，Bun 的单文件程序还会带有保存 Runtime 与应用 payload 的 <code>.bun</code> section。</p><p>迁移到 <code>resedit</code> 时，可靠的做法是只允许已知的 Bun 布局，并在写回前证明：除资源以外的 section，其虚拟地址、大小、属性和内容都没有变化。</p><h2 id="Windows-PE-是什么"><a href="#Windows-PE-是什么" class="headerlink" title="Windows PE 是什么"></a>Windows PE 是什么</h2><p>PE 的全称是 Portable Executable。按照 <a href="https://learn.microsoft.com/en-us/windows/win32/debug/pe-format">Microsoft 的 PE&#x2F;COFF 规范</a>，它是 Windows 家族操作系统使用的 executable image 和 object file 格式。常见的 <code>.exe</code>、<code>.dll</code>、<code>.sys</code> 都建立在这套结构上。</p><p>把 PE 理解成“Windows Loader 能读懂的带目录容器”会比较直观。文件内部不只是一段连续的机器码，而是由 Header、Section Table 和多块 section 共同组成：</p><table><thead><tr><th>组成部分</th><th>作用</th></tr></thead><tbody><tr><td>DOS Header &#x2F; PE Header</td><td>声明文件身份、目标架构、section 数量等基础信息</td></tr><tr><td>Optional Header</td><td>记录入口地址、Image Base、对齐方式和 Data Directory；名字叫 Optional，但 executable image 通常离不开它</td></tr><tr><td>Section Table</td><td>描述每个 section 的名称、磁盘位置、内存地址、大小和访问属性</td></tr><tr><td>Sections</td><td>保存代码、只读数据、可写数据、资源、重定位信息以及工具自定义 payload</td></tr></tbody></table><p>常见 section 如下：</p><table><thead><tr><th>Section</th><th>通常保存什么</th></tr></thead><tbody><tr><td><code>.text</code></td><td>可执行机器码</td></tr><tr><td><code>.rdata</code></td><td>只读数据</td></tr><tr><td><code>.data</code></td><td>可写数据</td></tr><tr><td><code>.rsrc</code></td><td>图标、字符串、Dialog、Manifest、<code>VERSIONINFO</code> 等资源</td></tr><tr><td><code>.reloc</code></td><td>Image 无法加载到首选地址时需要使用的重定位信息</td></tr><tr><td><code>.bun</code></td><td>Bun 单文件 executable 使用的自定义 payload section</td></tr></tbody></table><p><img src="/images/windows-pe-section-layout.webp" alt="Windows PE 的简化结构，以及修改 VERSIONINFO 时允许变化和必须保护的边界"></p><p><code>.rsrc</code> 内部也有严格结构。Windows 按 Type、Name、Language 组织资源树，最终叶子节点指向资源数据。资源编辑工具必须解析这棵树、修改指定节点，再重新生成合法的 section。</p><h3 id="RVA-和磁盘-offset-不是一回事"><a href="#RVA-和磁盘-offset-不是一回事" class="headerlink" title="RVA 和磁盘 offset 不是一回事"></a>RVA 和磁盘 offset 不是一回事</h3><p>理解 PE 修改风险，还需要分清两个位置概念。</p><p><code>PointerToRawData</code> 表示一块 section 在磁盘文件里的位置。前面的 <code>.rsrc</code> 变大后，后续 section 在文件中的 raw offset 可以顺延，只要 Section Table 同步更新即可。</p><p><code>VirtualAddress</code> 通常用 RVA 表示，是 image 映射进进程地址空间后，相对 Image Base 的地址。代码、数据和 Loader 读取的目录会依赖这些地址。修改资源时如果无意中移动后续 section 的 RVA，就会改变 executable 的内存布局。</p><p>因此，磁盘 offset 变化不一定是错误；非资源 section 的 RVA、大小或内容变化，则必须高度警惕。</p><h2 id="VERSIONINFO-保存了什么"><a href="#VERSIONINFO-保存了什么" class="headerlink" title="VERSIONINFO 保存了什么"></a>VERSIONINFO 保存了什么</h2><p>Windows 文件资源管理器中“详细信息”页显示的 Product name、File description、Company、File version、Product version，主要来自 PE 资源里的 <code>VERSIONINFO</code>。</p><p>这也是 <code>rcedit</code> 长期存在的原因。构建 Electron 或其他 Windows 应用时，可以在命令行里修改图标、Manifest 和版本字段，不必手写 <code>.rc</code> 文件并重新链接整个程序。</p><p>2026 年 4 月，Electron 团队将 <code>rcedit</code> 仓库归档，并在 <a href="https://github.com/electron/rcedit/issues/184">No Longer Maintained</a> 中说明：Electron 的内部程序化使用已经迁移到 <code>resedit</code>，仍依赖 <code>node-rcedit</code> 的项目也可以将它作为替代方案。</p><p><code>resedit</code> 的边界更适合现代 JavaScript 构建链。它以 JavaScript&#x2F;TypeScript 实现，可以在 Node.js 和 Web bundler 环境中运行，不需要在仓库里额外保存一个 <code>rcedit-x64.exe</code>。调用方可以直接读取 PE、修改 <code>VersionInfo</code>，再生成新的 binary。</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> * <span class="keyword">as</span> <span class="title class_">ResEdit</span> <span class="keyword">from</span> <span class="string">&#x27;resedit&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> executable = <span class="title class_">ResEdit</span>.<span class="property">NtExecutable</span>.<span class="title function_">from</span>(binary)</span><br><span class="line"><span class="keyword">const</span> resources = <span class="title class_">ResEdit</span>.<span class="property">NtExecutableResource</span>.<span class="title function_">from</span>(executable)</span><br><span class="line"><span class="keyword">const</span> [versionInfo] = <span class="title class_">ResEdit</span>.<span class="property">Resource</span>.<span class="property">VersionInfo</span>.<span class="title function_">fromEntries</span>(</span><br><span class="line">  resources.<span class="property">entries</span>,</span><br><span class="line">)</span><br><span class="line"></span><br><span class="line">versionInfo.<span class="title function_">setFileVersion</span>(<span class="string">&#x27;1.2.3.0&#x27;</span>, <span class="number">1033</span>)</span><br><span class="line">versionInfo.<span class="title function_">setProductVersion</span>(<span class="string">&#x27;1.2.3.0&#x27;</span>, <span class="number">1033</span>)</span><br><span class="line">versionInfo.<span class="title function_">setStringValues</span>(</span><br><span class="line">  &#123; <span class="attr">lang</span>: <span class="number">1033</span>, <span class="attr">codepage</span>: <span class="number">1200</span> &#125;,</span><br><span class="line">  &#123;</span><br><span class="line">    <span class="title class_">ProductName</span>: <span class="string">&#x27;Example Runway&#x27;</span>,</span><br><span class="line">    <span class="title class_">FileDescription</span>: <span class="string">&#x27;Example Runway&#x27;</span>,</span><br><span class="line">    <span class="title class_">CompanyName</span>: <span class="string">&#x27;Example Inc.&#x27;</span>,</span><br><span class="line">  &#125;,</span><br><span class="line">)</span><br></pre></td></tr></table></figure><p>不过，换成 JavaScript library 不等于自动获得安全性。<code>resedit</code> 自己的 README 也提醒，修改或签名 executable 的覆盖测试仍有限，调用方应谨慎检查输出文件。构建系统不能把“API 没报错”当成交付证据。</p><h2 id="为什么-Bun-让迁移复杂了一步"><a href="#为什么-Bun-让迁移复杂了一步" class="headerlink" title="为什么 Bun 让迁移复杂了一步"></a>为什么 Bun 让迁移复杂了一步</h2><p>Bun 支持用 <code>bun build --compile</code> 把 JavaScript 或 TypeScript 与 Runtime 一起编译成单文件 executable，也支持生成 Windows x64 目标。Bun 生成的 PE 除了常规 section，还包含保存自身数据的 <code>.bun</code> section。</p><p>在实际生成的 Windows x64 文件中，资源附近出现了 <code>.rsrc → .reloc → .bun</code> 的顺序。</p><p>原始 <code>resedit</code> 解析这份文件时会抛出错误：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">After Resource section, sections except for relocation are not supported</span><br></pre></td></tr></table></figure><p>这条异常来自 <code>resedit</code> 底层 PE library 的保守假设：资源 section 之后只接受 relocation，并不表示 Bun executable 已损坏。普通链接器常把 <code>.rsrc</code> 和 <code>.reloc</code> 放在靠后位置，但 PE 规范允许工具定义自己的 section；Bun 恰好在后面保留了 <code>.bun</code>。</p><p>Bun 本身也提供 <code>--windows-title</code>、<code>--windows-version</code>、<code>--windows-publisher</code> 等 metadata 参数，但 <a href="https://bun.sh/docs/bundler/executables">Bun executable 文档</a> 明确指出，这些参数依赖 Windows API，当前不能用于 cross-compiling。对于需要在 macOS 上交叉生成 Windows artifact 的 host-neutral 构建链，它不是完整替代。</p><p>于是出现了一个容易做错的选择：为了让 <code>resedit</code> 继续运行，直接删掉检查、忽略所有后续 section。这样能消除异常，却无法证明 <code>.bun</code> payload 没被移动或重写。</p><h2 id="兼容层的设计：先定义不能变的东西"><a href="#兼容层的设计：先定义不能变的东西" class="headerlink" title="兼容层的设计：先定义不能变的东西"></a>兼容层的设计：先定义不能变的东西</h2><p>这次实现采用 fail-closed 策略。只有同时满足以下条件，兼容路径才会启用：</p><ol><li>PE 中存在资源 section。</li><li><code>.rsrc</code> 之后除了 <code>.reloc</code>，只多出一个 <code>.bun</code>。</li><li>写入新资源后，整个 image 的虚拟大小不变。</li><li>所有非资源 section 的名称、RVA、virtual size、raw size、attributes 和二进制内容不变。</li><li>重新解析输出文件后，所有目标 <code>VERSIONINFO</code> 字段与预期值一致。</li></ol><p>遇到 <code>.custom</code> 等未知 section，构建立即失败；资源内容过长，导致后续虚拟布局需要移动时，构建同样失败。这两种情况都不会覆盖原始 executable。</p><h3 id="第一步：记录布局快照"><a href="#第一步：记录布局快照" class="headerlink" title="第一步：记录布局快照"></a>第一步：记录布局快照</h3><p>资源写入前，构建工具遍历所有非资源 section，保存它们的结构和内容：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">snapshotExecutableLayout</span>(<span class="params">executable, resourceSection</span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> &#123;</span><br><span class="line">    <span class="attr">imageSize</span>: executable.<span class="property">newHeader</span>.<span class="property">optionalHeader</span>.<span class="property">sizeOfImage</span>,</span><br><span class="line">    <span class="attr">sections</span>: executable</span><br><span class="line">      .<span class="title function_">getAllSections</span>()</span><br><span class="line">      .<span class="title function_">filter</span>(<span class="function">(<span class="params">section</span>) =&gt;</span> section !== resourceSection)</span><br><span class="line">      .<span class="title function_">map</span>(<span class="function">(<span class="params">section</span>) =&gt;</span> (&#123;</span><br><span class="line">        <span class="attr">name</span>: section.<span class="property">info</span>.<span class="property">name</span>,</span><br><span class="line">        <span class="attr">virtualAddress</span>: section.<span class="property">info</span>.<span class="property">virtualAddress</span>,</span><br><span class="line">        <span class="attr">virtualSize</span>: section.<span class="property">info</span>.<span class="property">virtualSize</span>,</span><br><span class="line">        <span class="attr">sizeOfRawData</span>: section.<span class="property">info</span>.<span class="property">sizeOfRawData</span>,</span><br><span class="line">        <span class="attr">characteristics</span>: section.<span class="property">info</span>.<span class="property">characteristics</span>,</span><br><span class="line">        <span class="attr">data</span>: <span class="title class_">Buffer</span>.<span class="title function_">from</span>(section.<span class="property">data</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>PointerToRawData</code> 设为不变量。<code>.rsrc</code> 增长时，后续 section 在磁盘文件中的位置可以移动；只要 PE Header 正确反映新位置，Loader 仍能读取它。兼容层需要锁住的是虚拟布局与 payload 内容。</p><h3 id="第二步：只绕过一个已知假设"><a href="#第二步：只绕过一个已知假设" class="headerlink" title="第二步：只绕过一个已知假设"></a>第二步：只绕过一个已知假设</h3><p>兼容层不会修改或删除 <code>.bun</code>。它只在 <code>NtExecutableResource.from()</code> 执行保守顺序检查时，暂时从 <code>getAllSections()</code> 的返回值中隐藏唯一的 <code>.bun</code> section。资源解析结束后立即恢复原方法。</p><p>这个处理范围很窄：放行的是已经识别并验证过的 Bun 布局，不是把任意 PE 都交给宽松解析器。</p><h3 id="第三步：写入后逐项比对"><a href="#第三步：写入后逐项比对" class="headerlink" title="第三步：写入后逐项比对"></a>第三步：写入后逐项比对</h3><p><code>resedit</code> 生成新资源后，构建工具再次读取所有 section。只要 section 数量、对象、名称、RVA、大小、属性或内容有一项变化，就拒绝生成 artifact。</p><p>随后把完整输出重新解析一次，检查 <code>FileVersion</code>、<code>ProductVersion</code>、<code>ProductName</code>、<code>FileDescription</code>、<code>CompanyName</code>、<code>InternalName</code> 和 <code>OriginalFilename</code>。只有结构保护和业务字段验证同时通过，才覆盖构建目录里的 executable。</p><p>这类兼容层的价值不在于“支持 Bun”这个结果，而在于把成功条件写成了机器可以拒绝的契约。</p><h2 id="测试不能只看文件属性页"><a href="#测试不能只看文件属性页" class="headerlink" title="测试不能只看文件属性页"></a>测试不能只看文件属性页</h2><p>Windows 文件属性页能证明目标字段可见，却不能证明其他 section 没有受损。测试按四层组织更可靠。</p><table><thead><tr><th>层级</th><th>验证内容</th><th>能证明什么</th></tr></thead><tbody><tr><td>合成 PE 回归测试</td><td>构造 <code>.rsrc → .reloc → .bun</code>，确认原始 <code>resedit</code> 失败、兼容层成功</td><td>精确复现并覆盖已知冲突</td></tr><tr><td>负向结构测试</td><td>在资源后放入未知 <code>.custom</code> section</td><td>未知布局不会被静默放行</td></tr><tr><td>资源增长测试</td><td>写入超长 metadata</td><td>虚拟 section 需要移动时会失败且不覆盖原文件</td></tr><tr><td>真实 Bun artifact</td><td>交叉编译 Windows x64 executable，写入 metadata，再解析并打包</td><td>兼容层能处理 Bun 的真实产物</td></tr></tbody></table><p>真实 artifact 验证完成后，仍不能把 macOS 上的结构检查描述成 Windows 原生验收。正式发布还需要在 Windows 上启动 executable、回读 File Properties 或 PowerShell <code>VersionInfo</code>，并在 metadata 修改之后完成 Authenticode 签名。</p><p>如果目标 executable 需要 Authenticode，顺序也必须固定：</p><p><img src="/images/resedit-safe-build-pipeline.webp" alt="resedit 修改 Windows executable 的安全构建顺序"></p><p>签名后的 PE 再被修改，原签名就不再对应当前文件内容。<code>resedit</code> 文档同样说明，解析签名文件需要显式选择，而重新生成 binary 会得到未签名输出。因此，资源编辑应处于签名前的构建阶段。</p><h2 id="构建工具和发行版应该怎么分工"><a href="#构建工具和发行版应该怎么分工" class="headerlink" title="构建工具和发行版应该怎么分工"></a>构建工具和发行版应该怎么分工</h2><p>这次清理还解决了一个 ownership 问题。<code>rcedit-x64.exe</code> 原来位于产品发行版源码目录，看起来像产品运行依赖，实际只服务于 Windows 构建。</p><p>更清晰的边界是：</p><table><thead><tr><th>层级</th><th>应该拥有的内容</th></tr></thead><tbody><tr><td>Distribution</td><td>品牌名、版本号、publisher、图标等产品输入</td></tr><tr><td>Build Kit</td><td>PE 解析、资源写入、结构保护、hash 与打包顺序</td></tr><tr><td>Target artifact</td><td>修改完成的 executable，不包含 <code>resedit</code> 或 <code>rcedit.exe</code></td></tr><tr><td>Windows release gate</td><td>原生启动、File Properties、签名和安装验收</td></tr></tbody></table><p>这次落地将 <code>resedit</code> 精确锁定在 <code>3.1.0</code>，作为 Build Kit 的 npm dependency。它参与构建，但不会被复制到 Runway payload，也不会作为一个孤立的 <code>.exe</code> 混入 Browser App。</p><p>这种边界比“把工具换成 npm 包”更重要。它决定了依赖由谁升级、兼容问题在哪里修复、最终安装包应该包含什么，以及故障由哪一层负责阻断。</p><h2 id="要不要用：我的判断框架"><a href="#要不要用：我的判断框架" class="headerlink" title="要不要用：我的判断框架"></a>要不要用：我的判断框架</h2><h3 id="值得迁移到-resedit"><a href="#值得迁移到-resedit" class="headerlink" title="值得迁移到 resedit"></a>值得迁移到 resedit</h3><p>项目使用 Node.js&#x2F;JavaScript 构建链，需要程序化修改未签名 PE 的图标或 <code>VERSIONINFO</code>，并希望移除仓库中的预编译 <code>rcedit.exe</code>，可以优先评估 <code>resedit</code>。Electron 已经给出了明确迁移方向，library API 也更容易接入结构检查和测试。</p><h3 id="可以直接使用-Bun-参数"><a href="#可以直接使用-Bun-参数" class="headerlink" title="可以直接使用 Bun 参数"></a>可以直接使用 Bun 参数</h3><p>构建始终发生在原生 Windows，Bun 提供的 title、publisher、version、description 和 copyright 已经覆盖需求时，直接使用 Bun metadata 参数更短。此时仍要验证 Bun 版本、目标 executable 和签名顺序。</p><h3 id="需要停下来审计"><a href="#需要停下来审计" class="headerlink" title="需要停下来审计"></a>需要停下来审计</h3><p>只要 PE 带有未知自定义 section、已经签名、资源增长可能改变虚拟布局，或者发行链没有原生 Windows 验收，就不应该把“生成成功”当成可发布。先建立结构快照、负向测试和重签名路径，再决定是否修改。</p><p>这次迁移给我的判断很直接：兼容第三方 binary format，最危险的实现是让异常消失，最可靠的实现是明确哪些变化被允许，并对其余变化全部失败。</p><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><ol><li><a href="https://learn.microsoft.com/en-us/windows/win32/debug/pe-format">Microsoft Learn：PE Format</a></li><li><a href="https://github.com/electron/rcedit/issues/184">Electron rcedit：No Longer Maintained</a></li><li><a href="https://github.com/jet2jet/resedit-js">resedit-js</a></li><li><a href="https://bun.sh/docs/bundler/executables">Bun：Single-file executable</a></li></ol><blockquote><p>出品人：俊宁（Aaron），阿里高级工程师、AI 开发者，拥有 10 年研发经验，现专注 AI 应用、Agent、AI Infra 与软件工程，FlyOS 作者。</p></blockquote><blockquote><p>本文使用 <a href="https://github.com/zisheng-ai/writting-skill">writting-skill</a> 辅助写作，配图使用 <a href="https://github.com/zisheng-ai/better-imagegen">better-imagegen</a> 生成。项目已开源，欢迎在 GitHub 点个 Star。</p></blockquote>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/c519d7610fe7/</id>
    <link href="https://blog.zisheng.pro/posts/c519d7610fe7/"/>
    <published>2026-09-01T10:44:35.000Z</published>
    <summary>最近一次 Windows 构建工具迁移，让我重新理解了 PE、VERSIONINFO 与 Bun 单文件可执行程序之间的关系。本文从零解释 Windows PE，并复盘如何用 resedit 替代归档的 rcedit，同时通过不变量校验保护 Bun 的自定义 section。</summary>
    <title>从 rcedit 到 resedit：给 Bun 单文件 EXE 修改 VERSIONINFO 的兼容层设计</title>
    <updated>2026-09-18T08:15:28.636Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="软件工程" scheme="https://blog.zisheng.pro/categories/%E8%BD%AF%E4%BB%B6%E5%B7%A5%E7%A8%8B/"/>
    <category term="AI Browser" scheme="https://blog.zisheng.pro/tags/AI-Browser/"/>
    <category term="Chromium" scheme="https://blog.zisheng.pro/tags/Chromium/"/>
    <category term="Windows" scheme="https://blog.zisheng.pro/tags/Windows/"/>
    <category term="Release Engineering" scheme="https://blog.zisheng.pro/tags/Release-Engineering/"/>
    <category term="Browser Security" scheme="https://blog.zisheng.pro/tags/Browser-Security/"/>
    <content>
      <![CDATA[<p><img src="/images/windows-ai-browser-launch-parameters.webp" alt="Windows 下 AI 浏览器的生产启动与诊断工具"></p><p>最近在检查一套 Chromium-based AI 浏览器的 Windows 安装包时，我遇到了一个很典型的现象：浏览器能打开，但窗口顶部常驻着“不受支持的命令行标记”警告。继续检查桌面快捷方式，才发现安装器把调试期参数一起写进了正式入口。</p><p>这类问题很容易被当成“删掉一个 <code>--no-sandbox</code> 就结束”。我的判断更严格：<strong>正式发布的浏览器快捷方式，默认应该只有可执行文件路径，不附加任何强制启动参数。</strong> GPU、Remote Debugging、独立 Profile、日志和网络覆盖都有使用场景，但它们应该属于有生命周期、有隔离边界、可审计的诊断或自动化 Profile，而不是用户每天点击的入口。</p><span id="more"></span><h2 id="一句话总结"><a href="#一句话总结" class="headerlink" title="一句话总结"></a>一句话总结</h2><p>Windows 下 AI 浏览器的启动参数可以按三类管理：</p><ul><li>Production：快捷方式参数为空，使用产品配置、Enterprise Policy 和编译期能力；</li><li>Internal Diagnosis：一次问题对应一个隔离 Profile，只临时打开必要日志或单变量实验；</li><li>Automation&#x2F;Eval：由 Harness 创建独立 User Data Directory 和调试通道，运行结束后回收。</li></ul><p>参数的专业管理标准不在于“记住更多 flags”，而在于回答四个问题：谁添加、作用于哪些进程、何时移除、用什么证据证明它解决了问题。</p><h2 id="为什么-AI-浏览器更不能把参数堆在快捷方式里"><a href="#为什么-AI-浏览器更不能把参数堆在快捷方式里" class="headerlink" title="为什么 AI 浏览器更不能把参数堆在快捷方式里"></a>为什么 AI 浏览器更不能把参数堆在快捷方式里</h2><p>普通 Chromium 浏览器已经是 Browser、Renderer、GPU、Network Service、Extension 等多进程系统。AI 浏览器通常还会增加 Extension Service Worker、Native Host、本地模型 Runtime 或 Automation Controller。它们可能共享一次用户任务，但不共享同一种资源模型。</p><table><thead><tr><th>运行层</th><th>主要职责</th><th>启动参数误配的典型后果</th></tr></thead><tbody><tr><td>Browser Process</td><td>窗口、Profile、权限、进程编排</td><td>调试能力常驻，用户状态与测试状态混用</td></tr><tr><td>Renderer &#x2F; Extension Renderer</td><td>页面、AI UI、Web 内容执行</td><td>GPU、V8、Site Isolation 相关行为被全局改变</td></tr><tr><td>Extension Service Worker</td><td>工具调度、事件处理、浏览器 API</td><td>用浏览器 flags 掩盖生命周期或权限设计问题</td></tr><tr><td>Native Host &#x2F; Local Runtime</td><td>本地工具、模型或系统能力</td><td>Renderer 的 V8 参数通常解决不了独立进程问题</td></tr><tr><td>Automation Controller</td><td>测试、Eval、CDP&#x2F;WebDriver 会话</td><td>远程调试端口和真实用户 Profile 变成攻击面</td></tr></tbody></table><p>这里有一个很重要的责任边界：<strong>浏览器启动参数只能调整 Chromium 接受的那部分 Runtime 行为。</strong> 本地模型显存不足、Native Host 内存泄漏、Extension 事件丢失，不能靠给所有 Renderer 加大堆上限来统一“治疗”。跨进程系统如果只剩一把 flags 锤子，最后通常会同时损失安全性、可观测性和问题定位能力。</p><h2 id="三套-Profile：先分场景，再选参数"><a href="#三套-Profile：先分场景，再选参数" class="headerlink" title="三套 Profile：先分场景，再选参数"></a>三套 Profile：先分场景，再选参数</h2><p>我建议把参数组合当成受管 Profile，而不是散落在 README、快捷方式和聊天记录里的命令片段。</p><table><thead><tr><th>Profile</th><th>使用者</th><th>Profile 数据</th><th>调试能力</th><th>生命周期</th><th>验收标准</th></tr></thead><tbody><tr><td>Production</td><td>最终用户</td><td>默认产品目录</td><td>关闭</td><td>跟随安装与升级</td><td>快捷方式无参数，安全警告为零</td></tr><tr><td>Internal Diagnosis</td><td>开发、QA、支持人员</td><td>每次诊断独立目录</td><td>按问题最小开启</td><td>收集证据后关闭</td><td>能复现问题，移除参数后恢复默认</td></tr><tr><td>Automation&#x2F;Eval</td><td>CI、Agent Harness、评测系统</td><td>每个 Run 独立目录</td><td>Pipe 优先，Port 按需</td><td>由 Harness 创建和回收</td><td>无 Profile 串扰，无常驻调试端口</td></tr></tbody></table><p>Production 的“无参数”不是绝对禁止 Chromium 内部派生子进程参数。Browser Process 启动 Renderer、GPU 等子进程时本来就会生成内部命令行。这里约束的是安装器、桌面快捷方式和用户入口不得额外注入产品级覆盖参数。</p><h2 id="参数矩阵：哪些能用，哪些只该出现在实验里"><a href="#参数矩阵：哪些能用，哪些只该出现在实验里" class="headerlink" title="参数矩阵：哪些能用，哪些只该出现在实验里"></a>参数矩阵：哪些能用，哪些只该出现在实验里</h2><p>下面这张表可以直接作为 Code Review 和 Release Gate 的基础。</p><table><thead><tr><th>参数或能力</th><th>Production</th><th>Internal Diagnosis</th><th>Automation&#x2F;Eval</th><th>判断</th></tr></thead><tbody><tr><td>无额外参数</td><td>推荐</td><td>可作为对照组</td><td>可作为基线</td><td>正式快捷方式的默认状态</td></tr><tr><td><code>--no-sandbox</code></td><td>禁止</td><td>仅离线、一次性底层调试</td><td>禁止</td><td>会移除关键安全边界</td></tr><tr><td><code>--disable-gpu-sandbox</code></td><td>禁止</td><td>通常也不需要</td><td>禁止</td><td>不应把 GPU 故障转化为 Sandbox 降级</td></tr><tr><td><code>--disable-gpu</code></td><td>不默认使用</td><td>GPU 故障 A&#x2F;B 对照</td><td>仅软件渲染基线</td><td>关闭硬件加速，不是通用兼容方案</td></tr><tr><td><code>--remote-debugging-pipe</code></td><td>禁止</td><td>工具支持时可用</td><td>推荐由 Harness 管理</td><td>不需要常驻 TCP 监听</td></tr><tr><td><code>--remote-debugging-port</code></td><td>禁止</td><td>受控临时使用</td><td>Harness 不支持 Pipe 时使用</td><td>必须配独立 <code>--user-data-dir</code></td></tr><tr><td><code>--user-data-dir=&lt;path&gt;</code></td><td>用户快捷方式不使用</td><td>推荐独立目录</td><td>每个 Run 必须唯一</td><td>隔离 Cookie、缓存、Extension 和 Local State</td></tr><tr><td><code>--proxy-server</code> &#x2F; <code>--proxy-pac-url</code></td><td>优先系统设置或 Policy</td><td>可用于网络复现</td><td>网络场景测试可用</td><td>代理本身也是安全和数据边界</td></tr><tr><td><code>--ignore-certificate-errors</code></td><td>禁止</td><td>不推荐</td><td>不推荐</td><td>应安装受控测试 CA，而不是关闭校验</td></tr><tr><td><code>--enable-logging --v=1</code></td><td>不常驻</td><td>推荐按需使用</td><td>失败重试或专项 Trace 使用</td><td>日志有成本，也可能包含敏感上下文</td></tr><tr><td><code>--js-flags=--max-old-space-size=N</code></td><td>默认不使用</td><td>仅用于验证内存假设</td><td>可作为明确实验变量</td><td>提高上限不等于修复泄漏或减少 RSS</td></tr><tr><td><code>--single-process</code></td><td>禁止</td><td>不作为真实问题证据</td><td>禁止</td><td>Chromium 官方文档也提醒它会制造或掩盖问题</td></tr></tbody></table><p>Chromium 的 switches 不是面向终端用户承诺稳定的产品配置 API。每次升级 Chromium baseline，都应该重新核对当前源码和真实二进制行为；不能因为某个参数在旧版本“能跑”，就把它永久固化。</p><h2 id="GPU：先让-Chromium-自己判断，再做单变量实验"><a href="#GPU：先让-Chromium-自己判断，再做单变量实验" class="headerlink" title="GPU：先让 Chromium 自己判断，再做单变量实验"></a>GPU：先让 Chromium 自己判断，再做单变量实验</h2><p><code>--disable-gpu</code> 的含义很直接：关闭 GPU hardware acceleration；如果软件 Renderer 不可用，GPU Process 甚至不会启动。Chromium 本身已经维护 <a href="https://chromium.googlesource.com/chromium/src/+/refs/heads/main/gpu/config/software_rendering_list.README">GPU software rendering list</a>，按 OS、设备、Driver 和已知问题决定哪些能力需要 blocklist。Chrome Enterprise 的 <a href="https://chromeenterprise.google/policies/hardware-acceleration-mode-enabled/">HardwareAccelerationModeEnabled</a> 也明确把默认行为定义为“可用时启用硬件加速”。</p><p>所以，遇到虚拟机、Windows Server 或远程桌面环境，正确顺序是：</p><ol><li>在 <code>chrome://gpu</code> 记录 GPU、Driver、Feature Status 和 Problems；</li><li>更新或回退 Driver，确认 RDP&#x2F;VM 的图形栈和 Software Renderer 是否可用；</li><li>用 <code>--disable-gpu</code> 做一次 A&#x2F;B 实验，确认症状是否真的来自 GPU 路径；</li><li>如果需要长期禁用，使用受管 Policy，并限定到已验证的设备群；</li><li>保留启动耗时、页面渲染、视频、WebGL、功耗和 AI UI 的对照结果。</li></ol><p>AI Browser 还要多问一句：出问题的是页面合成 GPU，还是本地模型 Runtime 使用的 CUDA、DirectML 或其他推理后端？前者的 flag 不一定影响后者。把两条链路混在一起，会得到“问题偶尔消失，但没人知道为什么”的假修复。</p><h2 id="Sandbox：任何正式入口都不该关闭"><a href="#Sandbox：任何正式入口都不该关闭" class="headerlink" title="Sandbox：任何正式入口都不该关闭"></a>Sandbox：任何正式入口都不该关闭</h2><p>Chromium 的 <a href="https://chromium.googlesource.com/chromium/src/+/main/docs/design/sandbox.md">Windows Sandbox 设计</a> 使用 Restricted Token、Job Object、Desktop Object 和 Integrity Level 限制 Renderer 等不可信进程。它的目的不是消除漏洞，而是限制漏洞能触达的文件、Registry、进程和用户数据。</p><p><code>--no-sandbox</code> 会直接拆掉这层边界。Chromium 的 <a href="https://chromium.googlesource.com/chromium/src/+/HEAD/docs/debugging.md">Debugging 文档</a> 虽然把它列为方便附加 Debugger 的手段，同时也明确提醒安全影响；同一份文档还说明 <code>--single-process</code> 不能代表 Chromium 的真实多进程运行方式。</p><p>我的规则很简单：</p><ul><li>Production、灰度、真实账号验收都禁止 <code>--no-sandbox</code>；</li><li>自动化访问真实或不可完全信任的网页时同样禁止；</li><li>只有在隔离 VM、无真实 Profile、无生产凭据、无外网数据的一次性底层调试中，才允许把它作为最后手段；</li><li>实验结束必须删除启动脚本或让配置自动过期，不能复制进快捷方式。</li></ul><p>如果 Sandbox 让 Native Host、文件访问或调试流程失败，优先修复 IPC、Manifest、权限和进程边界。关闭 Sandbox 只会让原本清晰的工程问题变成产品级风险。</p><h2 id="Remote-Debugging-与-User-Data-Directory-必须成对管理"><a href="#Remote-Debugging-与-User-Data-Directory-必须成对管理" class="headerlink" title="Remote Debugging 与 User Data Directory 必须成对管理"></a>Remote Debugging 与 User Data Directory 必须成对管理</h2><p>Remote Debugging 对 AI Browser 很有用：Agent Eval 可以读取页面、执行操作并收集 Trace。但它也能访问高价值浏览器状态。Chrome 团队披露过攻击者利用远程调试能力窃取 Cookie 的案例，因此从 Chrome 136 开始，<code>--remote-debugging-port</code> 和 <code>--remote-debugging-pipe</code> 在默认 Chrome User Data Directory 上不再生效，必须同时指定非默认目录。官方还建议浏览器自动化使用 Chrome for Testing。<a href="https://developer.chrome.com/blog/remote-debugging-port?hl=en">这项变更的安全说明</a> 值得所有 Chromium Distribution 维护者阅读。</p><p>Chromium 的 <a href="https://chromium.googlesource.com/chromium/src/+/master/docs/user_data_dir.md">User Data Directory 文档</a> 说明，这个目录不只是缓存，它包含 History、Bookmarks、Cookie、各 Profile 与安装级 Local State。因此：</p><ul><li>不把 Remote Debugging 接到用户日常 Profile；</li><li>每个并行 Run 使用唯一目录，不共享同一个 <code>C:\Temp\profile</code>；</li><li>Harness 优先使用 Pipe；必须用 Port 时，让 Runner 分配和回收，不固定写死 <code>9222</code>；</li><li>进程完全退出后再删除目录，避免未刷盘状态和并发损坏；</li><li>需要登录态的 Eval 使用专门测试账号和可重建状态，不复制真实用户目录。</li></ul><h2 id="Proxy-与证书：保留真实的-Windows-信任边界"><a href="#Proxy-与证书：保留真实的-Windows-信任边界" class="headerlink" title="Proxy 与证书：保留真实的 Windows 信任边界"></a>Proxy 与证书：保留真实的 Windows 信任边界</h2><p>Chromium 默认使用系统网络设置。官方的 <a href="https://chromium.googlesource.com/website/+/refs/heads/main/site/developers/design-documents/network-settings/index.md">Network Settings 设计说明</a> 同时列出了 <code>--proxy-server</code>、<code>--proxy-pac-url</code> 和 bypass list 等覆盖方式；更完整的 <a href="https://chromium.googlesource.com/chromium/src/+/main/net/docs/proxy.md">Proxy 文档</a> 还解释了 HTTP、HTTPS、SOCKS、DNS 和 fallback 的差异。</p><p>正式企业部署应优先使用 Windows 系统配置或 Enterprise Policy，因为管理员能统一审计、撤销和更新。启动参数适合复现“指定 Proxy&#x2F;PAC 下才失败”的诊断场景，以及明确要求不同网络拓扑的 Eval。</p><p>证书则不要走捷径。<code>--ignore-certificate-errors</code> 会让测试越过你真正需要验证的 TLS 行为，既扩大攻击面，也可能把证书链、代理中间人和时间错误一起掩盖。更可靠的做法是在隔离测试机安装受控测试 CA，限定域名和环境，并让用例同时覆盖证书有效、过期、名称不匹配与撤销场景。</p><h2 id="Logging-与-Crash：收集证据，不要长期改变运行模式"><a href="#Logging-与-Crash：收集证据，不要长期改变运行模式" class="headerlink" title="Logging 与 Crash：收集证据，不要长期改变运行模式"></a>Logging 与 Crash：收集证据，不要长期改变运行模式</h2><p>Windows 下最小的 Chromium 日志组合是 <code>--enable-logging --v=1</code>。官方 <a href="https://chromium.googlesource.com/website.git/+/main/site/for-testers/enable-logging/index.md">Debug Log 指南</a> 给出了同样的启动方式；如果增加 <code>--log-file</code>，Chromium 当前实现要求 Windows 使用绝对路径。</p><p>日志适合回答“哪个进程、哪个时间点、哪条调用失败”，但不适合永久常驻在用户快捷方式。详细日志可能影响 I&#x2F;O，也可能记录 URL、Extension ID 或请求上下文。诊断包应该包含采集范围、保存目录、脱敏规则和删除时间。</p><p>Crash 处理则应是 Distribution 能力。Chromium 的 <a href="https://chromium.googlesource.com/chromium/src/+/main/docs/stability.md">Stability 文档</a> 说明 Crashpad 会监控 Browser 和子进程，并在用户允许时收集、节流和上传 crash dump。对于没有接入自有 Crash Reporter 的独立 Windows 原生进程，可以临时启用 Microsoft 官方的 <a href="https://learn.microsoft.com/en-us/windows/win32/wer/collecting-user-mode-dumps">WER LocalDumps</a>；需要按异常、挂起或 CPU 条件抓取现场时，可以使用 <a href="https://learn.microsoft.com/en-us/sysinternals/downloads/procdump">ProcDump</a>。浏览器已经集成 Crashpad 时应先遵从自身 Crash Reporter 的设计，问题结束后撤销额外采集配置，Dump 也要按敏感数据管理。</p><h2 id="V8-Heap：4096-不是-AI-浏览器的默认答案"><a href="#V8-Heap：4096-不是-AI-浏览器的默认答案" class="headerlink" title="V8 Heap：4096 不是 AI 浏览器的默认答案"></a>V8 Heap：<code>4096</code> 不是 AI 浏览器的默认答案</h2><p><code>--js-flags=--max-old-space-size=4096</code> 看起来像是“给 AI 页面更多内存”，但 <a href="https://chromium.googlesource.com/v8/v8/+/main/src/flags/flag-definitions.h">V8 的 flag 定义</a> 只说明它设置 Old Space 的最大值，单位为 MB。它不会在启动时立刻分配 4 GB，也不会自动降低 RSS，更不会修复对象长期存活、跨进程重复缓存或 Native Runtime 泄漏。</p><p>在多 Renderer 浏览器里，全局放大上限还会改变 OOM、GC 和崩溃出现的时间。原本能快速暴露的泄漏，可能变成更晚、更重的系统内存压力。</p><p>只有满足下面条件，我才会保留 V8 Heap 覆盖：</p><ul><li>Trace 已证明压力发生在目标 V8 Isolate，而不是 Browser、GPU 或 Native Runtime；</li><li>业务工作集有明确上界，正常 GC 后仍需要更大 Old Space；</li><li>对照测试覆盖低内存 Windows 设备、长会话、多个 Tab 和 Extension；</li><li>参数能限定到目标 Renderer，而不是所有用户和所有页面；</li><li>版本升级时重新跑 Memory Regression，而不是永久继承旧数字。</li></ul><p>没有这些证据时，空数组或不传 <code>--js-flags</code> 才是更专业的默认值。</p><h2 id="三套可直接落地的配置"><a href="#三套可直接落地的配置" class="headerlink" title="三套可直接落地的配置"></a>三套可直接落地的配置</h2><h3 id="Production：快捷方式只指向-EXE"><a href="#Production：快捷方式只指向-EXE" class="headerlink" title="Production：快捷方式只指向 EXE"></a>Production：快捷方式只指向 EXE</h3><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">Target: &quot;C:\Program Files\Example AI Browser\Application\browser.exe&quot;</span><br><span class="line">Arguments: &lt;empty&gt;</span><br><span class="line">Start in: &quot;C:\Program Files\Example AI Browser\Application&quot;</span><br></pre></td></tr></table></figure><p>品牌、更新通道、内置 Extension、Native Host、默认 Policy 和 Runtime 发现应由安装包与产品配置负责。不要用快捷方式参数替代正式配置面。</p><p>安装后可以用 PowerShell 直接验收 <code>.lnk</code>：</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="variable">$shortcutPath</span> = <span class="built_in">Join-Path</span> <span class="variable">$env:PUBLIC</span> <span class="string">&#x27;Desktop\Example AI Browser.lnk&#x27;</span></span><br><span class="line"><span class="variable">$shell</span> = <span class="built_in">New-Object</span> <span class="literal">-ComObject</span> WScript.Shell</span><br><span class="line"><span class="variable">$shortcut</span> = <span class="variable">$shell</span>.CreateShortcut(<span class="variable">$shortcutPath</span>)</span><br><span class="line"></span><br><span class="line">[<span class="type">pscustomobject</span>]<span class="selector-tag">@</span>&#123;</span><br><span class="line">  TargetPath       = <span class="variable">$shortcut</span>.TargetPath</span><br><span class="line">  Arguments        = <span class="variable">$shortcut</span>.Arguments</span><br><span class="line">  WorkingDirectory = <span class="variable">$shortcut</span>.WorkingDirectory</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> (<span class="operator">-not</span> [<span class="built_in">string</span>]::IsNullOrWhiteSpace(<span class="variable">$shortcut</span>.Arguments)) &#123;</span><br><span class="line">  <span class="keyword">throw</span> <span class="string">&quot;Production shortcut must not contain arguments: <span class="variable">$</span>(<span class="variable">$shortcut</span>.Arguments)&quot;</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="Internal-Diagnosis：隔离-Profile，加最少日志"><a href="#Internal-Diagnosis：隔离-Profile，加最少日志" class="headerlink" title="Internal Diagnosis：隔离 Profile，加最少日志"></a>Internal Diagnosis：隔离 Profile，加最少日志</h3><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="variable">$browser</span> = <span class="string">&#x27;C:\Program Files\Example AI Browser\Application\browser.exe&#x27;</span></span><br><span class="line"><span class="variable">$runRoot</span> = <span class="built_in">Join-Path</span> <span class="variable">$env:TEMP</span> (<span class="string">&quot;ai-browser-diag-&quot;</span> + (<span class="built_in">Get-Date</span> <span class="literal">-Format</span> <span class="string">&#x27;yyyyMMdd-HHmmss&#x27;</span>))</span><br><span class="line"><span class="variable">$profile</span> = <span class="built_in">Join-Path</span> <span class="variable">$runRoot</span> <span class="string">&#x27;profile&#x27;</span></span><br><span class="line"><span class="variable">$log</span> = <span class="built_in">Join-Path</span> <span class="variable">$runRoot</span> <span class="string">&#x27;browser.log&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">New-Item</span> <span class="literal">-ItemType</span> Directory <span class="literal">-Path</span> <span class="variable">$profile</span> <span class="literal">-Force</span> | <span class="built_in">Out-Null</span></span><br><span class="line"></span><br><span class="line">&amp; <span class="variable">$browser</span> `</span><br><span class="line">  <span class="string">&quot;--user-data-dir=<span class="variable">$profile</span>&quot;</span> `</span><br><span class="line">  <span class="string">&#x27;--enable-logging&#x27;</span> `</span><br><span class="line">  <span class="string">&#x27;--v=1&#x27;</span> `</span><br><span class="line">  <span class="string">&quot;--log-file=<span class="variable">$log</span>&quot;</span></span><br></pre></td></tr></table></figure><p>先用这组配置复现，再按假设一次只增加一个参数，例如 <code>--disable-gpu</code>。同时改变 GPU、Sandbox、Proxy 和 V8 Heap，只能证明“某个组合改变了现象”，不能定位原因。</p><h3 id="Automation-Eval：让-Harness-拥有调试通道"><a href="#Automation-Eval：让-Harness-拥有调试通道" class="headerlink" title="Automation&#x2F;Eval：让 Harness 拥有调试通道"></a>Automation&#x2F;Eval：让 Harness 拥有调试通道</h3><p>下面是 Runner 的配置契约示意，不是直接传给 Chromium 的 JSON：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;binary&quot;</span><span class="punctuation">:</span> <span class="string">&quot;C:\\Program Files\\Example AI Browser\\Application\\browser.exe&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;profile&quot;</span><span class="punctuation">:</span> <span class="string">&quot;C:\\Temp\\ai-browser-eval\\&lt;run-id&gt;&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;debugTransport&quot;</span><span class="punctuation">:</span> <span class="string">&quot;pipe&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;headless&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">false</span></span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;extraArgs&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><p>Harness 负责把 <code>profile</code> 转为独立 <code>--user-data-dir</code>，创建 Pipe 或临时 Port，记录实际 Browser version、参数和 Run ID，并在所有子进程退出后回收目录。ChromeDriver 的 <a href="https://developer.chrome.com/docs/chromedriver/capabilities">Capabilities 文档</a> 也采用这一责任边界：Driver 默认创建临时 Profile，需要自定义时再明确传入 <code>user-data-dir</code>。</p><p>Headless 可以提高部分 Eval 的吞吐，但 AI Browser 最终还要有 Headful Release 验收。Extension UI、系统权限提示、Native Host 拉起、GPU 合成和 Windows 窗口行为，都不能只靠 Headless 结果证明。</p><h2 id="两种最常见的错误配置"><a href="#两种最常见的错误配置" class="headerlink" title="两种最常见的错误配置"></a>两种最常见的错误配置</h2><p>第一种是把所有“可能有帮助”的参数一次加齐：</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">&amp; <span class="variable">$browser</span> `</span><br><span class="line">  <span class="string">&#x27;--no-sandbox&#x27;</span> `</span><br><span class="line">  <span class="string">&#x27;--disable-gpu&#x27;</span> `</span><br><span class="line">  <span class="string">&#x27;--ignore-certificate-errors&#x27;</span> `</span><br><span class="line">  <span class="string">&#x27;--remote-debugging-port=9222&#x27;</span> `</span><br><span class="line">  <span class="string">&#x27;--js-flags=--max-old-space-size=4096&#x27;</span></span><br></pre></td></tr></table></figure><p>这条命令同时改变安全隔离、渲染路径、TLS、调试攻击面和内存行为。即使浏览器不崩了，也没有形成可复用结论，更不可能作为 Production 默认值。</p><p>第二种是让多个自动化任务共享同一个 Profile 和固定端口。它会带来 Cookie、Cache、Extension 状态和 Service Worker 串扰，还可能让一个 Run 连接到另一个 Run 的 Browser。测试偶尔变快，代价是结果不再可信。</p><h2 id="Windows-安装包验收-Checklist"><a href="#Windows-安装包验收-Checklist" class="headerlink" title="Windows 安装包验收 Checklist"></a>Windows 安装包验收 Checklist</h2><p>正式发布前，我会把下面这些检查放进自动测试和目标 Windows 真机验收：</p><ul><li><input disabled="" type="checkbox"> Start Menu、Desktop、Taskbar shortcut 的 <code>Arguments</code> 都为空；</li><li><input disabled="" type="checkbox"> 首次启动和重启后没有 unsupported command-line flag 警告；</li><li><input disabled="" type="checkbox"> <code>chrome://version</code> 中 Browser Process 没有 <code>--no-sandbox</code>、<code>--disable-gpu</code>、Remote Debugging 或证书绕过参数；</li><li><input disabled="" type="checkbox"> <code>chrome://sandbox</code> 显示 Renderer、GPU 等目标进程处于预期 Sandbox；</li><li><input disabled="" type="checkbox"> <code>chrome://gpu</code> 的 Hardware Acceleration 状态符合 Driver、Policy 和 blocklist 结果；</li><li><input disabled="" type="checkbox"> AI 对话、Extension、Native Host、本地 Runtime 和更新链路分别完成一次真实任务；</li><li><input disabled="" type="checkbox"> Automation Run 使用唯一 Profile，结束后没有残留 Browser、调试端口和目录锁；</li><li><input disabled="" type="checkbox"> 日志与 Dump 的采集、脱敏、保留和删除策略可验证；</li><li><input disabled="" type="checkbox"> Installer 升级、修复安装和卸载重装不会把诊断参数重新写回快捷方式；</li><li><input disabled="" type="checkbox"> 每次 Chromium baseline 升级后重新跑 GPU、Sandbox、Profile、Automation 与 Memory Regression。</li></ul><p>还可以用 CIM 对 Browser Process 做一道自动检查。子进程会带 Chromium 自己生成的 <code>--type=</code> 等参数，因此只筛选没有 <code>--type=</code> 的主进程：</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="variable">$browserProcesses</span> = <span class="built_in">Get-CimInstance</span> Win32_Process <span class="literal">-Filter</span> <span class="string">&quot;Name=&#x27;browser.exe&#x27;&quot;</span> |</span><br><span class="line">  <span class="built_in">Where-Object</span> &#123; <span class="variable">$_</span>.CommandLine <span class="operator">-notmatch</span> <span class="string">&#x27;--type=&#x27;</span> &#125;</span><br><span class="line"></span><br><span class="line"><span class="variable">$forbidden</span> = <span class="string">&#x27;--no-sandbox|--disable-gpu-sandbox|--ignore-certificate-errors|--remote-debugging-(port|pipe)&#x27;</span></span><br><span class="line"><span class="variable">$violations</span> = <span class="variable">$browserProcesses</span> | <span class="built_in">Where-Object</span> &#123; <span class="variable">$_</span>.CommandLine <span class="operator">-match</span> <span class="variable">$forbidden</span> &#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> (<span class="variable">$violations</span>) &#123;</span><br><span class="line">  <span class="variable">$violations</span> | <span class="built_in">Select-Object</span> ProcessId, CommandLine | <span class="built_in">Format-List</span></span><br><span class="line">  <span class="keyword">throw</span> <span class="string">&#x27;Forbidden production launch argument detected.&#x27;</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>把示例中的 <code>browser.exe</code> 换成实际可执行文件名，并在干净安装、升级安装和自动更新后各跑一次。</p><h2 id="要不要用：我的判断框架"><a href="#要不要用：我的判断框架" class="headerlink" title="要不要用：我的判断框架"></a>要不要用：我的判断框架</h2><p>正式版先做到零强制参数。GPU、Sandbox、Proxy、证书和 V8 Heap 不应因为“某台机器曾经有问题”就变成所有用户的永久配置。</p><p>内部诊断值得立即建立标准脚本：独立 Profile、绝对日志路径、单变量实验、自动过期。Automation&#x2F;Eval 则由 Harness 独占 Remote Debugging 和 Profile 生命周期，参数清单随 Run 进入 Evidence，而不是进入用户快捷方式。</p><p>一个参数只有同时回答了“明确故障、最小作用域、回退条件、跨版本验收”四件事，才有资格进入受管配置。其余参数都应该留在实验记录里。生产启动越干净，AI Browser 的 Renderer、Extension、Native Runtime 和自动化边界反而越容易长期维护。</p><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><ul><li><a href="https://chromium.googlesource.com/chromium/src/+/main/docs/design/sandbox.md">Chromium Windows Sandbox Design</a></li><li><a href="https://chromium.googlesource.com/chromium/src/+/HEAD/docs/debugging.md">Chromium Debugging Guide</a></li><li><a href="https://developer.chrome.com/blog/remote-debugging-port?hl=en">Chrome 136 Remote Debugging Security Change</a></li><li><a href="https://chromium.googlesource.com/chromium/src/+/master/docs/user_data_dir.md">Chromium User Data Directory</a></li><li><a href="https://chromeenterprise.google/policies/hardware-acceleration-mode-enabled/">Chrome Enterprise Hardware Acceleration Policy</a></li><li><a href="https://chromium.googlesource.com/chromium/src/+/main/net/docs/proxy.md">Chromium Proxy Configuration</a></li><li><a href="https://chromium.googlesource.com/chromium/src/+/main/docs/stability.md">Chromium Stability and Crashpad</a></li><li><a href="https://learn.microsoft.com/en-us/windows/win32/wer/collecting-user-mode-dumps">Microsoft WER LocalDumps</a></li><li><a href="https://learn.microsoft.com/en-us/sysinternals/downloads/procdump">Microsoft Sysinternals ProcDump</a></li><li><a href="https://chromium.googlesource.com/v8/v8/+/main/src/flags/flag-definitions.h">V8 Flag Definitions</a></li></ul><blockquote><p>本文使用 <a href="https://github.com/zisheng-ai/writting-skill">writting-skill</a> 辅助写作，配图使用 <a href="https://github.com/zisheng-ai/better-imagegen">better-imagegen</a> 生成。项目已开源，欢迎在 GitHub 点个 Star。</p></blockquote>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/67206851ee19/</id>
    <link href="https://blog.zisheng.pro/posts/67206851ee19/"/>
    <published>2026-08-26T00:05:15.000Z</published>
    <summary>Windows 下的 Chromium-based AI 浏览器不该靠一串启动参数维持生产可用。本文给出 Production、Internal Diagnosis、Automation/Eval 三套 Profile，逐项判断 GPU、Sandbox、Remote Debugging、Profile、Proxy、Crash 与 V8 参数的边界，并提供可复制配置和安装验收清单。</summary>
    <title>Windows 下 AI 浏览器的最佳启动参数配置</title>
    <updated>2026-09-18T08:15:28.632Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="软件工程" scheme="https://blog.zisheng.pro/categories/%E8%BD%AF%E4%BB%B6%E5%B7%A5%E7%A8%8B/"/>
    <category term="Omaha" scheme="https://blog.zisheng.pro/tags/Omaha/"/>
    <category term="Chromium Updater" scheme="https://blog.zisheng.pro/tags/Chromium-Updater/"/>
    <category term="Auto Update" scheme="https://blog.zisheng.pro/tags/Auto-Update/"/>
    <category term="CUP" scheme="https://blog.zisheng.pro/tags/CUP/"/>
    <category term="CRX3" scheme="https://blog.zisheng.pro/tags/CRX3/"/>
    <category term="Windows" scheme="https://blog.zisheng.pro/tags/Windows/"/>
    <category term="Release Engineering" scheme="https://blog.zisheng.pro/tags/Release-Engineering/"/>
    <content>
      <![CDATA[<p><img src="/images/omaha-update-protocol.webp" alt="Omaha 自动更新的检查、下载与回报"></p><p>最近在梳理 Chromium 桌面应用的更新链路时，我频繁遇到一个词：<code>Omaha</code>。</p><p>它一会儿出现在 <code>Omaha endpoint</code> 里，一会儿又变成 <code>Omaha Protocol</code>、<code>Omaha Server</code> 或 <code>Omaha response</code>。继续查源码，还会遇到 <code>Chromium Updater</code>、<code>update_client</code>、<code>CUP</code> 和 <code>CRX3</code>。如果把这些名字都当成“自动更新”的同义词，很快就会把客户端、协议、服务端和安装制品混在一起。</p><span id="more"></span><h2 id="一句话总结"><a href="#一句话总结" class="headerlink" title="一句话总结"></a>一句话总结</h2><p><strong>Omaha 最初是 Google Update 的开源客户端项目；在当今 Chromium 更新语境里，它更常指一套更新协议和云端决策接口。</strong> 真正在本机定期检查、下载并安装新版本的，是 Chromium Updater 这类客户端；Omaha Server 回答“这台设备现在应该拿哪个版本、去哪里下载、如何处理”。</p><p>所以，一句“我们要接入 Omaha”，至少要继续追问三件事：</p><ul><li>是否要编译和定制 Chromium Updater 客户端？</li><li>是否要实现 Omaha Protocol 的服务端响应？</li><li>更新制品、签名、CDN 和渠道激活由谁产出和管理？</li></ul><h2 id="同一个-Omaha，其实指三个不同对象"><a href="#同一个-Omaha，其实指三个不同对象" class="headerlink" title="同一个 Omaha，其实指三个不同对象"></a>同一个 Omaha，其实指三个不同对象</h2><p>Google 公开的 <a href="https://github.com/google/omaha">Omaha 仓库</a> 将它定义为 Google Update 的开源版，用于安装软件并保持更新。这份历史客户端代码以 Windows 为主，仓库已于 2025 年 8 月归档为只读。</p><p>现在的 Chromium 源码中，<code>chrome/updater</code> 下的 <a href="https://chromium.googlesource.com/chromium/src/+/main/chrome/updater/">Chromium Updater</a> 是 Google Update&#x2F;Omaha&#x2F;Keystone 的开源可定制替代者，目标平台包括 Windows、macOS 和 Linux。但客户端更名与替换后，它与服务端交流时仍然使用 Omaha Protocol。</p><table><thead><tr><th>对象</th><th>是什么</th><th>负责什么</th></tr></thead><tbody><tr><td>Omaha 历史项目</td><td>Google Update 的开源客户端</td><td>安装软件、周期检查、下载和应用更新</td></tr><tr><td>Omaha Protocol</td><td>客户端与更新基础设施之间的 HTTP 应用层协议</td><td>描述已安装应用、版本、平台、渠道、更新指令和执行结果</td></tr><tr><td>Omaha Server</td><td>实现协议的更新决策服务</td><td>识别 App ID，按版本、平台、渠道或 cohort 返回 <code>noupdate</code> 或更新 pipeline</td></tr></tbody></table><p>这也解释了为什么 Chromium 文档里会出现“Chromium Updater 轮询 Omaha Server”这种句子：前者是本机执行者，后者是云端决策者。</p><h2 id="一次完整的-Omaha-更新发生了什么"><a href="#一次完整的-Omaha-更新发生了什么" class="headerlink" title="一次完整的 Omaha 更新发生了什么"></a>一次完整的 Omaha 更新发生了什么</h2><p><a href="https://chromium.googlesource.com/chromium/src/+/main/docs/updater/protocol_4.md">Omaha Protocol 4.0</a> 把一次更新会话拆成三个阶段：Update Check、Download 和 Ping-Back。截至 2026 年 8 月 25 日，这份公开文档仍标注为 draft；真正实现时应固定到所使用的 Chromium commit，而不是假设 <code>main</code> 永远不变。</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">已安装客户端</span><br><span class="line">  │</span><br><span class="line">  ├─ 1. POST 当前 App ID、版本、OS、CPU、渠道</span><br><span class="line">  ▼</span><br><span class="line">Omaha Server</span><br><span class="line">  │</span><br><span class="line">  ├─ 2. 返回 noupdate，或新版本 + 下载/处理 pipeline</span><br><span class="line">  ▼</span><br><span class="line">Download Server / CDN</span><br><span class="line">  │</span><br><span class="line">  ├─ 3. GET 更新 payload，校验大小、hash 与签名</span><br><span class="line">  ▼</span><br><span class="line">Chromium Updater</span><br><span class="line">  │</span><br><span class="line">  ├─ 4. 解包、执行 installer、切换版本</span><br><span class="line">  └─ 5. POST 成功、失败或取消事件</span><br></pre></td></tr></table></figure><h3 id="1-Update-Check：客户端不只上报一个版本号"><a href="#1-Update-Check：客户端不只上报一个版本号" class="headerlink" title="1. Update Check：客户端不只上报一个版本号"></a>1. Update Check：客户端不只上报一个版本号</h3><p>客户端先用 HTTP POST 发送更新检查。请求中可以包含多个受管应用，每个应用都有独立的 <code>appid</code>、<code>version</code>、<code>cohort</code> 和 <code>updatecheck</code>。操作系统、CPU architecture、安装范围和前台&#x2F;后台触发等信息，都可能改变服务端的决策。</p><p>下面是一份刻意缩减过的示意请求，用于展示结构，不是可直接投产的完整 payload：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;request&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;protocol&quot;</span><span class="punctuation">:</span> <span class="string">&quot;4.0&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;@updater&quot;</span><span class="punctuation">:</span> <span class="string">&quot;chromium-updater&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;updaterversion&quot;</span><span class="punctuation">:</span> <span class="string">&quot;148.0.7778.97&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;os&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">      <span class="attr">&quot;platform&quot;</span><span class="punctuation">:</span> <span class="string">&quot;win&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;arch&quot;</span><span class="punctuation">:</span> <span class="string">&quot;x64&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;apps&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;appid&quot;</span><span class="punctuation">:</span> <span class="string">&quot;&#123;YOUR-APP-ID&#125;&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.2.3.4&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;cohort&quot;</span><span class="punctuation">:</span> <span class="string">&quot;stable&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;updatecheck&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span><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><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>appid</code> 是更新身份，不是展示名称。版本则是服务端和客户端共同消费的比较值。在 Chromium 发行工程里，我更倾向为它保留一条独立、单调递增的数值版本，而不是直接复用用户看到的 SemVer、npm package version 或 Git tag。它们的消费者和推进条件不同。</p><h3 id="2-Server-Decision：响应描述的是-pipeline"><a href="#2-Server-Decision：响应描述的是-pipeline" class="headerlink" title="2. Server Decision：响应描述的是 pipeline"></a>2. Server Decision：响应描述的是 pipeline</h3><p>服务端不会把安装包塞进 Update Check 的 JSON response。它先回答 <code>noupdate</code> 或 <code>ok</code>；当有新版本时，再提供 <code>nextversion</code> 与一组按优先级排列的 pipeline。</p><p>在 Protocol 4 中，pipeline 是一系列 operation。它可以表达：</p><ul><li>从哪些 URL 下载 payload；</li><li>预期的 size 和 SHA-256 hash；</li><li>是否经过 <code>xz</code>、Zucchini 或 Puffin 等压缩&#x2F;差分操作；</li><li>如何处理 <code>crx3</code> 并运行安装程序。</li></ul><p>服务端可以根据渠道、cohort、平台、当前版本和 rollout 策略对不同设备做不同决策。这才是 Omaha Server 的价值：<strong>它不只存放“最新版本”，还决定谁现在应该收到什么。</strong></p><h3 id="3-Download-与-Install：CDN-不替你执行更新"><a href="#3-Download-与-Install：CDN-不替你执行更新" class="headerlink" title="3. Download 与 Install：CDN 不替你执行更新"></a>3. Download 与 Install：CDN 不替你执行更新</h3><p>客户端收到 pipeline 后，才向 Download Server 或 CDN 发起 HTTP GET。下载成功只能证明一串 bytes 到了本地；Chromium Updater 还要校验大小与 hash，处理 CRX3，执行 installer，并根据 user&#x2F;system scope 处理权限、进程和安装状态。</p><p><a href="https://chromium.googlesource.com/chromium/src/+/main/docs/updater/functional_spec.md">Chromium Updater Functional Specification</a> 指出，Updater 接受 CRX3 更新包，包使用 publisher key 签名，相应公钥固化在客户端中；它还可以额外 pin 一枚 public key。在 Windows 上，最终执行的 Updater 与 installer 还应有正确的 Authenticode 签名。</p><h3 id="4-Ping-Back：没有回报，发布者就看不到失败发生在哪里"><a href="#4-Ping-Back：没有回报，发布者就看不到失败发生在哪里" class="headerlink" title="4. Ping-Back：没有回报，发布者就看不到失败发生在哪里"></a>4. Ping-Back：没有回报，发布者就看不到失败发生在哪里</h3><p>安装尝试后，客户端会把结果回传给 Omaha Server。Protocol 4 为 download、CRX3 安装、installer 和其他 operation 定义了事件类型、结果、error category 与 error code。</p><p>这条回路不是装饰性 telemetry。如果只知道新版本文件已经上传，却看不到客户端是在检查、下载、验签、解包还是 installer 阶段失败，更新系统就没有可操作的反馈。</p><h2 id="CUP、CRX3-和-Authenticode-为什么一个都不能少"><a href="#CUP、CRX3-和-Authenticode-为什么一个都不能少" class="headerlink" title="CUP、CRX3 和 Authenticode 为什么一个都不能少"></a>CUP、CRX3 和 Authenticode 为什么一个都不能少</h2><p>自动更新有一个很高的安全门槛：服务端的一次响应，有机会让大量客户端自动下载并执行代码。TLS 很重要，但 Chromium 的更新链路没有把全部信任都压在 TLS 上。</p><table><thead><tr><th>机制</th><th>主要保护的对象</th><th>能证明什么</th><th>不能代替什么</th></tr></thead><tbody><tr><td>TLS</td><td>HTTP 连接</td><td>传输加密和站点身份</td><td>不等于 payload 发布者签名</td></tr><tr><td>CUP</td><td>Update Check 的 request&#x2F;response 组合</td><td>响应完整性、新鲜度，以及对当次请求的绑定</td><td>不提供内容保密，不代替包签名</td></tr><tr><td>CRX3 publisher signature</td><td>更新 payload</td><td>包来自客户端信任的 publisher key</td><td>不代替 OS 对可执行文件的信任判断</td></tr><tr><td>Authenticode</td><td>Windows 可执行文件</td><td>文件的签名者与签名后完整性</td><td>不代替 Omaha 渠道决策或 CUP</td></tr></tbody></table><p><a href="https://chromium.googlesource.com/chromium/src/+/main/docs/updater/cup.md">CUP 文档</a> 对边界写得很明确：它通过 ECDSA key pair 保护请求体和响应体的完整性，并用 nonce 保护响应新鲜度；客户端内置公钥，服务端保存私钥。它不提供请求或响应的保密性，也不保护 HTTP header。</p><p>这意味着，自建 Omaha Server 时不能只写一个返回 JSON 的 API。CUP public key 必须在构建期进入客户端 trust root，服务端必须正确保管对应 private key，还要为旧客户端设计 key rotation 与历史 key 兼容。</p><h2 id="Omaha-不是-CDN、安装包或“最新版本-JSON”"><a href="#Omaha-不是-CDN、安装包或“最新版本-JSON”" class="headerlink" title="Omaha 不是 CDN、安装包或“最新版本 JSON”"></a>Omaha 不是 CDN、安装包或“最新版本 JSON”</h2><p>在工程实践里，最容易跑偏的是用一个 <code>update.json</code> 包办整条更新链路。如果客户端只是拉取：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;version&quot;</span><span class="punctuation">:</span> <span class="string">&quot;1.2.3&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;url&quot;</span><span class="punctuation">:</span> <span class="string">&quot;https://cdn.example.com/app.exe&quot;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p>这可以是一个自定义 updater 的起点，但它还不是完整的 Omaha 链路。你仍然需要回答：</p><ul><li>App ID 和 user&#x2F;system scope 如何注册、迁移和卸载？</li><li>不同 OS、CPU、channel 和 cohort 如何选择制品？</li><li>旧客户端如何确认 response 没有被替换？</li><li>payload 如何验签，失败如何回退或切换备用 URL？</li><li>下载、解包、安装、重启各阶段的错误如何回报？</li><li>服务过载时，客户端如何 backoff，避免集体重试把服务压垮？</li></ul><p>Omaha Protocol 对这些问题给出了一套经过大规模桌面软件验证的表达方式。但协议完整不等于你的发布系统已经完成：构建、签名、上传、渠道激活和旧客户端验收，仍然是五个独立状态。</p><h2 id="自建-Chromium-桌面应用时，哪些东西必须属于你"><a href="#自建-Chromium-桌面应用时，哪些东西必须属于你" class="headerlink" title="自建 Chromium 桌面应用时，哪些东西必须属于你"></a>自建 Chromium 桌面应用时，哪些东西必须属于你</h2><p>Chromium Updater 是可定制的，但它不会自动让一个 Chromium fork 拥有自主更新能力。至少要管住下面几类产品身份和发布输入：</p><table><thead><tr><th>输入</th><th>为什么必须自有</th></tr></thead><tbody><tr><td>App ID、company&#x2F;updater identity</td><td>避免与其他 updater 冲突，并让客户端稳定识别已安装产品</td></tr><tr><td>Omaha endpoint</td><td>把检查请求发到自己控制的决策服务</td></tr><tr><td>CUP public&#x2F;private key</td><td>建立客户端与服务端之间的更新指令信任根</td></tr><tr><td>CRX3 publisher key</td><td>让客户端只接受预期发布者的更新 payload</td></tr><tr><td>Code signing identity</td><td>满足操作系统对 installer 和 executable 的信任要求</td></tr><tr><td>版本、channel 与 cohort 策略</td><td>决定谁可以在什么时候升级到哪个版本</td></tr><tr><td>不可变 artifact 与可变 descriptor</td><td>先固化可回读的制品，再安全激活发布渠道</td></tr></tbody></table><p>我的建议是，先画清这些责任边界，再决定服务端是自行实现、使用现成实现，还是采用另一套 updater。先选中一个开源项目，再反推产品 identity 和 key 如何放置，很容易留下无法平滑迁移的历史负担。</p><h2 id="要不要用-Omaha：我的判断框架"><a href="#要不要用-Omaha：我的判断框架" class="headerlink" title="要不要用 Omaha：我的判断框架"></a>要不要用 Omaha：我的判断框架</h2><h3 id="值得上"><a href="#值得上" class="headerlink" title="值得上"></a>值得上</h3><p>你在做 Chromium fork、桌面客户端平台，或者需要一套兼容 Chromium Updater 的服务端；产品有明确的 App ID、多渠道或分批发布需求，也愿意持续管理 key、签名、制品和旧客户端兼容。</p><h3 id="可以再等"><a href="#可以再等" class="headerlink" title="可以再等"></a>可以再等</h3><p>你只有一个用户规模很小的内部工具，手动下载新版本的成本可接受；或者你还没有稳定的 code signing、artifact storage 和 release process。这时先把“可重复构建 + 正确签名 + 不可变上传”做稳，比匆忙增加 Omaha Server 更有价值。</p><h3 id="重点关注"><a href="#重点关注" class="headerlink" title="重点关注"></a>重点关注</h3><p>如果决定接入，优先验收一条真实旧客户端链路：发现新版本、验证 CUP response、下载 CRX3、校验 payload、执行已签名 installer、重启后读到新版本，同时保留用户数据。只调通一份 JSON response，证明不了这条链路。</p><p>这也是我现在对 Omaha 的最简单理解：<strong>它是自动更新的决策与交互契约，不是一个替你完成整条发布的黑盒。</strong></p><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><ul><li><a href="https://github.com/google/omaha">Omaha：Google Update for Windows</a></li><li><a href="https://chromium.googlesource.com/chromium/src/+/main/chrome/updater/">Chromium Updater</a></li><li><a href="https://chromium.googlesource.com/chromium/src/+/main/docs/updater/design_doc.md">Chromium Updater Design Document</a></li><li><a href="https://chromium.googlesource.com/chromium/src/+/main/docs/updater/protocol_4.md">Omaha Protocol 4.0</a></li><li><a href="https://chromium.googlesource.com/chromium/src/+/main/docs/updater/cup.md">Client Update Protocol（CUP）</a></li><li><a href="https://chromium.googlesource.com/chromium/src/+/main/docs/updater/functional_spec.md">Chromium Updater Functional Specification</a></li><li><a href="https://chromium.googlesource.com/chromium/src/+/main/docs/updater/dev_manual.md">Chromium Updater Developer’s Manual</a></li></ul><blockquote><p>本文使用 <a href="https://github.com/zisheng-ai/writting-skill">writting-skill</a> 辅助写作。项目已开源，欢迎在 GitHub 点个 Star。</p></blockquote>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/019e8d9d019b/</id>
    <link href="https://blog.zisheng.pro/posts/019e8d9d019b/"/>
    <published>2026-08-25T02:56:36.000Z</published>
    <summary>Omaha 既是 Google Update 的开源项目名，也常被用来指 Chromium 更新协议与服务端。本文拆清 Chromium Updater、Omaha Protocol、CUP、CRX3 和 Authenticode 的责任边界，并串起一次完整的自动更新。</summary>
    <title>Omaha 到底是什么：一篇讲清 Chromium 自动更新的客户端、协议与服务端</title>
    <updated>2026-09-18T08:15:28.629Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="软件工程" scheme="https://blog.zisheng.pro/categories/%E8%BD%AF%E4%BB%B6%E5%B7%A5%E7%A8%8B/"/>
    <category term="Linux" scheme="https://blog.zisheng.pro/tags/Linux/"/>
    <category term="Windows" scheme="https://blog.zisheng.pro/tags/Windows/"/>
    <category term="macOS" scheme="https://blog.zisheng.pro/tags/macOS/"/>
    <category term="CPU Architecture" scheme="https://blog.zisheng.pro/tags/CPU-Architecture/"/>
    <category term="x64" scheme="https://blog.zisheng.pro/tags/x64/"/>
    <category term="arm64" scheme="https://blog.zisheng.pro/tags/arm64/"/>
    <category term="ABI" scheme="https://blog.zisheng.pro/tags/ABI/"/>
    <category term="Cross Compilation" scheme="https://blog.zisheng.pro/tags/Cross-Compilation/"/>
    <content>
      <![CDATA[<p><img src="/images/platform-architecture-package-naming.webp" alt="安装包命名里的 OS、CPU 与 ABI 坐标"></p><p>最近整理多平台发行物时，我又遇到了一排很像“暗号”的文件名：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">tool-v1.8.0-darwin-arm64.tar.gz</span><br><span class="line">tool-v1.8.0-darwin-x64.tar.gz</span><br><span class="line">tool-v1.8.0-win32-x64.zip</span><br><span class="line">tool-v1.8.0-linux-x64-gnu.tar.gz</span><br><span class="line">tool-v1.8.0-linux-arm64-musl.tar.gz</span><br></pre></td></tr></table></figure><p>它们其实没有看上去那么乱。每一段都在回答一个兼容性问题：给哪个操作系统、哪种 CPU、哪套运行环境，以及用什么方式分发。</p><p>真正容易误解的是，大家常把这些维度混成一句“Windows 有 x64 渠道，Linux 也有”。<code>x64</code> 不是渠道，它是 CPU architecture；<code>stable</code>、<code>beta</code>、<code>nightly</code> 才是 release channel。把两者分开后，大部分安装包命名都能直接读懂。</p><span id="more"></span><h2 id="一句话总结"><a href="#一句话总结" class="headerlink" title="一句话总结"></a>一句话总结</h2><p><code>darwin-arm64</code> 可以读成“面向 Darwin&#x2F;macOS 平台、使用 64-bit Arm 指令集的构建”；<code>win32-x64</code> 是“面向 Windows 平台、使用 x86-64 指令集的构建”；<code>linux-x64-gnu</code> 则继续补充了“面向 Linux、使用 x86-64，并依赖 GNU&#x2F;glibc 运行环境”。</p><p>我习惯把完整文件名看成一组坐标：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&lt;product&gt;-&lt;version&gt;-&lt;os&gt;-&lt;cpu&gt;-&lt;abi&gt;-&lt;channel&gt;.&lt;package-format&gt;</span><br></pre></td></tr></table></figure><p>现实中的项目不一定把字段写全，顺序也可能不同，但背后通常逃不开下面几层：</p><table><thead><tr><th>维度</th><th>它回答的问题</th><th>常见值</th></tr></thead><tbody><tr><td>OS &#x2F; platform</td><td>这份程序调用哪套操作系统接口</td><td><code>darwin</code>、<code>win32</code>、<code>windows</code>、<code>linux</code></td></tr><tr><td>CPU architecture</td><td>CPU 能不能执行这套机器指令</td><td><code>x64</code>、<code>x86_64</code>、<code>amd64</code>、<code>arm64</code>、<code>aarch64</code>、<code>ia32</code></td></tr><tr><td>ABI &#x2F; runtime</td><td>二进制如何调用系统库、传参和链接</td><td><code>gnu</code>、<code>musl</code>、<code>msvc</code>、<code>gnueabihf</code></td></tr><tr><td>package format</td><td>用什么容器或安装机制交付</td><td><code>.zip</code>、<code>.tar.gz</code>、<code>.dmg</code>、<code>.msi</code>、<code>.deb</code>、<code>.rpm</code>、<code>AppImage</code></td></tr><tr><td>release channel</td><td>这是哪条发布节奏</td><td><code>stable</code>、<code>beta</code>、<code>canary</code>、<code>nightly</code>、<code>LTS</code></td></tr></tbody></table><p><strong>OS、CPU 和 ABI 决定“能不能跑”，package format 决定“怎么装”，channel 决定“拿哪条版本线”。</strong></p><h2 id="darwin-为什么代表-macOS"><a href="#darwin-为什么代表-macOS" class="headerlink" title="darwin 为什么代表 macOS"></a><code>darwin</code> 为什么代表 macOS</h2><p><code>darwin</code> 不是 Apple 给 macOS 起的下载渠道名，而是 macOS 底层系统家族的名字。</p><p>Apple 在 2000 年发布 Darwin 1.0 时，把它描述为 Mac OS X 的 operating system core，其中包括 Mach kernel 和 BSD layers。今天我们日常使用的 macOS 还包含图形界面、Cocoa framework 和大量闭源组件，但在编译器、Unix 工具链和 Runtime 眼里，<code>darwin</code> 仍是一个稳定的平台标识。</p><p>这也是为什么 Node.js 的 <code>process.platform</code> 在 Mac 上返回 <code>darwin</code>，Rust 会使用 <code>x86_64-apple-darwin</code> 与 <code>aarch64-apple-darwin</code>，Clang&#x2F;LLVM 也保留 Darwin 作为 Apple OS family 的 target 名称。</p><p>所以：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">darwin-arm64 = macOS 平台 + 64-bit Arm</span><br><span class="line">darwin-x64   = macOS 平台 + 64-bit x86</span><br></pre></td></tr></table></figure><p>这里的 <code>darwin</code> 决定程序会按照 Apple 平台的 Mach-O、system framework 与 ABI 规则构建；它不表示这份程序能在所有基于 Darwin 的系统上任意互换。macOS、iOS、Simulator 仍可能拥有不同的 platform 与 SDK 边界。</p><h2 id="win32-为什么也会出现在-64-bit-Windows-上"><a href="#win32-为什么也会出现在-64-bit-Windows-上" class="headerlink" title="win32 为什么也会出现在 64-bit Windows 上"></a><code>win32</code> 为什么也会出现在 64-bit Windows 上</h2><p><code>win32-x64</code> 是最容易让人误判的一种组合：前面写着 <code>32</code>，后面又写 <code>64</code>，到底谁算数？</p><p>答案是两个字段描述的对象不同：</p><ul><li><code>win32</code> 是 Windows platform&#x2F;API 家族的历史名称；</li><li><code>x64</code> 才是当前二进制的 CPU architecture。</li></ul><p>Microsoft 现在更常使用 “Windows API” 这个名字，但官方文档也说明它过去称为 Win32 API，而且这套 API 同样支持 64-bit Windows。Node.js 沿用了 <code>win32</code> 作为 Windows 的 <code>process.platform</code> 固定值，所以 Electron、Node CLI 和不少 npm native package 会自然产生 <code>win32-x64</code>、<code>win32-arm64</code> 这样的标签。</p><p>这几个名字可以这样读：</p><table><thead><tr><th>名称</th><th>实际含义</th></tr></thead><tbody><tr><td><code>win32-ia32</code> &#x2F; <code>win32-x86</code></td><td>Windows + 32-bit x86</td></tr><tr><td><code>win32-x64</code> &#x2F; <code>windows-x64</code> &#x2F; <code>win-amd64</code></td><td>Windows + 64-bit x86</td></tr><tr><td><code>win32-arm64</code> &#x2F; <code>windows-arm64</code></td><td>Windows + 64-bit Arm</td></tr></tbody></table><p>因此，看到 <code>win32</code> 时不要据此判断 CPU 位数。继续找同一文件名里的 <code>ia32</code>、<code>x86</code>、<code>x64</code> 或 <code>arm64</code>，那一段才负责回答架构问题。</p><h2 id="linux-只是第一层，后面往往还要看-libc"><a href="#linux-只是第一层，后面往往还要看-libc" class="headerlink" title="linux 只是第一层，后面往往还要看 libc"></a><code>linux</code> 只是第一层，后面往往还要看 libc</h2><p><code>linux-x64</code> 表示 Linux + 64-bit x86，但对包含 native code 的程序来说，这个坐标有时还不够精确。</p><p>Linux 生态不像 macOS、Windows 那样由单一厂商提供完整的用户态运行环境。相同的 Linux kernel 之上，可以有不同 distribution、libc、dynamic linker 和 system library 版本。于是发行物里经常继续出现：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">x86_64-unknown-linux-gnu</span><br><span class="line">x86_64-unknown-linux-musl</span><br><span class="line">aarch64-unknown-linux-gnu</span><br><span class="line">aarch64-unknown-linux-musl</span><br></pre></td></tr></table></figure><p><code>gnu</code> 通常表示 GNU toolchain &#x2F; glibc environment，<code>musl</code> 表示 musl libc environment。CPU 和 kernel 都对，libc 不匹配，程序仍可能在启动时报告 dynamic linker 不存在、symbol version 找不到，或者某个 native dependency 无法加载。</p><p><code>.deb</code>、<code>.rpm</code>、<code>AppImage</code> 又是另一层：它们主要描述交付与安装方式。</p><table><thead><tr><th>名称片段</th><th>描述对象</th><th>不能单独证明什么</th></tr></thead><tbody><tr><td><code>linux-arm64</code></td><td>OS + CPU</td><td>不保证 libc 与最低系统版本兼容</td></tr><tr><td><code>gnu</code> &#x2F; <code>musl</code></td><td>libc &#x2F; ABI environment</td><td>不表示 package manager</td></tr><tr><td><code>.deb</code> &#x2F; <code>.rpm</code></td><td>package format</td><td>不表示 CPU architecture</td></tr><tr><td><code>ubuntu</code> &#x2F; <code>debian</code> &#x2F; <code>rhel</code></td><td>distribution compatibility target</td><td>仍要继续看版本、CPU 与 library baseline</td></tr></tbody></table><p>这也是 Linux 安装包经常比 macOS 多出一截后缀的原因：发布者需要把更多用户态兼容边界写进名字。</p><h2 id="x86、x64、amd64、x86-64-为什么像一组近义词"><a href="#x86、x64、amd64、x86-64-为什么像一组近义词" class="headerlink" title="x86、x64、amd64、x86_64 为什么像一组近义词"></a><code>x86</code>、<code>x64</code>、<code>amd64</code>、<code>x86_64</code> 为什么像一组近义词</h2><p>这组名字背后是一段很长的兼容历史。</p><h3 id="x86-来自一串以-86-结尾的处理器"><a href="#x86-来自一串以-86-结尾的处理器" class="headerlink" title="x86 来自一串以 86 结尾的处理器"></a><code>x86</code> 来自一串以 86 结尾的处理器</h3><p>Intel 在 1978 年推出 8086，后续又有 80186、80286、80386、80486。因为这些型号都以 <code>86</code> 结尾，业界逐渐用 <code>x86</code> 指代这一指令集家族。</p><p>到了 32-bit 时代，<code>i386</code>、<code>i486</code>、<code>i586</code>、<code>i686</code> 常被用来表达不同代际或最低指令集 baseline。<code>IA-32</code> 则是 Intel 对 32-bit architecture 的正式叫法之一。Node.js 使用 <code>ia32</code> 表示 32-bit x86 构建。</p><p>这导致 <code>x86</code> 在不同页面里可能有两种语义：</p><ol><li>广义上指整个 x86 family，包含 32-bit 与 64-bit；</li><li>下载按钮里常特指 32-bit x86，用来和 <code>x64</code> 区分。</li></ol><p>选包时要服从当前下载页的对照关系，不能只凭词源判断。</p><h3 id="x86-的-64-bit-扩展最早叫-AMD64"><a href="#x86-的-64-bit-扩展最早叫-AMD64" class="headerlink" title="x86 的 64-bit 扩展最早叫 AMD64"></a>x86 的 64-bit 扩展最早叫 AMD64</h3><p>AMD 把传统 x86 扩展到 64-bit，并保持对既有 16-bit、32-bit 软件模型的兼容，这套 architecture 被称为 <code>AMD64</code>。Intel 后来实现兼容的 64-bit architecture，并使用 <code>Intel 64</code> 这个名称。</p><p>软件生态于是留下了几种常见写法：</p><table><thead><tr><th>名称</th><th>常见生态</th><th>一般指向</th></tr></thead><tbody><tr><td><code>x86_64</code></td><td>Unix、Linux、Clang、Rust、Python wheel</td><td>64-bit x86</td></tr><tr><td><code>amd64</code></td><td>Debian&#x2F;Ubuntu、Go、Windows 内部与云镜像</td><td>64-bit x86</td></tr><tr><td><code>x64</code></td><td>Windows、Node.js、Electron、普通下载页</td><td>64-bit x86</td></tr><tr><td><code>Intel 64</code></td><td>Intel 官方文档</td><td>Intel 对兼容 64-bit x86 的命名</td></tr></tbody></table><p>Microsoft 的官方文档直接把 <code>x64</code> 定义为同时包含 AMD64 与 Intel 64。对普通安装包选择来说，<code>x64</code>、<code>amd64</code>、<code>x86_64</code> 通常可以视为同一 instruction-set family 的不同生态写法；到了 compiler flags、calling convention、object format 或 container manifest 层面，仍应使用对应工具链规定的精确字符串。</p><h2 id="arm64-与-aarch64-又是什么关系"><a href="#arm64-与-aarch64-又是什么关系" class="headerlink" title="arm64 与 aarch64 又是什么关系"></a><code>arm64</code> 与 <code>aarch64</code> 又是什么关系</h2><p>Armv8-A 引入了新的 64-bit execution state，Arm 官方称它为 <code>AArch64</code>；对应的 instruction set 是 A64。与它并列的 32-bit execution state 叫 <code>AArch32</code>。</p><p>不同生态对同一个 64-bit Arm 家族选了不同标签：</p><table><thead><tr><th>名称</th><th>常见位置</th></tr></thead><tbody><tr><td><code>AArch64</code> &#x2F; <code>aarch64</code></td><td>Arm 文档、Linux <code>uname</code>、GNU toolchain、Rust target</td></tr><tr><td><code>arm64</code></td><td>Apple、Windows、Node.js、Docker platform、普通下载页</td></tr></tbody></table><p>因此，Apple Silicon Mac 下载 <code>darwin-arm64</code>；Rust 对同一类目标使用 <code>aarch64-apple-darwin</code>。它们不是两种互不兼容的 CPU，而是不同工具链对同一 64-bit Arm architecture family 的常用命名。</p><p>不过，“同属 AArch64”仍不等于二进制可以跨 OS 直接运行。一个 macOS arm64 Mach-O executable 不能因为 CPU 相同，就直接当作 Linux arm64 ELF executable 使用。OS API、object format、ABI 与 system library 仍然不同。</p><h2 id="把短名字展开，就是-target-triple"><a href="#把短名字展开，就是-target-triple" class="headerlink" title="把短名字展开，就是 target triple"></a>把短名字展开，就是 target triple</h2><p>编译器需要比下载页更精确的坐标。Clang&#x2F;LLVM、Rust 与 GNU toolchain 常用 target triple 描述目标：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ARCHITECTURE-VENDOR-OPERATING_SYSTEM[-ENVIRONMENT]</span><br></pre></td></tr></table></figure><p>LLVM 官方文档说明，经典形式包含 architecture、vendor、operating system 三段，也可以再加 environment。即使今天经常出现四段，工具链仍沿用 “target triple” 这个历史名称，因为它最初确实只有三个字段。</p><p>拆几个真实例子：</p><table><thead><tr><th>Target</th><th>Architecture</th><th>Vendor</th><th>OS</th><th>Environment &#x2F; ABI</th></tr></thead><tbody><tr><td><code>aarch64-apple-darwin</code></td><td><code>aarch64</code></td><td><code>apple</code></td><td><code>darwin</code></td><td>Apple 平台默认环境</td></tr><tr><td><code>x86_64-pc-windows-msvc</code></td><td><code>x86_64</code></td><td><code>pc</code></td><td><code>windows</code></td><td>MSVC ABI&#x2F;toolchain</td></tr><tr><td><code>x86_64-pc-windows-gnu</code></td><td><code>x86_64</code></td><td><code>pc</code></td><td><code>windows</code></td><td>GNU&#x2F;MinGW environment</td></tr><tr><td><code>x86_64-unknown-linux-gnu</code></td><td><code>x86_64</code></td><td><code>unknown</code></td><td><code>linux</code></td><td>GNU&#x2F;glibc environment</td></tr><tr><td><code>aarch64-unknown-linux-musl</code></td><td><code>aarch64</code></td><td><code>unknown</code></td><td><code>linux</code></td><td>musl environment</td></tr></tbody></table><p><code>unknown</code> 不表示作者不知道 CPU 或系统，只表示这个目标没有需要单独编码的 vendor。<code>pc</code> 也不等于“只能在某品牌 PC 上运行”，它是工具链 target 命名的一部分。</p><p>短文件名 <code>linux-x64</code> 可以看成产品面向普通用户的压缩写法；<code>x86_64-unknown-linux-gnu</code> 则是编译器和 linker 需要的精确坐标。</p><h2 id="macOS-为什么还有-Universal-Binary"><a href="#macOS-为什么还有-Universal-Binary" class="headerlink" title="macOS 为什么还有 Universal Binary"></a>macOS 为什么还有 Universal Binary</h2><p>macOS 经历过多次 CPU architecture 迁移。Apple 的技术文档记录了从 PowerPC 到 Intel，再从 Intel 到 Apple Silicon 的兼容需求。为了让同一份 App 同时包含多个 architecture 的机器码，macOS 使用 Universal Binary，也常被称为 fat binary。</p><p>今天常见的 universal macOS executable 同时包含：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">x86_64 arm64</span><br></pre></td></tr></table></figure><p>系统启动时会选择适合当前机器的 slice。Apple Silicon 上还可以通过 Rosetta 运行只有 <code>x86_64</code> 指令的 Mac app；但这属于 binary translation，不等于它已经变成原生 <code>arm64</code> 构建。</p><p>三种发行方式各有取舍：</p><table><thead><tr><th>发行物</th><th>优点</th><th>代价</th></tr></thead><tbody><tr><td><code>darwin-arm64</code></td><td>Apple Silicon 原生，体积只含一个 architecture</td><td>Intel Mac 不能运行</td></tr><tr><td><code>darwin-x64</code></td><td>支持 Intel Mac，Apple Silicon 在 Rosetta 条件下可运行</td><td>在 Apple Silicon 上不是原生执行；插件和 native module 还要一起兼容</td></tr><tr><td><code>universal</code> &#x2F; <code>universal2</code></td><td>一个 artifact 覆盖两种 Mac architecture</td><td>体积更大，所有 embedded binaries 都要包含正确 slice</td></tr></tbody></table><p>可以用 <code>lipo</code> 检查一个 macOS binary 里到底有哪些 slice：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">lipo -archs /path/to/MyApp.app/Contents/MacOS/MyApp</span><br></pre></td></tr></table></figure><p>看到 <code>x86_64 arm64</code> 才能证明主 executable 同时包含两种 architecture。对复杂 App，还要继续检查 framework、plugin、helper 与 native library，不能只验主程序。</p><h2 id="Windows-ARM64-和模拟执行怎么理解"><a href="#Windows-ARM64-和模拟执行怎么理解" class="headerlink" title="Windows ARM64 和模拟执行怎么理解"></a>Windows ARM64 和模拟执行怎么理解</h2><p>Windows 11 on Arm 支持运行 Arm64 native app，也能通过 emulation 运行 x86 与 x64 app。于是 Arm Windows 用户经常同时看到两个都“能装”的选项：<code>windows-x64</code> 和 <code>windows-arm64</code>。</p><p>我的选择顺序仍然是：<strong>有可信的 Arm64 native build 就优先 Arm64；只有 x64 build 时，再把 emulation 当兼容路径。</strong></p><p>原因并不神秘。模拟层能翻译 CPU instructions，但应用还可能加载 driver、shell extension、DLL、JIT、plugin 或 native addon。这些组件存在各自的 architecture 与 ABI 边界。Microsoft 的 WOW64 文档也明确指出，32-bit 与 64-bit DLL 不能随意注入不同位数的进程；“主程序能启动”不能替代整条依赖链验收。</p><p>Windows 还有 <code>ARM64EC</code> 这类面向混合迁移的 ABI，让同一进程在特定规则下组合 Arm64EC 与兼容 x64 code。它主要是开发和迁移概念，不是普通用户下载页面上的首要选择。看到它时应按产品官方说明处理，不要把 <code>EC</code> 当作另一个 CPU family。</p><h2 id="为什么-OS-和-CPU-都对，程序还是跑不起来"><a href="#为什么-OS-和-CPU-都对，程序还是跑不起来" class="headerlink" title="为什么 OS 和 CPU 都对，程序还是跑不起来"></a>为什么 OS 和 CPU 都对，程序还是跑不起来</h2><p>一个 native executable 能否运行，至少要连续通过四层匹配：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">OS / object format</span><br><span class="line">    ↓</span><br><span class="line">CPU instruction set</span><br><span class="line">    ↓</span><br><span class="line">ABI / libc / calling convention</span><br><span class="line">    ↓</span><br><span class="line">system library、driver、plugin 与最低版本</span><br></pre></td></tr></table></figure><p>几个典型反例：</p><ul><li><code>linux-x64-musl</code> 与机器的 x64 CPU 匹配，但程序期望的 dynamic loader 可能和当前 glibc environment 不同；</li><li><code>darwin-x64</code> 能通过 Rosetta 启动，App 内部某个只含 arm64 或只含 x86_64 的 plugin 仍可能加载失败；</li><li><code>windows-x64</code> 可以在 Windows on Arm 的 emulation 下启动，但 kernel driver 不能按普通 user-space app 的方式处理；</li><li>同样是 <code>linux-x64-gnu</code>，新 glibc 编译出的 binary 也可能因为 symbol version baseline 过高而无法运行在旧 distribution 上。</li></ul><p>所以“架构兼容”只是必要条件，不是充分条件。发行工程真正要验证的是整条 artifact：可执行文件、动态库、native addon、插件、安装器和更新器都必须对齐。</p><h2 id="怎么确认自己该下载哪一个"><a href="#怎么确认自己该下载哪一个" class="headerlink" title="怎么确认自己该下载哪一个"></a>怎么确认自己该下载哪一个</h2><h3 id="跨平台最省事：看当前-Runtime-的编译目标"><a href="#跨平台最省事：看当前-Runtime-的编译目标" class="headerlink" title="跨平台最省事：看当前 Runtime 的编译目标"></a>跨平台最省事：看当前 Runtime 的编译目标</h3><p>如果机器上已经有 Node.js：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">node -p <span class="string">&quot;process.platform + &#x27;-&#x27; + process.arch&quot;</span></span><br></pre></td></tr></table></figure><p>常见输出：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">darwin-arm64</span><br><span class="line">win32-x64</span><br><span class="line">linux-x64</span><br></pre></td></tr></table></figure><p>Node.js 官方定义得很清楚：<code>process.platform</code> 表示这份 Node binary 面向哪个 OS platform 编译，<code>process.arch</code> 表示它面向哪个 CPU architecture 编译。这里检测的是当前 Runtime，不一定是物理硬件。</p><p>例如 Apple Silicon 上如果运行的是 x64 Node，<code>process.arch</code> 可能仍是 <code>x64</code>。对 native addon 来说这恰好有用，因为 addon 必须匹配当前 process；对“我应该安装哪一版 Node”来说，你还要继续确认硬件与翻译状态。</p><h3 id="macOS"><a href="#macOS" class="headerlink" title="macOS"></a>macOS</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">uname</span> -m</span><br><span class="line">sysctl -<span class="keyword">in</span> sysctl.proc_translated 2&gt;/dev/null || <span class="literal">true</span></span><br></pre></td></tr></table></figure><p>常见的 <code>uname -m</code> 输出是 <code>arm64</code> 或 <code>x86_64</code>。在 Apple Silicon 上，<code>sysctl.proc_translated</code> 返回 <code>1</code> 表示当前 process 正在 Rosetta translation environment 中运行，返回 <code>0</code> 表示 native process。</p><p>如果只想通过图形界面判断：Apple 菜单 →“关于本机”，看到 Apple M 系列芯片就选 <code>arm64</code>；看到 Intel processor 就选 <code>x64</code> &#x2F; <code>x86_64</code>。</p><h3 id="Windows-PowerShell"><a href="#Windows-PowerShell" class="headerlink" title="Windows PowerShell"></a>Windows PowerShell</h3><p>如果安装了现代 .NET runtime，可以同时查看 OS 与当前 process：</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">[<span class="type">System.Runtime.InteropServices.RuntimeInformation</span>]::OSArchitecture</span><br><span class="line">[<span class="type">System.Runtime.InteropServices.RuntimeInformation</span>]::ProcessArchitecture</span><br></pre></td></tr></table></figure><p>两者可能不同。例如 Arm64 Windows 正在运行 x64 process 时，OS architecture 与 process architecture 就不能混为一个值。没有把握时，优先遵循产品下载页对 Windows on Arm 的明确说明。</p><h3 id="Linux"><a href="#Linux" class="headerlink" title="Linux"></a>Linux</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">uname</span> -m</span><br><span class="line">ldd --version</span><br></pre></td></tr></table></figure><p>常见 architecture 映射：</p><table><thead><tr><th><code>uname -m</code></th><th>下载页常见标签</th></tr></thead><tbody><tr><td><code>x86_64</code></td><td><code>x64</code> &#x2F; <code>amd64</code> &#x2F; <code>x86_64</code></td></tr><tr><td><code>aarch64</code></td><td><code>arm64</code> &#x2F; <code>aarch64</code></td></tr><tr><td><code>armv7l</code></td><td><code>armv7</code> &#x2F; <code>armhf</code>，还要确认 hard-float ABI</td></tr><tr><td><code>ppc64le</code></td><td><code>ppc64le</code></td></tr><tr><td><code>s390x</code></td><td><code>s390x</code></td></tr><tr><td><code>riscv64</code></td><td><code>riscv64</code></td></tr></tbody></table><p><code>ldd --version</code> 的输出通常能帮助判断当前环境属于 glibc 还是 musl，但不同 distribution 的实现与输出不完全一致。发布者若提供了专门的 <code>gnu</code> &#x2F; <code>musl</code> artifact，应继续对照其支持矩阵，而不是只看 <code>uname -m</code>。</p><h2 id="看到一个文件名，我会按什么顺序判断"><a href="#看到一个文件名，我会按什么顺序判断" class="headerlink" title="看到一个文件名，我会按什么顺序判断"></a>看到一个文件名，我会按什么顺序判断</h2><p>假设下载页给出：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">app-3.2.0-linux-arm64-musl-beta.tar.gz</span><br></pre></td></tr></table></figure><p>我会从兼容性到发布策略依次拆：</p><ol><li><code>linux</code>：目标 OS；</li><li><code>arm64</code>：目标 CPU architecture；</li><li><code>musl</code>：目标 libc &#x2F; ABI environment；</li><li><code>beta</code>：release channel；</li><li><code>.tar.gz</code>：archive format，不是 CPU 或 ABI。</li></ol><p>这套顺序有一个好处：即使项目把字段换成 <code>aarch64-unknown-linux-musl</code>、<code>linux-aarch64</code> 或 <code>linux-arm64-alpine</code>，你仍然是在找相同的几个坐标。</p><p>还有三个实用判断：</p><ul><li><strong>原生优先。</strong> Apple Silicon 选 <code>arm64</code>，Windows on Arm 有成熟 native build 时也优先 <code>arm64</code>；</li><li><strong>模拟是兼容手段，不是构建证明。</strong> x64 app 能通过 Rosetta 或 Windows emulation 启动，不代表 native module、plugin 与 driver 全部通过；</li><li><strong>Linux 多看一层 libc 与最低版本。</strong> CPU 匹配后，再核对 <code>gnu/musl</code>、distribution baseline 和 package format。</li></ul><h2 id="要不要记住所有名字：我的判断框架"><a href="#要不要记住所有名字：我的判断框架" class="headerlink" title="要不要记住所有名字：我的判断框架"></a>要不要记住所有名字：我的判断框架</h2><p>不需要背完整词典。记住三条主线就够了：</p><p><strong>第一，先分维度。</strong> <code>darwin</code>、<code>win32</code>、<code>linux</code> 是 OS&#x2F;platform；<code>x64</code>、<code>arm64</code> 是 CPU architecture；<code>gnu</code>、<code>musl</code>、<code>msvc</code> 是 ABI&#x2F;toolchain environment；<code>stable</code>、<code>beta</code> 才是 channel。</p><p><strong>第二，识别同义标签。</strong> <code>x64 ≈ amd64 ≈ x86_64</code>，<code>arm64 ≈ aarch64</code>。这里的“≈”表示 ordinary package selection 中通常指向同一个 instruction-set family，不代表不同 OS、ABI 下的 binary 可以互换。</p><p><strong>第三，把检测对象说清楚。</strong> 你检测的是 physical machine、operating system，还是当前 process？在 Rosetta、WOW64、container 和 cross-compilation 环境里，这三个答案完全可能不同。</p><p>下一次再看到 <code>darwin-arm64</code>，不用把它当成一串项目私有缩写。把连字符理解成坐标分隔符：平台、架构、环境、渠道，一层层读下去即可。</p><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><ul><li><a href="https://www.apple.com/newsroom/2000/04/05Apple-Releases-Darwin-1-0-Open-Source/">Apple：Apple Releases Darwin 1.0 Open Source</a></li><li><a href="https://developer.apple.com/documentation/apple-silicon/building-a-universal-macos-binary">Apple Developer：Building a universal macOS binary</a></li><li><a href="https://developer.apple.com/documentation/apple-silicon/about-the-rosetta-translation-environment">Apple Developer：About the Rosetta translation environment</a></li><li><a href="https://nodejs.org/api/process.html">Node.js：<code>process.platform</code> 与 <code>process.arch</code></a></li><li><a href="https://newsroom.intel.com/tech101/x86-architecture-foundation-of-modern-computing">Intel：What is x86 Architecture?</a></li><li><a href="https://docs.amd.com/v/u/en-US/40332_4.09_APM_PUB">AMD：AMD64 Architecture Programmer’s Manual</a></li><li><a href="https://developer.arm.com/-/media/Arm%20Developer%20Community/PDF/Learn%20the%20Architecture/Armv8-A%20Instruction%20Set%20Architecture.pdf">Arm：Armv8-A Instruction Set Architecture</a></li><li><a href="https://learn.microsoft.com/en-us/windows-hardware/drivers/debugger/x64-architecture">Microsoft Learn：x64 architecture</a></li><li><a href="https://learn.microsoft.com/en-us/windows/arm/faq">Microsoft Learn：Windows on Arm FAQ</a></li><li><a href="https://llvm.org/docs/LangRef.html#target-triple">LLVM：Target Triple</a></li><li><a href="https://doc.rust-lang.org/rustc/platform-support.html">Rust：Platform Support</a></li></ul><blockquote><p>本文使用 <a href="https://github.com/zisheng-ai/writting-skill">writting-skill</a> 辅助写作。项目已开源，欢迎在 GitHub 点个 Star。</p></blockquote>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/4b5ee7e9cf8a/</id>
    <link href="https://blog.zisheng.pro/posts/4b5ee7e9cf8a/"/>
    <published>2026-08-24T14:27:09.000Z</published>
    <summary>下载 CLI、桌面应用或原生依赖时，我们经常遇到 darwin-arm64、win32-x64、linux-x64、AMD64、AArch64、gnu、musl 等名字。本文从 OS、CPU、ABI 与 release channel 四个维度拆解这些标签，解释它们的历史来源、兼容关系与选包方法。</summary>
    <title>darwin-arm64、win32-x64、linux-x64 到底是什么：一篇讲清安装包命名</title>
    <updated>2026-09-18T08:15:28.632Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="软件工程" scheme="https://blog.zisheng.pro/categories/%E8%BD%AF%E4%BB%B6%E5%B7%A5%E7%A8%8B/"/>
    <category term="Chromium" scheme="https://blog.zisheng.pro/tags/Chromium/"/>
    <category term="FlyOS" scheme="https://blog.zisheng.pro/tags/FlyOS/"/>
    <category term="RSS" scheme="https://blog.zisheng.pro/tags/RSS/"/>
    <category term="Resident Set Size" scheme="https://blog.zisheng.pro/tags/Resident-Set-Size/"/>
    <category term="Performance" scheme="https://blog.zisheng.pro/tags/Performance/"/>
    <category term="macOS" scheme="https://blog.zisheng.pro/tags/macOS/"/>
    <category term="Memory" scheme="https://blog.zisheng.pro/tags/Memory/"/>
    <content>
      <![CDATA[<p>最近在整理 FlyOS 的性能 baseline 时，我碰到一组很容易被误读的数据：同一轮空闲采样中，完整进程树的 RSS 均值是 <code>949.4 MiB</code>，macOS Physical Footprint 均值却只有 <code>355.5 MiB</code>。</p><p>两个数字相差约 2.7 倍。是采集脚本错了，还是浏览器真的吃掉了近 1 GB 物理内存？都不是。真正的问题在于：我们把两个统计口径不同的指标，当成了同一个“内存占用”。这也是我想单独写清 RSS 的原因。</p><span id="more"></span><h2 id="一句话总结"><a href="#一句话总结" class="headerlink" title="一句话总结"></a>一句话总结</h2><p>RSS（Resident Set Size）表示一个进程当前驻留在物理内存中的页面规模。它适合观察同一口径下的趋势和拆解 Browser、Renderer、GPU、Utility 等进程角色，但在 Chromium 多进程架构里，把各进程 RSS 直接相加可能重复计算共享页，所以它不等于应用独占的真实物理内存，更不能单凭一次上涨就判定内存泄漏。</p><p>在 macOS 上，我的判断很明确：</p><ul><li>看整个应用给系统造成的内存压力，以进程树的 Physical Footprint 为主；</li><li>看内存主要分布在哪类进程、版本前后趋势是否异常，以同口径 RSS 为辅；</li><li>判断是不是泄漏，还要继续进入 heap、allocation、对象生命周期和 GC 证据，不能停在 RSS。</li></ul><h2 id="RSS-到底统计了什么"><a href="#RSS-到底统计了什么" class="headerlink" title="RSS 到底统计了什么"></a>RSS 到底统计了什么</h2><p>操作系统按页管理内存。进程的虚拟地址空间可能很大，但其中只有一部分页面此刻真正驻留在 RAM；RSS 统计的就是这部分 resident pages 的规模。</p><p>在 macOS 上，<code>ps</code> 对 <code>rss</code> 的定义是进程的 real memory resident set，并以 <code>1024 bytes</code> 为单位输出。FlyOS 的采样器读取的是：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ps -axo pid=,ppid=,rss=,%cpu=,<span class="built_in">command</span>=</span><br></pre></td></tr></table></figure><p>这里的 <code>rss</code> 不是 JavaScript heap，也不是应用申请过的全部虚拟地址，更不是“这个应用独占了多少内存”。几个常见指标可以这样区分：</p><table><thead><tr><th>指标</th><th>回答的问题</th><th>主要局限</th></tr></thead><tbody><tr><td>VSZ &#x2F; Virtual Size</td><td>进程映射了多大的虚拟地址空间</td><td>大量地址可能没有驻留，通常不能直接代表内存压力</td></tr><tr><td>RSS</td><td>这个进程当前有多少页面驻留在 RAM</td><td>可能包含 clean、file-backed 和共享页；跨进程相加可能重复</td></tr><tr><td>Heap</td><td>某个运行时或分配器管理了多少对象内存</td><td>只是进程内存的一部分，不包含全部 native、映射文件和共享内存</td></tr><tr><td>Physical Footprint</td><td>从系统记账角度，这个进程或进程组带来多少物理内存负担</td><td>更适合 macOS 压力判断，但不直接告诉你具体是哪类对象在增长</td></tr></tbody></table><p>Apple 在内存优化文档里也把 dirty、clean、compressed 等页面区别开来，并建议结合真实 memory pressure 理解应用影响，而不是只盯一个总数。参见 <a href="https://developer.apple.com/documentation/xcode/reducing-your-app-s-memory-use">Reducing your app’s memory use</a>。</p><h2 id="为什么-Chromium-多进程会把-RSS-“加大”"><a href="#为什么-Chromium-多进程会把-RSS-“加大”" class="headerlink" title="为什么 Chromium 多进程会把 RSS “加大”"></a>为什么 Chromium 多进程会把 RSS “加大”</h2><p>Chromium 不是单进程程序。Browser、Renderer、GPU、Network Service、Storage Service 等角色彼此隔离，这也是它稳定性和安全性的重要基础。Chromium 官方的 <a href="https://www.chromium.org/developers/design-documents/multi-process-architecture/">Multi-process Architecture</a> 文档对此有完整说明。</p><p>问题在于，不同进程可以映射同一份共享库、共享内存或 file-backed page。对单个进程看，这些页面确实属于它的 resident set；但如果把多个进程的 RSS 直接相加，同一批物理页就可能在多个进程名下重复出现。</p><p>可以把它想成一套公共技术手册：五个团队的桌上都放着它。按“每个团队正在使用多少资料”统计，这套手册会出现五次；按“公司仓库实际占了多少空间”统计，它只有一套。两个数字都没有错，只是在回答不同的问题。</p><p>这也是为什么 FlyOS 同时采集两条线：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 拆解进程角色，并得到每个进程的 RSS</span></span><br><span class="line">ps -axo pid=,ppid=,rss=,%cpu=,<span class="built_in">command</span>=</span><br><span class="line"></span><br><span class="line"><span class="comment"># 对同一组 PID 做 macOS Physical Footprint 统计</span></span><br><span class="line">footprint -j result.json -f bytes --noCategories &lt;pid...&gt;</span><br></pre></td></tr></table></figure><p><code>footprint</code> 在接收多个进程时，会把多重映射的对象去重，并把共享部分单独记账。因此，RSS 汇总和 Physical Footprint 出现明显差异，本身并不是异常。</p><h2 id="回到-FlyOS：949-4-MiB-和-355-5-MiB-各代表什么"><a href="#回到-FlyOS：949-4-MiB-和-355-5-MiB-各代表什么" class="headerlink" title="回到 FlyOS：949.4 MiB 和 355.5 MiB 各代表什么"></a>回到 FlyOS：949.4 MiB 和 355.5 MiB 各代表什么</h2><p>2026 年 8 月 19 日，我用 FlyOS CLI <code>1.2.0</code> 的本地 Release 构建，为 Fia <code>0.1.0</code> 建立了第一份 clean-profile 正式 baseline。测试环境是 macOS 26.5.2 arm64，临时干净 profile，只打开 <code>https://example.com/</code>，预热 5 秒后进入空闲采样。</p><table><thead><tr><th>采样项</th><th align="right">结果</th><th>采样口径</th></tr></thead><tbody><tr><td>RSS 均值</td><td align="right">949.4 MiB</td><td>进程树 5 次采样，间隔 1 秒</td></tr><tr><td>RSS 中位数</td><td align="right">948.5 MiB</td><td>同上</td></tr><tr><td>Physical Footprint 均值</td><td align="right">355.5 MiB</td><td>同一进程树，前 3 次采样</td></tr><tr><td>空闲 CPU 均值</td><td align="right">1.9%</td><td>同一空闲窗口</td></tr><tr><td>CDP reload CPU 峰值</td><td align="right">66.2%</td><td>40 次采样，间隔 200 ms</td></tr></tbody></table><p>RSS 的价值在角色拆解上尤其直观。下面是最后一次空闲采样的分布，总计 <code>948.5 MiB</code>：</p><table><thead><tr><th>进程角色</th><th align="right">数量</th><th align="right">RSS</th></tr></thead><tbody><tr><td>Browser</td><td align="right">1</td><td align="right">231.3 MiB</td></tr><tr><td>Renderer</td><td align="right">3</td><td align="right">322.5 MiB</td></tr><tr><td>GPU</td><td align="right">1</td><td align="right">109.3 MiB</td></tr><tr><td>Utility - Network</td><td align="right">1</td><td align="right">78.8 MiB</td></tr><tr><td>Utility - Storage</td><td align="right">1</td><td align="right">66.5 MiB</td></tr><tr><td>Runtime</td><td align="right">1</td><td align="right">140.1 MiB</td></tr></tbody></table><p>这张表能快速回答“主要增长发生在哪个角色”。例如 Renderer 突然多出一个，或者 Runtime RSS 连续抬升，调查方向会完全不同。但它不能回答“这 948.5 MiB 是否全部由 Fia 独占”。对于后一个问题，<code>355.5 MiB</code> 的 Physical Footprint 更接近我关心的系统压力口径。</p><h2 id="RSS-上涨不等于发生了内存泄漏"><a href="#RSS-上涨不等于发生了内存泄漏" class="headerlink" title="RSS 上涨不等于发生了内存泄漏"></a>RSS 上涨不等于发生了内存泄漏</h2><p>内存泄漏会推高 RSS，但 RSS 上涨还有很多正常原因：</p><ul><li>Chromium 新建了 Renderer 或 Utility 进程；</li><li>页面缓存、图片解码缓存或磁盘缓存映射增加；</li><li>V8 JIT 编译了更多代码，heap 也扩过容；</li><li>allocator 暂时保留已释放页面，没有立即归还操作系统；</li><li>file-backed 页面因为刚被访问而进入 resident set；</li><li>对象仍然可达，只是产品逻辑保留时间比预期更长。</li></ul><p>因此，“跑一次后 RSS 比之前高”最多是一个调查信号，不是泄漏结论。更可靠的链路应该是：先固定 workload 和进程范围复现趋势，再用 heap snapshot、allocation profile、对象数量、GC 后基线和进程生命周期去定位原因。</p><p>如果多轮相同操作后，RSS 与 Physical Footprint 都阶梯式增长，强制 GC 或回到空闲状态后仍不下降，同时 heap 或 native allocation 里能看到稳定增长的保留路径，才有资格把结论推进到“疑似泄漏”。</p><h2 id="性能评测里，怎样比较-RSS-才不误导"><a href="#性能评测里，怎样比较-RSS-才不误导" class="headerlink" title="性能评测里，怎样比较 RSS 才不误导"></a>性能评测里，怎样比较 RSS 才不误导</h2><p>我现在会把 RSS 对比当成一个有前置条件的实验，而不是把两个数字直接相减。至少要锁定下面六件事：</p><table><thead><tr><th>必须一致的条件</th><th>为什么</th></tr></thead><tbody><tr><td>指标定义</td><td>RSS 只能和 RSS 比，不能拿 RSS 与 Physical Footprint 做版本涨跌</td></tr><tr><td>进程范围</td><td>只看 Browser 与统计完整进程树，结论会完全不同</td></tr><tr><td>workload</td><td>空白页、真实业务页、视频页的进程和缓存结构不同</td></tr><tr><td>生命周期</td><td>冷启动、预热后空闲、操作中、操作后稳定态不能混在一起</td></tr><tr><td>构建产物</td><td>Debug、Release、符号、Feature Flag 都可能改变结果</td></tr><tr><td>采样窗口</td><td>单点很容易受进程启动、GC、缓存和后台任务干扰</td></tr></tbody></table><p>这也解释了为什么这份 baseline 没有拿历史报告中的 <code>1.53–1.67 GB RSS</code> 直接宣称“大幅下降”：历史数据的进程树、页面负载和采样口径并不完全一致。当前这组数据首先是一条可复现的基线，只有后续版本在同机、同 workload、同范围、同窗口下复测，才形成有效比较。</p><p>Apple 对性能回归的建议也是建立可重复的测试和 baseline，再观察变化，而不是靠一次快照下结论。参见 <a href="https://developer.apple.com/documentation/xcode/preventing-memory-use-regressions">Preventing memory-use regressions</a>。</p><h2 id="要不要看-RSS-我的判断框架"><a href="#要不要看-RSS-我的判断框架" class="headerlink" title="要不要看 RSS &#x2F; 我的判断框架"></a>要不要看 RSS &#x2F; 我的判断框架</h2><p>RSS 值得看，但要放在正确的位置上。</p><table><thead><tr><th>你要回答的问题</th><th>优先看什么</th><th>我的判断</th></tr></thead><tbody><tr><td>内存主要花在哪类进程</td><td>分角色 RSS</td><td>值得看，定位速度很快</td></tr><tr><td>新版本是否出现同口径回归</td><td>RSS 趋势 + Physical Footprint 趋势</td><td>两条线一起看，避免统计口径误导</td></tr><tr><td>macOS 上整个应用造成多大内存压力</td><td>进程树 Physical Footprint + memory pressure</td><td>以此为主，不用 RSS 汇总替代</td></tr><tr><td>是不是 JavaScript 对象泄漏</td><td>Heap snapshot、allocation、GC 后保留对象</td><td>RSS 只能报警，不能定罪</td></tr><tr><td>用户是否真的感知到退化</td><td>页面响应、切换延迟、崩溃、系统压力</td><td>最终仍要回到体验和稳定性</td></tr></tbody></table><p>我的核心原则只有一句：先问指标在回答什么问题，再看数字大小。</p><p>RSS 不是“假数据”，Physical Footprint 也不是 RSS 的修正版。它们是从不同层面观察内存的两把尺子。FlyOS 性能治理真正需要的，不是寻找一个万能数字，而是把进程结构、系统压力、运行时分配和用户体验连成一条证据链。</p><blockquote><p>本文使用 <a href="https://github.com/zisheng-ai/writting-skill">writting-skill</a> 辅助写作，配图使用 <a href="https://github.com/zisheng-ai/better-imagegen">better-imagegen</a> 生成。项目已开源，欢迎在 GitHub 点个 Star。</p></blockquote>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/5c1f79f05132/</id>
    <link href="https://blog.zisheng.pro/posts/5c1f79f05132/"/>
    <published>2026-08-24T12:47:17.000Z</published>
    <summary>最近做 FlyOS 性能 baseline 时，同一轮空闲采样里，进程树 RSS 均值是 949.4 MiB，Physical Footprint 均值却只有 355.5 MiB。本文从这组真实数据出发，讲清 Resident Set Size 的统计边界、Chromium 多进程下的重复计数，以及判断内存回归和泄漏时应该怎样选指标。</summary>
    <title>RSS 到底是什么：为什么 FlyOS 性能评测不能只看一个内存数字</title>
    <updated>2026-09-18T08:15:28.632Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="AI" scheme="https://blog.zisheng.pro/categories/AI/"/>
    <category term="AI Infra" scheme="https://blog.zisheng.pro/tags/AI-Infra/"/>
    <category term="Agent" scheme="https://blog.zisheng.pro/tags/Agent/"/>
    <category term="Codex" scheme="https://blog.zisheng.pro/tags/Codex/"/>
    <category term="Skill" scheme="https://blog.zisheng.pro/tags/Skill/"/>
    <category term="Software Architecture" scheme="https://blog.zisheng.pro/tags/Software-Architecture/"/>
    <content>
      <![CDATA[<p><img src="/images/agent-skill-architecture-boundary.webp" alt="平台开发通过已发布制品连接到发行版开发"></p><p>我最近整理一个浏览器平台工程时，发现最容易让 Agent 做错的，不是命令太多，而是把两类工作塞进了同一个 Skill：一边维护平台源码、协议和构建能力，另一边又让它操作具体产品的 Profile、签名、发布和验收。</p><p>看上去只是目录里少了一个文件夹，实际却把所有权混在了一起。Agent 一旦拿到模糊的“发布浏览器”指令，很容易从平台源码直接构建、拿本地 package 去验证产品，或者把签名、CDN、更新通道当成平台默认能力。每一步单独都可能跑通，组合起来却会破坏交付边界。</p><h2 id="一句话总结"><a href="#一句话总结" class="headerlink" title="一句话总结"></a>一句话总结</h2><p>Skill 应该按“谁拥有决策和证据”拆分，而不是按“这些命令能不能连着执行”拆分。平台开发与发行版开发之间，最重要的产物是已发布、精确版本锁定的 package，而不是共享一个工作目录。</p><h2 id="Skill-不是知识库目录"><a href="#Skill-不是知识库目录" class="headerlink" title="Skill 不是知识库目录"></a>Skill 不是知识库目录</h2><p>很多 Skill 最初都像一份团队 Wiki：构建命令、常见错误、发布步骤、文档链接全放进去。它在任务少时很方便，任务一多就会开始替 Agent 做隐式决策。</p><p>例如下面这两类问题，表面都和“浏览器构建”有关，实际输入完全不同：</p><table><thead><tr><th>工作</th><th>真正拥有者</th><th>Agent 应读取的输入</th><th>交付证据</th></tr></thead><tbody><tr><td>修改 Kernel、CLI、Build Kit、Runway Core</td><td>平台团队</td><td>平台源码、基线、测试、package manifest</td><td>测试、tarball、registry readback</td></tr><tr><td>配置品牌、内置扩展、签名、发布通道</td><td>发行版团队</td><td>Profile、lock、产品 workspace、受控凭据</td><td>native artifact、签名回读、CDN、旧客户端升级</td></tr></tbody></table><p>如果同一个 Skill 同时覆盖它们，Agent 很容易把“本地源码已经通过测试”说成“发行版已经采用”，或者让发行版直接依赖平台的 sibling checkout。前者缺少发布和安装证据，后者会让本地环境掩盖真实的 package 边界。</p><h2 id="我会把边界写进名字和目录"><a href="#我会把边界写进名字和目录" class="headerlink" title="我会把边界写进名字和目录"></a>我会把边界写进名字和目录</h2><p>拆分不是把原文复制两份，而是让名称先表达工作对象。</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">.codex/skills/</span><br><span class="line">  platform-develop/</span><br><span class="line">    平台源码、Kernel、Build Kit、CLI、公开 package</span><br><span class="line"></span><br><span class="line">skills/</span><br><span class="line">  distribution-develop/</span><br><span class="line">    产品 Profile、私有 workspace、签名、发布、客户端验收</span><br></pre></td></tr></table></figure><p>平台 Skill 放在平台仓库的 <code>.codex/skills/</code>，含义是：它服务这个仓库的维护者，不应被产品安装、CI 或运行时依赖。发行版 Skill 则面向拥有产品配置和交付责任的工程。两者可以引用同一套平台契约，但不能共享“直接读取平台源码”这条捷径。</p><p>目录位置不是审美问题。它决定 Agent 在任务开始时优先加载什么上下文，也提醒维护者：这里的规则究竟是本地开发习惯，还是要跟着某个产品一起交付的操作契约。</p><h2 id="把依赖方向写成-Agent-能检查的规则"><a href="#把依赖方向写成-Agent-能检查的规则" class="headerlink" title="把依赖方向写成 Agent 能检查的规则"></a>把依赖方向写成 Agent 能检查的规则</h2><p>我通常会要求平台与发行版满足三个约束。</p><p>第一，发行版只消费 registry 中已发布的精确 package。禁止 <code>file:</code>、<code>link:</code>、<code>workspace:</code>、本地 tarball、sibling lookup 或跨仓 symlink。这样能强迫每次采用都经过版本、锁文件和干净安装。</p><p>第二，产品专属内容留在产品侧。品牌、Bundle ID、私有 Extension、Runway 实现、签名密钥引用、发布渠道与真实客户端验收，都不是平台的默认输入。平台可以提供 CLI 和安全 primitive，但不应该猜测产品路径或替产品保存凭据。</p><p>第三，交付状态必须拆开记录。测试通过、package 已发布、产品已构建、签名完成、CDN 可读、更新通道已激活、旧客户端成功升级，分别是不同事实。Skill 的价值不在于让 Agent 多跑几条命令，而在于避免把前一个状态误报成后一个状态。</p><h2 id="一个足够小的判断框架"><a href="#一个足够小的判断框架" class="headerlink" title="一个足够小的判断框架"></a>一个足够小的判断框架</h2><p>当我准备新增或拆分一个 Skill 时，只问四个问题：</p><ol><li>这个任务最终由谁对用户结果负责？</li><li>Agent 为完成任务必须读取哪些 source of truth？</li><li>哪些凭据、产物和环境只能由特定工程拥有？</li><li>什么证据能证明交付，而不只是证明代码存在？</li></ol><p>四个答案相同，通常可以留在一个 Skill 里。只要其中一个答案分叉，特别是出现“平台已发布 package”与“产品已签名 artifact”两种证据时，就应该拆开。</p><p>这套方法不只适用于浏览器。任何同时维护 SDK、平台服务和多个产品的团队，都可以用同样的方式收紧 Agent 的工作面：让 Agent 在正确的仓库、正确的依赖方向和正确的验收门槛里工作。这样获得的不是更多流程，而是更少的错误捷径。</p><h2 id="要不要拆"><a href="#要不要拆" class="headerlink" title="要不要拆"></a>要不要拆</h2><p>如果一个 Skill 既能修改共享平台，又能替多个产品发布，值得拆。先按所有权拆，再用明确的 package、Profile 和验收证据连接。</p><p>如果工作只发生在一个产品仓库，且没有跨仓发布或受控凭据边界，暂时不必为了形式拆 Skill。真正需要关注的是：Agent 是否会因为错误的上下文和依赖方向，做出无法被交付证据支撑的结论。</p><blockquote><p>本文使用 <a href="https://github.com/zisheng-ai/writting-skill">writting-skill</a> 辅助写作。项目已开源，欢迎在 GitHub 点个 Star。</p></blockquote>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/35f1f49534bc/</id>
    <link href="https://blog.zisheng.pro/posts/35f1f49534bc/"/>
    <published>2026-08-22T17:05:00.000Z</published>
    <summary>Agent Skill 不只是提示词集合，它会影响 Agent 从哪里读上下文、能修改什么、如何判断交付完成。本文以平台工程与 Browser Distribution 为例，解释为什么要拆开平台开发和发行版开发 Skill，并给出命名、目录、依赖和验收边界的实践框架。</summary>
    <title>把 Agent Skill 当作架构边界：平台开发与发行版开发为什么要拆开</title>
    <updated>2026-09-18T08:15:28.640Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="AI" scheme="https://blog.zisheng.pro/categories/AI/"/>
    <category term="RAG" scheme="https://blog.zisheng.pro/tags/RAG/"/>
    <category term="Agent" scheme="https://blog.zisheng.pro/tags/Agent/"/>
    <category term="LLM Wiki" scheme="https://blog.zisheng.pro/tags/LLM-Wiki/"/>
    <category term="AI Coding" scheme="https://blog.zisheng.pro/tags/AI-Coding/"/>
    <category term="Skill" scheme="https://blog.zisheng.pro/tags/Skill/"/>
    <category term="Knowledge Engineering" scheme="https://blog.zisheng.pro/tags/Knowledge-Engineering/"/>
    <content>
      <![CDATA[<p><img src="/images/agent-skill-llm-wiki-knowledge-loop.webp" alt="Agent Skill 从静态 Reference 到 LLM Wiki 的知识架构"></p><p>最近我读到一篇数据研发实践文章。它讨论了一个很具体的问题：团队已经把需求调研、模型设计、代码生成和任务发布封装成 Agent Skill，但模型口径、数据资产和业务知识仍在持续变化，静态写进 <code>Reference</code> 的内容很快就会落后。</p><p>我认为这篇实践最值得复用的部分，不是某个知识库产品的接入方式，而是它重新划清了 Skill 与知识的边界：<strong>Skill 负责稳定的工作方法，知识库负责持续变化的事实。</strong></p><blockquote><p>说明：本文是脱敏后的原创技术解读。公开版未复用原文、内部链接、组织与人员信息、真实业务数据及操作截图，仅保留可泛化的工程方法。</p></blockquote><span id="more"></span><h2 id="一句话总结"><a href="#一句话总结" class="headerlink" title="一句话总结"></a>一句话总结</h2><p>当 Agent Skill 依赖的知识规模变大、变化变快之后，不要继续把资料塞进 <code>Reference</code>。更稳妥的结构是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">Raw Sources  →  Wiki  →  Schema / Skill</span><br><span class="line">事实源           知识编译     查询与执行规则</span><br></pre></td></tr></table></figure><p>Agent 通过 <code>Ingest</code> 把新资料编译进 Wiki，通过 <code>Query</code> 为当前任务取得带来源的知识，通过 <code>Lint</code> 发现冲突、孤岛和过期内容。每次实践产生的新结论再回到 Raw Sources，进入下一轮增量编译。</p><h2 id="静态-Reference-为什么会失效"><a href="#静态-Reference-为什么会失效" class="headerlink" title="静态 Reference 为什么会失效"></a>静态 Reference 为什么会失效</h2><p>第一版 Skill 把领域知识放进 <code>references/</code> 很合理。它简单、透明、可以和代码一起版本化，也不需要额外基础设施。</p><p>问题出现在知识开始具备下面几个特征之后：</p><ul><li>数据表、字段口径和任务代码持续变化；</li><li>需求文档、设计文档、规范和历史案例分散在不同位置；</li><li>同一个知识点可能存在多个版本，甚至相互冲突；</li><li>一次任务中校准出的结论，需要被下一次任务继续使用；</li><li>不同团队成员知道的信息不对称，Agent 也无法判断哪份资料更新。</li></ul><p>如果继续扩充静态 Reference，Skill 会逐渐变成一个难以维护的资料压缩包。知识更新需要修改 Skill，Skill 发布又和知识变更绑定；资料越多，Agent 每次加载的 Context 越大，真正需要的内容反而更难命中。</p><p>这时需要拆开两个生命周期：</p><table><thead><tr><th>对象</th><th>适合承载什么</th><th>变化频率</th><th>维护方式</th></tr></thead><tbody><tr><td>Skill</td><td>流程、输入输出、工具调用、质量门槛、失败恢复</td><td>相对稳定</td><td>代码评审与版本发布</td></tr><tr><td>Reference</td><td>稳定规则、模板、固定术语、少量示例</td><td>低频</td><td>随 Skill 维护</td></tr><tr><td>Wiki</td><td>业务知识、数据资产、设计结论、案例经验、关系与冲突</td><td>持续变化</td><td>增量编译与健康检查</td></tr><tr><td>Raw Sources</td><td>原始文档、代码、规范和已审核事实</td><td>按业务变化</td><td>由知识 Owner 维护</td></tr></tbody></table><h2 id="LLM-Wiki-的三层架构"><a href="#LLM-Wiki-的三层架构" class="headerlink" title="LLM Wiki 的三层架构"></a>LLM Wiki 的三层架构</h2><p><a href="https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f">Andrej Karpathy 提出的 LLM Wiki pattern</a> 把知识系统分成 Raw Sources、Wiki 和 Schema 三层。这个分层很适合 Agent Skill，因为它同时回答了三个问题：事实在哪里、知识怎样积累、Agent 按什么规则工作。</p><h3 id="Raw-Sources：不可被-AI-改写的事实源"><a href="#Raw-Sources：不可被-AI-改写的事实源" class="headerlink" title="Raw Sources：不可被 AI 改写的事实源"></a>Raw Sources：不可被 AI 改写的事实源</h3><p>Raw Sources 保存原始资料。放到数据研发场景里，可以包括：</p><ul><li>经过授权的代码仓库与 SQL；</li><li>数据资产说明、字段口径和模型设计文档；</li><li>需求调研、业务规则与研发规范；</li><li>已审核的故障案例、问答记录和设计反馈。</li></ul><p>这一层的核心约束是：AI 可以读取，但不能把自己的总结反向覆盖原始事实。任何 Wiki 页面和 Agent 答案都应该能够回到具体 Source、版本和位置。</p><h3 id="Wiki：由-AI-维护的知识中间层"><a href="#Wiki：由-AI-维护的知识中间层" class="headerlink" title="Wiki：由 AI 维护的知识中间层"></a>Wiki：由 AI 维护的知识中间层</h3><p>Wiki 不是原文切片的集合。它把分散资料整理成主题页、实体页、流程页、索引、交叉引用和编译日志。</p><p>例如，一个“核心交易事实表”主题页可以连接字段口径、上游来源、下游任务、历史变更与常见异常；当新设计文档进入系统时，Agent 不只新增一个 Chunk，还要更新已有页面并标记冲突。</p><p>这就是“编译”的价值：跨文档综合只做一次，后续任务可以复用已经整理过的知识，同时在关键结论上回查原文。</p><h3 id="Schema：让-Agent-按领域规则维护知识"><a href="#Schema：让-Agent-按领域规则维护知识" class="headerlink" title="Schema：让 Agent 按领域规则维护知识"></a>Schema：让 Agent 按领域规则维护知识</h3><p>Schema 可以写在 <code>AGENTS.md</code>、Skill 或专用配置里。它需要定义：</p><ul><li>Wiki 有哪些页面类型和字段；</li><li>新资料如何进入，哪些页面必须更新；</li><li>Query 时先查什么，什么时候必须回源；</li><li>哪些结论可以自动写入，哪些必须人工审核；</li><li>如何记录 Citation、Revision、冲突和不确定性；</li><li>Lint 要检查哪些结构与内容问题。</li></ul><p>没有 Schema，LLM Wiki 容易退化成模型自由发挥的笔记目录。Schema 才是把通用 LLM 变成领域知识维护者的关键约束。</p><h2 id="Ingest、Query、Lint：知识库的三个基本操作"><a href="#Ingest、Query、Lint：知识库的三个基本操作" class="headerlink" title="Ingest、Query、Lint：知识库的三个基本操作"></a>Ingest、Query、Lint：知识库的三个基本操作</h2><p>三层结构落地后，Agent 只需要掌握三个稳定操作。</p><h3 id="Ingest：把新资料编译进现有知识"><a href="#Ingest：把新资料编译进现有知识" class="headerlink" title="Ingest：把新资料编译进现有知识"></a>Ingest：把新资料编译进现有知识</h3><p>一次 Ingest 不应该只生成摘要。完整流程至少包括：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">读取新 Source</span><br><span class="line">  → 提取事实、实体、关系与适用范围</span><br><span class="line">  → 查找已有相关页面</span><br><span class="line">  → 新增或更新 Wiki 页面</span><br><span class="line">  → 标记冲突与待确认项</span><br><span class="line">  → 更新 index.md 与 log.md</span><br><span class="line">  → 保留 Source Citation</span><br></pre></td></tr></table></figure><p>增量更新比全量重建更重要。数据资产和业务口径每天都可能变化，系统必须知道这次新增了什么、替换了什么、哪些旧结论仍然有效。</p><h3 id="Query：先复用知识，再回到事实源"><a href="#Query：先复用知识，再回到事实源" class="headerlink" title="Query：先复用知识，再回到事实源"></a>Query：先复用知识，再回到事实源</h3><p>Query 的默认路径是先读取 Wiki，快速定位主题、实体和已有综合结论。遇到下面几类内容时，再回到 Raw Sources 核验：</p><ul><li>字段口径、时间范围、过滤条件等精确事实；</li><li>可能已被新版本替代的结论；</li><li>会影响代码生成或生产任务的关键规则；</li><li>Wiki 中明确标记为冲突或低置信度的内容。</li></ul><p>这样既避免每次从海量原文重新综合，也不会把 AI 生成的 Wiki 当成最终事实。</p><h3 id="Lint：让知识库能够长期维护"><a href="#Lint：让知识库能够长期维护" class="headerlink" title="Lint：让知识库能够长期维护"></a>Lint：让知识库能够长期维护</h3><p>知识库规模上来以后，检索能否返回答案只是最低门槛。Lint 还要持续检查：</p><ul><li>同一概念是否出现多个命名；</li><li>新旧页面是否存在未处理的矛盾；</li><li>页面是否缺少来源或 Revision；</li><li>是否出现没有入链的孤岛页面；</li><li>重要实体是否只被提及，却没有独立页面；</li><li>Source 已更新，Wiki 页面是否仍停留在旧版本。</li></ul><p>结构检查可以自动执行；涉及业务语义的冲突，应该进入人工审核，而不是让模型自行决定哪一方是真相。</p><h2 id="Agent-Skill-应该怎样改造"><a href="#Agent-Skill-应该怎样改造" class="headerlink" title="Agent Skill 应该怎样改造"></a>Agent Skill 应该怎样改造</h2><p>改造前，Skill 的知识路径通常是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">用户需求</span><br><span class="line">  → Skill</span><br><span class="line">  → 静态 Reference</span><br><span class="line">  → 生成设计与代码</span><br></pre></td></tr></table></figure><p>改造后，Skill 不再携带全部业务知识，而是声明查询策略和质量门槛：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">用户需求</span><br><span class="line">  → Skill 解析任务</span><br><span class="line">  → Query Wiki 定位知识</span><br><span class="line">  → 回查 Raw Sources 核验关键事实</span><br><span class="line">  → 生成设计与代码</span><br><span class="line">  → 人工校准与执行验证</span><br><span class="line">  → 经验写回 Raw Sources</span><br><span class="line">  → 增量 Ingest</span><br></pre></td></tr></table></figure><p>Skill 内部可以保留这样的稳定契约：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">knowledge_policy:</span></span><br><span class="line">  <span class="attr">query_first:</span> <span class="string">wiki</span></span><br><span class="line">  <span class="attr">source_of_truth:</span> <span class="string">raw_sources</span></span><br><span class="line">  <span class="attr">require_citation_for:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">field_definition</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">business_rule</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">production_code</span></span><br><span class="line">  <span class="attr">reject_when:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">unresolved_conflict</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">missing_revision</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">permission_denied</span></span><br><span class="line">  <span class="attr">feedback:</span></span><br><span class="line">    <span class="attr">destination:</span> <span class="string">reviewed_case_notes</span></span><br><span class="line">    <span class="attr">ingest_after_review:</span> <span class="literal">true</span></span><br></pre></td></tr></table></figure><p>这里最重要的变化，是 Skill 从“知识容器”变成“知识消费者和执行编排器”。领域知识可以独立演进，Skill 只需要保持操作协议稳定。</p><h2 id="一个脱敏的数据研发例子"><a href="#一个脱敏的数据研发例子" class="headerlink" title="一个脱敏的数据研发例子"></a>一个脱敏的数据研发例子</h2><p>假设数据同学收到一份“实验分组回收”的需求。任务需要找到现有核心表、确认分组口径、设计新增字段，并生成 DDL 与 ETL 任务。</p><p>静态 Reference 只能提供通用建模规范和代码模板。真正决定方案正确性的知识分散在资产说明、历史设计、现有 SQL 与最近一次口径调整中。</p><p>接入 LLM Wiki 后，执行链路可以变成：</p><ol><li>Skill 读取需求调研文档，提取业务对象、指标、时间范围和待确认项。</li><li>Query 从 Wiki 定位相关主题页、核心表、字段口径和历史设计结论。</li><li>对会进入代码的字段与过滤条件回查 Raw Sources，固定 Citation 与 Revision。</li><li>Skill 生成模型设计文档，显式列出已确认事实、推导结论和待确认问题。</li><li>设计确认后，代码生成 Skill 按模板产生 DDL、ETL 与发布清单。</li><li>测试和评审中发现的新口径，先进入经过审核的案例记录，再触发增量 Ingest。</li></ol><p>这个流程没有让 Agent 自动拥有最终决定权。它缩短的是查资料、建立关联和维护知识的时间；模型设计是否正确，仍然要由 Citation、测试结果和领域 Owner 的确认来证明。</p><h2 id="实践反哺为什么是关键闭环"><a href="#实践反哺为什么是关键闭环" class="headerlink" title="实践反哺为什么是关键闭环"></a>实践反哺为什么是关键闭环</h2><p>很多知识库只完成“资料进入系统”，没有处理“任务执行后产生的新知识”。结果是 Wiki 看起来越来越大，Agent 却在重复踩同样的坑。</p><p>真正有价值的反馈通常来自三类场景：</p><ul><li>Agent 找到了资料，但选错了适用范围；</li><li>设计结论基本正确，但遗漏了一个业务例外；</li><li>代码运行成功，却暴露出文档和真实实现不一致。</li></ul><p>这些反馈不能直接写成新的事实。更稳妥的链路是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">执行观察</span><br><span class="line">  → 人工确认</span><br><span class="line">  → 形成审核后的案例或口径变更</span><br><span class="line">  → 写入 Raw Sources</span><br><span class="line">  → Ingest 更新 Wiki</span><br><span class="line">  → Lint 检查受影响页面</span><br></pre></td></tr></table></figure><p>这样，知识库会随着真实任务逐步校准，而不是随着模型回答数量机械膨胀。</p><h2 id="这套架构的四个硬门槛"><a href="#这套架构的四个硬门槛" class="headerlink" title="这套架构的四个硬门槛"></a>这套架构的四个硬门槛</h2><h3 id="1-Wiki-不能替代事实源"><a href="#1-Wiki-不能替代事实源" class="headerlink" title="1. Wiki 不能替代事实源"></a>1. Wiki 不能替代事实源</h3><p>Wiki 是可重建的知识产物。删除 Wiki 后，系统应该能够从 Raw Sources 重新编译；删除 Raw Sources 后，只剩 Wiki 就失去了可信根基。</p><h3 id="2-Citation-与-Revision-必须进入查询协议"><a href="#2-Citation-与-Revision-必须进入查询协议" class="headerlink" title="2. Citation 与 Revision 必须进入查询协议"></a>2. Citation 与 Revision 必须进入查询协议</h3><p>“回答里带了一个链接”还不够。关键 Claim 应该对应具体 Source Span，并固定查询时使用的 Revision。否则新旧口径仍可能混进同一个方案。</p><h3 id="3-权限必须早于召回"><a href="#3-权限必须早于召回" class="headerlink" title="3. 权限必须早于召回"></a>3. 权限必须早于召回</h3><p>知识编译不能绕过原始权限。Source、Wiki 页面和查询结果都要继承 Scope 与 Permission，过滤应该发生在召回和生成之前。</p><h3 id="4-评测不能只看代码生成比例"><a href="#4-评测不能只看代码生成比例" class="headerlink" title="4. 评测不能只看代码生成比例"></a>4. 评测不能只看代码生成比例</h3><p>AI 生成了多少代码，只能说明工具参与度。知识底座更应该评估：核心事实召回率、Citation 正确率、冲突发现率、过期知识命中率、方案返工率，以及任务完成后是否形成了可复用的新知识。</p><h2 id="要不要用"><a href="#要不要用" class="headerlink" title="要不要用"></a>要不要用</h2><p>我的判断是：</p><ul><li><strong>值得上</strong>：同一类任务反复发生，知识分散且持续变化，错误口径会直接影响设计或生产代码。</li><li><strong>可以再等</strong>：资料规模小、规则稳定、任务复用率低，几份静态 Reference 已经能够清楚覆盖。</li><li><strong>重点关注</strong>：编译产物的 Citation 完整性、增量更新的影响范围、语义冲突的人工审核，以及权限是否贯穿 Raw、Wiki 与 Query。</li></ul><p>不要把 LLM Wiki 当成更复杂的 RAG，也不要把它当成新的万能知识平台。它最有价值的地方，是把 Agent 每次临时找资料、临时综合、临时纠错的过程，沉淀成一个可以持续维护的知识系统。</p><p>对 Agent Skill 来说，合理的边界也因此更清楚：<strong>稳定方法写进 Skill，稳定规则留在 Reference，变化中的事实进入知识库，执行中得到的经验经过审核后再反哺事实源。</strong></p><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><ul><li><a href="https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f">Andrej Karpathy：LLM Wiki</a></li></ul><blockquote><p>本文使用 <a href="https://github.com/zisheng-ai/writting-skill">writting-skill</a> 辅助写作，配图使用 <a href="https://github.com/zisheng-ai/better-imagegen">better-imagegen</a> 生成。项目已开源，欢迎在 GitHub 点个 Star。</p></blockquote>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/6637fa6941f7/</id>
    <link href="https://blog.zisheng.pro/posts/6637fa6941f7/"/>
    <published>2026-08-21T03:22:19.000Z</published>
    <summary>Agent Skill 可以封装流程，却不适合长期内嵌持续变化的业务知识。本文以脱敏的数据研发场景为例，拆解 Raw Sources、Wiki、Schema 三层架构，以及 Ingest、Query、Lint 如何把静态 Reference 升级为可追溯、可校准、可持续演进的知识底座。</summary>
    <title>从静态 Reference 到 LLM Wiki：让 Agent Skill 拥有可持续演进的知识底座</title>
    <updated>2026-09-18T08:15:28.636Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="AI" scheme="https://blog.zisheng.pro/categories/AI/"/>
    <category term="Agent" scheme="https://blog.zisheng.pro/tags/Agent/"/>
    <category term="Git" scheme="https://blog.zisheng.pro/tags/Git/"/>
    <category term="worktree" scheme="https://blog.zisheng.pro/tags/worktree/"/>
    <category term="AI Coding" scheme="https://blog.zisheng.pro/tags/AI-Coding/"/>
    <content>
      <![CDATA[<p>AI 编程真正改变的，不是我敲代码更快，而是同一时间能推进的事情突然变多了：一个 Agent 在修构建，一个在写文档，一个分支还留着线上问题的复现现场。</p><p>这时最容易出问题的不是模型能力，而是工作目录。切分支会覆盖未提交改动；为了并行而多 clone 几份，又会把依赖、构建产物和仓库状态弄得难以辨认。<code>git worktree</code> 正好补上了这一层：共享一个 Git 对象库，同时提供多个相互隔离的工作目录。</p><span id="more"></span><h2 id="一句话总结"><a href="#一句话总结" class="headerlink" title="一句话总结"></a>一句话总结</h2><p><code>git worktree</code> 是同一个 Git 仓库的多个 checkout。每个 checkout 有自己的分支、文件和未提交改动，但共享提交历史与对象数据库。</p><p>它在 AI 时代特别重要，是因为它把“并行执行”变成了可管理的物理边界：每个任务、每个 Agent、每个验证环境都有独立目录，而不是在同一个工作区里抢状态。</p><p><img src="/images/git-worktree-ai-era-topology.png" alt="同一仓库的多个独立工作目录"></p><h2 id="它和-clone、branch-到底有什么区别"><a href="#它和-clone、branch-到底有什么区别" class="headerlink" title="它和 clone、branch 到底有什么区别"></a>它和 clone、branch 到底有什么区别</h2><p>先把三个概念分开：</p><table><thead><tr><th>概念</th><th>解决什么</th><th>文件目录</th><th>Git 对象库</th></tr></thead><tbody><tr><td>branch</td><td>保存不同历史线</td><td>通常共用一个目录</td><td>共用</td></tr><tr><td>clone</td><td>获得一份完整副本</td><td>独立</td><td>各自一份</td></tr><tr><td>worktree</td><td>同时 checkout 多个分支</td><td>独立</td><td>共用同一份</td></tr></tbody></table><p>普通开发里，分支和工作目录是一对一的：我在 <code>main</code> 上改了一半，要看 hotfix，就得 stash、提交半成品，或者冒险切分支。worktree 允许我把 hotfix 放进另一间“房间”。</p><p>它不是替代 branch。branch 仍然定义历史；worktree 只是让多条历史线同时拥有可工作的文件系统。</p><h2 id="最常用的三个命令"><a href="#最常用的三个命令" class="headerlink" title="最常用的三个命令"></a>最常用的三个命令</h2><p>假设当前仓库在 <code>~/project</code>：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 为已有分支创建一个并行工作目录</span></span><br><span class="line">git worktree add ../project-hotfix hotfix/login-timeout</span><br><span class="line"></span><br><span class="line"><span class="comment"># 新建分支并同时创建目录</span></span><br><span class="line">git worktree add -b feat/agent-release ../project-agent-release main</span><br><span class="line"></span><br><span class="line"><span class="comment"># 查看所有 worktree 与对应分支</span></span><br><span class="line">git worktree list</span><br></pre></td></tr></table></figure><p>完成后再清理：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">git worktree remove ../project-hotfix</span><br><span class="line">git worktree prune</span><br></pre></td></tr></table></figure><p><code>remove</code> 删除的是那个工作目录，不会删除分支和提交历史。要删除分支，仍然显式执行 <code>git branch -d</code>。这条边界很重要：目录生命周期和代码历史生命周期不是一回事。</p><h2 id="为什么-AI-让它从技巧变成基础设施"><a href="#为什么-AI-让它从技巧变成基础设施" class="headerlink" title="为什么 AI 让它从技巧变成基础设施"></a>为什么 AI 让它从技巧变成基础设施</h2><p>过去我一个人开发，切分支的成本主要是心智负担。现在一个需求经常天然拆成多条链路：实现、测试、构建、文档、代码审查、发布验证。AI Agent 能同时跑这些任务，但它不会自动解决共享目录的冲突。</p><p>例如一次发布排障，最稳妥的结构往往是：</p><ul><li>主 worktree：保留当前真实状态，不动未提交工作。</li><li><code>release/*</code> worktree：只从指定 commit 打包发布，保证制品可追溯。</li><li><code>fix/*</code> worktree：验证补丁或复现失败，不污染发布输入。</li><li><code>docs/*</code> worktree：写说明、补测试或整理变更。</li></ul><p><img src="/images/git-worktree-ai-era-collaboration.png" alt="人和 AI Agent 在隔离工作区中协作"></p><p>这不是为了形式上的“多 Agent”。真正的收益是，每个 Agent 的输入和输出都更明确：它只能在自己的目录里修改；构建日志、<code>node_modules</code>、生成文件和未提交 diff 都不会悄悄影响另一个任务。</p><h2 id="一个适合-Agent-的工作流"><a href="#一个适合-Agent-的工作流" class="headerlink" title="一个适合 Agent 的工作流"></a>一个适合 Agent 的工作流</h2><p>我更推荐按任务边界创建，而不是按人名创建：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 保持主目录只做集成和最终检查</span></span><br><span class="line">git worktree add -b fix/importer-patch ../flyos-fix-importer main</span><br><span class="line">git worktree add -b release/1.1.4 ../flyos-release-1.1.4 main</span><br><span class="line">git worktree add -b docs/release-notes ../flyos-release-docs main</span><br></pre></td></tr></table></figure><p>然后约定四件事：</p><ol><li>一个 worktree 只服务一个可描述的目标。</li><li>Agent 完成后报告 commit、验证命令和残留风险，而不是只说“改好了”。</li><li>发布 worktree 必须从明确 commit 创建；不要把主目录中未提交的实验改动一起打包。</li><li>主 worktree 不承担长时间构建和大范围试验，它只负责集成、审查与最终提交。</li></ol><p>这套约定比“让 Agent 小心一点”可靠得多。边界落在目录和 Git 记录上，后续的人也能复现。</p><h2 id="它不解决什么"><a href="#它不解决什么" class="headerlink" title="它不解决什么"></a>它不解决什么</h2><p>worktree 不是隔离环境的万能替代品。</p><ul><li>它们仍共享同一个仓库的 Git 对象库，不能把它当成权限隔离。</li><li>端口、全局缓存、Docker 容器、数据库和远端环境依然可能冲突。</li><li>同一分支不能被两个 worktree 同时 checkout；这是 Git 用来避免同一分支出现两份独立未提交状态的保护。</li><li>大型依赖目录通常会各自安装，磁盘占用仍需管理。</li></ul><p>如果任务需要真正的安全边界或不同系统镜像，仍然要用 container、VM 或独立构建机。worktree 解决的是代码工作区隔离，不是运行时隔离。</p><h2 id="要不要用-我的判断框架"><a href="#要不要用-我的判断框架" class="headerlink" title="要不要用 &#x2F; 我的判断框架"></a>要不要用 &#x2F; 我的判断框架</h2><p>值得上：你同时处理发布、修复、审查或多个 Agent 任务；你不想为了切分支 stash 半成品；你需要从一个干净 commit 产出可追溯制品。</p><p>可以再等：仓库很小、任务严格串行，而且你几乎没有未提交改动。</p><p>重点关注：不要把 worktree 当成“多开几个目录”就结束了。给每个目录一个任务边界、一个分支和一份验证证据，才是它在 AI 时代真正放大交付能力的方式。</p>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/701fa4e342a0/</id>
    <link href="https://blog.zisheng.pro/posts/701fa4e342a0/"/>
    <published>2026-08-18T00:20:00.000Z</published>
    <summary>Git worktree 让同一个仓库同时拥有多个独立工作目录。它解决的不是“少 clone 一次”，而是人和 AI Agent 并行开发时的上下文隔离、可验证交付与低成本切换问题。</summary>
    <title>Git worktree：AI 时代的并行开发工作台</title>
    <updated>2026-09-18T08:15:28.629Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="AI" scheme="https://blog.zisheng.pro/categories/AI/"/>
    <category term="Distribution Profile" scheme="https://blog.zisheng.pro/tags/Distribution-Profile/"/>
    <category term="Build Profile" scheme="https://blog.zisheng.pro/tags/Build-Profile/"/>
    <category term="User Profile" scheme="https://blog.zisheng.pro/tags/User-Profile/"/>
    <category term="Product Engineering" scheme="https://blog.zisheng.pro/tags/Product-Engineering/"/>
    <category term="FlyOS" scheme="https://blog.zisheng.pro/tags/FlyOS/"/>
    <content>
      <![CDATA[<p><img src="/images/profile-meaning-contexts.webp" alt="Profile 在三种技术语境中的含义"></p><p><em>先看 <code>Profile</code> 描述的是谁，再决定它该翻译成什么。</em></p><p>我之前把 <code>Profile</code> 直接解释成“产品交付策略”，后来发现这个说法太快了。读者还没理解 <code>Profile</code> 这个词本身，就被带进了 Distribution、Runtime 和 Build Kit，当然容易越看越糊涂。</p><p>更准确的说法是：<strong><code>Profile</code> 没有唯一的技术含义。它表示某个对象在一组维度上的完整描述。对象不同，中文意思也不同。</strong></p><p>浏览器里的 User Profile 是用户资料；构建系统里的 Build Profile 是一套构建方案；FlyOS 里的 Distribution Profile 则是产品发行配置。技术文档如果只写一个裸的 <code>Profile</code>，通常不是读者不懂，而是作者省略了最重要的前缀。</p><h2 id="一句话总结"><a href="#一句话总结" class="headerlink" title="一句话总结"></a>一句话总结</h2><p>把 <code>Profile</code> 理解成“配置”只对了一半。它更接近一份<strong>特征档案或成套方案</strong>：把描述同一个对象的多项信息放在一起，形成一个可以识别、选择或执行的整体。</p><p>所以，看到 <code>Profile</code> 时先补全这句话：</p><blockquote><p>这是 <strong>谁的 Profile</strong>，它要描述这个对象的 <strong>哪些特征</strong>，又给 <strong>谁使用</strong>？</p></blockquote><p>这三个问题一旦有答案，Profile 的含义通常就清楚了。</p><h2 id="为什么同一个词会有这么多意思"><a href="#为什么同一个词会有这么多意思" class="headerlink" title="为什么同一个词会有这么多意思"></a>为什么同一个词会有这么多意思</h2><p><code>Profile</code> 的共同点不是“里面都有配置项”，而是它们都在给某个对象画轮廓。</p><table><thead><tr><th>完整术语</th><th>描述的对象</th><th>更自然的中文理解</th><th>典型内容</th></tr></thead><tbody><tr><td>Personal Profile</td><td>一个人</td><td>个人简介、人物资料</td><td>姓名、经历、技能、头像</td></tr><tr><td>Browser User Profile</td><td>一个浏览器用户</td><td>用户资料、用户数据档案</td><td>Cookie、历史记录、扩展、站点权限、偏好</td></tr><tr><td>Build Profile</td><td>一次构建方案</td><td>构建档位、构建预设</td><td>Debug&#x2F;Release、符号级别、优化参数、输出目录</td></tr><tr><td>Distribution Profile</td><td>一个产品发行版</td><td>发行配置、产品交付配置</td><td>产品身份、目标平台、Runtime、更新与交付策略</td></tr><tr><td>Profiling</td><td>一个程序的运行行为</td><td>性能分析、性能剖析</td><td>CPU、内存、调用栈、耗时分布</td></tr></tbody></table><p>最后一行尤其容易混淆。<code>profiling</code> 是分析程序运行特征的动作，产物可能是 CPU Profile 或 Memory Profile；它和产品发行配置不是同一类东西。</p><p>这也解释了为什么不能脱离前缀给 <code>Profile</code> 下定义。就像只说“端”而不说客户端、服务端还是端口，单词认识了，工程对象仍然没确定。</p><h2 id="Profile、Config-和-Settings-有什么区别"><a href="#Profile、Config-和-Settings-有什么区别" class="headerlink" title="Profile、Config 和 Settings 有什么区别"></a>Profile、Config 和 Settings 有什么区别</h2><p>这三个词经常同时出现，但关注点不同。</p><p><code>Setting</code> 通常是一个可调整的选项，例如主题、语言或首页。<code>Settings</code> 是这些选项的集合，常常直接面向用户。</p><p><code>Config</code> 的范围最宽。一个端口号、一份 JSON、环境变量和整套服务配置，都可以叫 configuration。</p><p><code>Profile</code> 往往强调一组成套、彼此协调的特征。选择一个 Profile，通常不是只改一个开关，而是切换到一整套已经定义好的身份或方案。例如 <code>production</code> Build Profile 可能同时决定优化级别、符号、输出目录和是否生成安装包。</p><p>可以用一个很实用的判断来区分：</p><table><thead><tr><th>你在做什么</th><th>更可能使用的词</th></tr></thead><tbody><tr><td>调一个独立开关</td><td>Setting</td></tr><tr><td>写入任意配置值</td><td>Config</td></tr><tr><td>选择一整套有明确用途的方案</td><td>Profile</td></tr></tbody></table><p>这不是语言标准，只是工程里常见的命名倾向。最终仍要回到具体项目的 contract 和文档。</p><h2 id="截图里的-Profile-指什么"><a href="#截图里的-Profile-指什么" class="headerlink" title="截图里的 Profile 指什么"></a>截图里的 Profile 指什么</h2><p>当一句话写着“Build Kit 按 Profile 选择 engine”时，这里的完整名称应该是 <strong>Distribution Profile</strong>。</p><p>它不是浏览器里的用户 Profile，也不是开发者选择的 Debug&#x2F;Release Build Profile。它描述的是：这一个 Browser Distribution 准备交付成什么产品。</p><p>以当前 FlyOS 为例，Distribution Profile 是 Distribution 仓库里的 <code>flyos.json</code>。截至 2026 年 8 月 26 日，当前 contract 使用 schema v5，主要声明：</p><ol><li>产品身份与 App 版本；</li><li>要交付的目标平台；</li><li>品牌资源与平台身份；</li><li>Runway 和内置 Extension 的选择；</li><li>Browser、Runway 与 Extension 的更新策略；</li><li>可选的 Lean delivery 约束。</li></ol><p>Build Kit 读取这份 Profile，再结合 lock、已经安装的 package、Chromium checkout 和目标平台 toolchain，完成后续构建与打包。</p><p>如果某个发行版同时支持 manual installer 和 automatic updater installer，那么“选择哪一种安装引擎”属于产品交付决策，可以由 Distribution Profile 表达或约束。这里的逻辑是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">Distribution Profile</span><br><span class="line">  → 声明这次要交付的产品与更新方式</span><br><span class="line">  → Build Kit 选择对应的构建、打包路径</span><br><span class="line">  → 生成最终安装包</span><br></pre></td></tr></table></figure><p>因此，截图里更不容易误解的写法应该是：</p><blockquote><p>Build Kit 根据 <strong>Distribution Profile</strong> 选择对应的 installer engine。</p></blockquote><p><code>Profile</code> 不是 engine，也不是安装器本身。它只是 Build Kit 的产品级输入之一。</p><h2 id="Distribution-Profile-不是用户数据"><a href="#Distribution-Profile-不是用户数据" class="headerlink" title="Distribution Profile 不是用户数据"></a>Distribution Profile 不是用户数据</h2><p>现在再回头看原来的结论就容易理解了：不是所有 Profile 都与用户无关，只有 <strong>Distribution Profile</strong> 与用户数据无关。</p><table><thead><tr><th>对象</th><th>谁维护</th><th>什么时候改变</th><th>是否跟着用户走</th></tr></thead><tbody><tr><td>Distribution Profile</td><td>产品与发行工程团队</td><td>开发、构建或发版时</td><td>否</td></tr><tr><td>Build Profile</td><td>构建工程团队</td><td>执行某种构建时</td><td>否</td></tr><tr><td>Browser User Profile</td><td>浏览器用户</td><td>日常使用中</td><td>是</td></tr><tr><td>User Settings</td><td>应用用户</td><td>日常使用中</td><td>通常是</td></tr></tbody></table><p>用户改深色模式，不应该重新生成安装包；产品切换更新方式，也不应该让用户去设置页完成。它们都叫“配置”，但所有权、生命周期和验收方式完全不同。</p><h2 id="Profile-和-lock-也不是一回事"><a href="#Profile-和-lock-也不是一回事" class="headerlink" title="Profile 和 lock 也不是一回事"></a>Profile 和 lock 也不是一回事</h2><p>在 FlyOS 的交付链路里，还要分清 Distribution Profile 与 <code>flyos-lock.yaml</code>。</p><p>Profile 由 Distribution 编写，表达“我想交付什么”；lock 由工具根据 Profile 和实际解析结果生成，记录“这次精确使用了什么”。前者是声明，后者是可复现证据。</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">flyos.json           flyos-lock.yaml</span><br><span class="line">期望状态       →      精确解析结果</span><br><span class="line">人工维护              工具生成</span><br><span class="line">产品选择              package / artifact identity</span><br></pre></td></tr></table></figure><p>修改 Profile 后需要重新解析和刷新 lock，而不是手工把两份文件改到“看起来一致”。</p><h2 id="Lean-Profile-又是什么"><a href="#Lean-Profile-又是什么" class="headerlink" title="Lean Profile 又是什么"></a>Lean Profile 又是什么</h2><p><code>Lean Profile</code> 也不是第四种全新的 Profile。在当前 FlyOS contract 里，<code>lean</code> 是 Distribution Profile 中可选的一组交付约束，例如只保留指定的 Chromium locale，或者明确排除某类 Runtime payload。</p><p>这里的 Lean 描述“这个发行版怎样精简交付内容”。它不代表一个常驻进程，也不是用户可以随手切换的“省内存模式”，更不等于 <code>fast-debug</code> 之类的 Build Profile。</p><p>把完整名称写出来，关系就很直观：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">Distribution Profile</span><br><span class="line">├── 产品身份与 target</span><br><span class="line">├── Runway / Extension / update 选择</span><br><span class="line">└── 可选的 Lean delivery 约束</span><br><span class="line"></span><br><span class="line">Build Profile</span><br><span class="line">└── production / symbolized / debuggable / fast-debug 等构建方案</span><br><span class="line"></span><br><span class="line">Browser User Profile</span><br><span class="line">└── Cookie / 历史记录 / 扩展 / 用户偏好</span><br></pre></td></tr></table></figure><p>三者可以同时存在，因为它们控制的是三条不同的轴。</p><h2 id="我的判断框架"><a href="#我的判断框架" class="headerlink" title="我的判断框架"></a>我的判断框架</h2><p>以后再看到 <code>Profile</code>，我会按这个顺序判断：</p><ol><li>先找前缀：User、Build、Distribution、CPU 还是别的对象；</li><li>再看所有者：用户、开发者、产品团队还是分析工具；</li><li>再看生命周期：运行中改变、构建时选择，还是发版时版本化；</li><li>最后看结果：它保存用户数据、切换构建方案，还是定义交付产品。</li></ol><p>如果一段技术文档只写 <code>Profile</code>，而上下文里又同时存在用户资料、构建方案和发行配置，最好的优化不是再补一句抽象定义，而是把完整术语写出来。</p><p>所以，截图里的答案可以压缩成一句话：<strong>那里的 <code>Profile</code> 是 <code>Distribution Profile</code>，指产品发行配置；<code>Profile</code> 这个词本身没有固定等于“交付策略”。</strong></p><blockquote><p>本文使用 <a href="https://github.com/zisheng-ai/writting-skill">writting-skill</a> 辅助写作，配图使用 <a href="https://github.com/zisheng-ai/better-imagegen">better-imagegen</a> 生成。项目已开源，欢迎在 GitHub 点个 Star。</p></blockquote>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/21a87445e60a/</id>
    <link href="https://blog.zisheng.pro/posts/21a87445e60a/"/>
    <published>2026-08-17T12:00:00.000Z</published>
    <summary>`Profile` 没有一个放之四海而皆准的中文解释。它可以指浏览器用户资料、工程构建方案，也可以指产品发行配置。本文从这个词的共同语义出发，重点解释 FlyOS 的 Distribution Profile、Build Profile、User Profile 与 Settings 分别描述什么，以及看到裸写的 Profile 时应该怎样判断。</summary>
    <title>Profile 到底是什么意思：先分清用户资料、构建方案与交付策略</title>
    <updated>2026-09-18T08:15:28.629Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="AI" scheme="https://blog.zisheng.pro/categories/AI/"/>
    <category term="AI Infra" scheme="https://blog.zisheng.pro/tags/AI-Infra/"/>
    <category term="AI Browser" scheme="https://blog.zisheng.pro/tags/AI-Browser/"/>
    <category term="BrowserOS" scheme="https://blog.zisheng.pro/tags/BrowserOS/"/>
    <category term="Chromium" scheme="https://blog.zisheng.pro/tags/Chromium/"/>
    <category term="Agent" scheme="https://blog.zisheng.pro/tags/Agent/"/>
    <category term="MCP" scheme="https://blog.zisheng.pro/tags/MCP/"/>
    <category term="CDP" scheme="https://blog.zisheng.pro/tags/CDP/"/>
    <content>
      <![CDATA[<p><img src="/images/ai-browser-browseros-seven-layers.webp" alt="AI 浏览器七层架构：六层运行时与一条工程交付控制面"></p><p><em>AI 浏览器的运行时负责理解和执行任务，工程交付控制面负责把整套系统变成可安装、可发布、可更新的产品。</em></p><p>最近我在基于 <a href="https://github.com/browseros-ai/BrowserOS">BrowserOS</a> 看 AI 浏览器的实现。最开始我也有一个很自然的理解：AI 浏览器大概就是 Chromium 加一个侧边栏，在里面调用大模型，再给 Extension 多开几个权限。</p><p>真正顺着源码往下拆以后，我发现这个理解只能解释最上面的一层。</p><p>一个完整的 AI 浏览器，至少同时包含浏览器内核、产品级原生集成、交互界面、Agent Runtime、浏览器工具协议、模型与外部连接，以及支撑多平台构建、签名、发布和更新的工程系统。它表面上是一个桌面应用，内部更接近一套被打包到本机运行的垂直 Agent 系统。</p><p>本文以 BrowserOS 仓库的 <a href="https://github.com/browseros-ai/BrowserOS/tree/5ddb0f2d04e15a2760f68729c217efaa2fd7437a"><code>5ddb0f2d0</code></a> commit 为源码快照。BrowserOS 后续会继续变化，固定 commit 能让文中的目录、代码和发布流程保持可复查。</p><h2 id="一句话总结"><a href="#一句话总结" class="headerlink" title="一句话总结"></a>一句话总结</h2><p><strong>AI 浏览器 &#x3D; Chromium 产品底座 + 原生集成 + Agent Runtime + Browser Tool Plane + Model &#x2F; MCP 生态 + Delivery Control Plane。</strong></p><p>这里最关键的不是某一个模型，也不是聊天 UI，而是两条完整链路：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">自然语言任务</span><br><span class="line">→ Agent 决策</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">→ Feed / OTA</span><br><span class="line">→ 已安装客户端</span><br></pre></td></tr></table></figure><p>第一条决定“它能不能完成任务”，第二条决定“它能不能作为一个软件产品持续交付”。</p><h2 id="先把几个容易混在一起的对象分开"><a href="#先把几个容易混在一起的对象分开" class="headerlink" title="先把几个容易混在一起的对象分开"></a>先把几个容易混在一起的对象分开</h2><p>分析 AI 浏览器之前，最好先把几个名词的边界钉住。</p><table><thead><tr><th>对象</th><th>它负责什么</th><th>它不是什么</th></tr></thead><tbody><tr><td>Chromium</td><td>渲染、网络、存储、Profile、进程与 Extension 基础</td><td>Agent Runtime</td></tr><tr><td>Extension UI</td><td>New Tab、Side Panel、设置与用户交互</td><td>整个 AI 浏览器</td></tr><tr><td>Agent Loop</td><td>调用模型、处理 Tool Call、持续推进任务</td><td>浏览器自动化协议</td></tr><tr><td>Browser Tool</td><td>面向模型的语义化浏览器能力</td><td>原始 CDP 命令</td></tr><tr><td>CDP</td><td>控制和观察 Chromium 的底层协议</td><td>MCP</td></tr><tr><td>MCP</td><td>将工具以标准协议暴露给 Agent Client</td><td>页面执行引擎</td></tr><tr><td>Updater</td><td>让客户端发现和安装已发布版本</td><td>Build 或 Release</td></tr></tbody></table><p>特别是 CDP 和 MCP，经常被泡在一起说。</p><p>CDP 的全称是 Chrome DevTools Protocol，它能创建 Tab、导航、执行 JavaScript、读取 DOM、截屏和监听浏览器事件。MCP 解决的是另一件事：如何把 <code>navigate</code>、<code>snapshot</code>、<code>act</code> 这类工具，以 Agent 能发现和调用的标准接口暴露出去。</p><p>简单说：<strong>CDP 更靠近浏览器，MCP 更靠近 Agent。</strong></p><h2 id="BrowserOS-的两个产品形态"><a href="#BrowserOS-的两个产品形态" class="headerlink" title="BrowserOS 的两个产品形态"></a>BrowserOS 的两个产品形态</h2><p>BrowserOS 当前仓库同时包含两个产品，这一点从根目录的 <a href="https://github.com/browseros-ai/BrowserOS/blob/5ddb0f2d04e15a2760f68729c217efaa2fd7437a/README.md">README</a> 就能看到：</p><ul><li>BrowserOS：给人使用的 AI 浏览器，Agent 出现在 New Tab 和 Side Panel 中；</li><li>BrowserOS neo：给外部 Agent 使用的浏览器，通过 MCP 接入 Claude Code、Codex、Cursor 等 Agent Client。</li></ul><p>它们共享 Chromium 和相当一部分浏览器能力，但入口不同。</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">BrowserOS</span><br><span class="line">人 → New Tab / Side Panel → 本地 Agent Runtime → 浏览器工具</span><br><span class="line"></span><br><span class="line">BrowserOS neo</span><br><span class="line">外部 Agent → MCP → Browser Runtime → 浏览器工具</span><br></pre></td></tr></table></figure><p>这篇文章以 BrowserOS 的主链路为主，因为它能完整展示一个“人使用的 AI 浏览器”如何工作。BrowserOS neo 放到后面作为架构演进：同一套底层浏览器能力，不一定只能服务内置聊天界面，也可以成为外部 Agent 的 Browser Runtime。</p><h2 id="第一层：Interaction-Surface"><a href="#第一层：Interaction-Surface" class="headerlink" title="第一层：Interaction Surface"></a>第一层：Interaction Surface</h2><p>第一层是用户和 Agent 能看见的入口。</p><p>BrowserOS 的 <a href="https://github.com/browseros-ai/BrowserOS/tree/5ddb0f2d04e15a2760f68729c217efaa2fd7437a/packages/browseros-agent/apps/app"><code>apps/app</code></a> 是一个使用 WXT 和 React 构建的 Extension。它提供 New Tab、Side Panel、设置、Onboarding 和 Chat UI。</p><p>从 <a href="https://github.com/browseros-ai/BrowserOS/blob/5ddb0f2d04e15a2760f68729c217efaa2fd7437a/packages/browseros-agent/apps/app/wxt.config.ts"><code>wxt.config.ts</code></a> 可以看到几个重要信号：</p><ul><li><code>chrome_url_overrides.newtab</code> 把 New Tab 指向 <code>app.html</code>；</li><li><code>sidePanel</code>、<code>tabs</code>、<code>bookmarks</code>、<code>history</code> 等权限提供浏览器交互入口；</li><li><code>host_permissions</code> 允许 Extension 访问本机 Server；</li><li><code>browserOS</code> 是 BrowserOS 自己增加的私有 Extension 权限；</li><li><code>update_url</code> 指向独立的 Extension 更新清单。</li></ul><p>这一层负责把任务、上下文和执行状态呈现给用户，但不要把它叫作“Agent”。UI 可以发起任务、订阅流式结果、展示 Tool Call，却不应该承担模型循环和底层浏览器状态管理。</p><p>同样，MCP Client 也属于 Interaction Surface。对 BrowserOS neo 来说，Claude Code 或 Codex 就是另一个交互入口，只不过输入来自外部 Agent，而不是浏览器里的输入框。</p><h2 id="第二层：Local-Agent-Runtime"><a href="#第二层：Local-Agent-Runtime" class="headerlink" title="第二层：Local Agent Runtime"></a>第二层：Local Agent Runtime</h2><p>用户提交任务后，请求进入本机运行的 Agent Server。</p><p>BrowserOS 的 <a href="https://github.com/browseros-ai/BrowserOS/tree/5ddb0f2d04e15a2760f68729c217efaa2fd7437a/packages/browseros-agent/apps/server"><code>apps/server</code></a> 使用 Bun 和 Hono，提供 <code>/chat</code>、<code>/mcp</code> 等接口。仓库的 <a href="https://github.com/browseros-ai/BrowserOS/blob/5ddb0f2d04e15a2760f68729c217efaa2fd7437a/packages/browseros-agent/README.md">Agent README</a> 给出了默认结构：Extension 或 MCP Client 通过 HTTP 与本地 Server 通信，Server 再通过 CDP 控制 Chromium。</p><p><a href="https://github.com/browseros-ai/BrowserOS/blob/5ddb0f2d04e15a2760f68729c217efaa2fd7437a/packages/browseros-agent/apps/server/src/api/server.ts"><code>api/server.ts</code></a> 负责启动 Hono 应用和 Bun Server，<a href="https://github.com/browseros-ai/BrowserOS/blob/5ddb0f2d04e15a2760f68729c217efaa2fd7437a/packages/browseros-agent/apps/server/src/agent/ai-sdk-agent.ts"><code>ai-sdk-agent.ts</code></a> 则把模型、Browser Tools、Filesystem Tools 和外部 MCP Tools 组合成 Agent Loop。</p><p>这一层至少承担：</p><ul><li>Conversation 和 Session；</li><li>Browser Context；</li><li>Model Provider 选择；</li><li>Prompt 与消息标准化；</li><li>Tool 注册与分发；</li><li>Context Window 和 Compaction；</li><li>Chat Mode 的只读工具限制；</li><li>流式输出、错误处理和运行指标。</li></ul><p>为什么要单独起一个本地 Server，而不是全部塞进 Extension Background？</p><p>我的理解是，独立进程给 Agent Runtime 提供了更稳定的执行边界。它可以拥有自己的数据库、文件系统访问、长任务、模型 SDK、MCP Client 和生命周期，也能被 Extension、CLI、MCP Client 等多个入口复用。</p><p>但“本地运行”不等于天然安全。本地 Server 仍然需要处理监听地址、来源校验、鉴权、Tool 权限和文件访问边界。Local-first 只是部署位置，不是安全结论。</p><h2 id="第三层：Model-与外部能力"><a href="#第三层：Model-与外部能力" class="headerlink" title="第三层：Model 与外部能力"></a>第三层：Model 与外部能力</h2><p>Agent Runtime 自己不会推理，它需要连接 Model Provider；只会操作网页也不够，它还可能要写 Notion、查 Linear、发邮件或处理本地文件。</p><p>BrowserOS 在 Agent Runtime 中把能力分成三类：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">Model</span><br><span class="line">  负责理解任务、生成下一步 Tool Call</span><br><span class="line"></span><br><span class="line">Browser Tools</span><br><span class="line">  负责观察和操作当前浏览器</span><br><span class="line"></span><br><span class="line">External MCP / Connect Apps</span><br><span class="line">  负责访问浏览器之外的业务系统</span><br></pre></td></tr></table></figure><p><a href="https://github.com/browseros-ai/BrowserOS/blob/5ddb0f2d04e15a2760f68729c217efaa2fd7437a/packages/browseros-agent/apps/server/src/agent/provider-factory.ts"><code>provider-factory.ts</code></a> 创建具体的 Language Model，<a href="https://github.com/browseros-ai/BrowserOS/blob/5ddb0f2d04e15a2760f68729c217efaa2fd7437a/packages/browseros-agent/apps/server/src/agent/mcp-builder.ts"><code>mcp-builder.ts</code></a> 连接外部 MCP Server。</p><p>这层的架构价值在于可替换性。只要上层 Agent Runtime 和下层 Tool Contract 保持稳定，模型可以从云端 Provider 切到本地模型，外部连接也可以从一个 MCP Server 换到另一个，而不必重写 Chromium 控制层。</p><p>当然，可替换不代表结果等价。不同模型对 Tool Schema、长上下文、截图理解和多步规划的表现不同，因此 Provider 切换之后仍然需要重新评测任务成功率，而不是只验证 API 能通。</p><h2 id="第四层：Browser-Capability"><a href="#第四层：Browser-Capability" class="headerlink" title="第四层：Browser Capability"></a>第四层：Browser Capability</h2><p>这是 AI 浏览器最容易被低估、也是最有工程价值的一层。</p><p>如果直接把原始 CDP 暴露给模型，模型会面对大量方法、参数、Target、Frame、Node 和 Session 状态。它不仅浪费 Token，也很难稳定地完成“点击登录按钮”这种在人类看来很简单的任务。</p><p>BrowserOS 在这一层继续拆成三部分：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">browser-mcp</span><br><span class="line">    语义工具：navigate、snapshot、act、screenshot、tabs……</span><br><span class="line">                ↓</span><br><span class="line">browser-core</span><br><span class="line">    状态抽象：BrowserSession、Page、Window、Navigation、Input</span><br><span class="line">                ↓</span><br><span class="line">CDP</span><br><span class="line">    底层执行：Target、Page、Runtime、DOM、Input、Network……</span><br></pre></td></tr></table></figure><p><a href="https://github.com/browseros-ai/BrowserOS/tree/5ddb0f2d04e15a2760f68729c217efaa2fd7437a/packages/browseros-agent/packages/browser-mcp"><code>browser-mcp</code></a> 定义 Agent 看得懂的 Browser Tools，<a href="https://github.com/browseros-ai/BrowserOS/tree/5ddb0f2d04e15a2760f68729c217efaa2fd7437a/packages/browseros-agent/packages/browser-core"><code>browser-core</code></a> 则把 CDP 包装成有状态的 Browser Session。</p><p>例如模型想点击页面元素时，不应该先猜 CSS Selector，再拼一串 CDP 命令。更稳定的方式是：</p><ol><li><code>snapshot</code> 返回对 Agent 友好的页面结构和元素引用；</li><li>模型根据语义选择引用；</li><li><code>act</code> 接收引用和动作；</li><li>Browser Core 将引用解析到真实页面元素；</li><li>CDP 执行输入事件；</li><li>Tool 返回新的页面状态或 Diff。</li></ol><p>所以，LLM 没有“点击页面”。<strong>它生成了一个点击意图，Browser Tool Plane 才负责把意图变成确定性的浏览器操作。</strong></p><p>这一层还应该维护几个重要约束：</p><ul><li>Page、Tab、Window 的稳定身份；</li><li>导航前后元素引用何时失效；</li><li>Frame 和弹窗如何归属；</li><li>Screenshot、DOM Snapshot 与当前页面是否来自同一时刻；</li><li>Tool 的副作用、超时、重试和错误语义；</li><li>多 Agent 并行时，谁拥有哪个 Tab 或 Session。</li></ul><p>模型能力会快速变化，但这些状态问题不会自己消失。对 AI 浏览器来说，真正值得长期积累的往往就是这层。</p><h2 id="第五层：Product-与-Native-Integration"><a href="#第五层：Product-与-Native-Integration" class="headerlink" title="第五层：Product 与 Native Integration"></a>第五层：Product 与 Native Integration</h2><p>如果 BrowserOS 只需要做一个网页助手，到第四层为止，用 Chrome Extension 加本地 Server 也可能实现大部分能力。</p><p>它选择 fork Chromium，是因为产品还需要控制 Extension 权限之外的浏览器生命周期。</p><p>BrowserOS 在 Chromium 中加入了 <code>chrome.browserOS.*</code> 私有 API。Extension 侧的 <a href="https://github.com/browseros-ai/BrowserOS/blob/5ddb0f2d04e15a2760f68729c217efaa2fd7437a/packages/browseros-agent/apps/app/lib/browseros/adapter.ts"><code>adapter.ts</code></a> 暴露了版本读取、Pref 读写、路径选择和指标记录等接口。</p><p>这一层可以包含：</p><ul><li>Browser 和 Sidecar Server 的启动、停止与端口配置；</li><li>私有 Extension API 与权限；</li><li>浏览器设置和产品 Pref；</li><li>首次启动、Importer、默认页面和产品入口；</li><li>内置 Extension、产品图标、名称和 Bundle Identity；</li><li>OS 级文件、进程、签名和更新集成。</li></ul><p>它相当于 Extension 世界与原生 Chromium 世界之间的一座桥。Extension 仍然适合快速构建 UI，但产品不再受限于标准 Extension API。</p><p>代价也很明显：一旦 fork Chromium，就要长期维护 Patch Stack、解决上游冲突、承担跨平台编译和安全更新压力。fork 不是“更高级”的默认答案，它只是用更高的维护成本换取更深的产品控制权。</p><h2 id="第六层：Chromium-Kernel"><a href="#第六层：Chromium-Kernel" class="headerlink" title="第六层：Chromium Kernel"></a>第六层：Chromium Kernel</h2><p>最底层仍然是 Chromium。</p><p>Blink 负责页面渲染，V8 执行 JavaScript，Network Stack 处理请求，Profile 保存 Cookie、密码、扩展和用户设置，多进程架构隔离 Browser、Renderer、GPU 等进程。BrowserOS 可以在上面加 Agent，但不能绕开这些基础设施。</p><p>这一层还决定了 AI 浏览器的一个重要特性：它操作的不是一个抽象网页，而是用户真实的浏览环境。</p><p>真实环境意味着已登录 Session、Cookie、本地扩展、下载、权限弹窗、多窗口和长期 Profile；也意味着一次错误点击可能产生真实副作用。相比临时启动的 Headless Browser，这种能力更接近日常工作，也更需要清晰的权限、确认和审计边界。</p><p>BrowserOS 的 Chromium 相关代码集中在 <a href="https://github.com/browseros-ai/BrowserOS/tree/5ddb0f2d04e15a2760f68729c217efaa2fd7437a/packages/browseros"><code>packages/browseros</code></a>，包括 Chromium 版本 Pin、Patches、替换文件、资源和 Python 构建系统。</p><h2 id="第七层：Engineering-Delivery-Control-Plane"><a href="#第七层：Engineering-Delivery-Control-Plane" class="headerlink" title="第七层：Engineering Delivery Control Plane"></a>第七层：Engineering Delivery Control Plane</h2><p>前六层解释了 AI 浏览器怎样运行，第七层解释它怎样成为一个能交付的软件产品。</p><p>我把它叫作 Engineering Delivery Control Plane，因为它不在一次 Agent 请求的调用链上，却贯穿 Browser、Server、Extension、CLI 和所有平台产物。</p><p>这一层负责回答：</p><ul><li>基于哪个 Chromium commit 构建？</li><li>哪些 Patch 按什么顺序应用？</li><li>BrowserOS 与 BrowserOS neo 各自包含哪些资源？</li><li>macOS、Windows、Linux 如何编译和签名？</li><li>多个平台怎样证明来自同一个 Release Candidate？</li><li>Browser、Extension、Server 和 CLI 怎样独立更新？</li><li>哪些产物只是 Staged，哪些已经真正发布？</li></ul><p>这也是为什么我不建议只从运行时代码理解 AI 浏览器。没有这层，前六层可以做 Demo，却很难稳定追上 Chromium 安全更新并持续交付给用户。</p><h2 id="一个任务如何穿过六层运行时"><a href="#一个任务如何穿过六层运行时" class="headerlink" title="一个任务如何穿过六层运行时"></a>一个任务如何穿过六层运行时</h2><p>现在用一个具体任务把调用链串起来：</p><blockquote><p>总结当前页面，并把结论写入 Notion。</p></blockquote><h3 id="1-Interaction-Surface-接收任务"><a href="#1-Interaction-Surface-接收任务" class="headerlink" title="1. Interaction Surface 接收任务"></a>1. Interaction Surface 接收任务</h3><p>用户在 Side Panel 输入指令。Extension 读取当前 Tab 和选中的上下文，向本机 <code>/chat</code> 发起请求。</p><h3 id="2-Agent-Runtime-创建执行上下文"><a href="#2-Agent-Runtime-创建执行上下文" class="headerlink" title="2. Agent Runtime 创建执行上下文"></a>2. Agent Runtime 创建执行上下文</h3><p>Server 恢复或创建 Conversation，组装 Browser Context、Model Config、Message History 和可用 Tools。</p><h3 id="3-Model-生成-Browser-Tool-Call"><a href="#3-Model-生成-Browser-Tool-Call" class="headerlink" title="3. Model 生成 Browser Tool Call"></a>3. Model 生成 Browser Tool Call</h3><p>模型并不知道页面完整内容，于是请求 <code>snapshot</code>。Agent Loop 不执行网页操作，只负责校验并分发这个 Tool Call。</p><h3 id="4-Browser-Capability-读取真实页面"><a href="#4-Browser-Capability-读取真实页面" class="headerlink" title="4. Browser Capability 读取真实页面"></a>4. Browser Capability 读取真实页面</h3><p><code>browser-mcp</code> 调用 <code>browser-core</code>，后者通过 CDP 从 Chromium 获得页面状态，再返回对模型更紧凑的 Snapshot。</p><h3 id="5-Agent-Loop-继续推理"><a href="#5-Agent-Loop-继续推理" class="headerlink" title="5. Agent Loop 继续推理"></a>5. Agent Loop 继续推理</h3><p>模型根据 Snapshot 生成总结。随后它决定调用 Notion 对应的外部 MCP Tool。</p><h3 id="6-External-Capability-产生业务副作用"><a href="#6-External-Capability-产生业务副作用" class="headerlink" title="6. External Capability 产生业务副作用"></a>6. External Capability 产生业务副作用</h3><p>Notion MCP 接收结构化参数，创建或更新页面，并把结果返回 Agent Runtime。</p><h3 id="7-Interaction-Surface-展示结果"><a href="#7-Interaction-Surface-展示结果" class="headerlink" title="7. Interaction Surface 展示结果"></a>7. Interaction Surface 展示结果</h3><p>Server 将执行状态和最终答案流式返回 Side Panel，用户看到任务结果。</p><p>完整路径可以压缩成：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">User</span><br><span class="line">  ↓</span><br><span class="line">New Tab / Side Panel</span><br><span class="line">  ↓ HTTP</span><br><span class="line">Local Agent Server</span><br><span class="line">  ↓</span><br><span class="line">Agent Loop ←→ LLM Provider</span><br><span class="line">  ↓ Tool Call</span><br><span class="line">Browser MCP / External MCP</span><br><span class="line">  ↓</span><br><span class="line">Browser Core</span><br><span class="line">  ↓ CDP</span><br><span class="line">Chromium</span><br><span class="line">  ↓</span><br><span class="line">Page State / Real Side Effect</span><br></pre></td></tr></table></figure><p>这里存在一个重要边界：BrowserOS 源码能证明这些组件和调用关系存在，但不能仅凭静态源码证明每个网站、每个模型、每个任务都能可靠完成。任务成功率仍然需要基于真实任务集、Provider 和环境做运行时评测。</p><h2 id="BrowserOS-怎样构建"><a href="#BrowserOS-怎样构建" class="headerlink" title="BrowserOS 怎样构建"></a>BrowserOS 怎样构建</h2><p>BrowserOS 没有把构建过程写成一条越来越长的 Shell Script，而是在 <a href="https://github.com/browseros-ai/BrowserOS/tree/5ddb0f2d04e15a2760f68729c217efaa2fd7437a/packages/browseros/bos_build"><code>bos_build</code></a> 中实现了一套 Python Build CLI。</p><p>它的心智模型是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">preset + product + platform + arch + switches</span><br><span class="line">                         ↓</span><br><span class="line">                ordered build steps</span><br></pre></td></tr></table></figure><p>其中：</p><ul><li><code>preset</code> 区分 Release、Debug 等构建目标；</li><li><code>product</code> 区分 BrowserOS 与 BrowserOS neo；</li><li><code>platform</code> 由当前 Host 决定；</li><li><code>arch</code> 可以是 x64、arm64 或 universal；</li><li><code>switches</code> 控制 Clean、Provision、Resource Mode、Sign、Upload 等行为。</li></ul><p><a href="https://github.com/browseros-ai/BrowserOS/blob/5ddb0f2d04e15a2760f68729c217efaa2fd7437a/packages/browseros/bos_build/core/planner.py"><code>planner.py</code></a> 根据这些输入生成有序步骤。一次完整构建大致经过：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">锁定 Chromium Source</span><br><span class="line">→ 准备或校验源码缓存</span><br><span class="line">→ Clean / Git Sync / Configure</span><br><span class="line">→ 构建或下载公共资源</span><br><span class="line">→ 应用文件替换和 Patch Stack</span><br><span class="line">→ 注入 Extension、Onboarding、Server</span><br><span class="line">→ 编译 Chromium</span><br><span class="line">→ 签名</span><br><span class="line">→ 打包</span><br><span class="line">→ 上传候选产物</span><br></pre></td></tr></table></figure><p>这里要注意两个点。</p><p>第一，BrowserOS 不是在 Chromium 编译完成后换个 Logo。Patches、替换文件、私有 API 和内置资源会在编译前进入源码树，最后生成一个新的 Chromium 产品。</p><p>第二，<code>build</code> 只负责生成当前 Host 能生成的产物。macOS 机器不能替代 Windows 完成 Windows 原生签名，单台机器也不能自然证明所有平台产物来自同一个版本。因此 Build 成功不等于完整 Release 完成。</p><h2 id="Build、Release、Update-是三件事"><a href="#Build、Release、Update-是三件事" class="headerlink" title="Build、Release、Update 是三件事"></a>Build、Release、Update 是三件事</h2><p>这三个词经常被混用，但它们对应三个不同的状态转换。</p><table><thead><tr><th>阶段</th><th>输入</th><th>输出</th><th>核心问题</th></tr></thead><tbody><tr><td>Build</td><td>源码、依赖、配置</td><td>二进制和安装包</td><td>能不能构建出来</td></tr><tr><td>Release</td><td>多平台产物、签名、Evidence</td><td>可发布候选和正式版本</td><td>这些产物是否一致、可信、可分发</td></tr><tr><td>Update</td><td>已发布版本、Feed、客户端策略</td><td>已安装客户端升级</td><td>客户端如何发现并安全消费新版本</td></tr></tbody></table><p>一个 Binary 在本地编译成功，只能证明 Build；上传到对象存储但没有切换 Feed，通常只是 Staged；客户端能从正式 Feed 发现并安装，才进入 Update 链路。</p><p>把这些状态分开，会少掉很多“明明发布了，为什么用户收不到”的问题。</p><h2 id="BrowserOS-怎样发布多平台版本"><a href="#BrowserOS-怎样发布多平台版本" class="headerlink" title="BrowserOS 怎样发布多平台版本"></a>BrowserOS 怎样发布多平台版本</h2><p>BrowserOS 的完整 Release 设计记录在 <a href="https://github.com/browseros-ai/BrowserOS/blob/5ddb0f2d04e15a2760f68729c217efaa2fd7437a/packages/browseros/bos_build/docs/release-ci.md"><code>release-ci.md</code></a> 和 <a href="https://github.com/browseros-ai/BrowserOS/blob/5ddb0f2d04e15a2760f68729c217efaa2fd7437a/.github/workflows/release-browseros.yml"><code>release-browseros.yml</code></a> 中。</p><p>主流程是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">从 main 手动触发</span><br><span class="line">→ 创建或恢复不可变 Candidate Commit / PR</span><br><span class="line">→ 公共资源只构建一次</span><br><span class="line">→ Linux x64</span><br><span class="line">→ Windows x64 + Signing</span><br><span class="line">→ macOS arm64 / x64 / universal + Signing / Notarization</span><br><span class="line">→ 校验所有 Lane Evidence</span><br><span class="line">→ 合并未发生变化的 Candidate PR</span><br><span class="line">→ 创建 Draft GitHub Release</span><br><span class="line">→ 生成 Appcast Preview</span><br><span class="line">→ 人工检查并 Promotion</span><br></pre></td></tr></table></figure><p>这里最关键的设计是 immutable candidate。</p><p>多平台编译通常需要不同 Runner，耗时也不同。如果每个 Runner 都在执行时读取最新 <code>main</code>，那么 Linux 开始编译后，<code>main</code> 可能又进入了新 commit；最终几个安装包虽然写着同一个版本号，内部源码却可能不一致。</p><p>BrowserOS 先冻结 Candidate SHA，再让公共资源和各平台 Lane 都绑定这个 SHA、Browser Version、Component Version 和资源摘要。Release Gate 只有在完整矩阵通过后才允许继续。</p><p>这不是为了让 CI 看起来复杂，而是在解决一个真实的 Supply Chain 问题：<strong>同一个 Release 中的多个产物，如何证明它们属于同一次构建意图。</strong></p><p>另一个值得借鉴的边界是“CI 只 Stage，不自动 Promote”。</p><p>完整 Release Workflow 会准备 R2 产物、Draft GitHub Release 和 Appcast Preview，但正式下载别名和更新 Feed 仍然需要显式发布。因为 Promotion 会直接影响已安装用户，它应该是一次独立、可检查、可回滚的决定。</p><h2 id="Update-不是一个-Updater"><a href="#Update-不是一个-Updater" class="headerlink" title="Update 不是一个 Updater"></a>Update 不是一个 Updater</h2><p>AI 浏览器不是单一 Binary。BrowserOS 至少包含 Browser、Extension、Local Server、BrowserOS neo Server、Onboarding 和 CLI 等可独立演进的产物。</p><table><thead><tr><th>产物</th><th>版本与发布方式</th><th>客户端发现方式</th></tr></thead><tbody><tr><td>Browser</td><td>多平台签名安装包、版本化对象、Release Metadata</td><td>Browser Appcast、正式下载别名或平台安装流程</td></tr><tr><td>Extension</td><td>固定 Extension ID、签名 CRX</td><td><code>update-manifest.xml</code></td></tr><tr><td>Local Agent Server</td><td>独立 Server Bundle 和版本</td><td>签名 OTA Appcast</td></tr><tr><td>BrowserOS neo Server</td><td>Rust Native Bundle、独立版本</td><td>独立 OTA Feed</td></tr><tr><td>Onboarding</td><td>独立资源 Bundle</td><td>Browser Build 或资源发布链路消费</td></tr><tr><td>CLI</td><td>独立 Workflow、Tag 和安装清单</td><td>CLI 安装或升级入口</td></tr></tbody></table><p>从 <a href="https://github.com/browseros-ai/BrowserOS/blob/5ddb0f2d04e15a2760f68729c217efaa2fd7437a/packages/browseros/bos_build/README.md"><code>bos_build/README.md</code></a> 和 Release Workflows 可以看到，Extension、Server 与 Browser 并不强制共享一个版本号。</p><p>这很合理。一个 Extension UI 修复不应该要求重新编译整个 Chromium；Server Agent Loop 更新也不一定要重新发布 Browser 安装包。独立版本减少了发布成本，但同时引入兼容性问题。</p><p>因此，一个完整的多产物更新系统至少需要：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">Component Version</span><br><span class="line">+ Compatibility Contract</span><br><span class="line">+ Immutable Artifact</span><br><span class="line">+ Signature / Checksum</span><br><span class="line">+ Mutable Feed / Alias</span><br><span class="line">+ Client Update Policy</span><br><span class="line">+ Rollback Strategy</span><br></pre></td></tr></table></figure><p>其中最容易被忽略的是 Compatibility Contract。Browser 内置的私有 API、Extension、Server 和 Browser Tools 如果独立升级，必须明确支持哪些相邻版本。否则每个组件都更新成功，组合在一起反而不能运行。</p><h2 id="为什么需要-BrowserOS-neo"><a href="#为什么需要-BrowserOS-neo" class="headerlink" title="为什么需要 BrowserOS neo"></a>为什么需要 BrowserOS neo</h2><p>BrowserOS neo 展示了这套分层的另一个价值：交互层可以替换，底层浏览器能力仍然复用。</p><p>BrowserOS 面向人，Agent UI 内置在浏览器中；BrowserOS neo 面向外部 Agent，核心入口是 MCP，并提供 Dashboard、并行 Session 和 Replay。当前代码分别位于 <a href="https://github.com/browseros-ai/BrowserOS/tree/5ddb0f2d04e15a2760f68729c217efaa2fd7437a/packages/browseros-agent/apps/claw-server-rust"><code>claw-server-rust</code></a> 和 <a href="https://github.com/browseros-ai/BrowserOS/tree/5ddb0f2d04e15a2760f68729c217efaa2fd7437a/packages/browseros-agent/apps/claw-app"><code>claw-app</code></a>。</p><p>这意味着“AI 浏览器”可以有两种产品角色：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">Browser for Humans</span><br><span class="line">浏览器是主产品，Agent 是内置能力</span><br><span class="line"></span><br><span class="line">Browser for Agents</span><br><span class="line">Agent 是调用者，浏览器是有状态的执行环境</span><br></pre></td></tr></table></figure><p>第二种形态很值得 Agent &#x2F; AI Infra 方向关注。它不要求每个 Agent Framework 都重新实现 Playwright 封装、登录态迁移、Tab 管理和页面观察，而是把浏览器沉淀成稳定的 Capability Server。</p><h2 id="什么情况下只做-Extension，什么情况下-fork-Chromium"><a href="#什么情况下只做-Extension，什么情况下-fork-Chromium" class="headerlink" title="什么情况下只做 Extension，什么情况下 fork Chromium"></a>什么情况下只做 Extension，什么情况下 fork Chromium</h2><p>最后回到一个务实问题：做 AI 浏览器，是不是都应该 fork Chromium？</p><p>我的判断是，不应该。</p><h3 id="优先做-Extension"><a href="#优先做-Extension" class="headerlink" title="优先做 Extension"></a>优先做 Extension</h3><p>如果产品主要是：</p><ul><li>总结、翻译和问答；</li><li>读取当前 Tab 内容；</li><li>在 Extension 权限范围内填表和导航；</li><li>快速验证产品价值；</li><li>可以接受跟随 Chrome 的产品入口和更新机制。</li></ul><p>那么 Extension + Local Server 通常是成本更低的选择。先验证用户是否真的需要 Agent 完成这些任务，再决定是否接管浏览器底座。</p><h3 id="考虑-fork-Chromium"><a href="#考虑-fork-Chromium" class="headerlink" title="考虑 fork Chromium"></a>考虑 fork Chromium</h3><p>当产品明确需要：</p><ul><li>浏览器级私有 API；</li><li>原生管理 Sidecar Runtime；</li><li>控制 New Tab、Importer、Profile、进程和产品身份；</li><li>内置并固定关键 Extension；</li><li>拥有安装包、签名、更新渠道和默认行为；</li><li>在 Chromium 上游之下维护长期产品差异。</li></ul><p>这时 fork 才有足够收益覆盖维护成本。</p><h3 id="重点建设-Browser-Capability-Layer"><a href="#重点建设-Browser-Capability-Layer" class="headerlink" title="重点建设 Browser Capability Layer"></a>重点建设 Browser Capability Layer</h3><p>不管最终选择 Extension、Chromium fork 还是独立 Browser Runtime，Browser Capability Layer 都值得尽早独立出来。</p><p>因为模型会换、UI 会改、MCP Client 会增加，但 Page、Tab、Frame、Navigation、Snapshot、Input 和 Side Effect 的语义需要长期稳定。只要这一层保持清晰，上层可以接内置 Agent，也可以接外部 Agent；下层可以继续使用 CDP，也可以逐步增加更原生的能力。</p><h2 id="我的判断"><a href="#我的判断" class="headerlink" title="我的判断"></a>我的判断</h2><p>拆完 BrowserOS 后，我对 AI 浏览器的判断变得更具体了。</p><p>聊天框决定用户从哪里开始，模型决定下一步想做什么，真正把任务落到网页上的，是 Browser Tool Plane；真正让产品能追上 Chromium 并交付给用户的，是 Delivery Control Plane。</p><p>所以 AI 浏览器的长期壁垒，很可能不在某个模型 API，也不在一个漂亮的 Side Panel，而在三件更慢的事情上：</p><ol><li>能否把复杂浏览器状态压缩成稳定、低 Token、可验证的 Tool Contract；</li><li>能否约束登录态、文件、页面操作和外部系统中的真实副作用；</li><li>能否持续完成 Chromium 升级、多平台签名、多产物兼容和安全更新。</li></ol><p>如果只想验证一个页面助手，Extension 已经足够；如果要做浏览器级产品，就要接受 Chromium fork 带来的长期成本；如果目标是让不同 Agent 稳定使用真实浏览环境，那么最值得先建设的，是独立于模型和 UI 的 Browser Runtime。</p><p>AI 浏览器看起来像一个应用，真正需要经营的却是两套系统：一套让 Agent 在浏览器里可靠工作，一套让这套能力持续、安全地到达用户机器。</p><h2 id="参考源码"><a href="#参考源码" class="headerlink" title="参考源码"></a>参考源码</h2><p>本文全部源码引用固定在 BrowserOS <code>5ddb0f2d0</code>：</p><ul><li><a href="https://github.com/browseros-ai/BrowserOS/blob/5ddb0f2d04e15a2760f68729c217efaa2fd7437a/README.md">BrowserOS monorepo architecture</a></li><li><a href="https://github.com/browseros-ai/BrowserOS/blob/5ddb0f2d04e15a2760f68729c217efaa2fd7437a/packages/browseros-agent/README.md">BrowserOS Agent architecture</a></li><li><a href="https://github.com/browseros-ai/BrowserOS/blob/5ddb0f2d04e15a2760f68729c217efaa2fd7437a/packages/browseros-agent/apps/app/wxt.config.ts">BrowserOS Extension configuration</a></li><li><a href="https://github.com/browseros-ai/BrowserOS/blob/5ddb0f2d04e15a2760f68729c217efaa2fd7437a/packages/browseros-agent/apps/app/lib/browseros/adapter.ts">BrowserOS native API adapter</a></li><li><a href="https://github.com/browseros-ai/BrowserOS/blob/5ddb0f2d04e15a2760f68729c217efaa2fd7437a/packages/browseros-agent/apps/server/src/api/server.ts">BrowserOS local Agent Server</a></li><li><a href="https://github.com/browseros-ai/BrowserOS/blob/5ddb0f2d04e15a2760f68729c217efaa2fd7437a/packages/browseros-agent/apps/server/src/agent/ai-sdk-agent.ts">BrowserOS Agent Loop</a></li><li><a href="https://github.com/browseros-ai/BrowserOS/tree/5ddb0f2d04e15a2760f68729c217efaa2fd7437a/packages/browseros-agent/packages/browser-core">Browser Core</a></li><li><a href="https://github.com/browseros-ai/BrowserOS/tree/5ddb0f2d04e15a2760f68729c217efaa2fd7437a/packages/browseros-agent/packages/browser-mcp">Browser MCP</a></li><li><a href="https://github.com/browseros-ai/BrowserOS/blob/5ddb0f2d04e15a2760f68729c217efaa2fd7437a/packages/browseros/bos_build/README.md">Browser build system</a></li><li><a href="https://github.com/browseros-ai/BrowserOS/blob/5ddb0f2d04e15a2760f68729c217efaa2fd7437a/packages/browseros/bos_build/docs/release-ci.md">Browser release CI</a></li></ul>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/af4fe7d7bf09/</id>
    <link href="https://blog.zisheng.pro/posts/af4fe7d7bf09/"/>
    <published>2026-08-09T18:43:34.000Z</published>
    <summary>AI 浏览器不只是 Chromium 加一个聊天界面，而是浏览器内核、原生集成、Agent Runtime、语义工具、模型连接与工程交付系统的组合。本文以 BrowserOS 源码为线索，拆解七层架构，并讲清构建、发布和多产物更新链路。</summary>
    <title>AI 浏览器不是 Chrome 加个聊天框：从 BrowserOS 拆解七层架构</title>
    <updated>2026-09-18T08:15:28.629Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="AI" scheme="https://blog.zisheng.pro/categories/AI/"/>
    <category term="AI" scheme="https://blog.zisheng.pro/tags/AI/"/>
    <category term="Agent" scheme="https://blog.zisheng.pro/tags/Agent/"/>
    <category term="Taste" scheme="https://blog.zisheng.pro/tags/Taste/"/>
    <category term="Vibe Coding" scheme="https://blog.zisheng.pro/tags/Vibe-Coding/"/>
    <category term="UI Design" scheme="https://blog.zisheng.pro/tags/UI-Design/"/>
    <category term="Design System" scheme="https://blog.zisheng.pro/tags/Design-System/"/>
    <category term="Owlet" scheme="https://blog.zisheng.pro/tags/Owlet/"/>
    <content>
      <![CDATA[<blockquote><p>当 AI 把实现速度拉到过去难以想象的水平，作品之间的差距就不再只取决于“能不能做出来”，还取决于你选择做什么、删掉什么，以及用什么标准宣布完成。</p></blockquote><p>过去一个月，我用 Coding Agent 跨进了不少原本陌生的领域。我用 Swift 做 macOS App，也独立完成 Agent 工作台、产品渲染、动效和技术文档。过去会被我拆成多个专业环节的事情，现在一个人就能先做出可运行、可比较的版本。</p><p>变化不只发生在速度上。当我让模型自由发挥时，它经常交回熟悉的答案：深色背景、蓝紫渐变、玻璃卡片，以及一句“输入你的需求，让 AI 帮你完成一切”。这些方案并不一定差，问题是它们太容易互换。换一个 Logo，产品仍然成立，也意味着它没有留下多少自己的判断。</p><p>实现成本下降后，另一个瓶颈开始变得清楚：<strong>Taste。</strong></p><p>我说的 Taste 不只是审美偏好。它是对目标、取舍、边界和完成标准的判断：什么问题值得做，哪个方案更适合用户，哪些细节必须坚持，哪些效果应该删掉，以及一个功能虽然能做，为什么仍然不该继续加。</p><p>标题借用了 <em>Attention Is All You Need</em> 的句式，“All” 是有意的夸张。业务理解、工程能力、领域知识和持续交付仍然不可替代。更准确的说法是：<strong>当 AI 可以快速提供大量可行实现时，Taste 决定这些实现最终会被组织成什么样的作品。</strong></p><h2 id="一句话总结"><a href="#一句话总结" class="headerlink" title="一句话总结"></a>一句话总结</h2><p>AI 没有让工程能力失去价值，它改变了瓶颈的位置：生成候选越来越便宜，定义问题、比较方案、拒绝噪声、验证结果和沉淀标准变得更重要；我把这组带责任的判断称为 Taste。</p><h2 id="AI-降低了“做一版”的门槛，也提高了“选一版”的要求"><a href="#AI-降低了“做一版”的门槛，也提高了“选一版”的要求" class="headerlink" title="AI 降低了“做一版”的门槛，也提高了“选一版”的要求"></a>AI 降低了“做一版”的门槛，也提高了“选一版”的要求</h2><p>传统软件开发里，执行成本本身就是一道筛选器。</p><p>一个想法要经过设计、开发、联调和测试，很多不够重要的需求会因为成本太高而自然消失。现在，这道筛选器正在变薄。在我的实际工作里，一个 Prompt 可以很快展开多套界面，Agent 可以连续修改几十个文件，过去需要较长排期才能比较的方向，可以先在一个晚上做成原型。</p><p>于是问题发生了变化：</p><table><thead><tr><th>实现成本较高时</th><th>候选大量出现后</th></tr></thead><tbody><tr><td>能不能实现？</td><td>值不值得实现？</td></tr><tr><td>能不能做出第一版？</td><td>哪一版更接近目标？</td></tr><tr><td>功能够不够？</td><td>哪些功能应该删除？</td></tr><tr><td>页面是否完整？</td><td>它有没有稳定的产品身份？</td></tr><tr><td>Demo 能否运行？</td><td>结果是否经得起长期使用？</td></tr></tbody></table><p>这里不存在一个严格的因果公式：使用 AI 不会自动让所有人拥有相同执行力，生成速度提高也不必然导致产品同质化。我的判断来自一个更具体的变化——<strong>当候选数量增加时，选择、验收和承担结果的成本不会一起消失。</strong></p><p>AI 可以把货架迅速摆满，但它不会替产品负责人决定什么应该上架。方向准确时，它能让模糊想法迅速长成产品；方向错误时，它也能以很高的完成度把错误做得更远。</p><p><img src="/images/taste-is-all-you-need-options.webp" alt="候选越多，选择越重要"></p><h2 id="Taste-是一套带责任的判断"><a href="#Taste-是一套带责任的判断" class="headerlink" title="Taste 是一套带责任的判断"></a>Taste 是一套带责任的判断</h2><p>我现在用五类选择理解 Taste：</p><table><thead><tr><th>判断</th><th>要回答的问题</th></tr></thead><tbody><tr><td>问题选择</td><td>这是一个真实问题，还是一个热闹概念？</td></tr><tr><td>目标与层级</td><td>用户第一眼应该看见什么，完成什么？</td></tr><tr><td>取舍</td><td>哪些能力、装饰和表达应该被删除？</td></tr><tr><td>边界</td><td>稳定性、性能、可访问性和业务约束在哪里？</td></tr><tr><td>完成标准</td><td>什么证据足以证明它可以交付，什么仍然只是 Demo？</td></tr></tbody></table><p>这也是为什么 Taste 不能只用“好不好看”衡量。一个漂亮但不稳定的窗口、一个功能很多却让用户失去控制的 Agent、一个氛围很强但读不清正文的页面，都可能有审美，却缺少完整的产品判断。</p><p>Taste 也不只体现在加了什么。那些没有进入最终版本的颜色、按钮、动画和功能，往往更能说明一个产品知道自己是谁。</p><p>我做 Owlet 和专业 Agent 工作台时，对这一点感受很深。</p><h2 id="Owlet：AI-帮我跨进陌生领域，产品仍然需要我拍板"><a href="#Owlet：AI-帮我跨进陌生领域，产品仍然需要我拍板" class="headerlink" title="Owlet：AI 帮我跨进陌生领域，产品仍然需要我拍板"></a>Owlet：AI 帮我跨进陌生领域，产品仍然需要我拍板</h2><p><img src="/images/owlet-vibe-coding-cover.webp" alt="Owlet 主视觉"></p><p>Owlet 是我用 Claude Code 和 Codex 做的一款 macOS menu-bar 应用。它来自一个很私人的问题：我每天大量使用 Coding Agent，到底消耗了多少 token，哪些项目最活跃？</p><p>我是前端工程师，并不熟悉 SwiftUI、AppKit、<code>NSPanel</code>、Sparkle 和 macOS 签名发布。按过去的学习路径，我可能会先系统学习 Swift，再从 Hello World 慢慢摸到 menu bar。Coding Agent 把这条路径压缩了：我可以直接从真实需求出发，在实现中补齐陌生领域知识。</p><p>AI 解决了大量“怎么做”的问题，下面这些决定仍然需要我回答：</p><ul><li>为什么常驻菜单栏，而不是再做一个 Web dashboard？</li><li>为什么叫 Owlet，为什么是一只会眨眼的小猫头鹰？</li><li>为什么功能围绕使用量、会话状态和学习展开，而不是堆成 Agent 百宝箱？</li><li>为什么界面要像原生 macOS 工具，却不能失去自己的气质？</li><li>当系统材质带来崩溃风险时，是继续追求效果，还是换一种安全实现保留视觉语义？</li></ul><p>模型可以列出候选，却不知道哪一个更像“我愿意长期放在自己菜单栏里的东西”。</p><p>Owlet 早期也出现过许多容易生成的方案：更强的光效、更满的卡片、更通用的科技蓝、更醒目的金色。单独看都说得过去，组合起来却越来越像一个没有身份的 AI dashboard。</p><p>让它定下来的，是一句和代码无关的话：</p><blockquote><p>你知道临安青山湖水上森林吗？产品配色和调性从这个景区的主要元素蒸馏。</p></blockquote><p>这句话没有指定 hex，也没有规定某个组件怎么写。它给了产品一个母题，让后续的局部选择拥有共同来源。</p><p><img src="/images/owlet-qingshan-lake-palette.webp" alt="Owlet 的青山湖配色体系"></p><p>湖面变成主操作的湖水青，水杉变成成功和运行状态，远山承担次级信息，雾白负责空间和层次，晨光金只留在极少量高光里。母题也同时定义了禁区：不做赛博霓虹，不让金色主导 surface，不用重阴影堆层级，不为了“可爱”牺牲信息效率。</p><p>后来为了让灵动卡接近系统组件的玻璃质感，我们尝试过 macOS material 和窗口采样，特定组合会带来稳定性风险。最后我没有放弃“雾”，也没有为了视觉效果继续赌系统边界，而是改用纯 SwiftUI 绘制柔焦层：保留雾白的视觉职责，换掉高风险实现。</p><p>这也是 Taste：知道哪个体验必须保留，哪个实现可以妥协，并让审美服从稳定性、性能和可读性。</p><h2 id="青山湖：一套风格要先学会删"><a href="#青山湖：一套风格要先学会删" class="headerlink" title="青山湖：一套风格要先学会删"></a>青山湖：一套风格要先学会删</h2><p><img src="/images/qingshan-lake-design-birth.webp" alt="青山湖设计风格"></p><p>青山湖风格是在一次次“不对”里长出来的，色板只是后来沉淀出的结果。</p><p>Agent 很容易把“自然感”理解成多放几种自然色，把“现代感”理解成更多渐变和胶囊，把“氛围感”理解成让装饰铺满页面。真实界面跑起来后，我反复给出的反馈却是：</p><ul><li>颜色太花，只保留一个主导色；</li><li>胶囊按钮突兀，不要为了风格强行统一形状；</li><li>云雾只负责环境，不能侵入阅读区；</li><li>卡片之间优先用间距和层级组织，不要继续套卡片；</li><li>晨光金只能是稀缺高光，不能变成主题色；</li><li>动画只解释状态，不负责表演。</li></ul><p>这段过程让我确认：<strong>设计风格首先是一套删减规则。</strong></p><p>如果只告诉 AI“可以用什么”，它会很积极地把所有特征一起用上。输出真正开始稳定，是因为规则同时写清了“最多用多少”“只能用在哪里”“什么情况下不要用”。</p><p>青山湖于是从一句审美偏好，变成一套可执行约束：颜色有 semantic roles，圆角有范围，间距有节奏，动效有性能与可访问性边界，组件有 anti-pattern。它被写进 <code>DESIGN.md</code>，又被收录进 <a href="https://design.zisheng.pro/">Design Language Atlas</a>。</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><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><p>Taste 走到最后一层，才不再依赖某一次聊天的运气，而成为下一次 Agent 仍能执行的工程资产。</p><h2 id="专业-Agent-工作台：同一种-Taste，不等于复制同一个界面"><a href="#专业-Agent-工作台：同一种-Taste，不等于复制同一个界面" class="headerlink" title="专业 Agent 工作台：同一种 Taste，不等于复制同一个界面"></a>专业 Agent 工作台：同一种 Taste，不等于复制同一个界面</h2><p>青山湖后来从 Owlet 迁移到了另一个专业 Agent 工作台。两个产品的技术栈和使用场景完全不同：Owlet 是个人使用的 macOS 工具，后者则面向复杂任务、页面探索和技能复用。</p><p>把两边都染成青绿色没有迁移价值。跨端复用的是同一套判断：低噪声画布、清晰的信息层级、克制的状态色、不过度装饰的 glass surface，以及让内容始终压过背景的优先级。</p><p>到了这个工作台，这套判断必须适应专业产品的密度。聊天入口可以有缓慢移动的云雾，进入任务和报告后，装饰必须后退。正文、筛选器、状态、日志和结果证据要更紧凑；loading、empty、error 和 success 继续使用同一套自然语义，但不能因为“青山湖”降低扫描效率。</p><p>这个工作台的产品路径也体现了另一种 Taste：不把 Agent 的全部能力堆到用户面前，而是把复杂执行收束成可理解、可确认、可验收的过程。</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">用户提出目标</span><br><span class="line">  → Agent 理解意图并匹配项目与技能</span><br><span class="line">  → 用户确认范围和边界</span><br><span class="line">  → 多个执行单元并行工作并保留证据</span><br><span class="line">  → 报告交付结果、优先级与行动建议</span><br></pre></td></tr></table></figure><p>范围确认没有“一句话直接开跑”那么酷，却让用户在消耗执行资源前看清边界；报告也不只展示 Agent 跑了多少步骤，而是把结果整理成业务能够使用的行动建议。</p><p>这让我对 Taste 有了更完整的理解：它还决定产品把复杂度放在哪里。好的产品不会假装复杂度不存在，而是把它安排在用户能够理解、控制和验收的位置。</p><h2 id="从直觉到-Skill：让下一次-Agent-继续做对"><a href="#从直觉到-Skill：让下一次-Agent-继续做对" class="headerlink" title="从直觉到 Skill：让下一次 Agent 继续做对"></a>从直觉到 Skill：让下一次 Agent 继续做对</h2><p>只在一个产品、一个页面或一次会话里成立的偏好，还不是可复用能力。</p><p><img src="/images/taste-is-all-you-need-skill.webp" alt="从景观母题到跨端设计系统"></p><p>我后来把 Owlet、专业 Agent 工作台、Design Language Atlas 和相关 Landing Page 里的实践重新扫描了一遍，抽取出公开的 <a href="https://github.com/zisheng-ai/qingshan-lake-design">Qingshan Lake Design Skill</a>。它覆盖 macOS、iPhone&#x2F;iPad、Android、React Native、Web dashboard 和 Landing Page。</p><p>这次抽取没有把同一套界面复制到六个平台。共享的是一套 semantic vocabulary：canvas、surface、text、primary action、focus、success、warning、error，以及明确的 anti-pattern；各平台仍然遵守自己的交互模型和验收方式：</p><table><thead><tr><th>平台</th><th>青山湖风格必须服从的系统边界</th></tr></thead><tbody><tr><td>macOS</td><td>窗口缩放、键盘与鼠标、原生 material、Reduced Transparency</td></tr><tr><td>iPhone&#x2F;iPad</td><td>Safe Area、Dynamic Type、触控目标和系统导航</td></tr><tr><td>Android</td><td>Material 3 roles、48dp target、edge-to-edge、tablet 与 foldable</td></tr><tr><td>React Native</td><td>共用 typed theme，但保留 VoiceOver、TalkBack 和双端行为差异</td></tr><tr><td>Web dashboard</td><td>中高信息密度、键盘 focus、响应式与可读 surface</td></tr><tr><td>Landing Page</td><td>叙事层级、LCP、响应式媒体和渐进式氛围</td></tr></tbody></table><p>Qingshan Lake Design Skill 里除了 token，还有布局顺序、材质策略、状态合同、Reduced Motion、对比度检查和禁区。它不证明 Taste 可以被 AI 自动拥有；它证明的是另一件更务实的事：<strong>人的判断可以被外化成 Agent 能读取、实现和验收的约束。</strong></p><h3 id="这不是三个项目，而是一条设计生产链"><a href="#这不是三个项目，而是一条设计生产链" class="headerlink" title="这不是三个项目，而是一条设计生产链"></a>这不是三个项目，而是一条设计生产链</h3><p>如果只讲 Qingshan Lake Design Skill，这套方法仍然少了两端：判断从哪里来，以及视觉资产怎样稳定生产出来。</p><table><thead><tr><th>环节</th><th>项目</th><th>解决的问题</th></tr></thead><tbody><tr><td>Reference</td><td><a href="https://design.zisheng.pro/">Design Language Atlas</a></td><td>建立可搜索、可比较的设计语言与真实界面参考，回答“有哪些可能，为什么这个方向适合当前产品”</td></tr><tr><td>Constraint</td><td><a href="https://github.com/zisheng-ai/qingshan-lake-design">Qingshan Lake Design Skill</a></td><td>把景观母题蒸馏成 semantic tokens、组件规则、跨端适配和 anti-pattern，回答“怎样持续做对”</td></tr><tr><td>Production</td><td><a href="https://github.com/zisheng-ai/better-imagegen">better-imagegen</a></td><td>按资产类型选择模型与工作流，完成 prompt 约束、模型级联、后处理、metadata 和 QA，回答“怎样稳定交付可用资产”</td></tr></tbody></table><p>Design Language Atlas 不是换皮网站。除了设计语言索引和可运行 Demo，它还在继续补 UI Reference、Design Systems、组件、模板和社区 UI Skills。它的价值不是替我选风格，而是把零散收藏变成可以搜索、对照和拆解的 Reference 系统。</p><p>better-imagegen 也不只是给图片 API 包一层调用。封面、插图、Logo、壁纸、sprite loop 和 Codex pet 对尺寸、透明度、文字准确性、压缩与验收的要求完全不同，因此需要不同的生产路径。比如本文封面先由图片模型生成无文字的青山湖氛围底图，再用 HTML&#x2F;CSS 确定性排版标题，最后转成 WebP 并记录生成 metadata。模型负责打开视觉可能性，工程流程负责保证文字、尺寸和交付结果没有靠运气。</p><p>这三层刚好对应 Taste 的三个动作：<strong>先扩大看过的世界，再把选择写成约束，最后把约束变成稳定产物。</strong> 对我来说，这同样是设计，而不是设计完成之后的辅助工作。</p><p>这也是整篇文章最关键的闭环。Taste 如果只能由作者本人“看一眼就知道”，它仍然难以规模化；当它能解释为什么、约束在哪里、怎样验证，才会从个人直觉变成团队和 Agent 可以复用的生产资料。</p><h2 id="AI-能不能帮助人提升-Taste？"><a href="#AI-能不能帮助人提升-Taste？" class="headerlink" title="AI 能不能帮助人提升 Taste？"></a>AI 能不能帮助人提升 Taste？</h2><p>可以，但不要把最终选择也外包给模型。</p><p>AI 对 Taste 最有价值的地方，是降低探索和对照成本。过去做视觉研究，需要自己收集案例、拆组件、试实现；现在可以让 Agent 快速生成不同方向、分析差异、把参考转换成 token，再放回真实界面比较。</p><p>我现在使用这样一条循环：</p><p><img src="/images/taste-is-all-you-need-loop.svg" alt="Taste 从参考、发散、选择、约束、验证到记忆的循环"></p><h3 id="Reference：先扩大看过的世界"><a href="#Reference：先扩大看过的世界" class="headerlink" title="Reference：先扩大看过的世界"></a>Reference：先扩大看过的世界</h3><p>Taste 不会从空白 Prompt 里凭空出现。产品、建筑、电影、自然景观和真实生活，共同构成判断的坐标系。</p><p>我做 <a href="https://design.zisheng.pro/">Design Language Atlas</a>，目的不是让 Agent 随机套一套皮肤，而是建立比较能力：同一个 dashboard 为什么适合青山湖、不适合 Cyberpunk；同样是 glass，原生系统 material 和业务后台的半透明 surface 分别承担什么职责；哪些产品需要情绪，哪些产品更需要秩序。</p><h3 id="Diverge：让-AI-打开有意义的可能性"><a href="#Diverge：让-AI-打开有意义的可能性" class="headerlink" title="Diverge：让 AI 打开有意义的可能性"></a>Diverge：让 AI 打开有意义的可能性</h3><p>AI 很适合快速生成候选。这个阶段先比较方向：产品更安静还是更活跃，信息更集中还是更舒展，品牌母题来自自然、建筑还是角色。</p><p>发散的价值是暴露选择，不是留下更多图片。</p><h3 id="Select：把“不对”说清楚"><a href="#Select：把“不对”说清楚" class="headerlink" title="Select：把“不对”说清楚"></a>Select：把“不对”说清楚</h3><p>“不好看”“不高级”无法形成长期能力。更有效的反馈是：主操作不突出、背景侵入阅读区、圆角让专业工具显得玩具化、动效节奏抢走注意力。</p><p>当我能说清“不对在哪里”，直觉才开始变成可以讨论和复现的判断。</p><h3 id="Constrain：把偏好写成规则"><a href="#Constrain：把偏好写成规则" class="headerlink" title="Constrain：把偏好写成规则"></a>Constrain：把偏好写成规则</h3><p>颜色要有语义，间距要有范围，组件要有职责，动效要有性能和可访问性边界，还要明确 anti-pattern。与其写“保持高级感”，不如写“湖水青是唯一主操作色；浮萍绿控制在极小面积；不要卡片嵌套卡片”。</p><p>清晰约束不会限制 Agent，反而能让它把算力用于更有价值的探索。</p><h3 id="Verify：回到真实产品"><a href="#Verify：回到真实产品" class="headerlink" title="Verify：回到真实产品"></a>Verify：回到真实产品</h3><p>设计稿里成立的东西，放到菜单栏的真实尺寸、数据密集工作台、不同尺寸屏幕和长时间动画里，可能完全是另一回事。</p><p>Owlet 的图标要在菜单栏真实尺寸验收，专业 Agent 工作台的云雾要连续观看仍不打扰阅读，玻璃组件要检查文字对比度，动效要尊重 Reduced Motion。截图只能证明一个瞬间，Taste 要经得起持续使用。</p><h3 id="Remember：记住拒绝过什么"><a href="#Remember：记住拒绝过什么" class="headerlink" title="Remember：记住拒绝过什么"></a>Remember：记住拒绝过什么</h3><p>只保存“最终用了什么”还不够。很多一致性来自对失败经验的记忆：为什么不再用大面积金色，为什么不恢复高风险系统材质，为什么数据页面不铺云雾，为什么不做纯黑科技风。</p><p>把这些决定写进 <code>DESIGN.md</code>、Skill 和验收清单，下一次 Agent 才不会重新走一遍老路。</p><h2 id="人和-AI-的新分工"><a href="#人和-AI-的新分工" class="headerlink" title="人和 AI 的新分工"></a>人和 AI 的新分工</h2><p>回看 Owlet、专业 Agent 工作台和青山湖风格的形成过程，我更愿意这样划分责任：</p><table><thead><tr><th>人承担最终责任</th><th>AI 放大执行能力</th></tr></thead><tbody><tr><td>选择问题与方向</td><td>搜索、生成和展开候选</td></tr><tr><td>定义价值、层级和完成标准</td><td>把规则翻译成代码与资产</td></tr><tr><td>决定取舍、边界和例外</td><td>快速试错并执行重复检查</td></tr><tr><td>验收真实体验</td><td>整理状态、差异与实现证据</td></tr><tr><td>沉淀长期判断</td><td>在既定约束内持续生产</td></tr></tbody></table><p>这不是“人负责想、AI 负责做”的简单二分。AI 也能分析、提出方案和发现问题，人也必须理解工程。真正不能模糊的是最终责任：模型给出十个版本之后，必须有人决定哪一个值得留下；Agent 把功能跑通之后，必须有人判断它是否解决了问题。</p><p>AI 时代的超级个体，不需要亲手包办所有专业角色。他需要一套足够清晰的判断框架，再让 Agent 把执行力放大。</p><h2 id="我的判断框架：Taste-什么时候真正有价值"><a href="#我的判断框架：Taste-什么时候真正有价值" class="headerlink" title="我的判断框架：Taste 什么时候真正有价值"></a>我的判断框架：Taste 什么时候真正有价值</h2><ul><li><strong>值得投入：</strong> 当 AI 已经能快速产出候选，而产品仍然缺少方向、辨识度或稳定完成标准时，优先补 Reference、选择规则、anti-pattern 和验收方式。</li><li><strong>不能替代：</strong> 涉及安全、合规、数据正确性和复杂系统边界时，Taste 不能替代专业知识、工程验证和责任机制。好判断必须建立在真实约束上。</li><li><strong>重点关注：</strong> 不要只记录最终选择。把删掉什么、为什么删、什么证据会推翻当前决定一起沉淀，下一次协作才不会从零开始。</li></ul><p>“Taste Is All You Need” 不是说有品味就不需要技术。只有理解技术边界，Taste 才不会停在漂亮图片；只有理解业务，Taste 才能从视觉偏好进入产品取舍。</p><p>发生变化的是稀缺性的顺序。</p><p>代码会更容易生成，界面会更容易完成，内容会更容易填满。更难复制的，是一个人长期形成的参考系、对细节的敏感、拒绝平庸方案的勇气，以及把一次直觉变成可复用标准的能力。</p><p>AI 会让更多人拥有更强的执行力，却不会自动替任何人承担选择的责任。</p><p>当“做出来”不再是唯一瓶颈，<strong>选择做什么、删掉什么、坚持什么，就是作品本身。</strong></p><blockquote><p>本文由 <a href="https://github.com/zisheng-ai/writting-skill">writting-skill</a> 辅助生成，文章观点、项目事实与最终判断由作者确认。</p></blockquote><hr><h2 id="延伸阅读与项目"><a href="#延伸阅读与项目" class="headerlink" title="延伸阅读与项目"></a>延伸阅读与项目</h2><ul><li><a href="https://github.com/zisheng-ai/qingshan-lake-design">Qingshan Lake Design Skill</a></li><li><a href="https://github.com/zisheng-ai/better-imagegen">better-imagegen：通用多模型 AI 生图 Skill</a></li><li><a href="https://blog.zisheng.pro/posts/8a33f17a26ad/">我用 Claude Code + Codex vibe 了一个 macOS App：Owlet</a></li><li><a href="https://blog.zisheng.pro/posts/8b52f4a0fd65/">青山湖设计风格的诞生记</a></li><li><a href="https://blog.zisheng.pro/posts/5633e84c7f72/">让云雾动起来：聊天界面的分层氛围动画</a></li><li><a href="https://design.zisheng.pro/">Design Language Atlas</a></li></ul>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/dd15888a88dd/</id>
    <link href="https://blog.zisheng.pro/posts/dd15888a88dd/"/>
    <published>2026-08-09T16:18:00.000Z</published>
    <summary>当 AI 把实现和生成的成本迅速压低，真正稀缺的能力开始转向问题选择、方案取舍、边界判断与结果验收。本文结合 Owlet、青山湖设计语言和跨端 Design Skill，讨论 Taste 为什么会成为 AI 时代的核心生产力，以及怎样把个人直觉沉淀为 Agent 可以执行和验证的规则。</summary>
    <title>Taste Is All You Need</title>
    <updated>2026-09-18T08:15:28.632Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="AI Infra" scheme="https://blog.zisheng.pro/categories/AI-Infra/"/>
    <category term="AI Infra" scheme="https://blog.zisheng.pro/tags/AI-Infra/"/>
    <category term="Agent" scheme="https://blog.zisheng.pro/tags/Agent/"/>
    <category term="Evidence" scheme="https://blog.zisheng.pro/tags/Evidence/"/>
    <category term="Claim" scheme="https://blog.zisheng.pro/tags/Claim/"/>
    <category term="Verification" scheme="https://blog.zisheng.pro/tags/Verification/"/>
    <category term="Provenance" scheme="https://blog.zisheng.pro/tags/Provenance/"/>
    <category term="Evaluation" scheme="https://blog.zisheng.pro/tags/Evaluation/"/>
    <content>
      <![CDATA[<img src="https://blog.zisheng.pro/images/evidence-verification-chain.webp" data-lazy-src="https://blog.zisheng.pro/images/evidence-verification-chain.webp" alt="Evidence 在 Agent 系统中的验证链路：Source、Evidence、Claim、Evaluation 与 Verification"><p><em>Evidence 不负责让 Agent 看起来更可信。它负责把“相信”变成一件可以复查、反驳和重新计算的事。</em></p><p>最近我反复看到 <code>Evidence</code>。它出现在 LLM Wiki、RAG、Agent 执行、评测、审计和业务验收中。有的产品把检索片段叫 Evidence，有的把截图和日志叫 Evidence，还有的把一整条 Trace 都塞进 Evidence Store。</p><p>这些做法都碰到了 Evidence 的一部分，但没有回答最核心的问题：<strong>Evidence 到底在证明什么？</strong></p><p>上一篇<a href="https://blog.zisheng.pro/posts/ef5159a9b8f5/">《Agent 系统的一级领域对象：从 Task、Run 到 Evidence》</a>把 Evidence 放进了 Agent 的一级领域模型。这一篇只拆 Evidence。我想讲清它为什么属于 Trust &amp; Verification、为什么不能和 Source、Citation、Trace、Artifact、Proof、Evaluation 混在一起，以及进入生产后应该怎样建模。</p><h2 id="一句话总结"><a href="#一句话总结" class="headerlink" title="一句话总结"></a>一句话总结</h2><p>我对 Evidence 的定义是：</p><blockquote><p><strong>Evidence 是带有来源、定位、时间、范围和完整性信息，并明确支持或反驳某个 Claim 的可验证事实记录。</strong></p></blockquote><p>它处在这样一条链路中：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">Source / Observation</span><br><span class="line">        ↓ 捕获与固化</span><br><span class="line">Evidence ── supports / refutes ──&gt; Claim</span><br><span class="line">        ↓                               ↓</span><br><span class="line">   Provenance                    Evaluation Policy</span><br><span class="line">        └──────────────&gt; Verification Result</span><br></pre></td></tr></table></figure><p>这条定义的锚点是“某个 Claim”。单独存在的事实，还没有获得证明方向。</p><p><strong>没有 Claim 的 Evidence，只是 Source、Record 或 Observation。</strong></p><h2 id="Evidence-属于什么范畴"><a href="#Evidence-属于什么范畴" class="headerlink" title="Evidence 属于什么范畴"></a>Evidence 属于什么范畴</h2><p>如果按 Agent 系统的领域边界划分，Evidence 属于：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Trust &amp; Verification</span><br></pre></td></tr></table></figure><p>它连接三个世界：</p><table><thead><tr><th>世界</th><th>核心问题</th><th>对象</th></tr></thead><tbody><tr><td>Execution</td><td>Agent 做了什么</td><td>Run、Tool Call、Trace</td></tr><tr><td>Outcome</td><td>Agent 交付或声称了什么</td><td>Artifact、Claim</td></tr><tr><td>Trust</td><td>我们凭什么相信它</td><td>Evidence、Evaluation、Verification Result</td></tr></tbody></table><p>Evidence 不属于 Knowledge Plane。Knowledge、Memory 和 Context 解决“Agent 能看到什么、记住什么”；Evidence 解决“系统凭什么接受 Agent 的某条结论”。</p><p>它也不属于 Observability。Trace 和 Log 记录过程，Evidence 从过程记录、交付物或权威系统中挑出与 Claim 有关的最小事实集合。</p><h2 id="一份材料怎样才成为-Evidence"><a href="#一份材料怎样才成为-Evidence" class="headerlink" title="一份材料怎样才成为 Evidence"></a>一份材料怎样才成为 Evidence</h2><img src="https://blog.zisheng.pro/images/evidence-qualification-gates.webp" data-lazy-src="https://blog.zisheng.pro/images/evidence-qualification-gates.webp" alt="一份原始材料通过 Claim、Provenance、Scope 与 Integrity 四道门成为 Evidence"><p>一份材料不会因为被 Agent 检索到、截图了或者写进数据库，就自动成为 Evidence。它至少要通过四道门。</p><h3 id="1-Claim-Binding：它在证明什么"><a href="#1-Claim-Binding：它在证明什么" class="headerlink" title="1. Claim Binding：它在证明什么"></a>1. Claim Binding：它在证明什么</h3><p>Evidence 必须绑定具体 Claim，并声明方向：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">supports</span><br><span class="line">refutes</span><br></pre></td></tr></table></figure><p>例如一次 HTTP 200：</p><ul><li>可以支持“这个 URL 在 02:10 可访问”；</li><li>不能单独支持“新版本已经发布”；</li><li>更不能支持“新功能符合业务预期”。</li></ul><p>同一条事实对不同 Claim 的证明力完全不同。Evidence 的证明力始终<strong>相对于 Claim</strong>而存在。</p><h3 id="2-Provenance：它从哪里来"><a href="#2-Provenance：它从哪里来" class="headerlink" title="2. Provenance：它从哪里来"></a>2. Provenance：它从哪里来</h3><p>Evidence 必须能够回到产生它的主体、活动和原始实体。</p><p><a href="https://www.w3.org/TR/prov-o/">W3C PROV-O</a>用 <code>Agent</code>、<code>Activity</code>、<code>Entity</code> 作为 Provenance 的起点：谁承担责任、什么活动发生过、活动使用或生成了什么实体。对 Agent 系统而言，至少要保留：</p><ul><li>Source System；</li><li>Source Object 或 URI；</li><li>Collector Identity；</li><li>Run ID；</li><li>Capture Method；</li><li>Observed At；</li><li>Content Digest。</li></ul><p>只有 URL 通常不够。URL 指向的内容可能变化，权限也可能变化。必要时要保存 Snapshot、精确 Locator 和 Hash。</p><h3 id="3-Scope-Time：它在什么范围内有效"><a href="#3-Scope-Time：它在什么范围内有效" class="headerlink" title="3. Scope &amp; Time：它在什么范围内有效"></a>3. Scope &amp; Time：它在什么范围内有效</h3><p>Evidence 必须说明：</p><ul><li>针对哪个环境；</li><li>哪个版本或 Revision；</li><li>哪个账号、租户或业务对象；</li><li>哪个时间点；</li><li>哪些前置条件；</li><li>什么时候过期。</li></ul><p>测试环境通过不等于生产环境通过；昨天的库存状态不能证明今天仍有货；管理员账号看到的页面不能证明普通用户也能看到。</p><h3 id="4-Integrity：它有没有被替换或篡改"><a href="#4-Integrity：它有没有被替换或篡改" class="headerlink" title="4. Integrity：它有没有被替换或篡改"></a>4. Integrity：它有没有被替换或篡改</h3><p>Integrity 解决“我现在看到的，还是不是当时采集的那份材料”。常见手段包括：</p><ul><li>内容 Hash；</li><li>不可变 Artifact ID；</li><li>数字签名；</li><li>时间戳；</li><li>Append-only Ledger；</li><li>受控的 Source of Truth。</li></ul><p>这里要特别区分 Evidence 和 Proof。<a href="https://www.w3.org/TR/vc-data-model-2.0/">W3C Verifiable Credentials Data Model 2.0</a>明确指出，Evidence 是支持 Credential 中 Claim 的材料；Securing Mechanism 用来验证签发者身份和 Credential 完整性。Credential 能被密码学验证，并不自动意味着其中 Claim 为真。</p><p>换到 Agent 领域同样成立：<strong>签名可以证明“这条记录由谁签发、有没有被改”，不能替你判断业务结论是否正确。</strong></p><h2 id="Evidence-对象内部应该有什么"><a href="#Evidence-对象内部应该有什么" class="headerlink" title="Evidence 对象内部应该有什么"></a>Evidence 对象内部应该有什么</h2><img src="https://blog.zisheng.pro/images/evidence-object-anatomy.webp" data-lazy-src="https://blog.zisheng.pro/images/evidence-object-anatomy.webp" alt="Evidence 对象的数据结构：Claim、Source、Locator、Scope、Time、Integrity 与 Provenance"><p>一个可复用的 Evidence 对象，可以先从下面这组字段开始：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> <span class="title class_">Evidence</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">claimId</span>: <span class="built_in">string</span>;</span><br><span class="line"></span><br><span class="line">  <span class="attr">type</span>: <span class="string">&#x27;document_span&#x27;</span> | <span class="string">&#x27;system_state&#x27;</span> | <span class="string">&#x27;test_result&#x27;</span></span><br><span class="line">    | <span class="string">&#x27;screenshot&#x27;</span> | <span class="string">&#x27;artifact_digest&#x27;</span> | <span class="string">&#x27;attestation&#x27;</span>;</span><br><span class="line">  <span class="attr">polarity</span>: <span class="string">&#x27;supports&#x27;</span> | <span class="string">&#x27;refutes&#x27;</span>;</span><br><span class="line"></span><br><span class="line">  <span class="attr">sourceRef</span>: <span class="built_in">string</span>;</span><br><span class="line">  locator?: <span class="built_in">string</span>;</span><br><span class="line">  snapshotRef?: <span class="built_in">string</span>;</span><br><span class="line">  contentDigest?: <span class="built_in">string</span>;</span><br><span class="line"></span><br><span class="line">  <span class="attr">scope</span>: &#123;</span><br><span class="line">    environment?: <span class="built_in">string</span>;</span><br><span class="line">    revision?: <span class="built_in">string</span>;</span><br><span class="line">    subject?: <span class="built_in">string</span>;</span><br><span class="line">    tenant?: <span class="built_in">string</span>;</span><br><span class="line">  &#125;;</span><br><span class="line"></span><br><span class="line">  <span class="attr">observedAt</span>: <span class="built_in">string</span>;</span><br><span class="line">  validAt?: <span class="built_in">string</span>;</span><br><span class="line">  expiresAt?: <span class="built_in">string</span>;</span><br><span class="line"></span><br><span class="line">  <span class="attr">collectedBy</span>: <span class="built_in">string</span>;</span><br><span class="line">  runId?: <span class="built_in">string</span>;</span><br><span class="line">  <span class="attr">captureMethod</span>: <span class="built_in">string</span>;</span><br><span class="line">  <span class="attr">integrityStatus</span>: <span class="string">&#x27;unknown&#x27;</span> | <span class="string">&#x27;verified&#x27;</span> | <span class="string">&#x27;failed&#x27;</span>;</span><br><span class="line">  <span class="attr">status</span>: <span class="string">&#x27;candidate&#x27;</span> | <span class="string">&#x27;validated&#x27;</span> | <span class="string">&#x27;rejected&#x27;</span></span><br><span class="line">    | <span class="string">&#x27;superseded&#x27;</span> | <span class="string">&#x27;expired&#x27;</span>;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>下面是一套最小领域结构，综合了几类成熟模型的共同思想：</p><ul><li>W3C PROV 的 Agent、Activity、Entity 与派生关系；</li><li><a href="https://github.com/in-toto/attestation/blob/main/spec/v1/statement.md">in-toto Statement</a>用 Subject Digest、Predicate Type 和 Predicate 把陈述绑定到不可变对象；</li><li><a href="https://slsa.dev/spec/v1.2/">SLSA 1.2</a>用 Provenance Attestation 描述软件产物如何生成，再由消费者执行验证策略；</li><li>Verifiable Credentials 用 Issuer、Subject、Validity、Status、Evidence 和 Securing Mechanism 组织可验证声明。</li></ul><p>我不会直接照搬其中任何一个 Schema，因为 Agent 的 Evidence 不只服务软件供应链或 Credential。真正值得复用的是它们的共同原则：<strong>先固定 Subject 与 Claim，再保存 Predicate、Provenance、Scope 和 Integrity。</strong></p><h2 id="Evidence-和相近概念的边界"><a href="#Evidence-和相近概念的边界" class="headerlink" title="Evidence 和相近概念的边界"></a>Evidence 和相近概念的边界</h2><img src="https://blog.zisheng.pro/images/evidence-boundary-map.webp" data-lazy-src="https://blog.zisheng.pro/images/evidence-boundary-map.webp" alt="Source、Citation、Context、Trace、Artifact、Evidence、Proof 与 Evaluation 的边界"><h3 id="Source-不是-Evidence"><a href="#Source-不是-Evidence" class="headerlink" title="Source 不是 Evidence"></a>Source 不是 Evidence</h3><p>Source 表达信息来自哪里，Evidence 表达 Source 中哪部分事实支撑哪个 Claim。</p><p>一份 200 页政策 PDF 是 Source。第 27 页表格中的某个单元格，加上版本、生效日期和定位信息，才可能成为某条 Claim 的 Evidence。</p><h3 id="Citation-不是-Evidence"><a href="#Citation-不是-Evidence" class="headerlink" title="Citation 不是 Evidence"></a>Citation 不是 Evidence</h3><p>Citation 是 Claim 指向 Source 或 Evidence 的定位关系。它解决“去哪里看”，不自动解决“这段内容是否真的支撑结论”。</p><p>只有链接、没有 Snapshot 和 Locator 的 Citation，还会受到内容漂移影响。</p><h3 id="Context-不是-Evidence"><a href="#Context-不是-Evidence" class="headerlink" title="Context 不是 Evidence"></a>Context 不是 Evidence</h3><p>Context 是本次 Run 获准看到的信息边界。材料进入 Context，只能说明 Agent 看过它，不能说明它正确、相关或足以支撑结论。</p><h3 id="Trace-不是-Evidence"><a href="#Trace-不是-Evidence" class="headerlink" title="Trace 不是 Evidence"></a>Trace 不是 Evidence</h3><p><a href="https://opentelemetry.io/docs/specs/otel/trace/api/">OpenTelemetry</a>把 Trace 组织为由 Span 构成的操作链；<a href="https://openai.github.io/openai-agents-python/tracing/">OpenAI Agents SDK Tracing</a>会记录 LLM Generation、Tool Call、Handoff、Guardrail 和自定义事件。这些内容非常适合调试、监控和复盘。</p><p>但 Trace 回答的是“系统记录到什么过程”，不是“业务结果是否成立”。</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">Trace: Agent 调用了 deploy()，接口返回 accepted</span><br><span class="line">Claim: 新版本已经在生产环境生效</span><br></pre></td></tr></table></figure><p>前者不能直接证明后者。只有部署平台状态、线上版本标识和用户可见结果，才能形成后者的 Evidence Bundle。</p><p>Trace 当然可以成为 Evidence。例如它能支撑“Agent 在 02:03 调用了某个 Tool”。但它必须绑定这个具体 Claim，并具备可信的采集与完整性边界。</p><h3 id="Artifact-不是-Evidence"><a href="#Artifact-不是-Evidence" class="headerlink" title="Artifact 不是 Evidence"></a>Artifact 不是 Evidence</h3><p>Artifact 是 Agent 产出的文件、代码、报告或数据。Artifact 存在，只能证明有交付物；它不能自动证明交付物正确。</p><p>测试报告可以同时扮演 Artifact 和 Evidence，但这是两个 Role：作为 Artifact，它是产物；作为 Evidence，它支撑“测试通过”这条 Claim。</p><h3 id="Evaluation-不是-Evidence"><a href="#Evaluation-不是-Evidence" class="headerlink" title="Evaluation 不是 Evidence"></a>Evaluation 不是 Evidence</h3><p>Evidence 是事实材料，Evaluation 是依据规则作出的判断。</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Evidence + Evaluation Policy → Verification Result</span><br></pre></td></tr></table></figure><p>不要把模型打出的 <code>0.87</code> 直接塞进 Evidence。这个分数属于 Evaluation Result；生成分数所依据的事实、样本和规则才是 Evidence 与 Policy。</p><h2 id="一个-Coding-Agent-的完整例子"><a href="#一个-Coding-Agent-的完整例子" class="headerlink" title="一个 Coding Agent 的完整例子"></a>一个 Coding Agent 的完整例子</h2><img src="https://blog.zisheng.pro/images/evidence-delivery-ladder.webp" data-lazy-src="https://blog.zisheng.pro/images/evidence-delivery-ladder.webp" alt="Coding Agent 从构建、提交、推送、部署到生产验收的 Claim 与 Evidence 阶梯"><p>假设 Coding Agent 说：“功能已经发布。”</p><p>这句话至少隐藏了五条 Claim：</p><table><thead><tr><th>Claim</th><th>可以采用的 Evidence</th><th>不能替代它的材料</th></tr></thead><tbody><tr><td>代码已修改</td><td>目标范围内的 Git Diff</td><td>Agent 的自然语言总结</td></tr><tr><td>修改通过工程验证</td><td>测试结果、Build Artifact、退出码</td><td>文件已经保存</td></tr><tr><td>版本已提交</td><td>Commit SHA、Tree Hash</td><td>本地工作区状态</td></tr><tr><td>版本已推送</td><td>Remote Ref 指向目标 Commit</td><td>本地 Commit 存在</td></tr><tr><td>版本已部署</td><td>Deployment ID、目标环境状态</td><td>Push 成功</td></tr><tr><td>生产结果符合预期</td><td>生产 URL、版本指纹、状态检查、真实页面或 API 结果</td><td>Deployment Job 显示 success</td></tr></tbody></table><p>这里最容易犯的错误，是拿低一层 Evidence 支撑高一层 Claim：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">Build passed  ≠  Deployed</span><br><span class="line">Git pushed    ≠  Production updated</span><br><span class="line">HTTP 200      ≠  Expected version served</span><br><span class="line">Screenshot    ≠  All users can complete the flow</span><br></pre></td></tr></table></figure><p>Evidence-based Delivery 应先把大而模糊的“完成”拆成可验证 Claim，再为每条 Claim 选择最接近 Source of Truth 的 Evidence。继续堆积截图和日志不会自动补上这层关系。</p><h2 id="在-RAG-与-LLM-Wiki-里，Evidence-应该是什么"><a href="#在-RAG-与-LLM-Wiki-里，Evidence-应该是什么" class="headerlink" title="在 RAG 与 LLM Wiki 里，Evidence 应该是什么"></a>在 RAG 与 LLM Wiki 里，Evidence 应该是什么</h2><p>假设回答中出现一条结论：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Claim：订单支付后 24 小时内可以免费取消。</span><br></pre></td></tr></table></figure><p>一个完整的 Evidence 至少应该包含：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">Source:        cancellation-policy.pdf</span><br><span class="line">Revision:      2026-07-18</span><br><span class="line">Locator:       page=12, table=2, row=3</span><br><span class="line">Quoted Span:   支付完成后 24 小时内……</span><br><span class="line">Effective At:  2026-07-20</span><br><span class="line">Scope:         普通订单；不含特价产品</span><br><span class="line">Digest:        sha256:...</span><br><span class="line">Polarity:      supports</span><br></pre></td></tr></table></figure><p>向量检索返回的 Chunk 只能算 Candidate Evidence。它还可能：</p><ul><li>命中过期版本；</li><li>丢失表头或脚注；</li><li>越过权限范围；</li><li>只在相似语义上相关；</li><li>无法支持回答中的具体数字。</li></ul><p>因此 LLM Wiki 真正应该管理的，不只是 Chunk 和 Citation，而是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Claim → Evidence Span → Source Revision → Permission Scope</span><br></pre></td></tr></table></figure><p>这时用户点开引用，看到的不只是“模型参考了哪篇文档”，而是“这句话具体由哪一段、哪个版本、在什么范围内支撑”。</p><h2 id="在-GUI-Agent-里，点击不是-Evidence"><a href="#在-GUI-Agent-里，点击不是-Evidence" class="headerlink" title="在 GUI Agent 里，点击不是 Evidence"></a>在 GUI Agent 里，点击不是 Evidence</h2><p>GUI Agent 的 Trace 经常是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">点击“提交”</span><br><span class="line">等待 2 秒</span><br><span class="line">页面出现绿色提示</span><br></pre></td></tr></table></figure><p>这只能证明执行过程被记录。业务验收应根据 Claim 选择更直接的 Evidence：</p><ul><li>表单提交：服务端返回的业务 ID；</li><li>订单创建：订单系统中的新记录；</li><li>退款完成：支付系统状态与交易号；</li><li>文件上传：对象存储中的 Key、Hash 与读取验证；</li><li>页面发布：生产 URL 的版本指纹与用户可见内容。</li></ul><p>截图可以作为补充 Evidence，但它通常存在三个限制：难以结构化比对、可能截错环境、无法证明后端状态。只要存在权威业务系统，就应该优先从权威系统重新读取状态。</p><h2 id="Evidence-的生命周期"><a href="#Evidence-的生命周期" class="headerlink" title="Evidence 的生命周期"></a>Evidence 的生命周期</h2><p>Evidence 不是写入后永久有效的附件。它应该有自己的生命周期：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">candidate</span><br><span class="line">  → captured</span><br><span class="line">  → validated</span><br><span class="line">  → admitted / rejected</span><br><span class="line">  → superseded / expired / revoked</span><br></pre></td></tr></table></figure><p>这里有两层判断，不能混在一起：</p><ol><li><strong>Evidence Validation</strong>：材料是否真实、完整、可定位、未过期，能否进入证据集合；</li><li><strong>Claim Evaluation</strong>：证据集合是否足以让某条 Claim 通过、失败或保持不确定。</li></ol><p>可能出现这些情况：</p><ul><li>Evidence 本身有效，但与 Claim 无关；</li><li>两条有效 Evidence 相互冲突；</li><li>Evidence 支持 Claim，但覆盖范围不足；</li><li>原本有效的 Evidence 因 Source Revision 更新而过期；</li><li>新 Evidence 推翻了旧 Verification Result。</li></ul><p>所以 Verification Result 最好也是可重算的派生对象，而不是直接写死在 Evidence 上。</p><h2 id="什么时候应该把-Evidence-做成一级对象"><a href="#什么时候应该把-Evidence-做成一级对象" class="headerlink" title="什么时候应该把 Evidence 做成一级对象"></a>什么时候应该把 Evidence 做成一级对象</h2><p>简单聊天机器人可以把 Citation 和 Evidence Span 放在 Message Metadata 中，不必一开始建设复杂平台。</p><p>当出现下面任意几种需求时，Evidence 应升级为一级领域对象：</p><ul><li>同一条 Evidence 会被多个 Claim、Run 或 Evaluator 引用；</li><li>Evidence 需要独立权限与脱敏；</li><li>Evidence 会过期、撤销、被替代或重新验证；</li><li>系统需要审计“当时为什么通过”；</li><li>需要比较不同 Agent、模型或版本采用了哪些证据；</li><li>需要第三方复查，或者生成 Acceptance Receipt；</li><li>业务不能接受 Agent 用自己的输出证明自己。</li></ul><p>最小架构可以分成五块：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">Evidence Collector   从 Trace、Artifact、Source of Truth 捕获候选材料</span><br><span class="line">Evidence Normalizer  补齐 Locator、Scope、Time、Digest、Provenance</span><br><span class="line">Evidence Store       保存不可变 Snapshot 与关系</span><br><span class="line">Claim Verifier       按 Policy 组合和评估 Evidence</span><br><span class="line">Verification Ledger  记录谁在何时基于哪组 Evidence 作出什么判断</span><br></pre></td></tr></table></figure><h2 id="常见反模式"><a href="#常见反模式" class="headerlink" title="常见反模式"></a>常见反模式</h2><h3 id="把所有运行数据都叫-Evidence"><a href="#把所有运行数据都叫-Evidence" class="headerlink" title="把所有运行数据都叫 Evidence"></a>把所有运行数据都叫 Evidence</h3><p>结果是 Evidence Store 退化成另一个日志平台。数据很多，但无法回答任何具体 Claim。</p><h3 id="让-Agent-自己证明自己"><a href="#让-Agent-自己证明自己" class="headerlink" title="让 Agent 自己证明自己"></a>让 Agent 自己证明自己</h3><p>Agent 输出“工具调用成功”只能算自述。高风险 Claim 应由独立 Verifier 从权威系统重新读取状态。</p><h3 id="只保存链接，不保存版本和定位"><a href="#只保存链接，不保存版本和定位" class="headerlink" title="只保存链接，不保存版本和定位"></a>只保存链接，不保存版本和定位</h3><p>链接内容一变，历史 Verification Result 就无法复现。关键材料至少要有 Revision、Locator 或 Digest。</p><h3 id="一个-Screenshot-证明整条业务链"><a href="#一个-Screenshot-证明整条业务链" class="headerlink" title="一个 Screenshot 证明整条业务链"></a>一个 Screenshot 证明整条业务链</h3><p>截图能证明某个时刻的视觉状态，不能自动证明后端数据、权限覆盖、持久化和其他用户视角。</p><h3 id="把-Confidence-写在-Evidence-上"><a href="#把-Confidence-写在-Evidence-上" class="headerlink" title="把 Confidence 写在 Evidence 上"></a>把 Confidence 写在 Evidence 上</h3><p>Confidence 依赖 Claim、Evidence Bundle、Evaluator 和 Policy。把一个全局置信度塞给单条 Evidence，会掩盖判断条件。</p><h3 id="只保留通过的-Evidence"><a href="#只保留通过的-Evidence" class="headerlink" title="只保留通过的 Evidence"></a>只保留通过的 Evidence</h3><p>反驳材料、冲突材料和被拒绝原因同样重要。否则系统会形成只为既有结论找支持的确认偏误。</p><h2 id="我的判断框架"><a href="#我的判断框架" class="headerlink" title="我的判断框架"></a>我的判断框架</h2><p>当一个系统说“我们有 Evidence”时，我会连续问八个问题：</p><ol><li>它绑定的 Claim 是什么？</li><li>它是支持还是反驳 Claim？</li><li>Source of Truth 是谁？</li><li>能否精确定位并重新获取原始事实？</li><li>它适用于哪个环境、版本、主体和时间？</li><li>如何确认内容没有被替换？</li><li>谁负责验证，依据哪条 Policy？</li><li>出现冲突、过期或撤销后，旧结论能否重新计算？</li></ol><p>如果这些问题回答不出来，系统拥有的多半只是 Logs、Links、Screenshots 或 Retrieved Chunks，还没有真正建立 Evidence Model。</p><p>Evidence 对 Agent 产品最重要的价值，是把“完成”从一次自述改造成可以独立验收的结论：</p><blockquote><p>Agent 可以提出 Claim，但不能只靠自己的话完成验收。系统必须保留足够的 Evidence，让另一个人、另一个 Verifier，甚至未来的另一套模型能够重新得到或推翻这个结论。</p></blockquote><h2 id="参考索引"><a href="#参考索引" class="headerlink" title="参考索引"></a>参考索引</h2><p>以下均为本文实际采用的规范、标准或官方工程文档，检索日期为 2026-08-05。</p><table><thead><tr><th>范畴</th><th>参考资料</th><th>本文采用的依据</th></tr></thead><tbody><tr><td>Provenance</td><td><a href="https://www.w3.org/TR/prov-o/">W3C PROV-O：The PROV Ontology</a></td><td>Agent、Activity、Entity，以及生成、使用、归属、派生和失效关系</td></tr><tr><td>Claims &#x2F; Evidence</td><td><a href="https://www.w3.org/TR/vc-data-model-2.0/">W3C Verifiable Credentials Data Model 2.0</a></td><td>Evidence、Issuer、Subject、Validity、Status 与 Securing Mechanism 的边界；可验证不等于 Claim 为真</td></tr><tr><td>Attestation</td><td><a href="https://github.com/in-toto/attestation/blob/main/spec/v1/statement.md">in-toto Statement Layer Specification</a></td><td>Subject Digest、Predicate Type、Predicate 的绑定结构</td></tr><tr><td>Software Provenance</td><td><a href="https://slsa.dev/spec/v1.2/">SLSA Specification 1.2</a></td><td>Provenance Attestation、Verification Summary 与消费者验证策略</td></tr><tr><td>Observability</td><td><a href="https://opentelemetry.io/docs/specs/otel/trace/api/">OpenTelemetry Tracing API</a></td><td>Trace、Span、Event、Status 的过程记录模型</td></tr><tr><td>Agent Runtime</td><td><a href="https://openai.github.io/openai-agents-python/tracing/">OpenAI Agents SDK：Tracing</a></td><td>Agent Run 中 Generation、Tool Call、Handoff、Guardrail 等 Trace 数据的范围</td></tr><tr><td>AI Risk</td><td><a href="https://www.nist.gov/publications/artificial-intelligence-risk-management-framework-generative-artificial-intelligence">NIST AI RMF：Generative AI Profile</a></td><td>Governance、Pre-deployment Testing、Content Provenance 与 Incident Disclosure 的风险管理视角</td></tr></tbody></table><p>本文给出的 Evidence 定义、生命周期和字段模型，是基于这些资料进行的 Agent 领域建模推导，不是任何单一规范的原文 Schema。</p>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/2017abed9c67/</id>
    <link href="https://blog.zisheng.pro/posts/2017abed9c67/"/>
    <published>2026-08-04T18:15:00.000Z</published>
    <summary>Evidence 不是知识、引用或日志的别名，而是 Agent 系统用来支撑或反驳具体 Claim 的可验证事实记录。本文从 W3C PROV、Verifiable Credentials、in-toto、SLSA 与 Agent Tracing 出发，讲清 Evidence 的领域边界、数据模型、生命周期和生产验收方法。</summary>
    <title>Evidence 到底是什么：Agent 系统如何证明自己真的完成了任务</title>
    <updated>2026-09-18T08:15:28.629Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="AI" scheme="https://blog.zisheng.pro/categories/AI/"/>
    <category term="AI Infra" scheme="https://blog.zisheng.pro/tags/AI-Infra/"/>
    <category term="Agent" scheme="https://blog.zisheng.pro/tags/Agent/"/>
    <category term="Evaluation" scheme="https://blog.zisheng.pro/tags/Evaluation/"/>
    <category term="Agent Runtime" scheme="https://blog.zisheng.pro/tags/Agent-Runtime/"/>
    <category term="Harness" scheme="https://blog.zisheng.pro/tags/Harness/"/>
    <category term="Checkpoint" scheme="https://blog.zisheng.pro/tags/Checkpoint/"/>
    <content>
      <![CDATA[<p><img src="/images/agent-runtime-harness-architecture.webp" alt="可验证、可恢复、可评测的 Agent Runtime 与 Harness 参考架构"></p><p><em>Agent Runtime &#x2F; Harness 参考架构：以稳定的 Run Contract 包住不稳定的 Model 与 Provider，把验证、恢复和评测变成系统能力。</em></p><p>最近我一直在想，所谓 Agent 工程到底比 Workflow 多了什么。</p><p>如果最终只是把 Prompt、LLM 和 Tool 连成一张图，再用 React Flow 画出来，它与 Dify、Coze 里的 Workflow 确实没有本质区别。真正让 Agent 从 Demo 走向生产的，不是节点更多，也不是图更炫，而是系统开始回答三类很不“智能”、却决定能否交付的问题：</p><ul><li>Agent 说任务完成了，我怎样证明它真的完成了？</li><li>执行到一半进程崩了，我怎样继续，而不是从头再赌一次？</li><li>换了模型、Prompt 或 Tool 后，效果究竟变好了还是变差了？</li></ul><p>从前端转向 Agent &#x2F; AI Infra 后，我越来越确信：值得投入的工程对象不是一个新的 Graph Editor，而是一套<strong>可验证、可恢复、可评测的 Agent Runtime &#x2F; Harness</strong>。</p><h2 id="一句话总结"><a href="#一句话总结" class="headerlink" title="一句话总结"></a>一句话总结</h2><p><strong>Runtime 负责让 Agent 跑起来，Harness 负责让这次运行可控制、可追踪、可恢复、可验收、可比较。</strong></p><p>服务器、容器、Sandbox 只是 Execution Environment；Agent Runtime &#x2F; Harness 是运行在这些环境之上的控制系统。它不需要替模型思考，但必须约束模型如何访问状态、调用工具、产生副作用、等待审批、保存进度和交付结果。</p><p>我会用下面这条公式判断它是否真正落地：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">Production Agent</span><br><span class="line">= Model / Agent SDK</span><br><span class="line">+ Stable Run Contract</span><br><span class="line">+ Controlled Side Effects</span><br><span class="line">+ Durable State</span><br><span class="line">+ Evidence-based Verification</span><br><span class="line">+ Continuous Evaluation</span><br></pre></td></tr></table></figure><p>少了后五项，通常只是一个可以演示的 Agent Loop。</p><h2 id="Runtime、Harness-和服务器运行环境不是一回事"><a href="#Runtime、Harness-和服务器运行环境不是一回事" class="headerlink" title="Runtime、Harness 和服务器运行环境不是一回事"></a>Runtime、Harness 和服务器运行环境不是一回事</h2><p>“Runtime”这个词确实容易模糊，因为它在不同语境中指向不同层次。</p><table><thead><tr><th>层次</th><th>典型对象</th><th>负责什么</th></tr></thead><tbody><tr><td>Execution Environment</td><td>Server、Container、Sandbox、Browser、Local Host</td><td>CPU、内存、网络、文件系统、进程隔离</td></tr><tr><td>Model Runtime</td><td>Model API、Inference Server</td><td>接收输入并生成 Token、Tool Call 或结构化输出</td></tr><tr><td>Agent Runtime</td><td>Agent Loop、Run State、Tool Dispatch、Streaming、Handoff</td><td>驱动一次 Agent 执行</td></tr><tr><td>Agent Harness</td><td>Policy、Approval、Checkpoint、Trace、Evidence、Eval</td><td>管理 Agent 执行的生产边界</td></tr></tbody></table><p>现实中，Runtime 和 Harness 经常被混用。我的建议不是争论名词，而是把职责写清楚：</p><ul><li>如果一个组件只负责 <code>while</code> 循环、调用模型和分发 Tool，它更接近 Agent Runtime。</li><li>如果它还拥有 Run ID、权限、审批、恢复、证据和评测，它已经是 Harness。</li><li>如果它只提供 Linux、Node.js、Python 和网络，那只是运行环境，不是 Agent Runtime。</li></ul><p>这也是为什么部署了一个 Agent SDK，并不等于拥有了生产级 Runtime。SDK 解决的是“怎样调用”，Harness 解决的是“这次调用在业务上是否受控、可信、可持续演进”。</p><h2 id="它与-Workflow-的真正区别"><a href="#它与-Workflow-的真正区别" class="headerlink" title="它与 Workflow 的真正区别"></a>它与 Workflow 的真正区别</h2><p>Workflow 描述<strong>希望执行的路径</strong>，Runtime &#x2F; Harness 管理<strong>实际发生的执行</strong>。</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">Workflow:  A → B → C</span><br><span class="line"></span><br><span class="line">Runtime:   谁启动了 A？</span><br><span class="line">           B 重试后会不会重复扣款？</span><br><span class="line">           C 等待审批时状态存在哪里？</span><br><span class="line">           进程重启后从哪一步继续？</span><br><span class="line">           最终结果由什么证据证明？</span><br><span class="line">           这次执行比上一个版本好在哪里？</span><br></pre></td></tr></table></figure><p>因此，一张静态 Graph 可以是 Runtime 的输入，却不是 Runtime 本身。很多 Agent 任务甚至无法预先画出完整路径：模型会根据中间结果决定继续搜索、修改计划、调用其他工具或请求人工介入。</p><p>我会把两者关系理解成：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">Workflow / Graph = Execution Plan</span><br><span class="line">Runtime / Harness = Execution Semantics + Control + Evidence</span><br></pre></td></tr></table></figure><p>React Flow 只解决编辑和展示。如果产品真的需要让用户配置确定性流程，可以引入；如果当前目标是让 Agent 可靠执行，先做 React Flow 往往会把注意力带到节点拖拽、连线和表单配置上，最难的恢复与验收仍然没有答案。</p><h2 id="先固定十个核心对象"><a href="#先固定十个核心对象" class="headerlink" title="先固定十个核心对象"></a>先固定十个核心对象</h2><p>一个可演进的 Harness，第一步不是选框架，而是定义自己的领域对象。至少要有下面十个：</p><table><thead><tr><th>对象</th><th>最小职责</th><th>不应该被什么替代</th></tr></thead><tbody><tr><td>Workspace</td><td>本次执行可见、可修改的工作边界</td><td>当前进程目录</td></tr><tr><td>Revision</td><td>执行绑定的输入与代码版本</td><td>“最新版本”</td></tr><tr><td>Run</td><td>一次可定位的端到端执行</td><td>Provider 的 Session ID</td></tr><tr><td>Step</td><td>可重试、可检查的状态转移</td><td>一条自然语言日志</td></tr><tr><td>Tool Call</td><td>结构化的工具调用请求</td><td>模型输出的一段文本</td></tr><tr><td>Permission &#x2F; Approval</td><td>谁允许什么副作用发生</td><td>Prompt 里的“请小心”</td></tr><tr><td>Checkpoint</td><td>可恢复的 Run State 快照</td><td>聊天记录</td></tr><tr><td>Event &#x2F; Trace</td><td>按时间记录发生了什么</td><td>最终回答</td></tr><tr><td>Artifact &#x2F; Evidence</td><td>支撑完成声明的产物与证据</td><td>Agent 自述“已完成”</td></tr><tr><td>Eval Result</td><td>对结果、过程、成本与韧性的评分</td><td>主观体验“看起来不错”</td></tr></tbody></table><p>这里最容易混淆的是 Memory、State、Trace、Evidence 和 Eval：</p><ul><li><strong>Memory</strong>：跨 Run 保留的偏好、事实和经验。</li><li><strong>State</strong>：当前 Run 此刻执行到哪里，以及继续所需的数据。</li><li><strong>Trace</strong>：这次 Run 按时间发生过哪些事件。</li><li><strong>Evidence</strong>：从产物和权威系统中选出的、能支撑某个 Claim 的证据。</li><li><strong>Eval</strong>：根据预先定义的标准，对结果或过程做判断。</li></ul><p>聊天记录可以参与恢复，但它不是完整 State；Trace 可以帮助调查，但它不自动构成 Evidence；Evidence 能证明一次交付，却不能替代跨版本的 Eval。</p><h2 id="三条必须守住的工程边界"><a href="#三条必须守住的工程边界" class="headerlink" title="三条必须守住的工程边界"></a>三条必须守住的工程边界</h2><h3 id="1-Provider-Adapter：隔离不稳定的-Agent-实现"><a href="#1-Provider-Adapter：隔离不稳定的-Agent-实现" class="headerlink" title="1. Provider Adapter：隔离不稳定的 Agent 实现"></a>1. Provider Adapter：隔离不稳定的 Agent 实现</h3><p>模型、Agent SDK 和 Coding Agent 都会变化。业务系统不应该直接依赖某个 Provider 的 Thread ID、事件名和审批格式，而应该维护自己的稳定协议。</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">interface</span> <span class="title class_">AgentRuntimeAdapter</span> &#123;</span><br><span class="line">  <span class="title function_">start</span>(<span class="attr">input</span>: <span class="title class_">StartRunInput</span>): <span class="title class_">Promise</span>&lt;<span class="title class_">ExternalRunRef</span>&gt;;</span><br><span class="line">  <span class="title function_">resume</span>(<span class="attr">input</span>: <span class="title class_">ResumeRunInput</span>): <span class="title class_">Promise</span>&lt;<span class="built_in">void</span>&gt;;</span><br><span class="line">  <span class="title function_">cancel</span>(<span class="attr">runId</span>: <span class="built_in">string</span>): <span class="title class_">Promise</span>&lt;<span class="built_in">void</span>&gt;;</span><br><span class="line">  <span class="title function_">stream</span>(<span class="attr">runId</span>: <span class="built_in">string</span>): <span class="title class_">AsyncIterable</span>&lt;<span class="title class_">AgentEvent</span>&gt;;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> <span class="title class_">AgentEvent</span> =</span><br><span class="line">  | &#123; <span class="attr">type</span>: <span class="string">&#x27;run.started&#x27;</span>; <span class="attr">runId</span>: <span class="built_in">string</span> &#125;</span><br><span class="line">  | &#123; <span class="attr">type</span>: <span class="string">&#x27;model.completed&#x27;</span>; <span class="attr">usage</span>: <span class="title class_">Usage</span> &#125;</span><br><span class="line">  | &#123; <span class="attr">type</span>: <span class="string">&#x27;tool.requested&#x27;</span>; <span class="attr">call</span>: <span class="title class_">ToolCall</span> &#125;</span><br><span class="line">  | &#123; <span class="attr">type</span>: <span class="string">&#x27;approval.required&#x27;</span>; <span class="attr">approval</span>: <span class="title class_">ApprovalRequest</span> &#125;</span><br><span class="line">  | &#123; <span class="attr">type</span>: <span class="string">&#x27;checkpoint.saved&#x27;</span>; <span class="attr">checkpointId</span>: <span class="built_in">string</span> &#125;</span><br><span class="line">  | &#123; <span class="attr">type</span>: <span class="string">&#x27;artifact.created&#x27;</span>; <span class="attr">artifact</span>: <span class="title class_">ArtifactRef</span> &#125;</span><br><span class="line">  | &#123; <span class="attr">type</span>: <span class="string">&#x27;run.completed&#x27;</span>; <span class="attr">claims</span>: <span class="title class_">Claim</span>[] &#125;</span><br><span class="line">  | &#123; <span class="attr">type</span>: <span class="string">&#x27;run.failed&#x27;</span>; <span class="attr">error</span>: <span class="title class_">NormalizedError</span> &#125;;</span><br></pre></td></tr></table></figure><p>内部 <code>runId</code> 是事实主键，Provider Session ID 只是 <code>ExternalRunRef</code>。这样才能在不重写上层业务的情况下更换模型、SDK，甚至把一次任务路由到不同 Runtime。</p><p>适配层也不应该强行抹平所有差异。比如某个 Provider 支持原生 Resume，另一个只能重建 Context，应该通过 Capability 声明暴露：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> <span class="title class_">RuntimeCapabilities</span> = &#123;</span><br><span class="line">  <span class="attr">resumable</span>: <span class="built_in">boolean</span>;</span><br><span class="line">  <span class="attr">approval</span>: <span class="string">&#x27;native&#x27;</span> | <span class="string">&#x27;emulated&#x27;</span> | <span class="string">&#x27;none&#x27;</span>;</span><br><span class="line">  <span class="attr">streaming</span>: <span class="built_in">boolean</span>;</span><br><span class="line">  <span class="attr">sandbox</span>: <span class="string">&#x27;hosted&#x27;</span> | <span class="string">&#x27;local&#x27;</span> | <span class="string">&#x27;external&#x27;</span>;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>“统一接口”不是假装大家能力相同，而是让差异变得可查询、可路由、可评测。</p><h3 id="2-Tool-Side-Effect：所有真实动作经过受控边界"><a href="#2-Tool-Side-Effect：所有真实动作经过受控边界" class="headerlink" title="2. Tool Side Effect：所有真实动作经过受控边界"></a>2. Tool Side Effect：所有真实动作经过受控边界</h3><p>Agent 最危险的部分不是生成了一句错话，而是用错误参数真的执行了退款、发版、删库或发消息。</p><p>因此 Tool 不应只是一个普通函数。每次调用至少要绑定：</p><ul><li><code>runId</code> 和 <code>stepId</code>；</li><li>调用者与 Workspace；</li><li>输入 Schema 和输出 Schema；</li><li>风险级别与所需 Permission；</li><li><code>idempotencyKey</code>；</li><li>超时、重试和补偿策略；</li><li>调用前后的权威状态快照；</li><li>产生的 Artifact 与 Evidence。</li></ul><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> result = <span class="keyword">await</span> toolGateway.<span class="title function_">execute</span>(&#123;</span><br><span class="line">  runId,</span><br><span class="line">  stepId,</span><br><span class="line">  <span class="attr">tool</span>: <span class="string">&#x27;release.create&#x27;</span>,</span><br><span class="line">  input,</span><br><span class="line">  <span class="attr">idempotencyKey</span>: <span class="string">`<span class="subst">$&#123;runId&#125;</span>:<span class="subst">$&#123;stepId&#125;</span>:release.create`</span>,</span><br><span class="line">  <span class="attr">policy</span>: &#123; <span class="attr">approval</span>: <span class="string">&#x27;required&#x27;</span>, <span class="attr">timeoutMs</span>: <span class="number">60_000</span> &#125;,</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p>重试只能作用于已声明幂等的边界。否则“失败后自动重试”可能变成重复扣款、重复发信或重复发布。</p><h3 id="3-Run-State：状态属于系统，不属于某次进程"><a href="#3-Run-State：状态属于系统，不属于某次进程" class="headerlink" title="3. Run State：状态属于系统，不属于某次进程"></a>3. Run State：状态属于系统，不属于某次进程</h3><p>如果 Node.js 进程一退出，任务就永远丢失，那么它没有真正的恢复能力。</p><p>Run State 至少应包含：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> <span class="title class_">RunState</span> = &#123;</span><br><span class="line">  <span class="attr">runId</span>: <span class="built_in">string</span>;</span><br><span class="line">  <span class="attr">status</span>: <span class="string">&#x27;queued&#x27;</span> | <span class="string">&#x27;running&#x27;</span> | <span class="string">&#x27;waiting_approval&#x27;</span> |</span><br><span class="line">          <span class="string">&#x27;suspended&#x27;</span> | <span class="string">&#x27;completed&#x27;</span> | <span class="string">&#x27;failed&#x27;</span> | <span class="string">&#x27;cancelled&#x27;</span>;</span><br><span class="line">  <span class="attr">workspace</span>: <span class="title class_">WorkspaceRef</span>;</span><br><span class="line">  <span class="attr">revision</span>: <span class="title class_">RevisionRef</span>;</span><br><span class="line">  <span class="attr">runtime</span>: <span class="title class_">RuntimeRef</span>;</span><br><span class="line">  currentStep?: <span class="built_in">string</span>;</span><br><span class="line">  <span class="attr">pendingToolCalls</span>: <span class="title class_">ToolCall</span>[];</span><br><span class="line">  <span class="attr">approvals</span>: <span class="title class_">ApprovalState</span>[];</span><br><span class="line">  <span class="attr">artifacts</span>: <span class="title class_">ArtifactRef</span>[];</span><br><span class="line">  providerState?: <span class="built_in">unknown</span>;</span><br><span class="line">  <span class="attr">contractVersion</span>: <span class="built_in">string</span>;</span><br><span class="line">  <span class="attr">updatedAt</span>: <span class="built_in">string</span>;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p><code>contractVersion</code> 很重要。OpenAI Agents SDK 的 Human-in-the-loop 文档已经支持序列化 <code>RunState</code> 后跨请求恢复，同时也提醒：长期挂起任务需要把 Agent 定义和 SDK 版本与序列化状态一起管理。换句话说，Checkpoint 不只是保存 JSON，还要知道它由哪一版代码解释。</p><h2 id="可验证：不要相信-Agent-的完成声明"><a href="#可验证：不要相信-Agent-的完成声明" class="headerlink" title="可验证：不要相信 Agent 的完成声明"></a>可验证：不要相信 Agent 的完成声明</h2><p>“任务已完成”只是一条 Claim，不是事实。</p><p>真正的验证应该回到权威系统重新读取状态。例如 Coding Agent 说“修复完成”，Harness 应检查：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">Claim: 修复已完成</span><br><span class="line">  ├─ Evidence: git diff 中只有目标范围的修改</span><br><span class="line">  ├─ Evidence: 目标测试从 fail 变为 pass</span><br><span class="line">  ├─ Evidence: 原有回归测试仍然通过</span><br><span class="line">  ├─ Evidence: 构建产物成功生成</span><br><span class="line">  └─ Acceptance: 目标环境 Smoke Test 通过</span><br></pre></td></tr></table></figure><p>如果是退款 Agent，Evidence 应来自支付系统的退款状态与交易号；如果是发布 Agent，Evidence 应来自构建产物、Deployment ID 和线上健康检查，而不是 Tool 返回的“请求已提交”。</p><p>我建议把完成逻辑设计成显式的 Claim Verifier：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> <span class="title class_">Claim</span> = &#123;</span><br><span class="line">  <span class="attr">type</span>: <span class="string">&#x27;code.fixed&#x27;</span> | <span class="string">&#x27;release.online&#x27;</span> | <span class="string">&#x27;refund.completed&#x27;</span>;</span><br><span class="line">  <span class="attr">subject</span>: <span class="built_in">string</span>;</span><br><span class="line">  <span class="attr">evidence</span>: <span class="title class_">EvidenceRef</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_">ClaimVerifier</span> &#123;</span><br><span class="line">  <span class="title function_">verify</span>(<span class="attr">claim</span>: <span class="title class_">Claim</span>, <span class="attr">context</span>: <span class="title class_">VerificationContext</span>): <span class="title class_">Promise</span>&lt;&#123;</span><br><span class="line">    <span class="attr">passed</span>: <span class="built_in">boolean</span>;</span><br><span class="line">    <span class="attr">checks</span>: <span class="title class_">CheckResult</span>[];</span><br><span class="line">  &#125;&gt;;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这使“完成”从自然语言变成可执行的 Acceptance Contract。</p><p>Trace 与 Evidence 也要分开。OpenAI Agents SDK 的 Tracing 会记录 LLM generation、Tool Call、Handoff、Guardrail 和自定义事件，这非常适合调试和监控；但完整 Trace 只是原始执行记录。Evidence 是从 Trace、Artifact 和权威状态中挑出的、足以支撑 Claim 的最小集合。</p><h2 id="可恢复：Checkpoint-只是起点"><a href="#可恢复：Checkpoint-只是起点" class="headerlink" title="可恢复：Checkpoint 只是起点"></a>可恢复：Checkpoint 只是起点</h2><p>恢复能力可以按五个等级理解：</p><table><thead><tr><th>等级</th><th>能力</th><th>主要问题</th></tr></thead><tbody><tr><td>L0</td><td>整个 Run 从头重试</td><td>重复成本高，副作用不安全</td></tr><tr><td>L1</td><td>恢复 Provider Session</td><td>能接着聊，不一定能恢复业务状态</td></tr><tr><td>L2</td><td>按 Step 保存 Checkpoint</td><td>能从明确边界继续</td></tr><tr><td>L3</td><td>Checkpoint + 幂等 Tool + Workspace Snapshot</td><td>可以安全应对进程崩溃和重复消息</td></tr><tr><td>L4</td><td>Checkpoint 可跨 Runtime &#x2F; Provider 迁移</td><td>通用性最高，成本和约束也最高</td></tr></tbody></table><p>大多数生产系统应该先做到 L3，不必一开始追求 L4。</p><p>LangGraph 的 Persistence 明确区分 Checkpointer 与 Store：前者保存单个 Thread 的 Graph State，用于中断恢复、Human-in-the-loop、Time Travel 和 Fault Tolerance；后者保存跨 Thread 的长期数据。官方文档也直接提醒，<code>MemorySaver</code> 在进程重启后会丢失，生产环境要使用持久化 Checkpointer。</p><p>这再次说明：Conversation Memory 不是 Recovery State。</p><p>一个安全的恢复循环应该是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">读取 Checkpoint</span><br><span class="line">  ↓</span><br><span class="line">校验 Contract Version 与 Workspace Revision</span><br><span class="line">  ↓</span><br><span class="line">检查上一个 Side Effect 是否已经生效</span><br><span class="line">  ↓</span><br><span class="line">用 Idempotency Key 决定复用结果还是重试</span><br><span class="line">  ↓</span><br><span class="line">恢复 Agent / Provider State</span><br><span class="line">  ↓</span><br><span class="line">继续下一步并写入新 Checkpoint</span><br></pre></td></tr></table></figure><p>对于持续数小时、数天，且横跨多个服务的业务流程，可以把 Agent Framework 与 Temporal 组合使用：Agent Framework 负责模型循环和 Tool 选择，Temporal 负责 Durable Execution、Timer、Retry 与跨服务状态。Temporal 的官方定义强调，即使发生进程崩溃、网络故障或基础设施中断，应用也能从离开的地方恢复。它不是 Agent Framework 的替代品，而是更底层的可靠执行基础设施。</p><h2 id="可评测：不要只评最终回答"><a href="#可评测：不要只评最终回答" class="headerlink" title="可评测：不要只评最终回答"></a>可评测：不要只评最终回答</h2><p>Agent 的输出可能正确，过程却越权、昂贵或不可恢复；也可能过程合理，但外部系统最终没有生效。因此评测至少要分四层：</p><table><thead><tr><th>层次</th><th>关键问题</th><th>指标示例</th></tr></thead><tbody><tr><td>Outcome</td><td>任务最终是否真的成功</td><td>Task Success、Test Pass、State Verification、人工验收率</td></tr><tr><td>Process</td><td>Agent 走的路径是否合理</td><td>Tool Selection、参数正确率、无效循环、越权次数、人工接管率</td></tr><tr><td>Cost</td><td>成功花了多少资源</td><td>Latency、Token、Tool Call、Compute、Cost per Success</td></tr><tr><td>Robustness</td><td>遇到故障还能否完成</td><td>Resume Success、重复消息、超时、部分失败、Checkpoint 恢复率</td></tr></tbody></table><p>OpenAI 的评测最佳实践强调 Eval-driven Development、任务特定数据集、完整日志、自动评分与持续评测，并明确反对“感觉它能工作”的 Vibe-based Evals。LangSmith 也把 Agent Eval 拆成 Final Response、Single Step 和 Trajectory；Trajectory 可以检查工具调用的顺序、集合与完整执行路径。</p><p>我倾向于组合三种 Grader：</p><ol><li><strong>Code-based Grader</strong>：测试、Schema、状态查询、静态规则，便宜、确定、可重复。</li><li><strong>Model-based Grader</strong>：Rubric、Pairwise、复杂语义质量，覆盖开放式任务。</li><li><strong>Human Grader</strong>：领域专家抽检和校准，防止自动评分体系自我欺骗。</li></ol><p>Anthropic 的 Agent Eval 实践也采用这三类，并指出模型评分需要由人工校准。我的优先级是：能用权威状态和代码判定的，绝不先交给 LLM Judge；只有难以结构化的质量维度，才使用模型评分。</p><h3 id="评测数据从哪里来"><a href="#评测数据从哪里来" class="headerlink" title="评测数据从哪里来"></a>评测数据从哪里来</h3><p>最初不需要几千条 Benchmark。可以先建立 20–30 个真实任务，覆盖：</p><ul><li>高频正常路径；</li><li>历史失败案例；</li><li>权限与审批边界；</li><li>Tool 超时和部分失败；</li><li>进程重启后的恢复；</li><li>重复消息与幂等；</li><li>模糊需求和人工接管；</li><li>成本或延迟极端值。</li></ul><p>每个 Case 固定 Workspace Revision、输入、可用 Tool、成功条件和预算，再对 Provider、Model、Prompt、Tool Version 做矩阵比较。非确定性的 Agent 至少重复运行 3–5 次，不要拿一次成功当能力结论。</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">Eval Case</span><br><span class="line">  ├─ Fixture: Workspace + Revision + External State</span><br><span class="line">  ├─ Input: User Intent</span><br><span class="line">  ├─ Policy: Allowed Tools + Approval Rules</span><br><span class="line">  ├─ Expected Outcome: Acceptance Contract</span><br><span class="line">  ├─ Process Constraints: Forbidden / Required Behaviors</span><br><span class="line">  └─ Budget: Time + Token + Cost</span><br></pre></td></tr></table></figure><p>Run、Trace、Evidence 和 Eval Result 必须共用同一个 <code>runId</code>。否则评测只剩一张分数表，无法回到具体轨迹解释为什么变好或变差。</p><h2 id="最小可落地架构"><a href="#最小可落地架构" class="headerlink" title="最小可落地架构"></a>最小可落地架构</h2><p>我不建议一开始自研完整平台。先做一个薄 Harness，把不可替代的控制权握在自己手里：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line">User / Trigger</span><br><span class="line">      ↓</span><br><span class="line">Harness API</span><br><span class="line">      ├─ Run Store / Event Log</span><br><span class="line">      ├─ Policy &amp; Approval</span><br><span class="line">      ├─ Workspace &amp; Revision</span><br><span class="line">      ├─ Tool Gateway</span><br><span class="line">      ├─ Checkpoint Store</span><br><span class="line">      ├─ Artifact &amp; Evidence</span><br><span class="line">      └─ Eval Runner</span><br><span class="line">              ↓</span><br><span class="line">      Runtime Adapter</span><br><span class="line">        ├─ OpenAI Agents SDK</span><br><span class="line">        ├─ LangGraph</span><br><span class="line">        └─ Other Agent / Coding Runtime</span><br><span class="line">              ↓</span><br><span class="line">      Model + Tools + Execution Environment</span><br></pre></td></tr></table></figure><p>最小技术选型可以非常朴素：</p><table><thead><tr><th>能力</th><th>第一阶段建议</th></tr></thead><tbody><tr><td>Run &#x2F; Event &#x2F; Checkpoint</td><td>PostgreSQL；本地单机可先 SQLite</td></tr><tr><td>Artifact</td><td>Object Storage 或受版本控制的 Workspace</td></tr><tr><td>Trace</td><td>OpenTelemetry，或先接 Provider 自带 Tracing 再导出</td></tr><tr><td>Tool Contract</td><td>JSON Schema + 统一 Gateway + Idempotency Key</td></tr><tr><td>Approval</td><td>数据库状态机 + 通知，不要让请求连接一直挂着</td></tr><tr><td>Eval</td><td>测试框架 + 自建 Case Schema；需要可视化再接评测平台</td></tr><tr><td>Durable Workflow</td><td>长周期、跨服务场景再引入 Temporal</td></tr><tr><td>Graph UI</td><td>只有用户确实要编辑流程时再引入 React Flow</td></tr></tbody></table><h3 id="现成框架怎么选"><a href="#现成框架怎么选" class="headerlink" title="现成框架怎么选"></a>现成框架怎么选</h3><p><strong>OpenAI Agents SDK</strong> 适合快速建立 Agent Loop，并直接获得 Tool、Handoff、Guardrail、Tracing 和 Human-in-the-loop。它的序列化 RunState 可以承担一部分恢复能力，但业务 Run、Tool 幂等、权威状态验证和跨 Provider 协议仍应由自己的 Harness 管理。</p><p><strong>LangGraph</strong> 适合状态和分支需要显式建模的 Agent。Graph State、Checkpoint、Interrupt、Time Travel 和持久化是它的强项。代价是图与状态 Schema 会进入核心代码，不要为了“看起来工程化”把开放式 Agent 强行拆成几十个节点。</p><p><strong>Temporal</strong> 适合长周期、高价值、跨服务、必须可靠恢复的业务执行。它解决 Durable Execution，不负责 Prompt、Tool Selection 和 Agent Quality，通常与 Agent SDK 组合，而不是二选一。</p><p><strong>LangSmith、OpenAI Tracing &#x2F; Evals 等平台</strong>可以承担观测和评测的一部分，但不要让平台生成的 Trace ID 成为业务主键。保留自己的 Run Contract，才能迁移、对比和审计。</p><p>所以我的默认组合不是“选一个大而全框架”，而是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">自有 Thin Harness</span><br><span class="line">+ 一个 Agent Runtime / SDK</span><br><span class="line">+ 持久化数据库</span><br><span class="line">+ 统一 Tool Gateway</span><br><span class="line">+ OpenTelemetry / Provider Trace</span><br><span class="line">+ 任务级 Eval Suite</span><br></pre></td></tr></table></figure><h2 id="分五个阶段落地"><a href="#分五个阶段落地" class="headerlink" title="分五个阶段落地"></a>分五个阶段落地</h2><h3 id="Phase-1：先让每次运行可定位"><a href="#Phase-1：先让每次运行可定位" class="headerlink" title="Phase 1：先让每次运行可定位"></a>Phase 1：先让每次运行可定位</h3><p>建立 <code>runId</code>、Run 状态机、统一 Event Schema 和基本 Trace。所有 Model Call、Tool Call、Artifact、Approval 都关联同一个 Run。</p><p>验收标准：任意一次失败都能回答“谁在什么时候，用什么版本，调用了什么，在哪一步失败”。</p><h3 id="Phase-2：控制所有副作用"><a href="#Phase-2：控制所有副作用" class="headerlink" title="Phase 2：控制所有副作用"></a>Phase 2：控制所有副作用</h3><p>把外部动作收口到 Tool Gateway，加入 Schema、Permission、Approval、Timeout、Idempotency 和调用前后状态。</p><p>验收标准：重复投递同一 Tool Call 不会造成重复业务结果；高风险操作不能绕过审批。</p><h3 id="Phase-3：做到进程级恢复"><a href="#Phase-3：做到进程级恢复" class="headerlink" title="Phase 3：做到进程级恢复"></a>Phase 3：做到进程级恢复</h3><p>持久化 Run State 和 Checkpoint，绑定 Workspace Revision、Agent Definition Version 与 Runtime Version，补 Resume、Cancel 和过期策略。</p><p>验收标准：在关键 Step 后强制杀掉进程，重启后可以继续，且不重复已完成的副作用。</p><h3 id="Phase-4：建立-Evidence-based-Acceptance"><a href="#Phase-4：建立-Evidence-based-Acceptance" class="headerlink" title="Phase 4：建立 Evidence-based Acceptance"></a>Phase 4：建立 Evidence-based Acceptance</h3><p>为核心 Claim 建立 Verifier，从权威系统重新读取结果，生成最小 Evidence Bundle 与 Acceptance Result。</p><p>验收标准：Agent 自述与真实状态不一致时，Run 不能被标记为业务完成。</p><h3 id="Phase-5：用-Eval-驱动演进"><a href="#Phase-5：用-Eval-驱动演进" class="headerlink" title="Phase 5：用 Eval 驱动演进"></a>Phase 5：用 Eval 驱动演进</h3><p>沉淀真实 Case，记录 Outcome、Process、Cost、Robustness，比较不同 Model、Prompt、Tool 和 Runtime，逐步加入线上采样与回归门禁。</p><p>验收标准：每次关键变更都能回答“成功率、成本、延迟和失败类型发生了什么变化”。</p><p>这五个阶段不需要 Graph Editor。等到系统已经拥有可靠的执行语义，再把适合人工配置的确定性部分可视化，React Flow 才会成为产品能力，而不是工程完成度的幻觉。</p><h2 id="常见反模式"><a href="#常见反模式" class="headerlink" title="常见反模式"></a>常见反模式</h2><h3 id="把-Provider-Session-ID-当自己的-Run-ID"><a href="#把-Provider-Session-ID-当自己的-Run-ID" class="headerlink" title="把 Provider Session ID 当自己的 Run ID"></a>把 Provider Session ID 当自己的 Run ID</h3><p>一旦切换 Provider 或同一业务 Run 包含多段模型执行，关联关系就会断裂。Provider ID 应只是外部引用。</p><h3 id="把聊天历史当-Checkpoint"><a href="#把聊天历史当-Checkpoint" class="headerlink" title="把聊天历史当 Checkpoint"></a>把聊天历史当 Checkpoint</h3><p>聊天历史没有完整表达审批状态、已执行副作用、Workspace Revision 和待处理 Tool Call，恢复时容易重复执行或错用环境。</p><h3 id="把-Trace-当-Evidence"><a href="#把-Trace-当-Evidence" class="headerlink" title="把 Trace 当 Evidence"></a>把 Trace 当 Evidence</h3><p>Trace 证明“系统记录了这个动作”，不证明“业务结果真的生效”。Evidence 必须能回到 Artifact 或权威状态。</p><h3 id="只评最终回答"><a href="#只评最终回答" class="headerlink" title="只评最终回答"></a>只评最终回答</h3><p>最终答案正确，不代表过程中没有越权、泄露、无效循环或不可接受的成本。Agent Eval 必须覆盖 Trajectory 和外部结果。</p><h3 id="一开始追求通用编排平台"><a href="#一开始追求通用编排平台" class="headerlink" title="一开始追求通用编排平台"></a>一开始追求通用编排平台</h3><p>没有真实 Case 时，通用节点、DSL 和图编辑器只会提前固化错误抽象。先用 20–30 个真实任务逼出稳定契约，再抽象框架。</p><h2 id="要不要用-我的判断框架"><a href="#要不要用-我的判断框架" class="headerlink" title="要不要用 &#x2F; 我的判断框架"></a>要不要用 &#x2F; 我的判断框架</h2><h3 id="值得建设"><a href="#值得建设" class="headerlink" title="值得建设"></a>值得建设</h3><ul><li>Agent 会修改代码、发布、退款、发消息或操作业务系统；</li><li>单次任务长、成本高，中断后不能从头开始；</li><li>需要人工审批、权限隔离、审计或合规；</li><li>需要比较多个 Model、Provider 或 Agent Runtime；</li><li>团队已经遇到“Demo 能跑，线上偶发失败却解释不清”的问题。</li></ul><h3 id="可以再等"><a href="#可以再等" class="headerlink" title="可以再等"></a>可以再等</h3><ul><li>只是单轮问答或内部 POC；</li><li>没有外部副作用，失败后整次重试成本很低；</li><li>需求和 Tool 还在快速变化，连成功标准都没有定义；</li><li>当前瓶颈是基础能力，而不是稳定性、治理或评测。</li></ul><h3 id="重点关注"><a href="#重点关注" class="headerlink" title="重点关注"></a>重点关注</h3><p>如果现在只能做一件事，我不会先引入 React Flow，也不会先造一个通用 Graph Engine。我会先建立统一 <code>runId</code> 和 Event Contract，让 Tool Call、Checkpoint、Artifact、Evidence 与 Eval Result 能围绕同一次运行串起来。</p><p>这条链一旦成立，后面接 OpenAI Agents SDK、LangGraph、Temporal 或其他 Runtime 都只是适配问题；这条链没有成立，框架越多，只会制造更多无法解释的状态。</p><p>我对 Agent Runtime &#x2F; Harness 的最终判断是：它不是一个新潮名词，也不该被包装成“更高级的 Workflow”。它真正有价值的地方，是把概率性的 Agent 包进确定性的工程边界里——<strong>让每次执行有身份，让每个副作用受控制，让每次中断能继续，让每个完成声明有证据，让每次升级都能被测量。</strong></p><p>这才是 Agent 工程开始进入 AI Infra 的分界线。</p><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><ul><li><a href="https://openai.github.io/openai-agents-js/guides/tracing/">OpenAI Agents SDK：Tracing</a></li><li><a href="https://openai.github.io/openai-agents-js/guides/human-in-the-loop/">OpenAI Agents SDK：Human-in-the-loop</a></li><li><a href="https://developers.openai.com/api/docs/guides/evaluation-best-practices">OpenAI：Evaluation best practices</a></li><li><a href="https://docs.langchain.com/oss/javascript/langgraph/persistence">LangGraph：Persistence</a></li><li><a href="https://docs.langchain.com/langsmith/trajectory-evals">LangSmith：Agent trajectory evaluations</a></li><li><a href="https://docs.temporal.io/">Temporal：Durable Execution</a></li><li><a href="https://www.anthropic.com/engineering/demystifying-evals-for-ai-agents">Anthropic：Demystifying evals for AI agents</a></li></ul>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/25de2ec09687/</id>
    <link href="https://blog.zisheng.pro/posts/25de2ec09687/"/>
    <published>2026-08-02T14:56:11.000Z</published>
    <summary>Agent 真正进入生产后，难点不再是能否调用模型和工具，而是结果能否验证、失败能否恢复、能力能否持续评测。本文从前端转 Agent/AI Infra 的视角，拆解 Runtime 与 Harness 的边界、核心对象、恢复等级、证据链和评测闭环，并给出可分阶段实施的工程方案。</summary>
    <title>如何构建可验证、可恢复、可评测的 Agent Runtime 与 Harness</title>
    <updated>2026-09-18T08:15:28.640Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="AI Infra" scheme="https://blog.zisheng.pro/categories/AI-Infra/"/>
    <category term="AI Infra" scheme="https://blog.zisheng.pro/tags/AI-Infra/"/>
    <category term="Agent" scheme="https://blog.zisheng.pro/tags/Agent/"/>
    <category term="MCP" scheme="https://blog.zisheng.pro/tags/MCP/"/>
    <category term="Domain Model" scheme="https://blog.zisheng.pro/tags/Domain-Model/"/>
    <category term="DDD" scheme="https://blog.zisheng.pro/tags/DDD/"/>
    <category term="Evidence" scheme="https://blog.zisheng.pro/tags/Evidence/"/>
    <category term="A2A" scheme="https://blog.zisheng.pro/tags/A2A/"/>
    <content>
      <![CDATA[<p><img src="https://blog.zisheng.pro/images/agent-domain-model.webp" alt="Agent 系统的一级领域对象：Agent、Task、Run、Artifact、Evidence 与 Evaluation 的关系"></p><p><em>一套跨框架的 Agent 领域模型：Agent 承担责任，Task 描述工作，Run 记录执行，Artifact 承载结果，Evidence 和 Evaluation 决定结果是否可信。</em></p><p>最近我反复遇到 <code>Evidence</code>。它出现在知识检索、Agent 执行、评测、审计和业务验收中，与 <code>Agent</code>、<code>Task</code>、<code>Run</code>、<code>Memory</code>、<code>Policy</code> 并列。问题随之而来：这些词到底是不是同一层概念？一个 Agent 系统真正需要维护哪些一级领域对象？</p><p>如果直接从某个 SDK 抄类名，答案会随着框架切换而变化。OpenAI Agents SDK 强调 Agent、Runner、Tools、Handoffs、Guardrails 和 Sessions；A2A 暴露 Agent Card、Task、Message、Artifact；MCP 提供 Tools、Resources、Prompts；LangGraph 围绕 State、Thread、Checkpoint 和 Memory 组织运行。它们都合理，但它们描述的是不同边界。</p><p>我更关心跨框架后仍然成立的业务语义。因为只有这些对象，才值得进入 Agent Platform 的数据库、API、权限、状态机和审计模型。</p><h2 id="一句话总结"><a href="#一句话总结" class="headerlink" title="一句话总结"></a>一句话总结</h2><p>我会把 Agent 系统的核心领域模型收敛为十二个对象：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">Actor      Agent / Identity</span><br><span class="line">Work       Goal / Task / Run</span><br><span class="line">Execution  Capability / Context</span><br><span class="line">Control    Policy</span><br><span class="line">Outcome    Artifact / Claim</span><br><span class="line">Trust      Evidence / Evaluation</span><br></pre></td></tr></table></figure><p>它们组成一条完整链路：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">Identity 授权 Agent</span><br><span class="line">Agent 为 Goal 承接 Task</span><br><span class="line">Task 产生一次或多次 Run</span><br><span class="line">Run 在 Policy 约束下使用 Capability 和 Context</span><br><span class="line">Run 产出 Artifact，并提出 Claim</span><br><span class="line">Evidence 支撑 Claim</span><br><span class="line">Evaluation 根据验收标准作出判断</span><br></pre></td></tr></table></figure><p><code>Tool</code>、<code>Prompt</code>、<code>Model</code>、<code>Runtime</code>、<code>Message</code> 和 <code>Trace</code> 同样重要，但默认属于实现、协议或可观测性对象，不必全部提升为一级领域对象。</p><h2 id="“一级领域对象”不是现成标准"><a href="#“一级领域对象”不是现成标准" class="headerlink" title="“一级领域对象”不是现成标准"></a>“一级领域对象”不是现成标准</h2><p>“一级领域对象”并不是 Agent 行业已经发布的正式术语。这里采用的是领域建模视角：当一个对象需要被系统长期、独立地管理，就把它当作 First-class Domain Object，而不只是某张配置表里的字段。</p><p>我用五个问题判断：</p><ol><li>它是否拥有稳定 ID，能被 API、权限和审计记录独立引用？</li><li>它是否有独立生命周期，而不是伴随一次函数调用即消失？</li><li>它是否承载业务规则或状态转换？</li><li>它是否会被多个模块共同使用，而不属于某个框架的内部实现？</li><li>更换 Model、Runtime 或 Agent Framework 后，它是否仍然存在？</li></ol><p>例如 <code>Run</code> 会被暂停、恢复、取消、重试和验收，显然应当独立建模。某个模型的 <code>temperature</code> 不具备这些特征，它只是配置值。</p><p>这也意味着“一级”不是永恒不变的。<code>Workspace</code> 对普通客服 Agent 可能只是 Context 的一个字段，对 Coding Agent 却可能有独立权限、Revision、锁和生命周期，此时就应升级为一级对象。</p><h2 id="先从跨框架的共同部分看"><a href="#先从跨框架的共同部分看" class="headerlink" title="先从跨框架的共同部分看"></a>先从跨框架的共同部分看</h2><h3 id="W3C-PROV-给出了最小骨架"><a href="#W3C-PROV-给出了最小骨架" class="headerlink" title="W3C PROV 给出了最小骨架"></a>W3C PROV 给出了最小骨架</h3><p>Agent 领域不是从零开始发明。W3C 的 <a href="https://www.w3.org/TR/prov-o/">PROV-O</a> 用三个起点描述可追溯系统：</p><ul><li><code>Agent</code>：对某项活动或实体承担责任的主体；</li><li><code>Activity</code>：在一段时间内发生、使用或生成实体的活动；</li><li><code>Entity</code>：具有相对固定特征的物理、数字或概念对象。</li></ul><p>映射到 Agent 系统里，最朴素的关系就是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">Agent 负责 Run</span><br><span class="line">Run 使用 Context / Resource</span><br><span class="line">Run 生成 Artifact / Evidence</span><br></pre></td></tr></table></figure><p>这个骨架解释了为什么 Evidence 不属于“知识库技术”或者“日志技术”本身。Evidence 是带有来源、生成活动和责任主体的 Entity，价值来自它与 Claim、Run、Agent 的关系。</p><h3 id="A2A-把-Task-和-Artifact-提升为协议对象"><a href="#A2A-把-Task-和-Artifact-提升为协议对象" class="headerlink" title="A2A 把 Task 和 Artifact 提升为协议对象"></a>A2A 把 Task 和 Artifact 提升为协议对象</h3><p><a href="https://a2a-protocol.org/latest/specification/">A2A 1.0 规范</a>把 <code>Task</code> 定义为有唯一 ID、状态和生命周期的基本工作单元；Agent 执行后产生的文档、图片或结构化数据进入 <code>Artifact</code>。<code>Message</code> 负责通信，<code>Context</code> 负责关联一组 Task 和 Message。</p><p>这个区分非常关键：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">Message：一次沟通</span><br><span class="line">Task：需要跟踪的工作</span><br><span class="line">Artifact：工作产生的交付物</span><br></pre></td></tr></table></figure><p>聊天记录不能代替任务状态，最终回复也不能自动等于正式交付物。</p><h3 id="MCP-描述能力接口，不负责完整业务模型"><a href="#MCP-描述能力接口，不负责完整业务模型" class="headerlink" title="MCP 描述能力接口，不负责完整业务模型"></a>MCP 描述能力接口，不负责完整业务模型</h3><p><a href="https://modelcontextprotocol.io/docs/learn/architecture">MCP 架构</a>定义了三个核心 Server Primitive：</p><ul><li>Tools：执行动作；</li><li>Resources：向模型提供上下文数据；</li><li>Prompts：提供可复用的交互模板。</li></ul><p>它回答的是“Agent 如何接入能力与资源”，不负责定义 Goal、Task、Run、Evidence 或 Evaluation。A2A 规范也把两者定位为互补协议：MCP 面向工具和资源连接，A2A 面向 Agent 之间的任务协作。</p><p>所以 <code>MCP Server</code> 不应成为业务领域模型的中心。它是 Capability 和 Resource 的一种接入方式。</p><h3 id="Agent-Framework-暴露的是-Runtime-Primitive"><a href="#Agent-Framework-暴露的是-Runtime-Primitive" class="headerlink" title="Agent Framework 暴露的是 Runtime Primitive"></a>Agent Framework 暴露的是 Runtime Primitive</h3><p><a href="https://openai.github.io/openai-agents-python/">OpenAI Agents SDK</a>把 Agent 定义为带有 Instructions、Tools、Handoffs 和 Guardrails 的 LLM，并由 Runner 执行 Agent Loop。它还提供 Session、Tracing、Human-in-the-loop 和可恢复的 Run State。</p><p><a href="https://www.anthropic.com/engineering/building-effective-agents">Anthropic 的 Agent 架构总结</a>则把 Agent 与 Workflow 分开：Workflow 走预定义代码路径，Agent 动态决定过程和工具使用。其基础构件是带 Retrieval、Tools、Memory 的 Augmented LLM；执行过程中，Agent 需要持续从工具结果、代码执行等环境反馈中取得 Ground Truth。</p><p>这些材料共同证明 Agent、Run、Capability、Context、Memory、Policy 和 Evaluation 的必要性，但 SDK 里的每个类并不等于领域对象。<code>Runner</code> 可以被替换，Task 和 Evidence 不能因为换了 Runtime 就消失。</p><p>把几套体系放在同一张表里，边界会更清楚：</p><table><thead><tr><th>来源</th><th>它显式建模的对象</th><th>对领域模型的启发</th></tr></thead><tbody><tr><td>W3C PROV</td><td>Agent、Activity、Entity、Attribution、Derivation</td><td>责任、活动、产物和来源关系是追溯骨架</td></tr><tr><td>OpenAI Agents SDK</td><td>Agent、Tools、Handoffs、Guardrails、Session、Run State、Trace</td><td>Agent Loop 之外还需要运行状态、控制与上下文</td></tr><tr><td>Anthropic</td><td>Agent、Workflow、Tools、Retrieval、Memory、Ground Truth、Evaluator</td><td>自主决策必须接受环境反馈与独立评价</td></tr><tr><td>A2A</td><td>Agent Card、Task、Message、Context、Artifact</td><td>通信、工作、上下文和交付物需要分开</td></tr><tr><td>MCP</td><td>Tools、Resources、Prompts</td><td>能力、资源和交互模板属于接入协议边界</td></tr><tr><td>LangGraph</td><td>State、Thread、Checkpoint、Memory</td><td>长运行需要状态快照、恢复与跨会话记忆</td></tr></tbody></table><p>这张表不是投票结果。某个概念在六套体系里都出现，不代表它一定是一级对象；反过来，Claim 没有被多数 Agent SDK 显式建模，也不代表生产系统可以省略它。最终仍要回到 ID、生命周期、业务规则和跨模块关系。</p><h2 id="十二个核心领域对象"><a href="#十二个核心领域对象" class="headerlink" title="十二个核心领域对象"></a>十二个核心领域对象</h2><h3 id="1-Agent：承担工作责任的持续主体"><a href="#1-Agent：承担工作责任的持续主体" class="headerlink" title="1. Agent：承担工作责任的持续主体"></a>1. Agent：承担工作责任的持续主体</h3><p><code>Agent</code> 不应只是一段 System Prompt，也不只是“模型 + 工具列表”。它代表一个可被分配工作、拥有能力边界和版本、能够产生运行记录与结果责任的持续主体。</p><p>一个可管理的 Agent 至少要回答：</p><ul><li>当前有效版本是什么；</li><li>可以承接哪些工作；</li><li>以谁的身份行动；</li><li>允许使用哪些能力；</li><li>失败、暂停或升级时由谁负责。</li></ul><p>模型只是 Agent 的实现组件。把模型名直接当 Agent ID，会让版本、权限、责任和历史运行全部粘在供应商配置上。</p><h3 id="2-Identity：谁在行动，代表谁行动"><a href="#2-Identity：谁在行动，代表谁行动" class="headerlink" title="2. Identity：谁在行动，代表谁行动"></a>2. Identity：谁在行动，代表谁行动</h3><p><code>Identity</code> 解决认证与责任归属。Agent 可以有自己的服务身份，也可能代表某个用户、团队或组织发起操作。</p><p>需要区分：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">Agent Identity：哪个 Agent 在执行</span><br><span class="line">Principal：它代表谁行动</span><br><span class="line">Credential：本次连接使用什么凭证</span><br></pre></td></tr></table></figure><p>Credential 应短期、可轮换，Identity 和 Principal 则需要进入审计关系。否则系统只能知道“某个 Token 调用了接口”，无法说明谁授权了哪个 Agent 做什么。</p><h3 id="3-Goal：希望世界达到什么状态"><a href="#3-Goal：希望世界达到什么状态" class="headerlink" title="3. Goal：希望世界达到什么状态"></a>3. Goal：希望世界达到什么状态</h3><p><code>Goal</code> 是期望结果，不是执行步骤。例如“让新版页面可被用户访问”是 Goal，“修改配置并运行发布命令”只是候选计划。</p><p>Goal 值得独立存在，是因为同一个 Goal 可能被拆成多个 Task，也可能在执行过程中改变计划，但成功条件不应随 Agent 的临时推理漂移。</p><p>一个可用 Goal 至少包含目标状态、作用范围、成功指标、截止条件和责任人。</p><h3 id="4-Task：可分配、可跟踪的工作单元"><a href="#4-Task：可分配、可跟踪的工作单元" class="headerlink" title="4. Task：可分配、可跟踪的工作单元"></a>4. Task：可分配、可跟踪的工作单元</h3><p><code>Task</code> 把 Goal 变成可以交给 Agent 的工作。A2A 把它作为协议核心，是因为长任务必须有稳定 ID、状态、历史和结果。</p><p>Task 与 Goal 的区别是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">Goal：为什么做、做到什么程度</span><br><span class="line">Task：谁承接哪一块工作、当前处于什么状态</span><br></pre></td></tr></table></figure><p>多 Agent 系统里的 Delegation 或 Handoff，本质上是在 Agent 与 Task 之间改变责任关系，而不只是转发一条 Message。</p><h3 id="5-Run：Task-的一次执行尝试"><a href="#5-Run：Task-的一次执行尝试" class="headerlink" title="5. Run：Task 的一次执行尝试"></a>5. Run：Task 的一次执行尝试</h3><p><code>Run</code> 是最容易被漏掉、也最需要独立建模的对象。同一个 Task 可能第一次执行超时，第二次恢复成功，第三次为了验证重新运行。它们属于一个 Task，却不是同一次 Run。</p><p>Run 应记录：</p><ul><li>使用的 Agent Version、Context Snapshot 和 Policy Version；</li><li>开始、暂停、恢复、取消和结束状态；</li><li>Tool Call、Handoff、异常和人工决定；</li><li>Usage、成本、产物与证据引用。</li></ul><p>OpenAI Agents SDK 的可序列化 <code>RunState</code> 和 LangGraph 的 Checkpoint 都在解决“运行如何暂停并恢复”，但领域层不应绑定它们的存储格式。</p><h3 id="6-Capability：Agent-能完成哪类动作"><a href="#6-Capability：Agent-能完成哪类动作" class="headerlink" title="6. Capability：Agent 能完成哪类动作"></a>6. Capability：Agent 能完成哪类动作</h3><p><code>Capability</code> 描述业务能力，例如“查询订单”“修改代码”“发布静态站点”。<code>Tool</code>、<code>Skill</code>、<code>Model</code>、<code>Runtime</code> 和 MCP Server 是能力的不同实现或依赖。</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">Capability：能做什么</span><br><span class="line">Tool：通过什么接口做</span><br><span class="line">Skill：按什么方法做</span><br><span class="line">Model：由什么推理能力参与</span><br><span class="line">Runtime：在哪个执行引擎里做</span><br></pre></td></tr></table></figure><p>把 Capability 与实现分开，才能替换工具和 Runtime，同时保留任务路由、权限、评测与历史成功率。</p><h3 id="7-Context：本次运行获准看到的工作集"><a href="#7-Context：本次运行获准看到的工作集" class="headerlink" title="7. Context：本次运行获准看到的工作集"></a>7. Context：本次运行获准看到的工作集</h3><p><code>Context</code> 不是“把所有资料塞进上下文窗口”，而是某次 Run 使用的信息边界。它可以引用 Conversation、Workspace、Resource、知识片段、环境状态和用户输入。</p><p>Context 最好以 Snapshot 或明确 Revision 固定下来。否则运行结束后无法回答：Agent 当时看到了哪个版本的文件、哪段知识、哪个系统状态？</p><p><a href="https://docs.langchain.com/oss/python/concepts/memory">LangGraph 的 Memory 文档</a>把短期记忆放在线程状态中，并通过 Checkpoint 持久化；跨线程的长期记忆则单独存储。这个划分说明 Memory 不是 Context 的同义词：Context 是本次运行实际使用的集合，Memory 是可能被召回到未来 Context 的持久状态。</p><h3 id="8-Policy：什么情况下允许怎么做"><a href="#8-Policy：什么情况下允许怎么做" class="headerlink" title="8. Policy：什么情况下允许怎么做"></a>8. Policy：什么情况下允许怎么做</h3><p><code>Policy</code> 是对 Agent、Task、Capability、Context 和 Run 的约束规则，例如：</p><ul><li>哪些数据可以发送给哪个 Model；</li><li>哪些 Tool 只能在指定 Workspace 使用；</li><li>哪些动作必须人工审批；</li><li>超过什么预算停止运行；</li><li>哪些 Evidence 缺失时不得宣布完成。</li></ul><p>Guardrail 是 Policy 的一种执行机制，不是 Policy 本身。Prompt 里的“请不要执行危险操作”也不能替代可版本化、可判定、可审计的 Policy。</p><h3 id="9-Artifact：Agent-实际交付了什么"><a href="#9-Artifact：Agent-实际交付了什么" class="headerlink" title="9. Artifact：Agent 实际交付了什么"></a>9. Artifact：Agent 实际交付了什么</h3><p><code>Artifact</code> 是 Run 产生的持久结果，例如代码 Diff、文档、报表、图片、构建包、结构化数据或发布清单。A2A 明确把 Artifact 与 Message 分开，这个设计值得保留。</p><p>一段“已经修改完成”的聊天回复不是 Artifact；被保存、定位、版本化并交给下游使用的文件或数据才是。</p><h3 id="10-Claim：系统正在声称什么事实"><a href="#10-Claim：系统正在声称什么事实" class="headerlink" title="10. Claim：系统正在声称什么事实"></a>10. Claim：系统正在声称什么事实</h3><p>Evidence 经常被直接建成附件列表，却没有说明它证明什么。缺少 <code>Claim</code>，Evidence 就只是材料堆积。</p><p>Claim 是一个可验证断言，例如：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><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><p>Claim 需要作用对象、范围、Revision、时间和声明主体。同一句“发布成功”，在测试环境和生产环境是两个不同 Claim。</p><h3 id="11-Evidence：支撑或反驳-Claim-的可验证事实"><a href="#11-Evidence：支撑或反驳-Claim-的可验证事实" class="headerlink" title="11. Evidence：支撑或反驳 Claim 的可验证事实"></a>11. Evidence：支撑或反驳 Claim 的可验证事实</h3><p><code>Evidence</code> 属于 Trust &amp; Verification 领域。它把 Claim 与 Source、Run、Artifact、Observation、Verifier 和时间连接起来。</p><p>一条合格 Evidence 至少需要：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;claimId&quot;</span><span class="punctuation">:</span> <span class="string">&quot;claim_production_ready&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;sourceRef&quot;</span><span class="punctuation">:</span> <span class="string">&quot;check://production/health&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;producedByRunId&quot;</span><span class="punctuation">:</span> <span class="string">&quot;run_20260802_01&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;observedAt&quot;</span><span class="punctuation">:</span> <span class="string">&quot;2026-08-02T17:00:00+08:00&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;revision&quot;</span><span class="punctuation">:</span> <span class="string">&quot;release_42&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;integrity&quot;</span><span class="punctuation">:</span> <span class="string">&quot;sha256:...&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;verifier&quot;</span><span class="punctuation">:</span> <span class="string">&quot;health-check-v3&quot;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p>Trace、日志、截图和测试输出只能算 Evidence Candidate。只有当它被绑定到具体 Claim，并说明来源、范围和验证方式后，才成为可以用于验收的 Evidence。</p><h3 id="12-Evaluation：依据标准作出的判断"><a href="#12-Evaluation：依据标准作出的判断" class="headerlink" title="12. Evaluation：依据标准作出的判断"></a>12. Evaluation：依据标准作出的判断</h3><p><code>Evaluation</code> 消费 Artifact、Claim 和 Evidence，根据 Criteria 给出 Verdict、Score、Explanation 或 Risk。</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">Evidence：生产接口在 17:00 返回 200</span><br><span class="line">Criteria：连续三次健康检查成功，响应体版本为 release_42</span><br><span class="line">Evaluation：PASS / FAIL / INCONCLUSIVE</span><br></pre></td></tr></table></figure><p>Anthropic 的 Evaluator-optimizer 模式和 OpenAI Agents SDK 的 Guardrail &#x2F; Eval 能力都说明“生成”和“判断”需要分离。Evidence 是事实材料，Evaluation 是基于规则或评审者作出的判断，两者不能混成一个分数字段。</p><h2 id="哪些对象需要按场景升级为一级"><a href="#哪些对象需要按场景升级为一级" class="headerlink" title="哪些对象需要按场景升级为一级"></a>哪些对象需要按场景升级为一级</h2><p>除了十二个核心对象，还有一组条件性对象。是否独立建模，取决于业务复杂度。</p><table><thead><tr><th>候选对象</th><th>什么时候升级为一级对象</th><th>否则放在哪里</th></tr></thead><tbody><tr><td>Memory</td><td>跨 Run 保存、可纠错、可删除、需要来源与保留策略</td><td>Context 的引用或 Session State</td></tr><tr><td>Approval</td><td>运行会跨小时或跨天暂停，需要记录批准人、范围与失效条件</td><td>Run Event &#x2F; Policy Decision</td></tr><tr><td>Workspace</td><td>有独立权限、Revision、文件锁、隔离和生命周期</td><td>Context Scope</td></tr><tr><td>Handoff &#x2F; Delegation</td><td>多 Agent 间需要追踪责任转移、输入输出契约和超时</td><td>Task 的关系事件</td></tr><tr><td>Trigger &#x2F; Schedule</td><td>周期任务、Webhook、事件订阅需要独立启停和重放</td><td>Task 创建来源</td></tr><tr><td>Budget &#x2F; Usage</td><td>存在额度分配、成本归属、限额和结算</td><td>Run Metric</td></tr><tr><td>Version &#x2F; Release</td><td>Agent 配置需要灰度、回滚、对比评测和生产追踪</td><td>Agent 的版本字段</td></tr><tr><td>Receipt</td><td>需要对外签发、不可变、可撤销或具备法律&#x2F;业务效力</td><td>Claim、Evidence、Evaluation 的聚合视图</td></tr></tbody></table><p>例如 <a href="https://openai.github.io/openai-agents-python/human_in_the_loop/">OpenAI Agents SDK 的 Human-in-the-loop</a>允许 Run 因 Tool Approval 暂停、序列化并在之后恢复。在短对话里，Approval 可以只是一次事件；在跨天的财务或发布流程里，它需要自己的 ID、审批人、Decision、Scope、Policy Version 和 Expiry，此时就应该成为一级对象。</p><h2 id="哪些常见名词默认不该升为一级"><a href="#哪些常见名词默认不该升为一级" class="headerlink" title="哪些常见名词默认不该升为一级"></a>哪些常见名词默认不该升为一级</h2><h3 id="Prompt"><a href="#Prompt" class="headerlink" title="Prompt"></a>Prompt</h3><p>Prompt 是 Agent Version 或 Capability Implementation 的组成部分。只有当平台专门做 Prompt 资产管理、评测、发布和回滚时，它才需要独立生命周期。</p><h3 id="Model-与-Runtime"><a href="#Model-与-Runtime" class="headerlink" title="Model 与 Runtime"></a>Model 与 Runtime</h3><p>它们是可替换执行依赖。模型路由平台会把 Model Deployment 当一级对象，通用 Agent 产品则应避免让业务 Task 直接绑定供应商模型。</p><h3 id="Tool-与-Skill"><a href="#Tool-与-Skill" class="headerlink" title="Tool 与 Skill"></a>Tool 与 Skill</h3><p>Tool 是调用接口，Skill 是完成任务的方法。它们可以作为 Capability 的实现对象独立管理，但业务路由首先应该依赖 Capability，而不是某个函数名或 Skill 文件名。</p><h3 id="Message"><a href="#Message" class="headerlink" title="Message"></a>Message</h3><p>Message 是通信协议对象，不等于 Task、Context 或 Artifact。一串消息可以产生多个 Task，一个长 Task 也可以跨越多轮 Message。</p><h3 id="Event、Log-与-Trace"><a href="#Event、Log-与-Trace" class="headerlink" title="Event、Log 与 Trace"></a>Event、Log 与 Trace</h3><p>它们记录运行轨迹，主要属于 Observability。OpenAI Agents SDK 的 Trace 会记录 Model Generation、Tool Call、Handoff 和 Guardrail 等 Span，这对排障很重要，但 Trace 不自动证明业务结果成立。</p><h3 id="Workflow-与-Graph"><a href="#Workflow-与-Graph" class="headerlink" title="Workflow 与 Graph"></a>Workflow 与 Graph</h3><p>Workflow 是编排定义，Graph 是一种结构表达。以 Workflow 为产品核心的平台可以把它作为一级对象；以开放式 Agent 为核心的系统，更适合把它作为 Capability 或 Execution Plan 的实现。</p><h2 id="一套可落地的最小关系模型"><a href="#一套可落地的最小关系模型" class="headerlink" title="一套可落地的最小关系模型"></a>一套可落地的最小关系模型</h2><p>如果从数据库和 API 开始，我会先建立下面几条稳定关系，而不是先画复杂的多 Agent 拓扑：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">Agent --has--&gt; AgentVersion</span><br><span class="line">Identity --authorizes--&gt; Agent</span><br><span class="line">Goal --decomposes_into--&gt; Task</span><br><span class="line">Agent --assigned_to--&gt; Task</span><br><span class="line">Task --attempted_by--&gt; Run</span><br><span class="line">Run --uses--&gt; Capability</span><br><span class="line">Run --reads--&gt; ContextSnapshot</span><br><span class="line">Policy --governs--&gt; Run</span><br><span class="line">Run --produces--&gt; Artifact</span><br><span class="line">Run --asserts--&gt; Claim</span><br><span class="line">Evidence --supports_or_refutes--&gt; Claim</span><br><span class="line">Evaluation --judges--&gt; Claim</span><br></pre></td></tr></table></figure><p>对应的生命周期应明确区分：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">Task:  created → assigned → active → completed / failed / canceled</span><br><span class="line">Run:   queued → running → waiting_approval → paused → completed / failed / interrupted</span><br><span class="line">Claim: proposed → supported / refuted / insufficient</span><br><span class="line">Eval:  pending → pass / fail / inconclusive</span><br></pre></td></tr></table></figure><p>这里最重要的不是状态名称，而是不要用一个 <code>status = done</code> 同时代表“Agent 停止运行”“文件已经生成”“测试通过”“代码已经发布”“业务已经验收”。这些是不同对象上的不同 Claim。</p><h2 id="应该按什么顺序建设"><a href="#应该按什么顺序建设" class="headerlink" title="应该按什么顺序建设"></a>应该按什么顺序建设</h2><h3 id="第一阶段：先让工作可运行"><a href="#第一阶段：先让工作可运行" class="headerlink" title="第一阶段：先让工作可运行"></a>第一阶段：先让工作可运行</h3><p>最小闭环需要：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Agent + Task + Run + Capability + Context + Artifact</span><br></pre></td></tr></table></figure><p>这一阶段解决“谁在什么上下文里，用什么能力，完成了哪项工作，产出了什么”。</p><h3 id="第二阶段：让结果可信"><a href="#第二阶段：让结果可信" class="headerlink" title="第二阶段：让结果可信"></a>第二阶段：让结果可信</h3><p>进入真实业务前补齐：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Identity + Policy + Claim + Evidence + Evaluation</span><br></pre></td></tr></table></figure><p>这一阶段解决权限、责任、结果边界与验真。系统只有 Trace，没有 Claim 和 Evidence Model，仍然无法可靠验收。</p><h3 id="第三阶段：处理长期与协作"><a href="#第三阶段：处理长期与协作" class="headerlink" title="第三阶段：处理长期与协作"></a>第三阶段：处理长期与协作</h3><p>当任务跨 Session、跨 Agent、跨天运行时，再把以下对象提升出来：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Memory + Approval + Workspace + Handoff + Trigger + Budget + Version</span><br></pre></td></tr></table></figure><p>它们不应该为了架构图完整而提前建设。升级信号应来自真实问题：上下文无法恢复、责任转移说不清、审批失效、成本无法归属、版本无法回滚。</p><h2 id="我的判断框架"><a href="#我的判断框架" class="headerlink" title="我的判断框架"></a>我的判断框架</h2><p>遇到一个新的 Agent 名词，我会按下面的顺序判断：</p><ol><li>它描述的是业务世界，还是某个 SDK 的实现方式？</li><li>它是否拥有独立 ID、生命周期和业务规则？</li><li>多个 Task、Run 或 Agent 是否需要共同引用它？</li><li>更换 Model、Runtime、Protocol 后，它是否仍然成立？</li><li>不独立建模会不会导致权限、恢复、审计或验收说不清？</li></ol><p>五个问题大多回答“是”，就把它提升为一级领域对象；否则先作为属性、事件、协议对象或实现对象存在。</p><p>这套模型给我的最大帮助，是把 Agent Platform 从“模型、Prompt、Tool 的配置集合”重新理解成责任系统：Agent 承担工作，Run 改变环境，Artifact 承载结果，Claim 表达结论，Evidence 与 Evaluation 决定我们能否相信它。</p><p>真正进入生产后，模型能力会持续变化，Runtime 也可以替换。长期稳定的资产，是这些领域对象及其关系。</p><h2 id="参考索引"><a href="#参考索引" class="headerlink" title="参考索引"></a>参考索引</h2><p>本文优先采用协议规范、标准本体和框架官方文档。下面列出的不是泛泛的“相关阅读”，而是实际参与本文领域对象归纳和边界判断的研究依据。检索日期：2026-08-02。</p><table><thead><tr><th>范畴</th><th>参考资料</th><th>本文采用的依据</th><th>使用边界</th></tr></thead><tbody><tr><td>Provenance &#x2F; Trust</td><td><a href="https://www.w3.org/TR/prov-o/">W3C PROV-O：The PROV Ontology</a></td><td>Agent、Activity、Entity 组成的最小 Provenance 骨架，以及归属、生成、使用等关系</td><td>PROV-O 描述来源与过程，不直接给出 Agent 产品的完整业务模型</td></tr><tr><td>Agent Interoperability</td><td><a href="https://a2a-protocol.org/latest/specification/">A2A Protocol Specification 1.0</a></td><td>Agent Card、Task、Message、Artifact、Context 等跨 Agent 协作对象的定义与生命周期</td><td>A2A 是互操作协议；本文没有把所有协议对象都提升为一级领域对象</td></tr><tr><td>Capability Interface</td><td><a href="https://modelcontextprotocol.io/docs/learn/architecture">Model Context Protocol：Architecture Overview</a></td><td>Tools、Resources、Prompts 的职责划分，用于区分 Capability、Context 与实现接口</td><td>MCP 解决 Host、Client、Server 之间的上下文与能力交换，不等于完整 Agent 领域模型</td></tr><tr><td>Agent Runtime</td><td><a href="https://openai.github.io/openai-agents-python/">OpenAI Agents SDK Documentation</a></td><td>Agent、Runner、Tools、Handoffs、Guardrails、Sessions、Tracing 等 Runtime Primitive</td><td>SDK 类名会随框架变化，因此只用于提取稳定语义，不直接照搬为业务对象</td></tr><tr><td>Approval &#x2F; Resume</td><td><a href="https://openai.github.io/openai-agents-python/human_in_the_loop/">OpenAI Agents SDK：Human-in-the-loop</a></td><td>Tool Approval、Run 暂停、序列化和恢复机制，用于判断 Approval 何时应拥有独立生命周期</td><td>短时审批仍可保留为 Run Event；跨时段、可失效审批才建议升级为一级对象</td></tr><tr><td>Agent Architecture</td><td><a href="https://www.anthropic.com/engineering/building-effective-agents">Anthropic：Building Effective AI Agents</a></td><td>Workflow 与 Agent 的区别、Tool Use、Evaluator-optimizer 等编排模式</td><td>这是一组工程模式，不是领域对象标准；本文主要用它校验 Workflow、Graph、Evaluation 的边界</td></tr><tr><td>State &#x2F; Memory</td><td><a href="https://docs.langchain.com/oss/python/concepts/memory">LangGraph Documentation：Memory</a></td><td>Short-term Memory、Long-term Memory、Thread、Checkpoint 的作用域与持久化边界</td><td>Memory 是否成为一级对象，取决于是否跨 Run 管理并具备来源、纠错、删除和保留策略</td></tr></tbody></table><h3 id="综合方法与结论边界"><a href="#综合方法与结论边界" class="headerlink" title="综合方法与结论边界"></a>综合方法与结论边界</h3><p>行业目前没有一份统一规范宣布“Agent 系统必须包含十二个一级领域对象”。本文的十二对象模型，是把上述资料放进同一套判据后得到的领域建模结论：对象需要具备相对稳定的 ID、生命周期、业务规则和跨对象关系，并且在替换 Model、Runtime 或 Protocol 后仍然成立。</p><p>因此，文中的分类需要区分三种性质：</p><ol><li><strong>规范直接定义</strong>：例如 W3C PROV 的 Agent、Activity、Entity，A2A 的 Task 与 Artifact。</li><li><strong>框架暴露的 Runtime Primitive</strong>：例如 Runner、Session、Handoff、Guardrail、Checkpoint。</li><li><strong>本文综合推导的领域对象</strong>：例如把 Claim 与 Evidence 分离，把 Policy 提升为控制面对象，以及将十二个对象组织为 Actor、Work、Execution、Control、Outcome、Trust 六组。</li></ol><p>这也意味着，十二个对象是一套用于架构决策的参考模型，不是对任何单一 SDK 或协议的重新命名。</p>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/ef5159a9b8f5/</id>
    <link href="https://blog.zisheng.pro/posts/ef5159a9b8f5/"/>
    <published>2026-08-02T09:20:00.000Z</published>
    <summary>Agent、Task、Run、Tool、Memory、Policy、Evidence 经常被放在同一张架构图里，却不是同一层概念。本文交叉比对 OpenAI Agents SDK、Anthropic、A2A、MCP、LangGraph 与 W3C PROV，给出一级领域对象的判定标准、十二个核心对象及生产化建模顺序。</summary>
    <title>Agent 系统的一级领域对象：从 Task、Run 到 Evidence</title>
    <updated>2026-09-18T08:15:28.629Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="AI Infra" scheme="https://blog.zisheng.pro/categories/AI-Infra/"/>
    <category term="Agent" scheme="https://blog.zisheng.pro/tags/Agent/"/>
    <category term="MCP" scheme="https://blog.zisheng.pro/tags/MCP/"/>
    <category term="AI Coding" scheme="https://blog.zisheng.pro/tags/AI-Coding/"/>
    <category term="Codex" scheme="https://blog.zisheng.pro/tags/Codex/"/>
    <category term="Context7" scheme="https://blog.zisheng.pro/tags/Context7/"/>
    <category term="Serena" scheme="https://blog.zisheng.pro/tags/Serena/"/>
    <category term="Token" scheme="https://blog.zisheng.pro/tags/Token/"/>
    <content>
      <![CDATA[<p>最近给 Codex 配了一个组合：Context7、Serena，再配合合理拆 Session。</p><p>这套组合很容易被描述成“省 token 三件套”。但我觉得这个说法不够准确，甚至有一点误导：MCP 不是装得越多越省，Session 也不是拆得越细越省。真正起作用的，是把正确的信息送到正确的阶段，尽量不让无关的文件、文档、日志和历史决策进入当前上下文。</p><p>这篇文章用一次真实的前端功能开发流程，把三个工具串起来，也把 token 节省的边界讲清楚。</p><h2 id="一句话总结"><a href="#一句话总结" class="headerlink" title="一句话总结"></a>一句话总结</h2><p>Context7 负责查对外部文档，Serena 负责缩小代码阅读范围，Session 负责切断已经失去价值的历史上下文。</p><p>它们共同优化的不是“工具数量”，而是 <strong>context selection</strong>：这一轮到底应该让 Codex 看什么。</p><p><img src="/images/codex-context-efficiency.webp" alt="Context7、Serena 和 Session 的上下文效率组合"></p><h2 id="先看一次真实工作流"><a href="#先看一次真实工作流" class="headerlink" title="先看一次真实工作流"></a>先看一次真实工作流</h2><p>假设现在要给一个 React + Midway 的企业工作台增加技能录制状态展示，同时补上接口错误处理。</p><p>我不会一上来就让 Codex“直接改好”。更稳的做法是把任务分成三个阶段。</p><h3 id="Session-1：只探索，不改代码"><a href="#Session-1：只探索，不改代码" class="headerlink" title="Session 1：只探索，不改代码"></a>Session 1：只探索，不改代码</h3><p>第一轮只确定入口、调用链和最小修改范围：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">先不要修改代码。</span><br><span class="line"></span><br><span class="line">请定位：</span><br><span class="line">1. 技能录制页面入口</span><br><span class="line">2. 页面使用的 hooks 和 API</span><br><span class="line">3. loading、empty、error 状态目前在哪里处理</span><br><span class="line">4. 最小修改范围和相关调用方</span><br><span class="line"></span><br><span class="line">用代码语义检索定位符号和引用关系。</span><br><span class="line">如果涉及 React、Umi 或 Midway 的版本行为，再查当前版本的官方文档。</span><br><span class="line"></span><br><span class="line">最后只输出：相关文件、调用链、推荐改法和风险。</span><br></pre></td></tr></table></figure><p>这一轮的目标不是产出代码，而是建立一个足够小的工作集。</p><p>Serena 用来找页面、函数、类型和引用关系；Context7 只在需要确认外部框架行为时介入。两者都不应该被用来“把整个项目读一遍”。</p><h3 id="Session-2：只实现已经确定的范围"><a href="#Session-2：只实现已经确定的范围" class="headerlink" title="Session 2：只实现已经确定的范围"></a>Session 2：只实现已经确定的范围</h3><p>探索完成后，我会把结论压缩成一份 handoff，再开始实现：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">背景：</span><br><span class="line">- 页面入口：client/src/pages/SkillRecord/index.tsx</span><br><span class="line">- API：client/src/apis/skill.ts 的 getSkillDetail</span><br><span class="line">- 现有问题：只处理成功态，没有 error/retry</span><br><span class="line">- 目标：补充 loading、empty、error、retry</span><br><span class="line">- 约束：复用现有 UI 组件，不改全局组件</span><br><span class="line"></span><br><span class="line">请直接实现，完成后运行相关检查。</span><br></pre></td></tr></table></figure><p>这里不需要把上一轮的所有探索日志重新粘进来。保留决策、文件、约束和验收标准就够了。</p><h3 id="Session-3：只验证本次改动"><a href="#Session-3：只验证本次改动" class="headerlink" title="Session 3：只验证本次改动"></a>Session 3：只验证本次改动</h3><p>实现完成后，如果出现大量构建或测试输出，可以把验证单独放到新的 Session：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">请只验证本次改动：</span><br><span class="line">1. 检查 TypeScript 类型</span><br><span class="line">2. 检查 loading、empty、error、retry</span><br><span class="line">3. 运行最小范围测试或构建</span><br><span class="line">4. 如果失败，只分析本次改动相关错误</span><br><span class="line">5. 不要重新扫描整个项目</span><br></pre></td></tr></table></figure><p>这三轮不是固定模板。小改动可以一轮完成；当目标、证据和日志明显发生变化时，再切 Session。</p><h2 id="Context7：把“查文档”变成定向查询"><a href="#Context7：把“查文档”变成定向查询" class="headerlink" title="Context7：把“查文档”变成定向查询"></a>Context7：把“查文档”变成定向查询</h2><p>Context7 是文档检索 MCP。它解决的问题是：</p><blockquote><p>这个 API 在当前版本到底怎么用？</p></blockquote><p>它适合查：</p><table><thead><tr><th>问题</th><th>适合查的内容</th></tr></thead><tbody><tr><td>React 行为</td><td>当前版本的 API、生命周期和边界条件</td></tr><tr><td>Umi 配置</td><td>当前版本的路由、插件和构建配置</td></tr><tr><td>Midway 接口</td><td>Controller、Service 和参数校验方式</td></tr><tr><td>第三方依赖</td><td>当前版本的参数、示例和 breaking change</td></tr></tbody></table><p>它替代的是凭模型记忆猜 API，或者手动把一整篇 README 粘进聊天。</p><p>正确的问法是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">查当前 React 版本中 useEffect cleanup 的官方行为。</span><br><span class="line">只返回和这个错误相关的说明、限制和最小示例。</span><br></pre></td></tr></table></figure><p>不太好的问法是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">把 React 文档都找出来。</span><br></pre></td></tr></table></figure><p>第二种问法会让工具返回大量当前任务用不到的内容。Context7 本身也消耗 token，所以它的价值取决于查询是否足够窄。</p><h2 id="Serena：从“读文件”切换到“读代码结构”"><a href="#Serena：从“读文件”切换到“读代码结构”" class="headerlink" title="Serena：从“读文件”切换到“读代码结构”"></a>Serena：从“读文件”切换到“读代码结构”</h2><p>Serena 是代码语义检索和编辑工具。它更适合回答：</p><blockquote><p>这个项目里，真正应该看哪些代码？</p></blockquote><p>它可以帮助定位：</p><ul><li>函数、类、接口和类型</li><li>符号引用</li><li>调用关系</li><li>实现位置和使用方</li><li>一个改动可能影响的局部范围</li></ul><p>可以用下面这张表理解三个常用工具的分工：</p><table><thead><tr><th>工具</th><th>最擅长的问题</th><th>不应该承担的工作</th></tr></thead><tbody><tr><td><code>rg</code></td><td>某个字符串、错误文案、配置项在哪里</td><td>理解复杂调用关系</td></tr><tr><td>Serena</td><td>某个符号怎么定义、被谁引用</td><td>查外部框架文档</td></tr><tr><td>Context7</td><td>某个依赖的当前版本怎么用</td><td>理解你的业务代码</td></tr></tbody></table><p>Serena 的 token 收益，主要来自减少无关文件读取。它不是把代码“压缩”了，而是把阅读入口从目录和全文，缩小到符号和调用链。</p><p>当然，Serena 也不是 <code>rg</code> 的替代品。精确字符串搜索仍然是 <code>rg</code> 的强项；语义定位和结构理解才是 Serena 的优势。</p><h2 id="Session：真正的上下文边界"><a href="#Session：真正的上下文边界" class="headerlink" title="Session：真正的上下文边界"></a>Session：真正的上下文边界</h2><p>一个 Session 里积累的不只是聊天内容，还包括：</p><ul><li>之前的用户要求</li><li>Codex 的分析过程</li><li>工具调用结果</li><li>读过的文件</li><li>测试日志和构建日志</li><li>已经被推翻的旧结论</li></ul><p>所以 Session 变长以后，常见的问题不是模型“突然变笨”，而是有效信息被噪音埋住了。</p><p>这通常表现为：</p><ol><li>反复重新解释已经确定的背景。</li><li>继续参考已经过时的探索结论。</li><li>从旧日志里找错误，而不是看最新结果。</li><li>改动范围逐渐扩大。</li></ol><p>新 Session 的价值，就是把上下文重置成一个更干净的工作面。代码和 Git 工作区不会因为换 Session 消失，但对话里的决策不会自动继承，所以需要一份短 handoff。</p><h2 id="token-到底省在哪里"><a href="#token-到底省在哪里" class="headerlink" title="token 到底省在哪里"></a>token 到底省在哪里</h2><p>可以把每一轮输入粗略看成：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">本轮输入</span><br><span class="line">≈ 对话历史</span><br><span class="line">  + AGENTS.md 和项目约束</span><br><span class="line">  + 工具定义</span><br><span class="line">  + MCP 返回结果</span><br><span class="line">  + 读取的代码</span><br><span class="line">  + 当前 prompt</span><br></pre></td></tr></table></figure><p>这套组合主要从四个地方节省 token。</p><h3 id="少读文件"><a href="#少读文件" class="headerlink" title="少读文件"></a>少读文件</h3><p>Serena 把“扫描整个目录”缩小成“定位相关符号和引用方”。这是最直接的节省。</p><h3 id="少重复背景"><a href="#少重复背景" class="headerlink" title="少重复背景"></a>少重复背景</h3><p>稳定的项目规则放进 <code>AGENTS.md</code>，就不需要每个任务都重复说明包管理器、构建命令、目录边界和验证要求。</p><p>但 <code>AGENTS.md</code> 也不能无限膨胀。它可能在每次相关任务里进入上下文，和当前项目无关的长篇背景会变成固定开销。</p><h3 id="少复制历史"><a href="#少复制历史" class="headerlink" title="少复制历史"></a>少复制历史</h3><p>新 Session 不会携带上一轮的全部探索过程。用几行 handoff 传递结论，通常比复制几十屏命令输出更划算。</p><h3 id="少返回日志"><a href="#少返回日志" class="headerlink" title="少返回日志"></a>少返回日志</h3><p>验证时不要让 Codex 每次都接收完整日志，可以明确要求：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">只保留首个错误、错误文件和行号，以及与本次改动相关的 warning。</span><br></pre></td></tr></table></figure><p>构建产物、依赖安装日志和重复堆栈，往往只增加上下文负担，不增加判断价值。</p><h2 id="MCP-越多，token-越省吗？"><a href="#MCP-越多，token-越省吗？" class="headerlink" title="MCP 越多，token 越省吗？"></a>MCP 越多，token 越省吗？</h2><p>不是。</p><p>每个 MCP 都可能增加：</p><ul><li>工具定义</li><li>工具调用</li><li>工具返回结果</li><li>额外的错误和重试信息</li></ul><p>所以应该按职责选择，而不是把所有工具都打开：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">查外部文档       → Context7</span><br><span class="line">理解代码结构     → Serena</span><br><span class="line">查精确字符串     → rg</span><br><span class="line">修改代码和跑测试 → Codex 原生工具</span><br></pre></td></tr></table></figure><p>如果一个任务只需要改一个 CSS 属性，同时打开多个文档和代码 MCP，成本可能比收益更高。</p><h2 id="Session-应该什么时候拆"><a href="#Session-应该什么时候拆" class="headerlink" title="Session 应该什么时候拆"></a>Session 应该什么时候拆</h2><p>合理拆 Session 是手动做的。判断标准不是“聊了多少轮”，而是当前上下文是否还服务同一个目标。</p><h3 id="保持在同一个-Session"><a href="#保持在同一个-Session" class="headerlink" title="保持在同一个 Session"></a>保持在同一个 Session</h3><ul><li>目标没有变化。</li><li>修改的是同一组文件。</li><li>还没有积累大量日志。</li><li>Codex 仍然能清楚复述当前约束和决策。</li></ul><h3 id="新开一个-Session"><a href="#新开一个-Session" class="headerlink" title="新开一个 Session"></a>新开一个 Session</h3><ul><li>从调查切换到实现。</li><li>从实现切换到大量测试和排障。</li><li>开始处理另一个独立问题。</li><li>当前对话不断重复背景。</li><li>旧的探索过程已经不再有参考价值。</li></ul><p>对一个中等规模功能，我通常会按下面的方式拆：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">Session A：探索 / 定位 / 方案</span><br><span class="line">Session B：实现</span><br><span class="line">Session C：验证 / 修复</span><br></pre></td></tr></table></figure><p>但不要为了“省 token”把一个小任务拆成很多 Session。每次新建 Session 都有重新传递背景和重新建立上下文的成本。</p><h2 id="要不要用-我的判断框架"><a href="#要不要用-我的判断框架" class="headerlink" title="要不要用 &#x2F; 我的判断框架"></a>要不要用 &#x2F; 我的判断框架</h2><p>这套组合值得用，但应该把它当成上下文管理方案，而不是工具收藏。</p><h3 id="值得上"><a href="#值得上" class="headerlink" title="值得上"></a>值得上</h3><ul><li>中大型代码库，文件和调用关系比较复杂。</li><li>经常需要查 React、Umi、Midway 等版本敏感的文档。</li><li>任务经常经历探索、实现、验证多个阶段。</li><li>长 Session 里经常出现重复解释和日志污染。</li></ul><h3 id="可以先简单一点"><a href="#可以先简单一点" class="headerlink" title="可以先简单一点"></a>可以先简单一点</h3><ul><li>小型仓库。</li><li>单文件修改。</li><li>不涉及外部依赖行为。</li><li>一次对话就能完成并验证的任务。</li></ul><h3 id="最应该记住的一句话"><a href="#最应该记住的一句话" class="headerlink" title="最应该记住的一句话"></a>最应该记住的一句话</h3><p>省 token 的核心不是“少让 Codex 工作”，而是让它少看无关的东西。</p><p>Context7 负责文档的准确性，Serena 负责代码阅读的精度，Session 负责上下文的边界。三者放进同一条工作流里，才会把 token 花在真正影响结果的地方。</p><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><ul><li><a href="https://learn.chatgpt.com/docs/codex/codex-manual.md">OpenAI Codex Manual</a></li><li><a href="https://context7.com/docs/resources/all-clients">Context7 MCP 文档</a></li><li><a href="https://github.com/oraios/serena">Serena GitHub</a></li></ul>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/891e86f631c1/</id>
    <link href="https://blog.zisheng.pro/posts/891e86f631c1/"/>
    <published>2026-07-25T08:20:00.000Z</published>
    <summary>我把 Context7、Serena 和合理拆 Session 放进一次真实的 coding agent 工作流里，解释它们各自解决什么问题、为什么有时能省 token、什么时候反而会增加上下文成本，以及如何在探索、实现和验证之间建立清晰的上下文边界。</summary>
    <title>用 Context7、Serena 和 Session 把 Codex 的 token 花在刀刃上</title>
    <updated>2026-09-18T08:15:28.640Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="AI" scheme="https://blog.zisheng.pro/categories/AI/"/>
    <category term="RAG" scheme="https://blog.zisheng.pro/tags/RAG/"/>
    <category term="GraphRAG" scheme="https://blog.zisheng.pro/tags/GraphRAG/"/>
    <category term="AI Infra" scheme="https://blog.zisheng.pro/tags/AI-Infra/"/>
    <category term="LLM Wiki" scheme="https://blog.zisheng.pro/tags/LLM-Wiki/"/>
    <category term="Vector Search" scheme="https://blog.zisheng.pro/tags/Vector-Search/"/>
    <category term="Knowledge Compilation" scheme="https://blog.zisheng.pro/tags/Knowledge-Compilation/"/>
    <content>
      <![CDATA[<img src="https://blog.zisheng.pro/images/rag-three-routes-decision.webp" data-lazy-src="https://blog.zisheng.pro/images/rag-three-routes-decision.webp" alt="向量 RAG、GraphRAG、编译式 RAG 选型图"><p><em>先看问题形状，再决定知识该被表示成 Chunk、Graph，还是可持续维护的编译产物。</em></p><p>最近看到一张图，把知识检索分成向量 RAG、GraphRAG 和编译式 RAG 三条路线。图很直观，但也留下了最难回答的部分：一个真实系统到底怎么选？</p><p>如果只按概念解释，很容易得到三句没法指导架构的结论：小资料库用向量，复杂关系用 GraphRAG，可维护交付用编译式 RAG。落地时，资料库多大算小、什么关系值得建图、编译出来的知识丢了事实怎么办，仍然没有答案。</p><p>这篇文章不再横向罗列名词。我会用同一个“跨行业客服知识助手”贯穿三条路线，观察它们如何处理同一批文档、回答三类问题，再给出我会如何从 MVP 演进到生产架构。</p><h2 id="一句话总结"><a href="#一句话总结" class="headerlink" title="一句话总结"></a>一句话总结</h2><p>我的选择顺序是：</p><ol><li><strong>默认先做 Hybrid RAG</strong>：Keyword &#x2F; BM25 保精确词，Vector Search 处理自然语言表达，再加 Metadata Filter 与 Rerank。</li><li><strong>当问题依赖稳定的实体关系和多跳路径时，增加 GraphRAG</strong>：它是一个面向关系问题的专用索引，不是默认底座。</li><li><strong>当团队反复综合同一批稳定知识，并希望结果能够积累、审阅和复用时，增加编译层</strong>：把原始资料加工成结构化 Wiki、Procedure、Decision 和 Conflict 页面，但原始 Source 仍然是事实源。</li></ol><p>生产系统往往不是三选一，而是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">Raw Sources</span><br><span class="line">  ├─ Hybrid Index        → 找相关原文</span><br><span class="line">  ├─ Relation Graph      → 走依赖与影响路径</span><br><span class="line">  └─ Compiled Knowledge  → 复用已经整理过的知识</span><br><span class="line">                ↓</span><br><span class="line">        Evidence Gateway</span><br><span class="line">                ↓</span><br><span class="line">          LLM / Agent</span><br></pre></td></tr></table></figure><p>三条路线共享 Scope、Revision、Permission、Citation 和 Trace。不要让 Agent 直接面对三个互不兼容的知识后端。</p><h2 id="读实战前，先认清这些专业术语"><a href="#读实战前，先认清这些专业术语" class="headerlink" title="读实战前，先认清这些专业术语"></a>读实战前，先认清这些专业术语</h2><p>RAG 相关术语容易混在一起，是因为它们来自不同层：有些描述数据，有些描述检索算法，有些描述知识结构，还有一些负责权限和证据治理。先把它们放回一条完整链路：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">Raw Source</span><br><span class="line">  → Parse / Chunk</span><br><span class="line">  → Index</span><br><span class="line">  → Query</span><br><span class="line">  → Retrieve / Rank</span><br><span class="line">  → Evidence Context</span><br><span class="line">  → LLM Generate 或 Agent Action</span><br></pre></td></tr></table></figure><p>这条链路里，Source 是事实，Index 是为了查询而生成的派生数据，Evidence 是本次回答实际采用的依据。三者不能混为一层。</p><img src="https://blog.zisheng.pro/images/rag-terminology-chain.webp" data-lazy-src="https://blog.zisheng.pro/images/rag-terminology-chain.webp" alt="RAG 全链路术语地图：从原始资料、索引到证据与动作"><p><em>先看一个词位于哪一层，再讨论它解决什么问题。Graph、Vector 和 Compiled Wiki 都只是中间的知识表示，最终仍要交付可追溯的 Evidence。</em></p><h3 id="RAG、Context-与-Agent"><a href="#RAG、Context-与-Agent" class="headerlink" title="RAG、Context 与 Agent"></a>RAG、Context 与 Agent</h3><table><thead><tr><th>术语</th><th>通俗解释</th><th>在客服实战中的位置</th></tr></thead><tbody><tr><td>RAG</td><td>Retrieval-Augmented Generation，先检索外部资料，再让 LLM 基于资料生成答案</td><td>回答退款政策前，先取回有效政策片段</td></tr><tr><td>Retrieval</td><td>根据 Query 从外部知识中找候选内容</td><td>找到退款时效、订单状态和活动例外</td></tr><tr><td>Generation</td><td>LLM 根据问题与 Evidence 组织自然语言答案</td><td>把政策改写成客服能直接使用的回复</td></tr><tr><td>Context Window</td><td>模型单次推理能接收的 Token 范围</td><td>决定一次能放入多少问题、对话和证据</td></tr><tr><td>Context Budget</td><td>系统主动分配给证据的上下文额度</td><td>在有限 Token 中优先放最关键的政策和例外</td></tr><tr><td>LLM</td><td>Large Language Model，负责理解与生成，不自动等于事实源</td><td>解释证据、生成回复，但不能凭空创造政策</td></tr><tr><td>Agent</td><td>能调用 Tool、读取结果并继续决策的 LLM 运行循环</td><td>查询知识后，再读取订单状态或创建升级建议</td></tr><tr><td>Copilot</td><td>嵌在现有工作台中辅助人的 Agent 形态</td><td>在当前工单里推荐字段、证据和下一步动作</td></tr><tr><td>Tool</td><td>Agent 可调用的受控接口</td><td>只读查询订单状态，或在审批后执行某项业务动作</td></tr></tbody></table><p>RAG 只规定“检索后再生成”这条工作流，并没有规定一定使用向量数据库。BM25、Vector Search、Graph Search，甚至直接读取编译好的 Wiki，都可以成为 Retrieval 的实现。</p><h3 id="从文档到索引"><a href="#从文档到索引" class="headerlink" title="从文档到索引"></a>从文档到索引</h3><table><thead><tr><th>术语</th><th>通俗解释</th><th>容易踩的坑</th></tr></thead><tbody><tr><td>Raw Source</td><td>未被检索系统改写的原始事实源</td><td>把自动摘要当成原始政策，导致错误无法追溯</td></tr><tr><td>Parse</td><td>从 Markdown、PDF、HTML、表格等格式中提取正文与结构</td><td>表格行列、标题层级和脚注在解析时丢失</td></tr><tr><td>Chunk</td><td>为检索切出的内容单元，不一定等于固定字数</td><td>切得太小会失去条件，太大会混入无关内容</td></tr><tr><td>Metadata</td><td>附在内容上的结构化信息，如类型、时间、权限和版本</td><td>只算相似度，不过滤过期或无权限政策</td></tr><tr><td>Index</td><td>为搜索构建的派生结构，如倒排索引、向量索引或图索引</td><td>把 Index 当事实源；正确做法是允许删除后重建</td></tr><tr><td>Ingestion</td><td>从 Source 到 Parse、Chunk、Metadata 和 Index 的整条构建流水线</td><td>只做首次导入，没有增量更新和失败重试</td></tr></tbody></table><p>以退款政策为例，一个 Chunk 不应只剩“预计 1–3 个工作日到账”。它还要带上“原路退款”“演示会员等级”“支付渠道结果为准”等条件，并保留 Source Version。否则检索命中了数字，回答仍可能是错的。</p><h3 id="检索与排序"><a href="#检索与排序" class="headerlink" title="检索与排序"></a>检索与排序</h3><table><thead><tr><th>术语</th><th>通俗解释</th><th>它擅长什么</th></tr></thead><tbody><tr><td>Query</td><td>用户问题或系统改写后的搜索表达式</td><td>表达这次到底要找什么</td></tr><tr><td>Query Rewrite</td><td>把口语问题改写成更适合搜索的一组表达式</td><td>补充别名、标准术语或拆出多个子问题</td></tr><tr><td>Keyword Search</td><td>按出现的词查找内容</td><td>状态码、产品编码、政策名称等精确词</td></tr><tr><td>BM25</td><td>根据词频、词的稀有程度和文档长度计算关键词相关性</td><td>把包含关键术语的文档排到前面</td></tr><tr><td>Embedding</td><td>把文字映射成一组浮点数，用距离近似语义相似度</td><td>连接“退款多久到”和“退款到账时效”等不同说法</td></tr><tr><td>Vector Search</td><td>比较 Query Vector 与文档 Vector，找语义接近的内容</td><td>自然语言问法、同义表达和模糊描述</td></tr><tr><td>Vector Database</td><td>存储向量并执行近邻搜索的数据库或索引能力</td><td>解决向量的存取与查询，不负责答案正确性</td></tr><tr><td>Metadata Filter</td><td>在召回前后按租户、时间、权限、版本等条件过滤</td><td>阻止测试规则、旧政策或越权内容进入上下文</td></tr><tr><td>Hybrid Search</td><td>同时使用 Keyword &#x2F; BM25 与 Vector Search</td><td>兼顾精确术语和自然语言语义</td></tr><tr><td>Rank Fusion</td><td>把多个检索器的排名合并成一个候选列表</td><td>合并 BM25 与 Vector 的结果，而不是简单拼接</td></tr><tr><td>Reranker</td><td>用更精细的模型对少量候选重新判断相关性</td><td>把“语义相近”进一步收窄为“能回答当前问题”</td></tr><tr><td>Top K</td><td>最终保留排名最靠前的 K 条结果</td><td>控制 Recall、噪声和 Context Budget 的平衡</td></tr></tbody></table><p>Embedding 不会“理解并保存答案”。它只提供一种相似度信号。Reranker 也不能找回第一轮完全没召回的资料。因此检索评测要分开看 Recall 和排序质量。</p><h3 id="图、编译与知识表示"><a href="#图、编译与知识表示" class="headerlink" title="图、编译与知识表示"></a>图、编译与知识表示</h3><table><thead><tr><th>术语</th><th>通俗解释</th><th>在本文中的例子</th></tr></thead><tbody><tr><td>Entity &#x2F; Node</td><td>可以独立识别的业务对象</td><td>订单、会员、权益券、政策</td></tr><tr><td>Relationship &#x2F; Edge</td><td>两个 Entity 之间有类型和方向的关系</td><td>订单使用权益券、权益券受活动政策约束</td></tr><tr><td>Knowledge Graph</td><td>由 Entity、Relationship 及其属性组成的知识网络</td><td>表达订单、商品、会员、权益与政策的连接</td></tr><tr><td>Multi-hop</td><td>为回答问题连续经过多条关系</td><td>取消订单 → 活动权益券 → 活动政策 → 退回条件</td></tr><tr><td>GraphRAG</td><td>用图结构定位关系，再结合原文 Evidence 生成答案</td><td>解释某类权益为什么没有随订单取消而退回</td></tr><tr><td>Community</td><td>图中连接紧密的一组实体</td><td>围绕退款、支付或会员权益形成的对象集合</td></tr><tr><td>Community Report</td><td>对某个 Community 预先生成的主题摘要</td><td>用于回答“这批客服资料有哪些主要问题”</td></tr><tr><td>Knowledge Compilation</td><td>在构建阶段把 Raw Sources 整理成稳定的知识产物</td><td>把多份政策编成可审阅的支付异常处理 Procedure</td></tr><tr><td>Intermediate Representation</td><td>Source 与最终消费之间的中间表示，简称 IR</td><td>Topic Page、Entity Page、Procedure、Conflict Page</td></tr><tr><td>Build Artifact</td><td>一次编译输出的、可版本化和可重建的产物</td><td>Wiki、Knowledge Pack、索引和 Citation Map</td></tr><tr><td>LLM Wiki</td><td>由 LLM 增量维护、相互链接的结构化 Wiki</td><td>让重复查询优先复用已有页面，再回原文核验</td></tr></tbody></table><p>GraphRAG 和编译式 RAG 都会在 Query 发生前做更多加工。区别是 GraphRAG 主要预构建关系，编译式 RAG 还会预先合并事实、流程、冲突和解释结构。</p><h3 id="Evidence、Citation-与治理"><a href="#Evidence、Citation-与治理" class="headerlink" title="Evidence、Citation 与治理"></a>Evidence、Citation 与治理</h3><table><thead><tr><th>术语</th><th>通俗解释</th><th>为什么需要它</th></tr></thead><tbody><tr><td>Evidence</td><td>本次回答实际使用、能够支撑某个 Claim 的内容</td><td>判断答案是不是基于资料，而不是模型猜测</td></tr><tr><td>Claim</td><td>回答里可以被验证真假的具体陈述</td><td>“退款预计 1–3 个工作日到账”就是一个 Claim</td></tr><tr><td>Citation</td><td>从 Claim &#x2F; Evidence 回到 Source 的可定位引用</td><td>让客服和审核人检查原文、章节与版本</td></tr><tr><td>Grounding</td><td>回答中的 Claim 是否被给定 Evidence 支撑</td><td>区分“答得像真的”和“确实有依据”</td></tr><tr><td>Scope</td><td>一次查询被允许使用的业务、租户或知识范围</td><td>防止不同业务空间的规则互相污染</td></tr><tr><td>Revision</td><td>本次查询固定使用的知识版本</td><td>避免新旧政策混在同一个答案里</td></tr><tr><td>Permission</td><td>当前主体是否有权读取或执行</td><td>有权看通用政策，不代表有权看真实订单</td></tr><tr><td>Approval</td><td>高风险动作执行前的明确批准</td><td>退款、补偿和改订单不能由知识命中自动触发</td></tr><tr><td>Trace</td><td>记录 Query、路由、候选、Evidence 和最终输出的链路</td><td>出错后能定位是召回、排序、编译还是生成问题</td></tr><tr><td>Knowledge Gateway</td><td>统一接收知识查询并治理多个后端的入口</td><td>屏蔽 Hybrid、Graph 和 Compiled Wiki 的接口差异</td></tr></tbody></table><p>Evidence 不等于“搜索结果”，Citation 也不只是文章末尾列几个链接。理想状态是回答中的关键 Claim 能逐条对应具体 Evidence，Evidence 再指向带 Revision 的 Source Span。</p><h2 id="先把“编译式-RAG”这个词说准确"><a href="#先把“编译式-RAG”这个词说准确" class="headerlink" title="先把“编译式 RAG”这个词说准确"></a>先把“编译式 RAG”这个词说准确</h2><p>Vector RAG 和 GraphRAG 已经有相对明确的工程含义，“编译式 RAG”却还不是一个边界稳定的标准术语。不同人可能用它指：</p><ul><li>提前生成摘要或 Contextual Chunk；</li><li>把整个知识库放进长上下文并缓存 KV；</li><li>把原始资料编译成相互链接的 Markdown Wiki；</li><li>从资料中抽取可执行的 Agent Skill；</li><li>把多种检索计划编译成一个查询。</li></ul><p>本文采用其中最适合知识工程的一种定义：</p><blockquote><p>编译式 RAG，是把不可直接高效消费的 Raw Sources，在构建阶段加工成面向后续任务的、可检查、可版本化、可增量更新的知识中间表示；查询时优先读取这些编译产物，必要时回到原文核验。</p></blockquote><p>这个定义主要对应 <a href="https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f">Andrej Karpathy 的 LLM Wiki pattern</a>：原始资料保持不可变，LLM 增量维护一个互相链接的 Wiki，再用 Schema &#x2F; Agent Instruction 约束写入和查询流程。</p><p>“编译”这个比喻有四个关键点：</p><table><thead><tr><th>编译器概念</th><th>知识系统里的对应物</th></tr></thead><tbody><tr><td>Source Code</td><td>产品说明、服务政策、FAQ、流程规范和已审核案例等原始事实源</td></tr><tr><td>Intermediate Representation</td><td>Topic Page、Entity Page、Procedure、Decision、Conflict、Citation Map</td></tr><tr><td>Compiler</td><td>LLM + 确定性解析器 + Schema + 校验器</td></tr><tr><td>Build Artifact</td><td>可被人阅读、被 Agent 搜索和遍历的 Wiki &#x2F; Knowledge Pack</td></tr></tbody></table><p>它不是“预先做了 Embedding”的新名字。Embedding 只是建立向量索引；编译式 RAG 会合并跨文档事实、建立链接、显式记录冲突，甚至把陈述性知识整理成可执行流程。</p><p>它也不能自动取代 RAG。编译过程会丢信息、写错关系、把条件句压成绝对规则。2026 年的 <a href="https://arxiv.org/abs/2605.07068">WiCER 研究</a>专门讨论了这个 compilation gap：盲目压缩原始知识会出现严重信息损失，必须用诊断问题找出丢失事实，再迭代修正编译产物。</p><h2 id="实战场景：给客服做一个知识助手"><a href="#实战场景：给客服做一个知识助手" class="headerlink" title="实战场景：给客服做一个知识助手"></a>实战场景：给客服做一个知识助手</h2><p>假设我们要做一个跨行业客服知识助手。它不绑定某家公司的产品、渠道或组织，示例中的会员等级、订单、规则、数值和 URI 均为虚构，仅用于说明架构。</p><p>团队准备了八份经过脱敏的资料：</p><table><thead><tr><th>ID</th><th>文档</th><th>主要内容</th></tr></thead><tbody><tr><td>D1</td><td><code>product-catalog.md</code></td><td>虚构产品、服务权益与适用范围</td></tr><tr><td>D2</td><td><code>refund-policy.md</code></td><td>退款方式、预计时效与解释口径</td></tr><tr><td>D3</td><td><code>membership-policy.md</code></td><td>虚构会员等级与权益限制</td></tr><tr><td>D4</td><td><code>order-status.md</code></td><td>订单状态、支付状态与状态转换</td></tr><tr><td>D5</td><td><code>resolved-cases.md</code></td><td>已审核、已脱敏的历史客服案例</td></tr><tr><td>D6</td><td><code>business-relations.yaml</code></td><td>订单、商品、会员、权益券和规则的关系</td></tr><tr><td>D7</td><td><code>service-sop.md</code></td><td>核验、回复、升级与留痕步骤</td></tr><tr><td>D8</td><td><code>campaign-exceptions.md</code></td><td>虚构活动订单的例外和覆盖规则</td></tr></tbody></table><p>为了避免把 Demo 做成“上传 PDF，然后问一句摘要”，我选三类形状完全不同的问题：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">Q1 事实查找：</span><br><span class="line">虚构的 Demo Plus 会员申请原路退款后，预计多久能到账？</span><br><span class="line"></span><br><span class="line">Q2 关系与影响：</span><br><span class="line">订单取消后，为什么某类活动权益券没有退回？要看哪些规则？</span><br><span class="line"></span><br><span class="line">Q3 稳定执行：</span><br><span class="line">用户已付款，但订单仍显示“待支付”，客服应该如何核验、回复和升级？</span><br></pre></td></tr></table></figure><p>这三问分别要求系统找到一个事实、走一条关系链、复用一套跨文档流程。它们比“总结这些文档”更接近真实系统。</p><p>这套实战也刻意划出产品边界。客服场景不能被粗暴地收敛成一个聊天框：</p><table><thead><tr><th>能力</th><th>合适的入口</th><th>知识系统负责什么</th></tr></thead><tbody><tr><td>通用知识问答与推荐</td><td>独立知识问答入口</td><td>查询产品说明、政策与标准口径</td></tr><tr><td>当前工单的信息提取</td><td>工单内 Copilot</td><td>读取已脱敏上下文，推荐字段和 Evidence</td></tr><tr><td>订单、会员与权益关联解释</td><td>工单内 Copilot + 受控业务 Tool</td><td>Graph 解释关系，Tool 提供当前状态</td></tr><tr><td>退款、补偿、改订单等动作</td><td>有权限和审批的操作流</td><td>知识只提供规则依据，不直接取得执行权</td></tr></tbody></table><p>时效要求不高、跨行业共用的知识问答可以沉到通用平台。和当前会话、订单状态、退款流程强绑定的能力，应留在客服工作台 Copilot 中。RAG 提供依据，业务 Tool 读取实时状态，Permission &#x2F; Approval 决定能否执行。</p><img src="https://blog.zisheng.pro/images/rag-customer-service-routing.webp" data-lazy-src="https://blog.zisheng.pro/images/rag-customer-service-routing.webp" alt="三类客服问题如何路由到 Hybrid RAG、GraphRAG 与编译式 RAG"><p><em>入口可以统一，知识路线不必统一。先识别问题需要一个片段、一条关系路径还是一套稳定流程，再路由到相应后端。</em></p><h2 id="第一轮：用向量-RAG-解决"><a href="#第一轮：用向量-RAG-解决" class="headerlink" title="第一轮：用向量 RAG 解决"></a>第一轮：用向量 RAG 解决</h2><h3 id="数据怎样进入系统"><a href="#数据怎样进入系统" class="headerlink" title="数据怎样进入系统"></a>数据怎样进入系统</h3><p>最小 Vector RAG 会做四件事：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">Document</span><br><span class="line">  → Parse</span><br><span class="line">  → Chunk</span><br><span class="line">  → Embedding</span><br><span class="line">  → Vector Index</span><br></pre></td></tr></table></figure><p>生产环境我不会只留向量通道。订单状态码、产品编码、政策编号和标准术语更适合 BM25，因此实际链路会是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">Query</span><br><span class="line">  ├─ Keyword / BM25</span><br><span class="line">  ├─ Vector Search</span><br><span class="line">  └─ Metadata Filter</span><br><span class="line">          ↓</span><br><span class="line">      Rank Fusion</span><br><span class="line">          ↓</span><br><span class="line">       Reranker</span><br><span class="line">          ↓</span><br><span class="line">   Evidence Top K</span><br></pre></td></tr></table></figure><img src="https://blog.zisheng.pro/images/rag-hybrid-retrieval-flow.webp" data-lazy-src="https://blog.zisheng.pro/images/rag-hybrid-retrieval-flow.webp" alt="客服 Hybrid RAG 从 Query Rewrite 到 Evidence Top K 的检索流程"><p><em>所谓“向量 RAG”在生产里通常不是只查向量：BM25 守住精确词，Vector Search 处理同义表达，Metadata Filter 先排除过期和越权内容。</em></p><p>每个 Chunk 至少要保留：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;chunkId&quot;</span><span class="punctuation">:</span> <span class="string">&quot;refund-policy#original-channel&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;content&quot;</span><span class="punctuation">:</span> <span class="string">&quot;演示规则：Demo Plus 会员的原路退款预计 1–3 个工作日到账；实际时间以支付渠道处理结果为准。&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;sourceUri&quot;</span><span class="punctuation">:</span> <span class="string">&quot;demo://policies/refund&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;sourceVersion&quot;</span><span class="punctuation">:</span> <span class="string">&quot;sha256:...&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;section&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;scope&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span><span class="string">&quot;customer-service-demo&quot;</span><span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;validFrom&quot;</span><span class="punctuation">:</span> <span class="string">&quot;2026-06-01&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;permission&quot;</span><span class="punctuation">:</span> <span class="string">&quot;engineering&quot;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p>没有这些 Metadata，检索器可能把旧版本、测试环境或无权限文档排在最前面。相似度高，不等于业务上可用。</p><h3 id="三个问题会发生什么"><a href="#三个问题会发生什么" class="headerlink" title="三个问题会发生什么"></a>三个问题会发生什么</h3><p>Q1 很适合 Vector &#x2F; Hybrid RAG。Query 中的“原路退款”“多久能到账”和 D2 的表述足够接近，Top K 很容易命中正确段落。若客服搜索某个标准状态码，BM25 通道还能兜住精确标识符。</p><p>Q2 开始吃力。向量检索可能召回 <code>change-184.md</code> 和 <code>service-topology.md</code>，但它不会天然执行这条路径：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><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><p>Top K 里只要漏掉一段，影响分析就断了。把 <code>k</code> 调大能提高 Recall，却会带来更多无关上下文和更高推理成本。</p><p>Q3 的问题更明显。支付与订单状态定义在 D4，核验和升级动作在 D7，解释口径在 D2，活动例外又可能覆盖默认规则。单轮 RAG 每次都要重新找到多份资料并现场拼装流程。</p><h3 id="向量-RAG-的真实边界"><a href="#向量-RAG-的真实边界" class="headerlink" title="向量 RAG 的真实边界"></a>向量 RAG 的真实边界</h3><p>Vector RAG 擅长的是“从大语料里找到语义相近的局部证据”。它没有承诺：</p><ul><li>一次检索覆盖完整依赖链；</li><li>不同 Chunk 之间的方向关系正确；</li><li>每次都以相同顺序重建同一套流程；</li><li>自动识别新规则覆盖旧规则；</li><li>将本次回答沉淀为下一次可复用的知识。</li></ul><p>所以它适合作为默认检索底座，却不等于完整知识架构。</p><h2 id="第二轮：什么时候-GraphRAG-值得上"><a href="#第二轮：什么时候-GraphRAG-值得上" class="headerlink" title="第二轮：什么时候 GraphRAG 值得上"></a>第二轮：什么时候 GraphRAG 值得上</h2><p>GraphRAG 在构建阶段把文本转成实体和关系：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">(CancelledOrder)</span><br><span class="line">  -[USED]-&gt; (CampaignVoucher)</span><br><span class="line"></span><br><span class="line">(CampaignVoucher)</span><br><span class="line">  -[GOVERNED_BY]-&gt; (CampaignPolicy)</span><br><span class="line"></span><br><span class="line">(PlusMember)</span><br><span class="line">  -[ELIGIBLE_FOR]-&gt; (RefundBenefit)</span><br></pre></td></tr></table></figure><p>查询 Q2 时，系统先定位“已取消订单”和“活动权益券”，再按允许的 Edge Type 遍历，最后把路径关联的原文片段一起交给模型。此时“为什么没有退回”不再靠模型从若干相似 Chunk 中猜，而是有一条显式路径。</p><img src="https://blog.zisheng.pro/images/rag-customer-graph-path.webp" data-lazy-src="https://blog.zisheng.pro/images/rag-customer-graph-path.webp" alt="GraphRAG 沿订单、权益券、政策与例外关系完成多跳解释"><p><em>GraphRAG 解决的是“沿什么关系找到答案”，不是“图里的结论天然可信”。每一跳仍要锚定带版本的 Source Span。</em></p><p>微软的 <a href="https://microsoft.github.io/graphrag/index/overview/">GraphRAG 官方索引流程</a>会从原文抽取 Entity、Relationship 和 Claim，做 Community Detection、生成 Community Report，并保留向量表示。它的 <a href="https://microsoft.github.io/graphrag/query/local_search/">Local Search</a>会结合实体关系与原始 Text Unit，适合具体实体问题；Global Search 则基于 Community Report 做 Map-Reduce，更适合“整批资料有哪些主要主题”这类全局问题。</p><h3 id="GraphRAG-不是“Vector-RAG-加一个图数据库”"><a href="#GraphRAG-不是“Vector-RAG-加一个图数据库”" class="headerlink" title="GraphRAG 不是“Vector RAG 加一个图数据库”"></a>GraphRAG 不是“Vector RAG 加一个图数据库”</h3><p>难点不在于把 Node 和 Edge 存进去，而在于：</p><ol><li><strong>实体消歧</strong>：“权益券”“优惠权益”和“活动凭证”是不是同一类对象？</li><li><strong>关系方向</strong>：A 调用 B 与 B 依赖 A 不能写反。</li><li><strong>时间有效性</strong>：v2 的关系不能污染 v3。</li><li><strong>来源追踪</strong>：每条 Edge 必须能回到产生它的 Source Span。</li><li><strong>增量更新</strong>：一条政策发布新版本后，哪些社区摘要和关系要重算？</li><li><strong>权限裁剪</strong>：删掉无权访问的 Node 后，剩余路径是否还泄露信息？</li></ol><p>如果团队不能维护这些约束，GraphRAG 会把“检索偶尔漏片段”变成“图谱稳定地提供错误关系”。</p><h3 id="我会怎样限制第一版图谱"><a href="#我会怎样限制第一版图谱" class="headerlink" title="我会怎样限制第一版图谱"></a>我会怎样限制第一版图谱</h3><p>不从所有自然语言里自由抽取任意关系，而是先定义少量 Typed Edge：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> <span class="title class_">EdgeType</span> =</span><br><span class="line">  | <span class="string">&#x27;BELONGS_TO&#x27;</span></span><br><span class="line">  | <span class="string">&#x27;PURCHASES&#x27;</span></span><br><span class="line">  | <span class="string">&#x27;USES&#x27;</span></span><br><span class="line">  | <span class="string">&#x27;GOVERNED_BY&#x27;</span></span><br><span class="line">  | <span class="string">&#x27;ELIGIBLE_FOR&#x27;</span></span><br><span class="line">  | <span class="string">&#x27;SUPERSEDES&#x27;</span></span><br><span class="line">  | <span class="string">&#x27;ESCALATES_TO&#x27;</span>;</span><br></pre></td></tr></table></figure><p>结构化的商品、订单状态和政策引用用确定性解析器产生；只有已脱敏客服案例、FAQ 等非结构化资料才交给 LLM 抽取，并标记较低 Confidence，等待审核。</p><p>这样做牺牲了“自动构建万物图谱”的想象力，换来可验证的影响分析。</p><h2 id="第三轮：编译式-RAG-改变了什么"><a href="#第三轮：编译式-RAG-改变了什么" class="headerlink" title="第三轮：编译式 RAG 改变了什么"></a>第三轮：编译式 RAG 改变了什么</h2><p>Q3 是编译式 RAG 最有价值的地方。团队已经反复处理“支付成功但订单状态未同步”，系统没必要每次从多份原文重新拼出核验与升级流程。</p><p>构建阶段可以产出：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">wiki/</span><br><span class="line">  products/</span><br><span class="line">    example-product.md</span><br><span class="line">  policies/</span><br><span class="line">    refund-policy.md</span><br><span class="line">    membership-policy.md</span><br><span class="line">  procedures/</span><br><span class="line">    payment-order-mismatch.md</span><br><span class="line">  decisions/</span><br><span class="line">    policy-revision.md</span><br><span class="line">  cases/</span><br><span class="line">    reviewed-payment-case.md</span><br><span class="line">  conflicts/</span><br><span class="line">    campaign-exceptions.md</span><br><span class="line">  index.md</span><br><span class="line">  log.md</span><br></pre></td></tr></table></figure><p>其中 <code>procedures/payment-order-mismatch.md</code> 不是一段随意摘要，而是带来源和适用条件的结构化产物：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">---</span></span><br><span class="line"><span class="attr">type:</span> <span class="string">procedure</span></span><br><span class="line"><span class="attr">status:</span> <span class="string">reviewed</span></span><br><span class="line"><span class="attr">revision:</span> <span class="number">7</span></span><br><span class="line"><span class="attr">applies_to:</span></span><br><span class="line">  <span class="attr">scenario:</span> <span class="string">payment_succeeded_order_pending</span></span><br><span class="line">  <span class="attr">excluded_cases:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">offline_payment_demo</span></span><br><span class="line"><span class="attr">source_refs:</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">demo://orders/status#pending</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">demo://policies/refund#original-channel</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">demo://sop/payment-check#escalation</span></span><br><span class="line"><span class="attr">compiled_from:</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">sha256:...</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">sha256:...</span></span><br><span class="line"><span class="meta">---</span></span><br></pre></td></tr></table></figure><p>正文再写清：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">触发条件</span><br><span class="line">1. 用户提供的脱敏支付凭证显示已付款；</span><br><span class="line">2. 订单在演示用正常同步窗口后仍为“待支付”；</span><br><span class="line">3. 当前订单不属于已声明的例外支付方式。</span><br><span class="line"></span><br><span class="line">核验与回复</span><br><span class="line">1. 通过只读工具核对订单与支付状态；</span><br><span class="line">2. 不要求用户提供完整卡号、证件号等敏感信息；</span><br><span class="line">3. 告知当前状态、预计处理路径和下一次反馈时间；</span><br><span class="line">4. 状态持续不一致时升级到对应处理队列。</span><br><span class="line"></span><br><span class="line">验证与留痕</span><br><span class="line">1. 订单与支付状态最终一致；</span><br><span class="line">2. 工单关联本次查询所用 Evidence；</span><br><span class="line">3. 面向用户的回复不暴露内部系统和规则标识；</span><br><span class="line">4. 敏感字段在日志与摘要中保持掩码。</span><br></pre></td></tr></table></figure><p>Agent 查询时先读 <code>index.md</code>，找到 Procedure，再沿链接读取相关 System、Decision 和 Conflict 页面。对于关键阈值，它仍要回到 Source Span 核验。</p><h3 id="编译式-RAG-的收益不是“少搜一次”"><a href="#编译式-RAG-的收益不是“少搜一次”" class="headerlink" title="编译式 RAG 的收益不是“少搜一次”"></a>编译式 RAG 的收益不是“少搜一次”</h3><p>它把重复劳动从 Query Time 移到 Build Time：</p><ul><li>跨文档合并提前完成；</li><li>常用概念拥有稳定页面；</li><li>冲突与覆盖关系被显式记录；</li><li>Procedure 可以进入 Review 和版本管理；</li><li>上一次探索产生的有效结构能够被下一次复用。</li></ul><p>这更接近程序编译：开发者不会每次运行程序时都重新理解所有 Source Code，而是消费经过检查的 Artifact。</p><img src="https://blog.zisheng.pro/images/rag-compiled-knowledge-loop.webp" data-lazy-src="https://blog.zisheng.pro/images/rag-compiled-knowledge-loop.webp" alt="编译式 RAG 从原始资料到知识产物、查询与重编译的闭环"><p><em>编译式 RAG 的核心不是提前写摘要，而是让知识产物进入版本、校验和重建闭环；查询时对关键 Claim 仍需回查原文。</em></p><h3 id="编译产物绝不能成为未经审查的新事实源"><a href="#编译产物绝不能成为未经审查的新事实源" class="headerlink" title="编译产物绝不能成为未经审查的新事实源"></a>编译产物绝不能成为未经审查的新事实源</h3><p>编译器会出错，LLM Compiler 也会。至少要守住四条线：</p><ol><li>Raw Sources 不可被编译器改写。</li><li>每个 Claim 都能追到 Source Span 与 Source Revision。</li><li>编译产物可以全部删除并重建。</li><li>高风险 Procedure 在进入 <code>reviewed</code> 前不能驱动自动执行。</li></ol><p>如果一个 Wiki 页面引用另一个 Wiki 页面，最后却找不到原文，这会形成循环证据。页面之间可以互相导航，事实依据必须落回 Source。</p><h2 id="把三条路线放回同一张运行图"><a href="#把三条路线放回同一张运行图" class="headerlink" title="把三条路线放回同一张运行图"></a>把三条路线放回同一张运行图</h2><img src="https://blog.zisheng.pro/images/rag-three-routes-runtime.webp" data-lazy-src="https://blog.zisheng.pro/images/rag-three-routes-runtime.webp" alt="向量 RAG、GraphRAG、编译式 RAG 的构建时与查询时成本"><p><em>三条路线都在“预处理”，区别在于构建出了什么中间表示，以及查询时还要做多少工作。</em></p><p>向量 RAG 的构建成本最低，查询时要重新召回和综合。GraphRAG 把实体、关系与社区摘要前移到构建阶段，换来更可控的多跳与全局查询。编译式 RAG 进一步把常用综合结果和流程前移，但也承担最大的知识维护与编译正确性责任。</p><p>生成的直觉图更容易看出这三个动作的差别：</p><img src="https://blog.zisheng.pro/images/rag-three-routes-workshop.webp" data-lazy-src="https://blog.zisheng.pro/images/rag-three-routes-workshop.webp" alt="从语义检索、关系网络到知识编译的三种工作方式"><p><em>向量路线在空间中找邻近片段；图路线沿关系走路径；编译路线把原始资料加工成可长期使用的知识产物。</em></p><h2 id="桌面推演：三问分别应该命中什么"><a href="#桌面推演：三问分别应该命中什么" class="headerlink" title="桌面推演：三问分别应该命中什么"></a>桌面推演：三问分别应该命中什么</h2><p>这不是一个声称“准确率提升 37%”的伪 Benchmark。数据量太小、问题由作者设计、没有盲测，百分比没有意义。这里做的是架构桌面推演：先写出每个问题必须命中的证据与关系，再看哪条路线最自然。</p><table><thead><tr><th>Query</th><th>正确答案所需材料</th><th>Vector &#x2F; Hybrid</th><th>GraphRAG</th><th>编译式 RAG</th></tr></thead><tbody><tr><td>Q1 退款预计多久</td><td>D2 的一个段落</td><td>最自然</td><td>可以，但过度设计</td><td>可以，仍应核验原文</td></tr><tr><td>Q2 权益券为什么没退</td><td>D4 + D6 + D8 的规则路径</td><td>容易漏一跳</td><td>最自然</td><td>若已有 Policy Pages 也能做</td></tr><tr><td>Q3 如何核验、回复和升级</td><td>D2 + D4 + D7 + D8</td><td>每次重新拼装</td><td>图能找关联，但流程顺序仍需组织</td><td>最自然，Procedure 可审阅复用</td></tr></tbody></table><p>还要加入反向问题：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Q4：订单刚显示“待支付”一分钟，是否应该立即承诺退款？</span><br></pre></td></tr></table></figure><p>正确行为不是从“状态不一致”直接跳到退款，而是先核对演示规则中的正常同步窗口和支付状态，并明确不能在证据不足时承诺处理结果。</p><p>再加入冲突问题：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Q5：活动订单取消后，权益券是否一定自动退回？</span><br></pre></td></tr></table></figure><p>系统必须同时找到默认权益规则和 D8 的例外规则，按有效 Revision 判断谁覆盖谁。如果它只返回默认政策，即使语言很流畅也算失败。</p><h2 id="评测不能只看最终回答"><a href="#评测不能只看最终回答" class="headerlink" title="评测不能只看最终回答"></a>评测不能只看最终回答</h2><p>三条路线需要不同的中间指标。</p><h3 id="Vector-Hybrid-RAG"><a href="#Vector-Hybrid-RAG" class="headerlink" title="Vector &#x2F; Hybrid RAG"></a>Vector &#x2F; Hybrid RAG</h3><table><thead><tr><th>指标</th><th>要回答的问题</th></tr></thead><tbody><tr><td>Recall@K</td><td>必要 Source Span 是否进入候选集</td></tr><tr><td>MRR &#x2F; NDCG</td><td>最有用证据是否排在前面</td></tr><tr><td>Filter Accuracy</td><td>Scope、时间、权限是否过滤正确</td></tr><tr><td>Citation Precision</td><td>引用是否真的支持对应 Claim</td></tr><tr><td>No-evidence Refusal</td><td>没有证据时是否拒答</td></tr></tbody></table><h3 id="GraphRAG"><a href="#GraphRAG" class="headerlink" title="GraphRAG"></a>GraphRAG</h3><table><thead><tr><th>指标</th><th>要回答的问题</th></tr></thead><tbody><tr><td>Entity Resolution Accuracy</td><td>同名、别名、版本实体是否合并正确</td></tr><tr><td>Edge Precision &#x2F; Recall</td><td>关系是否存在、方向是否正确</td></tr><tr><td>Path Coverage</td><td>关键影响路径是否完整</td></tr><tr><td>Temporal Validity</td><td>查询时是否使用了正确版本的关系</td></tr><tr><td>Source Traceability</td><td>Node &#x2F; Edge 是否能回到原文</td></tr></tbody></table><h3 id="编译式-RAG"><a href="#编译式-RAG" class="headerlink" title="编译式 RAG"></a>编译式 RAG</h3><table><thead><tr><th>指标</th><th>要回答的问题</th></tr></thead><tbody><tr><td>Fact Preservation</td><td>编译后是否丢掉关键条件与数字</td></tr><tr><td>Claim-Citation Alignment</td><td>页面中的每个 Claim 是否被引用支撑</td></tr><tr><td>Conflict Detection</td><td>新旧规则冲突是否被标出</td></tr><tr><td>Rebuild Determinism</td><td>相同 Source 与 Compiler Version 是否得到可解释的 Diff</td></tr><tr><td>Staleness</td><td>Source 变化后，受影响页面多久完成重编译</td></tr><tr><td>Procedure Success Rate</td><td>Agent 按编译流程执行是否真的完成任务</td></tr></tbody></table><p>一个系统最终答对了，不代表索引正确。LLM 可能靠训练知识猜中答案。Eval 数据里要包含专门构造、无法靠常识猜中的值，加入旧版本干扰项和“应该拒答”的问题，才能测出 Grounding。</p><p>建议把用例写成可执行契约：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><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;support-payment-order-mismatch&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;query&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;scope&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;domain&quot;</span><span class="punctuation">:</span> <span class="string">&quot;customer-service-demo&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;revision&quot;</span><span class="punctuation">:</span> <span class="string">&quot;policy-demo-r7&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;expectedEvidence&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">    <span class="string">&quot;order-status#pending&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="string">&quot;refund-policy#original-channel&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="string">&quot;service-sop#payment-check&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="string">&quot;campaign-exceptions#payment-method&quot;</span></span><br><span class="line">  <span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;mustMention&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">    <span class="string">&quot;先核对订单与支付状态&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="string">&quot;告知下一次反馈时间&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="string">&quot;持续不一致时升级&quot;</span></span><br><span class="line">  <span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;mustNotDo&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">    <span class="string">&quot;索取完整支付卡号&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="string">&quot;在证据不足时承诺退款&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><h2 id="选型时，我看这六个变量"><a href="#选型时，我看这六个变量" class="headerlink" title="选型时，我看这六个变量"></a>选型时，我看这六个变量</h2><h3 id="1-问题是找片段、走关系，还是复用综合结果"><a href="#1-问题是找片段、走关系，还是复用综合结果" class="headerlink" title="1. 问题是找片段、走关系，还是复用综合结果"></a>1. 问题是找片段、走关系，还是复用综合结果</h3><p>这是第一判断，优先级高于语料规模。</p><ul><li>“这条规则写在哪里”是 Lookup。</li><li>“改了它会影响谁”是 Relation。</li><li>“以后每次都该怎么做”是 Reuse &#x2F; Procedure。</li></ul><p>问题形状判断错了，后面调 Embedding Model、Graph Database 或 Prompt 都只是优化错误架构。</p><h3 id="2-关系是否稳定"><a href="#2-关系是否稳定" class="headerlink" title="2. 关系是否稳定"></a>2. 关系是否稳定</h3><p>商品、会员、订单、权益与正式政策之间的关系相对稳定，值得建图。客服对话里临时提到的“可能相关”，通常不值得成为长期 Edge。</p><p>关系变化比查询还快时，GraphRAG 的增量维护成本会吞掉收益。</p><h3 id="3-Query-是否重复"><a href="#3-Query-是否重复" class="headerlink" title="3. Query 是否重复"></a>3. Query 是否重复</h3><p>编译的收益依赖复用。同一批资料只问一次，直接 RAG 通常更便宜；团队每周都要重新综合同一套规则，提前编译才开始划算。</p><p>这里不能只算 Token。还要算人工复核、错误传播和旧知识清理成本。</p><h3 id="4-Source-是否持续变化"><a href="#4-Source-是否持续变化" class="headerlink" title="4. Source 是否持续变化"></a>4. Source 是否持续变化</h3><p>变化快的订单状态、库存和工单进度，应该通过受权限控制的实时 Tool 查询。稳定的产品说明、正式政策、领域概念和高频 Procedure，更适合编译。</p><p>一个常见组合是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">快速变化层：实时 Tool 查询当前业务状态</span><br><span class="line">稳定关系层：小范围 Typed Graph</span><br><span class="line">长期知识层：Versioned Compiled Wiki</span><br></pre></td></tr></table></figure><h3 id="5-错误代价有多高"><a href="#5-错误代价有多高" class="headerlink" title="5. 错误代价有多高"></a>5. 错误代价有多高</h3><p>个人研究笔记可以接受少量编译偏差；自动退款、赔付或修改订单不行。风险越高，越要加强：</p><ul><li>Source Span 级 Citation；</li><li>人工 Review；</li><li>Revision Pinning；</li><li>Permission &#x2F; Approval；</li><li>Dry Run 与 Evidence；</li><li>无证据拒绝执行。</li></ul><h3 id="6-团队有没有维护中间表示的能力"><a href="#6-团队有没有维护中间表示的能力" class="headerlink" title="6. 团队有没有维护中间表示的能力"></a>6. 团队有没有维护中间表示的能力</h3><p>Graph 和 Compiled Wiki 都不是“一次导入，永久准确”。它们需要 Owner、Schema、Lint、Eval、重建和迁移。</p><p>如果没人负责，最朴素的 Hybrid RAG 往往比一套陈旧的高级知识系统更可靠。</p><h2 id="我会怎样设计生产架构"><a href="#我会怎样设计生产架构" class="headerlink" title="我会怎样设计生产架构"></a>我会怎样设计生产架构</h2><p>生产架构需要提供一个统一的 Knowledge Contract：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> <span class="title class_">KnowledgeQuery</span> = &#123;</span><br><span class="line">  <span class="attr">query</span>: <span class="built_in">string</span>;</span><br><span class="line">  intent?: <span class="string">&#x27;lookup&#x27;</span> | <span class="string">&#x27;relation&#x27;</span> | <span class="string">&#x27;procedure&#x27;</span> | <span class="string">&#x27;global&#x27;</span>;</span><br><span class="line">  <span class="attr">scope</span>: &#123;</span><br><span class="line">    <span class="attr">tenantId</span>: <span class="built_in">string</span>;</span><br><span class="line">    <span class="attr">workspaceId</span>: <span class="built_in">string</span>;</span><br><span class="line">    <span class="attr">revision</span>: <span class="built_in">string</span>;</span><br><span class="line">    <span class="attr">principalId</span>: <span class="built_in">string</span>;</span><br><span class="line">  &#125;;</span><br><span class="line">  <span class="attr">budget</span>: &#123;</span><br><span class="line">    <span class="attr">maxTokens</span>: <span class="built_in">number</span>;</span><br><span class="line">    <span class="attr">maxLatencyMs</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><br><span class="line"><span class="keyword">type</span> <span class="title class_">Evidence</span> = &#123;</span><br><span class="line">  claim?: <span class="built_in">string</span>;</span><br><span class="line">  <span class="attr">content</span>: <span class="built_in">string</span>;</span><br><span class="line">  <span class="attr">sourceUri</span>: <span class="built_in">string</span>;</span><br><span class="line">  <span class="attr">sourceVersion</span>: <span class="built_in">string</span>;</span><br><span class="line">  sourceSpan?: &#123; <span class="attr">start</span>: <span class="built_in">number</span>; <span class="attr">end</span>: <span class="built_in">number</span> &#125;;</span><br><span class="line">  <span class="attr">retrievalPath</span>: <span class="built_in">string</span>[];</span><br><span class="line">  <span class="attr">confidence</span>: <span class="built_in">number</span>;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>Gateway 再按 Intent 路由：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line">lookup</span><br><span class="line">  → Hybrid Search</span><br><span class="line"></span><br><span class="line">relation / impact</span><br><span class="line">  → Entity Resolve</span><br><span class="line">  → Graph Traverse</span><br><span class="line">  → Raw Evidence Join</span><br><span class="line"></span><br><span class="line">procedure / repeated synthesis</span><br><span class="line">  → Compiled Index</span><br><span class="line">  → Read Page</span><br><span class="line">  → Follow Links</span><br><span class="line">  → Critical Claim Source Check</span><br><span class="line"></span><br><span class="line">global</span><br><span class="line">  → Community Summary / Compiled Overview</span><br><span class="line">  → 必要时分解 Subquery 回查原文</span><br></pre></td></tr></table></figure><p>返回值统一为 Evidence，而不是让某个后端直接生成最终答案。这样权限、版本、引用、预算和 Trace 才不会散落在各条路线里。</p><h2 id="分阶段落地：不要第一天同时建三套"><a href="#分阶段落地：不要第一天同时建三套" class="headerlink" title="分阶段落地：不要第一天同时建三套"></a>分阶段落地：不要第一天同时建三套</h2><h3 id="Phase-0：先建评测集"><a href="#Phase-0：先建评测集" class="headerlink" title="Phase 0：先建评测集"></a>Phase 0：先建评测集</h3><p>从 30–50 个真实问题开始，标注必要 Evidence、允许版本、禁止答案和拒答条件。没有这一步，后续只能靠“看起来更聪明”做选型。</p><h3 id="Phase-1：Hybrid-RAG"><a href="#Phase-1：Hybrid-RAG" class="headerlink" title="Phase 1：Hybrid RAG"></a>Phase 1：Hybrid RAG</h3><p>完成 Parse、Chunk、BM25、Vector、Metadata Filter、Rerank、Citation。先把单事实和局部解释做好。</p><p>升级信号：</p><ul><li>必要证据经常分散在三跳以上；</li><li>影响分析靠调大 Top K 仍不稳定；</li><li>大量问题反复综合同一批资料。</li></ul><h3 id="Phase-2：只为高价值关系建图"><a href="#Phase-2：只为高价值关系建图" class="headerlink" title="Phase 2：只为高价值关系建图"></a>Phase 2：只为高价值关系建图</h3><p>先选一个子域，例如订单—权益—政策关系。使用 Typed Edge，保留 Source Span，不追求覆盖所有名词。</p><p>验收条件：</p><ul><li>影响路径比 Hybrid RAG 的 Recall 更稳定；</li><li>Edge Precision 达到业务可接受标准；</li><li>增量更新耗时和成本可控。</li></ul><h3 id="Phase-3：编译高频知识"><a href="#Phase-3：编译高频知识" class="headerlink" title="Phase 3：编译高频知识"></a>Phase 3：编译高频知识</h3><p>从 10–20 个高频 Topic &#x2F; Procedure 开始。每个编译产物必须有 Source Manifest、Compiler Version、Status 与 Eval。</p><p>不要一上来把全部文档自动改写成 Wiki。编译范围越大，信息损失和陈旧页面越难定位。</p><h3 id="Phase-4：让路由器学会组合"><a href="#Phase-4：让路由器学会组合" class="headerlink" title="Phase 4：让路由器学会组合"></a>Phase 4：让路由器学会组合</h3><p>复杂问题可以先查编译页获得结构，再回 Vector Index 找最新原文，最后沿 Graph 验证影响路径。</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">Compiled Page 提供计划</span><br><span class="line">      ↓</span><br><span class="line">Graph 提供关系范围</span><br><span class="line">      ↓</span><br><span class="line">Hybrid Search 提供当前证据</span><br><span class="line">      ↓</span><br><span class="line">Evidence Gateway 合并、去重、校验</span><br></pre></td></tr></table></figure><p>到这个阶段，三条路线才形成互补。</p><h2 id="常见误判"><a href="#常见误判" class="headerlink" title="常见误判"></a>常见误判</h2><h3 id="“资料少，就用向量-RAG”"><a href="#“资料少，就用向量-RAG”" class="headerlink" title="“资料少，就用向量 RAG”"></a>“资料少，就用向量 RAG”</h3><p>资料少但关系密集，例如商品、会员、订单、活动与售后政策之间的关联查询，Graph 仍可能有价值。资料量不是唯一变量。</p><h3 id="“有-Graph-就不用-Vector”"><a href="#“有-Graph-就不用-Vector”" class="headerlink" title="“有 Graph 就不用 Vector”"></a>“有 Graph 就不用 Vector”</h3><p>图谱负责结构，原文负责细节。微软 GraphRAG 的 Local Search 本身也会组合 Knowledge Graph 与原始 Text Unit。Graph 不能替代所有文本证据。</p><h3 id="“编译一次，以后查询更便宜”"><a href="#“编译一次，以后查询更便宜”" class="headerlink" title="“编译一次，以后查询更便宜”"></a>“编译一次，以后查询更便宜”</h3><p>不一定。一项 2026 年的小规模预注册比较发现，LLM-compiled Wiki 更擅长跨论文连接与 Claim 级引用对齐，但测试设置下每次查询使用的 Token 反而明显高于 RAG；分解式 RAG 又追回了大部分综合能力差距。研究的核心结论很克制：组织证据、引用支撑和成本是三项不同能力，没有一个架构全胜。详见 <a href="https://arxiv.org/abs/2605.18490">Vector RAG vs LLM-Compiled Wiki</a>。</p><p>编译能否回本，要用自己的 Query 分布、更新频率和 Review 成本测。</p><h3 id="“Wiki-已经整理好了，可以直接执行”"><a href="#“Wiki-已经整理好了，可以直接执行”" class="headerlink" title="“Wiki 已经整理好了，可以直接执行”"></a>“Wiki 已经整理好了，可以直接执行”</h3><p>Wiki 是派生知识，不是权限系统。Agent 查询真实订单或执行退款、赔付等动作，仍要经过 Permission、Approval、Dry Run 和 Evidence 记录。</p><h3 id="“让-LLM-自动维护，维护成本接近零”"><a href="#“让-LLM-自动维护，维护成本接近零”" class="headerlink" title="“让 LLM 自动维护，维护成本接近零”"></a>“让 LLM 自动维护，维护成本接近零”</h3><p>LLM 降低了写页面和补链接的成本，却增加了事实校验、冲突处理、Schema 演进和错误传播风险。自动化的是体力活，不是责任。</p><h2 id="要不要用-我的判断框架"><a href="#要不要用-我的判断框架" class="headerlink" title="要不要用 &#x2F; 我的判断框架"></a>要不要用 &#x2F; 我的判断框架</h2><p>如果今天从零做一个知识助手，我不会把“向量、Graph、编译”写进同一张采购清单。</p><p><strong>值得马上做：</strong></p><ul><li>Hybrid Search；</li><li>Metadata &#x2F; Permission &#x2F; Revision Filter；</li><li>Source Span Citation；</li><li>一组包含拒答和旧版本干扰的 Eval。</li></ul><p><strong>出现明确瓶颈再做：</strong></p><ul><li>多跳依赖和影响分析不稳定时，加 Typed Graph；</li><li>高频综合和流程每次都被重复推理时，加 Versioned Compiled Knowledge；</li><li>跨整个语料的主题总结成为核心需求时，再评估 Community Report &#x2F; Global Search。</li></ul><p><strong>无论选哪条路线都不能省：</strong></p><ul><li>Raw Source 与派生索引分离；</li><li>Evidence 能回到 Source；</li><li>索引和编译产物可重建；</li><li>查询绑定 Scope、Revision 与 Permission；</li><li>没有足够证据时明确拒答。</li></ul><p>最后的架构判断可以压缩成一句话：</p><blockquote><p>Vector RAG 解决“相关内容在哪里”，GraphRAG 解决“这些对象怎么关联”，编译式 RAG 解决“哪些稳定知识值得提前整理并持续复用”。</p></blockquote><p>先让真实问题暴露知识形状，再选择表示方式。技术路线不应该从一张产品能力表里长出来，而应该从失败用例和评测数据里长出来。</p>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/fbf445faef3e/</id>
    <link href="https://blog.zisheng.pro/posts/fbf445faef3e/"/>
    <published>2026-07-24T06:00:00.000Z</published>
    <summary>向量 RAG、GraphRAG 和编译式 RAG 并不是一条从低级到高级的升级路线。本文用一个脱敏的跨行业客服知识助手贯穿三种方案，拆解知识问答、订单关联和服务流程如何命中证据，并给出从 MVP 到生产架构的选型矩阵、评测方法与组合策略。</summary>
    <title>向量 RAG、GraphRAG、编译式 RAG：用一个客服实战讲清如何选</title>
    <updated>2026-09-18T08:15:28.636Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="AI" scheme="https://blog.zisheng.pro/categories/AI/"/>
    <category term="Coding Agent" scheme="https://blog.zisheng.pro/tags/Coding-Agent/"/>
    <category term="Agentic Engineering" scheme="https://blog.zisheng.pro/tags/Agentic-Engineering/"/>
    <category term="工程效率" scheme="https://blog.zisheng.pro/tags/%E5%B7%A5%E7%A8%8B%E6%95%88%E7%8E%87/"/>
    <category term="项目管理" scheme="https://blog.zisheng.pro/tags/%E9%A1%B9%E7%9B%AE%E7%AE%A1%E7%90%86/"/>
    <category term="软件交付" scheme="https://blog.zisheng.pro/tags/%E8%BD%AF%E4%BB%B6%E4%BA%A4%E4%BB%98/"/>
    <content>
      <![CDATA[<p><img src="/images/%E5%B7%A5%E4%BD%9C%E6%B1%87%E6%8A%A5%E6%A1%86%E6%9E%B6%E9%80%89%E6%8B%A9.webp" alt="Coding Agent 交付汇报的 6 种状态：完成交付、进行中、失败阻塞、Bug 诊断、方案决策和评审发布"></p><p><em>Coding Agent 的汇报要让下一个执行者能复现、验证或继续推进，而不是只知道“做了很多事”。</em></p><p>人写周报，常常是为了同步进度。Coding Agent 的汇报不一样：读者往往会顺着它继续改代码、跑测试、审查 diff，或者判断是否能发布。此时“已完成”“已修复”没有证据就没有意义。</p><p>我更愿意把 Agent 汇报看成一次交接：它应该告诉下一个人，代码现在处于什么状态、改动在哪里、已验证到哪一步、还有什么没有验证。</p><h2 id="一句话总结"><a href="#一句话总结" class="headerlink" title="一句话总结"></a>一句话总结</h2><p>Coding Agent 汇报的最小单位不是“做了什么”，而是<strong>可核验状态</strong>：变更范围、验证证据、Git 状态、部署状态和下一步。先给状态，再给证据；不要用构建通过推断线上可用，也不要用推送成功推断部署完成。</p><table><thead><tr><th>场景</th><th>读者要判断什么</th><th>必填信息</th></tr></thead><tbody><tr><td>完成交付</td><td>能否接手或合并？</td><td>变更、验证、工作区&#x2F;提交&#x2F;推送状态</td></tr><tr><td>进行中</td><td>还差什么，何时能继续？</td><td>当前落点、未完成范围、依赖、下一步</td></tr><tr><td>失败阻塞</td><td>卡在哪里，谁能解除？</td><td>根因、影响、已试路径、需要的外部输入</td></tr><tr><td>Bug 诊断</td><td>修复是否覆盖问题？</td><td>现象、影响范围、根因、回归证据</td></tr><tr><td>方案决策</td><td>现在该选哪条路？</td><td>建议、选项、取舍、截止时间</td></tr><tr><td>评审&#x2F;发布</td><td>能否进入下一环境？</td><td>Findings、风险、验证、回滚条件</td></tr></tbody></table><h2 id="1-完成交付：给出可以复跑的证据"><a href="#1-完成交付：给出可以复跑的证据" class="headerlink" title="1. 完成交付：给出可以复跑的证据"></a>1. 完成交付：给出可以复跑的证据</h2><p>完成不等于写完代码。它至少包含四层彼此独立的状态：工作区、提交、推送、部署。它们必须分别报告。</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">【结论】文章页的目录滚动高亮已修复。</span><br><span class="line">【变更】修改 src/toc.ts、src/article.css；未改路由和数据结构。</span><br><span class="line">【验证】pnpm test toc 通过；localhost 逐段滚动验证 6 个标题均切换高亮。</span><br><span class="line">【状态】工作区干净；已提交 abc123；已推送 origin/main；未验证生产环境。</span><br><span class="line">【风险】Info：Safari 尚未手测。</span><br></pre></td></tr></table></figure><p>这比“修复完成，已发布”多了一点长度，却少了三个常见误解：构建通过不等于功能正确，推送成功不等于 CI 已部署，部署成功也不等于用户路径已经验证。</p><p>项目状态报告通常也会要求摘要、阻塞、指标和下一步；对 Coding Agent 来说，要把这些信息进一步落到具体文件、命令和环境。<a href="https://www.atlassian.com/agile/project-management/status-report">Atlassian 的项目状态报告指南</a>提供了状态、风险和行动项的基础骨架。</p><h2 id="2-进行中：写清“已经落在哪”和“还没有做什么”"><a href="#2-进行中：写清“已经落在哪”和“还没有做什么”" class="headerlink" title="2. 进行中：写清“已经落在哪”和“还没有做什么”"></a>2. 进行中：写清“已经落在哪”和“还没有做什么”</h2><p>进行中最怕伪完成。不要说“完成了 80%”，因为没有人知道剩下的 20% 是一个 CSS 微调，还是一套迁移和回滚逻辑。</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">【当前落点】Provider Adapter 已抽出接口，Codex 实现已迁移。</span><br><span class="line">【未完成】Qoder Adapter、失败重试、端到端回归尚未开始。</span><br><span class="line">【依赖】需要确认 Qoder 最新 SDK 的流式事件字段。</span><br><span class="line">【下一步】确认字段后补 Adapter 映射与两条集成用例。</span><br><span class="line">【状态】仅本地工作区修改，未提交、未推送。</span><br></pre></td></tr></table></figure><p>这里“有意未动的范围”很重要。它把真实边界交给下一位执行者，避免对方误以为某个目录只是漏看了。</p><h2 id="3-失败阻塞：先说根因、影响和解除条件"><a href="#3-失败阻塞：先说根因、影响和解除条件" class="headerlink" title="3. 失败阻塞：先说根因、影响和解除条件"></a>3. 失败阻塞：先说根因、影响和解除条件</h2><p>失败报告不需要整段日志。日志是证据链接，不是结论本身。先给读者足够的信息判断是否要介入：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">【阻塞】生产 API 返回 403，部署无法验证。</span><br><span class="line">【根因】当前 token 缺少 release:read Scope；代码请求已在本地 mock 验证。</span><br><span class="line">【影响】不影响本地实现与单测，阻塞生产 Smoke Test。</span><br><span class="line">【已尝试】刷新 token、切换 staging 凭证，均无效。</span><br><span class="line">【需要】由凭证 Owner 补充 Scope，或提供可用的 staging token。</span><br></pre></td></tr></table></figure><p>“外部依赖未就绪”不够具体。谁能解决、需要什么、影响到哪一步，才是可执行的阻塞信息。</p><h2 id="4-Bug-诊断：把现象、根因和修复证据拆开"><a href="#4-Bug-诊断：把现象、根因和修复证据拆开" class="headerlink" title="4. Bug 诊断：把现象、根因和修复证据拆开"></a>4. Bug 诊断：把现象、根因和修复证据拆开</h2><p>Bug 汇报最容易把猜测写成根因。正确顺序是现象、范围、已确认根因、修复和回归证据；尚未确认的内容要标成假设。</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">【现象】中文输入法确认候选词时，Enter 被当成消息发送。</span><br><span class="line">【范围】Chrome 与桌面 WebView 复现；英文输入不受影响。</span><br><span class="line">【根因】keydown 未检查 composition 状态，已用最小页面复现。</span><br><span class="line">【修复】加入 isComposing 与 keyCode 229 guard。</span><br><span class="line">【回归】中文拼音、五笔、英文 Enter、Shift+Enter 换行均通过手测。</span><br></pre></td></tr></table></figure><p>如果修复没有回归范围，下一位开发者无法判断这次改动是解决了根因，还是只是把症状藏起来。</p><h2 id="5-方案决策：把分析压缩成能拍板的选择"><a href="#5-方案决策：把分析压缩成能拍板的选择" class="headerlink" title="5. 方案决策：把分析压缩成能拍板的选择"></a>5. 方案决策：把分析压缩成能拍板的选择</h2><p>Coding Agent 不只交付代码，也会遇到依赖、架构和迁移路径的选择。此时不需要把调研过程逐条搬出来，而要给出自己的建议和明确取舍。</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">【建议】首期采用方案 B：保留现有 SQLite，新增异步索引表。</span><br><span class="line">【收益】迁移风险低，可在当前版本上线。</span><br><span class="line">【代价】复杂查询继续由应用层处理，第二阶段再评估独立检索服务。</span><br><span class="line">【备选】A 一次性迁移到新存储，能力完整但多 3 周迁移和回滚成本。</span><br><span class="line">【需要决定】请在周三 17:00 前确认 B；确认后我按 B 落地并补迁移验证。</span><br></pre></td></tr></table></figure><p>决策文档的关键是让决策人知道该批准什么。Atlassian 的 <a href="https://www.atlassian.com/dam/jcr%3Ac13e0440-f990-4e13-b226-1f3cf6eb37d4/Decision.pdf">Decision 模板</a>也把状态、影响、驱动者、批准者和截止时间单独列出，避免“讨论过”被误认为“决定了”。</p><h2 id="6-评审与发布：把“能合”与“能上”分开"><a href="#6-评审与发布：把“能合”与“能上”分开" class="headerlink" title="6. 评审与发布：把“能合”与“能上”分开"></a>6. 评审与发布：把“能合”与“能上”分开</h2><p>Code Review 的结论和发布结论不是同一件事。</p><table><thead><tr><th>结论</th><th>需要的证据</th></tr></thead><tbody><tr><td>可合并</td><td>Findings 已处理或有明确豁免；测试覆盖改动范围</td></tr><tr><td>可发布</td><td>构建、迁移、配置和回滚路径已验证</td></tr><tr><td>已部署</td><td>部署系统返回成功，且目标环境可访问</td></tr><tr><td>已验收</td><td>关键用户路径、指标或 Smoke Test 已在目标环境通过</td></tr></tbody></table><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">【Review】未发现阻断性问题；1 个 Warning：空状态未覆盖窄屏。</span><br><span class="line">【发布】构建与 migration dry-run 通过；尚未执行生产部署。</span><br><span class="line">【回滚】保留上一个镜像标签，迁移为向后兼容。</span><br><span class="line">【下一步】补窄屏截图后发起部署。</span><br></pre></td></tr></table></figure><p>把这些状态拆开，会减少“PR 合了所以线上应该没问题”这种错误推断。</p><h2 id="什么时候不该套汇报模板"><a href="#什么时候不该套汇报模板" class="headerlink" title="什么时候不该套汇报模板"></a>什么时候不该套汇报模板</h2><p>短问题、单个命令、纯事实查询不需要包装成六段报告。模板只用于复杂、可持续或有风险的交付。若读者只需要一个命令或一个结论，直接给答案更好。</p><p>对涉及规划和大范围投入的任务，叙事型方案仍然有价值。Amazon 的 Narrative 方法要求把规划写成连续论证，AWS 的公开说明提到此类文档用于团队和组织规划，正文通常限制在六页以内。<a href="https://aws.amazon.com/blogs/startups/startup-advice-how-to-write-a-narrative/">AWS Startups 对 Narrative 的介绍</a>适合在 Agent 完成调研后，沉淀为需要人拍板的方案，而不是替代每次代码交付的短报告。</p><h2 id="我的判断框架"><a href="#我的判断框架" class="headerlink" title="我的判断框架"></a>我的判断框架</h2><p>先问下一个读者要做什么，再决定输出粒度：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><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></pre></td></tr></table></figure><p>对 Coding Agent，好的汇报不是一段漂亮总结，而是一份可以接手的工程状态：事实可定位、验证可复跑、风险有边界、下一步有人能执行。</p>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/96ff37611317/</id>
    <link href="https://blog.zisheng.pro/posts/96ff37611317/"/>
    <published>2026-07-23T12:15:00.000Z</published>
    <summary>Coding Agent 的汇报不是给管理者读的周报，而是交付给下一个执行者的状态接口。本文拆分完成交付、进行中、失败阻塞、Bug 诊断、方案决策和评审发布 6 种场景，说明每种状态必须留下哪些变更、验证与 Git/部署证据。</summary>
    <title>Coding Agent 的交付汇报：6 种状态怎么写</title>
    <updated>2026-09-18T08:15:28.629Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="AI" scheme="https://blog.zisheng.pro/categories/AI/"/>
    <category term="AI Native" scheme="https://blog.zisheng.pro/tags/AI-Native/"/>
    <category term="Knowledge Base" scheme="https://blog.zisheng.pro/tags/Knowledge-Base/"/>
    <category term="RAG" scheme="https://blog.zisheng.pro/tags/RAG/"/>
    <category term="GraphRAG" scheme="https://blog.zisheng.pro/tags/GraphRAG/"/>
    <category term="Agentic Retrieval" scheme="https://blog.zisheng.pro/tags/Agentic-Retrieval/"/>
    <category term="Hybrid Search" scheme="https://blog.zisheng.pro/tags/Hybrid-Search/"/>
    <category term="AI Infra" scheme="https://blog.zisheng.pro/tags/AI-Infra/"/>
    <content>
      <![CDATA[<p><img src="/images/ai-native-knowledge-base-architecture.webp" alt="AI Native 知识库参考架构：给人维护、给 Agent 调用、给双方追溯"></p><p><em>AI Native 知识库参考架构：Source of Truth 与 Index 分离，Knowledge Gateway 统一处理 Query Rewrite、Scope、Permission、Context Budget、Evidence 与 Trace。</em></p><p>最近几天，我围绕一个 Agent 应用连续讨论和验证了 FTS、BM25、Vector Search、RAG、GraphRAG、LLM Wiki、代码结构分析、Query Rewrite 与 Evidence。名词越查越多，最后暴露出的缺口却很具体：缺的并非更大的 Vector Database，而是一套能让 Agent 稳定取用知识、同时让人审查依据的基础设施。</p><p>从前端转向 Agent &#x2F; AI Infra 后，我发现知识库的默认用户变了。人会打开目录、搜索标题、阅读页面；Agent 不会沿着侧边栏一页页翻文档，它需要一个带 Scope、版本、权限、证据和失败语义的接口。只做“把文档塞进向量库”，Demo 很快，系统却很难进入真实执行链路。</p><h2 id="一句话总结"><a href="#一句话总结" class="headerlink" title="一句话总结"></a>一句话总结</h2><p>如果系统只是让 AI 搜文档、回答问题，叫 <strong>AI 知识库</strong> 足够准确；如果知识的生产、索引、检索、引用、版本、权限和反馈闭环都围绕 Agent 的运行方式设计，才应该叫 <strong>AI Native 知识库</strong>。</p><p>这套系统同时服务人和 Agent，只是职责不同：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">人维护事实源</span><br><span class="line">    ↓</span><br><span class="line">系统编译成 Agent 可检索的索引与工具接口</span><br><span class="line">    ↓</span><br><span class="line">Agent 返回带 Evidence / Citation 的回答或行动依据</span><br><span class="line">    ↓</span><br><span class="line">人和系统共同验证、纠错、发布新版本</span><br></pre></td></tr></table></figure><blockquote><p>给人维护，给 Agent 调用，给双方追溯。</p></blockquote><h2 id="到底叫-AI-知识库，还是-AI-Native-知识库"><a href="#到底叫-AI-知识库，还是-AI-Native-知识库" class="headerlink" title="到底叫 AI 知识库，还是 AI Native 知识库"></a>到底叫 AI 知识库，还是 AI Native 知识库</h2><p>“AI 知识库”是宽泛的产品分类，“AI Native 知识库”是更严格的架构判断。判断标准在于系统把谁当成一等消费者。</p><table><thead><tr><th>形态</th><th>主要消费者</th><th>典型能力</th><th>合适的名字</th></tr></thead><tbody><tr><td>Wiki &#x2F; 文档站</td><td>人</td><td>目录、编辑、全文搜索、权限</td><td>知识库</td></tr><tr><td>文档问答</td><td>人通过 Chat UI 使用</td><td>文档上传、Vector Search、RAG 回答</td><td>AI 知识库</td></tr><tr><td>Agent 知识基础设施</td><td>Agent + 人</td><td>多源检索、Scope、Revision、Evidence、Citation、Trace、Eval</td><td>AI Native 知识库</td></tr></tbody></table><p>我会用六个条件判断一个系统是否真的 AI Native：</p><ol><li>Agent 通过稳定的 Tool &#x2F; API 获取知识，而不是抓取某个 Wiki 页面。</li><li>Source of Truth 与检索索引分离，索引损坏后可以重建。</li><li>每次检索都绑定 Tenant、Workspace、Revision 和 Permission。</li><li>返回的不只是答案，而是 Evidence、Citation、Source 与有效版本。</li><li>系统能按问题做 Query Rewrite、拆分子问题、路由不同知识源。</li><li>检索与回答进入 Eval、Trace 和反馈闭环，错误能够被定位和修复。</li></ol><p>只满足“上传 PDF 后能聊天”，我不会把它叫 AI Native。这个词应该表达架构差异，而不是给普通 RAG 换一个更时髦的包装。</p><h2 id="知识库究竟是给人看的，还是给-Agent-看的"><a href="#知识库究竟是给人看的，还是给-Agent-看的" class="headerlink" title="知识库究竟是给人看的，还是给 Agent 看的"></a>知识库究竟是给人看的，还是给 Agent 看的</h2><p>我把职责拆成三层。</p><h3 id="第一层：事实源给人维护"><a href="#第一层：事实源给人维护" class="headerlink" title="第一层：事实源给人维护"></a>第一层：事实源给人维护</h3><p>Spec、ADR、Repository、数据库记录、产品规则和正式流程，首先要能被 Owner 阅读、评审和修改。Markdown、Git、文档平台和业务系统依然重要，因为人需要理解完整上下文，也需要承担发布责任。</p><p>人需要在这一层回答五个问题：</p><ul><li>谁写的；</li><li>谁批准的；</li><li>哪个版本有效；</li><li>冲突时听谁的；</li><li>原文在哪里。</li></ul><p>如果原始内容只存在于 Vector Database 里，人无法审查，也无法稳定重建，这不是事实源，只是一份难以治理的派生数据。</p><h3 id="第二层：索引和接口给-Agent-使用"><a href="#第二层：索引和接口给-Agent-使用" class="headerlink" title="第二层：索引和接口给 Agent 使用"></a>第二层：索引和接口给 Agent 使用</h3><p>Agent 不需要每次读取整本 Wiki。它需要结构化、可过滤、可预算的 Knowledge Context：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;query&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;scope&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;workspace&quot;</span><span class="punctuation">:</span> <span class="string">&quot;project-a&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;revision&quot;</span><span class="punctuation">:</span> <span class="string">&quot;rev-2026-07-23&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;principal&quot;</span><span class="punctuation">:</span> <span class="string">&quot;current-user&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;evidence&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;content&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;sourceUri&quot;</span><span class="punctuation">:</span> <span class="string">&quot;kb://runbook/release-rollback&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;sourceVersion&quot;</span><span class="punctuation">:</span> <span class="string">&quot;v7&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;score&quot;</span><span class="punctuation">:</span> <span class="number">0.91</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><span class="line">  <span class="attr">&quot;traceId&quot;</span><span class="punctuation">:</span> <span class="string">&quot;ktrace_xxx&quot;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p>这份结果要控制 Context Window 预算，也要允许程序检查。Agent 关心的是：哪些片段与当前任务相关、是否有权限、是否属于当前版本、证据能否支撑下一步行动。</p><h3 id="第三层：Evidence-给双方验证"><a href="#第三层：Evidence-给双方验证" class="headerlink" title="第三层：Evidence 给双方验证"></a>第三层：Evidence 给双方验证</h3><p>Evidence 是这两层之间的桥。对 Agent，它是生成回答和执行计划的 Grounding；对人，它是回到原始事实、判断答案是否可信的入口。</p><p>界面不能只展示一段自然语言答案，还应允许查看：</p><ul><li>引用了哪些片段；</li><li>每个片段来自哪个 Source；</li><li>检索时绑定了哪个 Revision；</li><li>哪些 Query 被改写或拆分；</li><li>哪个检索器命中了它；</li><li>Agent 最终使用了哪些 Evidence。</li></ul><p>知识库的正文给人看，编译结果给 Agent 用，Evidence 则必须让双方都看得懂。</p><h2 id="知识库技术发展历程"><a href="#知识库技术发展历程" class="headerlink" title="知识库技术发展历程"></a>知识库技术发展历程</h2><p><img src="/images/knowledge-stack-evolution.webp" alt="知识库技术发展历程：从搜索系统到 Agentic Retrieval"></p><p>这段历史最容易被讲成一条“后浪淘汰前浪”的升级路线：Vector Search 淘汰 BM25，GraphRAG 淘汰 RAG，Agentic Retrieval 再淘汰 GraphRAG。这个理解不对。它们解决的是不同层次的问题，今天仍然会同时出现在一套系统里。</p><h3 id="1990s–2010s：从倒排索引到-BM25"><a href="#1990s–2010s：从倒排索引到-BM25" class="headerlink" title="1990s–2010s：从倒排索引到 BM25"></a>1990s–2010s：从倒排索引到 BM25</h3><p>最早的问题很直接：文档很多，怎样快速找到包含某个词的内容？</p><p>Inverted Index 把“词 → 文档”建立映射，FTS 负责全文检索，BM25 负责根据词频、稀有程度和文档长度排序。它们不理解抽象语义，却非常擅长精确对象：函数名、错误码、配置项、订单号、API 名称和领域术语。</p><p>这类能力没有过时。只要系统里存在精确标识符，Keyword Search 就仍然是不可替代的召回通道。</p><h3 id="2019–2020：Dense-Retrieval-与-Vector-Search"><a href="#2019–2020：Dense-Retrieval-与-Vector-Search" class="headerlink" title="2019–2020：Dense Retrieval 与 Vector Search"></a>2019–2020：Dense Retrieval 与 Vector Search</h3><p>Embedding 把 Query 和文档映射到同一个向量空间，检索开始从“出现了同一个词”走向“表达了相近的意思”。2020 年的 <a href="https://arxiv.org/abs/2004.04906">Dense Passage Retrieval</a> 让 Dense Retrieval 在开放域问答中成为重要路线。</p><p>Vector Search 解决了同义表达和自然语言问题。例如“怎么固定一次工作区使用的内容”和“Workspace Revision 是什么”可能指向同一概念，即使它们共享的关键词很少。</p><p>但语义相似不等于业务上正确。Vector Search 可能把相似概念混在一起，也可能让精确的类名和错误码沉到后面。它增加了一种召回能力，没有取代关键词检索。</p><h3 id="2020–2022：RAG-把检索接到生成模型前面"><a href="#2020–2022：RAG-把检索接到生成模型前面" class="headerlink" title="2020–2022：RAG 把检索接到生成模型前面"></a>2020–2022：RAG 把检索接到生成模型前面</h3><p>2020 年的 <a href="https://arxiv.org/abs/2005.11401">RAG 论文</a> 把 Parametric Memory 与可检索的 Non-parametric Memory 结合起来。工程上最朴素的 RAG 流程是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Query → Retrieve Top K → 拼进 Prompt → LLM Generate</span><br></pre></td></tr></table></figure><p>从这里开始，知识库不只返回文档，还开始参与回答。系统质量也不再只由搜索相关性决定：错误的 Chunk 会诱导模型自信地答错，没有证据时模型是否拒答，Citation 是否真的支撑 Claim，都成为新的问题。</p><h3 id="2023–2024：Production-RAG"><a href="#2023–2024：Production-RAG" class="headerlink" title="2023–2024：Production RAG"></a>2023–2024：Production RAG</h3><p>RAG 从 Demo 进入生产后，“选一个 Embedding Model + Vector Database”很快暴露出边界。生产链路开始加入：</p><ul><li>Keyword + Vector 的 Hybrid Search；</li><li>RRF 或其他 Fusion；</li><li>Cross-encoder &#x2F; LLM Reranker；</li><li>Metadata Filter；</li><li>结构化或 Contextual Chunking；</li><li>Citation、Groundedness 与检索评测；</li><li>权限、版本、增量更新、延迟和成本控制。</li></ul><p><a href="https://www.elastic.co/docs/solutions/search/hybrid-search">Elastic 的官方文档</a>直接把 Hybrid Search 定义为全文检索与 Vector Search 的组合，并推荐用 RRF 合并排名。<a href="https://www.anthropic.com/engineering/contextual-retrieval">Anthropic 的 Contextual Retrieval</a>则把文档级上下文补回每个 Chunk，再同时建立 Contextual Embedding 和 Contextual BM25，并叠加 Rerank。</p><p>到这一步，RAG 已经从 Prompt 技巧扩展成 Search &#x2F; Data &#x2F; Eval 工程。</p><h3 id="2024–：GraphRAG-处理关系和全局问题"><a href="#2024–：GraphRAG-处理关系和全局问题" class="headerlink" title="2024–：GraphRAG 处理关系和全局问题"></a>2024–：GraphRAG 处理关系和全局问题</h3><p>GraphRAG 显式抽取 Entity、Relationship、Claim 和 Community，让检索能回答两类普通 Top K RAG 不擅长的问题：</p><ul><li>局部关系：某个实体与哪些对象相关，依赖链是什么；</li><li>全局问题：整批资料有哪些主要主题、组织和冲突。</li></ul><p><a href="https://microsoft.github.io/graphrag/">Microsoft GraphRAG</a>同时保留 Basic Search、Local Search、Global Search 和 DRIFT Search。Local Search 会把图中的实体关系与原始文本片段组合，Global Search 则基于 Community Report 做全局汇总。这本身就说明 GraphRAG 不是 RAG 的替代品，而是增加了适合关系型问题的索引与 Query Mode。</p><p>实体和关系抽取需要额外模型调用，图谱可能有噪声，增量更新、冲突和版本治理也更复杂。Microsoft 的官方入门文档直接提醒 GraphRAG 索引会消耗大量 LLM 资源。没有真实的关系查询，就不该为了架构图好看而上 GraphRAG。</p><h3 id="2025–2026：Agentic-Retrieval"><a href="#2025–2026：Agentic-Retrieval" class="headerlink" title="2025–2026：Agentic Retrieval"></a>2025–2026：Agentic Retrieval</h3><p>普通 RAG 通常“搜一次”；Agentic Retrieval 会先理解任务，再决定搜什么、去哪里搜、是否继续追问。</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">用户任务</span><br><span class="line">  ↓</span><br><span class="line">Query Planning / Rewrite</span><br><span class="line">  ↓</span><br><span class="line">拆分多个 Subquery</span><br><span class="line">  ↓</span><br><span class="line">选择共享知识、当前文件、代码结构或实时系统</span><br><span class="line">  ↓</span><br><span class="line">并行 Keyword / Vector / Hybrid Search</span><br><span class="line">  ↓</span><br><span class="line">Rerank + Evidence Merge</span><br><span class="line">  ↓</span><br><span class="line">返回 Citation 与 Activity Trace</span><br></pre></td></tr></table></figure><p><a href="https://learn.microsoft.com/en-us/azure/search/agentic-retrieval-overview">Azure AI Search 的 Agentic Retrieval</a>已经把这个过程产品化：根据 Query 和 Conversation History 生成聚焦的 Subquery，并行执行 Keyword、Vector 或 Hybrid Search，经过 Semantic Rerank 后保留引用与执行日志。</p><p>简单查询直接走低成本路径更稳。多意图、多来源或需要多跳证据时，再让 LLM 参与检索规划。</p><h2 id="这些技术之间到底是什么关系"><a href="#这些技术之间到底是什么关系" class="headerlink" title="这些技术之间到底是什么关系"></a>这些技术之间到底是什么关系</h2><p>我现在按层级理解它们，不再把这些名词排成版本号：</p><table><thead><tr><th>概念</th><th>所在层</th><th>它解决什么</th><th>它不负责什么</th></tr></thead><tbody><tr><td>FTS &#x2F; BM25</td><td>Retrieval</td><td>精确词召回与排序</td><td>不理解深层语义</td></tr><tr><td>Embedding &#x2F; Vector Search</td><td>Retrieval</td><td>同义表达与语义召回</td><td>不保证精确标识符优先</td></tr><tr><td>Hybrid &#x2F; RRF</td><td>Retrieval</td><td>合并关键词与语义结果</td><td>不验证证据是否正确</td></tr><tr><td>Reranker</td><td>Ranking</td><td>对候选结果做更精细排序</td><td>不能召回候选集中不存在的内容</td></tr><tr><td>RAG</td><td>Generation Workflow</td><td>用检索结果约束生成</td><td>不等于知识治理</td></tr><tr><td>GraphRAG</td><td>Structured Retrieval</td><td>关系、多跳、全局主题</td><td>不适合默认替代所有检索</td></tr><tr><td>LLM Wiki</td><td>Knowledge Engineering</td><td>组织事实源、知识页面、引用和演进</td><td>不是一种单独的检索算法</td></tr><tr><td>Agentic Retrieval</td><td>Runtime Orchestration</td><td>改写、拆问、选源、迭代和留 Trace</td><td>不应绕过权限与治理</td></tr><tr><td>AI Native 知识库</td><td>System Architecture</td><td>把以上能力变成 Agent 可依赖的基础设施</td><td>不是某个数据库或框架名</td></tr></tbody></table><p>“LLM Wiki”并不是像 BM25 那样有严格定义的算法名。它更像一种产品和知识工程形态：让 LLM 帮助摄取、整理、链接、总结和维护知识页面。它可以使用 RAG，也可以使用 GraphRAG，但不会让底层检索失去用武之地。</p><h2 id="2026-年搭建知识库的最佳实践"><a href="#2026-年搭建知识库的最佳实践" class="headerlink" title="2026 年搭建知识库的最佳实践"></a>2026 年搭建知识库的最佳实践</h2><h3 id="1-先定义事实源，再选-Vector-Database"><a href="#1-先定义事实源，再选-Vector-Database" class="headerlink" title="1. 先定义事实源，再选 Vector Database"></a>1. 先定义事实源，再选 Vector Database</h3><p>先列出哪些系统有资格声明事实：正式 Spec、ADR、当前 Repository、业务数据库、审批记录、Runbook。会议记录、聊天记录和自动总结通常只能算 Raw Knowledge，不能自动覆盖正式结论。</p><p>我通常采用这样的优先级：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">已发布 Knowledge Asset</span><br><span class="line">&gt; 当前 Repository Revision</span><br><span class="line">&gt; 经确认的业务记录</span><br><span class="line">&gt; Raw Document / Meeting / Chat</span><br><span class="line">&gt; 模型生成的 Summary</span><br></pre></td></tr></table></figure><p>“更新时间更晚”不等于“权威性更高”。</p><h3 id="2-Source、Asset-与-Index-必须分开"><a href="#2-Source、Asset-与-Index-必须分开" class="headerlink" title="2. Source、Asset 与 Index 必须分开"></a>2. Source、Asset 与 Index 必须分开</h3><p>这三个对象很容易被混成“已经导入知识库”：</p><ul><li>Source：原始事实在哪里；</li><li>Knowledge Asset：经过清洗、归属、审核和发布的知识版本；</li><li>Index：为了检索生成的 Chunk、Embedding、Posting List、Entity 与 Edge。</li></ul><p>Index 应该是可重建的 Projection，不应该成为第二事实源。Embedding Model、Chunking Strategy 或 Schema 变化时，正确动作是从 Source &#x2F; Asset 重建索引，而不是让旧向量永久背负历史包袱。</p><h3 id="3-Chunk-不是按固定字数切完就结束"><a href="#3-Chunk-不是按固定字数切完就结束" class="headerlink" title="3. Chunk 不是按固定字数切完就结束"></a>3. Chunk 不是按固定字数切完就结束</h3><p>Chunk 至少要保留标题层级、表格语义、代码符号、列表边界和来源位置。不同内容应该用不同策略：</p><table><thead><tr><th>内容</th><th>优先切法</th></tr></thead><tbody><tr><td>Markdown &#x2F; Spec</td><td>按 Heading 与语义段落</td></tr><tr><td>API 文档</td><td>按 Endpoint &#x2F; Symbol</td></tr><tr><td>代码</td><td>按函数、类、模块与调用关系</td></tr><tr><td>表格</td><td>保留表头与相关行</td></tr><tr><td>长 PDF</td><td>结构解析后再按 Section</td></tr></tbody></table><p>不要迷信一个全局 Chunk Size。OpenAI 的 <a href="https://developers.openai.com/api/docs/guides/retrieval">Retrieval 文档</a>允许自定义 Chunk Size 和 Overlap，也支持文件 Attribute Filter；这类参数应该由 Eval 驱动，而不是从教程里复制一个数字。</p><p>如果 Chunk 脱离父文档后不知道自己在说谁，可以给它增加简短的 Context Prefix，再建立 Keyword 和 Vector Index。</p><h3 id="4-Hybrid-Search-应该是团队知识的默认候选"><a href="#4-Hybrid-Search-应该是团队知识的默认候选" class="headerlink" title="4. Hybrid Search 应该是团队知识的默认候选"></a>4. Hybrid Search 应该是团队知识的默认候选</h3><p>对于只包含精确技术术语的小型知识库，BM25 足够简单可靠；一旦用户会用自然语言、中文或同义词提问，Keyword + Vector 通常更稳。</p><p>推荐链路是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">Keyword Candidates ─┐</span><br><span class="line">                    ├→ RRF / Weighted Fusion → Reranker → Top Evidence</span><br><span class="line">Vector Candidates ──┘</span><br></pre></td></tr></table></figure><p>先并行扩大 Recall，再用 Reranker 提升 Precision。不要直接拿两个分数相加，BM25 Score 与 Vector Similarity 往往不在同一标度上。</p><h3 id="5-Scope-与-Permission-要在检索前生效"><a href="#5-Scope-与-Permission-要在检索前生效" class="headerlink" title="5. Scope 与 Permission 要在检索前生效"></a>5. Scope 与 Permission 要在检索前生效</h3><p>先检索全部内容、再在答案阶段隐藏结果，是危险设计。过滤至少要覆盖：</p><ul><li>tenant &#x2F; organization；</li><li>workspace &#x2F; project；</li><li>source；</li><li>revision &#x2F; effective time；</li><li>principal &#x2F; role；</li><li>confidentiality；</li><li>content status。</li></ul><p>安全边界应尽量在 Candidate Retrieval 前收紧，避免跨项目内容进入模型上下文。对 Agent 来说，这既是数据权限问题，也是 Prompt Injection 与错误行动的隔离问题。</p><h3 id="6-Query-Rewrite-先做确定性版本"><a href="#6-Query-Rewrite-先做确定性版本" class="headerlink" title="6. Query Rewrite 先做确定性版本"></a>6. Query Rewrite 先做确定性版本</h3><p>中文、缩写和领域词经常让 FTS 零召回。第一阶段不必立刻引入昂贵的 LLM Query Planner，可以先从受治理的 Glossary 生成确定性映射：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">“工作区版本是什么”</span><br><span class="line">→ “Workspace Revision”</span><br></pre></td></tr></table></figure><p>先查改写结果，零命中再回退原始 Query。这样行为可解释、可测试，也能快速验证问题到底在分词、Query 还是内容本身。</p><p>当复杂 Query 占比上升，再增加 Multi-query、Subquery Planning 和 Source Routing。只有任务复杂度值得支付规划成本时，才进入 Agentic Retrieval。</p><h3 id="7-Rerank-之前先保证-Candidate-Recall"><a href="#7-Rerank-之前先保证-Candidate-Recall" class="headerlink" title="7. Rerank 之前先保证 Candidate Recall"></a>7. Rerank 之前先保证 Candidate Recall</h3><p>Reranker 只能重排已有候选。正确文档没有进入 Top 50，后面的模型再强也救不回来。</p><p>因此评测要分层：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">Retrieval Eval：正确 Evidence 是否进入候选集？</span><br><span class="line">Ranking Eval：正确 Evidence 是否排到足够靠前？</span><br><span class="line">Generation Eval：回答是否被 Evidence 支撑？</span><br><span class="line">Agent Eval：行动是否遵守 Scope、Permission 与事实？</span><br></pre></td></tr></table></figure><p>把最终回答的好坏全部归因于 LLM，会让 Search 问题长期藏在 Prompt 里。</p><h3 id="8-每个关键-Claim-都应能回到-Evidence"><a href="#8-每个关键-Claim-都应能回到-Evidence" class="headerlink" title="8. 每个关键 Claim 都应能回到 Evidence"></a>8. 每个关键 Claim 都应能回到 Evidence</h3><p>Citation 不是装饰性的文末链接。一个可用的 Citation 至少包含：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">source_uri + source_version + location + content_hash</span><br></pre></td></tr></table></figure><p>如果 Source 更新后 Citation 指向另一段内容，审计就失效了。对于会触发写文件、发消息、部署或审批的 Agent，关键决策必须保留 Knowledge Trace：用了哪些 Evidence、哪个版本、如何生成结论。</p><h3 id="9-明确“没有答案”也是正确结果"><a href="#9-明确“没有答案”也是正确结果" class="headerlink" title="9. 明确“没有答案”也是正确结果"></a>9. 明确“没有答案”也是正确结果</h3><p>知识库缺少证据时，系统应该返回 <code>no_evidence</code>，而不是用模型常识补一段看似合理的答案。评测集中必须有反向样本：</p><ul><li>知识库根本没有答案；</li><li>只有过期版本有答案；</li><li>当前用户无权访问答案；</li><li>两个正式 Source 互相冲突。</li></ul><p>这几类问题比普通命中更能检验知识系统是否可信。</p><h3 id="10-用-Eval-决定升级，不用架构焦虑决定升级"><a href="#10-用-Eval-决定升级，不用架构焦虑决定升级" class="headerlink" title="10. 用 Eval 决定升级，不用架构焦虑决定升级"></a>10. 用 Eval 决定升级，不用架构焦虑决定升级</h3><p>我会维护一组 Query → Expected Evidence 的离线数据，并持续观察：</p><table><thead><tr><th>指标</th><th>回答的问题</th></tr></thead><tbody><tr><td>Recall@5 &#x2F; Recall@20</td><td>正确证据有没有被找回</td></tr><tr><td>MRR &#x2F; nDCG</td><td>正确证据排得是否足够靠前</td></tr><tr><td>Zero-hit Rate</td><td>是否频繁完全无结果</td></tr><tr><td>Citation Precision</td><td>引用是否真的支撑 Claim</td></tr><tr><td>Grounded Answer Rate</td><td>答案是否只使用已给证据</td></tr><tr><td>No-evidence Accuracy</td><td>没答案时能否正确拒答</td></tr><tr><td>Scope Leakage</td><td>是否混入其他项目或无权限内容</td></tr><tr><td>Freshness Lag</td><td>Source 更新多久后可被检索</td></tr><tr><td>P50 &#x2F; P95 Latency</td><td>检索给 Agent Turn 增加多少延迟</td></tr><tr><td>Cost per Retrieval</td><td>Rewrite、Embedding、Rerank 与 Planner 的成本</td></tr></tbody></table><p>我的当前门槛是：先用确定性 Query Rewrite；中文评测出现 <code>Recall@5 &lt; 85%</code> 或 <code>Zero-hit Rate &gt; 5%</code> 时，再把 Multilingual Embedding + Hybrid Search 提到下一阶段。这两个数字服务于当前项目，不是行业标准。</p><h2 id="每个技术如何决策"><a href="#每个技术如何决策" class="headerlink" title="每个技术如何决策"></a>每个技术如何决策</h2><table><thead><tr><th>技术</th><th>什么时候用</th><th>什么时候先别用</th><th>主要代价</th></tr></thead><tbody><tr><td><code>grep</code> &#x2F; Raw File Search</td><td>当前 Workspace、文件刚变化、精确字符串</td><td>团队共享知识与自然语言问答</td><td>能力简单，结果缺少治理</td></tr><tr><td>FTS + BM25</td><td>错误码、类名、配置项、领域词</td><td>Query 大量使用同义表达</td><td>分词与语言适配</td></tr><tr><td>Vector Search</td><td>自然语言、跨语言、同义表达</td><td>精确 ID 与小规模纯术语库</td><td>Embedding 成本与语义误召回</td></tr><tr><td>Hybrid Search</td><td>团队文档、代码说明、中文问答</td><td>数据量极小且 BM25 已达标</td><td>两套索引与 Fusion 调参</td></tr><tr><td>Reranker</td><td>候选集 Recall 已好、Top K 排序不稳</td><td>正确证据根本没进候选集</td><td>额外模型延迟和成本</td></tr><tr><td>Contextual Retrieval</td><td>Chunk 离开父文档后语义残缺</td><td>文档短且 Chunk 本身自包含</td><td>索引时生成上下文的成本</td></tr><tr><td>GraphRAG</td><td>依赖链、影响分析、组织关系、全局主题</td><td>主要是定义查询和精确搜索</td><td>图谱抽取、更新与噪声治理</td></tr><tr><td>Code Graph</td><td>跨文件 Symbol、调用关系、模块依赖</td><td>只是找文本或当前未保存文件</td><td>语言解析器与增量更新</td></tr><tr><td>Agentic Retrieval</td><td>多意图、多源、多跳、需要迭代检索</td><td>单一事实查询、高 QPS 低延迟路径</td><td>Planner 成本、不可预测性与 Trace 复杂度</td></tr></tbody></table><p>我用下面这个顺序做技术决策：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">问题能否靠精确词回答？</span><br><span class="line">  ├─ 能 → BM25 / Raw File Search</span><br><span class="line">  └─ 不能 → 是否主要是语义改写？</span><br><span class="line">       ├─ 是 → Vector + Hybrid</span><br><span class="line">       └─ 否 → 是否需要关系或全局理解？</span><br><span class="line">            ├─ 是 → Code Graph / GraphRAG</span><br><span class="line">            └─ 否 → 是否需要跨多个 Source 拆问？</span><br><span class="line">                 ├─ 是 → Agentic Retrieval</span><br><span class="line">                 └─ 否 → 先检查 Source、Chunk 与 Metadata</span><br></pre></td></tr></table></figure><p>很多“需要更强模型”的问题，最后根因其实是 Source 不可信、Chunk 丢上下文、Scope 错了，或 Query 用错了检索器。</p><h2 id="这些技术应该如何搭配"><a href="#这些技术应该如何搭配" class="headerlink" title="这些技术应该如何搭配"></a>这些技术应该如何搭配</h2><p>落地时，我把系统拆成一个事实面、多个检索投影和一个统一 Knowledge Gateway。</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line">┌──────────────────── Human-owned Sources ────────────────────┐</span><br><span class="line">│ Spec / ADR / Repository / Runbook / Business Data / Raw Doc │</span><br><span class="line">└───────────────────────────┬───────────────────────────────────┘</span><br><span class="line">                            ↓</span><br><span class="line">                 Normalize / Validate / Publish</span><br><span class="line">                            ↓</span><br><span class="line">┌──────────────────── Knowledge Assets ────────────────────────┐</span><br><span class="line">│ owner · trust · version · effective time · ACL · source URI │</span><br><span class="line">└───────────────────────────┬───────────────────────────────────┘</span><br><span class="line">                            ↓ rebuildable projections</span><br><span class="line">        ┌───────────────────┼────────────────────┐</span><br><span class="line">        ↓                   ↓                    ↓</span><br><span class="line">   BM25 / FTS          Vector Index       Graph / Code Index</span><br><span class="line">        └───────────────────┼────────────────────┘</span><br><span class="line">                            ↓</span><br><span class="line">┌──────────────────── Knowledge Gateway ───────────────────────┐</span><br><span class="line">│ Scope Filter → Rewrite / Plan → Route → Fusion → Rerank     │</span><br><span class="line">│ → Evidence Package → Citation → Knowledge Trace             │</span><br><span class="line">└───────────────────────────┬───────────────────────────────────┘</span><br><span class="line">                            ↓</span><br><span class="line">                    Agent Runtime / Tools</span><br><span class="line">                            ↓</span><br><span class="line">               Answer / Plan / Action / Delivery</span><br><span class="line">                            ↓</span><br><span class="line">                       Eval &amp; Feedback</span><br></pre></td></tr></table></figure><p>BM25、Vector Database、Graph Engine 和本地搜索都收口到 Gateway；如果直接暴露给每个 Agent Provider，权限、版本、Citation 和审计会散落在不同 Adapter 里。</p><p>Provider 应该看到一个稳定的领域接口，例如：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> <span class="title class_">KnowledgeSearchRequest</span> = &#123;</span><br><span class="line">  <span class="attr">query</span>: <span class="built_in">string</span>;</span><br><span class="line">  intent?: <span class="string">&quot;lookup&quot;</span> | <span class="string">&quot;explain&quot;</span> | <span class="string">&quot;impact&quot;</span> | <span class="string">&quot;global&quot;</span>;</span><br><span class="line">  <span class="attr">workspaceId</span>: <span class="built_in">string</span>;</span><br><span class="line">  <span class="attr">revision</span>: <span class="built_in">string</span>;</span><br><span class="line">  <span class="attr">principal</span>: <span class="built_in">string</span>;</span><br><span class="line">  <span class="attr">budget</span>: &#123;</span><br><span class="line">    <span class="attr">maxResults</span>: <span class="built_in">number</span>;</span><br><span class="line">    <span class="attr">maxTokens</span>: <span class="built_in">number</span>;</span><br><span class="line">    <span class="attr">maxLatencyMs</span>: <span class="built_in">number</span>;</span><br><span class="line">  &#125;;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>具体使用 BM25、Vector、Graph 还是多路并行，由 Knowledge Gateway 根据 Intent、数据新鲜度、预算和 Eval 结果决定。</p><h2 id="三种可直接落地的组合"><a href="#三种可直接落地的组合" class="headerlink" title="三种可直接落地的组合"></a>三种可直接落地的组合</h2><h3 id="组合-A：个人-小项目"><a href="#组合-A：个人-小项目" class="headerlink" title="组合 A：个人 &#x2F; 小项目"></a>组合 A：个人 &#x2F; 小项目</h3><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">Markdown / Repository</span><br><span class="line">→ SQLite FTS5 或本地 BM25</span><br><span class="line">→ Glossary Query Rewrite</span><br><span class="line">→ Top 5 Evidence + 文件行号 Citation</span><br><span class="line">→ Agent</span><br></pre></td></tr></table></figure><p>这套组合完全在本地运行，依赖少，检索行为也容易解释。内容不多时，不要先搭分布式 Vector Database。</p><h3 id="组合-B：团队知识库"><a href="#组合-B：团队知识库" class="headerlink" title="组合 B：团队知识库"></a>组合 B：团队知识库</h3><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">Git / 文档平台 / 业务系统</span><br><span class="line">→ 规范化 Knowledge Asset</span><br><span class="line">→ BM25 + Multilingual Embedding</span><br><span class="line">→ Metadata Filter + RRF + Rerank</span><br><span class="line">→ Citation + Revision + ACL</span><br><span class="line">→ Chat / Agent 共用</span><br></pre></td></tr></table></figure><p>对多数团队，这已经是一条够用的 Production RAG 基线。先把 Hybrid、治理和 Eval 做稳，再讨论图谱。</p><h3 id="组合-C：应用侧集成"><a href="#组合-C：应用侧集成" class="headerlink" title="组合 C：应用侧集成"></a>组合 C：应用侧集成</h3><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">共享长期知识：团队 Spec / ADR / Runbook</span><br><span class="line">当前工作区：实时 Raw File Search</span><br><span class="line">代码结构：Symbol / Dependency Graph</span><br><span class="line">实时事实：业务 API / Database Tool</span><br><span class="line">                ↓</span><br><span class="line">         Unified Knowledge Gateway</span><br><span class="line">                ↓</span><br><span class="line"> Query Planning / Source Routing / Evidence Merge</span><br><span class="line">                ↓</span><br><span class="line">      Agent Action + Knowledge Trace + Eval</span><br></pre></td></tr></table></figure><p>四类信息的时效和结构不同：</p><ul><li>共享知识回答“团队已经确认了什么”；</li><li>Raw File Search 回答“当前磁盘上刚刚发生了什么”；</li><li>Code Graph 回答“代码对象怎样连接、改动影响哪里”；</li><li>业务 API 回答“此刻真实世界的状态是什么”。</li></ul><p>把它们全部塞进同一个 Vector Index，会失去新鲜度、结构、权限和事实边界。</p><h2 id="一条务实的实施路线"><a href="#一条务实的实施路线" class="headerlink" title="一条务实的实施路线"></a>一条务实的实施路线</h2><h3 id="Phase-0：先做-Eval-Set"><a href="#Phase-0：先做-Eval-Set" class="headerlink" title="Phase 0：先做 Eval Set"></a>Phase 0：先做 Eval Set</h3><p>准备 30–100 条真实问题，每条标注 Expected Source、Expected Evidence、允许的 Revision 和拒答条件。没有这一步，后续所有“效果不错”都只是观感。</p><h3 id="Phase-1：Keyword-Evidence"><a href="#Phase-1：Keyword-Evidence" class="headerlink" title="Phase 1：Keyword + Evidence"></a>Phase 1：Keyword + Evidence</h3><p>先打通 Source、Chunk、FTS&#x2F;BM25、Citation 和 <code>no_evidence</code>。对中文和领域词加入确定性 Query Rewrite。</p><p>我把这一阶段的验收写成五条：</p><ul><li>正确 Source 能进入 Top K；</li><li>Citation 能回到原文；</li><li>Scope 不串；</li><li>没证据时拒答；</li><li>索引可以重建。</li></ul><h3 id="Phase-2：Hybrid-Rerank"><a href="#Phase-2：Hybrid-Rerank" class="headerlink" title="Phase 2：Hybrid + Rerank"></a>Phase 2：Hybrid + Rerank</h3><p>当同义表达和中文召回成为主要问题，加入 Multilingual Embedding，用 RRF 合并 Keyword &#x2F; Vector 候选。只有 Candidate Recall 达标后再上 Reranker。</p><h3 id="Phase-3：按问题增加结构索引"><a href="#Phase-3：按问题增加结构索引" class="headerlink" title="Phase 3：按问题增加结构索引"></a>Phase 3：按问题增加结构索引</h3><p>代码影响分析用 Code Graph，实体关系和全局主题用 GraphRAG。不要创建一个包治百病的“统一大图谱”。</p><h3 id="Phase-4：Agentic-Retrieval"><a href="#Phase-4：Agentic-Retrieval" class="headerlink" title="Phase 4：Agentic Retrieval"></a>Phase 4：Agentic Retrieval</h3><p>当任务确实需要多个 Source、多轮补证和动态路由，再引入 Planner。给每次检索设置 Token、Latency 和 Cost Budget，并完整记录 Activity Trace。</p><h2 id="最容易踩的坑"><a href="#最容易踩的坑" class="headerlink" title="最容易踩的坑"></a>最容易踩的坑</h2><h3 id="把-Vector-Database-当知识库"><a href="#把-Vector-Database-当知识库" class="headerlink" title="把 Vector Database 当知识库"></a>把 Vector Database 当知识库</h3><p>Vector Database 只保存一种检索投影。它不知道哪份文档是正式结论，也不天然理解权限、版本和冲突。</p><h3 id="把-simple-当中文分词"><a href="#把-simple-当中文分词" class="headerlink" title="把 simple 当中文分词"></a>把 <code>simple</code> 当中文分词</h3><p>取消英文 Stemming 或 Stop Words，不等于获得中文 Tokenizer。中文检索应通过真实 Query Eval 判断是先做 Rewrite、Multilingual Embedding，还是增加中文 BM25 Side Index。</p><h3 id="把-GraphRAG-当默认终局"><a href="#把-GraphRAG-当默认终局" class="headerlink" title="把 GraphRAG 当默认终局"></a>把 GraphRAG 当默认终局</h3><p>图谱最擅长关系与全局问题。大多数“某个概念是什么”“某个错误怎么修”仍然更适合 BM25、Vector 或 Hybrid。</p><h3 id="让-Agent-自由搜索却没有-Scope"><a href="#让-Agent-自由搜索却没有-Scope" class="headerlink" title="让 Agent 自由搜索却没有 Scope"></a>让 Agent 自由搜索却没有 Scope</h3><p>Agentic Retrieval 增加了自主性，也放大了跨项目污染、越权和 Prompt Injection 风险。Planner 可以决定怎么搜，不能决定自己有权看什么。</p><h3 id="只测最终回答"><a href="#只测最终回答" class="headerlink" title="只测最终回答"></a>只测最终回答</h3><p>一段流畅答案可能引用错文档。必须分别评测 Retrieval、Ranking、Grounding 和 Action。</p><h3 id="索引不可重建"><a href="#索引不可重建" class="headerlink" title="索引不可重建"></a>索引不可重建</h3><p>如果团队不敢删掉 Index 重建，说明事实源、转换流程或版本记录并不可靠。可重建是知识基础设施成熟度的重要标志。</p><h2 id="要不要用-我的判断框架"><a href="#要不要用-我的判断框架" class="headerlink" title="要不要用 &#x2F; 我的判断框架"></a>要不要用 &#x2F; 我的判断框架</h2><p>值得现在做：</p><ul><li>把系统定位为“人维护事实、Agent 调用知识、双方追溯 Evidence”；</li><li>明确 Source of Truth、Knowledge Asset 与 Index 的边界；</li><li>从 FTS&#x2F;BM25、Citation、Scope 和 Eval 起步；</li><li>团队知识默认评估 Hybrid Search；</li><li>把当前文件、代码结构、共享知识和实时业务事实分开接入；</li><li>让所有 Provider 通过统一 Knowledge Gateway 使用它们。</li></ul><p>可以再等：</p><ul><li>在没有关系型 Query 证据前建设完整 GraphRAG；</li><li>在单次 Query 已足够时启用 LLM Planner；</li><li>为了“AI Native”这个名字重写已有 Wiki；</li><li>在没有 Eval Set 时频繁更换 Embedding Model 和 Vector Database。</li></ul><p>重点关注：</p><ul><li>Permission 和 Revision 必须先于检索结果进入模型；</li><li>Citation 必须指向稳定的 Source Version；</li><li><code>no_evidence</code> 是正常结果，不是系统故障；</li><li>Agent 的每个关键行动都要能回到 Knowledge Trace。</li></ul><p>我的责任划分已经很明确：<strong>人负责事实源，Agent 使用检索接口，Knowledge Gateway 管住 Scope、Revision 和 Permission。</strong> Agent 给出答案或准备执行动作时，再把 Evidence、Citation 和 Trace 交回给人。</p><p>是否 AI Native，可以用一次错误行动来检验：系统能不能沿着 Trace 找到它用了哪条 Evidence、哪个版本、哪个检索分支，并据此修正 Source 或索引。只能看到一段回答、查不到依据的产品，即使加了 Chat Box，也仍然只是文档问答。</p><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><ul><li><a href="https://arxiv.org/abs/2005.11401">Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks</a></li><li><a href="https://arxiv.org/abs/2004.04906">Dense Passage Retrieval for Open-Domain Question Answering</a></li><li><a href="https://developers.openai.com/api/docs/guides/retrieval">OpenAI Retrieval Guide</a></li><li><a href="https://www.elastic.co/docs/solutions/search/hybrid-search">Elastic Hybrid Search</a></li><li><a href="https://www.anthropic.com/engineering/contextual-retrieval">Anthropic Contextual Retrieval</a></li><li><a href="https://microsoft.github.io/graphrag/">Microsoft GraphRAG</a></li><li><a href="https://learn.microsoft.com/en-us/azure/search/agentic-retrieval-overview">Azure AI Search Agentic Retrieval</a></li></ul>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/27da22f1c5be/</id>
    <link href="https://blog.zisheng.pro/posts/27da22f1c5be/"/>
    <published>2026-07-23T00:32:44.000Z</published>
    <summary>传统知识库围绕人阅读，AI 知识库增加问答，AI Native 知识库则把 Agent 作为一等消费者。本文梳理从 FTS、BM25、Vector Search、RAG、GraphRAG 到 Agentic Retrieval 的发展，给出 Source、Index、Evidence、Revision 与 Permission 的最佳实践，以及不同技术的选型和组合方法。</summary>
    <title>AI Native 知识库：给人维护，给 Agent 调用，给双方追溯</title>
    <updated>2026-09-18T08:15:28.629Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="AI" scheme="https://blog.zisheng.pro/categories/AI/"/>
    <category term="Hybrid Search" scheme="https://blog.zisheng.pro/tags/Hybrid-Search/"/>
    <category term="AI Infra" scheme="https://blog.zisheng.pro/tags/AI-Infra/"/>
    <category term="GBrain" scheme="https://blog.zisheng.pro/tags/GBrain/"/>
    <category term="Raw Knowledge" scheme="https://blog.zisheng.pro/tags/Raw-Knowledge/"/>
    <category term="Knowledge Asset" scheme="https://blog.zisheng.pro/tags/Knowledge-Asset/"/>
    <category term="FTS" scheme="https://blog.zisheng.pro/tags/FTS/"/>
    <category term="Embedding" scheme="https://blog.zisheng.pro/tags/Embedding/"/>
    <content>
      <![CDATA[<p>最近我把 GBrain 接到一个 Agent 应用的应用侧，能够检索本地文档、给 Codex 注入上下文，也能在回答中带上 <code>gbrain://</code> Citation。</p><p>链路跑通之后，我反而遇到了两个更本质的问题：一个新项目有代码仓库、语雀、钉钉、Issue 和会议记录，这些 Raw Knowledge 到底怎样才算“进入知识库”？另一方面，中文文档明明已经导入，为什么问一句“Revision 是什么”却可能一条也搜不到？</p><p>这两个问题表面上分别属于知识治理和搜索，底层却指向同一个误区：<strong>派生索引不是事实源，语义检索也不是全文检索。</strong></p><h2 id="一句话总结"><a href="#一句话总结" class="headerlink" title="一句话总结"></a>一句话总结</h2><p>Raw Knowledge 导入 GBrain 后，只是变成可检索的 Page、Chunk、Embedding 和 Edge；只有经过归属、规范化、校验和发布，才成为应用侧的 Knowledge Asset。中文检索则需要 Query Rewrite、Multilingual Embedding 与关键词检索协作，单纯把 PostgreSQL FTS 从 <code>english</code> 改成 <code>simple</code> 并不会获得中文分词能力。</p><p>完整链路应该是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">Raw Knowledge</span><br><span class="line">    ↓ 归属、清洗、规范化</span><br><span class="line">Knowledge Source</span><br><span class="line">    ↓ GBrain import / sync</span><br><span class="line">Page → Chunk → FTS / Embedding → Edge</span><br><span class="line">    ↓ 整理、校验、审核、发布</span><br><span class="line">Knowledge Asset Version</span><br><span class="line">    ↓</span><br><span class="line">Workspace Revision</span><br></pre></td></tr></table></figure><p>这里存在两条可以并行、但不能混为一谈的流水线：一条负责“先搜起来”，另一条负责“让知识变可信”。</p><h2 id="被-GBrain-索引，不等于成为-Knowledge-Asset"><a href="#被-GBrain-索引，不等于成为-Knowledge-Asset" class="headerlink" title="被 GBrain 索引，不等于成为 Knowledge Asset"></a>被 GBrain 索引，不等于成为 Knowledge Asset</h2><p>这是我这次最重要的认知修正。</p><p>GBrain 认识的核心对象是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Source → Page → Chunk → Embedding / FTS → Edge</span><br></pre></td></tr></table></figure><p>应用侧认识的核心对象则是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">Knowledge Draft</span><br><span class="line">→ Knowledge Asset</span><br><span class="line">→ Knowledge Asset Version</span><br><span class="line">→ Asset Manifest</span><br><span class="line">→ Workspace Revision</span><br></pre></td></tr></table></figure><p>两边的概念不能直接画等号。</p><p>一篇语雀文档被同步进 GBrain 后，可以被搜索、切块、建立向量和关系，但它依然可能过期、互相冲突、没有明确 Owner，甚至只是一场会议里的临时观点。它是 Raw Knowledge，不是已经发布的 Knowledge Asset。</p><p>如果把“进入索引”直接理解成“成为资产”，GBrain 数据库就会悄悄变成第二事实源。之后谁也说不清：正式 Spec 和会议记录冲突时听谁的？索引中的旧 Chunk 是否仍然有效？一个已经删除的页面能不能继续影响 Agent？</p><p>所以我现在采用一个更严格的边界：</p><table><thead><tr><th>对象</th><th>是什么</th><th>能否作为正式依据</th></tr></thead><tbody><tr><td>Raw Knowledge</td><td>仓库、文档、Issue、会议、对话等原始内容</td><td>不能默认信任</td></tr><tr><td>GBrain Page &#x2F; Chunk</td><td>从 Source 生成的可检索派生数据</td><td>只能回到 Source 验证</td></tr><tr><td>Knowledge Draft</td><td>从 Raw Knowledge 提炼出的待审核结论</td><td>尚未发布</td></tr><tr><td>Knowledge Asset Version</td><td>已完成校验和发布的不可变知识版本</td><td>可以作为正式依据</td></tr><tr><td>Workspace Revision</td><td>固定一组 Asset Version 的不可变快照</td><td>决定当前 Workspace 使用什么</td></tr></tbody></table><h2 id="新项目的知识库应该怎样组织"><a href="#新项目的知识库应该怎样组织" class="headerlink" title="新项目的知识库应该怎样组织"></a>新项目的知识库应该怎样组织</h2><p>一个新项目不应该只有一个不分来源的“大知识库”。至少应该按来源和信任级别拆成三类 Source：</p><table><thead><tr><th>Source</th><th>内容</th><th>信任级别</th></tr></thead><tbody><tr><td><code>team-specs</code></td><td>正式 Spec、ADR、团队规范</td><td>高</td></tr><tr><td><code>{project}-repo</code></td><td>当前代码、README、仓库文档</td><td>中高</td></tr><tr><td><code>{project}-raw</code></td><td>语雀导出、会议记录、Issue、调研材料</td><td>中低</td></tr></tbody></table><p>当不同 Source 冲突时，我采用下面的优先级：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">正式 Knowledge Asset</span><br><span class="line">&gt; 当前 Repository Revision</span><br><span class="line">&gt; 团队 Raw Knowledge</span><br><span class="line">&gt; 历史会议和对话</span><br></pre></td></tr></table></figure><p>“更新得晚”不等于“权威性更高”。一条昨天的会议讨论，不能因为时间更近就覆盖上周已经评审通过的 ADR。</p><p>对于语雀和钉钉，正确的数据流也不是“连接以后全部自动变成知识”：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">语雀 / 钉钉</span><br><span class="line">→ Connector 拉取</span><br><span class="line">→ 保存 document_id、source_url、author、updated_at</span><br><span class="line">→ 转换成规范 Markdown</span><br><span class="line">→ 导入 project-raw Source</span><br><span class="line">→ 提取候选结论生成 Draft</span><br><span class="line">→ 人工或规则校验</span><br><span class="line">→ 发布 Knowledge Asset Version</span><br></pre></td></tr></table></figure><p>LLM 可以帮助提取 Draft，但不能因为“总结得像”就自动获得发布权。</p><h2 id="GBrain-实际怎样处理一份-Raw-Knowledge"><a href="#GBrain-实际怎样处理一份-Raw-Knowledge" class="headerlink" title="GBrain 实际怎样处理一份 Raw Knowledge"></a>GBrain 实际怎样处理一份 Raw Knowledge</h2><p>一份适合导入 GBrain 的最小 Markdown 可以是：</p><figure class="highlight md"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line">---</span><br><span class="line">title: Workspace Revision</span><br><span class="line">type: concept</span><br><span class="line">tags:</span><br><span class="line"><span class="bullet">  -</span> example-workspace</span><br><span class="line"><span class="bullet">  -</span> architecture</span><br><span class="line">source<span class="emphasis">_uri: https://example.com/original-document</span></span><br><span class="line"><span class="emphasis">owner: architecture-team</span></span><br><span class="line"><span class="emphasis">---</span></span><br><span class="line"><span class="emphasis"></span></span><br><span class="line"><span class="emphasis">## 一句话定义</span></span><br><span class="line"><span class="emphasis"></span></span><br><span class="line"><span class="emphasis">Workspace Revision 是面向 Workspace 发布的不可变资产集合快照。</span></span><br><span class="line"><span class="emphasis"></span></span><br><span class="line"><span class="emphasis">## 约束</span></span><br><span class="line"><span class="emphasis"></span></span><br><span class="line"><span class="emphasis">- 由 Asset Manifest 固定所有 Asset Version。</span></span><br><span class="line"><span class="emphasis">- 本地只能激活校验通过的版本。</span></span><br><span class="line"><span class="emphasis"></span></span><br><span class="line"><span class="emphasis">&lt;!-- timeline --&gt;</span></span><br><span class="line"><span class="emphasis"></span></span><br><span class="line"><span class="emphasis">## 2026-07-22</span></span><br><span class="line"><span class="emphasis"></span></span><br><span class="line"><span class="emphasis">首次确认该定义。</span></span><br></pre></td></tr></table></figure><p>GBrain 导入时会解析 Front Matter、生成稳定 Slug、计算 Content Hash、创建 Page、拆分 Chunk，然后建立 Keyword Index；配置 Embedding 后还会生成向量，并从文档链接或代码引用中提取 Edge。</p><p>这些步骤把内容变得容易搜索，但没有替代 Source 的所有权、版本和审核流程。Page、Chunk、Embedding 和 Edge 都应该可以从已知 Source 重新构建。</p><h2 id="中文文档已经导入，为什么还是搜不到"><a href="#中文文档已经导入，为什么还是搜不到" class="headerlink" title="中文文档已经导入，为什么还是搜不到"></a>中文文档已经导入，为什么还是搜不到</h2><p>当前 GBrain MVP 使用 PGLite，初始化参数是：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">gbrain init --pglite --no-embedding</span><br></pre></td></tr></table></figure><p>这意味着当前实际依赖的是 PostgreSQL Keyword Search，而且 FTS language 默认为 <code>english</code>。</p><p>我用同一个 Brain 做了一组直接检索：</p><table><thead><tr><th>Query</th><th align="right">Hit</th></tr></thead><tbody><tr><td><code>事实源</code></td><td align="right">3</td></tr><tr><td><code>本地知识</code></td><td align="right">2</td></tr><tr><td><code>Revision 是什么</code></td><td align="right">0</td></tr><tr><td><code>Workspace Revision 是什么</code></td><td align="right">0</td></tr><tr><td><code>工作区版本是什么</code></td><td align="right">0</td></tr><tr><td><code>Workspace Revision</code></td><td align="right">3</td></tr><tr><td>包含多个英文领域词的完整中文 Prompt</td><td align="right">3</td></tr></tbody></table><p>这个结果说明它不是“完全不支持中文”。索引里恰好存在的完整中文词可以命中，英文领域词也很稳定；真正不可靠的是中文自然问句。</p><p>例如：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Workspace Revision 是什么</span><br></pre></td></tr></table></figure><p>检索器可能把 <code>Workspace</code>、<code>Revision</code> 和后面的中文片段一起当作必须满足的条件。只要中文 token 与索引里的形式不一致，整个查询就可能变成零召回。</p><h2 id="simple-不是中文分词器"><a href="#simple-不是中文分词器" class="headerlink" title="simple 不是中文分词器"></a><code>simple</code> 不是中文分词器</h2><p>GBrain 支持通过 <code>GBRAIN_FTS_LANGUAGE</code> 改变 PostgreSQL text search configuration。看到这里很容易产生一个朴素想法：把 <code>english</code> 改成 <code>simple</code>，是不是中文就好了？</p><p>答案是否定的。</p><p><code>simple</code> 的主要作用是取消英文停用词和 stemming，不会自动把：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">工作区修订是不可变资产集合快照</span><br></pre></td></tr></table></figure><p>切成：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">工作区 / 修订 / 不可变 / 资产 / 集合 / 快照</span><br></pre></td></tr></table></figure><p>因此，改成 <code>simple</code> 最多改变 token 的处理规则，不能补齐中文 tokenizer。严格意义上的中文 FTS 仍然需要 <code>jieba</code>、<code>zhparser</code>、Lindera 或其他中文分词能力。</p><h2 id="Embedding-能解决什么，不能解决什么"><a href="#Embedding-能解决什么，不能解决什么" class="headerlink" title="Embedding 能解决什么，不能解决什么"></a>Embedding 能解决什么，不能解决什么</h2><p>Embedding 能把问题和文档映射到语义空间，适合处理：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">Revision 是什么？</span><br><span class="line">工作区如何固定可信资产？</span><br><span class="line">一次执行怎样保证知识版本一致？</span><br></pre></td></tr></table></figure><p>这些问题用词不同，但都可能指向 Workspace Revision。</p><p>不过，Embedding 不是 FTS。它擅长语义相似，不保证函数名、错误码、配置项、缩写和精确领域词一定排在最前面。对于代码和 AI Infra 项目，完全用 Vector Search 替代关键词检索同样会出问题。</p><p>正确结构是 Hybrid Search：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">Keyword Search ─┐</span><br><span class="line">                ├→ RRF Fusion → Metadata / Permission Filter → Evidence</span><br><span class="line">Vector Search ──┘</span><br></pre></td></tr></table></figure><p>Keyword Search 守住精确术语，Vector Search 补足中文和同义表达，再通过 RRF 合并两个排序列表。</p><h2 id="应用侧的三阶段修复路线"><a href="#应用侧的三阶段修复路线" class="headerlink" title="应用侧的三阶段修复路线"></a>应用侧的三阶段修复路线</h2><h3 id="第一阶段：Query-Rewrite"><a href="#第一阶段：Query-Rewrite" class="headerlink" title="第一阶段：Query Rewrite"></a>第一阶段：Query Rewrite</h3><p>这是成本最低、应该最先做的一步。</p><p>用户输入：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Workspace Revision 是什么？</span><br></pre></td></tr></table></figure><p>应用侧生成三条确定性 Query：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">原始：Workspace Revision 是什么</span><br><span class="line">精确：Workspace Revision</span><br><span class="line">别名：Workspace Revision 修订 版本快照</span><br></pre></td></tr></table></figure><p>每条分别检索，最后合并去重。别名不应该每次调用 LLM 临时生成，而应该来自项目的 <code>CONTEXT.md</code> 和受治理 Glossary，这样结果可解释、可重复。</p><h3 id="第二阶段：Multilingual-Embedding"><a href="#第二阶段：Multilingual-Embedding" class="headerlink" title="第二阶段：Multilingual Embedding"></a>第二阶段：Multilingual Embedding</h3><p>创建一个带 Embedding 的新 Brain，重新导入全部 Source，再启用 GBrain 已有的 Hybrid Search。</p><p>这里不能直接在当前 PGLite 上修改一个配置完事。Embedding dimensions 会影响 Schema，当前 <code>no-embedding</code> Brain 应该按照下面的方式迁移：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">保留旧 Brain</span><br><span class="line">→ 初始化 Brain v2</span><br><span class="line">→ 重新导入所有 Source</span><br><span class="line">→ 回填 Embedding</span><br><span class="line">→ 跑中文检索评测</span><br><span class="line">→ 验证后原子切换</span><br><span class="line">→ 保留旧 Brain 回滚</span><br></pre></td></tr></table></figure><p>这也是为什么事实源必须独立于索引存在：只有 Source 可靠，索引才能放心重建。</p><h3 id="第三阶段：真正的本地中文-FTS"><a href="#第三阶段：真正的本地中文-FTS" class="headerlink" title="第三阶段：真正的本地中文 FTS"></a>第三阶段：真正的本地中文 FTS</h3><p>如果后续评测证明 Embedding 仍不能满足精确中文术语检索，再给应用侧增加中文 Side Index：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">GBrain Vector Search</span><br><span class="line">+ GBrain Keyword Search</span><br><span class="line">+ 本地 Tantivy 中文 BM25</span><br><span class="line">→ RRF Fusion</span><br></pre></td></tr></table></figure><p>另一条路线是在云端 PostgreSQL 使用 <code>pg_jieba</code> 或 <code>zhparser</code>，但它增加了数据库扩展和部署运维复杂度，不适合当前以本地 PGLite 为主的 MVP。</p><h2 id="不能只看回答“像不像对”"><a href="#不能只看回答“像不像对”" class="headerlink" title="不能只看回答“像不像对”"></a>不能只看回答“像不像对”</h2><p>中文检索升级之后，我会准备一组带期望 Citation 的评测数据：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;query&quot;</span><span class="punctuation">:</span> <span class="string">&quot;Workspace Revision 是什么？&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;expectedSlugs&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">    <span class="string">&quot;example-workspace-spec&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><p>至少观察下面几个指标：</p><table><thead><tr><th>指标</th><th>关注的问题</th></tr></thead><tbody><tr><td>Recall@5</td><td>正确文档是否进入前 5</td></tr><tr><td>MRR@5</td><td>正确文档排得是否足够靠前</td></tr><tr><td>Zero-hit Rate</td><td>中文问句是否仍频繁零召回</td></tr><tr><td>Citation Precision</td><td>引用是否真的支撑回答</td></tr><tr><td>P95 Latency</td><td>Hybrid Search 增加了多少延迟</td></tr><tr><td>Scope Leakage</td><td>是否检索到其他 Workspace 的知识</td></tr></tbody></table><p>最后一项尤其重要。一个全局 Brain 如果没有绑定 Workspace 和 Source，再高的召回率也可能意味着更严重的跨项目污染。</p><h2 id="要不要用-我的判断框架"><a href="#要不要用-我的判断框架" class="headerlink" title="要不要用 &#x2F; 我的判断框架"></a>要不要用 &#x2F; 我的判断框架</h2><p>值得现在做：</p><ul><li>把 Raw Knowledge、GBrain Index 和 Knowledge Asset 分开建模。</li><li>每个项目按 <code>team-specs</code>、<code>project-repo</code>、<code>project-raw</code> 划分 Source。</li><li>用 Query Rewrite 立即修复中英混合问句。</li><li>建立带期望 Citation 的中文评测集。</li></ul><p>可以随后做：</p><ul><li>迁移到 Multilingual Embedding。</li><li>用 Keyword + Vector + RRF 构建 Hybrid Search。</li><li>把检索 Trace、Hit Count、Latency 和 Citation 暴露到应用侧 UI。</li></ul><p>重点关注：</p><ul><li>不要把 <code>GBRAIN_FTS_LANGUAGE=simple</code> 当成中文 FTS。</li><li>不要因为文档已经进入 GBrain 就称它为 Knowledge Asset。</li><li>不要让索引成为无法重建、无法追溯的第二事实源。</li><li>不要在没有 Workspace &#x2F; Source Scope 的情况下把多个项目全部塞进默认 Brain。</li></ul><p>我现在对这套系统的判断比刚接入 GBrain 时更保守，也更清楚：真正困难的从来不是把文档导进去，而是决定哪些内容可信、什么时候有效、检索为什么命中，以及 Agent 最后引用的到底是哪一条事实。</p>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/602a718099d9/</id>
    <link href="https://blog.zisheng.pro/posts/602a718099d9/"/>
    <published>2026-07-22T15:55:00.000Z</published>
    <summary>接入 GBrain 后，我真正困惑的不是如何执行 import，而是原始仓库、语雀文档和会议记录何时才算可信知识，以及中文问句为什么明明包含术语却检索不到。本文结合一个 Agent 应用的实测，拆清 Raw Knowledge、GBrain Page、Knowledge Asset、中文 FTS 与 Embedding 的边界，并给出一条可落地的项目知识接入路线。</summary>
    <title>Raw Knowledge 如何变成可信知识资产：以及中文 FTS 为什么不是改个配置</title>
    <updated>2026-09-18T08:15:28.632Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="AI" scheme="https://blog.zisheng.pro/categories/AI/"/>
    <category term="RAG" scheme="https://blog.zisheng.pro/tags/RAG/"/>
    <category term="AI Infra" scheme="https://blog.zisheng.pro/tags/AI-Infra/"/>
    <category term="Evidence" scheme="https://blog.zisheng.pro/tags/Evidence/"/>
    <category term="GBrain" scheme="https://blog.zisheng.pro/tags/GBrain/"/>
    <category term="LLM Wiki" scheme="https://blog.zisheng.pro/tags/LLM-Wiki/"/>
    <category term="Knowledge Plane" scheme="https://blog.zisheng.pro/tags/Knowledge-Plane/"/>
    <category term="Citation" scheme="https://blog.zisheng.pro/tags/Citation/"/>
    <content>
      <![CDATA[<p>我最近把 GBrain 接到了一个 Agent 应用的应用侧。接入之前，我对它的理解比较简单：把团队文档导进去，然后让 Agent 搜索。</p><p>真正做起来之后，我发现 GBrain 的价值不只是“能不能搜到文档”，而是能不能把团队知识变成一层可检索、可引用、可版本化、可被 Agent 安全消费的 Knowledge Plane。</p><p>这篇文章记录我对 GBrain 的完整理解，也给出一条从本地 MVP 走向团队系统的实践路径。</p><h2 id="一句话总结"><a href="#一句话总结" class="headerlink" title="一句话总结"></a>一句话总结</h2><p>GBrain 是一个面向 LLM 的知识组织和检索运行时：它把事实源编译成可查询的知识索引，再把相关内容以 Evidence 和 Citation 的形式交给 Agent。</p><p>它在系统里的位置可以表示成：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">代码 / Spec / ADR / 团队文档</span><br><span class="line">          ↓</span><br><span class="line">      GBrain Ingestion</span><br><span class="line">          ↓</span><br><span class="line">   FTS / BM25 / Vector / Graph</span><br><span class="line">          ↓</span><br><span class="line">   Evidence + Citation + Revision</span><br><span class="line">          ↓</span><br><span class="line">    应用侧 / Agent Context</span><br><span class="line">          ↓</span><br><span class="line">       Codex / Qoder</span><br></pre></td></tr></table></figure><p>GBrain 负责“知识怎么被找到和组织”，应用侧负责“谁能使用这些知识、作用于哪个 Workspace、最终执行是否成功”。两者不能混成一个边界。</p><h2 id="GBrain-解决什么问题"><a href="#GBrain-解决什么问题" class="headerlink" title="GBrain 解决什么问题"></a>GBrain 解决什么问题</h2><p>普通聊天模型有三个天然限制：</p><ul><li>不知道团队刚刚写下的内部规范。</li><li>不知道当前仓库的真实代码状态。</li><li>回答有依据时，也很难告诉你依据具体来自哪里。</li></ul><p>把所有文档直接拼到 Prompt 里也不可行：内容太多、上下文太长、成本太高，还会把过期和无权限内容一起带进去。</p><p>GBrain 的基本任务就是把这个问题拆开：</p><ol><li>收集事实源。</li><li>把事实源切成可检索的知识单元。</li><li>建立关键词、语义和关系索引。</li><li>根据问题取回最相关的 Evidence。</li><li>为结果保留 Source、Citation、Provenance 和 Revision。</li><li>把受治理的上下文交给 LLM。</li></ol><p>所以，GBrain 不是“让模型记住一切”，而是让模型在需要时找到可信的外部事实。</p><h2 id="先分清几个概念"><a href="#先分清几个概念" class="headerlink" title="先分清几个概念"></a>先分清几个概念</h2><h3 id="Source：事实源"><a href="#Source：事实源" class="headerlink" title="Source：事实源"></a>Source：事实源</h3><p>Source 是知识从哪里来的，例如：</p><ul><li>Git repository</li><li>Markdown、PDF、设计文档</li><li>语雀文档、钉钉文档</li><li>ADR、会议纪要、需求说明</li><li>构建报告、测试报告和发布回执</li></ul><p>语雀或钉钉文档当然可以是 Source，但它们只是事实源，不等于已经完成治理的 Knowledge Asset。</p><h3 id="Evidence：证据"><a href="#Evidence：证据" class="headerlink" title="Evidence：证据"></a>Evidence：证据</h3><p>Evidence 是能够支撑某个结论的具体内容片段。</p><p>例如：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">Claim：Overlay 不修改基础 Revision。</span><br><span class="line">Evidence：架构 Spec 中对 Overlay 的定义段落。</span><br><span class="line">Citation：gbrain://default/example-workspace-spec</span><br></pre></td></tr></table></figure><p>Evidence 不等于 Agent 的自述，也不等于一段没有来源的摘要。它应该能定位回原始文档、文件、行号或 Chunk。</p><h3 id="Citation：引用"><a href="#Citation：引用" class="headerlink" title="Citation：引用"></a>Citation：引用</h3><p>Citation 是 Evidence 的可回溯地址。在 GBrain 接入应用侧的 MVP 中，我使用类似下面的形式：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">gbrain://default/example-workspace-knowledge-architecture</span><br></pre></td></tr></table></figure><p>Citation 的价值是让用户知道答案为什么可信，也让后续的评测和审计有入口。</p><h3 id="Revision：知识版本"><a href="#Revision：知识版本" class="headerlink" title="Revision：知识版本"></a>Revision：知识版本</h3><p>同一个文件可能在不同时间代表不同事实。Revision 用来回答：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">这条知识在哪个版本、哪个 Workspace、哪个时间范围内有效？</span><br></pre></td></tr></table></figure><p>应用侧不允许拿旧 Revision 的索引结果冒充当前 Workspace 的知识。知识检索必须和当前 Session Pin、Workspace 和 Permission Scope 对齐。</p><h2 id="GBrain-的内部数据流"><a href="#GBrain-的内部数据流" class="headerlink" title="GBrain 的内部数据流"></a>GBrain 的内部数据流</h2><p>一个比较完整的 GBrain 数据流如下：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">Source</span><br><span class="line">  ↓ parse</span><br><span class="line">Document</span><br><span class="line">  ↓ split</span><br><span class="line">Chunk</span><br><span class="line">  ↓ index</span><br><span class="line">FTS / BM25 / Vector / Graph</span><br><span class="line">  ↓ search</span><br><span class="line">Evidence</span><br><span class="line">  ↓ cite</span><br><span class="line">Citation + Provenance</span><br><span class="line">  ↓ compose</span><br><span class="line">LLM Context</span><br></pre></td></tr></table></figure><p>这里最容易犯的错误是把“索引”当成事实源。索引只是派生数据，应该可以从已验证的原始输入重新构建；真正的事实源应该保留自己的版本和摘要。</p><h2 id="GBrain-如何检索"><a href="#GBrain-如何检索" class="headerlink" title="GBrain 如何检索"></a>GBrain 如何检索</h2><h3 id="FTS-和-BM25"><a href="#FTS-和-BM25" class="headerlink" title="FTS 和 BM25"></a>FTS 和 BM25</h3><p>FTS 是 Full-Text Search，负责全文索引和关键词召回；BM25 负责对候选结果排序。</p><p>它们适合：</p><ul><li>函数名、类名、错误码</li><li>API 名称和配置项</li><li><code>Workspace Revision</code> 这类领域术语</li><li>代码和规范里的精确标识符</li></ul><p>它们不真正理解语义，所以中文自然语言问题可能需要更好的分词、Query Rewrite 或 Vector Search。</p><h3 id="Vector-Search"><a href="#Vector-Search" class="headerlink" title="Vector Search"></a>Vector Search</h3><p>Vector Search 通过 Embedding 把问题和文本放进向量空间，适合处理同义表达和自然语言描述。</p><p>例如：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">Revision 是什么？</span><br><span class="line">工作区如何固定可信资产？</span><br><span class="line">如何保证 Agent 使用的知识版本一致？</span><br></pre></td></tr></table></figure><p>这些问题的词不完全一样，但语义可能指向同一组文档。</p><h3 id="Graph-Search"><a href="#Graph-Search" class="headerlink" title="Graph Search"></a>Graph Search</h3><p>当问题关注实体之间的关系时，图结构更有用：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">Workspace Revision ──固定──&gt; Asset Version</span><br><span class="line">Overlay ──叠加于──&gt; Revision</span><br><span class="line">Projection ──派生自──&gt; Revision</span><br><span class="line">Evidence ──支撑──&gt; Claim</span><br></pre></td></tr></table></figure><p>实际系统通常会采用 Hybrid Retrieval：关键词保证精确术语，向量补足语义召回，图和 Metadata Filter 限定关系、权限和版本范围。</p><h2 id="GBrain-和-LLM-Wiki-的关系"><a href="#GBrain-和-LLM-Wiki-的关系" class="headerlink" title="GBrain 和 LLM Wiki 的关系"></a>GBrain 和 LLM Wiki 的关系</h2><p>LLM Wiki 可以理解为“知识治理层”，GBrain 可以作为它的知识运行时。</p><p>简单 RAG 重点是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">问题 → 找文本 → 塞 Prompt → 回答</span><br></pre></td></tr></table></figure><p>成熟的 LLM Wiki 还要处理：</p><ul><li>哪个 Source 是事实源？</li><li>两份文档冲突时谁优先？</li><li>这条知识是否已经过期？</li><li>当前用户有没有权限看到它？</li><li>结论能否回到 Evidence？</li><li>一次执行产生的结果能否经过审核后反哺知识库？</li></ul><p>GBrain 提供检索和知识组织能力，但不应该直接拥有组织发布权，也不应该替应用侧作出 Permission 或 Approval 决策。</p><h2 id="在应用侧的正确位置"><a href="#在应用侧的正确位置" class="headerlink" title="在应用侧的正确位置"></a>在应用侧的正确位置</h2><p>应用侧通过 Codex 和 Qoder 执行任务，因此 GBrain 最适合放在应用侧后面：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">Agent Chat UI</span><br><span class="line">      ↓</span><br><span class="line">应用侧</span><br><span class="line">  ├── Workspace / Revision / Permission</span><br><span class="line">  ├── GBrain Adapter</span><br><span class="line">  ├── Session Pin</span><br><span class="line">  ├── Approval</span><br><span class="line">  └── Evidence / Audit</span><br><span class="line">      ↓</span><br><span class="line">Codex / Qoder Provider</span><br></pre></td></tr></table></figure><p>这样做有三个好处：</p><ol><li>Provider 不需要分别理解 GBrain、Sirchmunk 和 Understand Anything。</li><li>应用侧可以先做 Workspace、Revision 和权限过滤。</li><li>GBrain 不可用时，可以降级为无知识检索，不阻断 Agent 主流程。</li></ol><p>当前 MVP 的具体行为是：</p><ul><li>将 Bun 和固定版本的 GBrain 一起打进桌面应用。</li><li>用户不需要安装 Bun、GBrain 或 Postgres。</li><li>使用本地 PGLite 保存 GBrain brain。</li><li>每个 Agent Turn 开始前最多检索 5 条知识。</li><li>结果通过只读上下文注入 Codex 或 Qoder。</li><li>检索超时约 8 秒时自动降级。</li><li>每条结果生成 <code>gbrain://</code> Citation。</li></ul><p>这是一个很重要的工程原则：GBrain 增强 Agent，但不应该成为 Agent 执行的单点故障。</p><h2 id="从零搭建-GBrain-的实践顺序"><a href="#从零搭建-GBrain-的实践顺序" class="headerlink" title="从零搭建 GBrain 的实践顺序"></a>从零搭建 GBrain 的实践顺序</h2><h3 id="第一步：确定事实源"><a href="#第一步：确定事实源" class="headerlink" title="第一步：确定事实源"></a>第一步：确定事实源</h3><p>不要一开始就把所有文件都导进去。先选择高价值、相对稳定的内容：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">docs/specs/</span><br><span class="line">docs/adr/</span><br><span class="line">CONTEXT.md</span><br><span class="line">核心 README</span><br></pre></td></tr></table></figure><p>代码仓库可以接入，但需要区分“团队长期知识”和“当前 Raw Files”。变化很快的源码，更适合作为当前 Turn 的上下文或通过专门的 Code Search 读取。</p><h3 id="第二步：给-Source-补-Metadata"><a href="#第二步：给-Source-补-Metadata" class="headerlink" title="第二步：给 Source 补 Metadata"></a>第二步：给 Source 补 Metadata</h3><p>至少保留这些字段：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;source&quot;</span><span class="punctuation">:</span> <span class="string">&quot;example-repository&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;example-workspace&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;revision&quot;</span><span class="punctuation">:</span> <span class="string">&quot;git-sha-or-asset-digest&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;scope&quot;</span><span class="punctuation">:</span> <span class="string">&quot;team&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;path&quot;</span><span class="punctuation">:</span> <span class="string">&quot;docs/specs/architecture.md&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;updatedAt&quot;</span><span class="punctuation">:</span> <span class="string">&quot;2026-07-22T12:00:00Z&quot;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p>没有 Metadata 的索引，很快会变成一个无法解释、无法隔离、无法清理的文本仓库。</p><h3 id="第三步：先实现可解释检索"><a href="#第三步：先实现可解释检索" class="headerlink" title="第三步：先实现可解释检索"></a>第三步：先实现可解释检索</h3><p>第一版建议从 FTS&#x2F;BM25 开始，因为它快、便宜、结果可解释。先确保：</p><ul><li>能命中精确领域词。</li><li>能返回 Source 和 Citation。</li><li>能按 Revision 过滤。</li><li>没有结果时能明确返回空。</li></ul><p>再根据评测结果加入 Embedding，而不是为了“看起来先进”一开始就堆完整检索栈。</p><h3 id="第四步：把检索结果包装成-Evidence"><a href="#第四步：把检索结果包装成-Evidence" class="headerlink" title="第四步：把检索结果包装成 Evidence"></a>第四步：把检索结果包装成 Evidence</h3><p>不要把原始搜索结果直接拼接给模型。建议统一成：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;content&quot;</span><span class="punctuation">:</span> <span class="string">&quot;Overlay 不修改基础 Revision……&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;source&quot;</span><span class="punctuation">:</span> <span class="string">&quot;example-workspace-spec&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;citation&quot;</span><span class="punctuation">:</span> <span class="string">&quot;gbrain://default/example-workspace-spec&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;revision&quot;</span><span class="punctuation">:</span> <span class="string">&quot;workspace-revision-digest&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;score&quot;</span><span class="punctuation">:</span> <span class="number">0.99</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;retrievedAt&quot;</span><span class="punctuation">:</span> <span class="string">&quot;2026-07-22T12:00:00Z&quot;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p>这样 LLM 得到的是可治理的上下文，而不是一堆没有身份的文本。</p><h3 id="第五步：建立拒答和降级策略"><a href="#第五步：建立拒答和降级策略" class="headerlink" title="第五步：建立拒答和降级策略"></a>第五步：建立拒答和降级策略</h3><p>知识系统必须允许三个结果：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">命中可靠 Evidence → 基于证据回答</span><br><span class="line">命中不足 → 明确说明依据不足</span><br><span class="line">GBrain 不可用 → 降级执行，并记录诊断信息</span><br></pre></td></tr></table></figure><p>最危险的状态是“没有命中，但模型假装命中了”。因此 Prompt 和 Adapter 都应该要求：没有 Citation 的关键结论不得伪装成团队事实。</p><h2 id="如何验证-GBrain-是否真的有效"><a href="#如何验证-GBrain-是否真的有效" class="headerlink" title="如何验证 GBrain 是否真的有效"></a>如何验证 GBrain 是否真的有效</h2><p>不要只看界面显示“已连接”。至少做四组测试。</p><h3 id="正向命中"><a href="#正向命中" class="headerlink" title="正向命中"></a>正向命中</h3><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">只根据当前 Workspace 的本地知识回答：</span><br><span class="line">Projection、Overlay、Workspace Revision 三者有什么区别？</span><br><span class="line">每条关键结论附上 gbrain:// Citation。</span><br></pre></td></tr></table></figure><p>检查定义是否正确、Citation 是否存在、来源是否能回溯。</p><h3 id="负向拒答"><a href="#负向拒答" class="headerlink" title="负向拒答"></a>负向拒答</h3><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">当前应用已上线的云端 Control Plane 域名是什么？</span><br><span class="line">如果知识库没有可靠证据，请回答不知道。</span><br></pre></td></tr></table></figure><p>如果知识库没有这个事实，系统应该拒答或声明依据不足。</p><h3 id="A-B-对照"><a href="#A-B-对照" class="headerlink" title="A&#x2F;B 对照"></a>A&#x2F;B 对照</h3><p>同一批问题分别在 GBrain 开启和关闭时运行，比较：</p><ul><li>正确率</li><li>Citation 覆盖率</li><li>幻觉率</li><li>首 token 延迟和总延迟</li><li>Agent 是否减少了重复探索</li></ul><h3 id="版本隔离"><a href="#版本隔离" class="headerlink" title="版本隔离"></a>版本隔离</h3><p>修改一份 Spec，建立新 Revision，再查询旧 Session 和新 Session。旧 Session 不应该突然看到新版本，新 Session 也不应该读取旧索引冒充当前知识。</p><h2 id="生产化时最容易踩的坑"><a href="#生产化时最容易踩的坑" class="headerlink" title="生产化时最容易踩的坑"></a>生产化时最容易踩的坑</h2><h3 id="把数据库当成事实源"><a href="#把数据库当成事实源" class="headerlink" title="把数据库当成事实源"></a>把数据库当成事实源</h3><p>GBrain database、FTS index、Vector index 都应该是可重建的派生实现。跨云端和本地同步时，同步版本化 Knowledge Asset，不要粗暴复制数据库文件。</p><h3 id="把执行结果直接写回-Wiki"><a href="#把执行结果直接写回-Wiki" class="headerlink" title="把执行结果直接写回 Wiki"></a>把执行结果直接写回 Wiki</h3><p>一次 Agent 执行产生的 Log、Diff、测试结果属于 Execution Evidence。它不能未经审核就变成团队当前事实。</p><p>正确的闭环是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">Execution Evidence</span><br><span class="line">  → Knowledge Draft</span><br><span class="line">  → Citation / Conflict Check</span><br><span class="line">  → Review Gate</span><br><span class="line">  → Published Knowledge</span><br></pre></td></tr></table></figure><h3 id="把三个工具都暴露给-Provider"><a href="#把三个工具都暴露给-Provider" class="headerlink" title="把三个工具都暴露给 Provider"></a>把三个工具都暴露给 Provider</h3><p>GBrain、Sirchmunk、Understand Anything 可以各自擅长不同问题，但不应该让 Codex 或 Qoder 自己决定调用哪一个。应该由 Host 做意图路由：</p><table><thead><tr><th>问题</th><th>默认能力</th></tr></thead><tbody><tr><td>团队 Spec、ADR、长期知识</td><td>GBrain</td></tr><tr><td>当前变化很快的 Raw Files</td><td>Sirchmunk &#x2F; File Search</td></tr><tr><td>代码结构、调用关系、Diff Impact</td><td>Understand Anything &#x2F; Code Graph</td></tr></tbody></table><p>最终 Provider 只看到统一的 Knowledge Context 契约。</p><h2 id="要不要用-我的判断框架"><a href="#要不要用-我的判断框架" class="headerlink" title="要不要用 &#x2F; 我的判断框架"></a>要不要用 &#x2F; 我的判断框架</h2><p>GBrain 值得使用，但不要把它当成“装上就拥有团队记忆”的黑盒。</p><p>我的落地顺序是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">先接入高价值事实源</span><br><span class="line">→ 先用 FTS/BM25 验证精确检索</span><br><span class="line">→ 补 Evidence 和 Citation</span><br><span class="line">→ 再加入 Vector Search</span><br><span class="line">→ 关系稳定后再建 Graph</span><br><span class="line">→ 最后补权限、Revision、冲突和审核</span><br></pre></td></tr></table></figure><p>对应用侧来说，GBrain 的第一目标不是回答所有问题，而是让 Agent 在执行前拿到当前 Workspace 可用、来源明确、版本一致的知识。</p><p>如果它能让 Agent 少猜一次、少重复读一次 Spec、少改错一次代码，并且能解释“为什么这么做”，那它就已经从一个搜索工具变成了真正有价值的 Knowledge Plane。</p>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/0d51acab9a0c/</id>
    <link href="https://blog.zisheng.pro/posts/0d51acab9a0c/"/>
    <published>2026-07-22T15:50:00.000Z</published>
    <summary>GBrain 不只是一个 RAG 工具，而是一层连接事实源、检索、Evidence、Citation 和 LLM 的知识基础设施。本文从基本概念、数据流、索引检索、云端与本地部署讲起，结合一个 Agent 应用给出一套可落地的接入、验证和生产化方法。</summary>
    <title>GBrain 从入门到精通：构建可引用的本地知识层</title>
    <updated>2026-09-18T08:15:28.629Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="AI" scheme="https://blog.zisheng.pro/categories/AI/"/>
    <category term="RAG" scheme="https://blog.zisheng.pro/tags/RAG/"/>
    <category term="GraphRAG" scheme="https://blog.zisheng.pro/tags/GraphRAG/"/>
    <category term="AI Infra" scheme="https://blog.zisheng.pro/tags/AI-Infra/"/>
    <category term="LLM Wiki" scheme="https://blog.zisheng.pro/tags/LLM-Wiki/"/>
    <category term="FTS" scheme="https://blog.zisheng.pro/tags/FTS/"/>
    <category term="BM25" scheme="https://blog.zisheng.pro/tags/BM25/"/>
    <category term="Vector Search" scheme="https://blog.zisheng.pro/tags/Vector-Search/"/>
    <content>
      <![CDATA[<p>我最近在一个 Agent 应用里接入 GBrain，连续遇到几个看起来很像、实际分工完全不同的词：RAG、FTS、BM25、Vector Search、GraphRAG、LLM Wiki、Evidence。</p><p>一开始我也容易把它们理解成一条“越来越高级的产品线”：RAG 是基础，GraphRAG 是升级，LLM Wiki 是终局。这个理解不准确。它们分别描述的是检索方式、数据结构、工程系统和知识治理方法，不能简单排成一条替代关系。</p><h2 id="一句话总结"><a href="#一句话总结" class="headerlink" title="一句话总结"></a>一句话总结</h2><p>RAG 是“让 LLM 回答前先查资料”的工作流；FTS 和 BM25 是关键词检索与排序；Vector Search 是语义检索；GraphRAG 是利用实体和关系回答关联问题；LLM Wiki 是围绕事实源、Evidence、Revision 和引用建立的一套知识工程系统。</p><p>换句话说：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">事实源 → 索引/图谱 → 检索 → Evidence → LLM 回答 → Citation</span><br></pre></td></tr></table></figure><p>其中，FTS、BM25、Vector Search、Graph Search 都可以是“检索”这一层的实现。LLM Wiki 负责把这条链路治理起来。</p><h2 id="先从-RAG-说起"><a href="#先从-RAG-说起" class="headerlink" title="先从 RAG 说起"></a>先从 RAG 说起</h2><p>RAG 是 Retrieval-Augmented Generation，中文通常叫“检索增强生成”。它不是某个数据库，也不是某个 UI 产品，而是一种回答流程：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">用户问题</span><br><span class="line">  ↓</span><br><span class="line">检索相关资料</span><br><span class="line">  ↓</span><br><span class="line">把资料放进 Prompt</span><br><span class="line">  ↓</span><br><span class="line">LLM 基于资料生成答案</span><br></pre></td></tr></table></figure><p>普通 LLM 只能依赖训练数据和当前对话上下文。RAG 让它在回答之前访问外部知识，因此适合项目代码、团队规范、内部文档、实时数据等场景。</p><p>RAG 最重要的不是“把文档塞给模型”，而是检索到正确的上下文。检索错了，LLM 可能会非常自信地基于错误资料回答；检索不到，模型又会回到自己的先验知识，产生幻觉。</p><p>所以 RAG 的核心指标通常不是模型有多大，而是：</p><ul><li>Recall：应该找到的资料有没有找到？</li><li>Precision：找到的资料是不是确实相关？</li><li>Groundedness：回答是否真的被检索结果支撑？</li><li>Citation：用户能不能回到原始证据？</li></ul><h2 id="FTS：全文搜索是什么"><a href="#FTS：全文搜索是什么" class="headerlink" title="FTS：全文搜索是什么"></a>FTS：全文搜索是什么</h2><p>FTS 是 Full-Text Search，也就是全文搜索。</p><p>普通字符串搜索更像 <code>grep</code>：在文件里找一段文本。FTS 会先把文档分词、建立索引，再快速查找包含查询词的文档或 chunk。</p><p>例如查询：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Projection Overlay Workspace Revision</span><br></pre></td></tr></table></figure><p>FTS 会寻找包含这些词的文档片段。它特别适合搜索：</p><ul><li>类名、函数名、变量名</li><li>错误码和配置项</li><li>技术术语</li><li>产品名和领域词</li><li>精确的 API 名称</li></ul><p>FTS 的特点是“看词”，不真正理解语义。因此“Revision 是什么”和“Revision 的定义”未必会得到同样好的结果，中文自然语言问题也可能因为分词和语言配置而召回不稳定。</p><h2 id="BM25：给搜索结果排序"><a href="#BM25：给搜索结果排序" class="headerlink" title="BM25：给搜索结果排序"></a>BM25：给搜索结果排序</h2><p>FTS 负责找候选结果，BM25 负责判断谁更相关。</p><p>BM25 是 Best Matching 25，一种经典的关键词相关性排序算法。它会综合考虑：</p><ul><li>查询词是否出现在文档中</li><li>查询词出现了多少次</li><li>这个词是不是稀有词</li><li>文档长度是否过长</li></ul><p>可以把它理解成：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">FTS：哪些文档包含这些词？</span><br><span class="line">BM25：包含这些词的文档，谁最值得排在前面？</span><br></pre></td></tr></table></figure><p>BM25 不理解“意思”，但它对代码和技术术语非常实用。搜索 <code>Workspace Revision</code> 时，BM25 往往比纯向量相似度更可靠，因为向量模型可能把一些语义相近但并不包含这个精确标识符的文本排得很前。</p><h2 id="Vector-Search：按语义找内容"><a href="#Vector-Search：按语义找内容" class="headerlink" title="Vector Search：按语义找内容"></a>Vector Search：按语义找内容</h2><p>Vector Search 会用 Embedding Model 把问题和文档转换成向量，再比较它们在向量空间中的距离。</p><p>例如下面几个问题的关键词不完全相同，但语义接近：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">Revision 是什么？</span><br><span class="line">一个 Workspace 的版本如何确定？</span><br><span class="line">工作区如何固定一组可信资产？</span><br></pre></td></tr></table></figure><p>纯 FTS 可能认为它们差异很大；Vector Search 有机会把它们召回到同一组文档。</p><p>但 Vector Search 也不是万能的。它可能忽略精确术语、混淆相似概念，还可能把“相关但不适用”的内容排到前面。因此工程上更常见的是 Hybrid Search：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Hybrid Search = FTS/BM25 + Vector Search + Metadata Filter</span><br></pre></td></tr></table></figure><p>当前的 GBrain MVP 先使用 FTS，原因很务实：本地部署简单、依赖少、可解释，而且对项目里的英文领域术语已经有效。后续再加入 Embedding，解决中文自然语言和同义表达的召回问题。</p><h2 id="GraphRAG：回答关系问题"><a href="#GraphRAG：回答关系问题" class="headerlink" title="GraphRAG：回答关系问题"></a>GraphRAG：回答关系问题</h2><p>GraphRAG 不是“把所有 RAG 都换成图数据库”，而是把知识表达成实体和关系：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">Workspace Revision ──固定──&gt; Asset Version</span><br><span class="line">Overlay ──叠加于──&gt; Revision</span><br><span class="line">Projection ──生成自──&gt; Revision</span><br><span class="line">Evidence ──支撑──&gt; Claim</span><br></pre></td></tr></table></figure><p>当用户问的是“某个词是什么”时，FTS 或 Vector Search 通常已经够用；当用户问的是“几个对象之间有什么关系”“这条结论依赖哪些事实”“修改某个事实会影响什么”时，图结构就更有价值。</p><p>GraphRAG 的优势是关系可追踪、路径可解释，适合架构依赖、权限关系、组织知识、实体关联和影响分析。代价是数据建模和维护成本更高，图谱抽取也可能引入错误关系。</p><h2 id="LLM-Wiki-到底是什么"><a href="#LLM-Wiki-到底是什么" class="headerlink" title="LLM Wiki 到底是什么"></a>LLM Wiki 到底是什么</h2><p>LLM Wiki 更像一个知识工程系统，而不只是一个检索算法。它通常包含：</p><ol><li>Fact Sources：事实源，例如代码仓库、语雀文档、钉钉文档、ADR、会议纪要。</li><li>Ingestion：把事实源同步、解析、切块和建立索引。</li><li>Retrieval：用 FTS、BM25、Vector Search 或 Graph Search 找资料。</li><li>Evidence：检索出来、可以支撑某个结论的证据片段。</li><li>Citation：证据的可回溯引用地址。</li><li>Revision：事实在某个时间点的版本和有效范围。</li><li>Governance：处理冲突、过期、权限和审核。</li></ol><p>因此，LLM Wiki 的关键问题不是“模型能不能回答”，而是：</p><blockquote><p>这条回答依据的事实是什么？事实来自哪里？什么时候有效？谁能修改？</p></blockquote><p>这就是它和简单 RAG 的差异。简单 RAG 重点解决“找一些相关文本”；LLM Wiki 还要解决“哪些文本可信、如何引用、冲突怎么办、知识怎么演进”。</p><h2 id="Evidence-为什么重要"><a href="#Evidence-为什么重要" class="headerlink" title="Evidence 为什么重要"></a>Evidence 为什么重要</h2><p>Evidence 可以翻译成“证据”，但在知识系统里它不是随便一段上下文，而是能够支撑某个 Claim 的可定位事实。</p><p>例如：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">Claim：Projection 不是真实事实源，而是可重建的派生视图。</span><br><span class="line">Evidence：架构文档中的 Projection 定义段落。</span><br><span class="line">Citation：gbrain://default/example-workspace-spec</span><br></pre></td></tr></table></figure><p>没有 Evidence，LLM 只是“说得像”；有 Evidence 和 Citation，回答才具备审计能力。</p><p>在这个 Agent 应用里，我希望每一个关键回答都能回答三个问题：</p><ul><li>依据是哪条 Evidence？</li><li>来源是哪一个事实源？</li><li>如果事实发生 Revision，当前回答是否仍然有效？</li></ul><h2 id="什么时候用什么"><a href="#什么时候用什么" class="headerlink" title="什么时候用什么"></a>什么时候用什么</h2><table><thead><tr><th>问题类型</th><th>优先技术</th><th>原因</th></tr></thead><tbody><tr><td>查函数名、错误码、配置项</td><td>FTS + BM25</td><td>精确词匹配可靠、速度快</td></tr><tr><td>用自然语言找相似说明</td><td>Vector Search</td><td>能处理同义表达和语义相近问题</td></tr><tr><td>同时兼顾术语和语义</td><td>Hybrid Search</td><td>工程上最常见的综合方案</td></tr><tr><td>查询实体之间的关系</td><td>GraphRAG</td><td>关系和路径可解释</td></tr><tr><td>要求回答可引用、可审计、可演进</td><td>LLM Wiki</td><td>需要 Evidence、Citation、Revision 和治理</td></tr><tr><td>当前阶段</td><td>FTS&#x2F;BM25 + Evidence</td><td>先解决本地、可解释、低依赖的知识接入</td></tr></tbody></table><p>不建议一开始就上完整 GraphRAG。知识量还小、关系还没稳定时，先把事实源、分块、检索、引用和版本做好，收益更高。</p><h2 id="应用侧的落地顺序"><a href="#应用侧的落地顺序" class="headerlink" title="应用侧的落地顺序"></a>应用侧的落地顺序</h2><p>我现在采用的顺序是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">本地事实源</span><br><span class="line">  ↓</span><br><span class="line">GBrain 索引</span><br><span class="line">  ↓</span><br><span class="line">FTS/BM25 检索</span><br><span class="line">  ↓</span><br><span class="line">Evidence + Citation</span><br><span class="line">  ↓</span><br><span class="line">应用侧注入 Agent Turn</span><br><span class="line">  ↓</span><br><span class="line">Codex / Qoder 生成或执行</span><br></pre></td></tr></table></figure><p>后续按实际问题升级：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">第一阶段：FTS/BM25，确保精确术语能找到</span><br><span class="line">第二阶段：Embedding，提升中文和语义召回</span><br><span class="line">第三阶段：Hybrid Search，结合关键词与向量</span><br><span class="line">第四阶段：对稳定实体建立关系图</span><br><span class="line">第五阶段：补齐 Revision、权限、冲突和审核</span><br></pre></td></tr></table></figure><p>验证效果时，不要只看“回答听起来不错”。至少要测四件事：命中率、证据准确率、引用覆盖率、额外延迟。尤其要做反向问题：知识库没有答案时，系统能不能明确说不知道，而不是编造一个答案。</p><h2 id="要不要用-我的判断框架"><a href="#要不要用-我的判断框架" class="headerlink" title="要不要用 &#x2F; 我的判断框架"></a>要不要用 &#x2F; 我的判断框架</h2><p>我的判断很简单：</p><ul><li>只有精确技术词：先用 FTS + BM25。</li><li>用户问题表达多样：加入 Vector Search。</li><li>问题核心是实体关系：再加入 GraphRAG。</li><li>团队需要长期维护、追责和协作：建设 LLM Wiki。</li></ul><p>LLM Wiki 不是 RAG 的“替代品”，而是把 RAG 变成可维护知识基础设施的一种方式。GraphRAG 也不是必选终点，它只是关系型问题的一种强力检索手段。</p><p>对应用侧来说，最重要的不是把所有名词都装进去，而是先让每次 Agent 执行都能拿到可信、可引用、与当前 Revision 对齐的 Evidence。知识系统的价值，最终体现在它能不能让 Agent 少猜一次、少改错一次、少重复问团队一次。</p>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/849fe92ca6d7/</id>
    <link href="https://blog.zisheng.pro/posts/849fe92ca6d7/"/>
    <published>2026-07-22T15:20:00.000Z</published>
    <summary>RAG、FTS、BM25、Vector Search、GraphRAG 和 LLM Wiki 经常一起出现，却解决着不同层次的问题。本文用一条从文档到答案的链路，把这些概念放进同一张地图，并结合一个 Agent 应用说明什么时候该用关键词检索、语义检索、图谱，什么时候需要 Evidence 和 Revision。</summary>
    <title>从 FTS、BM25 到 LLM Wiki：把 RAG 知识检索讲明白</title>
    <updated>2026-09-18T08:15:28.636Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="AI" scheme="https://blog.zisheng.pro/categories/AI/"/>
    <category term="RAG" scheme="https://blog.zisheng.pro/tags/RAG/"/>
    <category term="AI Infra" scheme="https://blog.zisheng.pro/tags/AI-Infra/"/>
    <category term="GBrain" scheme="https://blog.zisheng.pro/tags/GBrain/"/>
    <category term="Sirchmunk" scheme="https://blog.zisheng.pro/tags/Sirchmunk/"/>
    <category term="Understand Anything" scheme="https://blog.zisheng.pro/tags/Understand-Anything/"/>
    <category term="Knowledge Graph" scheme="https://blog.zisheng.pro/tags/Knowledge-Graph/"/>
    <category term="Agent Memory" scheme="https://blog.zisheng.pro/tags/Agent-Memory/"/>
    <content>
      <![CDATA[<p>最近在设计一个 Agent Host 的知识架构时，我连续遇到了三个名字：GBrain、Sirchmunk 和 Understand Anything。</p><p>它们都宣称能让 Agent 更懂知识、更懂文件或更懂代码，功能列表里也都有 Search、Knowledge、Graph、MCP。第一眼很容易把它们排成一条升级路线：普通 RAG 不够，就上 Graph；Graph 还不够，就做 LLM Wiki；最后找一个“大而全”的项目把其他方案全部替代。</p><p>但我真正把场景拆开后发现，这个思路的问题很大。团队维护的 Spec、正在变化的本地 Diff、函数之间的调用关系，本来就是三种不同的数据。硬塞进同一个检索范式，最终往往不是架构统一，而是让一个组件承担它不擅长的工作。</p><p>这篇文章记录我目前的判断：三者分别适合什么，什么时候组合，以及为什么第一版不应该一次性把它们全部接入。</p><h2 id="一句话总结"><a href="#一句话总结" class="headerlink" title="一句话总结"></a>一句话总结</h2><p><strong>GBrain 负责沉淀长期知识，Sirchmunk 负责探索实时原始数据，Understand Anything 负责理解代码结构。它们不是相互替代的升级关系，而是 Knowledge Plane、Raw Context Search 和 Code Intelligence 三个不同层次。</strong></p><p>如果只能先选一个，我会以 GBrain 作为知识底座；Sirchmunk 和 Understand Anything 都等真实瓶颈出现后，再作为 Adapter 接入。</p><h2 id="先把三个项目放回各自的问题里"><a href="#先把三个项目放回各自的问题里" class="headerlink" title="先把三个项目放回各自的问题里"></a>先把三个项目放回各自的问题里</h2><table><thead><tr><th>项目</th><th>核心问题</th><th>最适合的数据</th><th>典型输出</th></tr></thead><tbody><tr><td>GBrain</td><td>怎样让知识长期积累、关联并被 Agent 稳定使用</td><td>Wiki、Spec、ADR、研究结论、团队知识</td><td>页面、Citation、Hybrid Search、Graph、综合回答</td></tr><tr><td>Sirchmunk</td><td>怎样直接从快速变化的原始数据里找到有效证据</td><td>本地文件、日志、临时文档、未提交内容</td><td>Evidence Snippet、Knowledge Cluster、实时回答</td></tr><tr><td>Understand Anything</td><td>怎样理解代码库的结构和影响范围</td><td>Repository、函数、类、Import、Call、Diff</td><td>Code Graph、Domain Flow、Diff Impact、Guided Tour</td></tr></tbody></table><p>三个项目都能“搜索”，但搜索对象和目标不同。</p><p>搜索一篇 ADR 的结论，核心是语义召回和 Citation；搜索刚生成的构建日志，核心是不用等待重新建索引；回答“修改这个接口会影响哪些模块”，核心是调用和依赖关系。把它们都叫 RAG，会掩盖真正的工程差异。</p><h2 id="GBrain：团队和-Agent-的长期知识底座"><a href="#GBrain：团队和-Agent-的长期知识底座" class="headerlink" title="GBrain：团队和 Agent 的长期知识底座"></a>GBrain：团队和 Agent 的长期知识底座</h2><p><a href="https://github.com/garrytan/gbrain">GBrain</a> 更接近一个完整的 Agent Brain：Markdown Brain Repo 保存长期知识，检索层组合 Keyword、Vector、RRF 和 Graph Signal，查询层既能返回原始结果，也能生成带 Citation 的综合回答和知识缺口分析。</p><p>它同时提供两种有吸引力的部署形态：</p><table><thead><tr><th>场景</th><th>引擎</th></tr></thead><tbody><tr><td>个人、本地、零服务部署</td><td>PGLite</td></tr><tr><td>团队共享、大规模、多机器</td><td>Postgres + pgvector</td></tr></tbody></table><p>这对桌面 Agent 产品很合适：云端可以维护团队 Brain，本地可以维护当前 Workspace 的 Brain，Agent 在断网时仍然能查询已经同步的知识。</p><p>更重要的是，GBrain 覆盖了我最初理解的 LLM Wiki 核心能力：知识不是永远停留在原始 Chunk，而是被编译为可读、可链接、可持续维护的页面。查询产生的新结论也可以继续积累，而不是随着 Chat Session 消失。</p><p>不过这里有一个关键边界：<strong>GBrain 是知识引擎，不应该自动成为业务系统的权限和发布中心。</strong></p><p>在企业或团队场景里，我仍然需要外层系统负责：</p><ul><li>哪个 Knowledge Version 已经发布；</li><li>当前 Workspace 能访问哪些知识；</li><li>哪些页面经过 Owner 审核；</li><li>哪个 Session 固定了哪个 Revision；</li><li>查询和执行是否需要进入 Audit。</li></ul><p>因此，我更愿意把 GBrain 放在 Knowledge Plane，把版本、签名、权限和审计留给宿主系统。</p><h2 id="Sirchmunk：不预建索引，直接探索-Raw-Data"><a href="#Sirchmunk：不预建索引，直接探索-Raw-Data" class="headerlink" title="Sirchmunk：不预建索引，直接探索 Raw Data"></a>Sirchmunk：不预建索引，直接探索 Raw Data</h2><p><a href="https://github.com/modelscope/sirchmunk">Sirchmunk</a> 的价值不在于又实现了一套 Vector RAG，而是它选择了另一条路径：直接面对 Raw Data 做 Indexless Agentic Search。</p><p>它通过 Grep Retriever、关键词级联和 Monte Carlo Evidence Sampling，从原始文件里定位相关区域，再让 LLM 综合为 Knowledge Cluster。官方提供 FAST、DEEP 和 FILENAME_ONLY 等模式，也提供 MCP，适合直接挂到 Agent 上。</p><p>这个设计特别适合快速变化的数据：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">未提交 Diff</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><p>这些内容如果每改一次就重新 Embedding、重新构图，维护成本可能比检索收益还高。Sirchmunk 的思路是先搜索当前真相，再决定哪些结论值得沉淀。</p><p>但它不适合直接接管正式团队知识。一次 Agentic Search 得到的 Knowledge Cluster，仍然需要回答：来源是什么、是否过期、是否和正式 Spec 冲突、谁审核过。我的处理方式是把 Sirchmunk 的结果当作当前任务上下文；需要长期复用时，先转成 Knowledge Draft，再进入审核和发布。</p><h2 id="Understand-Anything：代码不是普通文档"><a href="#Understand-Anything：代码不是普通文档" class="headerlink" title="Understand Anything：代码不是普通文档"></a>Understand Anything：代码不是普通文档</h2><p><a href="https://github.com/Lum1104/Understand-Anything">Understand Anything</a> 解决的是另一类问题：Agent 能读到代码，却缺少整个 Repository 的结构认识。</p><p>它使用 Tree-sitter 和多 Agent Pipeline 提取 File、Function、Class、Dependency 等节点与关系，生成可交互 Knowledge Graph，并提供 Architecture Layer、Domain Flow、Diff Impact、Guided Tour 等视图。它还能分析 Karpathy 风格的 LLM Wiki，把显式 Wikilink 和隐式知识关系可视化。</p><p>这和把源代码切 Chunk 做 Vector Search 有本质区别。</p><p>假设我问：</p><blockquote><p>修改 <code>PermissionBroker</code> 的返回结构，会影响哪些 Workflow、Adapter 和 UI？</p></blockquote><p>向量检索能找到“语义相似”的文件，但它不能保证遍历真实 Import、Call 和 Type Dependency。Code Graph 的价值就在于把这类结构事实显式化。</p><p>Understand Anything 的输出也不应该被理解为第二份代码事实源。代码仍然以 Git Revision 为准，Graph 是可重建 Projection。只要这一点守住，它很适合作为 Repository Knowledge 的生产器，把代码结构结果继续交给 GBrain 做长期查询和关联。</p><h2 id="正确组合：一个底座，两个按需-Adapter"><a href="#正确组合：一个底座，两个按需-Adapter" class="headerlink" title="正确组合：一个底座，两个按需 Adapter"></a>正确组合：一个底座，两个按需 Adapter</h2><p>我目前采用的组合关系是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">团队 Spec / ADR / Workflow ────────┐</span><br><span class="line">                                   │</span><br><span class="line">Repository Revision ─ Understand Anything</span><br><span class="line">                                   │</span><br><span class="line">                                   ▼</span><br><span class="line">                              GBrain Cloud</span><br><span class="line">                                   │</span><br><span class="line">                         发布版本化 Knowledge</span><br><span class="line">                                   │</span><br><span class="line">                                   ▼</span><br><span class="line">                              GBrain Local</span><br><span class="line">                                   ▲</span><br><span class="line">当前 Workspace / Diff ─ Sirchmunk ┘</span><br><span class="line">                                   │</span><br><span class="line">                                   ▼</span><br><span class="line">                              Coding Agent</span><br></pre></td></tr></table></figure><p>但这张图容易产生一个误解：Sirchmunk 的结果不是默认全部写入 GBrain。更准确的运行逻辑是：</p><ol><li>正式知识优先查询 GBrain；</li><li>问题涉及当前未提交内容时，查询 Sirchmunk；</li><li>问题涉及调用链和 Diff Impact 时，查询 Understand Anything；</li><li>多个结果由宿主系统合并，并保留各自 Citation；</li><li>只有经过验证和审核的稳定结论，才进入 GBrain 的正式页面。</li></ol><p>统一接口可以很小：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> <span class="title class_">ContextIntent</span> =</span><br><span class="line">  | <span class="string">&quot;knowledge&quot;</span></span><br><span class="line">  | <span class="string">&quot;raw-files&quot;</span></span><br><span class="line">  | <span class="string">&quot;code-graph&quot;</span></span><br><span class="line">  | <span class="string">&quot;auto&quot;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">interface</span> <span class="title class_">ContextQuery</span> &#123;</span><br><span class="line">  <span class="attr">workspace</span>: <span class="built_in">string</span>;</span><br><span class="line">  <span class="attr">revision</span>: <span class="built_in">string</span>;</span><br><span class="line">  <span class="attr">query</span>: <span class="built_in">string</span>;</span><br><span class="line">  <span class="attr">intent</span>: <span class="title class_">ContextIntent</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_">ContextResult</span> &#123;</span><br><span class="line">  <span class="attr">engine</span>: <span class="string">&quot;gbrain&quot;</span> | <span class="string">&quot;sirchmunk&quot;</span> | <span class="string">&quot;understand-anything&quot;</span>;</span><br><span class="line">  <span class="attr">content</span>: <span class="built_in">string</span>;</span><br><span class="line">  <span class="attr">citation</span>: <span class="built_in">string</span>;</span><br><span class="line">  <span class="attr">digest</span>: <span class="built_in">string</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>Agent 不需要知道三套引擎的数据库、进程和权限模型，只需要调用宿主系统暴露的 <code>knowledge.search</code>、<code>workspace.search</code> 和 <code>code.graph_query</code>。</p><h2 id="云端和本地应该怎样分工"><a href="#云端和本地应该怎样分工" class="headerlink" title="云端和本地应该怎样分工"></a>云端和本地应该怎样分工</h2><p>我不赞成“所有知识都在云端”或“所有知识都只在本地”这两种极端方案。</p><p>云端适合维护：</p><ul><li>团队 Spec、ADR、Workflow 和 Skill；</li><li>已发布 Repository Revision 的稳定分析结果；</li><li>Owner Review 后的 Wiki 页面；</li><li>跨团队共享检索和正式 Knowledge Version。</li></ul><p>本地适合维护：</p><ul><li>当前 Workspace；</li><li>未提交 Diff；</li><li>本地 Overlay；</li><li>当前 Session 固定的 Knowledge Revision；</li><li>不能默认上传的代码、日志和执行结果。</li></ul><p>同步的也不应该是数据库文件，而应该是版本化资产：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">Cloud Knowledge</span><br><span class="line">  → Asset Version + Digest + Signature</span><br><span class="line">  → Local Verify</span><br><span class="line">  → Materialize Local Brain</span><br><span class="line">  → Build Index</span><br><span class="line">  → Atomic Activate Revision</span><br></pre></td></tr></table></figure><p>这样才能做到离线、回滚和可复现。如果 Agent 每次都读云端的“最新知识”，同一个长 Session 前后可能得到不同结论，审计和复现都会失去基础。</p><h2 id="Execution-Evidence-不等于-Wiki-Citation"><a href="#Execution-Evidence-不等于-Wiki-Citation" class="headerlink" title="Execution Evidence 不等于 Wiki Citation"></a>Execution Evidence 不等于 Wiki Citation</h2><p>在讨论这套系统时，我反复看到 Evidence 这个词，但它不是 LLM Wiki 独有的核心概念。</p><table><thead><tr><th>概念</th><th>含义</th></tr></thead><tbody><tr><td>Source</td><td>原始材料</td></tr><tr><td>Citation</td><td>结论引用的具体位置</td></tr><tr><td>Provenance</td><td>知识经历了哪些加工和审核</td></tr><tr><td>Execution Evidence</td><td>Agent 的动作是否真的成功</td></tr></tbody></table><p>例如“测试已经通过”只是一句 Agent Message；真正的 Execution Evidence 应该包含命令、退出码、测试数量、时间和对应 Revision。</p><p>Evidence 可以反哺知识，但不能自动成为知识：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">测试结果 / Diff / Commit</span><br><span class="line">        ↓</span><br><span class="line">Execution Evidence</span><br><span class="line">        ↓</span><br><span class="line">提炼稳定机制</span><br><span class="line">        ↓</span><br><span class="line">Knowledge Draft</span><br><span class="line">        ↓</span><br><span class="line">Review + Citation</span><br><span class="line">        ↓</span><br><span class="line">正式 Wiki</span><br></pre></td></tr></table></figure><p>这个边界非常重要。否则一次错误推理或偶发现象写进团队 Wiki，之后所有 Agent 都会把它当成事实。</p><h2 id="第一版不要三件套全上"><a href="#第一版不要三件套全上" class="headerlink" title="第一版不要三件套全上"></a>第一版不要三件套全上</h2><p>从工程投入看，三者同时接入意味着：</p><ul><li>三套生命周期和升级策略；</li><li>三套索引或持久化；</li><li>多种查询结果的融合与去重；</li><li>更多 API Key、Sidecar 和供应链风险；</li><li>更复杂的 Revision、Permission 和 Citation 映射。</li></ul><p>因此我的实施顺序是：</p><table><thead><tr><th>阶段</th><th>目标</th></tr></thead><tbody><tr><td>Phase 1</td><td>GBrain Local 跑通团队 Knowledge 到 Agent 的查询闭环</td></tr><tr><td>Phase 2</td><td>增加 GBrain Cloud、Knowledge Draft、Review 和版本发布</td></tr><tr><td>Phase 3</td><td>Raw Files 实时检索成为瓶颈时接 Sirchmunk</td></tr><tr><td>Phase 4</td><td>调用链、依赖和 Diff Impact 成为瓶颈时接 Understand Anything</td></tr></tbody></table><p>甚至在小型 Repository 中，<code>rg + LSP + 架构文档</code> 可能已经够用。是否引入新组件，应该由真实任务的 Recall、Precision、Latency、Token 和维护成本决定，而不是由功能列表决定。</p><h2 id="要不要用-我的判断框架"><a href="#要不要用-我的判断框架" class="headerlink" title="要不要用 &#x2F; 我的判断框架"></a>要不要用 &#x2F; 我的判断框架</h2><p>我的结论不是“三个都值得上”，而是分三档：</p><h3 id="值得上：GBrain"><a href="#值得上：GBrain" class="headerlink" title="值得上：GBrain"></a>值得上：GBrain</h3><p>如果 Agent 需要跨 Session 积累团队知识，需要云端共享、本地离线和长期 Citation，GBrain 值得作为 Knowledge Plane 验证。它覆盖了 LLM Wiki、Hybrid RAG、Graph 和 Synthesis，能先建立一条完整主链路。</p><h3 id="可以再等：Sirchmunk"><a href="#可以再等：Sirchmunk" class="headerlink" title="可以再等：Sirchmunk"></a>可以再等：Sirchmunk</h3><p>如果当前 <code>rg</code>、FTS 或 GBrain 对 Workspace Raw Files 已经够快，Sirchmunk 可以先不接。等频繁重建索引、临时大文件和日志检索真的影响体验，再引入它的 Indexless Search。</p><h3 id="重点关注：Understand-Anything"><a href="#重点关注：Understand-Anything" class="headerlink" title="重点关注：Understand Anything"></a>重点关注：Understand Anything</h3><p>如果产品的核心价值包含 Repository Onboarding、影响分析和架构理解，Understand Anything 很有价值；如果只是让 Coding Agent 修改几个文件，它可能暂时是过度设计。</p><p>最终我用一句话约束自己的选型：</p><blockquote><p><strong>先建立一个稳定知识底座，再用真实任务证明是否需要实时搜索器和代码图谱；不要为了“看起来完整”一次性维护三套基础设施。</strong></p></blockquote><p>这也是我从前端工程走向 Agent &#x2F; AI Infra 后越来越强烈的体会：真正的架构能力，不是把最多的组件画进图里，而是知道每个组件何时应该缺席。</p>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/6bb08b12e2eb/</id>
    <link href="https://blog.zisheng.pro/posts/6bb08b12e2eb/"/>
    <published>2026-07-21T18:00:00.000Z</published>
    <summary>GBrain、Sirchmunk 和 Understand Anything 都在解决 Agent 的上下文问题，却分别面向长期知识、实时原始数据和代码结构。本文结合桌面 Agent Host 的架构实践，拆解三者的能力边界、组合方式、云端与本地部署，并给出从单一知识底座逐步演进的选型框架。</summary>
    <title>GBrain、Sirchmunk、Understand Anything 怎么搭配：Agent 知识基础设施的三层实践</title>
    <updated>2026-09-18T08:15:28.629Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="React" scheme="https://blog.zisheng.pro/tags/React/"/>
    <category term="IME" scheme="https://blog.zisheng.pro/tags/IME/"/>
    <category term="CompositionEvent" scheme="https://blog.zisheng.pro/tags/CompositionEvent/"/>
    <category term="KeyboardEvent" scheme="https://blog.zisheng.pro/tags/KeyboardEvent/"/>
    <category term="Agentic Chat" scheme="https://blog.zisheng.pro/tags/Agentic-Chat/"/>
    <content>
      <![CDATA[<p>最近在做 Agentic Chat 时，我遇到一个看似很小、实际会直接破坏输入体验的问题：使用中文输入法输入英文片段，候选框还开着，此时按 Enter 本来只是确认候选词，Composer 却把这次 Enter 当成了“发送消息”。</p><p>这个 Bug 特别容易被纯英文开发环境漏掉。按钮点击正常，英文键盘正常，甚至多数浏览器里只判断 <code>isComposing</code> 也能正常；但到了 macOS 输入法和 WebKit 的真实事件时序，消息仍然会提前发出去。</p><h2 id="一句话总结"><a href="#一句话总结" class="headerlink" title="一句话总结"></a>一句话总结</h2><p>Chat Composer 不能把所有 <code>Enter</code> 都视为提交。发送前至少要同时检查 <code>KeyboardEvent.isComposing</code>、IME 的兼容值 <code>keyCode === 229</code>，并处理部分 WebKit 环境中 <code>compositionend</code> 早于提交用 <code>keydown</code> 到达的时序差异。</p><p>最终的判断不是“用户有没有按 Enter”，而是：<strong>这次 Enter 的语义是确认输入法候选，还是提交已经完成的消息？</strong></p><h2 id="先理解-IME：输入框里的文字不一定已经提交"><a href="#先理解-IME：输入框里的文字不一定已经提交" class="headerlink" title="先理解 IME：输入框里的文字不一定已经提交"></a>先理解 IME：输入框里的文字不一定已经提交</h2><p>IME 是 Input Method Editor。中文拼音、日文假名、韩文以及部分语音和手写输入，并不是每按一个键就立即产生最终字符，而是先进入一个 composition session：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">compositionstart</span><br><span class="line">  ↓</span><br><span class="line">compositionupdate × N</span><br><span class="line">  ↓</span><br><span class="line">compositionend</span><br></pre></td></tr></table></figure><p>在这个阶段，输入框中看起来已经出现了 <code>auto</code>，但它可能仍处于输入法的组合态。用户按 Enter 的意图是从候选列表中选择 <code>automation</code>，不是把整条聊天消息发给 Agent。</p><p>浏览器通过 <code>KeyboardEvent.isComposing</code> 暴露当前按键是否发生在 composition session 内。MDN 对它的定义很直接：在 <code>compositionstart</code> 之后、<code>compositionend</code> 之前触发的键盘事件，该值应为 <code>true</code>。</p><p>因此最基础的发送逻辑应该是：</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">handleKeyDown</span>(<span class="params">event: React.KeyboardEvent&lt;HTMLTextAreaElement&gt;</span>) &#123;</span><br><span class="line">  <span class="keyword">if</span> (event.<span class="property">key</span> !== <span class="string">&quot;Enter&quot;</span>) <span class="keyword">return</span>;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (event.<span class="property">nativeEvent</span>.<span class="property">isComposing</span>) &#123;</span><br><span class="line">    <span class="keyword">return</span>;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (!event.<span class="property">shiftKey</span>) &#123;</span><br><span class="line">    event.<span class="title function_">preventDefault</span>();</span><br><span class="line">    <span class="title function_">sendMessage</span>();</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这能解决大多数标准事件顺序，但还不够。</p><h2 id="为什么只判断-isComposing-仍然会误发送"><a href="#为什么只判断-isComposing-仍然会误发送" class="headerlink" title="为什么只判断 isComposing 仍然会误发送"></a>为什么只判断 isComposing 仍然会误发送</h2><p>问题出在浏览器和操作系统对 composition 与 keyboard event 的排序并不完全一致。</p><p>理想情况下，用户按 Enter 确认候选时，<code>keydown</code> 发生在 composition 结束之前：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">keydown Enter (isComposing: true)</span><br><span class="line">compositionend</span><br><span class="line">keyup Enter</span><br></pre></td></tr></table></figure><p>此时 <code>isComposing</code> 足以阻止发送。</p><p>但 WebKit 在 macOS 上长期存在另一种时序：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">compositionend</span><br><span class="line">keydown Enter (isComposing: false)</span><br><span class="line">keyup Enter</span><br></pre></td></tr></table></figure><p>对业务代码来说，收到 <code>keydown</code> 时 composition 已经结束，<code>isComposing</code> 自然变成 <code>false</code>。如果发送条件只是：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">event.<span class="property">key</span> === <span class="string">&quot;Enter&quot;</span> &amp;&amp; !event.<span class="property">nativeEvent</span>.<span class="property">isComposing</span></span><br></pre></td></tr></table></figure><p>这次用于确认候选的 Enter 就会穿透保护，直接提交表单。</p><p>WebKit Bugzilla 的相关记录明确提到：macOS 上接受 IME 输入的 Enter 可能在 <code>compositionend</code> 之后才以 <code>keydown</code> 到达，因此 <code>isComposing === false</code>。这不是 React 自己的状态问题，而是底层事件顺序已经让上层失去了原始语义。</p><h2 id="keyCode-229-为什么仍然有价值"><a href="#keyCode-229-为什么仍然有价值" class="headerlink" title="keyCode 229 为什么仍然有价值"></a>keyCode 229 为什么仍然有价值</h2><p><code>keyCode</code> 已经是 legacy API，正常键盘处理应该优先使用 <code>event.key</code> 和 <code>event.code</code>。但 UI Events 规范仍然保留了一条与 IME 直接相关的兼容规则：如果 <code>keydown</code> 正由 Input Method Editor 处理，返回值是 <code>229</code>。</p><p>因此，IME guard 中使用 <code>keyCode === 229</code> 不是把所有键盘逻辑退回旧 API，而是把它作为一个窄范围的兼容信号：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> nativeEvent = event.<span class="property">nativeEvent</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> isImeEvent =</span><br><span class="line">  nativeEvent.<span class="property">isComposing</span> ||</span><br><span class="line">  nativeEvent.<span class="property">keyCode</span> === <span class="number">229</span>;</span><br></pre></td></tr></table></figure><p>它能覆盖一部分 <code>isComposing</code> 不可靠、但浏览器仍把按键标记为 IME processing 的情况。</p><p>不过它仍不是完整答案。有些环境可能在 <code>compositionend</code> 后把这次 Enter 暴露成普通的 <code>keyCode 13</code>。因此还需要记录组件自己的 composition 状态，并给刚结束的 composition 留一个很短的保护窗口。</p><h2 id="稳健修复：三层-IME-Guard"><a href="#稳健修复：三层-IME-Guard" class="headerlink" title="稳健修复：三层 IME Guard"></a>稳健修复：三层 IME Guard</h2><p>我最后采用了三层判断：</p><ol><li>组件内的 <code>composingRef</code>，记录 <code>compositionstart</code> 到 <code>compositionend</code>。</li><li>浏览器提供的 <code>nativeEvent.isComposing</code> 与 <code>keyCode === 229</code>。</li><li><code>compositionend</code> 后一个短暂的 grace period，挡住乱序到达的 Enter。</li></ol><p>React 实现如下：</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; useRef &#125; <span class="keyword">from</span> <span class="string">&quot;react&quot;</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_">ChatComposer</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> composing = <span class="title function_">useRef</span>(<span class="literal">false</span>);</span><br><span class="line">  <span class="keyword">const</span> compositionJustEnded = <span class="title function_">useRef</span>(<span class="literal">false</span>);</span><br><span class="line">  <span class="keyword">const</span> compositionTimer = useRef&lt;<span class="title class_">ReturnType</span>&lt;<span class="keyword">typeof</span> <span class="built_in">setTimeout</span>&gt; | <span class="literal">null</span>&gt;(<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">textarea</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">      <span class="attr">onCompositionStart</span>=<span class="string">&#123;()</span> =&gt;</span> &#123;</span></span><br><span class="line"><span class="language-xml">        composing.current = true;</span></span><br><span class="line"><span class="language-xml">        compositionJustEnded.current = false;</span></span><br><span class="line"><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">        if (compositionTimer.current) &#123;</span></span><br><span class="line"><span class="language-xml">          clearTimeout(compositionTimer.current);</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">      onCompositionEnd=&#123;() =&gt; &#123;</span></span><br><span class="line"><span class="language-xml">        composing.current = false;</span></span><br><span class="line"><span class="language-xml">        compositionJustEnded.current = true;</span></span><br><span class="line"><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">        if (compositionTimer.current) &#123;</span></span><br><span class="line"><span class="language-xml">          clearTimeout(compositionTimer.current);</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">        compositionTimer.current = setTimeout(() =&gt; &#123;</span></span><br><span class="line"><span class="language-xml">          compositionJustEnded.current = false;</span></span><br><span class="line"><span class="language-xml">        &#125;, 80);</span></span><br><span class="line"><span class="language-xml">      &#125;&#125;</span></span><br><span class="line"><span class="language-xml">      onKeyDown=&#123;(event) =&gt; &#123;</span></span><br><span class="line"><span class="language-xml">        const nativeEvent = event.nativeEvent;</span></span><br><span class="line"><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">        const isImeEnter =</span></span><br><span class="line"><span class="language-xml">          event.key === &quot;Enter&quot; &amp;&amp;</span></span><br><span class="line"><span class="language-xml">          (</span></span><br><span class="line"><span class="language-xml">            composing.current ||</span></span><br><span class="line"><span class="language-xml">            compositionJustEnded.current ||</span></span><br><span class="line"><span class="language-xml">            nativeEvent.isComposing ||</span></span><br><span class="line"><span class="language-xml">            nativeEvent.keyCode === 229</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">        if (isImeEnter) &#123;</span></span><br><span class="line"><span class="language-xml">          event.preventDefault();</span></span><br><span class="line"><span class="language-xml">          return;</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">        if (event.key === &quot;Enter&quot; &amp;&amp; !event.shiftKey) &#123;</span></span><br><span class="line"><span class="language-xml">          event.preventDefault();</span></span><br><span class="line"><span class="language-xml">          sendMessage();</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>80ms</code> 不是协议常量，而是一个很窄的事件时序缓冲。它只保护 composition 刚结束后紧随而来的 Enter，不会改变用户完成输入后再次按 Enter 发送的习惯。如果产品允许调整发送方式，提供 <code>Cmd/Ctrl + Enter</code> 模式仍然是很好的补充，但不能用它掩盖默认 Enter 模式的 IME Bug。</p><h2 id="在-assistant-ui-里应该在哪里拦截"><a href="#在-assistant-ui-里应该在哪里拦截" class="headerlink" title="在 assistant-ui 里应该在哪里拦截"></a>在 assistant-ui 里应该在哪里拦截</h2><p>应用侧使用 assistant-ui 的 <code>ComposerPrimitive.Input</code>。这类 headless primitive 通常已经包含基础的 <code>isComposing</code> 判断，但宿主应用仍可能需要为特定桌面 WebView、输入法和系统版本补充 guard。</p><p>关键是把处理放在 Composer 输入边界，而不是 <code>sendMessage()</code> 里事后猜测：</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">&lt;<span class="title class_">ComposerPrimitive</span>.<span class="property">Input</span></span><br><span class="line">  onCompositionStart=&#123;handleCompositionStart&#125;</span><br><span class="line">  onCompositionEnd=&#123;handleCompositionEnd&#125;</span><br><span class="line">  onKeyDown=&#123;handleComposerKeyDown&#125;</span><br><span class="line">/&gt;</span><br></pre></td></tr></table></figure><p>原因有三点：</p><ul><li>输入组件最接近真实 DOM event，能拿到 composition 信息。</li><li>在这里 <code>preventDefault()</code>，可以阻止 primitive 后续的 submit handler。</li><li>发送函数不应该知道用户使用了拼音、假名还是物理键盘，它只负责提交已确认的消息。</li></ul><p>如果组件库会组合用户传入的 handler 与内部 handler，需要确认组合顺序及 <code>defaultPrevented</code> 语义。理想行为是外部 handler 先运行；外部调用 <code>preventDefault()</code> 后，内部 Enter-submit 不再执行。</p><h2 id="不要只测“中文能不能输入”"><a href="#不要只测“中文能不能输入”" class="headerlink" title="不要只测“中文能不能输入”"></a>不要只测“中文能不能输入”</h2><p>这个 Bug 的验收重点是事件语义，而不是文字最终有没有出现在 textarea。</p><p>至少覆盖下面这些用例：</p><table><thead><tr><th>场景</th><th>期望结果</th></tr></thead><tbody><tr><td>中文输入法候选打开，按 Enter</td><td>确认候选，不发送</td></tr><tr><td>日文输入法候选打开，按 Enter</td><td>确认候选，不发送</td></tr><tr><td>composition 完成后再次按 Enter</td><td>发送一次</td></tr><tr><td>普通英文输入后按 Enter</td><td>发送一次</td></tr><tr><td><code>Shift + Enter</code></td><td>插入换行，不发送</td></tr><tr><td>composition 中按 <code>Shift + Enter</code></td><td>交给输入法，不发送</td></tr><tr><td>连续快速确认候选</td><td>不丢字、不重复发送</td></tr><tr><td>Agent 正在运行时输入</td><td>遵守 queue &#x2F; steer 策略，不被 IME 改写</td></tr></tbody></table><p>自动化测试可以构造 <code>compositionStart</code>、<code>keyDown</code>、<code>compositionEnd</code> 序列，但不要只相信 JSDOM。IME 与 WebView 的差异发生在浏览器事件层，至少还需要一轮 macOS Safari &#x2F; WKWebView、Chrome，以及 Windows 中文输入法的真机测试。</p><p>下面是一个简化的测试意图：</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">fireEvent.<span class="title function_">compositionStart</span>(textarea);</span><br><span class="line">fireEvent.<span class="title function_">keyDown</span>(textarea, &#123;</span><br><span class="line">  <span class="attr">key</span>: <span class="string">&quot;Enter&quot;</span>,</span><br><span class="line">  <span class="attr">keyCode</span>: <span class="number">229</span>,</span><br><span class="line">  <span class="attr">isComposing</span>: <span class="literal">true</span>,</span><br><span class="line">&#125;);</span><br><span class="line">fireEvent.<span class="title function_">compositionEnd</span>(textarea);</span><br><span class="line"></span><br><span class="line"><span class="title function_">expect</span>(sendMessage).<span class="property">not</span>.<span class="title function_">toHaveBeenCalled</span>();</span><br><span class="line"></span><br><span class="line"><span class="keyword">await</span> <span class="title function_">waitForCompositionGuardToExpire</span>();</span><br><span class="line">fireEvent.<span class="title function_">keyDown</span>(textarea, &#123; <span class="attr">key</span>: <span class="string">&quot;Enter&quot;</span>, <span class="attr">keyCode</span>: <span class="number">13</span> &#125;);</span><br><span class="line"></span><br><span class="line"><span class="title function_">expect</span>(sendMessage).<span class="title function_">toHaveBeenCalledTimes</span>(<span class="number">1</span>);</span><br></pre></td></tr></table></figure><h2 id="更通用的工程判断：快捷键不能脱离输入状态"><a href="#更通用的工程判断：快捷键不能脱离输入状态" class="headerlink" title="更通用的工程判断：快捷键不能脱离输入状态"></a>更通用的工程判断：快捷键不能脱离输入状态</h2><p>这次问题不只属于聊天框。搜索框、命令面板、评论框、AI Prompt Composer、表格快捷编辑都可能把 Enter、Escape、方向键当作产品命令，但这些按键在 IME、自动补全、mention、slash command 和 accessibility 输入中还有另一层语义。</p><p>我现在会把键盘处理按下面的优先级设计：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">IME composition</span><br><span class="line">  → suggestion / mention / command popover</span><br><span class="line">  → editor-local behavior</span><br><span class="line">  → product shortcut</span><br><span class="line">  → global shortcut</span><br></pre></td></tr></table></figure><p>越靠前的输入状态，越应该先消费事件。全局快捷键不能因为“捕获到了 Enter”就认为用户要求执行命令。</p><p>这也是 Agentic Chat 与普通表单的差别：Composer 一旦误发送，触发的不只是一次 HTTP 请求，而可能是一个拥有文件、终端或外部系统权限的 Agent run。输入确认和任务执行之间必须有可靠边界。</p><h2 id="要不要用-我的判断框架"><a href="#要不要用-我的判断框架" class="headerlink" title="要不要用 &#x2F; 我的判断框架"></a>要不要用 &#x2F; 我的判断框架</h2><p><strong>值得上：</strong> 所有 Enter-to-submit 的 Chat、搜索、评论和命令输入框，都应该至少判断 <code>isComposing</code>；面向 macOS Safari、WKWebView 或 Tauri WebView 的产品，再补 <code>keyCode 229</code> 和 composition 结束保护。</p><p><strong>可以再等：</strong> 如果产品强制点击按钮发送，或者唯一快捷键是明确的 <code>Cmd/Ctrl + Enter</code>，grace period 的优先级可以降低，但 composition 状态仍应被组件正确维护。</p><p><strong>重点关注：</strong> 不要把 <code>80ms</code> 当成神奇数字散落在多个组件里。把 IME guard 封装成 hook 或 Composer adapter，集中处理计时器清理、平台差异和测试。未来替换编辑器或 UI primitive 时，也只需要重新验证这一层。</p><p>参考资料：</p><ul><li><a href="https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/isComposing">MDN：KeyboardEvent.isComposing</a></li><li><a href="https://www.w3.org/TR/uievents/">W3C：UI Events</a></li><li><a href="https://bugs.webkit.org/show_bug.cgi?id=165004">WebKit Bug 165004：macOS 上 keyboard 与 composition 事件顺序</a></li></ul>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/c76802c3a82f/</id>
    <link href="https://blog.zisheng.pro/posts/c76802c3a82f/"/>
    <published>2026-07-21T11:35:00.000Z</published>
    <summary>在 Chat Composer 里，Enter 通常代表发送，但在中文、日文、韩文等 IME 输入过程中，它还承担确认候选词的职责。本文从真实故障出发，拆解 composition 与 keyboard 事件时序、Safari/WebKit 的兼容陷阱，并给出适用于 React 和 Agentic Chat 的稳健修复与测试方案。</summary>
    <title>修复中文输入法按回车误发送：React Chat Composer 的 IME 事件陷阱</title>
    <updated>2026-09-18T08:15:28.636Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="AI" scheme="https://blog.zisheng.pro/categories/AI/"/>
    <category term="AI Infra" scheme="https://blog.zisheng.pro/tags/AI-Infra/"/>
    <category term="Provider Pattern" scheme="https://blog.zisheng.pro/tags/Provider-Pattern/"/>
    <category term="Adapter Pattern" scheme="https://blog.zisheng.pro/tags/Adapter-Pattern/"/>
    <category term="Agent Ops" scheme="https://blog.zisheng.pro/tags/Agent-Ops/"/>
    <category term="Codex" scheme="https://blog.zisheng.pro/tags/Codex/"/>
    <category term="Qoder" scheme="https://blog.zisheng.pro/tags/Qoder/"/>
    <category term="Architecture" scheme="https://blog.zisheng.pro/tags/Architecture/"/>
    <content>
      <![CDATA[<p>最近在做一个 Agent Ops 桌面应用时，我先接了 Codex，下一步要接 Qoder，后面还可能接 Hermes。最开始这看起来只是一个 SDK 集成问题：再写一个 <code>QoderClient</code>，在 UI 里加个选择框就结束了。</p><p>但我很快发现，真正的问题不是“怎么调用第二个 Agent”，而是：<strong>当第三个、第四个执行引擎出现时，业务代码是否还需要跟着修改？</strong></p><p>如果 UI 认识 <code>thread/start</code>，Workflow 判断 <code>provider === &#39;qoder&#39;</code>，Session 直接拿 Provider Session ID 当主键，那么所谓“多 Provider”只是把多个私有协议揉进了同一个应用。接入得越多，系统越难维护。</p><p>我最后采用的是 Provider Pattern：把 Codex、Qoder、Hermes 看作可替换的执行引擎，用 Adapter 吸收协议差异，再通过 Registry、Capability 和 Session Pin 建立稳定的 Agent Ops 控制面。</p><h2 id="一句话总结"><a href="#一句话总结" class="headerlink" title="一句话总结"></a>一句话总结</h2><p><strong>Provider 是能力提供者，Adapter 是把能力接入统一契约的翻译层。Agent Ops App 应该拥有 Session、权限、Workflow 和审计，Provider 只拥有原生执行上下文；新增 Provider 时，理想状态是只增加 Adapter 和 Projection，而不是修改整条业务链路。</strong></p><h2 id="Provider-不是-Adapter"><a href="#Provider-不是-Adapter" class="headerlink" title="Provider 不是 Adapter"></a>Provider 不是 Adapter</h2><p>这两个词经常同时出现，也最容易混在一起。</p><table><thead><tr><th>概念</th><th>回答的问题</th><th>在 Agent Ops App 中的例子</th></tr></thead><tbody><tr><td>Provider</td><td>谁提供执行能力</td><td>Codex、Qoder、Hermes</td></tr><tr><td>Provider Adapter</td><td>怎样接入这个 Provider</td><td><code>CodexAdapter</code>、<code>QoderAdapter</code></td></tr><tr><td>Provider Contract</td><td>App 要求所有 Provider 遵守什么接口</td><td><code>createSession</code>、<code>runTurn</code>、<code>interrupt</code></td></tr><tr><td>Provider Registry</td><td>运行时选择哪个 Adapter</td><td><code>providerId → ProviderAdapter</code></td></tr></tbody></table><p>Provider 更接近一种架构角色，不是 GoF 设计模式。支付系统里的支付宝、微信支付是 Payment Provider；对象存储里的 S3、OSS、GCS 是 Storage Provider；Agent Ops 中的 Codex、Qoder、Hermes就是 Agent Provider。</p><p>Adapter 则是经典的 Adapter Pattern。它不负责推理，只负责把 Provider 的原生接口翻译成 App 的统一语义。</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">Agent Ops App</span><br><span class="line">    ↓ Provider Contract</span><br><span class="line">Provider Adapter</span><br><span class="line">    ↓ Provider Native API</span><br><span class="line">Codex / Qoder / Hermes</span><br></pre></td></tr></table></figure><p>一句话区分：<strong>Provider 是被接入的对象，Adapter 是负责接入它的代码。</strong></p><h2 id="为什么不能在-UI-里直接判断-Provider"><a href="#为什么不能在-UI-里直接判断-Provider" class="headerlink" title="为什么不能在 UI 里直接判断 Provider"></a>为什么不能在 UI 里直接判断 Provider</h2><p>Codex 和 Qoder 都支持会话、流式输出和中断，但原生语义并不相同。</p><p>Codex App Server 可能使用：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">thread/start</span><br><span class="line">turn/start</span><br><span class="line">turn/interrupt</span><br><span class="line">item/agentMessage/delta</span><br></pre></td></tr></table></figure><p>Qoder Agent SDK 则可能使用：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">query()</span><br><span class="line">options.sessionId</span><br><span class="line">options.resume</span><br><span class="line">query.interrupt()</span><br><span class="line">stream_event</span><br></pre></td></tr></table></figure><p>最直接的实现是在业务层写分支：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (provider === <span class="string">&#x27;codex&#x27;</span>) &#123;</span><br><span class="line">  <span class="keyword">await</span> codex.<span class="title function_">startTurn</span>(...);</span><br><span class="line">&#125; <span class="keyword">else</span> <span class="keyword">if</span> (provider === <span class="string">&#x27;qoder&#x27;</span>) &#123;</span><br><span class="line">  <span class="keyword">await</span> qoder.<span class="title function_">query</span>(...);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这个写法在第二个 Provider 时成本很低，但它会迅速扩散到 UI、Session、Workflow、Approval、错误处理和测试。最终每个功能都要知道所有 Provider 的私有细节。</p><p>更稳妥的做法是先定义 Provider-neutral Contract：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> <span class="title class_">ProviderId</span> = <span class="built_in">string</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">interface</span> <span class="title class_">ProviderAdapter</span> &#123;</span><br><span class="line">  <span class="keyword">readonly</span> <span class="attr">id</span>: <span class="title class_">ProviderId</span>;</span><br><span class="line"></span><br><span class="line">  <span class="title function_">probe</span>(): <span class="title class_">Promise</span>&lt;<span class="title class_">ProviderDescriptor</span>&gt;;</span><br><span class="line">  <span class="title function_">createSession</span>(<span class="attr">input</span>: <span class="title class_">CreateSessionInput</span>): <span class="title class_">Promise</span>&lt;<span class="built_in">string</span>&gt;;</span><br><span class="line">  <span class="title function_">runTurn</span>(</span><br><span class="line">    <span class="attr">input</span>: <span class="title class_">RunTurnInput</span>,</span><br><span class="line">    <span class="attr">emit</span>: <span class="function">(<span class="params">event: AgentEvent</span>) =&gt;</span> <span class="built_in">void</span>,</span><br><span class="line">  ): <span class="title class_">Promise</span>&lt;<span class="built_in">void</span>&gt;;</span><br><span class="line">  <span class="title function_">interrupt</span>(<span class="attr">providerSessionId</span>: <span class="built_in">string</span>): <span class="title class_">Promise</span>&lt;<span class="built_in">void</span>&gt;;</span><br><span class="line">  <span class="title function_">close</span>(<span class="attr">providerSessionId</span>: <span class="built_in">string</span>): <span class="title class_">Promise</span>&lt;<span class="built_in">void</span>&gt;;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>业务层只处理 <code>AgentEvent</code>：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> <span class="title class_">AgentEvent</span> =</span><br><span class="line">  | &#123; <span class="attr">type</span>: <span class="string">&#x27;turn.started&#x27;</span>; <span class="attr">turnId</span>: <span class="built_in">string</span> &#125;</span><br><span class="line">  | &#123; <span class="attr">type</span>: <span class="string">&#x27;message.delta&#x27;</span>; <span class="attr">text</span>: <span class="built_in">string</span> &#125;</span><br><span class="line">  | &#123; <span class="attr">type</span>: <span class="string">&#x27;tool.started&#x27;</span>; <span class="attr">tool</span>: <span class="built_in">string</span> &#125;</span><br><span class="line">  | &#123; <span class="attr">type</span>: <span class="string">&#x27;tool.completed&#x27;</span>; <span class="attr">tool</span>: <span class="built_in">string</span>; <span class="attr">status</span>: <span class="built_in">string</span> &#125;</span><br><span class="line">  | &#123; <span class="attr">type</span>: <span class="string">&#x27;turn.completed&#x27;</span> &#125;</span><br><span class="line">  | &#123; <span class="attr">type</span>: <span class="string">&#x27;session.failed&#x27;</span>; <span class="attr">message</span>: <span class="built_in">string</span> &#125;;</span><br></pre></td></tr></table></figure><p>Codex 的 <code>item/agentMessage/delta</code> 和 Qoder 的 <code>stream_event</code> 都在 Adapter 内转换成 <code>message.delta</code>。上层不需要知道事件来自哪个 SDK。</p><h2 id="Provider-Pattern-实际组合了哪些模式"><a href="#Provider-Pattern-实际组合了哪些模式" class="headerlink" title="Provider Pattern 实际组合了哪些模式"></a>Provider Pattern 实际组合了哪些模式</h2><p>我把它叫作 Provider Pattern，但严格来说，它是一组模式的组合。</p><table><thead><tr><th>模式</th><th>解决的问题</th></tr></thead><tbody><tr><td>Adapter Pattern</td><td>把不同 Provider 的接口和事件翻译成统一语义</td></tr><tr><td>Strategy Pattern</td><td>在运行时选择 Codex、Qoder 或 Hermes 执行任务</td></tr><tr><td>Registry Pattern</td><td>管理 <code>providerId</code> 与 Adapter 实例的映射</td></tr><tr><td>Ports and Adapters</td><td>让 Agent Ops 领域层不依赖外部 SDK</td></tr><tr><td>Anti-Corruption Layer</td><td>阻止 Provider 私有概念污染业务模型</td></tr></tbody></table><p>Registry 的实现可以很小：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">ProviderRegistry</span> &#123;</span><br><span class="line">  #adapters = <span class="keyword">new</span> <span class="title class_">Map</span>&lt;<span class="built_in">string</span>, <span class="title class_">ProviderAdapter</span>&gt;();</span><br><span class="line"></span><br><span class="line">  <span class="title function_">constructor</span>(<span class="params">adapters: ProviderAdapter[]</span>) &#123;</span><br><span class="line">    <span class="keyword">for</span> (<span class="keyword">const</span> adapter <span class="keyword">of</span> adapters) &#123;</span><br><span class="line">      <span class="keyword">if</span> (<span class="variable language_">this</span>.#adapters.<span class="title function_">has</span>(adapter.<span class="property">id</span>)) &#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">`Duplicate provider: <span class="subst">$&#123;adapter.id&#125;</span>`</span>);</span><br><span class="line">      &#125;</span><br><span class="line">      <span class="variable language_">this</span>.#adapters.<span class="title function_">set</span>(adapter.<span class="property">id</span>, adapter);</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="title function_">get</span>(<span class="attr">providerId</span>: <span class="built_in">string</span>): <span class="title class_">ProviderAdapter</span> &#123;</span><br><span class="line">    <span class="keyword">const</span> adapter = <span class="variable language_">this</span>.#adapters.<span class="title function_">get</span>(providerId);</span><br><span class="line">    <span class="keyword">if</span> (!adapter) <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">Error</span>(<span class="string">`Unsupported provider: <span class="subst">$&#123;providerId&#125;</span>`</span>);</span><br><span class="line">    <span class="keyword">return</span> adapter;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这里我刻意把 <code>ProviderId</code> 定义成 <code>string</code>，而不是：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> <span class="title class_">ProviderId</span> = <span class="string">&#x27;codex&#x27;</span> | <span class="string">&#x27;qoder&#x27;</span>;</span><br></pre></td></tr></table></figure><p>封闭 union 在小型应用里类型体验很好，但对可扩展 Provider 系统意味着每接入一个 Provider 都要修改公共契约。真正的约束应该来自 Registry、配置校验和兼容矩阵，而不是把已知 Provider 列表写死在领域类型中。</p><h2 id="Session-必须由-Agent-Ops-App-持有"><a href="#Session-必须由-Agent-Ops-App-持有" class="headerlink" title="Session 必须由 Agent Ops App 持有"></a>Session 必须由 Agent Ops App 持有</h2><p>这是多 Provider 架构里最重要的所有权边界。</p><p>Codex 有 Thread ID，Qoder 有 Session ID，Hermes 未来也会有自己的会话标识。但这些 ID 都不能直接成为 Agent Ops 的业务主键。</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">应用 Session ID</span><br><span class="line">    ├── providerId: qoder</span><br><span class="line">    ├── providerVersion: 1.0.47</span><br><span class="line">    ├── adapterVersion: 0.1.0</span><br><span class="line">    ├── workspaceRevisionId</span><br><span class="line">    ├── permissionProfileId</span><br><span class="line">    └── providerSessionId: &lt;Qoder native ID&gt;</span><br></pre></td></tr></table></figure><p>原因很直接：</p><ul><li>Provider Session ID 的格式和生命周期不受 App 控制。</li><li>Provider 可能删除、迁移或无法恢复原生 Session。</li><li>Workflow、审批和审计需要跨 Provider 保持统一身份。</li><li>用户切换 Provider 时，业务记录不能跟着换主键。</li></ul><p>因此 Agent Ops Session 是权威记录，Provider Session 只是外部引用。</p><p>创建 Session 时还应该固定 Provider 版本、Workspace Revision、权限配置和资产投影版本。否则同一个 Session 运行到一半，系统 CLI 自动升级，前后两次 Turn 可能已经不再遵守相同协议。</p><h2 id="Desktop、CLI、SDK-和-Adapter-要分层"><a href="#Desktop、CLI、SDK-和-Adapter-要分层" class="headerlink" title="Desktop、CLI、SDK 和 Adapter 要分层"></a>Desktop、CLI、SDK 和 Adapter 要分层</h2><p>Codex 和 Qoder 都可能同时提供 Desktop、CLI 和 SDK，这几个东西不能混为一谈。</p><table><thead><tr><th>层级</th><th>作用</th><th>是否进入 Agent Runtime 主链路</th></tr></thead><tbody><tr><td>Desktop</td><td>用户直接操作的产品界面</td><td>否，适合 Open&#x2F;Handoff</td></tr><tr><td>CLI</td><td>可执行 Runtime、登录和诊断入口</td><td>是</td></tr><tr><td>SDK</td><td>管理 CLI、协议和结构化事件</td><td>优先使用</td></tr><tr><td>Adapter</td><td>Agent Ops 内部的转换边界</td><td>是</td></tr></tbody></table><p>桌面端不适合成为 Agent Ops 的执行依赖：它的生命周期由用户控制，可能自动升级，窗口里的会话也不等于 Agent Ops Session。</p><p>真正的调用链应该是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">Agent Ops App</span><br><span class="line">  → Provider Adapter</span><br><span class="line">  → SDK / CLI structured protocol</span><br><span class="line">  → Provider Runtime</span><br></pre></td></tr></table></figure><p>Desktop 只负责用户交接：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">Open in Codex</span><br><span class="line">Open in Qoder</span><br><span class="line">Handoff current task</span><br></pre></td></tr></table></figure><h2 id="Runtime-Resolver：版本稳定性的关键一层"><a href="#Runtime-Resolver：版本稳定性的关键一层" class="headerlink" title="Runtime Resolver：版本稳定性的关键一层"></a>Runtime Resolver：版本稳定性的关键一层</h2><p>只写 <code>spawn(&#39;codex&#39;)</code> 或 <code>spawn(&#39;qodercli&#39;)</code> 很方便，但这会默认依赖用户 <code>PATH</code> 中的版本。系统 CLI 一旦自动升级，Adapter 可能在用户毫无感知的情况下失效。</p><p>我倾向于给每个 Provider 增加 Runtime Resolver：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> <span class="title class_">ResolvedProviderRuntime</span> = &#123;</span><br><span class="line">  <span class="attr">executablePath</span>: <span class="built_in">string</span>;</span><br><span class="line">  <span class="attr">source</span>: <span class="string">&#x27;override&#x27;</span> | <span class="string">&#x27;bundled&#x27;</span> | <span class="string">&#x27;system&#x27;</span>;</span><br><span class="line">  <span class="attr">version</span>: <span class="built_in">string</span>;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>解析顺序是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">显式环境变量覆盖</span><br><span class="line">  → App/SDK bundled runtime</span><br><span class="line">  → 系统 CLI 开发兜底</span><br></pre></td></tr></table></figure><p>以 Qoder 为例，SDK 可能锁定一个经过验证的 bundled CLI，系统新版本只用于登录和人工诊断。Codex 也应该采用相同思路，只是在真正随 App 分发 binary 前，还需要确认授权、签名、公证和登录态复用边界。</p><p>版本不能只在日志里打印一次，而要进入 Session Pin 和 Compatibility Manifest：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;provider&quot;</span><span class="punctuation">:</span> <span class="string">&quot;qoder&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;providerRuntimeVersion&quot;</span><span class="punctuation">:</span> <span class="string">&quot;1.0.47&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;providerAdapterVersion&quot;</span><span class="punctuation">:</span> <span class="string">&quot;0.1.0&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;sidecarProtocolVersion&quot;</span><span class="punctuation">:</span> <span class="number">1</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;capabilities&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span><span class="string">&quot;stream&quot;</span><span class="punctuation">,</span> <span class="string">&quot;interrupt&quot;</span><span class="punctuation">,</span> <span class="string">&quot;resume&quot;</span><span class="punctuation">]</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><h2 id="Capability-不要伪装成统一能力"><a href="#Capability-不要伪装成统一能力" class="headerlink" title="Capability 不要伪装成统一能力"></a>Capability 不要伪装成统一能力</h2><p>统一接口不代表所有 Provider 的能力完全相同。</p><p>有些 Provider 支持 resume，有些只支持重建上下文；有些能 fork Session，有些能提供结构化 Tool Approval；有些只有文本流，没有稳定的 Tool Event。</p><p>错误做法是用文本解析或业务分支伪造能力。正确做法是让 <code>probe()</code> 返回 Capability：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> <span class="title class_">ProviderDescriptor</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">version</span>: <span class="built_in">string</span>;</span><br><span class="line">  <span class="attr">status</span>: <span class="string">&#x27;ready&#x27;</span> | <span class="string">&#x27;unavailable&#x27;</span>;</span><br><span class="line">  <span class="attr">capabilities</span>: <span class="title class_">Array</span>&lt;</span><br><span class="line">    <span class="string">&#x27;stream&#x27;</span> | <span class="string">&#x27;interrupt&#x27;</span> | <span class="string">&#x27;resume&#x27;</span> | <span class="string">&#x27;fork&#x27;</span> | <span class="string">&#x27;toolApproval&#x27;</span></span><br><span class="line">  &gt;;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>上层根据数据决定是否展示“恢复”“Fork”或“审批”按钮，而不是判断 Provider 名称：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (provider.<span class="property">capabilities</span>.<span class="title function_">includes</span>(<span class="string">&#x27;resume&#x27;</span>)) &#123;</span><br><span class="line">  <span class="title function_">showResumeAction</span>();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这能让未来 Hermes 接入时只声明真实能力，不必模仿 Codex 或 Qoder。</p><h2 id="权限不能交给-Provider-自己决定"><a href="#权限不能交给-Provider-自己决定" class="headerlink" title="权限不能交给 Provider 自己决定"></a>权限不能交给 Provider 自己决定</h2><p>Provider Adapter 解决的是兼容性，不自动解决安全问题。</p><p>即使 Codex、Qoder 都有自己的 Permission Mode，Agent Ops App 仍然必须持有上层权限策略。Provider 原生审批只能进一步收紧，不能放宽组织策略已经拒绝的动作。</p><p>我当前采用的 MVP 边界是：</p><ol><li>原生 Core 验证 Workspace Grant。</li><li>Core 生成有界的只读 Workspace Context。</li><li>Adapter 禁用 Provider 原生文件、Shell、网络和 MCP 副作用工具。</li><li>Provider 只负责推理与流式输出。</li><li>未来开放 Tool 时，必须先转换成 Canonical Tool Request，再由 Core 执行。</li></ol><p>这能避免一个常见误区：UI 看起来使用了 read-only，但 Provider Runtime 实际继承了完整用户目录和 Shell 权限。</p><h2 id="新增-Hermes-时应该改多少代码"><a href="#新增-Hermes-时应该改多少代码" class="headerlink" title="新增 Hermes 时应该改多少代码"></a>新增 Hermes 时应该改多少代码</h2><p>一个健康的 Provider 架构应该用“新增代码”扩展，而不是到处修改旧代码。</p><p>理想的 Hermes 接入包括：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">hermes-adapter.ts</span><br><span class="line">hermes-runtime-resolver.ts</span><br><span class="line">hermes-materializer.ts</span><br><span class="line">hermes-contract-fixtures/</span><br></pre></td></tr></table></figure><p>然后注册：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">new</span> <span class="title class_">ProviderRegistry</span>([</span><br><span class="line">  <span class="keyword">new</span> <span class="title class_">CodexAdapter</span>(),</span><br><span class="line">  <span class="keyword">new</span> <span class="title class_">QoderAdapter</span>(),</span><br><span class="line">  <span class="keyword">new</span> <span class="title class_">HermesAdapter</span>(),</span><br><span class="line">]);</span><br></pre></td></tr></table></figure><p>不应该修改：</p><ul><li>Agent Chat 的事件渲染逻辑</li><li>Workflow Run 状态机</li><li>Approval 数据模型</li><li>Audit Event 格式</li><li>Asset 的事实源模型</li></ul><p>如果新增 Hermes 还需要修改以上模块，说明 Provider 私有语义已经穿透 Adapter 边界。</p><h2 id="最容易踩的五个坑"><a href="#最容易踩的五个坑" class="headerlink" title="最容易踩的五个坑"></a>最容易踩的五个坑</h2><h3 id="1-把-Provider-和-Model-混为一谈"><a href="#1-把-Provider-和-Model-混为一谈" class="headerlink" title="1. 把 Provider 和 Model 混为一谈"></a>1. 把 Provider 和 Model 混为一谈</h3><p>Provider 是执行系统，Model 只是其中一个配置。Qoder 可能动态选择多个 Model，Codex 也可能切换推理配置。Session 应固定 Provider，Model 是否固定则由产品策略决定。</p><h3 id="2-一个-Adapter-只保存一个全局-Session"><a href="#2-一个-Adapter-只保存一个全局-Session" class="headerlink" title="2. 一个 Adapter 只保存一个全局 Session"></a>2. 一个 Adapter 只保存一个全局 Session</h3><p>类似 <code>#threadId</code> 的单例字段只能支持一个会话。生产实现应按 Provider Session ID 管理进程、事件监听器、Pending Request 和取消状态。</p><h3 id="3-UI-出现-Provider-私有事件"><a href="#3-UI-出现-Provider-私有事件" class="headerlink" title="3. UI 出现 Provider 私有事件"></a>3. UI 出现 Provider 私有事件</h3><p>一旦 UI 直接处理 <code>item/agentMessage/delta</code>，Adapter 就已经失去意义。Provider 原生类型应该止步于 Adapter 文件。</p><h3 id="4-依赖系统-CLI-自动升级"><a href="#4-依赖系统-CLI-自动升级" class="headerlink" title="4. 依赖系统 CLI 自动升级"></a>4. 依赖系统 CLI 自动升级</h3><p>开发机上很方便，生产环境中却是协议漂移来源。至少要做 Runtime Resolver、版本 Probe、兼容范围校验和 Session Pin。</p><h3 id="5-为了“统一”而虚构能力"><a href="#5-为了“统一”而虚构能力" class="headerlink" title="5. 为了“统一”而虚构能力"></a>5. 为了“统一”而虚构能力</h3><p>不支持 resume 就明确返回不支持，不要偷偷重放 Prompt 并告诉上层“恢复成功”。能力差异应该由数据表达，不能靠语义造假。</p><h2 id="要不要用-Provider-Pattern：我的判断框架"><a href="#要不要用-Provider-Pattern：我的判断框架" class="headerlink" title="要不要用 Provider Pattern：我的判断框架"></a>要不要用 Provider Pattern：我的判断框架</h2><p>如果应用只调用一个稳定 API，且没有切换执行引擎的计划，直接封装一个 Client 往往足够，不必为了模式而模式。</p><p>但满足下面任意三项时，我会尽早建立 Provider 边界：</p><ul><li>已经有两个以上执行引擎。</li><li>Provider 拥有不同的 Session 和事件协议。</li><li>SDK&#x2F;CLI 更新频繁，需要独立兼容测试。</li><li>UI、Workflow、审批和审计必须保持统一。</li><li>需要运行时选择 Provider。</li><li>未来可能接入企业内部或自托管 Agent Runtime。</li></ul><p>对 Agent Ops App 来说，我的结论是“值得上”。因为它的核心价值不是把某个 Agent 包一层 UI，而是拥有跨 Provider 的控制面：资产、权限、Session、Workflow、Evidence 和审计。</p><p>Provider 可以替换，Adapter 可以升级，CLI 可以演进；但 Agent Ops App 的领域模型和治理边界必须保持稳定。做到这一点以后，Codex、Qoder、Hermes 才真正只是可替换的执行引擎，而不是反过来绑架整个产品架构。</p>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/ff9d44d48468/</id>
    <link href="https://blog.zisheng.pro/posts/ff9d44d48468/"/>
    <published>2026-07-21T06:30:00.000Z</published>
    <summary>当 Agent Ops App 同时接入 Codex、Qoder，未来还要支持 Hermes 时，真正的难点不是多写几个 SDK 调用，而是阻止 Provider 私有协议渗透业务层。本文结合桌面 Agent Host 实践，拆解 Provider、Adapter、Registry、Capability、Session Pin 与 Runtime Resolver 的职责，并给出一套可持续扩展的实现框架。</summary>
    <title>Provider Pattern：在 Agent Ops App 中统一 Codex、Qoder 与未来执行引擎</title>
    <updated>2026-09-18T08:15:28.629Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="AI" scheme="https://blog.zisheng.pro/categories/AI/"/>
    <category term="Node.js" scheme="https://blog.zisheng.pro/tags/Node-js/"/>
    <category term="AI Infra" scheme="https://blog.zisheng.pro/tags/AI-Infra/"/>
    <category term="Agent" scheme="https://blog.zisheng.pro/tags/Agent/"/>
    <category term="Architecture" scheme="https://blog.zisheng.pro/tags/Architecture/"/>
    <category term="Sidecar" scheme="https://blog.zisheng.pro/tags/Sidecar/"/>
    <category term="Tauri" scheme="https://blog.zisheng.pro/tags/Tauri/"/>
    <category term="原生运行时" scheme="https://blog.zisheng.pro/tags/%E5%8E%9F%E7%94%9F%E8%BF%90%E8%A1%8C%E6%97%B6/"/>
    <content>
      <![CDATA[<p>最近在设计一个本地 Agent Host 时，我遇到了一个很典型的架构冲突：桌面应用希望用 Tauri 的原生层建立稳定的本地可信边界，但 Codex、Qoder、MCP 等 Agent 能力又高度依赖 Node.js 与 TypeScript 生态。</p><p>把所有能力塞进一个进程当然最快，但代价也很直接：UI、数据库、凭据、文件系统和 Agent Runtime 混成一团，一个 Provider SDK 崩溃或越权，就可能拖垮整个应用。</p><p>我最后采用的是 Sidecar，也就是边车模式。</p><h2 id="一句话总结"><a href="#一句话总结" class="headerlink" title="一句话总结"></a>一句话总结</h2><p><strong>Sidecar 是由主应用启动和监管、通过明确协议协作的独立辅助进程。它适合隔离异构运行时和高风险能力，但进程隔离不等于安全沙箱，权限仍然必须由主应用强制执行。</strong></p><p><img src="/images/sidecar-pattern-overview.svg" alt="Sidecar 边车模式总体结构"></p><h2 id="Sidecar-到底是什么"><a href="#Sidecar-到底是什么" class="headerlink" title="Sidecar 到底是什么"></a>Sidecar 到底是什么</h2><p>这个名字来自摩托车边车：主车决定方向，边车跟随主车一起运行，但拥有独立空间。</p><p>放到软件架构里，主应用负责生命周期、权限和核心状态；Sidecar 负责一组边界清晰的辅助能力。实现上它通常就是子进程、独立容器或本地服务，但“子进程”是操作系统概念，“Sidecar”描述的是架构角色。</p><table><thead><tr><th>概念</th><th>关注点</th></tr></thead><tbody><tr><td>Child Process</td><td>谁创建进程、PID、stdin&#x2F;stdout、退出码</td></tr><tr><td>Sidecar</td><td>为什么拆分、职责边界、协议、权限和故障治理</td></tr><tr><td>Microservice</td><td>独立部署、网络边界、团队和服务自治</td></tr><tr><td>Plugin</td><td>可扩展能力，可能仍与宿主运行在同一进程</td></tr></tbody></table><p>一个进程只有在具备“随主应用交付、由主应用监管、职责受限、通过协议协作”这些特征时，才更接近 Sidecar，而不只是随手启动的脚本。</p><h2 id="为什么本地-Agent-特别适合边车模式"><a href="#为什么本地-Agent-特别适合边车模式" class="headerlink" title="为什么本地 Agent 特别适合边车模式"></a>为什么本地 Agent 特别适合边车模式</h2><p>本地 Agent 天然跨越多个技术栈：桌面壳需要窗口、更新、Keychain 和系统权限；Agent Runtime 需要 Provider SDK、MCP、工具调用和流式事件；数据层又需要 SQLite、索引与事务一致性。</p><p>如果全部交给 Node 主进程，接入生态很舒服，但 Node 同时成了窗口、数据库、凭据和执行权限的可信根。反过来，如果全部重写为原生实现，首版成本高，Provider SDK 更新也要长期追赶。</p><p>边车提供了第三条路：原生 Core 保留控制权，Node Agent Host 只承载 Node 生态中不可替代的部分。</p><table><thead><tr><th>组件</th><th>主要职责</th><th>不应该拥有的能力</th></tr></thead><tbody><tr><td>React Webview</td><td>UI、交互、无特权状态</td><td>原始 Secret、任意 Shell、数据库连接</td></tr><tr><td>原生 Core</td><td>权限、生命周期、更新、审计、IPC 校验</td><td>Provider 业务实现细节</td></tr><tr><td>原生数据服务</td><td>SQLite、同步、索引、事务</td><td>UI 和 Provider Token</td></tr><tr><td>Node Sidecar</td><td>Codex&#x2F;Qoder SDK、ACP、MCP Adapter</td><td>最终授权决策、主数据库所有权</td></tr></tbody></table><h2 id="一次-Agent-请求怎样穿过边界"><a href="#一次-Agent-请求怎样穿过边界" class="headerlink" title="一次 Agent 请求怎样穿过边界"></a>一次 Agent 请求怎样穿过边界</h2><p>Sidecar 设计真正难的不是“把 Node 启起来”，而是定义每一步谁有权做什么。</p><p><img src="/images/sidecar-request-flow.svg" alt="Sidecar 请求与事件流"></p><p>典型链路是：</p><ol><li>Webview 提交声明式任务，例如“在当前 workspace 分析这组文件”。</li><li>原生 Core 校验调用来源、workspace scope、参数长度、组织策略和用户审批。</li><li>Core 给任务生成受限 capability profile，再通过版本化 IPC 发给 Sidecar。</li><li>Sidecar 调用 Provider SDK，并把 token、tool call、progress 和 error 作为有序事件流返回。</li><li>Core 对事件做审计和脱敏，再通过 Tauri Channel 推给 UI。</li></ol><p>低频控制请求可以用 NDJSON framed protocol；高频流也可以使用本地 Socket。关键不是选哪一种传输，而是必须有 <code>protocolVersion</code>、<code>requestId</code>、事件序号、取消语义、超时和背压。</p><p>不要把协议消息和普通 stdout 日志混在一起。否则一条调试日志就可能破坏 framing，让控制面变得不可预测。</p><h2 id="进程隔离不等于安全沙箱"><a href="#进程隔离不等于安全沙箱" class="headerlink" title="进程隔离不等于安全沙箱"></a>进程隔离不等于安全沙箱</h2><p>这是 Sidecar 最容易被误解的地方。</p><p>把 Node 放进独立进程，只解决了部分故障隔离：Sidecar 崩溃后可以单独拉起，内存泄漏不会直接污染原生 Core。但如果它继承了完整环境变量、用户目录和网络权限，它仍然能够读取不该读取的文件，或者绕过审批直接访问外部服务。</p><p><img src="/images/sidecar-trust-boundary.svg" alt="Sidecar 的可信边界与防线"></p><p>因此我会把原生 Core 设计为 Permission Broker：</p><ul><li>Frontend 不能直接启动 Sidecar，只能提交声明式 Intent。</li><li>Sidecar 只接收完成当前任务所需的最小输入，不接收完整配置和全部 Secret。</li><li>文件、进程、网络和命令权限按 workspace 与任务 scope 收紧。</li><li>启动时校验 binary hash、签名、协议版本和 capability profile。</li><li>所有高风险工具调用经过 Core 审批与审计，Sidecar 不能自己给自己授权。</li></ul><p>Tauri capability 主要约束 <code>Webview → 原生 Core</code>，不会自动沙箱已经启动的原生插件或 Node Sidecar。把平台 ACL 当成完整安全方案，是一个危险的边界误判。</p><h2 id="生命周期治理比启动命令重要"><a href="#生命周期治理比启动命令重要" class="headerlink" title="生命周期治理比启动命令重要"></a>生命周期治理比启动命令重要</h2><p>生产级 Sidecar 至少需要处理下面这些状态：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">starting → handshaking → ready → busy → draining → stopped</span><br><span class="line">                         ↘ degraded → restarting</span><br></pre></td></tr></table></figure><p>主应用需要回答：握手失败怎么办？任务执行中 Sidecar 崩溃是否重试？取消后怎样确保子进程和工具进程都被清理？升级时协议不兼容如何阻止启动？连续崩溃是否进入熔断？</p><p>我的判断是：Sidecar 可以独立重启，但主数据库事务和资产 revision 不能由它持有。否则所谓故障隔离只是把崩溃位置挪了一个进程，数据一致性仍被绑在 Agent Runtime 上。</p><h2 id="什么时候值得用，什么时候不要用"><a href="#什么时候值得用，什么时候不要用" class="headerlink" title="什么时候值得用，什么时候不要用"></a>什么时候值得用，什么时候不要用</h2><p>Sidecar 值得用：</p><ul><li>必须复用另一种语言或运行时的成熟 SDK。</li><li>任务容易崩溃、泄漏内存或需要单独限流。</li><li>能力涉及 Shell、网络、第三方插件等较高风险边界。</li><li>希望 Agent Runtime 可单独重启，而不影响 UI 和核心数据。</li></ul><p>不值得用：</p><ul><li>只是几个纯函数或轻量业务逻辑。</li><li>无法定义稳定协议，双方需要共享大量可变内存状态。</li><li>团队没有能力维护打包、签名、IPC、可观测性和崩溃恢复。</li><li>拆出去以后仍给 Sidecar 全部文件、网络、凭据和数据库权限。</li></ul><p>最后一种尤其常见：付出了多进程复杂度，却没有获得真实边界。</p><h2 id="我的判断框架"><a href="#我的判断框架" class="headerlink" title="我的判断框架"></a>我的判断框架</h2><p>我不会因为“Sidecar 是一种成熟模式”就默认采用它。我的判断顺序是：</p><ol><li>是否存在必须保留的异构运行时或高风险能力？</li><li>能否用小而稳定的协议描述输入、输出和事件？</li><li>宿主能否真正掌握权限、生命周期和核心数据？</li><li>故障隔离收益是否大于打包、签名和调试成本？</li></ol><p>四个问题都能给出明确答案，Sidecar 才是架构；否则它只是多启动了一个进程。</p><p>对本地 Agent 来说，我倾向于采用“可信 Core + 受控 Sidecar”：Core 决定谁能做什么，Sidecar 负责把某一种运行时的能力做到最好。边车可以执行任务，但不能决定规则。</p>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/a96ec6569ac0/</id>
    <link href="https://blog.zisheng.pro/posts/a96ec6569ac0/"/>
    <published>2026-07-18T10:30:00.000Z</published>
    <summary>Sidecar 不只是 Kubernetes 里的辅助容器，也适合解决桌面 Agent 的多运行时、权限和故障隔离问题。本文从 Tauri、原生 Core 与 Node Agent Host 的真实架构取舍出发，解释边车模式是什么、什么时候值得用、IPC 和权限边界怎样设计，以及它为什么不等于安全沙箱。</summary>
    <title>Sidecar 边车模式：给本地 Agent 划出一间受控执行舱</title>
    <updated>2026-09-18T08:15:28.632Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="AI" scheme="https://blog.zisheng.pro/categories/AI/"/>
    <category term="AI Infra" scheme="https://blog.zisheng.pro/tags/AI-Infra/"/>
    <category term="better-imagegen" scheme="https://blog.zisheng.pro/tags/better-imagegen/"/>
    <category term="GPT Image 2" scheme="https://blog.zisheng.pro/tags/GPT-Image-2/"/>
    <category term="Image Generation" scheme="https://blog.zisheng.pro/tags/Image-Generation/"/>
    <category term="Agent Skill" scheme="https://blog.zisheng.pro/tags/Agent-Skill/"/>
    <category term="Sprite Animation" scheme="https://blog.zisheng.pro/tags/Sprite-Animation/"/>
    <content>
      <![CDATA[<p>最近两周，我密集地用 AI 生成了几类差异很大的视觉资产：产品竞赛主视觉、Agent 角色分身、18px 菜单栏帧动画、macOS App Icon、动态壁纸、错误状态插画，以及博客和社交媒体封面。</p><p>一开始我以为问题只是“怎么写出更好的 Prompt”。做得越多，我越确定：<strong>Prompt 只是视觉交付链路里的一小段</strong>。真正费判断的是选什么工作流、怎样约束输出、如何处理透明通道、怎么保证角色一致、如何把一张图交付成前端和客户端能直接使用的资产。</p><p>这也是我持续完善 <a href="https://github.com/zisheng-ai/better-imagegen">better-imagegen</a> 的原因。它不是收藏 Prompt 的目录，而是一个面向 Agent 的视觉生成 skill：输入目标和交付类型，输出可以进入工程的图片、帧序列、元数据和平台产物。</p><h2 id="一句话总结"><a href="#一句话总结" class="headerlink" title="一句话总结"></a>一句话总结</h2><p><strong>AI 生图要从“抽卡”走向工程化，关键不是把 Prompt 写得更长，而是把类型路由、规格约束、模型回退、后处理、质量检查和产物封装变成稳定流程。</strong></p><p><img src="/images/better-imagegen-capability-map.webp" alt="better-imagegen 能力概览"></p><h2 id="我最近到底拿它做了什么"><a href="#我最近到底拿它做了什么" class="headerlink" title="我最近到底拿它做了什么"></a>我最近到底拿它做了什么</h2><p>我没有只扫描某一次会话，而是回看了近期 Codex 会话、项目资产和 better-imagegen 自身的 Git 演进。真实需求大致分成这些类型：</p><table><thead><tr><th>场景</th><th>难点</th><th>最终沉淀的能力</th></tr></thead><tbody><tr><td>博客与社交媒体封面</td><td>需要快速建立视觉主题，并为标题预留稳定空间</td><td><code>portrait / cover</code> 构图规范、留白与裁切约束</td></tr><tr><td>产品竞赛主视觉</td><td>大量准确中文、角色和产品界面同屏</td><td>背景生图 + HTML&#x2F;CSS 精确排字</td></tr><tr><td>Agent 角色与分身</td><td>已有角色换皮、动作变化，但必须保留身份特征</td><td>image edit、reference image、角色一致性</td></tr><tr><td>Argos 菜单栏动画</td><td>18px 仍需可识别，透明背景，循环不能跳帧</td><td>sprite sheet、拆帧、manifest、真实尺寸验收</td></tr><tr><td>macOS App Icon</td><td>透明源图不等于可交付 <code>.icns</code></td><td>alpha 检查、留白、iconset、<code>.icns</code> 打包</td></tr><tr><td>动态壁纸</td><td>不是两张图轮播，而是系统可识别的动态资产</td><td>4K 帧、HEIC、<code>apple_desktop:apr</code></td></tr><tr><td>产品错误态</td><td>角色要表达故障，但不能像通用红色警告图标</td><td>场景化角色物料、站点色彩适配</td></tr></tbody></table><p>这些案例让我看到：同样叫“生成一张图”，背后的交付合同完全不同。</p><h2 id="第一条经验：先判断产物类型，再选择模型"><a href="#第一条经验：先判断产物类型，再选择模型" class="headerlink" title="第一条经验：先判断产物类型，再选择模型"></a>第一条经验：先判断产物类型，再选择模型</h2><p>很多生图流程第一步就是选模型。我现在反过来：<strong>先问最终文件要被谁消费</strong>。</p><p>如果是博客封面，我需要 16:9、WebP、合适的压缩质量和可放标题的负空间；如果是 macOS 菜单栏图标，我关心的是透明通道、18px 下的轮廓和帧间稳定性；如果是动态壁纸，最终消费者是 macOS，不是浏览器，因此交付物应该是符合系统要求的 HEIC，而不是两张漂亮的 PNG。</p><p>目前 better-imagegen 把常见任务拆成独立工作流：</p><table><thead><tr><th>类型</th><th>默认交付重点</th></tr></thead><tbody><tr><td>Portrait &#x2F; Cover</td><td>构图、人物、情绪、WebP 压缩</td></tr><tr><td>Text Poster</td><td>生图负责视觉底板，HTML&#x2F;CSS 负责准确文字</td></tr><tr><td>Logo &#x2F; Icon</td><td>透明背景、alpha 边缘、缩小后的可读性</td></tr><tr><td>macOS App Icon</td><td>squircle、安全区、iconset、<code>.icns</code></td></tr><tr><td>Static Wallpaper</td><td>4K、PNG、桌面图标可读区域</td></tr><tr><td>Dynamic Wallpaper</td><td>多状态帧、HEIC、系统元数据</td></tr><tr><td>Sprite Loop</td><td>精灵图、编号帧、GIF 预览、manifest</td></tr><tr><td>Image Edit</td><td>参考图输入、身份保持、局部变化</td></tr></tbody></table><p>这样做的好处是：Agent 不再把所有需求都塞进同一个“万能生图 Prompt”。</p><h2 id="第二条经验：Prompt-应该是一份-Asset-Contract"><a href="#第二条经验：Prompt-应该是一份-Asset-Contract" class="headerlink" title="第二条经验：Prompt 应该是一份 Asset Contract"></a>第二条经验：Prompt 应该是一份 Asset Contract</h2><p>一个可执行的 Prompt，不是堆“高级感、电影感、8K、杰作”这类形容词，而是写清一份资产合同。</p><p>我现在通常至少写这 6 类信息：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">1. Subject：画面里必须有什么</span><br><span class="line">2. Composition：主体位置、留白、视线方向、帧网格</span><br><span class="line">3. Style：摄影、插画、像素、产品渲染等视觉语言</span><br><span class="line">4. Delivery context：博客、社交媒体、菜单栏、桌面、App Icon、头像</span><br><span class="line">5. Hard constraints：人数、透明背景、尺寸、帧数、角色身份</span><br><span class="line">6. Negative constraints：不要文字、不要 Logo、不要水印、不要额外肢体</span><br></pre></td></tr></table></figure><p>例如做 Argos 菜单栏猫头鹰动画时，“可爱猫头鹰”远远不够。真正决定可用性的约束是：横向精灵图、固定 6 帧、每格等宽、透明背景、暖橙色轮廓、18px 仍能辨识、首尾动作连续。</p><p>模型生成的是像素，但 Prompt 描述的是<strong>运行时约束</strong>。</p><h2 id="第三条经验：准确文字不要交给图像模型"><a href="#第三条经验：准确文字不要交给图像模型" class="headerlink" title="第三条经验：准确文字不要交给图像模型"></a>第三条经验：准确文字不要交给图像模型</h2><p>产品竞赛主视觉是一个典型转折点。画面里既要角色、空间感和产品 UI，又要大量准确中文。直接让模型把所有文字画出来，结果通常是：中文错字、字重漂移、对齐不可控，修改一个词还要整张图重抽。</p><p>我最后采用的流程是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">AI 生成无文字视觉底板</span><br><span class="line">        ↓</span><br><span class="line">HTML / CSS 叠加真实文本和品牌组件</span><br><span class="line">        ↓</span><br><span class="line">浏览器渲染并截图</span><br><span class="line">        ↓</span><br><span class="line">WebP 压缩 + 尺寸检查</span><br></pre></td></tr></table></figure><p>这个方法没有“纯 AI 生图”那么浪漫，但交付质量稳定得多。图像模型负责它擅长的氛围、材质和构图；浏览器负责它擅长的字体、排版和像素级控制。</p><p>这其实很符合我从前端转 Agent 的经验：<strong>不要让一个模型承担完整系统里所有职责，边界清晰比单点能力更重要。</strong></p><h2 id="第四条经验：角色一致性必须走-Image-Edit"><a href="#第四条经验：角色一致性必须走-Image-Edit" class="headerlink" title="第四条经验：角色一致性必须走 Image Edit"></a>第四条经验：角色一致性必须走 Image Edit</h2><p>在“小界”和“录制小界”的角色设计里，我需要同一角色拥有不同身份和动作：基础小界负责体验治理，录制小界负责根据目标自动探索操作路径并沉淀技能。</p><p>如果每次都从文本重新生成，脸型、发型、服装结构和整体年龄感都会漂。更可靠的方法是把现有角色图作为 reference，通过 image edit 只修改服装、姿态、表情或手持物。</p><p>我的判断标准很直接：</p><ul><li>用户必须认出“还是同一个角色”时，使用 image edit。</li><li>只需要同类风格、不要求身份连续时，才用 text-to-image。</li><li>角色进入帧动画时，先锁定标准立绘，再做动作变化。</li></ul><p><img src="/images/better-imagegen-sprite-loop.gif" alt="角色帧动画交付示例"></p><h2 id="第五条经验：帧动画的验收单位不是大图，而是运行尺寸"><a href="#第五条经验：帧动画的验收单位不是大图，而是运行尺寸" class="headerlink" title="第五条经验：帧动画的验收单位不是大图，而是运行尺寸"></a>第五条经验：帧动画的验收单位不是大图，而是运行尺寸</h2><p>我第一次给 Argos 做 RunCat 风格的菜单栏动画，生成的大图看起来不错，放进 macOS 菜单栏却几乎不可见。原因很简单：深色主体、细轮廓和轻微动作，缩到 18px 后全部丢失。</p><p>后来我把 sprite loop 的交付定义成了一组工程产物：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">sprite-sheet.png        # 原始等分精灵图</span><br><span class="line">frame-01.png ...        # 拆分后的独立帧</span><br><span class="line">preview.gif             # 快速预览循环</span><br><span class="line">manifest.json           # 帧数、尺寸、顺序、时长</span><br></pre></td></tr></table></figure><p>并增加了 4 个验收条件：</p><ol><li>每个 cell 尺寸完全一致，不能靠肉眼切。</li><li>首帧和末帧的姿态能自然衔接。</li><li>alpha 通道真实存在，不能用棋盘格“画透明”。</li><li>必须在实际运行尺寸验收，而不是只看 512px 预览。</li></ol><p>这条经验也适用于头像、favicon、状态图标：<strong>缩小后的信息密度，才是最终质量。</strong></p><h2 id="第六条经验：一张透明-PNG-还不是-macOS-App-Icon"><a href="#第六条经验：一张透明-PNG-还不是-macOS-App-Icon" class="headerlink" title="第六条经验：一张透明 PNG 还不是 macOS App Icon"></a>第六条经验：一张透明 PNG 还不是 macOS App Icon</h2><p>macOS App Icon 是另一个很容易被低估的场景。模型能生成一个透明图标，不代表它可以直接放进 Xcode。</p><p>稳定流程至少包括：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">生成透明源图</span><br><span class="line">  → 检查 alpha 边缘</span><br><span class="line">  → 裁掉异常透明留白</span><br><span class="line">  → 设置视觉安全区</span><br><span class="line">  → 生成 macOS squircle 底板</span><br><span class="line">  → 输出 16 / 32 / 128 / 256 / 512 / 1024 尺寸</span><br><span class="line">  → 组成 .iconset</span><br><span class="line">  → 转换为 .icns</span><br></pre></td></tr></table></figure><p>这里模型只完成了“源艺术”。后面的尺寸矩阵和系统封装，才决定它是不是一个可安装的 App Icon。</p><h2 id="第七条经验：模型回退不是换个名字重试"><a href="#第七条经验：模型回退不是换个名字重试" class="headerlink" title="第七条经验：模型回退不是换个名字重试"></a>第七条经验：模型回退不是换个名字重试</h2><p>better-imagegen 现在默认使用 GPT Image 2，失败后归一化 Prompt 重试一次，再回退到 Gemini，最后才考虑 Doubao。</p><p>但 fallback 不是无脑串行，因为不同模型的交付特性不同：</p><table><thead><tr><th>模型</th><th>我主要看重的能力</th><th>需要注意</th></tr></thead><tbody><tr><td>GPT Image 2</td><td>Prompt 遵循、真实感、复杂构图</td><td>偶发超时或拒绝，需要一次规范化重试</td></tr><tr><td>Gemini Image</td><td>自由尺寸、4K、无水印</td><td>适合作为大多数任务的第一回退</td></tr><tr><td>Doubao Seedream</td><td>普通视觉任务的兜底能力</td><td>水印和最小像素面积会影响透明图、精灵图</td></tr></tbody></table><p>因此透明 Logo、Icon 和 sprite sheet 不应该回退到一个会破坏 alpha 或网格的模型。<strong>Fallback 的目标不是“总能出图”，而是“仍然能交付同一种资产”。</strong></p><h2 id="第八条经验：每张图都应该可追溯"><a href="#第八条经验：每张图都应该可追溯" class="headerlink" title="第八条经验：每张图都应该可追溯"></a>第八条经验：每张图都应该可追溯</h2><p>最早生成完一张图，我只关心“保存在哪”。当使用频率变高后，这种方式很快失控：不知道用的哪个模型、不知道请求尺寸、分不清生成耗时和后处理、也无法复现一张突然很好的图。</p><p>所以我给每个产物增加了同名 JSON 元数据：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;file&quot;</span><span class="punctuation">:</span> <span class="string">&quot;cover.webp&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;gpt-image-2-all&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;requested_size&quot;</span><span class="punctuation">:</span> <span class="string">&quot;1536x1024&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;actual_resolution&quot;</span><span class="punctuation">:</span> <span class="string">&quot;1280x720&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;generation_time_seconds&quot;</span><span class="punctuation">:</span> <span class="number">38.8</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;output_format&quot;</span><span class="punctuation">:</span> <span class="string">&quot;webp&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;postprocess&quot;</span><span class="punctuation">:</span> <span class="string">&quot;center-cropped to 16:9; WebP quality 78&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;prompt&quot;</span><span class="punctuation">:</span> <span class="string">&quot;...&quot;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p>元数据看起来是小事，但它让视觉生成从“聊天记录里的偶然结果”变成了可审计的工程产物。</p><h2 id="我是怎样把一次成功沉淀成-skill-的"><a href="#我是怎样把一次成功沉淀成-skill-的" class="headerlink" title="我是怎样把一次成功沉淀成 skill 的"></a>我是怎样把一次成功沉淀成 skill 的</h2><p>最近有一次生成结果特别好，我第一反应不是只保存图片，而是追问：这次为什么好？哪些步骤可以复用？</p><p>我会把过程拆成 4 层：</p><ol><li><strong>类型层</strong>：这是封面、图标、壁纸还是动画？</li><li><strong>Prompt 层</strong>：哪些构图和负向约束真正起作用？</li><li><strong>后处理层</strong>：裁切、透明、压缩、拆帧、系统封装做了什么？</li><li><strong>验收层</strong>：在真实博客、菜单栏、Dock、网页里效果如何？</li></ol><p>然后把稳定部分写进对应 reference，而不是继续依赖我的临场记忆。better-imagegen 最近的演进也基本沿着这条路线：</p><table><thead><tr><th>时间</th><th>沉淀内容</th></tr></thead><tbody><tr><td>7 月 4 日</td><td>静态壁纸规格、无损 PNG</td></tr><tr><td>7 月 6 日</td><td>macOS 动态壁纸、HEIC 元数据、模型扩展</td></tr><tr><td>7 月 7 日</td><td>默认输出目录、元数据汇报、类型 reference 拆分、Prompt compliance</td></tr><tr><td>7 月 9 日</td><td>GPT → Gemini → Doubao 的回退链路、文字型竞赛封面流程</td></tr><tr><td>7 月 10 日</td><td>macOS <code>.iconset</code> &#x2F; <code>.icns</code> 工作流</td></tr></tbody></table><p>这就是我理解的 Agent skill：不是一段“万能指令”，而是把成功经验变成一套带条件分支、失败处理和交付标准的操作系统。</p><p><img src="/images/better-imagegen-argos-case.webp" alt="Argos 产品概念视觉案例"></p><h2 id="better-imagegen-的能力边界"><a href="#better-imagegen-的能力边界" class="headerlink" title="better-imagegen 的能力边界"></a>better-imagegen 的能力边界</h2><p>它目前很适合：</p><ul><li>快速生成产品概念视觉、封面、角色和状态物料。</li><li>让 Agent 自主选择图像工作流并完成后处理。</li><li>把 sprite、App Icon、壁纸等非单图资产交付到工程。</li><li>在多模型之间做有约束的 fallback。</li><li>给生成结果补齐规格和元数据。</li></ul><p>但它不是 Figma 的替代品，也不会自动解决所有品牌一致性问题。涉及精确排版、复杂 Design System、连续镜头或严格 3D 资产时，仍然需要专业工具和人工判断。</p><h2 id="要不要用-我的判断框架"><a href="#要不要用-我的判断框架" class="headerlink" title="要不要用 &#x2F; 我的判断框架"></a>要不要用 &#x2F; 我的判断框架</h2><p>我的结论是：<strong>如果你每周都有两次以上视觉生成需求，值得把生图从 Prompt 升级成 skill。</strong></p><p>判断是否值得工程化，我看 3 个问题：</p><table><thead><tr><th>问题</th><th>是</th><th>否</th></tr></thead><tbody><tr><td>同类资产会重复出现吗？</td><td>建立类型 reference</td><td>临时生成即可</td></tr><tr><td>输出要进入代码、客户端或内容系统吗？</td><td>增加后处理和验收</td><td>保存图片即可</td></tr><tr><td>失败会影响交付节奏吗？</td><td>增加模型回退和元数据</td><td>手动重试即可</td></tr></tbody></table><p>对我来说，better-imagegen 最大的价值不是“让我更会画图”，而是让视觉生成开始拥有软件工程的确定性：知道该走哪条路径、失败时如何退、最后交付什么文件、下一次怎样复现。</p><p>从前端转向 Agent &#x2F; AI Infra 后，我越来越相信一件事：<strong>超级个体的优势不在于每件事都亲手做，而在于能把判断沉淀成系统，再让 Agent 把执行力拉满。</strong> better-imagegen 就是这套思路在视觉生成上的一次实践。</p>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/d75fc714b340/</id>
    <link href="https://blog.zisheng.pro/posts/d75fc714b340/"/>
    <published>2026-07-15T08:30:00.000Z</published>
    <summary>这篇文章复盘我近期把 AI 生图用于产品视觉、角色设计、透明图标、macOS App Icon、动态壁纸与帧动画的真实过程，并介绍 better-imagegen 如何把模型路由、Prompt 约束、后处理、交付规格和元数据沉淀成一套可复用的视觉工程能力。</summary>
    <title>我把AI生图做成了工程能力：better-imagegen的实战与判断</title>
    <updated>2026-09-18T08:15:28.640Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="AI" scheme="https://blog.zisheng.pro/categories/AI/"/>
    <category term="Agent" scheme="https://blog.zisheng.pro/tags/Agent/"/>
    <category term="macOS" scheme="https://blog.zisheng.pro/tags/macOS/"/>
    <category term="Vibe Coding" scheme="https://blog.zisheng.pro/tags/Vibe-Coding/"/>
    <category term="UI Design" scheme="https://blog.zisheng.pro/tags/UI-Design/"/>
    <category term="Design System" scheme="https://blog.zisheng.pro/tags/Design-System/"/>
    <content>
      <![CDATA[<p>7 月初，我在做 Argos 的界面时遇到一个很典型的问题：功能已经成形，页面也不算难看，但它没有自己的气质。</p><p>当时最容易走的路，是让 Agent 套一套现成的 macOS glass 风格，再选一组低饱和蓝绿色。但这种做法只能得到“正确的现代界面”，得不到一个能被记住、还能持续指导后续决策的产品视觉。</p><p>我最后给 Codex 的要求只有一句：</p><blockquote><p>你知道临安青山湖水上森林吗？产品配色和调性从这个景区的主要元素蒸馏。</p></blockquote><p>后面十天里，这句话从一个模糊意象，逐渐变成 Argos 的 dashboard、menu bar、floating island 和灵动卡，又迁移到“小界”的 Web 工作台，最后沉淀成一份 Agent 可以直接执行的 <code>DESIGN.md</code>。</p><p>这篇文章不是介绍一组颜色，而是复盘一套设计语言是怎样在我和 Agent 的连续反馈中长出来的。</p><h2 id="一句话总结"><a href="#一句话总结" class="headerlink" title="一句话总结"></a>一句话总结</h2><p>青山湖设计风格的核心，不是把界面染成绿色，而是把青山湖的自然层次翻译成数字产品的职责分工：雾白负责空间与材质，湖水青负责操作，水杉绿负责状态，青山灰蓝负责信息层级，晨光金只负责极少量高光。</p><p>它真正成型的标志也不是有了一份 palette，而是面对一个新组件时，我们开始能稳定回答三个问题：什么该突出、什么该退后、什么绝不能出现。</p><h2 id="起点：我需要的不是“好看”，而是一个母题"><a href="#起点：我需要的不是“好看”，而是一个母题" class="headerlink" title="起点：我需要的不是“好看”，而是一个母题"></a>起点：我需要的不是“好看”，而是一个母题</h2><p>AI 很擅长生成看起来完成度很高的界面，但如果提示词只有“高级、简洁、科技感”，结果通常会收敛到几种熟悉套路：高饱和蓝紫渐变、纯黑科技背景、玻璃卡片层层嵌套、发光描边和胶囊按钮。</p><p>这些元素单独都没有错，问题是它们没有产品身份。换一个 Logo，仍然可以属于任何 AI 工具。</p><p>青山湖对我有价值，是因为它不是一个抽象形容词，而是一组可以观察、拆解和约束的真实对象：</p><table><thead><tr><th>自然元素</th><th>视觉感受</th><th>UI 职责</th></tr></thead><tbody><tr><td>湖面</td><td>稳定、清透、有纵深</td><td>主操作色、进度、选中态</td></tr><tr><td>雾带</td><td>柔化边界、制造层次</td><td>画布、glass surface、blur</td></tr><tr><td>水杉林</td><td>垂直、克制、有生命力</td><td>成功态、在线态、进度状态</td></tr><tr><td>浮萍</td><td>有生机但不主导</td><td>5% 以内的自然 accent</td></tr><tr><td>远山</td><td>安静、可靠、低对比</td><td>信息色、次级文字、结构层</td></tr><tr><td>晨光</td><td>稀缺、温暖、有方向</td><td>极小面积高光与提醒</td></tr><tr><td>湿木与陶土</td><td>自然的警示感</td><td>error &#x2F; danger，而不是通用红</td></tr></tbody></table><p>这里最重要的动作是“蒸馏”，不是“复刻”。</p><p>我不需要在每个页面放一张水上森林背景，也不想把产品做成旅游宣传页。我需要的是把自然景观里的层次关系，映射成界面里的语义关系。</p><h2 id="第一版并没有直接成功"><a href="#第一版并没有直接成功" class="headerlink" title="第一版并没有直接成功"></a>第一版并没有直接成功</h2><p>有了母题，Agent 很快能生成一组视觉上相关的颜色。但真实界面一跑起来，问题马上暴露。</p><p>最早的倾向是把“自然感”理解成更多颜色：湖蓝、青绿、浅金、泥土色同时出现；把“现代感”理解成更多胶囊和渐变；把“氛围感”理解成更大的装饰面积。</p><p>它们组合在一起，反而让界面显得花哨。</p><p>我在几轮截图反馈里不断删东西：</p><ul><li>两个胶囊按钮太突兀，不要求为了“风格”强行使用胶囊。</li><li>中间区域颜色太花，应该只从青山湖体系里选一个承担主导职责的颜色。</li><li>彩带装饰太长，不能侵入信息区中心。</li><li>Header 不是越窄越专业，也不是越宽越大气，要回到统一的页面间距。</li><li>组件贴边时，先修尺寸与布局约束，不要用更多装饰掩盖问题。</li></ul><p>这段过程让我确认了一件事：<strong>设计风格首先是一套删减规则。</strong></p><p>如果一套风格只告诉 Agent“可以用什么”，它很快就会把所有特征同时用上。真正能提高输出稳定性的，是同时写清楚“最多用多少”“只能用在哪里”“什么情况下不要用”。</p><p>因此后来规范里出现了很多带比例和边界的表达：浮萍绿控制在 5% 以内，晨光金只做边缘光，常规圆角控制在 6–12px，动效只服务状态变化，卡片不能继续嵌套卡片。</p><h2 id="雾白成为核心，不只是因为它好看"><a href="#雾白成为核心，不只是因为它好看" class="headerlink" title="雾白成为核心，不只是因为它好看"></a>雾白成为核心，不只是因为它好看</h2><p>青山湖风格真正定型，是在做 Argos 灵动卡时。</p><p>灵动卡需要接近 macOS 桌面小组件的玻璃质感。最直接的实现是 <code>NSVisualEffectView</code>、系统 material 和 behind-window sampling，但这条路径曾经触发打开即崩。为了稳定性，我们一度把它降级成普通的浅色 panel，结果功能稳定了，气质也一起消失了。</p><p>我当时给出的反馈是：</p><blockquote><p>加安全 blur，因为雾白这个是青山湖风格的核心设计元素之一。</p></blockquote><p>这句话推动了一个很关键的技术取舍：保留“雾”的视觉语义，但不恢复高风险的系统采样链路。</p><p>最后采用的是纯 SwiftUI 内部绘制：</p><ul><li>大面积雾白柔焦带；</li><li>左上角雾白 soft blur；</li><li>右下角很淡的湖水青 blur；</li><li>所有效果裁剪在卡片内部；</li><li>不使用 <code>NSVisualEffectView</code>；</li><li>不使用 <code>.behindWindow</code> 或 <code>.underWindowBackground</code>。</li></ul><p>这次取舍让“雾白”从一个颜色升级成了整个设计体系的基础设施。它既是背景色，也是分层方式；既负责氛围，也负责降低信息噪声；还必须服从稳定性、性能和可读性。</p><p>这也是我现在判断视觉方案的一个原则：<strong>设计语言不能绕过工程约束，好的设计 token 应该能在安全实现与理想效果之间找到可迁移的中间层。</strong></p><h2 id="从色板走向语义系统"><a href="#从色板走向语义系统" class="headerlink" title="从色板走向语义系统"></a>从色板走向语义系统</h2><p>经过几轮实战后，青山湖不再是一组零散的 hex，而是逐渐形成了 Seed → Map → Semantic → Alias 的 token 架构。</p><figure class="highlight css"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="selector-pseudo">:root</span> &#123;</span><br><span class="line">  <span class="attr">--color-bg-base</span>: <span class="number">#f4f6f1</span>;       <span class="comment">/* 暖雾白画布 */</span></span><br><span class="line">  <span class="attr">--color-bg-container</span>: <span class="number">#fbfcfa</span>;  <span class="comment">/* 玻璃容器 */</span></span><br><span class="line">  <span class="attr">--color-primary</span>: <span class="number">#3c8f88</span>;       <span class="comment">/* 湖水青主操作 */</span></span><br><span class="line">  <span class="attr">--color-text</span>: <span class="number">#183b40</span>;          <span class="comment">/* 深湖墨正文 */</span></span><br><span class="line">  <span class="attr">--color-success</span>: <span class="number">#4f8751</span>;       <span class="comment">/* 水杉绿 */</span></span><br><span class="line">  <span class="attr">--color-info</span>: <span class="number">#4d7180</span>;          <span class="comment">/* 青山灰蓝 */</span></span><br><span class="line">  <span class="attr">--color-warning</span>: <span class="number">#9c782d</span>;       <span class="comment">/* 克制晨光金 */</span></span><br><span class="line">  <span class="attr">--color-error</span>: <span class="number">#a95b4b</span>;         <span class="comment">/* 雨后陶土 */</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>其中最重要的不是颜色值，而是语义稳定：</p><ul><li><code>lake-teal</code> 永远优先服务主操作，而不是随手给一个装饰图标。</li><li><code>cypress-green</code> 表示完成、在线与健康状态。</li><li><code>dawn-gold</code> 不承担大面积品牌色，只提示稀缺状态。</li><li>浅色区域的主文字使用深湖蓝或水杉墨绿，不滥用纯白发光字。</li><li>Claude Code 与 Codex 可以有不同 accent，但都必须来自同一景区 palette。</li></ul><p>颜色一旦有职责，Agent 的自由度反而更健康。它仍然可以生成新组件，却不必每次重新猜一遍品牌方向。</p><h2 id="布局也是青山湖风格的一部分"><a href="#布局也是青山湖风格的一部分" class="headerlink" title="布局也是青山湖风格的一部分"></a>布局也是青山湖风格的一部分</h2><p>如果只换颜色，任何设计系统最后都会沦为皮肤。</p><p>在 dashboard 的几轮调整里，我逐渐把“青山湖的层次”落实到布局密度和信息结构上：稳定侧栏、清晰控制条、12–16px 卡片内距、20–24px section gap、连续圆角，以及不依赖巨大留白的中高信息密度。</p><p>一次 Header 调整很能说明问题。最初品牌名与 Tab 名都挤在左侧，为了显得紧凑又不断压缩高度，最后整个 Header 既窄又别扭。重新排布后，结构变成：</p><ul><li>“灵动台”作为产品标识放在上方中间；</li><li>当前 Tab 名放在下方左侧；</li><li>筛选放在中间；</li><li>刷新操作放在右侧；</li><li>间距统一回到同一套节奏。</li></ul><p>这里没有新增任何“青山湖装饰”，但它比多加一条湖水渐变更符合这套风格。因为自然景观给人的舒适感，本质上也来自前景、中景、远景各自有位置，不互相争抢。</p><h2 id="从-Argos-到小界：迁移后才算设计系统"><a href="#从-Argos-到小界：迁移后才算设计系统" class="headerlink" title="从 Argos 到小界：迁移后才算设计系统"></a>从 Argos 到小界：迁移后才算设计系统</h2><p>只在一个产品里成立的规则，还只能叫项目风格。</p><p>后来我把这套语言迁移到“小界”的 Web 工作台。平台从 SwiftUI 变成 React、Umi、styled-components 和 Tailwind，产品形态也从 macOS menu bar 工具变成 AI 控制台，但核心映射依然成立：</p><ul><li>暖雾白作为低噪声 canvas；</li><li>湖水青作为唯一主操作色；</li><li>水杉绿表达任务完成和运行状态；</li><li>青山灰蓝承担信息与次级层次；</li><li>glass surface 保持轻量，不做卡片套卡片；</li><li>页面维持专业工具需要的中高信息密度；</li><li>loading、empty、error 和 success 使用同一套自然语义。</li></ul><p>迁移过程中，设计体系又吸收了 Web 产品的新约束：正文默认 14px、工具栏控件 32–36px、桌面优先但窄屏不溢出、动效支持 <code>prefers-reduced-motion</code>，以及资源统一使用 WebP。</p><p>最终沉淀出的 <code>DESIGN.md</code> 不再叫某个产品主题，而是更通用的 <strong>Jiangnan Landscape Glass Dashboard</strong>。它保留青山湖的来源，同时把能力抽象成 AI 控制台、桌面 dashboard、settings、menu bar 和 floating panel 都能复用的设计 brief。</p><h2 id="Agent-参与设计，人的价值在哪里"><a href="#Agent-参与设计，人的价值在哪里" class="headerlink" title="Agent 参与设计，人的价值在哪里"></a>Agent 参与设计，人的价值在哪里</h2><p>回看这些会话，Agent 承担了大量执行工作：从截图理解问题、搜索现有实现、调整布局、生成 token、修改 SwiftUI&#x2F;CSS、跑构建和测试，再把经验写进规范。</p><p>但方向真正发生变化的节点，几乎都来自人的判断：</p><ul><li>“不要泛科技感，要从一个真实地点蒸馏。”</li><li>“胶囊很突兀，不必为了统一而强行使用。”</li><li>“颜色太花，只选一个承担主导职责。”</li><li>“雾白是核心，但不能恢复会崩溃的实现。”</li><li>“不要太紧凑，也不要太宽松，回到统一间距。”</li></ul><p>这恰好符合我对“超级个体”的理解：AI 把执行速度拉满，人负责建立判断框架。</p><p>在 UI 工作里，判断框架不是一句“再高级一点”，而是把模糊审美不断压缩成可验证的约束：颜色比例、组件职责、布局节奏、反例清单、技术安全边界和跨端迁移规则。</p><p>当这些规则被写进 <code>DESIGN.md</code> 和项目级 <code>AGENTS.md</code>，审美才从一次性的聊天上下文，变成可以在下一次会话继续生效的工程资产。</p><h2 id="我现在怎样定义青山湖设计风格"><a href="#我现在怎样定义青山湖设计风格" class="headerlink" title="我现在怎样定义青山湖设计风格"></a>我现在怎样定义青山湖设计风格</h2><p>如果今天再让我用一段话描述它，我会这样写：</p><blockquote><p>以暖雾白和云带灰蓝构成低噪声画布，用轻量半透明材质、连续圆角、柔和边框和克制阴影组织信息；湖水青是唯一主操作色，水杉绿用于成功与进度，浮萍绿只作少量自然活性点缀，晨光金只用于稀缺高光。界面保持专业工具的中高信息密度，不做旅游海报、赛博霓虹、重边框、硬阴影和嵌套卡片。</p></blockquote><p>它的反模式同样明确：</p><ul><li>不是贴一张青山湖照片；</li><li>不是把所有组件改成青绿色；</li><li>不是处处 glassmorphism；</li><li>不是用大面积留白制造“高级感”；</li><li>不是遇到层级问题就再套一张卡片；</li><li>不是牺牲稳定性去追求系统材质效果。</li></ul><h2 id="我的判断框架"><a href="#我的判断框架" class="headerlink" title="我的判断框架"></a>我的判断框架</h2><p>青山湖风格值得继续用，但不是所有产品都应该用。</p><p><strong>值得上</strong>：AI 控制台、桌面效率工具、settings、数据 dashboard、menu bar 与 floating panel。这些场景需要稳定、清透、低噪声，又不能完全失去品牌辨识度。</p><p><strong>可以再等</strong>：强营销 landing page、娱乐化社区、需要极强情绪刺激的消费产品。青山湖的优势是克制，不适合强行承担所有视觉目标。</p><p><strong>重点关注</strong>：不要让 Agent 把设计系统退化成 palette。每次迁移至少检查四层：语义 token、信息密度、组件形态和 anti-pattern。只换颜色，不算迁移完成。</p><p>这套风格的诞生没有一个“设计完成”的瞬间。它更像青山湖早晨的雾：先有模糊的方向，再在一次次实现、崩溃、截图和删改中显出层次。</p><p>而我最想保留下来的，也不是某个具体色值，而是这套与 Agent 协作的方法：从真实世界找到母题，用真实产品验证它，用反例收紧边界，最后把判断写成下一次会话仍能执行的规则。</p>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/8b52f4a0fd65/</id>
    <link href="https://blog.zisheng.pro/posts/8b52f4a0fd65/"/>
    <published>2026-07-15T08:30:00.000Z</published>
    <summary>青山湖设计风格不是从一套漂亮配色表开始的，而是在 Argos 与小界两个真实产品里，经过景区意象提炼、界面反例纠偏、macOS 安全实现和跨项目复用逐步长出来的。本文复盘它从一句自然语言需求到 DESIGN.md 的全过程，以及我如何和 Agent 一起建立可执行、可验证、可迁移的设计语言。</summary>
    <title>青山湖设计风格的诞生记</title>
    <updated>2026-09-18T08:15:28.640Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="AI" scheme="https://blog.zisheng.pro/categories/AI/"/>
    <category term="Agent" scheme="https://blog.zisheng.pro/tags/Agent/"/>
    <category term="Performance" scheme="https://blog.zisheng.pro/tags/Performance/"/>
    <category term="CSS" scheme="https://blog.zisheng.pro/tags/CSS/"/>
    <category term="Animation" scheme="https://blog.zisheng.pro/tags/Animation/"/>
    <category term="UI Engineering" scheme="https://blog.zisheng.pro/tags/UI-Engineering/"/>
    <content>
      <![CDATA[<p>最近给一个体验治理 Agent 的聊天欢迎页补氛围时，我没有再加一张背景大图，而是把「云雾」做成了一个独立的 CSS layer。它在页面里缓慢游移，既让空状态不显得空，又不会干扰输入框、对话气泡和任务状态这些真正需要被看见的内容。</p><p>这件事看起来是视觉微调，实际很像前端工程里的控制问题：动画有了不代表体验变好了。云雾的范围、透明度、节奏和层级，只要有一个失控，就会立刻变成一层抢注意力的滤镜。</p><h2 id="一句话总结"><a href="#一句话总结" class="headerlink" title="一句话总结"></a>一句话总结</h2><p>把背景云雾拆成一个不参与布局的绝对定位 layer，再用两组不同形状、不同节奏的 <code>radial-gradient</code> 和 <code>transform</code> 动画叠加。调参时优先收束可见范围和对比度，而不是继续增加颜色或动画。</p><h2 id="先明确它不是内容，而是环境"><a href="#先明确它不是内容，而是环境" class="headerlink" title="先明确它不是内容，而是环境"></a>先明确它不是内容，而是环境</h2><p>聊天页的视觉优先级应该很清晰：用户先看到 Agent 在说什么，再看到输入框和建议，最后才感知页面有一点流动感。</p><p>所以云雾层需要满足四个约束：</p><table><thead><tr><th>约束</th><th>实现选择</th></tr></thead><tbody><tr><td>不影响交互</td><td><code>pointer-events: none</code></td></tr><tr><td>不挤压内容布局</td><td><code>position: absolute</code>，不放进正常 flow</td></tr><tr><td>永远在内容下方</td><td>雾层 <code>z-index: 0</code>，内容容器 <code>z-index: 1</code></td></tr><tr><td>动画成本可控</td><td>只动 <code>transform</code> 和 <code>opacity</code></td></tr></tbody></table><p>页面结构最终很简单：</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">&lt;div className=<span class="string">&quot;hq-agent-courtyard&quot;</span>&gt;</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;hq-courtyard-mist&quot;</span> <span class="attr">aria-hidden</span>=<span class="string">&quot;true&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;hq-courtyard-content&quot;</span>&gt;</span>&#123;/* 消息、输入框与任务面板 */&#125;<span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line">&lt;/div&gt;</span><br></pre></td></tr></table></figure><p>这里的 <code>aria-hidden</code> 也很重要。云雾没有语义，不应该被屏幕阅读器读到，更不该被当成一个可操作元素。</p><h2 id="两朵雾，而不是一张渐变背景"><a href="#两朵雾，而不是一张渐变背景" class="headerlink" title="两朵雾，而不是一张渐变背景"></a>两朵雾，而不是一张渐变背景</h2><p>一开始用单层大渐变时，效果很像带颜色的蒙版。它有颜色变化，但没有空间关系。</p><p>后来我改成 <code>::before</code> 和 <code>::after</code> 两层。每层不是一个圆，而是多组椭圆形 <code>radial-gradient</code> 拼出来的非规则云团：左侧留一团偏深的青绿，中部和右侧再放更浅的雾，边缘用很长的透明过渡消掉。</p><figure class="highlight css"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="selector-class">.hq-courtyard-mist</span> &#123;</span><br><span class="line">  <span class="attribute">position</span>: absolute;</span><br><span class="line">  inset: <span class="number">5%</span>;</span><br><span class="line">  <span class="attribute">z-index</span>: <span class="number">0</span>;</span><br><span class="line">  <span class="attribute">pointer-events</span>: none;</span><br><span class="line">  <span class="attribute">opacity</span>: <span class="number">0.82</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="selector-class">.hq-courtyard-mist</span><span class="selector-pseudo">::before</span>,</span><br><span class="line"><span class="selector-class">.hq-courtyard-mist</span><span class="selector-pseudo">::after</span> &#123;</span><br><span class="line">  <span class="attribute">content</span>: <span class="string">&#x27;&#x27;</span>;</span><br><span class="line">  <span class="attribute">position</span>: absolute;</span><br><span class="line">  inset: <span class="number">0</span>;</span><br><span class="line">  <span class="attribute">border-radius</span>: <span class="number">48%</span> <span class="number">52%</span> <span class="number">57%</span> <span class="number">43%</span> / <span class="number">44%</span> <span class="number">49%</span> <span class="number">51%</span> <span class="number">56%</span>;</span><br><span class="line">  <span class="attribute">filter</span>: <span class="built_in">blur</span>(<span class="number">12px</span>);</span><br><span class="line">  <span class="attribute">will-change</span>: transform, opacity;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="selector-class">.hq-courtyard-mist</span><span class="selector-pseudo">::before</span> &#123;</span><br><span class="line">  <span class="attribute">background</span>:</span><br><span class="line">    <span class="built_in">radial-gradient</span>(ellipse <span class="number">26%</span> <span class="number">15%</span> at <span class="number">16%</span> <span class="number">22%</span>, <span class="built_in">rgba</span>(<span class="number">93</span>, <span class="number">171</span>, <span class="number">153</span>, <span class="number">0.5</span>), transparent <span class="number">78%</span>),</span><br><span class="line">    <span class="built_in">radial-gradient</span>(ellipse <span class="number">21%</span> <span class="number">28%</span> at <span class="number">29%</span> <span class="number">32%</span>, <span class="built_in">rgba</span>(<span class="number">129</span>, <span class="number">190</span>, <span class="number">175</span>, <span class="number">0.4</span>), transparent <span class="number">78%</span>),</span><br><span class="line">    <span class="built_in">radial-gradient</span>(ellipse <span class="number">27%</span> <span class="number">36%</span> at <span class="number">65%</span> <span class="number">72%</span>, <span class="built_in">rgba</span>(<span class="number">117</span>, <span class="number">184</span>, <span class="number">168</span>, <span class="number">0.5</span>), transparent <span class="number">76%</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>真正起作用的不是某一个色值，而是三层关系：</p><ol><li><strong>形状不规则</strong>：每个 gradient 的大小和位置不同，避免出现一眼可识别的圆形光斑。</li><li><strong>透明边界足够长</strong>：色彩在 76% 到 82% 之间才消失，云团会更像在空气里扩散。</li><li><strong>两层方向相反</strong>：一层从左下缓慢移动到右上，另一层反向移动，页面才有轻微的深度感。</li></ol><h2 id="动画只负责“活着”，不负责表演"><a href="#动画只负责“活着”，不负责表演" class="headerlink" title="动画只负责“活着”，不负责表演"></a>动画只负责“活着”，不负责表演</h2><p>这个页面不是营销首页，云雾不能用大幅位移抢镜。我把两层动画拉成 6 秒和 8 秒，并使用 <code>alternate</code> 往返：不需要额外做首尾拼接，也不会出现明显的瞬间跳回。</p><figure class="highlight css"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">@keyframes</span> jiangnan-mist-cloud-a &#123;</span><br><span class="line">  <span class="selector-tag">from</span> &#123;</span><br><span class="line">    <span class="attribute">transform</span>: <span class="built_in">translate3d</span>(-<span class="number">22%</span>, <span class="number">10%</span>, <span class="number">0</span>) <span class="built_in">scale</span>(<span class="number">0.82</span>);</span><br><span class="line">    <span class="attribute">opacity</span>: <span class="number">0.36</span>;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="selector-tag">to</span> &#123;</span><br><span class="line">    <span class="attribute">transform</span>: <span class="built_in">translate3d</span>(<span class="number">22%</span>, -<span class="number">10%</span>, <span class="number">0</span>) <span class="built_in">scale</span>(<span class="number">1.18</span>);</span><br><span class="line">    <span class="attribute">opacity</span>: <span class="number">0.92</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">@keyframes</span> jiangnan-mist-cloud-b &#123;</span><br><span class="line">  <span class="selector-tag">from</span> &#123;</span><br><span class="line">    <span class="attribute">transform</span>: <span class="built_in">translate3d</span>(<span class="number">24%</span>, -<span class="number">12%</span>, <span class="number">0</span>) <span class="built_in">scale</span>(<span class="number">1.2</span>);</span><br><span class="line">    <span class="attribute">opacity</span>: <span class="number">0.84</span>;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="selector-tag">to</span> &#123;</span><br><span class="line">    <span class="attribute">transform</span>: <span class="built_in">translate3d</span>(-<span class="number">24%</span>, <span class="number">12%</span>, <span class="number">0</span>) <span class="built_in">scale</span>(<span class="number">0.8</span>);</span><br><span class="line">    <span class="attribute">opacity</span>: <span class="number">0.3</span>;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这里我刻意只用了 <code>transform</code> 和 <code>opacity</code>。它们通常可以由 compositor 处理，不会每一帧触发布局计算。相比直接动画化 <code>top</code>、<code>left</code>、<code>width</code> 或一串渐变参数，这更适合常驻的背景效果。</p><h2 id="最重要的一次调整：从“铺满”改成“收束”"><a href="#最重要的一次调整：从“铺满”改成“收束”" class="headerlink" title="最重要的一次调整：从“铺满”改成“收束”"></a>最重要的一次调整：从“铺满”改成“收束”</h2><p>第一版我把雾层设成 <code>inset: -16%</code>。直觉是让云团从视口外飘进来，避免边缘被裁切。结果实际看下来，页面整个区域都被雾覆盖：背景有了，但聊天卡片的玻璃质感反而被稀释，输入框周边也不够干净。</p><p>最终调整是：</p><figure class="highlight css"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/* 初版：云雾覆盖过大，内容区域缺少留白 */</span></span><br><span class="line"><span class="comment">/* inset: -16%; */</span></span><br><span class="line"></span><br><span class="line"><span class="comment">/* 调整后：云团收进安全区，边缘交给页面底色 */</span></span><br><span class="line">inset: <span class="number">5%</span>;</span><br><span class="line"><span class="attribute">opacity</span>: <span class="number">0.82</span>;</span><br></pre></td></tr></table></figure><p>这个改动的收益比继续调色更大：</p><table><thead><tr><th>调整</th><th>视觉结果</th></tr></thead><tbody><tr><td>雾层缩到安全区</td><td>四周恢复呼吸感，卡片边界更清楚</td></tr><tr><td>保留多层 gradient</td><td>中央不再是一张平淡的浅绿底图</td></tr><tr><td>降低极亮区域的覆盖面积</td><td>输入框和欢迎气泡成为稳定焦点</td></tr><tr><td>两层保持反向漂移</td><td>页面依然有“空气在流动”的感觉</td></tr></tbody></table><p>这也是我这次最有价值的判断：<strong>背景动画的优化，大多数时候不是“做得更多”，而是“让它少出现一点”。</strong></p><h2 id="玻璃组件和云雾层如何配合"><a href="#玻璃组件和云雾层如何配合" class="headerlink" title="玻璃组件和云雾层如何配合"></a>玻璃组件和云雾层如何配合</h2><p>云雾只是底景，玻璃卡片负责把内容从底景里提出来。欢迎气泡和输入框用了半透明背景、细边框、内高光和 <code>backdrop-filter</code>。这样背景雾会隐约透进组件，但文本所在区域仍有足够对比度。</p><p>可以把它理解成三层：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">底色与页面纹理</span><br><span class="line">  -&gt; 缓慢漂移的云雾 layer</span><br><span class="line">    -&gt; 玻璃卡片与真实内容</span><br></pre></td></tr></table></figure><p>如果没有中间那层云雾，玻璃很容易显得只是半透明白色；如果没有最上层的玻璃容器，雾又会让文字显得发灰。它们应当一起设计，而不是各自加一个效果。</p><h2 id="我会怎样验证这类背景动画"><a href="#我会怎样验证这类背景动画" class="headerlink" title="我会怎样验证这类背景动画"></a>我会怎样验证这类背景动画</h2><p>这类效果很容易陷入“截图看着不错”的错觉，我通常按下面三件事检查：</p><ol><li><strong>停在任意时刻都成立</strong>：截取动画中的任意一帧，内容层级都清楚，不能依赖某个刚好干净的瞬间。</li><li><strong>连续看 10 秒不烦</strong>：如果能明确察觉云团在左右移动，节奏已经太强；正确的感觉应该是视线离开后才意识到页面在呼吸。</li><li><strong>性能面板没有持续压力</strong>：确认动画只涉及合成友好的属性，并在低性能设备上观察掉帧和 GPU 占用。</li></ol><p>此外，生产页面还应该补一条 <code>prefers-reduced-motion</code>，让用户可以在系统层面关闭或静止这类环境动画。审美不能覆盖可访问性偏好。</p><h2 id="要不要用-我的判断框架"><a href="#要不要用-我的判断框架" class="headerlink" title="要不要用 &#x2F; 我的判断框架"></a>要不要用 &#x2F; 我的判断框架</h2><ul><li><strong>值得上</strong>：空状态、Agent 对话页、工作台欢迎页这类内容稀疏但需要建立产品情绪的界面。用低对比、慢节奏的环境层补足空间。</li><li><strong>可以再等</strong>：数据密集的列表、表格和监控台。那里信息密度已经够高，背景动画多半只会制造噪声。</li><li><strong>重点关注</strong>：优先控制雾层范围、透明度和内容对比度，再讨论颜色和动画曲线。层级错了，再漂亮的 gradient 也是干扰。</li></ul><p>从前端转向 Agent 和 AI Infra 后，我越来越在意这类“非功能性”的交互细节。Agent 的能力决定它能做什么，界面的节奏决定用户是否愿意把注意力交给它。云雾当然不会让模型更聪明，但它可以让等待、输入和阅读这几个瞬间更安静一些。</p>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/5633e84c7f72/</id>
    <link href="https://blog.zisheng.pro/posts/5633e84c7f72/"/>
    <published>2026-07-14T12:30:00.000Z</published>
    <summary>复盘一次 Agent 聊天界面的云雾层改造：如何用伪元素、多个 radial-gradient 和低成本 transform 动画，把静态淡绿背景做成有层次但不抢内容的氛围层；也记录了从“铺满整屏”到“收束留白”的关键调参过程。</summary>
    <title>让云雾动起来：聊天界面的分层氛围动画</title>
    <updated>2026-09-18T08:15:28.640Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="AI" scheme="https://blog.zisheng.pro/categories/AI/"/>
    <category term="AI Infra" scheme="https://blog.zisheng.pro/tags/AI-Infra/"/>
    <category term="Agent" scheme="https://blog.zisheng.pro/tags/Agent/"/>
    <category term="Performance" scheme="https://blog.zisheng.pro/tags/Performance/"/>
    <category term="macOS" scheme="https://blog.zisheng.pro/tags/macOS/"/>
    <category term="SwiftUI" scheme="https://blog.zisheng.pro/tags/SwiftUI/"/>
    <category term="Observability" scheme="https://blog.zisheng.pro/tags/Observability/"/>
    <content>
      <![CDATA[<p>我最近在做 Owlet：一个本地读取 Claude Code 与 Codex 会话记录、展示 Token 和实时活动的 macOS 工具。它的难点不在「能不能把日志读出来」，而在「数据源有任何变化时，怎样既足够快，又不把本地 app 刷成一个持续工作的轮询器」。</p><p>这轮改动之后，我更确定一件事：本地 Agent 工具的性能优化，重点不是把某一次 query 跑快，而是把<strong>刷新语义、数据计算和 UI 发布</strong>拆开管理。</p><h2 id="一句话总结"><a href="#一句话总结" class="headerlink" title="一句话总结"></a>一句话总结</h2><p>把“任何事件都立即全量刷新”改成“合并请求、分层计算、变化才发布”。这样既能保住 Agent 正在运行时的及时性，也能显著减少重复 SQLite 查询和无意义的 SwiftUI invalidation。</p><h2 id="问题不在单个刷新，而在刷新会叠加"><a href="#问题不在单个刷新，而在刷新会叠加" class="headerlink" title="问题不在单个刷新，而在刷新会叠加"></a>问题不在单个刷新，而在刷新会叠加</h2><p>Owlet 的数据链路大致是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">Claude Code JSONL / Codex SQLite</span><br><span class="line">  -&gt; Adapter 增量同步</span><br><span class="line">  -&gt; 本地 analytics SQLite</span><br><span class="line">  -&gt; AppState</span><br><span class="line">  -&gt; 菜单栏 / 灵动岛 / 灵动卡 / 仪表盘</span><br></pre></td></tr></table></figure><p>一开始最直觉的实现是：文件变化一次，触发一次 <code>refresh()</code>；刷新完成后，重新计算所有 analytics 并发布所有 <code>@Published</code> 状态。</p><p>它的问题会在真实使用里迅速放大：一次 Agent 工作往往连续写入多个文件；文件观察、定时活跃探测、用户手动刷新又可能同时到达。若每次都启动完整链路，就会出现 3 类浪费：</p><table><thead><tr><th>浪费</th><th>结果</th></tr></thead><tbody><tr><td>同一时段的多次刷新互相重叠</td><td>重复读取来源、争用本地数据库</td></tr><tr><td>小组件只需要今日 Token，却计算了完整 dashboard</td><td>菜单栏和灵动岛被大查询拖慢</td></tr><tr><td>查询结果没有变化，仍重新赋值</td><td>SwiftUI 继续 diff、重绘所有依赖视图</td></tr></tbody></table><p>这不是 CPU 单点热点，而是一个典型的控制面问题：系统没有定义好“下一次刷新是否仍有必要”。</p><h2 id="第一层：合并请求，但不丢掉尾部更新"><a href="#第一层：合并请求，但不丢掉尾部更新" class="headerlink" title="第一层：合并请求，但不丢掉尾部更新"></a>第一层：合并请求，但不丢掉尾部更新</h2><p>普通 debounce 的常见写法是：每次事件都取消旧 Task，重新等待几秒再刷新。它能降频，但有一个隐蔽问题：如果刷新正在进行时又来了事件，新事件很容易被 <code>isRefreshing</code> 直接丢掉。</p><p>Owlet 改成了单个长期持有的 auto-refresh Task，加一个 <code>pendingAutoRefresh</code> 标记：</p><figure class="highlight swift"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">func</span> <span class="title function_">scheduleAutoRefresh</span>() &#123;</span><br><span class="line">    pendingAutoRefresh <span class="operator">=</span> <span class="literal">true</span></span><br><span class="line">    <span class="keyword">guard</span> autoRefreshTask <span class="operator">==</span> <span class="literal">nil</span> <span class="keyword">else</span> &#123; <span class="keyword">return</span> &#125;</span><br><span class="line"></span><br><span class="line">    autoRefreshTask <span class="operator">=</span> <span class="type">Task</span> &#123; <span class="meta">@MainActor</span> [<span class="keyword">weak</span> <span class="keyword">self</span>] <span class="keyword">in</span></span><br><span class="line">        <span class="keyword">while</span> <span class="operator">!</span><span class="type">Task</span>.isCancelled &#123;</span><br><span class="line">            <span class="keyword">try?</span> <span class="keyword">await</span> <span class="type">Task</span>.sleep(for: .milliseconds(<span class="number">200</span>))</span><br><span class="line">            <span class="keyword">guard</span> <span class="keyword">let</span> <span class="keyword">self</span>, <span class="operator">!</span><span class="type">Task</span>.isCancelled <span class="keyword">else</span> &#123; <span class="keyword">return</span> &#125;</span><br><span class="line"></span><br><span class="line">            <span class="keyword">self</span>.pendingAutoRefresh <span class="operator">=</span> <span class="literal">false</span></span><br><span class="line">            <span class="keyword">let</span> didRefresh <span class="operator">=</span> <span class="keyword">await</span> <span class="keyword">self</span>.refresh(preloadAnalytics: .none)</span><br><span class="line">            <span class="keyword">if</span> <span class="operator">!</span>didRefresh &#123; <span class="keyword">self</span>.pendingAutoRefresh <span class="operator">=</span> <span class="literal">true</span> &#125;</span><br><span class="line"></span><br><span class="line">            <span class="keyword">guard</span> <span class="keyword">self</span>.pendingAutoRefresh <span class="operator">==</span> <span class="literal">false</span> <span class="keyword">else</span> &#123; <span class="keyword">continue</span> &#125;</span><br><span class="line">            <span class="keyword">self</span>.autoRefreshTask <span class="operator">=</span> <span class="literal">nil</span></span><br><span class="line">            <span class="keyword">return</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>200 ms</code> 这个数字，而是状态机：</p><ol><li>任意事件只负责标记“有新工作”。</li><li>已经有 refresh worker 时，不再创建第二个 worker。</li><li>worker 刷新后再次检查标记；期间来的新事件会自然形成下一轮。</li></ol><p>这相当于一个 <strong>trailing-edge coalescer</strong>。它允许短时间内的事件合并，又保证「刷新期间发生的最后一次变化」不会消失。对本地日志同步来说，这比无限取消的 debounce 更符合数据正确性。</p><h2 id="第二层：不同-surface，不要共用一份重型刷新"><a href="#第二层：不同-surface，不要共用一份重型刷新" class="headerlink" title="第二层：不同 surface，不要共用一份重型刷新"></a>第二层：不同 surface，不要共用一份重型刷新</h2><p>菜单栏、灵动岛和灵动卡真正关心的是“现在有没有活跃 Agent、今天用了多少 Token”。仪表盘才需要项目分布、模型分布、小时趋势、最近会话等完整数据。</p><p>此前这些 UI surface 共用一份 analytics reload，打开菜单栏也可能触发全套 dashboard 查询。优化后把 snapshot 分成两类：</p><figure class="highlight swift"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">enum</span> <span class="title class_">AnalyticsReloadScope</span> &#123;</span><br><span class="line">    <span class="keyword">case</span> none, surface, board, all</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><table><thead><tr><th>Scope</th><th>计算内容</th><th>消费者</th></tr></thead><tbody><tr><td><code>surface</code></td><td>今日 Token、近 7 日日聚合</td><td>菜单栏、灵动岛、灵动卡</td></tr><tr><td><code>board</code></td><td>会话、项目、模型、日&#x2F;小时趋势</td><td>灵动台仪表盘、会话页</td></tr><tr><td><code>all</code></td><td>两者</td><td>首次启动、用户显式刷新</td></tr></tbody></table><p>当用户点开菜单栏时，先从已经落到本地 SQLite 的数据快速加载 <code>surface</code> snapshot，再在后台走 adapter 增量同步。用户看到的是立刻可用的旧但可信数据；同步完成后再应用新结果。</p><p>这个策略背后的取舍是：<strong>本地可观测性 UI 优先 freshness 的连续性，不必执着于每次打开都同步到最新字节。</strong> 只要后续同步能很快收敛，体验比“打开后空白等待一次全量链路”好得多。</p><h2 id="第三层：查询完成不等于应该发布状态"><a href="#第三层：查询完成不等于应该发布状态" class="headerlink" title="第三层：查询完成不等于应该发布状态"></a>第三层：查询完成不等于应该发布状态</h2><p>SwiftUI 的刷新成本不只来自 view body 本身。一个 <code>@Published</code> 数组被重新赋值，会使依赖它的图表、列表、卡片都重新进入 diff。</p><p>所以 snapshot 要实现 <code>Equatable</code>，并在主线程发布前做一次值级短路：</p><figure class="highlight swift"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">func</span> <span class="title function_">applyBoardAnalyticsSnapshot</span>(<span class="keyword">_</span> <span class="params">snapshot</span>: <span class="type">BoardAnalyticsSnapshot</span>) &#123;</span><br><span class="line">    <span class="keyword">guard</span> cachedBoardAnalyticsSnapshot <span class="operator">!=</span> snapshot <span class="keyword">else</span> &#123; <span class="keyword">return</span> &#125;</span><br><span class="line">    cachedBoardAnalyticsSnapshot <span class="operator">=</span> snapshot</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> totals <span class="operator">!=</span> snapshot.totals &#123; totals <span class="operator">=</span> snapshot.totals &#125;</span><br><span class="line">    <span class="keyword">if</span> recentSessions <span class="operator">!=</span> snapshot.recentSessions &#123; recentSessions <span class="operator">=</span> snapshot.recentSessions &#125;</span><br><span class="line">    <span class="keyword">if</span> hourlyUsage <span class="operator">!=</span> snapshot.hourlyUsage &#123; hourlyUsage <span class="operator">=</span> snapshot.hourlyUsage &#125;</span><br><span class="line">    <span class="comment">// 其余字段同理</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这一步很朴素，但它把“数据库读到一个结果”与“UI 需要看到一次新状态”明确区分开了。特别是 3 秒一次的活动探测中，绝大多数轮次的活跃会话并没有变化；这时只要不写回 <code>activeSession</code>，就不会无端唤醒整棵视图树。</p><h2 id="再补一层：活跃时快，空闲时慢"><a href="#再补一层：活跃时快，空闲时慢" class="headerlink" title="再补一层：活跃时快，空闲时慢"></a>再补一层：活跃时快，空闲时慢</h2><p>Agent 正在工作时，用户在意灵动岛是否实时；空闲时，每秒检查一次只是在浪费电和 I&#x2F;O。</p><p>因此 periodic refresh 采用两档节奏：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">检测到活跃 Agent：每 3 秒检查并按需同步</span><br><span class="line">没有活跃 Agent：30 秒才做一次兜底刷新</span><br><span class="line">文件观察事件：立即合并到下一次刷新</span><br></pre></td></tr></table></figure><p>文件观察负责绝大多数实时性，低频轮询只负责兜底。这个组合比“纯轮询”可靠，也比“完全相信文件事件”更能应对日志轮转、外部工具写入和观察器偶发失效。</p><h2 id="我会怎样验证这类优化"><a href="#我会怎样验证这类优化" class="headerlink" title="我会怎样验证这类优化"></a>我会怎样验证这类优化</h2><p>这里有一个容易犯的错：没有 trace 就先宣布“快了 X%”。这次我没有编造一个漂亮数字，而是把验证拆成三个层次：</p><ol><li><strong>行为正确性</strong>：并发事件不会丢最后一次更新；手动刷新与自动刷新不会并发重入。</li><li><strong>数据正确性</strong>：没有 delta 时不改 analytics；有 delta 时会重新读取对应 scope。</li><li><strong>运行时证据</strong>：用 Instruments 观察主线程占用、SwiftUI update 原因，以及菜单栏打开和仪表盘切换时的 SQLite 查询量。</li></ol><p>性能优化应该以“减少了哪一类工作”为单位陈述，而不只是看某一次 demo 的体感。</p><h2 id="要不要用-我的判断框架"><a href="#要不要用-我的判断框架" class="headerlink" title="要不要用 &#x2F; 我的判断框架"></a>要不要用 &#x2F; 我的判断框架</h2><p>如果你在做的是会持续观察本地日志、数据库、文件系统或多个 Agent runtime 的桌面工具，我建议尽早建立这三条边界：</p><ul><li><strong>值得上</strong>：数据源有 burst 写入，且多个 UI surface 订阅同一份状态。优先做刷新合并和 scope 拆分。</li><li><strong>可以再等</strong>：只有一个页面、一次刷新只读极少量数据。先保持简单，不要过早拆 Task。</li><li><strong>重点关注</strong>：每次把新数组重新塞进 <code>@Published</code> 的地方。它往往不是数据变慢的根因，却是界面无故重绘的最后一跳。</li></ul><p>对我来说，这也是从前端转向 Agent &#x2F; AI Infra 时很有意思的一点：真正的性能不是“把某个函数写得更快”，而是替整个系统定义一套更少做无效工作的调度规则。</p>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/3ed23499e155/</id>
    <link href="https://blog.zisheng.pro/posts/3ed23499e155/"/>
    <published>2026-07-13T04:30:00.000Z</published>
    <summary>Owlet 同时读取 Claude Code JSONL 与 Codex SQLite，并驱动菜单栏、灵动岛、灵动卡和仪表盘。本文复盘最近一轮性能优化：怎样合并高频刷新、拆分不同 UI surface 的 analytics，并避免无变化数据触发 SwiftUI 重绘。</summary>
    <title>从高频轮询到按需刷新：本地 Agent 数据面的性能优化</title>
    <updated>2026-09-18T08:15:28.636Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="AI" scheme="https://blog.zisheng.pro/categories/AI/"/>
    <category term="Agent" scheme="https://blog.zisheng.pro/tags/Agent/"/>
    <category term="macOS" scheme="https://blog.zisheng.pro/tags/macOS/"/>
    <category term="Owlet" scheme="https://blog.zisheng.pro/tags/Owlet/"/>
    <category term="Swift" scheme="https://blog.zisheng.pro/tags/Swift/"/>
    <category term="SwiftPM" scheme="https://blog.zisheng.pro/tags/SwiftPM/"/>
    <content>
      <![CDATA[<p>Owlet 是我用 Claude Code + Codex vibe 出来的常驻 menu-bar app，统计每天 Claude Code 和 Codex 的 Token 用量、排名、实时会话活动。它是个<strong>纯 SPM 项目</strong>——没有 <code>.xcodeproj</code>，靠脚本 <code>swift build</code> 出可执行文件，再手动组装成 <code>Owlet.app</code>。这个「纯 SPM + 手动打包」的选择很轻量，但也埋了一个坑：这次我收到两份用户 crash report，症状是最吓人的那种——<strong>第一次安装，一打开就崩，连界面都没露脸</strong>。</p><p>更迷惑的是：我自己机器上从来没崩过。开发、构建、<code>swift run</code>，一路顺畅。但用户装上就崩，而且两台完全不同的机器（macOS 26.5 和 26.3.1、不同硬件、不同用户）崩得一模一样。这种「开发机好好的，用户机必崩」的 bug，往往指向同一类问题：<strong>运行环境的资源布局，和你开发时不一样。</strong></p><p><img src="/images/swiftpm-bundle-module-crash.webp" alt="打包后 Bundle.module 直接崩"></p><h2 id="一句话总结"><a href="#一句话总结" class="headerlink" title="一句话总结"></a>一句话总结</h2><p>SwiftPM 会为带资源的 target 自动合成一个 <code>Bundle.module</code> 访问器，它在找不到资源包时<strong>不是返回 nil，而是直接 <code>fatalError</code> 崩掉进程</strong>。纯 SPM 的 app 手动组装成 <code>.app</code> 后，资源包位置和 <code>.build/</code> 目录里不一样，<code>Bundle.module</code> 的默认定位逻辑就失效了。开发时它能在可执行文件旁边找到资源包所以不崩，用户首装时找不到就必崩。</p><h2 id="崩溃现场：栈顶是一句-fatalError"><a href="#崩溃现场：栈顶是一句-fatalError" class="headerlink" title="崩溃现场：栈顶是一句 fatalError"></a>崩溃现场：栈顶是一句 fatalError</h2><p>两份 crash report 关键部分完全一致。信号是 <code>EXC_BREAKPOINT (SIGTRAP)</code>——这不是野指针越界那种 <code>SIGSEGV</code>，而是代码里主动触发的断言&#x2F;<code>fatalError</code>：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">Process:             Owlet [21723]</span><br><span class="line">Version:             0.4.2 (28)</span><br><span class="line">OS Version:          macOS 26.5 (25F71)</span><br><span class="line"></span><br><span class="line">Exception Type:      EXC_BREAKPOINT (SIGTRAP)</span><br><span class="line">Termination Reason:  Namespace SIGNAL, Code 5, Trace/BPT trap: 5</span><br><span class="line"></span><br><span class="line">Thread 0 Crashed::  Dispatch queue: com.apple.main-thread</span><br><span class="line">0   libswiftCore.dylib   _assertionFailure(_:_:file:line:flags:) + 172</span><br><span class="line">1   Owlet                closure #1 in variable initialization expression of static NSBundle.module + 584</span><br><span class="line">2   Owlet                one-time initialization function for module + 12</span><br><span class="line">3   libdispatch.dylib    _dispatch_client_callout + 16</span><br><span class="line">4   libdispatch.dylib    _dispatch_once_callout + 32</span><br><span class="line">5   Owlet                TokenRank.iconImage.getter + 500</span><br><span class="line">6   Owlet                RankIconView.body.getter + 304</span><br><span class="line">7   SwiftUICore          ViewBodyAccessor.updateBody(of:changed:) ...</span><br></pre></td></tr></table></figure><p>这条栈从下往上读，故事非常清楚：</p><ol><li><strong>frame 6-5</strong>：SwiftUI 要渲染 <code>RankIconView</code>，读它的 <code>body</code>，进而调用 <code>TokenRank.iconImage</code> 这个 getter——也就是「加载排名图标」。</li><li><strong>frame 4-2</strong>：<code>iconImage</code> 里访问了 <code>Bundle.module</code>。<code>Bundle.module</code> 是个 <code>static let</code>，第一次访问会触发 <code>_dispatch_once</code> 一次性初始化（<code>one-time initialization function for module</code>）。</li><li><strong>frame 1-0</strong>：这个初始化闭包里执行了 <code>_assertionFailure</code>——<strong>直接崩</strong>。</li></ol><p>栈顶的 <code>_assertionFailure</code> + 中间的 <code>static NSBundle.module</code> 初始化，是这类 bug 的经典指纹。它对应的其实就是 SwiftPM 生成代码里那句：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Fatal error: unable to find bundle named Owlet_Owlet</span><br></pre></td></tr></table></figure><h2 id="为什么-Bundle-module-会-fatalError"><a href="#为什么-Bundle-module-会-fatalError" class="headerlink" title="为什么 Bundle.module 会 fatalError"></a>为什么 Bundle.module 会 fatalError</h2><p>如果你的 SPM target 声明了 <code>resources</code>，Swift Package Manager 会在编译期<strong>自动合成</strong>一段 <code>Bundle.module</code> 的代码，逻辑大致是：</p><figure class="highlight swift"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// SwiftPM 自动生成，简化版</span></span><br><span class="line"><span class="keyword">extension</span> <span class="title class_">Foundation</span>.<span class="title class_">Bundle</span> &#123;</span><br><span class="line">    <span class="keyword">static</span> <span class="keyword">let</span> module: <span class="type">Bundle</span> <span class="operator">=</span> &#123;</span><br><span class="line">        <span class="keyword">let</span> bundleName <span class="operator">=</span> <span class="string">&quot;Owlet_Owlet&quot;</span></span><br><span class="line">        <span class="comment">// 依次在几个候选路径里找 Owlet_Owlet.bundle</span></span><br><span class="line">        <span class="keyword">let</span> candidates <span class="operator">=</span> [</span><br><span class="line">            <span class="type">Bundle</span>.main.resourceURL,</span><br><span class="line">            <span class="type">Bundle</span>.main.bundleURL,</span><br><span class="line">            <span class="comment">// .build 相关路径……</span></span><br><span class="line">        ]</span><br><span class="line">        <span class="keyword">for</span> candidate <span class="keyword">in</span> candidates &#123;</span><br><span class="line">            <span class="keyword">if</span> <span class="keyword">let</span> path <span class="operator">=</span> candidate<span class="operator">?</span>.appendingPathComponent(bundleName <span class="operator">+</span> <span class="string">&quot;.bundle&quot;</span>),</span><br><span class="line">               <span class="keyword">let</span> bundle <span class="operator">=</span> <span class="type">Bundle</span>(url: path) &#123;</span><br><span class="line">                <span class="keyword">return</span> bundle</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="built_in">fatalError</span>(<span class="string">&quot;unable to find bundle named <span class="subst">\(bundleName)</span>&quot;</span>)</span><br><span class="line">    &#125;()</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>注意最后一行：<strong>找不到资源包，它不给你 fallback，直接 <code>fatalError</code>。</strong> 这是 SwiftPM 的刻意设计——在它的世界观里，资源包应该总是跟着可执行文件走，找不到就是构建&#x2F;打包出错，属于「不该发生」的致命错误。</p><p>问题在于，它内置的候选路径是为 <code>swift build</code>&#x2F;<code>swift test</code> 那套 <code>.build/</code> 目录布局准备的。当你<strong>手动</strong>把可执行文件塞进 <code>Owlet.app/Contents/MacOS/</code>、把资源包塞进 <code>Contents/Resources/</code> 时，布局对不上，<code>Bundle(url:)</code> 在它预设的候选路径里全部返回 nil，于是走到最后一行崩掉。</p><p>这就解释了那个最迷惑的现象：</p><table><thead><tr><th>场景</th><th>可执行文件位置</th><th>资源包位置</th><th>Bundle.module 结果</th></tr></thead><tbody><tr><td>开发机 <code>swift run</code></td><td><code>.build/debug/Owlet</code></td><td><code>.build/debug/Owlet_Owlet.bundle</code>（就在旁边）</td><td>✅ 找到，不崩</td></tr><tr><td>用户首装 <code>.app</code></td><td><code>.../MacOS/Owlet</code></td><td><code>.../Resources/Owlet_Owlet.bundle</code>（布局不同）</td><td>❌ 找不到 → fatalError</td></tr></tbody></table><p><strong>开发时永远复现不出来</strong>，因为开发时资源包就躺在可执行文件旁边，SwiftPM 的默认路径一找一个准。</p><p><img src="/images/swiftpm-bundle-module-crash-1.webp" alt="同一份代码，开发机布局和 .app 布局不同：一个能找到资源包，一个找不到"></p><h2 id="我早就知道这个坑——但漏了一处"><a href="#我早就知道这个坑——但漏了一处" class="headerlink" title="我早就知道这个坑——但漏了一处"></a>我早就知道这个坑——但漏了一处</h2><p>最扎心的是：这个坑我<strong>早就踩过并且修过</strong>。项目里专门有一个封装，就是为了绕开 <code>Bundle.module</code> 在 <code>.app</code> 里定位失败的问题：</p><figure class="highlight swift"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">enum</span> <span class="title class_">AppResourceBundle</span> &#123;</span><br><span class="line">    <span class="keyword">static</span> <span class="keyword">let</span> module: <span class="type">Bundle</span> <span class="operator">=</span> &#123;</span><br><span class="line">        <span class="keyword">let</span> fm <span class="operator">=</span> <span class="type">FileManager</span>.default</span><br><span class="line">        <span class="comment">// 1. 按 .app 的真实布局找：Contents/Resources/Owlet_Owlet.bundle</span></span><br><span class="line">        <span class="keyword">if</span> <span class="keyword">let</span> resourcesURL <span class="operator">=</span> <span class="type">Bundle</span>.main.resourceURL &#123;</span><br><span class="line">            <span class="keyword">let</span> url <span class="operator">=</span> resourcesURL.appendingPathComponent(<span class="string">&quot;Owlet_Owlet.bundle&quot;</span>)</span><br><span class="line">            <span class="keyword">if</span> fm.fileExists(atPath: url.path), <span class="keyword">let</span> b <span class="operator">=</span> <span class="type">Bundle</span>(url: url) &#123; <span class="keyword">return</span> b &#125;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="comment">// 2. 兼容另一种布局</span></span><br><span class="line">        <span class="keyword">let</span> legacy <span class="operator">=</span> <span class="type">Bundle</span>.main.bundleURL.appendingPathComponent(<span class="string">&quot;Owlet_Owlet.bundle&quot;</span>)</span><br><span class="line">        <span class="keyword">if</span> fm.fileExists(atPath: legacy.path), <span class="keyword">let</span> b <span class="operator">=</span> <span class="type">Bundle</span>(url: legacy) &#123; <span class="keyword">return</span> b &#125;</span><br><span class="line">        <span class="comment">// 3. 实在找不到才回落到 SwiftPM 的 Bundle.module</span></span><br><span class="line">        <span class="keyword">return</span> <span class="type">Bundle</span>.module</span><br><span class="line">    &#125;()</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>它的核心就是：<strong>先用 <code>fileExists</code> + <code>Bundle(url:)</code> 按 <code>.app</code> 的真实布局显式查找</strong>，命中就返回；只有全都落空时，才把烫手山芋丢回给 <code>Bundle.module</code>。这样在 <code>.app</code> 里能稳定命中第 1 步，根本走不到会 <code>fatalError</code> 的那一行。</p><p><img src="/images/swiftpm-bundle-module-crash-3.webp" alt="八处资源加载里七处都有护栏，唯独一处的护盾缺失，漏出红色告警"></p><p>全项目<strong>一共 8 处加载资源</strong>，其中 7 处都规规矩矩用了 <code>AppResourceBundle.module</code>：hook 脚本、app 图标、菜单栏猫头鹰、Dashboard 图片、本地化 bundle……唯独排名图标这一处漏了网：</p><figure class="highlight swift"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// TokenRankIcon.swift —— 出事的那一行</span></span><br><span class="line"><span class="keyword">extension</span> <span class="title class_">TokenRank</span> &#123;</span><br><span class="line">    <span class="keyword">var</span> iconImage: <span class="type">NSImage</span>? &#123;</span><br><span class="line">        <span class="keyword">let</span> name <span class="operator">=</span> <span class="string">&quot;rank-<span class="subst">\(persistenceValue)</span>&quot;</span></span><br><span class="line">        <span class="keyword">guard</span> <span class="keyword">let</span> url <span class="operator">=</span> <span class="type">Bundle</span>.module.url(          <span class="comment">// ❌ 直接用了 Bundle.module</span></span><br><span class="line">            forResource: name, withExtension: <span class="string">&quot;png&quot;</span>, subdirectory: <span class="string">&quot;RankIcons&quot;</span></span><br><span class="line">        ) <span class="keyword">else</span> &#123; <span class="keyword">return</span> <span class="literal">nil</span> &#125;</span><br><span class="line">        <span class="keyword">return</span> <span class="type">NSImage</span>(contentsOf: url)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>修复只有一行：</p><figure class="highlight diff"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="deletion">-        guard let url = Bundle.module.url(</span></span><br><span class="line"><span class="addition">+        guard let url = AppResourceBundle.module.url(</span></span><br><span class="line">             forResource: name, withExtension: &quot;png&quot;, subdirectory: &quot;RankIcons&quot;</span><br><span class="line">         ) else &#123; return nil &#125;</span><br></pre></td></tr></table></figure><h2 id="为什么这种「只漏一处」的-bug-最难防"><a href="#为什么这种「只漏一处」的-bug-最难防" class="headerlink" title="为什么这种「只漏一处」的 bug 最难防"></a>为什么这种「只漏一处」的 bug 最难防</h2><p>这行代码有几个特别阴的地方，值得单独拎出来说：</p><p><strong>第一，<code>guard let ... else { return nil }</code> 给了你虚假的安全感。</strong> 这行写得很「防御」——找不到资源就返回 nil，调用方还有 SF Symbol 兜底。看起来无懈可击。但崩溃发生在 <code>Bundle.module</code> 这个表达式<strong>被求值的瞬间</strong>，压根轮不到 <code>.url(...)</code> 返回 nil。你的 <code>else</code> 分支再周全也拦不住，因为进程在进入 <code>guard</code> 之前就已经 <code>fatalError</code> 了。</p><p><img src="/images/swiftpm-bundle-module-crash-2.webp" alt="Bundle.module 是一枚 lazy 定时炸弹，menu bar、灵动卡、排名弹窗三条路都会引爆它"></p><p><strong>第二，它是 lazy 的定时炸弹。</strong> <code>Bundle.module</code> 是 <code>static let</code> + <code>dispatch_once</code>，只在<strong>第一次被访问</strong>时初始化。只要 app 有任何一条路径会渲染排名图标（menu bar 状态图标、灵动卡、排名弹窗都会），首启就必然触发。而恰恰这个 app 的核心功能就是「显示排名」，等于把炸弹放在了必经之路上。</p><p><strong>第三，编译器完全帮不上忙。</strong> <code>Bundle.module</code> 和 <code>AppResourceBundle.module</code> 类型一样、API 一样、调用方式一样，编译期没有任何区别。少写四个字符 <code>AppResource</code>，代码照样编译通过、开发机照样跑得欢，只在用户机上炸。这种「语义上错、语法上对、且只有一处」的 bug，靠 code review 用肉眼扫也极容易滑过去。</p><p>真正能防住它的其实只有一招：<strong>从源头上让「危险的那个 API」不可直接访问。</strong> 如果我能把 <code>Bundle.module</code> 的可见性收窄、逼所有资源加载都必须走 <code>AppResourceBundle</code>，这个 bug 从一开始就写不出来。可惜 SwiftPM 合成的 <code>Bundle.module</code> 是 internal 可见的，没法轻易禁掉——退而求其次，至少可以加一条 lint &#x2F; grep 规则，在 CI 里扫「除了 <code>AppResourceBundle.swift</code> 之外任何文件出现裸的 <code>Bundle.module</code>」直接 fail。</p><h2 id="要不要用-我的判断框架"><a href="#要不要用-我的判断框架" class="headerlink" title="要不要用 &#x2F; 我的判断框架"></a>要不要用 &#x2F; 我的判断框架</h2><p>这次踩坑给「纯 SPM 打 macOS app」这条路线补了几条经验，供同样在 vibe macOS app 的人参考：</p><ul><li><p><strong>值得上</strong>：纯 SPM + 脚本打包对小工具型 app 依然是划算的，省掉整个 Xcode 工程的心智负担。但你必须清楚地知道——<strong>一旦你手动组装 <code>.app</code>，SwiftPM 那套「资源就在可执行文件旁边」的假设就不成立了</strong>，所有 <code>Bundle.module</code> 都得换成按 <code>.app</code> 真实布局查找的封装。这不是可选优化，是必修课。</p></li><li><p><strong>重点关注</strong>：凡是「开发机好好的、用户机崩」的 bug，第一反应就去查<strong>运行环境差异</strong>——资源布局、签名方式、路径假设、权限。这次是资源包路径，上次是 codesign identity 漂移。它们的共同点是：你的开发环境「太顺了」，顺到掩盖了打包产物里的问题。<strong>能在装好的 <code>.app</code> 上做一次冷启动冒烟测试，胜过在 <code>.build/</code> 里跑一百次。</strong></p></li><li><p><strong>可以再等 &#x2F; 顺手做</strong>：给危险 API 加护栏这件事，值得排进 backlog。<code>fatalError</code> 类的「找不到就崩」API（<code>Bundle.module</code>、强解包、<code>try!</code>、数组越界下标）在常驻 app 里都是首启崩溃的高发点。与其靠自觉每次都记得用封装，不如让「裸用」在 CI 里直接编不过或过不了 lint——把「记得」变成「想漏都漏不掉」。</p></li></ul><p>一句话收尾：<strong>这个 bug 的技术根因五分钟就能讲清，但它能上线，靠的是「开发机永远复现不出」+「编译器不报错」+「八处只漏一处」三重巧合。防它的关键不在于更聪明地写这一行，而在于让这一行根本没机会写错。</strong></p>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/bce102b76701/</id>
    <link href="https://blog.zisheng.pro/posts/bce102b76701/"/>
    <published>2026-07-10T14:30:00.000Z</published>
    <summary>这篇文章复盘我在 Owlet 上遇到的一个「用户第一次装、一打开就崩」的 bug。两台不同机器、不同用户、不同 macOS 版本，crash log 一模一样，崩溃点是 SwiftPM 合成的 Bundle.module 在触发 _assertionFailure。根因是纯 SPM 的 macOS app 手动组装成 .app 之后，Swift Package Manager 默认生成的 Bundle.module 资源定位逻辑找不到资源包，会直接 fatalError；而我在 8 处加载资源里，唯独一处漏用了自己写的兜底封装。文章从 crash log 反推根因，讲清楚 Bundle.module 为什么在开发时不崩、用户首装必崩，以及为什么这种「只有一处漏网」的 bug 最难在本地复现。</summary>
    <title>打包后 Bundle.module 直接崩：一个 SPM app 首启必崩的资源陷阱</title>
    <updated>2026-09-18T08:15:28.640Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="AI" scheme="https://blog.zisheng.pro/categories/AI/"/>
    <category term="Agent" scheme="https://blog.zisheng.pro/tags/Agent/"/>
    <category term="macOS" scheme="https://blog.zisheng.pro/tags/macOS/"/>
    <category term="Owlet" scheme="https://blog.zisheng.pro/tags/Owlet/"/>
    <category term="Swift" scheme="https://blog.zisheng.pro/tags/Swift/"/>
    <category term="并发" scheme="https://blog.zisheng.pro/tags/%E5%B9%B6%E5%8F%91/"/>
    <content>
      <![CDATA[<p>Owlet 是我用 Claude Code + Codex vibe 出来的一个常驻 menu-bar app，用来统计每天 Claude Code 和 Codex 的 Token 用量、排名、实时会话活动。常驻类 app 最怕两件事：一是空闲耗电，二是莫名其妙崩。这次一天之内，我连着修了两个崩溃——而且它们像俄罗斯套娃：修好外面那个，里面那个才露出来。</p><p>第一个崩得相当有迷惑性——崩溃栈指向的那一行代码本身没有任何问题。真正的凶手，是一个我亲手写下 <code>@unchecked Sendable</code>、然后就再也没管过它线程安全的普通 <code>Dictionary</code>。第二个则相反，是个藏在一行 <code>0</code> 里、只要用起来就必崩的确定性 bug。两个坑的共同点是：它们都利用了 Swift 里某个”看起来无害”的默认行为。</p><p><img src="/images/swift-unchecked-sendable-dictionary-crash.webp" alt="Swift 并发字典崩溃复盘"></p><h2 id="一句话总结"><a href="#一句话总结" class="headerlink" title="一句话总结"></a>一句话总结</h2><p><code>@unchecked Sendable</code> 不是”让这个类型线程安全”，而是”我向编译器保证它已经安全了，你别查了”。如果你写下它却没做任何同步，普通 <code>Dictionary</code> 在并发写下会直接损坏底层存储、破坏整个进程的堆内存，崩溃点还会飘到无辜的其他线程上。</p><h2 id="崩溃现场：两个线程站在同一个栈上"><a href="#崩溃现场：两个线程站在同一个栈上" class="headerlink" title="崩溃现场：两个线程站在同一个栈上"></a>崩溃现场：两个线程站在同一个栈上</h2><p>先看崩溃日志最关键的部分。信号是 <code>EXC_BAD_ACCESS (SIGSEGV)</code>，访问了一个明显非法的地址 <code>0x8000000000000028</code>：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">Thread 4 Crashed:</span><br><span class="line">0   libswiftCore.dylib   __RawDictionaryStorage.find&lt;A&gt;(_:) + 36</span><br><span class="line">1   libswiftCore.dylib   _NativeDictionary.setValue(_:forKey:isUnique:) + 192</span><br><span class="line">2   libswiftCore.dylib   Dictionary._Variant.setValue(_:forKey:) + 196</span><br><span class="line">3   libswiftCore.dylib   Dictionary.subscript.setter + 604</span><br><span class="line">4   Owlet                ClaudeCodeAdapter.discoverSessions() (ClaudeCodeAdapter.swift:200)</span><br></pre></td></tr></table></figure><p>崩在 <code>__RawDictionaryStorage.find</code> &#x2F; <code>setValue</code>——也就是往字典里写一个 key 的时候，字典的内部哈希存储已经是坏的了。这不是”key 不存在”这种业务问题，是底层内存结构被破坏。</p><p>再往下看，会发现一个决定性的线索：<strong>另一个线程也停在同一个函数里。</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">Thread 2:</span><br><span class="line">5   Owlet   ClaudeCodeAdapter.discoverSessions() (ClaudeCodeAdapter.swift:199)</span><br></pre></td></tr></table></figure><p>两个后台线程，同一时刻，都在执行 <code>discoverSessions()</code>，都在读写同一个 <code>Dictionary</code>。一个在 199 行读，一个在 200 行写。到这里，数据竞争（data race）已经基本实锤了。</p><p>还有一个更迷惑人的细节：崩溃报告里第三个线程停在完全不相干的地方——</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">Thread 7 (GRDB.DatabasePool.reader):</span><br><span class="line">   key path getter for Character.isNewline</span><br><span class="line">   Collection.split(whereSeparator:)</span><br><span class="line">   CodexDataReader.rolloutTokenUsageEvents(path:)</span><br></pre></td></tr></table></figure><p><code>CodexDataReader</code> 是读 Codex 数据的另一套代码，跟 Claude 的 adapter 八竿子打不着，凭什么它也崩？</p><p>答案是：<strong>堆内存被写坏之后，整个进程都在雷区里跑。</strong> Thread 4 把字典的底层存储写坏，破坏的是共享的堆。此后任何线程在任何位置做一次内存访问，都可能踩到那片被污染的区域，然后随机崩溃。所以排查并发内存崩溃时，有一个重要原则:</p><blockquote><p>崩溃日志里”触发信号的那个线程”（<code>triggered: true</code>）才是凶手，其他线程往往只是受害者。</p></blockquote><p>如果你盯着 Thread 7 的 <code>CodexDataReader</code> 去查，会彻底跑偏。</p><h2 id="unchecked-Sendable：一句你亲手写下的空头承诺"><a href="#unchecked-Sendable：一句你亲手写下的空头承诺" class="headerlink" title="@unchecked Sendable：一句你亲手写下的空头承诺"></a>@unchecked Sendable：一句你亲手写下的空头承诺</h2><p>顺着凶手 <code>ClaudeCodeAdapter</code> 看类型声明：</p><figure class="highlight swift"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">final</span> <span class="keyword">class</span> <span class="title class_">ClaudeCodeAdapter</span>: <span class="title class_">AgentAdapter</span>, @<span class="title class_">unchecked</span> <span class="title class_">Sendable</span> &#123;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">var</span> sessionEntryCache: [<span class="type">String</span>: <span class="type">CachedSessionEntry</span>] <span class="operator">=</span> [:]</span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>问题全在这两行里。</p><p><code>Sendable</code> 是 Swift 并发模型里的一个协议，意思是”这个类型的值可以安全地跨并发域传递”。对于 <code>class</code> 这种引用类型，编译器默认不认为它是 <code>Sendable</code>——因为多个线程可能同时持有同一个引用、改同一块状态。</p><p>而 <code>@unchecked Sendable</code> 是一个逃生舱：它告诉编译器”这个类型我人工审计过了，线程安全我自己保证，你不用检查”。它常用于两种情况：一种是内部用了锁&#x2F;串行队列做同步，只是编译器看不出来；另一种是……写的人当时觉得”应该没事吧”。</p><p>我这次就是第二种。<code>sessionEntryCache</code> 是一个普通的 <code>Dictionary</code>，没有锁、没有串行队列、没有 actor，什么保护都没有。<code>@unchecked Sendable</code> 关掉了编译器的并发检查，于是这个隐患一路绿灯上了线。</p><p>这里要记住一件事：<strong><code>@unchecked Sendable</code> 不提供任何运行时保护，它只是关掉编译期的报错。</strong> 你写下它的那一刻，线程安全就从”编译器帮你保证”变成了”你自己口头保证”。</p><p>那为什么会有两个线程并发进来？看调用方：</p><figure class="highlight swift"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// AppState.refresh() 里</span></span><br><span class="line"><span class="keyword">_</span> <span class="operator">=</span> <span class="keyword">try</span> <span class="keyword">await</span> <span class="type">Task</span>.detached(priority: .utility) &#123;</span><br><span class="line">    <span class="keyword">for</span> runtime <span class="keyword">in</span> runtimesToImport &#123;</span><br><span class="line">        imported <span class="operator">+=</span> <span class="keyword">try</span> <span class="keyword">await</span> capturedIngestion.importSessions(from: runtime.adapter)</span><br><span class="line">        <span class="comment">// -&gt; 最终调用 adapter.discoverSessions()</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;.value</span><br></pre></td></tr></table></figure><p><code>refresh()</code> 会在多个时机被触发——app 启动 bootstrap、文件监听 debounce 后的自动刷新、手动刷新。每次触发都开一个 <code>Task.detached</code> 跑 import。当两次刷新时间上挨得很近（比如启动时 bootstrap 和文件监听几乎同时发生），两个 detached task 就并发进入了同一个 adapter 实例的 <code>discoverSessions()</code>，一起写那个没有任何保护的字典。</p><h2 id="为什么不能用细粒度锁：read-modify-write-的原子性"><a href="#为什么不能用细粒度锁：read-modify-write-的原子性" class="headerlink" title="为什么不能用细粒度锁：read-modify-write 的原子性"></a>为什么不能用细粒度锁：read-modify-write 的原子性</h2><p>找到根因，很多人第一反应是”给字典加个锁不就行了”。但加在哪、加多粗，是这次真正值得记下来的点。</p><p>先看 <code>discoverSessions()</code> 里对 cache 的访问模式：</p><figure class="highlight swift"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> <span class="keyword">let</span> entry <span class="operator">=</span> sessionEntryCache[cacheKey] &#123;        <span class="comment">// ① 读旧 entry</span></span><br><span class="line">    <span class="keyword">if</span> fileSize <span class="operator">&gt;</span> entry.fileSize &#123;</span><br><span class="line">        <span class="comment">// 只读新增的字节，从 entry.offset 处 seek</span></span><br><span class="line">        parseIncremental(file, entry: entry, <span class="operator">...</span>)    <span class="comment">// ② 用 entry.offset seek 文件</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">sessionEntryCache[cacheKey] <span class="operator">=</span> newEntry               <span class="comment">// ③ 写回带新 offset 的 entry</span></span><br></pre></td></tr></table></figure><p>这是一个典型的 <strong>read-modify-write</strong>：读出旧的 <code>entry</code>（里面记着”上次读到文件的哪个字节偏移”），基于这个 offset 只读文件新追加的部分（增量解析，避免每次全量重读几十 MB），再把新的 offset 写回去。</p><p>如果只给字典的每次下标访问加一把细粒度锁（读一次锁一次、写一次锁一次），会发生什么？</p><blockquote><p>线程 A 读到 offset&#x3D;1000，线程 B 也读到 offset&#x3D;1000。两个线程都从 1000 开始读文件、各自解析、各自把”新 offset”写回。其中一个的结果被覆盖，offset 错乱，同一段 Token 用量被重复计入。</p></blockquote><p>字典本身不崩了，但数据错了。细粒度锁保护了”每一次字典操作”的原子性，却没保护”读—改—写”这个序列整体的原子性。这是并发编程里最容易栽的一类坑：<strong>锁的粒度对齐的应该是不变量（invariant），而不是单个操作。</strong></p><p>这里的不变量是”cache 里的 offset 必须和已经解析进去的字节严格对应”。要维持它，整个 <code>discoverSessions()</code> pass 必须串行执行。所以正确的修法是一把粗粒度锁，锁住整趟：</p><figure class="highlight swift"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">let</span> discoverLock <span class="operator">=</span> <span class="type">NSLock</span>()</span><br><span class="line"></span><br><span class="line"><span class="keyword">func</span> <span class="title function_">discoverSessions</span>() <span class="keyword">async</span> <span class="keyword">throws</span> -&gt; [<span class="type">AgentSessionDraft</span>] &#123;</span><br><span class="line">    discoverLock.withLock &#123; discoverSessionsLocked() &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">private</span> <span class="keyword">func</span> <span class="title function_">discoverSessionsLocked</span>() -&gt; [<span class="type">AgentSessionDraft</span>] &#123;</span><br><span class="line">    <span class="comment">// 原来那一整段读目录、读文件、增量解析、写 cache 的逻辑</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>有人会担心：持锁期间还在做磁盘 I&#x2F;O，第二个并发调用不就干等着吗？会。但这恰恰是最优解——第二个调用等第一个做完之后拿到锁，此时 cache 已经是热的，绝大多数文件的 <code>mtime</code> &#x2F; <code>fileSize</code> 都没变，直接命中缓存、零 I&#x2F;O 秒回。串行化在这里几乎没有额外代价，反而消除了两个 task 各跑一遍的重复劳动。</p><h2 id="Swift-6-的暗礁：async-上下文里不能-lock"><a href="#Swift-6-的暗礁：async-上下文里不能-lock" class="headerlink" title="Swift 6 的暗礁：async 上下文里不能 lock()"></a>Swift 6 的暗礁：async 上下文里不能 lock()</h2><p>按上面思路，我一开始是直接在 <code>discoverSessions()</code> 里写的：</p><figure class="highlight swift"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">func</span> <span class="title function_">discoverSessions</span>() <span class="keyword">async</span> <span class="keyword">throws</span> -&gt; [<span class="type">AgentSessionDraft</span>] &#123;</span><br><span class="line">    discoverLock.lock()</span><br><span class="line">    <span class="keyword">defer</span> &#123; discoverLock.unlock() &#125;</span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>编译能过，但�报了个 warning，而且明说了在 Swift 6 语言模式下会直接变成 error：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">warning: instance method &#x27;lock&#x27; is unavailable from asynchronous contexts;</span><br><span class="line">Use async-safe scoped locking instead;</span><br><span class="line">this is an error in the Swift 6 language mode</span><br></pre></td></tr></table></figure><p>原因很本质：<code>async</code> 函数可能在任意 <code>await</code> 处挂起，挂起之后恢复执行的线程<strong>不一定是原来那个线程</strong>。而 <code>NSLock</code> 是要求”谁上锁谁解锁、同一线程”的。如果你在 <code>lock()</code> 和 <code>unlock()</code> 之间跨了一个 <code>await</code>，挂起前后换了线程，就会变成”A 线程上锁、B 线程解锁”，直接是未定义行为。</p><p>编译器无法证明你两者之间没有 <code>await</code>，于是干脆禁止在 async 上下文里裸用 <code>lock()</code>&#x2F;<code>unlock()</code>。</p><p>正解是用 scoped 的 <code>withLock { }</code>：把需要保护的逻辑抽成一个<strong>同步</strong>函数（我这里是 <code>discoverSessionsLocked()</code>），闭包体内不可能出现 <code>await</code>，锁的获取和释放必然在同一个同步执行段里完成。这也顺便逼你把”要保护的临界区”想清楚、圈干净，而不是散着写一堆 lock&#x2F;unlock。</p><h2 id="加餐：同一天的第二个崩溃，藏在一行-0-里"><a href="#加餐：同一天的第二个崩溃，藏在一行-0-里" class="headerlink" title="加餐：同一天的第二个崩溃，藏在一行 0 里"></a>加餐：同一天的第二个崩溃，藏在一行 <code>0</code> 里</h2><p>修完并发那个崩溃、重新打包上线，没过多久又收到一份新的崩溃日志。这次信号不一样，是 <code>EXC_BREAKPOINT (SIGTRAP)</code>——Swift 运行时主动触发的陷阱，通常意味着数组或 <code>Data</code> 越界。</p><p>崩溃点在一个监听 JSONL 会话文件、检测”用户中断”的后台组件里：</p><figure class="highlight swift"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">func</span> <span class="title function_">scanBufferForInterrupt</span>() &#123;</span><br><span class="line">    <span class="keyword">var</span> scanStart <span class="operator">=</span> <span class="number">0</span></span><br><span class="line">    <span class="keyword">while</span> scanStart <span class="operator">&lt;</span> leftoverData.count &#123;</span><br><span class="line">        <span class="keyword">guard</span> <span class="keyword">let</span> index <span class="operator">=</span> leftoverData[scanStart<span class="operator">...</span>].firstIndex(of: newline) <span class="keyword">else</span> &#123; <span class="keyword">break</span> &#125;</span><br><span class="line">        <span class="comment">// ... 处理一行 ...</span></span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">if</span> consumed <span class="operator">&gt;</span> <span class="number">0</span> &#123;</span><br><span class="line">        leftoverData.removeFirst(consumed)   <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>leftoverData</code> 是一个不断追加、扫描完就把已处理部分 <code>removeFirst</code> 掉的 <code>Data</code> buffer。看起来天经地义。但它踩中了 <code>Data</code> 一个反直觉的设定：</p><blockquote><p><strong>Swift <code>Data</code> 的 index 是绝对的。<code>removeFirst(n)</code> 之后，它的 <code>startIndex</code> 会前移到 n，而不是回到 0。</strong></p></blockquote><p>第一次扫描时 <code>startIndex == 0</code>，<code>leftoverData[0...]</code> 一切正常。扫完执行 <code>removeFirst(consumed)</code>，<code>startIndex</code> 变成了 consumed（比如 87）。等文件第二次增长、再进来扫描时，<code>scanStart</code> 还是硬编码的 <code>0</code>，而 <code>leftoverData[0...]</code> 里的这个 <code>0</code> 已经落在 <code>startIndex</code> 之前——越界，SIGTRAP。</p><p>这不是偶发的并发时序问题，是个<strong>确定性 bug</strong>：只要有一个 Claude 会话在跑、它的 JSONL 被追加写入两次，就必崩。之所以之前一直没暴露，是因为上一个并发崩溃发生得更早，先把进程带走了；修掉前一个，这个就顶到了最前面。</p><p>修复本身很简单，但记性要长——别用字面量 <code>0</code> 当 <code>Data</code> 的索引，一切从 <code>startIndex</code> 起：</p><figure class="highlight swift"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> lineStart <span class="operator">=</span> leftoverData.startIndex</span><br><span class="line"><span class="keyword">while</span> lineStart <span class="operator">&lt;</span> leftoverData.endIndex &#123;</span><br><span class="line">    <span class="keyword">guard</span> <span class="keyword">let</span> newlineIndex <span class="operator">=</span> leftoverData[lineStart<span class="operator">...</span>].firstIndex(of: newline) <span class="keyword">else</span> &#123; <span class="keyword">break</span> &#125;</span><br><span class="line">    <span class="keyword">let</span> lineData <span class="operator">=</span> leftoverData.subdata(in: lineStart<span class="operator">..&lt;</span>newlineIndex)</span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">    lineStart <span class="operator">=</span> leftoverData.index(after: newlineIndex)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>Collection</code> 的下标是绝对索引，这件事在 <code>Array</code> 上很少咬到你——因为 <code>Array</code> 的 index 永远从 0 开始。但 <code>Data</code>、<code>ArraySlice</code>、<code>Substring</code> 这些”可能是别人一部分”的类型，<code>startIndex</code> 随时可能不是 0。<strong>凡是切片类型，就别信 0。</strong></p><h2 id="我的判断框架：actor-锁-unchecked-Sendable-怎么选"><a href="#我的判断框架：actor-锁-unchecked-Sendable-怎么选" class="headerlink" title="我的判断框架：actor &#x2F; 锁 &#x2F; @unchecked Sendable 怎么选"></a>我的判断框架：actor &#x2F; 锁 &#x2F; @unchecked Sendable 怎么选</h2><p>这次踩坑之后，我给自己定了一套在 Swift 并发里选同步手段的判断顺序：</p><table><thead><tr><th>场景</th><th>首选</th><th>理由</th></tr></thead><tbody><tr><td>新写的、可以 async 化的共享可变状态</td><td><code>actor</code></td><td>编译器强制隔离，从根上杜绝数据竞争，不用自己管锁</td></tr><tr><td>已有同步 API、不方便全改 async，临界区清晰</td><td><code>NSLock</code> + <code>withLock</code></td><td>改动最小，粗粒度锁住不变量即可</td></tr><tr><td>高频只读、极少写</td><td>读写锁 &#x2F; 原子类型</td><td>读不互斥，避免锁竞争成为热点</td></tr><tr><td>确实由底层机制保证了安全</td><td><code>@unchecked Sendable</code> + <strong>注释写清凭什么安全</strong></td><td>逃生舱，但必须留下审计依据</td></tr></tbody></table><p>几条给自己的硬规矩：</p><ul><li><strong>看到 <code>@unchecked Sendable</code>，就当成一个待验证的 TODO。</strong> 它旁边必须有一句注释说明”靠什么保证线程安全”（哪把锁、哪个串行队列、哪个 actor）。说不出来，就是隐患。</li><li><strong>锁的粒度对齐不变量，不对齐单个操作。</strong> 遇到 read-modify-write，先问自己”这三步之间的中间状态能不能被别人看到”，不能就整段串行。</li><li><strong>排查并发内存崩溃，先认准 <code>triggered</code> 线程。</strong> 堆一旦被写坏，崩溃点会飘，别被无辜的受害线程带偏。</li><li><strong>async 里要锁，用 <code>withLock</code>，别用裸 <code>lock()</code>。</strong> 顺手把临界区抽成同步函数，思路和编译器都会更清楚。</li><li><strong>切片类型的索引从 <code>startIndex</code> 起，别信字面量 <code>0</code>。</strong> <code>Data</code>、<code>ArraySlice</code>、<code>Substring</code> 的 <code>startIndex</code> 随时可能不是 0，尤其在 <code>removeFirst</code> &#x2F; 切片之后。遍历用原生 index，不用 <code>Int</code> 硬编码。</li></ul><p><code>@unchecked Sendable</code> 这类逃生舱不是不能用——<code>actor</code> 化整个 adapter 也要改协议、改一堆调用点，工程上未必划算。真正的问题是：我当初写下它的时候,把”我保证安全”当成了”它已经安全”。这两者之间隔着的，正是一次会飘到无辜线程上的堆内存崩溃。</p>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/8215610282bf/</id>
    <link href="https://blog.zisheng.pro/posts/8215610282bf/"/>
    <published>2026-07-10T13:30:00.000Z</published>
    <summary>这篇文章复盘我在 Owlet 里一天内连修的两次 Swift 崩溃。第一次是并发数据竞争：一个被 @unchecked Sendable 骗过编译器的普通 Dictionary，在两个并发 import 里被同时写坏内存，崩溃点还飘到毫不相干的第三个线程上。第二次是确定性的 Data 下标越界：removeFirst 之后 Data 的 startIndex 不归零，用字面量 0 当索引直接越界 SIGTRAP。文章从崩溃日志反推根因，讲清楚为什么细粒度锁不够、read-modify-write 为什么必须整段串行、Swift 6 里 async 上下文为什么不让你裸用 NSLock.lock()，以及切片类型的索引为什么不能信 0。</summary>
    <title>@unchecked Sendable 骗过了编译器：一次 Swift 并发字典崩溃复盘</title>
    <updated>2026-09-18T08:15:28.636Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="AI" scheme="https://blog.zisheng.pro/categories/AI/"/>
    <category term="LLM" scheme="https://blog.zisheng.pro/tags/LLM/"/>
    <category term="Agent" scheme="https://blog.zisheng.pro/tags/Agent/"/>
    <category term="Claude Code" scheme="https://blog.zisheng.pro/tags/Claude-Code/"/>
    <category term="Context Engineering" scheme="https://blog.zisheng.pro/tags/Context-Engineering/"/>
    <category term="知识管理" scheme="https://blog.zisheng.pro/tags/%E7%9F%A5%E8%AF%86%E7%AE%A1%E7%90%86/"/>
    <category term="vibe coding" scheme="https://blog.zisheng.pro/tags/vibe-coding/"/>
    <content>
      <![CDATA[<p>今天我干了三件看起来不相干的小事：把”怎么把一张图做成 macOS 应用图标”写进了一个 Claude Code skill，把”博客每篇至少配 3 张插图”写进了全局 <code>CLAUDE.md</code>，还顺手把一次踩坑记成了博客。</p><p>写到第三件的时候我突然停下来：这三件事，本质上是同一件事——<strong>我在把”怎么做”这件知识，沉淀成 AI 下次能直接读、直接用的形式。</strong></p><p>换句话说，我在给自己建一个 wiki。只不过这个 wiki 的读者不是人，是 LLM。</p><p><img src="/images/llm-wiki.webp" alt="LLM wiki 主视觉"></p><h2 id="一句话总结"><a href="#一句话总结" class="headerlink" title="一句话总结"></a>一句话总结</h2><p><strong>传统 wiki 是写给人查的，LLM wiki 是写给模型执行的。</strong> 前者追求”读得懂”，后者追求”能复用”——要可执行、有触发条件、能被真实结果校验。当你开始有意识地为 AI 沉淀知识，你的个人生产力会从”一次性完成任务”变成”每次完成都让下一次更快”。</p><h2 id="我是怎么撞见这个想法的"><a href="#我是怎么撞见这个想法的" class="headerlink" title="我是怎么撞见这个想法的"></a>我是怎么撞见这个想法的</h2><p>起因很具体。我在 vibe coding 一个 macOS menu-bar 应用，需要把一张 AI 生成的猫头鹰图变成真正的 app 图标。这中间有一堆非显而易见的坑：macOS 不会帮你自动圆角、AI 出的图常常自带一圈 padding 和阴影、直接用会变成”方块里套圆角块”、squircle 的圆角半径大概是边长的 0.2237 倍……</p><p>我踩完这些坑、跑通整条管线之后，做了一个选择：<strong>不是把结果留在这次对话里，而是把方法写进 skill。</strong></p><p>于是下次我（或者任何一个读这个 skill 的 AI）再遇到”把图做成 mac 图标”，不用重新踩一遍——自动裁 padding、套 squircle、生成 iconset、打包 <code>.icns</code>，一条龙。</p><p>那一刻我意识到：我做的不是”完成一个任务”，是”给未来的 AI 写了一页可执行的文档”。这页文档，就是 LLM wiki 的一条词条。</p><h2 id="LLM-wiki-的三种载体"><a href="#LLM-wiki-的三种载体" class="headerlink" title="LLM wiki 的三种载体"></a>LLM wiki 的三种载体</h2><p>我目前的知识沉淀，落在三个地方，各有分工：</p><p><img src="/images/llm-wiki-1.webp" alt="三种沉淀载体：规则、技能、文章"></p><table><thead><tr><th>载体</th><th>存什么</th><th>何时被读</th><th>类比</th></tr></thead><tbody><tr><td><code>CLAUDE.md</code> &#x2F; <code>AGENTS.md</code></td><td>全局约束、偏好、红线</td><td>每次会话自动加载</td><td>wiki 的”编辑守则”</td></tr><tr><td>Claude Code skill</td><td>可执行的方法论、管线</td><td>命中触发词时按需加载</td><td>wiki 的”操作手册词条”</td></tr><tr><td>博客文章</td><td>判断框架、踩坑复盘、观点</td><td>我或别人主动检索</td><td>wiki 的”深度专题页”</td></tr></tbody></table><p><strong><code>CLAUDE.md</code> 是常驻规则。</strong> 它每次都进上下文，所以只放最高优先级、最稳定的东西：比如”博客每篇至少 3 张插图””性能规则不能回退””发布流程怎么走”。它贵，所以要克制——不该把操作细节全塞进来。</p><p><strong>skill 是按需加载的操作手册。</strong> 它平时不占上下文，只有当任务命中触发词（”做 mac 应用图标”、”.icns”）才被拉进来。这意味着 skill 可以写得很详细、很长，因为它不是常驻成本。我那条 macOS 图标管线就适合放这儿。</p><p><strong>博客是给人也给 AI 的深度页。</strong> 它存的是”为什么这么做”的判断，而不只是”怎么做”的步骤。而且它是公开的——别人的 AI 也能检索到、学到。这层是前两层的”公共版”。</p><h2 id="LLM-wiki-和传统-wiki-差在哪"><a href="#LLM-wiki-和传统-wiki-差在哪" class="headerlink" title="LLM wiki 和传统 wiki 差在哪"></a>LLM wiki 和传统 wiki 差在哪</h2><p>关键区别只有一个：<strong>读者变了。</strong></p><p>传统 wiki 写给人，人会脑补、会容错、会举一反三。所以传统文档可以模糊——“把图标处理一下”，人能懂。</p><p>LLM wiki 写给模型，你得假设读者<strong>不会脑补、只会照做</strong>。所以三条硬要求：</p><ol><li><strong>可执行，不是描述。</strong> 别写”记得处理透明背景”，直接给能跑的脚本 + 参数 + 兜底分支。我那条图标 skill 里直接嵌了一段 Python，模型抽出来就能运行。</li><li><strong>有触发条件。</strong> 人会自己判断该翻哪一页；模型需要你明确写”什么时候用我”。skill 的 description 和触发词就是干这个的——写不好，该被加载时它不出现。</li><li><strong>能被 ground truth 校验。</strong> 这是我今天最深的一课。</li></ol><p><img src="/images/llm-wiki-2.webp" alt="Ground truth：以真实结果为准"></p><h2 id="最痛的一课：不要相信”我以为做了”"><a href="#最痛的一课：不要相信”我以为做了”" class="headerlink" title="最痛的一课：不要相信”我以为做了”"></a>最痛的一课：不要相信”我以为做了”</h2><p>今天我在提交一个 skill 改动时，中间的工具输出显示”已提交、已推送”，我就信了、往下走了。后来用 <code>git rev-parse HEAD</code> 和 <code>git show</code> 一核对——<strong>根本没提交成功，那几步是我基于未经核验的中间状态脑补出来的。</strong></p><p>这件事放在 LLM wiki 的语境里特别有意思：<strong>模型（包括我这个 AI）会基于”我以为的状态”往前跑，而不是”真实的状态”。</strong> 所以一条好的 LLM wiki 词条，必须内建”用真值校验”这一步。</p><p>我后来把这条也固化了：涉及提交推送，一律以 <code>git rev-parse</code>、<code>git status</code>、<code>git show</code> 的真实输出为准，绝不用中间回显下结论。这就是一条为 AI 写的”自我校验规则”。</p><p>传统 wiki 不需要教人”提交完要确认真的提交了”——人有直觉。但给 AI 写的 wiki 需要，因为模型的失败模式和人不一样：它不是不会，是会一本正经地跑偏。</p><h2 id="怎么让沉淀真的被复用"><a href="#怎么让沉淀真的被复用" class="headerlink" title="怎么让沉淀真的被复用"></a>怎么让沉淀真的被复用</h2><p>沉淀不等于复用。我踩过的坑：</p><ul><li><strong>写了没触发词 &#x3D; 白写。</strong> skill 描述里不写”mac 应用图标 &#x2F; .icns”，下次问”帮我做个应用图标”时它根本不会被加载。触发词是 LLM wiki 的”索引”。</li><li><strong>写太细放错层 &#x3D; 变负担。</strong> 把一次性的操作细节塞进 <code>CLAUDE.md</code>，每次会话都要为它付上下文成本。分层很重要：常驻的极简、按需的详尽。</li><li><strong>只存结果不存判断 &#x3D; 不可迁移。</strong> “这次用了方案 A”没价值；”因为 X 约束所以选 A、代价是 Y”才能迁移到下一个场景。这也是为什么博客这层不可替代——它存的是判断框架。</li></ul><h2 id="要不要这么干-我的判断框架"><a href="#要不要这么干-我的判断框架" class="headerlink" title="要不要这么干 &#x2F; 我的判断框架"></a>要不要这么干 &#x2F; 我的判断框架</h2><table><thead><tr><th>场景</th><th>判断</th></tr></thead><tbody><tr><td>你反复让 AI 做同类任务（生图、部署、某种重构）</td><td><strong>值得上</strong>：沉淀成 skill，一次写、无限复用</td></tr><tr><td>你有稳定的个人偏好和红线</td><td><strong>值得上</strong>：写进 <code>CLAUDE.md</code>，别每次重复交代</td></tr><tr><td>一次性的、不会再遇到的任务</td><td><strong>可以再等</strong>：沉淀成本大于收益</td></tr><tr><td>你享受”每次做完都让下次更快”的复利感</td><td><strong>值得上</strong></td></tr><tr><td>你还在纯手动、每次从零跟 AI 解释背景</td><td><strong>重点关注</strong>：你在重复付一笔本可以免掉的税</td></tr></tbody></table><p>我越来越确信一件事：在 Agent &#x2F; AI Infra 时代，超级个体的护城河不是”会用 AI”，而是<strong>有没有为 AI 持续建一个高质量的知识库</strong>。工具谁都能用，但你喂给它的上下文、你沉淀的判断框架，是别人复制不走的。</p><p>判断框架负责决定”做什么、不做什么、底线在哪”，AI 负责把执行力拉满。而 LLM wiki，就是把你的判断框架，翻译成 AI 每次都能读到的形式。</p><p>写到这我就明白了：这篇博客本身，也是我 LLM wiki 里的一页。它遵守我今天刚写进 <code>CLAUDE.md</code> 的规则——至少配 3 张插图。</p><p>挺自洽的，也挺酷的。</p>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/b612b935f3f6/</id>
    <link href="https://blog.zisheng.pro/posts/b612b935f3f6/"/>
    <published>2026-07-10T02:30:00.000Z</published>
    <summary>这篇文章想聊一个我最近才想明白的事：当我把&quot;怎么做一件事&quot;沉淀进 CLAUDE.md 规则、Claude Code skill、还有博客文章时，我其实是在为 AI 建一个知识库——一个 LLM wiki。它和传统给人看的 wiki 不一样：读者是 LLM，所以要可执行、有触发词、能被 ground truth 校验。文章结合我这半年从前端转 Agent 的实践，讲清这三种沉淀载体各自的定位，以及怎么写才能真正被复用。</summary>
    <title>我在给自己建一个 LLM wiki</title>
    <updated>2026-09-18T08:15:28.640Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="AI" scheme="https://blog.zisheng.pro/categories/AI/"/>
    <category term="Agent" scheme="https://blog.zisheng.pro/tags/Agent/"/>
    <category term="Claude Code" scheme="https://blog.zisheng.pro/tags/Claude-Code/"/>
    <category term="Codex" scheme="https://blog.zisheng.pro/tags/Codex/"/>
    <category term="macOS" scheme="https://blog.zisheng.pro/tags/macOS/"/>
    <category term="Owlet" scheme="https://blog.zisheng.pro/tags/Owlet/"/>
    <category term="Swift" scheme="https://blog.zisheng.pro/tags/Swift/"/>
    <category term="vibe coding" scheme="https://blog.zisheng.pro/tags/vibe-coding/"/>
    <content>
      <![CDATA[<p>过去一个月，我的 Mac 菜单栏里多了一只小猫头鹰。</p><p>它叫 Owlet，是我用 Claude Code 和 Codex 一起 vibe 出来的 macOS 小工具：常驻菜单栏，实时显示 Claude Code 和 Codex 的 token 消耗，附带一个贴合刘海的灵动岛面板、一个桌面灵动卡、还有一个看历史用量和排名的 dashboard。</p><p>做这个 app 的动机很私人。今年我开始从前端往 Agent &#x2F; AI Infra 方向转，每天大部分时间都在跟 Claude Code 和 Codex 泡在一起。泡着泡着就产生了一个很朴素的疑问：<strong>我每天到底烧了多少 token？钱花哪儿了？哪个项目最费钱？</strong> 现有的工具都不直接回答这个问题，那我就自己做一个。</p><p>于是就有了 Owlet。</p><p><img src="/images/owlet-vibe-coding-cover.webp" alt="Owlet 主视觉"></p><h2 id="一句话总结"><a href="#一句话总结" class="headerlink" title="一句话总结"></a>一句话总结</h2><p><strong>Claude Code 负责架构、长上下文和需要判断力的重构；Codex 负责快速试错和小范围实现；我负责拍板、验收和把家乡青山湖的颜色塞进 UI。</strong> 三者分工明确时，vibe coding 真的能把一个 idea 从 0 推到能用。</p><h2 id="为什么是一只猫头鹰"><a href="#为什么是一只猫头鹰" class="headerlink" title="为什么是一只猫头鹰"></a>为什么是一只猫头鹰</h2><p>Owlet 的英文意思是“小猫头鹰”。图标是一只像素风猫头鹰，缩在菜单栏里，眼睛一眨一眨地观察你的 AI 会话。</p><p>选它有两个原因。一是气质贴合：这个 app 做的就是“盯着你用 AI 的过程”，猫头鹰那种夜间警觉、悄无声息的感觉，比什么“机器人”“仪表盘”都更对味。二是可爱——macOS 菜单栏天天见的东西，不能太硬核，得让人愿意让它一直待着。</p><p>定下名字那晚我对 Claude 说：</p><blockquote><p>“根据现在帧动画里猫头鹰 icon 的形象重新命名 app。”</p></blockquote><p>最后从几个候选里挑了 Owlet。短小、好读、不容易撞名，而且一听就知道跟 owl 有关。</p><p>这就是独立开发的好处：审美和命名这种在别人公司要开三天会的决定，自己一个人一晚上就能拍板。</p><h2 id="Claude-Code-是架构师，Codex-是快枪手"><a href="#Claude-Code-是架构师，Codex-是快枪手" class="headerlink" title="Claude Code 是架构师，Codex 是快枪手"></a>Claude Code 是架构师，Codex 是快枪手</h2><p><img src="/images/owlet-vibe-coding-workbench.webp" alt="Claude Code + Codex 双开工作台"></p><p>这个项目里我同时开了两条 AI 线：Claude Code 和 Codex。</p><p><strong>Claude Code 是我的主架构师。</strong> 它的长上下文能力太适合这种代码量不大但跨模块关联多的项目。比如设计实时 session 监控链路时，我需要同时理解：</p><ul><li>Claude Code 的 hook 机制（<code>owlet-hook.py</code> 怎么装、怎么往 Unix socket 发事件）</li><li>macOS 上 <code>NSPanel</code> 和 SwiftUI 的边界行为</li><li>事件驱动的文件监听该怎么 debounce</li><li>SQLite 里 session、hourly、daily 三张表怎么 upsert 不丢数据</li></ul><p>这种时候我会把问题抛给 Claude Code，让它先给方案，我再挑毛病。</p><p><strong>Codex 则是我的快枪手。</strong> 有些改动范围很小、很具体，不需要长篇讨论，直接让 Codex 上。比如：</p><blockquote><p>“Codex 判断了有没有刘海的逻辑，删了。”</p><p>“这个黑框是为啥，这也是 Codex 今天下午加的。”</p></blockquote><p>Codex 的特点是快，但偶尔会留下一些“看起来能动，但边界没处理干净”的痕迹。那个黑框就是典型：它在刘海屏设备上加了一层黑色背景，意图是隐藏刘海，结果在非刘海屏上反而露了馅。最后还是我拉着 Claude Code 一起复盘，把透明 panel 的圆角和 masksToBounds 重新理了一遍才解决。</p><p>这件事让我明确了一条分工：<strong>需要理解系统边界和做取舍的，找 Claude Code；需要快速验证一个想法的，找 Codex。</strong></p><h2 id="一个-idea-长出四块屏幕"><a href="#一个-idea-长出四块屏幕" class="headerlink" title="一个 idea 长出四块屏幕"></a>一个 idea 长出四块屏幕</h2><p><img src="/images/owlet-lingdong-family.webp" alt="Owlet 的灵动家族：灵动岛、灵动卡、灵动栏、灵动板"></p><p>Owlet 的 UI 表面不算多，但每一块都有自己的坑：</p><table><thead><tr><th>Surface</th><th>作用</th><th>难点</th></tr></thead><tbody><tr><td>Menu bar</td><td>常驻图标 + token 总数</td><td>图标动画不能太耗电</td></tr><tr><td>Floating island</td><td>刘海区实时会话面板</td><td><code>NSPanel</code> 圆角、多屏、透明度</td></tr><tr><td>Dashboard</td><td>历史用量、排名、设置</td><td>SwiftUI 图表和数据库查询</td></tr><tr><td>Dynamic card</td><td>桌面小组件式面板</td><td>像系统 widget，但不能真用 WidgetKit</td></tr></tbody></table><p>最有意思的是命名。某天我突然说：</p><blockquote><p>“刷新数据放到标题旁边怎么样？还有在灵动家族给这个菜单起个名字，一家人要整整齐齐。”</p></blockquote><p>于是就有了“灵动家族”：灵动岛、灵动卡、灵动栏。这个命名一点都不高大上，但它让产品在聊天时变得很好描述。<strong>“灵动卡又崩了”</strong> 比 <strong>“桌面浮动窗口崩溃了”</strong> 可爱多了。</p><p>另一个让我印象深刻的是灵动卡的崩溃排查。同样是 Apple Silicon、同样是 macOS 15，有的机器能打开，有的机器直接崩。最后定位到问题不在业务逻辑，而在 AppKit window level、<code>NSPanel</code>、系统材质和透明圆角叠加后的边界行为。那篇文章我已经单独发过，这里不再展开。</p><h2 id="把青山湖写进代码"><a href="#把青山湖写进代码" class="headerlink" title="把青山湖写进代码"></a>把青山湖写进代码</h2><p><img src="/images/owlet-qingshan-lake-palette.webp" alt="Owlet 的青山湖配色体系：蓝雾、湖青、杉绿、雾白、晨光浅金"></p><p>我是杭州临安人，青山湖就在家门口。Owlet 的视觉方向最后定成了：<strong>像原生 macOS 工具，但从青山湖的湖水、杉林、青山、雾白里取色。</strong></p><p>具体 Palette 长这样：</p><ul><li>蓝雾天空 → 浅背景</li><li>浮萍湖绿 → 主要强调色</li><li>水杉深绿 → 深色文字和进度条</li><li>雾带白 → 面板底色</li><li>晨光浅金 → 仅用于边缘光和状态提示</li><li>雨后湿木 → 少量阴影和分隔</li></ul><p>我甚至专门给 Claude 下了一条规则：</p><blockquote><p>“晨光浅金只用于边缘光、状态提示或小面积 accent，不能主导整个 surface。”</p></blockquote><p>因为项目早期用过一段时间黄&#x2F;金色做主色，结果越调越脏，最后全推翻了。改成青山湖色系后，整个 app 立刻从“网吧风”回到了“能放在 macOS 桌面上不丢人”的状态。</p><h2 id="性能偏执：一个-menu-bar-app-的电量焦虑"><a href="#性能偏执：一个-menu-bar-app-的电量焦虑" class="headerlink" title="性能偏执：一个 menu bar app 的电量焦虑"></a>性能偏执：一个 menu bar app 的电量焦虑</h2><p>Owlet 是常驻应用。只要 Claude 不活跃，它就不应该醒来。</p><p>为此我们做了几件看起来很细节、但很重要的事：</p><ol><li><strong>文件监听用 Dispatch Source，而不是轮询。</strong> 之前有些实现会 sleep-loop 检查 active session，我全部砍掉了。</li><li><strong>JSONL 增量读取。</strong> 50 MB 的会话日志不应该造成 50 MB 内存峰值。ClaudeCodeAdapter 用 <code>(mtime, fileSize, byteOffset)</code> 做缓存，文件增长时只读新增 bytes。</li><li><strong>动画门控。</strong> 菜单栏猫头鹰只有 token 总数变化时才脉冲；dashboard 和 island 只有 active session 存在时才按 0.2s 帧率动画，idle 时最多每秒一帧。</li><li><strong>数据库 upsert 而不是 deleteAll + insert。</strong> 这个改动很小，但能避免大量无用写入。</li></ol><p>这些规则都被写进了 <code>CLAUDE.md</code> 和 <code>AGENTS.md</code>，成为后续所有改动的底线。<strong>一个工具要是自己先变成电老虎，就没资格监控别人的 token 消耗。</strong></p><h2 id="AI-协作的边界：它不是替你做主"><a href="#AI-协作的边界：它不是替你做主" class="headerlink" title="AI 协作的边界：它不是替你做主"></a>AI 协作的边界：它不是替你做主</h2><p>整个过程中，AI 没有替我做任何一个真正重要的决定。</p><ul><li>要不要做这个项目？我决定。</li><li>功能优先级怎么排？我决定。</li><li>命名 Owlet？我决定。</li><li>青山湖配色方向？我决定。</li><li>性能规则里哪些不能回退？我决定。</li></ul><p>AI 做的是：<strong>把我模糊的判断快速翻译成可执行的代码和文档，并在执行过程中帮我规避低级错误。</strong></p><p>这让我越来越确信一个判断框架：在 Agent &#x2F; AI Infra 时代，人的核心竞争力不是写代码的速度，而是<strong>判断框架</strong>——知道什么该做、什么不该做、什么要取舍、什么是底线。AI 负责把判断拉满执行力，两者结合才是极致交付速度。</p><h2 id="要不要用-我的判断框架"><a href="#要不要用-我的判断框架" class="headerlink" title="要不要用 &#x2F; 我的判断框架"></a>要不要用 &#x2F; 我的判断框架</h2><p>如果你也想尝试 vibe coding 一个小工具，我的建议是：</p><table><thead><tr><th>场景</th><th>判断</th></tr></thead><tbody><tr><td>想做一个小而美的个人工具</td><td><strong>值得上</strong></td></tr><tr><td>项目涉及大量系统边界（macOS 权限、沙盒、codesign）</td><td><strong>重点关注</strong></td></tr><tr><td>需要长期维护和多端同步</td><td><strong>可以再等</strong></td></tr><tr><td>你享受“说一句话，AI 帮你验证”的快感</td><td><strong>值得上</strong></td></tr><tr><td>你无法接受 AI 偶尔留下小坑</td><td><strong>可以再等</strong></td></tr></tbody></table><p>对我来说，Owlet 是一个完美的实验场：规模可控、用户就是自己、技术栈有挑战性但不需要服务百万用户。如果后续它能帮更多开发者看清 AI 使用成本，那就更好了。</p><p>现在菜单栏里那只小猫头鹰还在闪。它看着自己诞生的过程，也看着我每天烧掉的 token。</p><p>挺 meta 的，也挺酷的。</p>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/8a33f17a26ad/</id>
    <link href="https://blog.zisheng.pro/posts/8a33f17a26ad/"/>
    <published>2026-07-09T18:30:00.000Z</published>
    <summary>这篇文章记录我用 Claude Code 和 Codex 以 vibe coding 方式做一个 macOS menu-bar 应用的全过程：从最初想“看看我每天烧了多少 token”，到做出 Owlet 这个能实时监控 Claude Code 和 Codex 使用量的小工具。过程中有 AI 双开协作的节奏、把家乡青山湖风景写进配色的执念，也有对 menu-bar app 电量的偏执。</summary>
    <title>我用 Claude Code + Codex vibe 了一个 macOS App：Owlet</title>
    <updated>2026-09-18T08:15:28.640Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="AI" scheme="https://blog.zisheng.pro/categories/AI/"/>
    <category term="Agent" scheme="https://blog.zisheng.pro/tags/Agent/"/>
    <category term="macOS" scheme="https://blog.zisheng.pro/tags/macOS/"/>
    <category term="SwiftUI" scheme="https://blog.zisheng.pro/tags/SwiftUI/"/>
    <category term="AppKit" scheme="https://blog.zisheng.pro/tags/AppKit/"/>
    <category term="Argos" scheme="https://blog.zisheng.pro/tags/Argos/"/>
    <content>
      <![CDATA[<p>最近给 Argos 做了一个桌面上的“灵动卡”：它不属于主窗口，也不是菜单栏 popover，而是一个常驻桌面的轻量信息面板，用来展示当天 Claude Code 和 Codex 的 Token 用量。</p><p>视觉上它应该接近系统桌面小组件，有一点毛玻璃、有圆角、有环境透色；交互上它又必须像一个普通 Mac app surface，可以拖动、可以关闭、可以从设置里重新打开。问题也出在这里：我们很容易把“看起来像系统小组件”和“实现方式也像系统小组件”混成一件事。</p><p>这次的真实反馈很直接：用户点击打开灵动卡，Argos 直接崩溃。</p><p><img src="/images/argos-dynamic-card-crash-dialog.webp" alt="Argos 崩溃弹窗"></p><h2 id="一句话总结"><a href="#一句话总结" class="headerlink" title="一句话总结"></a>一句话总结</h2><p>macOS 桌面浮层的稳定性不能只看 SwiftUI 视图本身，真正危险的是 AppKit window level、<code>NSPanel</code> 行为、系统材质、透明圆角和启动持久化一起叠加后的系统边界。</p><h2 id="这不是一个普通-SwiftUI-bug"><a href="#这不是一个普通-SwiftUI-bug" class="headerlink" title="这不是一个普通 SwiftUI bug"></a>这不是一个普通 SwiftUI bug</h2><p>最开始我也倾向于按普通 UI bug 排查：是不是某个 <code>@EnvironmentObject</code> 没注入？是不是 <code>TimelineView</code> 刷新太频繁？是不是某个统计数据为空导致 view body 崩了？</p><p>但反馈里有两个信号很关键：</p><table><thead><tr><th>现象</th><th>初步判断</th></tr></thead><tbody><tr><td>只有部分机器崩</td><td>不是纯业务数据问题，更像系统组合差异</td></tr><tr><td>重装也无法恢复</td><td>可能和持久化设置有关，应用启动后自动走到崩溃路径</td></tr><tr><td>打开灵动卡才崩</td><td>主窗口、菜单栏、数据读取不是第一怀疑对象</td></tr><tr><td>同类机器不一定复现</td><td>芯片型号只是表象，系统窗口栈和显示环境也会影响</td></tr></tbody></table><p>这类问题最麻烦的地方是：你的本机不崩，不代表代码没问题。macOS 的 window server、桌面组件层级、Stage Manager、多屏、刘海屏、透明材质都可能让同一段代码走到不同系统路径。</p><h2 id="真正危险的是-window-level"><a href="#真正危险的是-window-level" class="headerlink" title="真正危险的是 window level"></a>真正危险的是 window level</h2><p>灵动卡一开始为了“悬浮在桌面上方”，用了更高的窗口层级。视觉上这很合理：它不应该被普通窗口轻易盖住。</p><p>但 AppKit 的窗口层级不是一个单纯的 z-index。不同 level 背后隐含了不同的系统行为：</p><table><thead><tr><th>层级选择</th><th>优点</th><th>风险</th></tr></thead><tbody><tr><td><code>.normal</code></td><td>最稳定，接近普通窗口</td><td>可能被其他窗口盖住</td></tr><tr><td><code>.floating</code></td><td>更符合悬浮卡片直觉</td><td>更容易进入特殊窗口路径</td></tr><tr><td>desktop widget 附近</td><td>视觉接近系统小组件</td><td>系统版本、桌面状态、多屏下差异最大</td></tr></tbody></table><p>最后我把层级选择暴露到了设置里：默认用最稳的普通窗口，用户可以自己切到浮动或桌面组件风格。这个决策不够“产品洁癖”，但足够工程务实。</p><figure class="highlight swift"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">switch</span> windowLevel &#123;</span><br><span class="line"><span class="keyword">case</span> .normal:</span><br><span class="line">    panel.isFloatingPanel <span class="operator">=</span> <span class="literal">false</span></span><br><span class="line">    panel.level <span class="operator">=</span> .normal</span><br><span class="line"><span class="keyword">case</span> .floating:</span><br><span class="line">    panel.isFloatingPanel <span class="operator">=</span> <span class="literal">true</span></span><br><span class="line">    panel.level <span class="operator">=</span> .floating</span><br><span class="line"><span class="keyword">case</span> .desktopWidget:</span><br><span class="line">    panel.isFloatingPanel <span class="operator">=</span> <span class="literal">false</span></span><br><span class="line">    panel.level <span class="operator">=</span> <span class="type">NSWindow</span>.<span class="type">Level</span>(rawValue: <span class="type">Int</span>(<span class="type">CGWindowLevelForKey</span>(.desktopIconWindow)) <span class="operator">+</span> <span class="number">1</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这个代码片段的重点不是 API 本身，而是思路：把“视觉偏好”从“稳定默认值”里拆出来。默认路径必须保守，高风险路径可以给高级用户选择。</p><h2 id="透明圆角-panel-不是只画一个-RoundedRectangle"><a href="#透明圆角-panel-不是只画一个-RoundedRectangle" class="headerlink" title="透明圆角 panel 不是只画一个 RoundedRectangle"></a>透明圆角 panel 不是只画一个 RoundedRectangle</h2><p>第二个坑是透明圆角。</p><p>SwiftUI 里画一个 <code>RoundedRectangle</code> 很简单，但 <code>NSPanel</code> 本身仍然是一个 AppKit window。如果只处理 SwiftUI 背景，不处理 <code>NSHostingView</code> 和 window content view，系统可能仍然按矩形 frame 计算阴影、命中区域和合成边界。</p><p>这就是为什么透明 panel 要同时处理两层：</p><table><thead><tr><th>层</th><th>要处理什么</th></tr></thead><tbody><tr><td>SwiftUI view</td><td><code>clipShape</code>、背景、边框、视觉层次</td></tr><tr><td>AppKit hosting&#x2F;container</td><td>clear background、cornerRadius、continuous corner、masksToBounds</td></tr></tbody></table><p>尤其是透明窗口，最好不要依赖 AppKit 默认 shadow。默认 shadow 是按 window frame 投影的，frame 是矩形，圆角外就可能出现多出来的直角、脏边或奇怪边界。</p><h2 id="玻璃质感也要分“安全”和“不安全”"><a href="#玻璃质感也要分“安全”和“不安全”" class="headerlink" title="玻璃质感也要分“安全”和“不安全”"></a>玻璃质感也要分“安全”和“不安全”</h2><p>桌面小组件风格很容易让人想到系统 material，比如 <code>.ultraThinMaterial</code>、<code>NSVisualEffectView</code>、<code>behindWindow</code>、<code>underWindowBackground</code>。</p><p>但这次排查里，我对灵动卡做了一个取舍：先不用 AppKit 的 <code>NSVisualEffectView</code> 做底层材质，而是用 SwiftUI 的轻量 blur、半透明 tint、渐变 stroke 和局部 glow 模拟“雾白玻璃”。</p><p>原因很简单：灵动卡本身已经是一个透明 <code>NSPanel</code>，再叠系统 material 和特殊 window level，变量太多。为了排查和发布稳定性，先减少系统合成路径。</p><table><thead><tr><th>方案</th><th align="right">质感</th><th align="right">稳定性</th><th>当前选择</th></tr></thead><tbody><tr><td><code>NSVisualEffectView</code> 强系统材质</td><td align="right">高</td><td align="right">中低</td><td>暂缓</td></tr><tr><td>SwiftUI blur + tint</td><td align="right">中高</td><td align="right">高</td><td>采用</td></tr><tr><td>纯实色卡片</td><td align="right">中</td><td align="right">最高</td><td>不采用</td></tr></tbody></table><p>这也是我现在对 macOS 视觉实现的一个判断：能用系统能力当然好，但一旦 surface 是透明浮窗、跨桌面、多屏、可拖动，还要启动自动恢复，就不能盲目堆 material。</p><h2 id="持久化是体验，也是事故放大器"><a href="#持久化是体验，也是事故放大器" class="headerlink" title="持久化是体验，也是事故放大器"></a>持久化是体验，也是事故放大器</h2><p>还有一个容易忽略的点：灵动卡是否应该持久化打开状态。</p><p>从产品体验看，答案是应该。用户打开了桌面卡片，下次启动自然还应该在。默认也应该打开，否则用户可能根本不知道这个能力存在。</p><p>但从事故恢复看，持久化会放大问题：如果“打开灵动卡”本身会崩，那么应用每次启动自动恢复灵动卡，就会变成“打开应用即崩”，用户连进设置关闭它的机会都没有。</p><p>所以这里有一个典型工程取舍：</p><table><thead><tr><th>策略</th><th align="right">体验</th><th align="right">故障恢复</th></tr></thead><tbody><tr><td>不持久化，启动默认关闭</td><td align="right">差</td><td align="right">好</td></tr><tr><td>持久化，启动默认打开</td><td align="right">好</td><td align="right">差</td></tr><tr><td>持久化 + 稳定默认层级</td><td align="right">较好</td><td align="right">可接受</td></tr></tbody></table><p>最后我选择恢复持久化和默认打开，但把默认 window level 降到普通窗口，并把高层级选择放到设置里。这不是绝对安全，但比“默认走最高风险路径”合理得多。</p><h2 id="为什么我的机器不崩，用户机器会崩"><a href="#为什么我的机器不崩，用户机器会崩" class="headerlink" title="为什么我的机器不崩，用户机器会崩"></a>为什么我的机器不崩，用户机器会崩</h2><p>这个问题很值得单独说。</p><p>两个 Mac 看起来“除了芯片都一样”，并不代表窗口系统环境一样。至少这些因素都可能不同：</p><table><thead><tr><th>差异点</th><th>可能影响</th></tr></thead><tbody><tr><td>芯片&#x2F;显卡路径</td><td>WindowServer 合成、透明 blur、外接屏策略</td></tr><tr><td>系统小版本</td><td>AppKit &#x2F; SwiftUI bridge 行为变化</td></tr><tr><td>Stage Manager &#x2F; 桌面组件状态</td><td>桌面层级和特殊 window level 行为</td></tr><tr><td>多屏与刘海屏</td><td>默认定位、visibleFrame、notch 区域</td></tr><tr><td>辅助显示工具</td><td>BetterDisplay、HiDPI、虚拟屏可能改变 screen frame</td></tr><tr><td>历史设置</td><td>上一次保存的 window level、位置、visible 状态</td></tr></tbody></table><p>所以这类 bug 不能只问“为什么我不复现”。更有效的问题是：哪些系统路径只有对方会走到？哪些设置会让用户每次启动都自动进入危险路径？</p><h2 id="我的判断框架"><a href="#我的判断框架" class="headerlink" title="我的判断框架"></a>我的判断框架</h2><p>以后再做 macOS 上的桌面浮层，我会按这个顺序判断：</p><ol><li>默认层级先保守：普通窗口优先，浮动和桌面组件风格作为可选项。</li><li>透明圆角同时处理 SwiftUI 和 AppKit，不只画背景。</li><li>系统 material 先少用，等稳定性验证后再加。</li><li>持久化状态要配合故障恢复路径，否则一个开关就可能把用户锁死在崩溃循环里。</li><li>对“我本机不崩”的结论保持怀疑，多看系统版本、显示环境和历史设置。</li></ol><h2 id="要不要用-我的判断框架"><a href="#要不要用-我的判断框架" class="headerlink" title="要不要用 &#x2F; 我的判断框架"></a>要不要用 &#x2F; 我的判断框架</h2><p>如果你只是做一个普通工具窗口，不建议一上来就追系统小组件质感。先把窗口层级、圆角、拖动、恢复、关闭这些基础行为做稳。</p><p>如果你确实要做桌面小组件式 surface，可以上玻璃质感，但默认路径要克制。我的建议是：默认普通层级 + 安全 blur + 可选高级层级。等收集到足够多真实设备反馈后，再决定要不要把更像系统小组件的层级设为默认。</p><p>这次踩坑对我的提醒是：macOS 原生体验不是“看起来像原生”就够了，它还要求你尊重 AppKit 的历史包袱和窗口系统边界。尤其是 Agent 工具这种常驻型应用，稳定性永远比一个更漂亮的默认层级更重要。</p>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/a6040b00245e/</id>
    <link href="https://blog.zisheng.pro/posts/a6040b00245e/"/>
    <published>2026-07-09T14:38:30.000Z</published>
    <summary>这篇文章记录我在做 Argos 灵动卡时踩到的一次 macOS 桌面浮窗崩溃：同样是 Apple Silicon、同样是新版系统，有的机器稳定，有的机器一打开就崩。真正的问题不在业务逻辑，而在 AppKit window level、透明 panel、系统材质和启动持久化策略叠加后的边界行为。</summary>
    <title>一次 macOS 灵动卡崩溃排查：别把桌面小组件当普通窗口</title>
    <updated>2026-09-18T08:15:28.636Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="AI Infra" scheme="https://blog.zisheng.pro/categories/AI-Infra/"/>
    <category term="macOS" scheme="https://blog.zisheng.pro/tags/macOS/"/>
    <category term="SwiftUI" scheme="https://blog.zisheng.pro/tags/SwiftUI/"/>
    <category term="Argos" scheme="https://blog.zisheng.pro/tags/Argos/"/>
    <category term="Particle System" scheme="https://blog.zisheng.pro/tags/Particle-System/"/>
    <category term="Canvas" scheme="https://blog.zisheng.pro/tags/Canvas/"/>
    <category term="Animation" scheme="https://blog.zisheng.pro/tags/Animation/"/>
    <category term="UI Engineering" scheme="https://blog.zisheng.pro/tags/UI-Engineering/"/>
    <content>
      <![CDATA[<p>今天给 Argos 做了一个“段位晋级彩纸特效”。</p><p>听起来是个很小的 UI 动画：用户 token 用量触发段位变化时，全屏弹一下彩纸，提示“黄金 III”之类的晋级结果。但真做起来，我发现这类效果很容易翻车。粒子太少，不像庆祝；粒子太多，像噪音。彩带想做飘逸，结果像屏幕上画了几条乱线。加爆点，本意是增强“爆炸感”，实际像廉价烟花。加光晕，本意是高级，实际会脏。</p><p>这篇文章复盘这次踩坑。重点不是 SwiftUI 代码，而是把粒子系统里那些概念讲清楚：<strong>particle、emitter、confetti、streamer、glint、ambient sparkles、bloom、burst、motion field、lifetime、distribution</strong>。以后再做类似 UI 特效，至少知道自己在调什么。</p><h2 id="一句话总结"><a href="#一句话总结" class="headerlink" title="一句话总结"></a>一句话总结</h2><p>UI 里的庆祝特效，本质是一个小型 particle system。好看的关键不是“粒子越多越好”，而是让粒子的<strong>出生位置、生命周期、运动方向、密度分布、视觉层级</strong>服务同一个动势。</p><hr><h2 id="这次到底做了什么"><a href="#这次到底做了什么" class="headerlink" title="这次到底做了什么"></a>这次到底做了什么</h2><p>Argos 里有一个段位系统：用户每天使用 Claude Code &#x2F; Codex 的 token 会累计，达到某个 milestone 时触发段位变化。</p><p>我想要的效果是：</p><ol><li>段位变化时出现全屏庆祝反馈。</li><li>中间有一个段位 toast，例如“黄金 III”。</li><li>屏幕上有彩纸、亮片、星芒，形成“晋级”的瞬间感。</li><li>只在调试环境有“测试彩纸”按钮，正式逻辑仍由真实段位变化触发。</li></ol><p>最终通过的版本是：</p><table><thead><tr><th>元素</th><th align="right">最终保留</th><th>原因</th></tr></thead><tbody><tr><td>Confetti rectangles</td><td align="right">是</td><td>语义最清晰，一眼就是庆祝</td></tr><tr><td>Circular chips</td><td align="right">是</td><td>补充面积和层次，不抢主视觉</td></tr><tr><td>Diamond glints</td><td align="right">是</td><td>增加高光和精致感</td></tr><tr><td>Small cross sparkles</td><td align="right">是</td><td>少量点缀，强化“闪一下”</td></tr><tr><td>Streamers &#x2F; 彩带</td><td align="right">否</td><td>方向、曲线、交叉都很难调，容易显脏</td></tr><tr><td>Burst &#x2F; 爆点</td><td align="right">否</td><td>在这个场景里太像烟花，不符合 macOS 工具气质</td></tr><tr><td>Heavy bloom &#x2F; 大光晕</td><td align="right">否</td><td>容易污染画面，让文字和桌面背景变糊</td></tr></tbody></table><p>这其实是一个很典型的 UI 粒子系统取舍：<strong>不是把所有效果都加上，而是删到只剩能稳定表达语义的元素</strong>。</p><hr><h2 id="Particle：粒子不是“点”，而是一个小演员"><a href="#Particle：粒子不是“点”，而是一个小演员" class="headerlink" title="Particle：粒子不是“点”，而是一个小演员"></a>Particle：粒子不是“点”，而是一个小演员</h2><p>很多人听到 particle 会下意识理解成“点”。但在 UI 动画里，一个 particle 更像一个小演员，它至少有这些属性：</p><table><thead><tr><th>属性</th><th>含义</th><th>彩纸特效里的例子</th></tr></thead><tbody><tr><td>position</td><td>当前坐标</td><td>彩纸当前在屏幕哪里</td></tr><tr><td>velocity</td><td>速度</td><td>往左上、右上、下落还是漂移</td></tr><tr><td>acceleration</td><td>加速度</td><td>重力、阻尼、回弹</td></tr><tr><td>lifetime</td><td>生命周期</td><td>0.8s 出现，3.5s 消失</td></tr><tr><td>size</td><td>尺寸</td><td>小彩纸、大亮片</td></tr><tr><td>rotation</td><td>旋转</td><td>彩纸翻转角度</td></tr><tr><td>opacity</td><td>透明度</td><td>出现、停留、淡出</td></tr><tr><td>color</td><td>颜色</td><td>高饱和彩纸色、湖水青、晨光金</td></tr><tr><td>shape</td><td>形状</td><td>矩形、圆形、菱形、星芒</td></tr></tbody></table><p>所以“加一点彩纸”不是加几个随机矩形，而是定义一组小对象：它们什么时候出生、从哪里出生、往哪里走、多久消失、怎么旋转、颜色如何分布。</p><p>这次一开始怪异，根因就是我把粒子看成“画面装饰”，而不是一个系统。粒子的运动方向、密度和语义没对齐，就会出现“从上往下落”“一坨挤在一起”“不够满”“没有爆炸感”这些问题。</p><hr><h2 id="Emitter：发射器决定第一眼的动势"><a href="#Emitter：发射器决定第一眼的动势" class="headerlink" title="Emitter：发射器决定第一眼的动势"></a>Emitter：发射器决定第一眼的动势</h2><p>Emitter 是粒子系统里的发射器。它决定粒子从哪里出生。</p><p>常见 emitter 有几种：</p><table><thead><tr><th>Emitter 类型</th><th>视觉效果</th><th>适用场景</th></tr></thead><tbody><tr><td>Point emitter</td><td>从一个点喷出</td><td>烟花、爆炸、魔法点</td></tr><tr><td>Line emitter</td><td>从一条线喷出</td><td>雨、雪、瀑布、顶部落彩纸</td></tr><tr><td>Area emitter</td><td>从一个区域出现</td><td>环境漂浮、空间尘埃</td></tr><tr><td>Edge emitter</td><td>从屏幕边缘进入</td><td>庆祝彩纸、舞台入场</td></tr><tr><td>Multi-emitter</td><td>多个发射源</td><td>全屏庆祝、游戏技能</td></tr></tbody></table><p>这次最大的教训是：<strong>发射器位置会强烈决定用户对动画的理解</strong>。</p><p>最开始用户反馈“怎么是从上往下落的”，这说明粒子的 emitter 或 velocity 让人联想到“掉落”，而不是“庆祝喷发”。彩纸如果主要从顶部进入，语义就偏“下雨”；如果从左右两侧或底部斜向上出现，语义才接近“庆祝”。</p><p>后来我尝试让彩带从左右两边飞出，但又出现了新问题：左右彩带在中间交叉、起点过于集中、曲线不丝滑。这说明 emitter 不是只看一个点，还要看<strong>发射区域分布</strong>。</p><p>更准确地说，emitter 要调这几个参数：</p><table><thead><tr><th>参数</th><th>作用</th></tr></thead><tbody><tr><td>origin</td><td>发射源位置</td></tr><tr><td>spread</td><td>发射范围</td></tr><tr><td>angle</td><td>初速度方向</td></tr><tr><td>variance</td><td>随机扰动</td></tr><tr><td>rate</td><td>单位时间生成多少粒子</td></tr><tr><td>burst count</td><td>瞬间生成多少粒子</td></tr></tbody></table><p>最终我放弃彩带后，保留了更稳定的 area&#x2F;edge 混合：彩纸从不同区域出现，星芒覆盖更大的屏幕区域，但不再让连续线条承担主视觉。</p><hr><h2 id="Confetti：彩纸是最稳的庆祝语义"><a href="#Confetti：彩纸是最稳的庆祝语义" class="headerlink" title="Confetti：彩纸是最稳的庆祝语义"></a>Confetti：彩纸是最稳的庆祝语义</h2><p>Confetti 就是彩纸。它是庆祝动画里最稳定、最不需要解释的元素。</p><p>UI 里常见的 confetti 粒子通常是：</p><table><thead><tr><th>形状</th><th>视觉特点</th></tr></thead><tbody><tr><td>矩形条</td><td>最像真实彩纸</td></tr><tr><td>小方块</td><td>更像游戏奖励</td></tr><tr><td>圆片</td><td>像亮片或纸屑</td></tr><tr><td>菱形</td><td>更有闪光感</td></tr><tr><td>短弧片</td><td>更像翻转中的纸片</td></tr></tbody></table><p>这次最终保留的是亮色矩形彩纸。原因很简单：它的语义最强。</p><p>彩纸要好看，核心不是形状复杂，而是三件事：</p><ol><li><strong>颜色要亮</strong>：庆祝粒子不能太灰，否则像灰尘。</li><li><strong>尺寸要有层次</strong>：全是同一尺寸会机械。</li><li><strong>旋转要变化</strong>：不旋转就像静态贴纸。</li></ol><p>我一开始颜色不够亮，用户直接指出“彩纸色彩要亮”。这很关键。UI 主体可以克制，但庆祝粒子本身必须高饱和，否则它的情绪表达不成立。</p><p>这里有一个设计边界：<strong>背景和主 UI 需要低饱和，粒子可以高饱和</strong>。如果整个界面都高饱和，会廉价；但庆祝粒子高饱和，是语义需要。</p><hr><h2 id="Streamer：彩带为什么最容易翻车"><a href="#Streamer：彩带为什么最容易翻车" class="headerlink" title="Streamer：彩带为什么最容易翻车"></a>Streamer：彩带为什么最容易翻车</h2><p>Streamer 是长彩带、飘带。它的问题比彩纸复杂很多。</p><p>彩纸是离散粒子，单个粒子不好看也不会毁掉整体。彩带是连续曲线，一条不自然就非常显眼。</p><p>这次彩带翻车主要有几个原因：</p><table><thead><tr><th>问题</th><th>根因</th></tr></thead><tbody><tr><td>太高</td><td>lift 参数过大，曲线飞到屏幕顶部</td></tr><tr><td>太长</td><td>reach 过大，线条穿过视觉中心</td></tr><tr><td>挤在一起</td><td>emitter 起点过于集中</td></tr><tr><td>全是并行</td><td>控制点变化不足，曲线缺少角度差</td></tr><tr><td>交叉很尬</td><td>左右两侧没有限定各自活动区域</td></tr><tr><td>不丝滑</td><td>Bézier 控制点没有形成自然切线</td></tr></tbody></table><p>彩带要自然，至少要同时满足：</p><ol><li>起点分布不能重合。</li><li>曲线控制点要有连续的切线方向。</li><li>同侧可以有交叉，但交叉角度要自然。</li><li>左右两侧如果语义是“各自飞出”，就不能互相穿越中心。</li><li>长度、弧度、透明度要一起衰减。</li></ol><p>这就不是“加几条曲线”了，而是在调一个小型 motion field。</p><p>最后我判断：在这个产品场景里，彩带收益不够高。Argos 是一个 macOS 工具，不是游戏结算页。彩带一旦做不好，负收益很明显。所以最终删掉。</p><p>这个决策很重要：<strong>粒子系统不是越复杂越高级，删掉不稳定元素也是工程能力</strong>。</p><hr><h2 id="Glint：星芒负责“精致”，不要负责“热闹”"><a href="#Glint：星芒负责“精致”，不要负责“热闹”" class="headerlink" title="Glint：星芒负责“精致”，不要负责“热闹”"></a>Glint：星芒负责“精致”，不要负责“热闹”</h2><p>Glint 是闪光、星芒。它通常是菱形、四角星、十字星，出现在高光点附近。</p><p>它适合做：</p><ol><li>金属反光。</li><li>奖励高光。</li><li>魔法感。</li><li>“刚刚发生了好事”的瞬间提示。</li></ol><p>但 glint 不能太多。太多会变成噪点，也会像廉价贴纸。</p><p>这次最终保留了两种 glint：</p><table><thead><tr><th>类型</th><th>作用</th></tr></thead><tbody><tr><td>Diamond glints</td><td>菱形闪光，提供精致感</td></tr><tr><td>Cross sparkles</td><td>十字闪光，提供瞬间感</td></tr></tbody></table><p>它们不是主角，主角仍然是 confetti。glint 的职责是让画面从“只有纸片”变成“有奖励感”。</p><p>一个实用规则：<strong>glint 的 opacity 应该随 pulse 变化，而不是常亮</strong>。常亮的星芒像贴图；一闪一灭才像反光。</p><hr><h2 id="Ambient-Sparkles：环境闪光不是爆点"><a href="#Ambient-Sparkles：环境闪光不是爆点" class="headerlink" title="Ambient Sparkles：环境闪光不是爆点"></a>Ambient Sparkles：环境闪光不是爆点</h2><p>Ambient sparkles 可以翻译成环境闪光、氛围闪点。它和 burst 不一样。</p><table><thead><tr><th>概念</th><th>特点</th><th>观感</th></tr></thead><tbody><tr><td>Burst</td><td>从中心或局部突然爆开</td><td>强刺激、像烟花</td></tr><tr><td>Ambient sparkles</td><td>分散在画面中轻微闪烁</td><td>氛围、精致、空间感</td></tr></tbody></table><p>这次我试过加爆点，但用户反馈“不好看”。原因是爆点会把语义带向“烟花爆炸”，而 Argos 的晋级反馈更像“系统奖励”，不应该过度娱乐化。</p><p>Ambient sparkles 更适合这个场景：它们不抢段位 toast，不制造视觉中心，只是在背景层增加一点“庆祝正在发生”的环境反馈。</p><p>实现时要注意：</p><ol><li>不要集中在一个点。</li><li>不要全部同步闪。</li><li>不要持续太久。</li><li>不要比主彩纸更亮。</li></ol><p>Ambient sparkles 的价值是“补空气感”，不是“制造高潮”。</p><hr><h2 id="Bloom-Glow：光晕是调味料，不是主菜"><a href="#Bloom-Glow：光晕是调味料，不是主菜" class="headerlink" title="Bloom &#x2F; Glow：光晕是调味料，不是主菜"></a>Bloom &#x2F; Glow：光晕是调味料，不是主菜</h2><p>Bloom 是光晕、泛光。Glow 也是发光，但在 UI 语境里更泛一点。Bloom 通常指亮部向周围扩散，让画面有曝光感。</p><p>光晕很容易让效果看起来“高级”，也很容易让画面变脏。</p><p>这次我对光晕的判断是：</p><table><thead><tr><th>光效</th><th align="right">适合程度</th><th>原因</th></tr></thead><tbody><tr><td>小面积 highlight</td><td align="right">高</td><td>能增强奖励感</td></tr><tr><td>粒子边缘轻微 glow</td><td align="right">中</td><td>可以让粒子更亮</td></tr><tr><td>大面积 bloom</td><td align="right">低</td><td>会污染桌面背景和文字</td></tr><tr><td>中央强光爆点</td><td align="right">低</td><td>会抢段位 toast</td></tr></tbody></table><p>尤其在 macOS 上，全屏 overlay 下面是真实桌面和窗口。如果光晕太重，用户看到的是“脏”和“糊”，不是高级。</p><p>所以最后只保留轻量星芒和亮片描边，没有做大面积 bloom。</p><p>我的经验是：<strong>工具型产品里的 bloom，要像盐，不能像汤底</strong>。</p><hr><h2 id="Burst：爆点为什么不一定有“爆炸感”"><a href="#Burst：爆点为什么不一定有“爆炸感”" class="headerlink" title="Burst：爆点为什么不一定有“爆炸感”"></a>Burst：爆点为什么不一定有“爆炸感”</h2><p>Burst 是瞬间爆发。很多庆祝动画都会从一个中心点喷出大量粒子，形成爆炸感。</p><p>但 burst 有一个问题：它非常依赖上下文。</p><p>在游戏里，burst 很自然，因为奖励、击杀、升级本来就是强反馈。在 macOS menu bar 工具里，burst 太强就会显得浮夸。用户这次明确说“彩纸特效不要加爆点不好看”，这个判断是对的。</p><p>Burst 适合：</p><ol><li>游戏结算。</li><li>支付成功。</li><li>大型 milestone。</li><li>首次完成关键任务。</li></ol><p>Burst 不适合：</p><ol><li>高频状态变化。</li><li>工具型产品里的轻量反馈。</li><li>已经有 toast 的场景。</li><li>背景复杂、可读性重要的场景。</li></ol><p>段位晋级当然是好事，但它不是游戏胜利。Argos 的整体调性是克制的 macOS 工具，所以 burst 被删掉是合理的。</p><hr><h2 id="Motion-Field：自然感来自“方向场”，不是-random"><a href="#Motion-Field：自然感来自“方向场”，不是-random" class="headerlink" title="Motion Field：自然感来自“方向场”，不是 random"></a>Motion Field：自然感来自“方向场”，不是 random</h2><p>很多粒子系统不好看，是因为用了太多随机数。</p><p>随机数只能避免重复，不能自动产生自然感。自然感来自 motion field：一套整体一致的运动方向场。</p><p>这次我踩过的坑是：</p><ol><li>每条彩带都有随机控制点，但整体没有一致方向。</li><li>左右彩带有交叉，但交叉没有空间逻辑。</li><li>起点随机，但集中在相近区域，实际观感还是一坨。</li></ol><p>也就是说，random 只是 variation，不是 composition。</p><p>一个更靠谱的思路是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">先定义整体动势：从两侧 / 中下区域进入，向中上区域扩散，再缓慢下落淡出</span><br><span class="line">再给每个粒子加小随机：角度 ±10°，速度 ±20%，尺寸 ±30%，delay 分层</span><br></pre></td></tr></table></figure><p>如果反过来，先给每个粒子随机，再希望整体自然，通常会失败。</p><hr><h2 id="Distribution：满不满，取决于分布，不只取决于数量"><a href="#Distribution：满不满，取决于分布，不只取决于数量" class="headerlink" title="Distribution：满不满，取决于分布，不只取决于数量"></a>Distribution：满不满，取决于分布，不只取决于数量</h2><p>用户后来反馈“感觉不够满”。第一反应容易是加数量。但只加数量会导致两个问题：</p><ol><li>原来空的地方还是空。</li><li>原来挤的地方更挤。</li></ol><p>所以要区分两个概念：</p><table><thead><tr><th>概念</th><th>解决什么</th></tr></thead><tbody><tr><td>Count</td><td>粒子数量</td></tr><tr><td>Distribution</td><td>粒子分布</td></tr></tbody></table><p>这次最终我同时做了两件事：</p><ol><li>提高粒子数量：彩纸、亮片、星芒都增加。</li><li>扩大覆盖区域：横向更接近全屏，纵向从中下区域扩到中上&#x2F;中下区域。</li></ol><p>这才让画面变“满”。如果只加 count，不改 distribution，只会更乱。</p><p>一个实用判断：如果你觉得画面“不够满”，先看热力图，不要先加粒子。</p><hr><h2 id="Lifetime：动画太快，本质是生命周期太短"><a href="#Lifetime：动画太快，本质是生命周期太短" class="headerlink" title="Lifetime：动画太快，本质是生命周期太短"></a>Lifetime：动画太快，本质是生命周期太短</h2><p>用户一开始还反馈“太快了”。这通常不是单个粒子速度的问题，而是 lifetime 的问题。</p><p>Lifetime 包含：</p><ol><li>出生 delay。</li><li>可见时间。</li><li>fade out 时间。</li><li>整体 overlay 持续时间。</li></ol><p>如果所有粒子几乎同时出现、同时消失，即使速度不快，也会显得“啪一下没了”。如果 delay 分层，粒子批次进入，动画会更有呼吸。</p><p>这次我把整体展示时间拉长，并让不同粒子有不同 delay。这样用户看到的是一段庆祝过程，而不是一帧贴图。</p><hr><h2 id="Toast：中心信息不能和粒子抢主视觉"><a href="#Toast：中心信息不能和粒子抢主视觉" class="headerlink" title="Toast：中心信息不能和粒子抢主视觉"></a>Toast：中心信息不能和粒子抢主视觉</h2><p>这个特效里还有一个中心段位 toast。</p><p>Toast 的任务是告诉用户：你升到了哪个段位。粒子的任务是表达情绪。二者不能互相抢。</p><p>我中间也犯过一个错误：段位 badge 设计太突兀，颜色和粒子不搭，又不够明显。后来调整为更克制的青山湖风格，用深绿&#x2F;湖水青承载，标题变大，减少花哨颜色。</p><p>这里的原则是：</p><ol><li>Toast 负责信息。</li><li>粒子负责情绪。</li><li>Toast 颜色要稳定，粒子颜色可以跳。</li><li>Toast 不要跟粒子一样花。</li></ol><p>如果中心信息也花，背景粒子也花，用户就不知道该看哪里。</p><hr><h2 id="Canvas-实现：为什么不用一堆-SwiftUI-View"><a href="#Canvas-实现：为什么不用一堆-SwiftUI-View" class="headerlink" title="Canvas 实现：为什么不用一堆 SwiftUI View"></a>Canvas 实现：为什么不用一堆 SwiftUI View</h2><p>这次最终用 SwiftUI <code>Canvas</code> 画粒子，而不是 <code>ForEach</code> 渲染一堆 View。</p><p>原因很直接：</p><table><thead><tr><th>方案</th><th>优点</th><th>问题</th></tr></thead><tbody><tr><td>多个 SwiftUI View</td><td>写起来直观</td><td>大量 View diff，动画状态复杂</td></tr><tr><td>Canvas</td><td>单层绘制，适合粒子</td><td>要自己管理坐标、时间、路径</td></tr></tbody></table><p>粒子系统本质上是每帧根据时间计算位置，然后画出来。Canvas 更贴近这个模型。</p><p>大致结构是：</p><figure class="highlight swift"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">TimelineView</span>(.animation(minimumInterval: <span class="number">1.0</span> <span class="operator">/</span> <span class="number">30.0</span>)) &#123; timeline <span class="keyword">in</span></span><br><span class="line">    <span class="type">ConfettiCanvas</span>(</span><br><span class="line">        elapsed: timeline.date.timeIntervalSince(startDate),</span><br><span class="line">        frozen: <span class="literal">false</span></span><br><span class="line">    )</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>elapsed</code> 是整个系统的时钟。每个粒子根据 <code>elapsed - delay</code> 算自己的局部时间，再算 position、rotation、opacity。</p><p>这个模型比给每个粒子挂一堆 <code>.animation</code> 更可控。</p><hr><h2 id="这次最后留下的系统结构"><a href="#这次最后留下的系统结构" class="headerlink" title="这次最后留下的系统结构"></a>这次最后留下的系统结构</h2><p>最终版本可以理解成 3 层：</p><table><thead><tr><th>层级</th><th>元素</th><th>作用</th></tr></thead><tbody><tr><td>主粒子层</td><td>Confetti rectangles</td><td>庆祝语义</td></tr><tr><td>辅助粒子层</td><td>Circular chips</td><td>增加密度和面积</td></tr><tr><td>高光层</td><td>Diamond glints + cross sparkles</td><td>精致感、瞬间感</td></tr></tbody></table><p>被删掉的是：</p><table><thead><tr><th>元素</th><th>删除原因</th></tr></thead><tbody><tr><td>Streamers</td><td>曲线难自然，抢画面</td></tr><tr><td>Burst</td><td>太像烟花，不符合产品气质</td></tr><tr><td>Heavy bloom</td><td>污染背景，可读性差</td></tr></tbody></table><p>这个结构其实很朴素，但稳定。</p><p>很多 UI 特效最后不是赢在复杂，而是赢在“每个元素都知道自己负责什么”。</p><hr><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>emitter 方向、颜色亮度、confetti 形状</td></tr><tr><td>太乱</td><td>删除连续元素，降低 bloom，减少同步运动</td></tr><tr><td>不够满</td><td>distribution 先于 count</td></tr><tr><td>太快</td><td>lifetime、delay、fade out</td></tr><tr><td>不自然</td><td>motion field 先于 random</td></tr><tr><td>主信息不明显</td><td>toast 层级、粒子避让、颜色对比</td></tr><tr><td>廉价</td><td>少用爆点和大光晕，控制高饱和面积</td></tr></tbody></table><p>一句话：<strong>先定语义，再定动势，最后才调随机参数</strong>。</p><hr><h2 id="要不要用粒子系统"><a href="#要不要用粒子系统" class="headerlink" title="要不要用粒子系统"></a>要不要用粒子系统</h2><p>我的判断是：值得用，但要克制。</p><p>适合用的场景：</p><ol><li>用户完成一个阶段性目标。</li><li>系统需要表达奖励。</li><li>状态变化低频且重要。</li><li>动画不会打断用户工作流。</li></ol><p>不适合用的场景：</p><ol><li>高频反馈。</li><li>错误提示。</li><li>信息密集页面。</li><li>用户正在专注输入或阅读。</li></ol><p>Argos 这次段位晋级适合用粒子系统，因为它是低频、正向、阶段性的反馈。但最终也证明了一点：工具型产品里的庆祝动画，不应该朝游戏化一路狂奔。真正合适的方向是：<strong>有情绪，但不喧宾夺主</strong>。</p><p>这也是我对这次迭代最大的收获。</p>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/e253ed7eb4b2/</id>
    <link href="https://blog.zisheng.pro/posts/e253ed7eb4b2/"/>
    <published>2026-07-08T15:58:00.000Z</published>
    <summary>这篇文章复盘一次 macOS 工具里“段位晋级彩纸特效”的真实迭代：为什么一开始会像从上往下掉、为什么彩带会显得别扭、爆点和光晕为什么不一定高级，以及如何用 emitter、particle、glint、ambient sparkle、bloom、lifetime、distribution、motion field 这些概念，把一个庆祝动画调到可接受。</summary>
    <title>粒子系统实践：从一版难看的彩纸特效说起</title>
    <updated>2026-09-18T08:15:28.640Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="AI Infra" scheme="https://blog.zisheng.pro/categories/AI-Infra/"/>
    <category term="Node.js" scheme="https://blog.zisheng.pro/tags/Node-js/"/>
    <category term="Next.js" scheme="https://blog.zisheng.pro/tags/Next-js/"/>
    <category term="Fumadocs" scheme="https://blog.zisheng.pro/tags/Fumadocs/"/>
    <category term="Midway.js" scheme="https://blog.zisheng.pro/tags/Midway-js/"/>
    <category term="OpenAPI" scheme="https://blog.zisheng.pro/tags/OpenAPI/"/>
    <category term="文档站" scheme="https://blog.zisheng.pro/tags/%E6%96%87%E6%A1%A3%E7%AB%99/"/>
    <category term="Tailwind CSS" scheme="https://blog.zisheng.pro/tags/Tailwind-CSS/"/>
    <content>
      <![CDATA[<p>后端服务到了一定规模，文档就成了刚需。不是给外部用户的 README，而是给自己人的：新成员怎么跑起来、接口入参是什么、数据库里几张表是怎么关联的。</p><p>我负责的这个 Node.js 后端服务跑着一套自动化平台，随着接口越来越多，”靠脑子记”已经不够了。于是我决定正经做一个内嵌文档站，要求只有一个：<strong>好看，且不要花时间调样式</strong>。</p><h2 id="一句话总结"><a href="#一句话总结" class="headerlink" title="一句话总结"></a>一句话总结</h2><p>Fumadocs 是目前我见过最开箱即用的 Node.js 文档框架——接入 OpenAPI 两行命令就能出接口文档，颜值不需要额外调，跟 Midway.js &#x2F; Egg.js 静态托管的结合也很自然。</p><hr><h2 id="选型过程：为什么不选其他的"><a href="#选型过程：为什么不选其他的" class="headerlink" title="选型过程：为什么不选其他的"></a>选型过程：为什么不选其他的</h2><p>我评估了以下几个方案：</p><table><thead><tr><th>框架</th><th>技术栈</th><th>OpenAPI 支持</th><th>颜值开箱</th><th>备注</th></tr></thead><tbody><tr><td><strong>VitePress</strong></td><td>Vue 3</td><td>无官方插件</td><td>中等</td><td>适合纯文档，定制自由度高但要花时间</td></tr><tr><td><strong>Docusaurus</strong></td><td>React</td><td>docusaurus-plugin-openapi-docs</td><td>一般</td><td>生态成熟，但 OpenAPI 插件渲染效果不够精致</td></tr><tr><td><strong>Nextra</strong></td><td>Next.js</td><td>有，需要配置</td><td>中等</td><td>和 Next.js 绑定深，学习成本适中</td></tr><tr><td><strong>Mintlify</strong></td><td>SaaS</td><td>一流</td><td>最好看</td><td>需要联网托管，私有项目不适合</td></tr><tr><td><strong>Fumadocs</strong></td><td>Next.js</td><td>fumadocs-openapi，官方维护</td><td><strong>一流</strong></td><td>可自部署，颜值对标 Mintlify</td></tr></tbody></table><p>Mintlify 的 OpenAPI 渲染是业内标杆，但它是 SaaS——文档得推到他们的服务器，对内部项目不合适。</p><p>Fumadocs 是我找到的唯一一个<strong>可以自部署、OpenAPI 一流、不需要调样式就好看</strong>的方案。</p><hr><h2 id="Fumadocs-的颜值从何而来"><a href="#Fumadocs-的颜值从何而来" class="headerlink" title="Fumadocs 的颜值从何而来"></a>Fumadocs 的颜值从何而来</h2><h3 id="设计系统内置"><a href="#设计系统内置" class="headerlink" title="设计系统内置"></a>设计系统内置</h3><p>Fumadocs UI 本身是 Tailwind CSS v4 驱动的设计系统，所有颜色、排版、间距都基于 CSS 变量。导入一行 CSS 就能用：</p><figure class="highlight css"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/* app/globals.css */</span></span><br><span class="line"><span class="keyword">@import</span> <span class="string">&#x27;fumadocs-ui/style.css&#x27;</span>;</span><br><span class="line"><span class="keyword">@import</span> <span class="string">&#x27;tailwindcss&#x27;</span>;</span><br></pre></td></tr></table></figure><p>不需要写主题配置，深色模式自动支持，字体和间距比例都对。</p><h3 id="文档布局开箱可用"><a href="#文档布局开箱可用" class="headerlink" title="文档布局开箱可用"></a>文档布局开箱可用</h3><p>侧边栏、TOC（目录）、面包屑、代码高亮——这些在大多数框架里都要自己拼，Fumadocs 全部内置：</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// app/docs/layout.tsx</span></span><br><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">DocsLayout</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;fumadocs-ui/layouts/docs&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; source &#125; <span class="keyword">from</span> <span class="string">&#x27;@/lib/source&#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_">Layout</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">DocsLayout</span> <span class="attr">tree</span>=<span class="string">&#123;source.pageTree&#125;</span> <span class="attr">nav</span>=<span class="string">&#123;&#123;</span> <span class="attr">title:</span> &#x27;<span class="attr">我的项目</span>&#x27; &#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">DocsLayout</span>&gt;</span></span></span><br><span class="line">  );</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这就是文档布局的全部代码。侧边栏自动从 <code>content/docs/</code> 的目录结构生成，<code>meta.json</code> 控制排序和分组。</p><h3 id="MDX-组件"><a href="#MDX-组件" class="headerlink" title="MDX 组件"></a>MDX 组件</h3><p><code>&lt;Cards&gt;</code>、<code>&lt;Card&gt;</code>、<code>&lt;Callout&gt;</code>、<code>&lt;Steps&gt;</code> 这些常见 MDX 组件全部内置，写文档时直接用，不需要自己实现。</p><hr><h2 id="fumadocs-openapi：两行命令出接口文档"><a href="#fumadocs-openapi：两行命令出接口文档" class="headerlink" title="fumadocs-openapi：两行命令出接口文档"></a>fumadocs-openapi：两行命令出接口文档</h2><p>这是我最看重的功能。维护一份 <code>openapi.yaml</code>，然后：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 安装</span></span><br><span class="line">npm install fumadocs-openapi</span><br><span class="line"></span><br><span class="line"><span class="comment"># 从 YAML 生成 MDX 页面</span></span><br><span class="line">npx fumadocs-openapi generate -i openapi/my-api.yaml -o content/docs/api</span><br></pre></td></tr></table></figure><p>生成的每个接口都是一个独立的 MDX 页面，包含：</p><ul><li>请求方法 + 路径（带颜色标签）</li><li>路径参数 &#x2F; Query 参数 &#x2F; 请求体的字段说明和类型</li><li>响应示例，支持多状态码</li><li>交互式 “Try it” 面板（可选）</li></ul><p>效果对标 Mintlify 和 Swagger UI Pro，但完全自部署。</p><p>OpenAPI spec 的骨架是这样的：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># openapi/my-api.yaml</span></span><br><span class="line"><span class="attr">openapi:</span> <span class="number">3.0</span><span class="number">.3</span></span><br><span class="line"><span class="attr">info:</span></span><br><span class="line">  <span class="attr">title:</span> <span class="string">My</span> <span class="string">API</span></span><br><span class="line">  <span class="attr">version:</span> <span class="number">1.0</span><span class="number">.0</span></span><br><span class="line"><span class="attr">paths:</span></span><br><span class="line">  <span class="string">/api/skills:</span></span><br><span class="line">    <span class="attr">get:</span></span><br><span class="line">      <span class="attr">summary:</span> <span class="string">获取技能列表</span></span><br><span class="line">      <span class="attr">tags:</span> [<span class="string">Skills</span>]</span><br><span class="line">      <span class="attr">parameters:</span></span><br><span class="line">        <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">projectKey</span></span><br><span class="line">          <span class="attr">in:</span> <span class="string">query</span></span><br><span class="line">          <span class="attr">schema:</span></span><br><span class="line">            <span class="attr">type:</span> <span class="string">string</span></span><br><span class="line">      <span class="attr">responses:</span></span><br><span class="line">        <span class="attr">&#x27;200&#x27;:</span></span><br><span class="line">          <span class="attr">description:</span> <span class="string">技能列表</span></span><br><span class="line">          <span class="attr">content:</span></span><br><span class="line">            <span class="attr">application/json:</span></span><br><span class="line">              <span class="attr">schema:</span></span><br><span class="line">                <span class="attr">type:</span> <span class="string">array</span></span><br><span class="line">                <span class="attr">items:</span></span><br><span class="line">                  <span class="string">$ref:</span> <span class="string">&#x27;#/components/schemas/Skill&#x27;</span></span><br></pre></td></tr></table></figure><p>写一次 spec，既能生成文档，将来也能用于接口测试和 Mock Server——比手写 Markdown 维护成本低。</p><hr><h2 id="怎么和-Midway-js-Egg-js-结合"><a href="#怎么和-Midway-js-Egg-js-结合" class="headerlink" title="怎么和 Midway.js &#x2F; Egg.js 结合"></a>怎么和 Midway.js &#x2F; Egg.js 结合</h2><p>这里有个有意思的架构问题：文档站是 Next.js 项目，服务端是 Midway.js（Egg.js 底层）。两个进程，怎么变成”一个项目”？</p><h3 id="静态导出-egg-static-托管"><a href="#静态导出-egg-static-托管" class="headerlink" title="静态导出 + egg-static 托管"></a>静态导出 + egg-static 托管</h3><p>Next.js 支持 <code>output: &#39;export&#39;</code>，构建后输出纯静态文件：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// client/next.config.mjs</span></span><br><span class="line"><span class="keyword">const</span> config = &#123;</span><br><span class="line">  <span class="attr">output</span>: <span class="string">&#x27;export&#x27;</span>,      <span class="comment">// 输出静态 HTML/JS/CSS，不需要 Node.js 运行时</span></span><br><span class="line">  <span class="attr">images</span>: &#123; <span class="attr">unoptimized</span>: <span class="literal">true</span> &#125;,</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>Egg.js 内置 <code>egg-static</code> 插件，开启后可以托管静态目录：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// src/config/config.default.ts</span></span><br><span class="line"><span class="keyword">import</span> &#123; join &#125; <span class="keyword">from</span> <span class="string">&#x27;path&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="attr">static</span>: &#123;</span><br><span class="line">  <span class="attr">prefix</span>: <span class="string">&#x27;/&#x27;</span>,</span><br><span class="line">  <span class="attr">dir</span>: [<span class="title function_">join</span>(appInfo.<span class="property">appDir</span>, <span class="string">&#x27;client&#x27;</span>, <span class="string">&#x27;out&#x27;</span>)],  <span class="comment">// Next.js 导出目录</span></span><br><span class="line">  <span class="attr">dynamic</span>: <span class="literal">true</span>,</span><br><span class="line">  <span class="attr">preload</span>: <span class="literal">false</span>,</span><br><span class="line">&#125;,</span><br></pre></td></tr></table></figure><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// src/config/plugin.ts</span></span><br><span class="line"><span class="attr">static</span>: &#123;</span><br><span class="line">  <span class="attr">enable</span>: <span class="literal">true</span>,</span><br><span class="line">  <span class="attr">package</span>: <span class="string">&#x27;egg-static&#x27;</span>,</span><br><span class="line">&#125;,</span><br></pre></td></tr></table></figure><p>请求流程：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">HTTP 请求</span><br><span class="line">  ↓</span><br><span class="line">egg-static（命中 client/out/ 静态文件 → 直接返回 HTML/JS/CSS）</span><br><span class="line">  ↓</span><br><span class="line">Midway 路由（没有匹配的静态文件 → 走 API 控制器）</span><br></pre></td></tr></table></figure><p>也就是说，<code>GET /</code> 返回文档站首页，<code>GET /api/skills</code> 走 Midway 控制器——静态文件和 API 共用同一个端口，完全无感。</p><h3 id="本地开发：一个命令同时跑两个进程"><a href="#本地开发：一个命令同时跑两个进程" class="headerlink" title="本地开发：一个命令同时跑两个进程"></a>本地开发：一个命令同时跑两个进程</h3><p>生产环境用静态导出。本地开发时，Next.js 需要一个 dev server 做热更新，不能用静态文件。用 <code>concurrently</code> 解决：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// package.json（项目根目录）</span></span><br><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;concurrently --names &#x27;server,docs&#x27; --prefix-colors &#x27;cyan,magenta&#x27; --kill-others-on-fail &#x27;npm run dev:server&#x27; &#x27;npm run dev:docs&#x27;&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;dev:server&quot;</span><span class="punctuation">:</span> <span class="string">&quot;cross-env NODE_ENV=local mwtsc --watch --run @midwayjs/mock/app.js&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;dev:docs&quot;</span><span class="punctuation">:</span> <span class="string">&quot;npm --prefix client run dev&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>npm run dev</code> 之后，终端里：</p><ul><li><code>cyan</code> 前缀的输出是 Midway 后端（<code>:7001</code>）</li><li><code>magenta</code> 前缀的输出是 Fumadocs（<code>:3000</code>）</li></ul><p>Home controller 做一个简单的判断：本地环境没有 <code>client/out/</code>，就重定向到 <code>:3000</code>；其他环境如果 build 缺失就显示一个保底状态页。</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// src/controller/home.ts</span></span><br><span class="line"><span class="keyword">const</span> <span class="variable constant_">CLIENT_INDEX</span> = <span class="title function_">join</span>(__dirname, <span class="string">&#x27;../../client/out/index.html&#x27;</span>);</span><br><span class="line"></span><br><span class="line"><span class="meta">@Get</span>(<span class="string">&#x27;/&#x27;</span>)</span><br><span class="line"><span class="keyword">async</span> <span class="title function_">home</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">if</span> (<span class="title function_">existsSync</span>(<span class="variable constant_">CLIENT_INDEX</span>)) &#123;</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">ctx</span>.<span class="property">type</span> = <span class="string">&#x27;html&#x27;</span>;</span><br><span class="line">    <span class="keyword">return</span> <span class="title function_">readFileSync</span>(<span class="variable constant_">CLIENT_INDEX</span>, <span class="string">&#x27;utf-8&#x27;</span>);</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">if</span> (<span class="variable language_">this</span>.<span class="property">ctx</span>.<span class="property">app</span>.<span class="property">config</span>.<span class="property">env</span> === <span class="string">&#x27;local&#x27;</span>) &#123;</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">ctx</span>.<span class="title function_">redirect</span>(<span class="string">&#x27;http://localhost:3000&#x27;</span>);</span><br><span class="line">    <span class="keyword">return</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><h3 id="目录结构"><a href="#目录结构" class="headerlink" title="目录结构"></a>目录结构</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">my-server/</span><br><span class="line">├── src/              # Midway.js 后端</span><br><span class="line">│   ├── controller/</span><br><span class="line">│   ├── service/</span><br><span class="line">│   └── model/</span><br><span class="line">├── client/           # Fumadocs 文档站（Next.js）</span><br><span class="line">│   ├── app/</span><br><span class="line">│   ├── content/docs/ # MDX 文档内容</span><br><span class="line">│   ├── openapi/      # OpenAPI spec</span><br><span class="line">│   └── out/          # next build 产物（被 egg-static 服务）</span><br><span class="line">└── package.json      # dev/build 统一入口</span><br></pre></td></tr></table></figure><hr><h2 id="版本对齐的坑"><a href="#版本对齐的坑" class="headerlink" title="版本对齐的坑"></a>版本对齐的坑</h2><p>Fumadocs 各包的 peer dependency 比较严格，截至写作时：</p><table><thead><tr><th>包</th><th>版本</th><th>备注</th></tr></thead><tbody><tr><td><code>fumadocs-core</code></td><td><code>^16.9.0</code></td><td></td></tr><tr><td><code>fumadocs-mdx</code></td><td><code>^15.0.0</code></td><td>注意不是 ^12，v15 才支持 core v16</td></tr><tr><td><code>fumadocs-ui</code></td><td><code>^16.0.0</code></td><td></td></tr><tr><td><code>fumadocs-openapi</code></td><td><code>^10.0.0</code></td><td>要求 core ^16.9、ui ^16.9</td></tr><tr><td><code>tailwindcss</code></td><td><code>^4.0.0</code></td><td>v16 ui 内置 Tailwind v4，不再用 v3 preset</td></tr></tbody></table><p><code>fumadocs-mdx</code> 对 Next.js 16 有 optional peer dep，安装时加 <code>--legacy-peer-deps</code> 即可，不影响 Next.js 15 运行。</p><p>CSS 配置从 Tailwind v3 的 <code>tailwind.config.ts</code> 改成 v4 的 <code>@import</code> 方式：</p><figure class="highlight css"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">@import</span> <span class="string">&#x27;fumadocs-ui/style.css&#x27;</span>;  <span class="comment">/* Fumadocs 设计系统 + Tailwind v4 */</span></span><br><span class="line"><span class="keyword">@import</span> <span class="string">&#x27;tailwindcss&#x27;</span>;             <span class="comment">/* 补充 utility classes */</span></span><br><span class="line"><span class="keyword">@source</span> <span class="string">&#x27;../app/**/*.&#123;ts,tsx&#125;&#x27;</span>;</span><br><span class="line"><span class="keyword">@source</span> <span class="string">&#x27;../content/**/*.&#123;md,mdx&#125;&#x27;</span>;</span><br></pre></td></tr></table></figure><hr><h2 id="要不要用-我的判断框架"><a href="#要不要用-我的判断框架" class="headerlink" title="要不要用 &#x2F; 我的判断框架"></a>要不要用 &#x2F; 我的判断框架</h2><p><strong>适合用 Fumadocs 的场景：</strong></p><ul><li>需要 OpenAPI 接口文档，且对渲染效果有要求</li><li>项目本身是 Node.js，可以把文档站代码一起放在 repo 里</li><li>团队熟悉 React &#x2F; Next.js（文档站本身就是 Next.js 项目）</li><li>不想花时间在文档样式上，要开箱即用</li></ul><p><strong>不适合的场景：</strong></p><ul><li>项目是纯静态网站 &#x2F; 博客，VitePress 更轻量</li><li>团队技术栈是 Vue，Vitepress 更顺手</li><li>需要公开托管 + 自定义域名，Mintlify 的 SaaS 体验更好</li><li>对 Next.js 包体积有严格限制（文档站打出来还是有一定体积的）</li></ul><p><strong>我的判断</strong>：值得上。特别是如果你的项目已经在写 OpenAPI spec，fumadocs-openapi 自动生成这一步能省很多时间，比手写 Markdown 维护成本低，比 Swagger UI 好看一个量级。</p>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/99abd9f09884/</id>
    <link href="https://blog.zisheng.pro/posts/99abd9f09884/"/>
    <published>2026-07-08T15:30:00.000Z</published>
    <summary>给 Midway.js 后端服务搭文档站的选型过程：为什么在 Vitepress / Docusaurus 里最终选了 Fumadocs，它开箱即用的颜值从何而来，fumadocs-openapi 如何自动从 OpenAPI spec 生成接口文档，以及怎么用 Next.js 静态导出 + egg-static 把它嵌进 Midway 项目、一个命令同时跑服务端和文档站。</summary>
    <title>为什么选 Fumadocs：Node.js 项目接入开箱即用文档站</title>
    <updated>2026-09-18T08:15:28.636Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="AI" scheme="https://blog.zisheng.pro/categories/AI/"/>
    <category term="AI Infra" scheme="https://blog.zisheng.pro/tags/AI-Infra/"/>
    <category term="Facebook Ads" scheme="https://blog.zisheng.pro/tags/Facebook-Ads/"/>
    <category term="投流" scheme="https://blog.zisheng.pro/tags/%E6%8A%95%E6%B5%81/"/>
    <category term="小说站" scheme="https://blog.zisheng.pro/tags/%E5%B0%8F%E8%AF%B4%E7%AB%99/"/>
    <category term="广告预算" scheme="https://blog.zisheng.pro/tags/%E5%B9%BF%E5%91%8A%E9%A2%84%E7%AE%97/"/>
    <category term="增长" scheme="https://blog.zisheng.pro/tags/%E5%A2%9E%E9%95%BF/"/>
    <content>
      <![CDATA[<p>这两天我在看小说站的 Facebook 投流数据时，遇到一个很典型的问题：<strong>一天预算 100 元，想拿到 2000 个成效，怎么做到？</strong></p><p>如果只看投放后台，很容易把它理解成「是不是受众太窄」「是不是预算没有花满」「是不是素材还可以优化」。这些都可能对，但第一步不是调后台，而是先做一遍反推。增长问题最后都要落到单位经济模型上，尤其是广告套利这种业务，数学先于手感。</p><h2 id="一句话总结"><a href="#一句话总结" class="headerlink" title="一句话总结"></a>一句话总结</h2><p><strong>100 元预算拿 2000 个成效，要求单次成效成本降到 0.05 元。</strong> 如果当前单次成本是 0.33 元，那就意味着还要再降 85%。这不是常规优化能稳定做到的目标。真正可执行的路径是：先扩受众让 100 元预算稳定花满，再在 0.33 元成本附近逐步放量到约 660 元&#x2F;天。</p><h2 id="先算目标单次成本"><a href="#先算目标单次成本" class="headerlink" title="先算目标单次成本"></a>先算目标单次成本</h2><p>这个问题的第一行公式非常简单：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">目标单次成效成本 = 总预算 / 目标成效数</span><br><span class="line">100 / 2000 = 0.05 元</span><br></pre></td></tr></table></figure><p>也就是说，如果一天只花 100 元，还要拿 2000 个落地页浏览或类似成效，每一次成效最多只能花 5 分钱。</p><p>再对比当前数据：</p><table><thead><tr><th>指标</th><th align="right">数值</th><th>含义</th></tr></thead><tbody><tr><td>日预算目标</td><td align="right">100 元</td><td>希望每天最多花这么多</td></tr><tr><td>目标成效</td><td align="right">2000 次</td><td>希望每天拿到的结果数</td></tr><tr><td>目标单次成本</td><td align="right">0.05 元</td><td>100 &#x2F; 2000</td></tr><tr><td>当前单次成本</td><td align="right">0.33 元</td><td>已经跑出来的实际成本</td></tr><tr><td>需要降幅</td><td align="right">约 85%</td><td>从 0.33 降到 0.05</td></tr></tbody></table><p>这里的关键判断是：<strong>0.33 元并不是一个明显偏高的落地页浏览成本。</strong> 在 Facebook 上跑冷流量，尤其是内容站、小说站这种非强交易场景，0.33 元已经属于相当低的区间。素材、受众、地域、设备、落地页速度都继续优化，当然可能还能降，但把长期稳定成本压到 0.05 元，不应该作为计划基线。</p><h2 id="这不是「单次成本太高」的问题"><a href="#这不是「单次成本太高」的问题" class="headerlink" title="这不是「单次成本太高」的问题"></a>这不是「单次成本太高」的问题</h2><p>很多人看到「2000 成效没达成」，第一反应是单次成效费用太高。但这句话要拆成两种情况。</p><p>第一种是真的高。比如目标是 0.3 元，当前跑到 2 元，那就是素材、受众、出价或落地页承诺不匹配，要从投放质量上修。</p><p>第二种是目标本身不现实。比如目标是 0.05 元，当前是 0.33 元，而且 0.33 元已经在低成本区间，那就不是「优化一下」能解决的问题。继续追 0.05 元，只会把团队带进错误动作：频繁换受众、频繁改预算、过早切换转化目标，最后学习期不断重置，成本反而上升。</p><p>我更愿意把它叫做 <strong>目标成本错配</strong>：预算目标、成效目标、市场真实成本不在同一个平面上。</p><h2 id="为什么前面还会说「先扩受众」"><a href="#为什么前面还会说「先扩受众」" class="headerlink" title="为什么前面还会说「先扩受众」"></a>为什么前面还会说「先扩受众」</h2><p>在原始数据里还有一个信号：覆盖人数只有 939，展示 1019。这个量级太小了。</p><p>如果一个广告组一天只覆盖不到 1000 人，就算单次成本看起来不差，也不能直接判断它有放量能力。原因有三个：</p><ol><li>样本太小，CPC&#x2F;LPV 容易被少量点击扰动。</li><li>受众太窄，预算提高后频次会快速上升，CTR 下滑。</li><li>算法探索空间不足，Facebook 没法找到更多相似人群。</li></ol><p>所以「先扩受众」不是为了让 100 元直接变成 2000 成效，而是为了验证两件事：</p><table><thead><tr><th>要验证什么</th><th>为什么重要</th></tr></thead><tbody><tr><td>100 元能不能花满</td><td>花不满说明受众池或出价&#x2F;审核存在约束</td></tr><tr><td>成本能不能维持在 0.33 元附近</td><td>只有成本稳定，后续预算放量才有意义</td></tr></tbody></table><p>扩受众的动作可以很直接：</p><table><thead><tr><th>动作</th><th>目的</th></tr></thead><tbody><tr><td>用 Advantage+ Audience 或放宽兴趣词</td><td>给算法更大的探索空间</td></tr><tr><td>地域从单城市扩到省&#x2F;全国</td><td>避免局部受众耗尽</td></tr><tr><td>去掉非必要年龄&#x2F;性别限制</td><td>降低系统性 CPM 压力</td></tr><tr><td>有数据后建 1%-5% Lookalike</td><td>用真实访问者替代人工猜兴趣</td></tr></tbody></table><p>这里的阶段目标不是「立刻 2000 成效」，而是「100 元预算能稳定花完，同时成本没有明显劣化」。</p><h2 id="真正的预算应该是多少"><a href="#真正的预算应该是多少" class="headerlink" title="真正的预算应该是多少"></a>真正的预算应该是多少</h2><p>如果当前单次成本是 0.33 元，目标是每天 2000 次成效，那预算反推是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">所需预算 = 目标成效数 * 当前单次成本</span><br><span class="line">2000 * 0.33 = 660 元/天</span><br></pre></td></tr></table></figure><p>所以答案很直接：<strong>要稳定拿 2000 成效&#x2F;天，预算基线不是 100 元，而是约 660 元&#x2F;天。</strong></p><p>这不是说马上把预算从 100 拉到 660。Facebook 的学习期对预算变化很敏感，突然大幅加预算会让系统重新探索，成本可能被放大。更稳的方式是按 20%-30% 的步长放量：</p><table><thead><tr><th>阶段</th><th align="right">日预算</th><th>观察周期</th></tr></thead><tbody><tr><td>1</td><td align="right">100 元</td><td>2-3 天</td></tr><tr><td>2</td><td align="right">130 元</td><td>2-3 天</td></tr><tr><td>3</td><td align="right">170 元</td><td>2-3 天</td></tr><tr><td>4</td><td align="right">220 元</td><td>2-3 天</td></tr><tr><td>5</td><td align="right">290 元</td><td>2-3 天</td></tr><tr><td>6</td><td align="right">380 元</td><td>2-3 天</td></tr><tr><td>7</td><td align="right">500 元</td><td>2-3 天</td></tr><tr><td>8</td><td align="right">660 元</td><td>稳定观察</td></tr></tbody></table><p>每一步只看一个问题：<strong>成本有没有保持在可接受范围内。</strong> 如果从 0.33 元涨到 0.6 元甚至更高，就说明当前素材或受众承载不了这个预算，需要回头补素材、扩受众或拆广告组，而不是继续硬加。</p><h2 id="对小说站投流尤其要克制"><a href="#对小说站投流尤其要克制" class="headerlink" title="对小说站投流尤其要克制"></a>对小说站投流尤其要克制</h2><p>小说站这种 AdSense &#x2F; AdX 变现场景，收入不是一次点击立刻发生，而是由后续阅读页数和广告 RPM 慢慢摊出来。简单写就是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">单次访问价值 = 平均阅读页数 * 页面 RPM / 1000</span><br><span class="line">利润 = 单次访问价值 - CPC</span><br></pre></td></tr></table></figure><p>这意味着 Facebook 端的 CPC 只是上半场。真正决定能不能套利的，还有两个站内指标：</p><table><thead><tr><th>杠杆</th><th>解释</th></tr></thead><tbody><tr><td>平均阅读页数</td><td>用户从第 1 章继续读到第几章</td></tr><tr><td>页面 RPM</td><td>每 1000 次页面浏览能产生多少广告收入</td></tr></tbody></table><p>如果站内阅读深度和 RPM 还没稳定，盲目追求低到不现实的 CPC，会让投放策略变形。比如为了便宜点击去投低质量受众，短期 CPC 降了，但用户进站 3 秒就走，最后广告收入更差。</p><p>所以我现在更关心的是三层漏斗：</p><table><thead><tr><th>层级</th><th>核心指标</th><th>判断</th></tr></thead><tbody><tr><td>广告层</td><td>CTR &#x2F; CPC &#x2F; CPM</td><td>素材能不能把人带进来</td></tr><tr><td>落地层</td><td>LPV &#x2F; ViewContent</td><td>页面有没有真正加载并开始阅读</td></tr><tr><td>阅读层</td><td>ChapterRead50 &#x2F; NextChapterClick &#x2F; ChapterCompleted</td><td>用户是不是继续读</td></tr></tbody></table><p>便宜点击不是目标，<strong>便宜且会读的点击</strong>才是目标。</p><h2 id="我的判断框架"><a href="#我的判断框架" class="headerlink" title="我的判断框架"></a>我的判断框架</h2><p>以后遇到类似问题，我会按这个顺序判断。</p><p>第一步，先算目标成本。</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">目标单次成本 = 预算 / 目标成效</span><br></pre></td></tr></table></figure><p>如果目标成本低到明显脱离市场区间，先改目标，不要急着改广告。</p><p>第二步，看当前成本是不是异常。</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">当前成本 / 目标成本 = 需要优化的倍数</span><br></pre></td></tr></table></figure><p>如果只差 20%-30%，素材和受众优化可能有效。如果差 80% 以上，就要高度怀疑目标不现实。</p><p>第三步，看预算有没有花满。</p><p>预算花不满，优先扩受众、放宽地域、检查审核和出价。预算能花满但成本上升，说明放量承载能力不足。</p><p>第四步，再决定是降本还是加预算。</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">目标预算 = 目标成效 * 可稳定维持的单次成本</span><br></pre></td></tr></table></figure><p>用稳定成本反推预算，而不是用愿望预算反推不现实的成本。</p><h2 id="要不要用-我的判断框架"><a href="#要不要用-我的判断框架" class="headerlink" title="要不要用 &#x2F; 我的判断框架"></a>要不要用 &#x2F; 我的判断框架</h2><p>这次结论很明确：</p><table><thead><tr><th>判断</th><th>结论</th></tr></thead><tbody><tr><td>100 元拿 2000 成效</td><td>不作为现实目标</td></tr><tr><td>当前 0.33 元单次成本</td><td>不是明显偏高，先视为可接受基线</td></tr><tr><td>下一步重点</td><td>扩受众，让 100 元稳定花满</td></tr><tr><td>放量路径</td><td>成本稳定后逐步加到约 660 元&#x2F;天</td></tr><tr><td>风险点</td><td>过快加预算、频繁改广告组、追求低质便宜点击</td></tr></tbody></table><p>我的最终判断是：<strong>当前任务不是把 0.33 元硬压到 0.05 元，而是验证 0.33 元能否在更大受众池里稳定复现。</strong> 复现成功，再用预算换量；复现失败，再回头修素材、落地页和读者质量。增长不是许愿，先把单位经济模型算清楚，后面的动作才不会跑偏。</p>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/7926e49e1567/</id>
    <link href="https://blog.zisheng.pro/posts/7926e49e1567/"/>
    <published>2026-07-07T02:45:00.000Z</published>
    <summary>很多投流问题看起来像优化问题，本质却是数学约束。本文用「一天 100 元预算、目标 2000 个成效」这个例子，拆解目标单次成本、当前成本、受众规模和放量节奏之间的关系，给出一个判断框架：什么时候该优化素材，什么时候该扩受众，什么时候必须承认预算目标不现实。</summary>
    <title>Facebook广告预算反推：一天100元想拿2000成效现实吗</title>
    <updated>2026-09-18T08:15:28.629Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="工具" scheme="https://blog.zisheng.pro/categories/%E5%B7%A5%E5%85%B7/"/>
    <category term="macOS" scheme="https://blog.zisheng.pro/tags/macOS/"/>
    <category term="键盘" scheme="https://blog.zisheng.pro/tags/%E9%94%AE%E7%9B%98/"/>
    <category term="效率" scheme="https://blog.zisheng.pro/tags/%E6%95%88%E7%8E%87/"/>
    <content>
      <![CDATA[<p>用 macOS 写代码或者写文章，有个小事经常让我抓狂：长按 <code>Backspace</code> 删内容，前半秒没动静，开始动了又慢得像在爬。这个体感问题不大，但它会在你每天反复删除、修改、回退的时候持续消耗效率。</p><h2 id="一句话总结"><a href="#一句话总结" class="headerlink" title="一句话总结"></a>一句话总结</h2><p>系统偏好设置 → 键盘，把「键重复速率」和「重复前延迟」两个滑块都拉到最右边，如果还嫌不够快，用两条终端命令突破 GUI 上限。</p><h2 id="在系统设置里调"><a href="#在系统设置里调" class="headerlink" title="在系统设置里调"></a>在系统设置里调</h2><p>打开<strong>系统设置 → 键盘</strong>（直接 Spotlight 搜「键盘」），你会看到类似下面这个界面：</p><p><img src="/images/macos-keyboard-repeat-settings.webp" alt="macOS 键盘设置面板"></p><p>两个关键滑块：</p><table><thead><tr><th>设置项</th><th>作用</th><th>推荐</th></tr></thead><tbody><tr><td><strong>键重复速率</strong></td><td>长按后每秒触发多少次按键</td><td>拖到最右「快」</td></tr><tr><td><strong>重复前延迟</strong></td><td>按住多久后开始重复</td><td>拖到最右「短」</td></tr></tbody></table><p>改完立刻生效，不需要重启。</p><h2 id="系统设置不够用？用终端突破上限"><a href="#系统设置不够用？用终端突破上限" class="headerlink" title="系统设置不够用？用终端突破上限"></a>系统设置不够用？用终端突破上限</h2><p>GUI 滑块有最小值限制，终端可以设置更激进的数字：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 重复速度（数字越小越快，GUI 最小值是 2，这里设 1）</span></span><br><span class="line">defaults write NSGlobalDomain KeyRepeat -int 1</span><br><span class="line"></span><br><span class="line"><span class="comment"># 开始重复前的延迟（数字越小延迟越短，GUI 最小值约 15，这里设 10）</span></span><br><span class="line">defaults write NSGlobalDomain InitialKeyRepeat -int 10</span><br></pre></td></tr></table></figure><p><strong>改完需要注销重新登录</strong>才能生效。</p><p>单位说明：<code>KeyRepeat</code> 的单位是 15ms&#x2F;tick，设 1 &#x3D; 15ms 重复一次（约 66次&#x2F;秒）；<code>InitialKeyRepeat</code> 单位也是 15ms，设 10 &#x3D; 150ms 后开始重复。</p><h2 id="我的判断框架"><a href="#我的判断框架" class="headerlink" title="我的判断框架"></a>我的判断框架</h2><p><strong>值得上</strong>：任何重度键盘用户，尤其是写代码、写长文的，改完体感提升很明显，Backspace 终于跟上思维速度了。</p><p><strong>可以再等</strong>：偶尔写写邮件、主要用鼠标操作的用户，默认值就够了，改太激进反而容易误触删多。</p><p><strong>注意</strong>：如果用的是外接键盘，部分键盘固件有自己的重复速度设置，系统设置可能对其无效，需要去键盘配套软件里改（比如 VIA、QMK）。</p>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/f1449d85feaf/</id>
    <link href="https://blog.zisheng.pro/posts/f1449d85feaf/"/>
    <published>2026-07-06T12:00:00.000Z</published>
    <summary>macOS 默认的键盘重复速度对重度键盘用户来说偏慢，长按 Backspace 删文字像在等动画播完。本文结合系统设置截图，说明键重复速率和重复前延迟怎么调，以及什么时候需要用终端突破 GUI 限制，适合程序员和写作重度用户。</summary>
    <title>macOS 长按 Backspace 删除太慢？改这两个设置</title>
    <updated>2026-09-18T08:15:28.632Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="运营" scheme="https://blog.zisheng.pro/categories/%E8%BF%90%E8%90%A5/"/>
    <category term="Facebook Ads" scheme="https://blog.zisheng.pro/tags/Facebook-Ads/"/>
    <category term="流量变现" scheme="https://blog.zisheng.pro/tags/%E6%B5%81%E9%87%8F%E5%8F%98%E7%8E%B0/"/>
    <category term="版位选择" scheme="https://blog.zisheng.pro/tags/%E7%89%88%E4%BD%8D%E9%80%89%E6%8B%A9/"/>
    <category term="内容站" scheme="https://blog.zisheng.pro/tags/%E5%86%85%E5%AE%B9%E7%AB%99/"/>
    <category term="AdX" scheme="https://blog.zisheng.pro/tags/AdX/"/>
    <content>
      <![CDATA[<p>我在跑几个英语小说站的流量变现（romance、paranormal、fantasy 题材，AdX 广告收益模式）。Facebook 广告是主要拉新渠道，目标受众是 25-45 岁英语女性读者。</p><p>跑了几个月下来，版位选择这件事看似简单，实则有不少坑。记录一下我的判断框架。</p><h2 id="一句话总结"><a href="#一句话总结" class="headerlink" title="一句话总结"></a>一句话总结</h2><p><strong>动态信息流选 Facebook + Instagram + Instagram 探索，快拍&#x2F;Reels 看素材，Marketplace 和 Messenger 全跳过。</strong></p><hr><h2 id="动态信息流版位"><a href="#动态信息流版位" class="headerlink" title="动态信息流版位"></a>动态信息流版位</h2><p>这是流量最大、质量最稳的版位类型，优先配置。</p><p><strong>必选：</strong></p><ul><li><strong>Facebook 动态</strong> — 基本盘，CPM 稳定，受众覆盖广</li><li><strong>Instagram 动态</strong> — 小说受众在 Instagram 的核心阵地，点击质量高</li><li><strong>Instagram 探索首页</strong> — 用户在主动发现新内容，意图强，适合拉新</li></ul><p><strong>可跳过：</strong></p><ul><li><strong>Facebook 主页动态 &#x2F; Instagram 主页动态</strong> — 量极小，意义不大</li><li><strong>Facebook 右边栏</strong> — 纯桌面端，CTR 低得离谱</li><li><strong>Threads 动态</strong> — 受众偏科技&#x2F;新闻，不匹配 romance fiction 读者画像</li><li><strong>Facebook 通知</strong> — 广告出现在通知栏，用户体验极差，反感率高</li></ul><p><strong>坑：Facebook Marketplace</strong></p><p>Facebook 经常在「复制广告组」时默认勾选 Marketplace。<strong>去掉它。</strong> Marketplace 的用户在找二手商品，购物心态和阅读心态完全不同，点进小说站后跳出率极高，白烧预算。</p><hr><h2 id="快拍-Reels-版位"><a href="#快拍-Reels-版位" class="headerlink" title="快拍 &amp; Reels 版位"></a>快拍 &amp; Reels 版位</h2><p>这类版位<strong>依赖素材格式</strong>，不能无脑勾。</p><p><strong>前提：素材是竖版视频（9:16）才开，图片素材直接跳过整组。</strong></p><p>在有竖版视频素材的情况下：</p><ul><li><strong>Instagram 快拍</strong> — 优先，受众活跃</li><li><strong>Instagram Reels</strong> — 优先，算法推流强，新受众覆盖好</li><li><strong>Facebook 快拍</strong> — 次选，补量用</li><li><strong>Facebook Reels</strong> — 可选，跑量后看 CPM 决定保不保留</li></ul><p><strong>跳过：</strong></p><ul><li><strong>Messenger 快拍</strong> — 场景割裂，用户不在内容消费状态，CTR 极低</li><li><strong>Reels 插播广告 &#x2F; Reels 内嵌广告</strong> — 素材被强制裁剪，图片广告在这里投放效果极差</li></ul><hr><h2 id="搜索结果版位"><a href="#搜索结果版位" class="headerlink" title="搜索结果版位"></a>搜索结果版位</h2><ul><li><strong>Instagram 搜索结果</strong> — 值得开。用户主动搜索 romance、dark fiction 等词时出现，意图明确</li><li><strong>Facebook 搜索结果</strong> — 跳过。Facebook 搜索使用率极低，量小到忽略不计</li></ul><hr><h2 id="别被系统建议带跑"><a href="#别被系统建议带跑" class="headerlink" title="别被系统建议带跑"></a>别被系统建议带跑</h2><p>Facebook「复制广告组」时会推荐几条建议，值得单独说一下：</p><table><thead><tr><th>建议</th><th>操作</th></tr></thead><tbody><tr><td>添加 Instagram 版位（降低 23% CPA）</td><td>可接受，方向对</td></tr><tr><td>将预算增加至与同类广告组一致</td><td>跳过，这是 Meta 的增收话术</td></tr><tr><td>发送 Messenger 消息</td><td>坚决拒绝，会把广告目标从「流量」切换成「对话」，Pixel 事件全断</td></tr></tbody></table><hr><h2 id="我的判断框架"><a href="#我的判断框架" class="headerlink" title="我的判断框架"></a>我的判断框架</h2><p><strong>版位能不能开，核心看两件事：</strong></p><ol><li><strong>受众在不在那里</strong> — romance 读者的主力阵地是 Instagram 和 Facebook 动态，不是 Marketplace、不是 Messenger</li><li><strong>素材格式匹不匹配</strong> — 图片素材不要碰竖版视频版位，被系统裁剪后效果等于废</li></ol><p>按这两个维度过滤一遍，版位选择基本不会踩坑。</p><p>WhatsApp 动态不用考虑，受众集中在印度&#x2F;巴西&#x2F;东南亚，和英语小说站的目标市场完全不重叠。</p>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/6797aa7ce19a/</id>
    <link href="https://blog.zisheng.pro/posts/6797aa7ce19a/"/>
    <published>2026-07-06T12:00:00.000Z</published>
    <summary>运营英语小说站近半年，Facebook 广告版位踩了不少坑。这篇文章整理我实际跑量的版位选择逻辑——哪些是核心版位、哪些是陷阱、哪些要看素材类型才能开——给同类内容站（小说、短文、UGC）的运营参考。</summary>
    <title>小说站 Facebook 广告版位选择指南</title>
    <updated>2026-09-18T08:15:28.640Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="AI" scheme="https://blog.zisheng.pro/categories/AI/"/>
    <category term="Facebook Ads" scheme="https://blog.zisheng.pro/tags/Facebook-Ads/"/>
    <category term="流量变现" scheme="https://blog.zisheng.pro/tags/%E6%B5%81%E9%87%8F%E5%8F%98%E7%8E%B0/"/>
    <category term="Meta Pixel" scheme="https://blog.zisheng.pro/tags/Meta-Pixel/"/>
    <category term="小说站" scheme="https://blog.zisheng.pro/tags/%E5%B0%8F%E8%AF%B4%E7%AB%99/"/>
    <category term="Android" scheme="https://blog.zisheng.pro/tags/Android/"/>
    <category term="ATT" scheme="https://blog.zisheng.pro/tags/ATT/"/>
    <content>
      <![CDATA[<p>最近在给自己的小说站跑 Facebook 广告，一个不起眼的配置让我琢磨了很久：设备定向要不要排除 iOS？</p><p>直觉上觉得应该不排，覆盖更多用户嘛。但实际想清楚之后，我做了一个反直觉的决定：<strong>只投 Android，最低 Android 9，iOS 暂时全排除</strong>。</p><p>这个决策背后有技术原因，不是玄学。</p><h2 id="一句话总结"><a href="#一句话总结" class="headerlink" title="一句话总结"></a>一句话总结</h2><p>iOS 14.5+ 的 ATT 政策让 Meta Pixel 无法回传事件，广告系列的优化方向会跑偏；在没有 CAPI（Conversions API）的情况下，投 iOS 等于把预算喂给算法噪音。</p><h2 id="iOS-信号丢失：不只是归因问题"><a href="#iOS-信号丢失：不只是归因问题" class="headerlink" title="iOS 信号丢失：不只是归因问题"></a>iOS 信号丢失：不只是归因问题</h2><p>先讲技术机制。</p><p>iOS 14.5 之后 Apple 推出了 ATT（App Tracking Transparency），要求所有应用在追踪用户行为前必须显式获得授权。绝大多数用户选择「不允许追踪」。</p><p>这对 Meta Pixel 的影响是致命的：</p><ol><li>用户在 FB 上看到广告，点击进入小说站</li><li>小说站的 Pixel 代码触发 <code>ViewContent</code> 事件，准备回传给 FB</li><li><strong>iOS 系统层面直接拦截</strong>，事件永远到不了 Meta 服务器</li></ol><p>结果是：FB 的广告系列认为这批 iOS 用户点了广告之后「什么都没发生」。算法会把这批流量标记为低质量，调整投放方向——但这个调整方向是错的，因为信息本身就是残缺的。</p><p>更严重的问题是：<strong>这不只是归因断裂，而是优化方向整体跑偏</strong>。</p><p>你以为广告系列在学习「哪些人喜欢看小说」，实际上它在学习「哪些 Android 用户点击了广告」（因为 Android 的 Pixel 信号是完整的）混合「哪些 iOS 用户不知道为什么点了」（信号丢失的噪音）。两个群体混在一起喂给算法，学习效率会被拉低。</p><h2 id="为什么选-Android-9-作为最低版本"><a href="#为什么选-Android-9-作为最低版本" class="headerlink" title="为什么选 Android 9 作为最低版本"></a>为什么选 Android 9 作为最低版本</h2><p>排除 iOS 之后，还需要设一个 Android 的最低系统版本。我选了 <strong>Android 9（Pie）</strong>，原因很具体：</p><p><strong>技术侧</strong>：我的小说站用 Next.js 静态导出，前端依赖了几个关键的现代 Web API：</p><ul><li><code>IntersectionObserver</code>：AdSense 懒加载和 ChapterPixel 的章节完成检测都依赖这个 API，Android 8 以上才稳定支持</li><li>CSS Grid &#x2F; Gap &#x2F; Aspect-ratio：Android 9 以上渲染稳定，Android 8 的 WebView 有已知 bug</li></ul><p><strong>运营侧</strong>：Android 8 发布于 2017 年，2026 年在用 Android 8 的用户基本是 5 年以上没换机的存量，购买力和活跃度都很低。排除掉这批人不影响核心受众覆盖，还能减少无效曝光。</p><p>实际覆盖：最低 Android 9 能覆盖 <strong>约 98% 的活跃 Android 用户</strong>。</p><h2 id="只投-Android-对费用的影响"><a href="#只投-Android-对费用的影响" class="headerlink" title="只投 Android 对费用的影响"></a>只投 Android 对费用的影响</h2><p>信号质量之外，设备选择还直接影响投放成本。</p><p><strong>CPM（千次曝光成本）</strong>：iOS 广告主竞争密度更高，因为多数广告主默认认为 iOS 用户消费力强。实际测下来，仅投 Android 的 CPM 通常低 15–40%，同预算能买到更多曝光。</p><p><strong>单次成效费用（CPR）</strong>：这个才是核心指标。CPR 低不只因为 CPM 低，更因为归因完整——算法清楚知道哪次点击带来了 ViewContent，出价越来越精准。iOS 混进来后，算法收到的是残缺信号，出价逻辑会变得保守且不稳定。</p><p><strong>学习期</strong>：Facebook 广告系列需要积累 50 次转化事件才能脱离学习期。Android 信号完整，50 次的门槛更快达到；iOS 混进来后，部分转化事件丢失，学习期被拉长，预算在不确定状态下消耗更多。</p><p>什么时候 Android 反而更贵：如果落地页在 Android 低端机上加载慢（碎片化严重），跳出率高会把 CPR 拉上去。这也是为什么要设最低 Android 9——过滤低端慢机，间接保住落地页体验。</p><h2 id="操作路径"><a href="#操作路径" class="headerlink" title="操作路径"></a>操作路径</h2><p>在 FB 广告管理工具里，广告组层级配置：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">广告组 → 版位 → 手动版位</span><br><span class="line"></span><br><span class="line">设备类型：仅限移动设备</span><br><span class="line">操作系统：仅限 Android（选移动设备后出现）</span><br><span class="line">操作系统版本：最低 Android 9 / 最高：无</span><br></pre></td></tr></table></figure><p>注意：这个设置在「手动版位」里，先切换到手动版位才能看到 OS 筛选项。「进阶赋能型版位（推荐）」模式下无法手动指定 OS。</p><h2 id="什么时候回开-iOS"><a href="#什么时候回开-iOS" class="headerlink" title="什么时候回开 iOS"></a>什么时候回开 iOS</h2><p>当前排除 iOS 是因为没有 CAPI，信号丢失没有补救手段。有两个条件满足任意一个，就可以考虑回开：</p><p><strong>条件一：CAPI 上线</strong>。Conversions API 是服务端事件回传，绕过 iOS 的浏览器层拦截，直接从服务器把事件推给 Meta。上了 CAPI 之后，iOS 用户的 ViewContent 等核心事件可以正常回传，算法能重新学习这批用户。</p><p>实现门槛不低——静态站（Next.js output: ‘export’）没有运行中的 Node.js 服务器，需要额外部署一个 API 服务（Vercel Function &#x2F; Cloudflare Worker &#x2F; AWS Lambda 都可以）。当前日耗 &lt;$50，性价比不足，等规模上来再搞。</p><p><strong>条件二：GA4 数据证明 iOS 质量更高</strong>。接入 GA4 后，可以按操作系统分组看读者质量指标：人均阅读章节数、平均停留时长、跳出率。如果 iOS 用户这些指标显著优于 Android，那即使信号丢失，从流量质量角度也值得回开——只是归因会不准，但读者是真实的。</p><h2 id="我的判断框架"><a href="#我的判断框架" class="headerlink" title="我的判断框架"></a>我的判断框架</h2><table><thead><tr><th>场景</th><th>建议</th></tr></thead><tbody><tr><td>日耗 &lt; $50，没有 CAPI，没有 GA4 数据</td><td><strong>只投 Android（当前）</strong>，先把算法喂干净的信号</td></tr><tr><td>GA4 数据显示 iOS 读者质量 ≥ Android</td><td><strong>考虑回开 iOS</strong>，接受归因不准的代价换流量</td></tr><tr><td>CAPI 上线，信号丢失消除</td><td><strong>回开 iOS</strong>，此时投全量设备效益最大</td></tr><tr><td>日耗 &gt; $100，CAPI 还没上</td><td><strong>重点关注</strong>，Signal Loss 警告会出现，CAPI ROI 开始合算</td></tr></tbody></table><p>当前我的判断：<strong>可以再等</strong>。先把 GA4 接进来，有了读者质量数据之后再决定 iOS 是否值得回开。</p><p>这个决策不是永久的，是阶段性的——在信号不完整的情况下，优先喂给算法干净的数据，比追求覆盖面更重要。</p>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/e7f31df10b7c/</id>
    <link href="https://blog.zisheng.pro/posts/e7f31df10b7c/"/>
    <published>2026-07-05T13:00:00.000Z</published>
    <summary>在跑小说站 Facebook 广告的过程中，我选择了只投 Android、排除 iOS。不是因为 iOS 用户少，而是 iOS 14.5+ 之后的 ATT 政策让 Meta Pixel 的信号回传彻底断裂——算法学到的根本不是「爱读小说的人」。本文从技术原理到广告组配置，完整讲清楚这个决策，以及什么时候该回开 iOS。</summary>
    <title>Facebook 广告投小说站：为什么只投 Android，不投 iOS</title>
    <updated>2026-09-18T08:15:28.629Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="AI" scheme="https://blog.zisheng.pro/categories/AI/"/>
    <category term="Facebook Pixel" scheme="https://blog.zisheng.pro/tags/Facebook-Pixel/"/>
    <category term="Meta 广告" scheme="https://blog.zisheng.pro/tags/Meta-%E5%B9%BF%E5%91%8A/"/>
    <category term="Next.js" scheme="https://blog.zisheng.pro/tags/Next-js/"/>
    <category term="小说站" scheme="https://blog.zisheng.pro/tags/%E5%B0%8F%E8%AF%B4%E7%AB%99/"/>
    <category term="前端踩坑" scheme="https://blog.zisheng.pro/tags/%E5%89%8D%E7%AB%AF%E8%B8%A9%E5%9D%91/"/>
    <content>
      <![CDATA[<p>最近在给几个小说站点（Next.js 静态导出）接入 Facebook Pixel 做社交流量变现，过程里踩了一堆坑。这些坑不是 API 文档写错了，而是”文档说清楚了但你不知道这是个坑”——前端转 AI&#x2F;增长方向最容易踩的那种。</p><p>整理成 6 个问题，给同样在做内容站 + FB 投流的人参考。</p><h2 id="一句话总结"><a href="#一句话总结" class="headerlink" title="一句话总结"></a>一句话总结</h2><p>Facebook Pixel 接入表面是三行代码的事，实际上涉及浏览器追踪、事件分层、两个完全独立的 Meta 后台工具、广告算法优化方向——每一层都有不明显的陷阱。</p><hr><h2 id="坑-1：AEM-已死，别再用这个词"><a href="#坑-1：AEM-已死，别再用这个词" class="headerlink" title="坑 1：AEM 已死，别再用这个词"></a>坑 1：AEM 已死，别再用这个词</h2><p>接入之前我研究了一堆文章，大量英文资料在讲 AEM（Aggregated Event Measurement）——iOS 14 之后 Meta 引入的事件聚合测量机制，当时限制每域名 8 个优先级事件槽。</p><p>结果打开 Meta 事件管理工具，完全找不到 AEM 的配置入口。</p><p><strong>原因</strong>：Meta 在 2023 年左右悄悄把这套机制改名了，现在叫「广告组转化事件」，UI 路径在广告管理工具 → 广告组 → 广告组转化事件（不在事件管理工具里）。</p><p>原来的 8 槽限制也放开了，现在可以正常选任意自定义转化事件作为优化目标，不再有数量硬上限。</p><p><strong>教训</strong>：看到 AEM 相关资料，先确认发布时间。2023 年之前的文章大概率在讲一个已经不存在的界面。</p><hr><h2 id="坑-2：三层事件，搞清楚再动手"><a href="#坑-2：三层事件，搞清楚再动手" class="headerlink" title="坑 2：三层事件，搞清楚再动手"></a>坑 2：三层事件，搞清楚再动手</h2><p>这是最容易搞混的一块，我在广告管理工具里死活找不到 <code>ChapterRead50</code> 事件，折腾了很久才搞清楚原因。</p><p>FB Pixel 有三种完全不同的东西：</p><h3 id="标准事件（Standard-Events）"><a href="#标准事件（Standard-Events）" class="headerlink" title="标准事件（Standard Events）"></a>标准事件（Standard Events）</h3><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="title function_">fbq</span>(<span class="string">&#x27;track&#x27;</span>, <span class="string">&#x27;ViewContent&#x27;</span>, &#123; <span class="attr">content_type</span>: <span class="string">&#x27;chapter&#x27;</span>, <span class="attr">content_name</span>: title &#125;)</span><br></pre></td></tr></table></figure><p>Meta 内置，有固定的事件名列表（ViewContent、Purchase、Lead、AddToCart 等）。<strong>可以直接在广告管理工具里选为优化目标</strong>，不需要任何额外配置。</p><h3 id="自定义事件（Custom-Events）"><a href="#自定义事件（Custom-Events）" class="headerlink" title="自定义事件（Custom Events）"></a>自定义事件（Custom Events）</h3><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="title function_">fbq</span>(<span class="string">&#x27;trackCustom&#x27;</span>, <span class="string">&#x27;ChapterRead50&#x27;</span>, &#123; <span class="attr">content_name</span>: title, <span class="attr">chapter_order</span>: <span class="number">1</span> &#125;)</span><br></pre></td></tr></table></figure><p>你自己取名字，Meta 不认识这个名字。<strong>在广告管理工具的转化事件下拉里根本不会出现</strong>——这就是我卡住的地方。</p><h3 id="自定义转化（Custom-Conversions）"><a href="#自定义转化（Custom-Conversions）" class="headerlink" title="自定义转化（Custom Conversions）"></a>自定义转化（Custom Conversions）</h3><p>在<strong>事件管理工具</strong>里创建的规则，把自定义事件包装成 Meta 认识的转化目标：规则 &#x3D; “当收到 <code>ChapterRead50</code> 事件时，记为一次转化”。</p><p>创建完自定义转化之后，才能在广告管理工具的广告组里选它作为优化事件。</p><p><strong>数据流</strong>：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">网站代码 fbq(&#x27;trackCustom&#x27;, &#x27;ChapterRead50&#x27;)</span><br><span class="line">  → 事件管理工具接收原始事件</span><br><span class="line">  → 你手动创建自定义转化（规则：事件 = ChapterRead50）</span><br><span class="line">  → 广告管理工具广告组：转化事件选「阅读章节 50%」（这个名字你自己取）</span><br></pre></td></tr></table></figure><p><code>ViewContent</code> 是标准事件，跳过中间步骤直接可选。<code>ChapterRead50</code> &#x2F; <code>ChapterCompleted</code> &#x2F; <code>TimeOnPage30</code> 是自定义事件，必须先包装。</p><hr><h2 id="坑-3：ChapterCompleted-触发时机错了"><a href="#坑-3：ChapterCompleted-触发时机错了" class="headerlink" title="坑 3：ChapterCompleted 触发时机错了"></a>坑 3：ChapterCompleted 触发时机错了</h2><p>我最开始的实现逻辑：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="title function_">onScroll</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> ratio = <span class="variable language_">window</span>.<span class="property">scrollY</span> / (<span class="variable language_">document</span>.<span class="property">body</span>.<span class="property">scrollHeight</span> - <span class="variable language_">window</span>.<span class="property">innerHeight</span>)</span><br><span class="line">  <span class="keyword">if</span> (!firedCompleted.<span class="property">current</span> &amp;&amp; ratio &gt;= <span class="number">0.9</span>) &#123;</span><br><span class="line">    firedCompleted.<span class="property">current</span> = <span class="literal">true</span></span><br><span class="line">    <span class="title function_">fbq</span>(<span class="string">&#x27;trackCustom&#x27;</span>, <span class="string">&#x27;ChapterCompleted&#x27;</span>, payload)</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>看起来没问题——滚动到 90% 触发章节完成。</p><p><strong>实际问题</strong>：<code>document.body.scrollHeight</code> 包含正文下方的”YOU MIGHT ALSO LIKE”推荐区。这块区域通常占页面高度的 15-25%。</p><p>结果：</p><ul><li>如果 90% 触发，用户需要滚进推荐区才算”读完”，触发时机太晚</li><li>如果改低阈值，推荐区越长的页面误差越大，无法准确对应正文结束位置</li></ul><p><strong>正确做法</strong>：在正文末尾、推荐区之前放一个哨兵 div：</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">&#123;<span class="comment">/* 正文内容区 */</span>&#125;</span><br><span class="line">&lt;<span class="title class_">AdsenseSlot</span> slot=<span class="string">&quot;9124751639&quot;</span> /&gt;</span><br><span class="line"></span><br><span class="line">&#123;<span class="comment">/* 哨兵：ChapterCompleted 在此触发 */</span>&#125;</span><br><span class="line">&lt;div id=<span class="string">&quot;chapter-content-end&quot;</span> /&gt;</span><br><span class="line"></span><br><span class="line">&#123;<span class="comment">/* YOU MIGHT ALSO LIKE 推荐区 */</span>&#125;</span><br><span class="line">&lt;div className=<span class="string">&quot;recommendations&quot;</span>&gt;...&lt;/div&gt;</span><br></pre></td></tr></table></figure><p>然后用 IntersectionObserver 监听这个元素进入视口：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> sentinel = <span class="variable language_">document</span>.<span class="title function_">getElementById</span>(<span class="string">&#x27;chapter-content-end&#x27;</span>)</span><br><span class="line"><span class="keyword">if</span> (sentinel) &#123;</span><br><span class="line">  <span class="keyword">const</span> io = <span class="keyword">new</span> <span class="title class_">IntersectionObserver</span>(<span class="function">(<span class="params">entries</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> (entries[<span class="number">0</span>].<span class="property">isIntersecting</span> &amp;&amp; !firedCompleted.<span class="property">current</span>) &#123;</span><br><span class="line">      firedCompleted.<span class="property">current</span> = <span class="literal">true</span></span><br><span class="line">      <span class="title function_">fbq</span>(<span class="string">&#x27;trackCustom&#x27;</span>, <span class="string">&#x27;ChapterCompleted&#x27;</span>, payload)</span><br><span class="line">      io.<span class="title function_">disconnect</span>()</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;, &#123; <span class="attr">threshold</span>: <span class="number">0.1</span> &#125;)</span><br><span class="line">  io.<span class="title function_">observe</span>(sentinel)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>好处：不依赖高度计算，哨兵放哪就在哪触发，推荐区多长都不影响。</p><hr><h2 id="坑-4：两个后台工具职责完全不同"><a href="#坑-4：两个后台工具职责完全不同" class="headerlink" title="坑 4：两个后台工具职责完全不同"></a>坑 4：两个后台工具职责完全不同</h2><p>很多人（包括我最开始）把事件管理工具和广告管理工具搞混。</p><table><thead><tr><th>工具</th><th>URL</th><th>干什么</th></tr></thead><tbody><tr><td>事件管理工具</td><td>eventsmanager.facebook.com&#x2F;events_manager2</td><td>数据层：看 Pixel 收到的事件、测试验证、建自定义转化</td></tr><tr><td>广告管理工具</td><td>adsmanager.facebook.com&#x2F;adsmanager&#x2F;manage&#x2F;campaigns</td><td>投放层：建广告系列、选优化目标、设预算、花钱</td></tr></tbody></table><p><strong>关键链路</strong>：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">事件管理工具：验证 ChapterRead50 收到了 → 创建自定义转化</span><br><span class="line">           ↓</span><br><span class="line">广告管理工具：广告组 → 转化事件 → 选「阅读章节 50%」</span><br></pre></td></tr></table></figure><p>自定义事件必须先在事件管理工具里”包装”成自定义转化，才能在广告管理工具的下拉列表里出现。两个工具缺一不可，操作有顺序。</p><p>另外，事件管理工具显示的状态：</p><ul><li><strong>使用中</strong>：近期有事件触发</li><li><strong>已暂停</strong>：0 次事件，Meta 自动暂停——<strong>不影响选择，也不需要手动激活</strong>，有真实流量触发后自动切回使用中</li></ul><hr><h2 id="坑-5：Meta-广告系列目标创建后锁死，必须新建"><a href="#坑-5：Meta-广告系列目标创建后锁死，必须新建" class="headerlink" title="坑 5：Meta 广告系列目标创建后锁死，必须新建"></a>坑 5：Meta 广告系列目标创建后锁死，必须新建</h2><p>从冷启动阶段（流量目标 &#x2F; PageView 优化）升级到 ViewContent 优化阶段，需要的不是”修改广告组的优化事件”，而是<strong>新建一条销量目标的广告系列</strong>。</p><p>原因：广告系列目标（流量 &#x2F; 销量 &#x2F; 品牌知名度等）在广告系列层面设置，创建后<strong>不能修改</strong>。优化事件在广告组层面，但广告组的可用优化事件范围受广告系列目标约束——流量目标的广告系列里，无法选「销量」相关的转化事件。</p><p><strong>操作路径</strong>：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">新建广告系列</span><br><span class="line">  → 广告目标：销量</span><br><span class="line">  → 转化位置：网站</span><br><span class="line">  → 广告组：转化事件选 ViewContent</span><br><span class="line">  → 成效目标：转化量最大化</span><br></pre></td></tr></table></figure><p>不要试图在已有的流量广告系列里改，改不了。</p><hr><h2 id="坑-6：CAPI-在静态导出站不是零成本"><a href="#坑-6：CAPI-在静态导出站不是零成本" class="headerlink" title="坑 6：CAPI 在静态导出站不是零成本"></a>坑 6：CAPI 在静态导出站不是零成本</h2><p>很多教程推荐同时上 Browser Pixel + Conversions API（CAPI），理由是 iOS 14 之后浏览器 Pixel 会丢 20-40% 信号。</p><p>这个说法本身没错，但有个前提：你得有服务端。</p><p>我的站点用 Next.js <code>output: &#39;export&#39;</code> 静态导出，全站 HTML&#x2F;CSS&#x2F;JS 推 CDN，<strong>没有运行中的 Node.js 服务</strong>。CAPI 需要从服务端调用 Meta Graph API 回传事件，这意味着：</p><ul><li><strong>方案 A</strong>：改用 Vercel 部署（支持 Next.js Route Handler），在 <code>app/api/fb-event/route.ts</code> 里回传</li><li><strong>方案 B</strong>：单独部署 Cloudflare Worker 或 Vercel Function 作为中转</li><li><strong>方案 C</strong>：Meta 官方 CAPI Gateway（零后端成本，但灵活性差）</li></ul><p>每个方案都有额外成本（架构成本或金钱成本）。当前阶段流量不大、日耗不高，浏览器 Pixel 足够，CAPI 等 Events Manager 出现”信号丢失”警告再评估。</p><p>另外 CAPI 和 Browser Pixel 同时上报同一事件时，需要用 <code>event_id</code> 去重——前端生成 UUID 写 Cookie，服务端从 Cookie 读同一个 ID 再回传，这套打通有额外工程量。</p><hr><h2 id="要不要上-我的判断框架"><a href="#要不要上-我的判断框架" class="headerlink" title="要不要上 &#x2F; 我的判断框架"></a>要不要上 &#x2F; 我的判断框架</h2><table><thead><tr><th>条件</th><th>建议</th></tr></thead><tbody><tr><td>刚接 Pixel，流量 &lt; 1000 UV&#x2F;天</td><td>只上 Browser Pixel，先跑通 ViewContent + 自定义转化流程</td></tr><tr><td>ViewContent 稳定 ≥ 50 次&#x2F;周</td><td>新建销量目标广告系列，切 ViewContent 优化</td></tr><tr><td>ChapterRead50 稳定 ≥ 50 次&#x2F;周</td><td>考虑升级第三阶段，切 ChapterRead50 优化</td></tr><tr><td>Events Manager 出现”信号丢失”</td><td>开始评估 CAPI 方案</td></tr><tr><td>日广告消耗 &gt; $100</td><td>优先上 CAPI，信号准确率的收益大于实现成本</td></tr></tbody></table><p>核心判断：先把事件质量做好（正确触发、正确包装自定义转化），再考虑量的问题（CAPI）。顺序搞反了没意义。</p>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/55b8cce2d3ce/</id>
    <link href="https://blog.zisheng.pro/posts/55b8cce2d3ce/"/>
    <published>2026-07-04T18:00:00.000Z</published>
    <summary>为小说站点接入 Facebook Pixel 并对接广告投流，踩了一堆坑。整理 6 个非显而易见的问题：AEM 术语已废弃、三层事件类型搞混、ChapterCompleted 触发时机错误、两个后台工具职责不清、Meta 广告系列目标锁死、CAPI 在静态导出站的额外成本。给同样做内容站投流的前端开发者参考。</summary>
    <title>Facebook Pixel 接入踩坑全记录：从 AEM 到 ChapterCompleted 触发时机</title>
    <updated>2026-09-18T08:15:28.629Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="AI" scheme="https://blog.zisheng.pro/categories/AI/"/>
    <category term="AI Infra" scheme="https://blog.zisheng.pro/tags/AI-Infra/"/>
    <category term="Claude Code" scheme="https://blog.zisheng.pro/tags/Claude-Code/"/>
    <category term="Prompt Engineering" scheme="https://blog.zisheng.pro/tags/Prompt-Engineering/"/>
    <category term="Token Optimization" scheme="https://blog.zisheng.pro/tags/Token-Optimization/"/>
    <content>
      <![CDATA[<p>给 Claude Code 写自定义 Skill 已经有一段时间了。最近做了一次专项 token 优化，顺手记录一下思路和方法。</p><h2 id="一句话总结"><a href="#一句话总结" class="headerlink" title="一句话总结"></a>一句话总结</h2><p>Skill 的 reference 文件不是文档，是 context。每 KB 都是运行时成本。按 phase 拆分文件比压缩内容更有杠杆。</p><hr><h2 id="背景：Skill-的-token-消耗来自哪里"><a href="#背景：Skill-的-token-消耗来自哪里" class="headerlink" title="背景：Skill 的 token 消耗来自哪里"></a>背景：Skill 的 token 消耗来自哪里</h2><p>Claude Code 的 Skill 系统工作原理很简单：用户触发 Skill → <code>SKILL.md</code> 进入 context → 执行时按 phase 按需加载 <code>references/*.md</code>。</p><p>理论上”按需加载”已经很省了。但当某个 reference 文件本身就是 162KB，问题就来了：</p><ul><li>它被加载时就是 162KB 的 context 消耗</li><li>如果这个文件里有一半内容是”当前 phase 不需要的”，那一半就是纯浪费</li></ul><p>我的 <code>cover-allure-elements.md</code> 就是这个情况。它同时服务两个 phase：</p><ul><li><strong>A2</strong>（封面生成）：需要 allure 规则 + costume 模板 + genre prompt 模板</li><li><strong>A2.5</strong>（插图生成）：只需要 allure 规则 + pose 表 + exposure tier，<strong>不需要</strong> genre 完整 prompt 模板（~30KB）</li></ul><hr><h2 id="三类优化，效果从高到低"><a href="#三类优化，效果从高到低" class="headerlink" title="三类优化，效果从高到低"></a>三类优化，效果从高到低</h2><h3 id="1-结构性拆分（最高杠杆）"><a href="#1-结构性拆分（最高杠杆）" class="headerlink" title="1. 结构性拆分（最高杠杆）"></a>1. 结构性拆分（最高杠杆）</h3><p>把”不同 phase 的消费者用不同部分”升级为”不同 phase 加载不同文件”。</p><p><strong>操作</strong>：把 <code>cover-allure-elements.md</code> 里的 15 个 genre prompt 模板（约 380 行、30KB）拆出来，新建 <code>cover-genre-playbook.md</code>。</p><p><strong>SKILL.md reference loading 更新</strong>：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">A2：加载 cover-allure-elements.md + cover-genre-playbook.md</span><br><span class="line">A2.5：只加载 cover-allure-elements.md</span><br></pre></td></tr></table></figure><p><strong>效果</strong>：每次插图生成任务（A2.5）比之前少加载 27KB。这是结构性节省，不是内容压缩——文件总量没变，但每次调用用到的更少了。</p><h3 id="2-跨文件去重（中等杠杆）"><a href="#2-跨文件去重（中等杠杆）" class="headerlink" title="2. 跨文件去重（中等杠杆）"></a>2. 跨文件去重（中等杠杆）</h3><p>审查发现同一段合规说明出现了三次：</p><table><thead><tr><th>位置</th><th>内容</th><th>行数</th></tr></thead><tbody><tr><td><code>SKILL.md</code></td><td>“no nipples&#x2F;genitals&#x2F;sex acts”</td><td>~8 行</td></tr><tr><td><code>cover-allure-elements.md §0</code></td><td>同上，散文版</td><td>~12 行</td></tr><tr><td><code>adsense-arbitrage.md §1.1</code></td><td>同上，加业务背景</td><td>~10 行</td></tr></tbody></table><p>处理方式：<code>cover-allure-elements.md §0</code> 保留权威版（因为它被 A2 和 A2.5 都加载，离使用者最近），另外两处改为 1 行 cross-reference：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">§0 floor: per `cover-allure-elements.md §0` — no explicit content. Suggestive allure fully allowed; push it.</span><br></pre></td></tr></table></figure><p>节省约 22 行。规则只在一个地方维护，更新时不会出现三处不同步的问题。</p><h3 id="3-模板压缩（中低杠杆）"><a href="#3-模板压缩（中低杠杆）" class="headerlink" title="3. 模板压缩（中低杠杆）"></a>3. 模板压缩（中低杠杆）</h3><p><code>cover-allure-elements.md</code> 里有 21 个 costume 类型，每个都是完整的 prompt 模板（visual + best-for + prompt block）。问题在于：模型其实只需要看懂”如何构造 costume 提示词”的方法论，不需要 21 份完整示例。</p><p>操作：保留 5 个最高频的 costume（Academy、Nurse、Silk Robe、Evening Gown、Wet White Shirt），剩余 16 个改为一行列举：</p><blockquote><p>Other costume types (same structure — use Fabric Allure Ranking to construct prompts): Flight Attendant, Police, Maid, Qipao, Wedding…</p></blockquote><p>节省约 200 行。模型用 5 个 exemplar 学会方法论，遇到其他类型自行套用。</p><hr><h2 id="工具：如何快速找到可优化点"><a href="#工具：如何快速找到可优化点" class="headerlink" title="工具：如何快速找到可优化点"></a>工具：如何快速找到可优化点</h2><p>用一个分析 agent 扫描所有 reference 文件，输出：</p><ol><li><strong>跨文件冗余</strong>：同一内容在哪些文件里出现了多次</li><li><strong>verbose prose 转 table</strong>：段落式描述是否可以压缩为表格</li><li><strong>example 密度</strong>：8 个完整示例是否可以替换为 3 个 exemplar + 方法论</li><li><strong>重复 preamble</strong>：每个文件开头的”为什么这很重要”是否是多余重复</li></ol><p>Prompt 给到 agent 时要求输出具体行号和节省估算，否则结果太泛化没法执行。</p><hr><h2 id="一个反直觉的结论"><a href="#一个反直觉的结论" class="headerlink" title="一个反直觉的结论"></a>一个反直觉的结论</h2><p><strong>内容压缩是锦上添花，结构分离才是关键。</strong></p><p>文件从 162KB 压缩到 117KB，感觉很好。但真正节省大的是”A2.5 不再加载 genre playbook”那 27KB——它每次插图任务都生效，是乘法效应。</p><p>类比到普通工程：代码优化一个函数（内容压缩）vs 拆出一个只在特定路径加载的模块（结构分离）。后者的效益通常更高，因为它减少的是每次调用的基础成本。</p><hr><h2 id="顺带发现的合规问题"><a href="#顺带发现的合规问题" class="headerlink" title="顺带发现的合规问题"></a>顺带发现的合规问题</h2><p><code>facebook-ads.md</code> 全文混用了中文术语（广告系列、广告组、类似受众等）。Skill 文件有一条硬规则：必须用英文写。这不是审美问题，是一致性问题——如果 skill 文件被 agent 加载并转述给另一个 agent，中英混杂会增加解析摩擦。</p><p>处理方式是直接 find-replace 全部术语：广告系列 → Campaign，广告组 → Ad Set，类似受众 → Lookalike Audience，等。</p><hr><h2 id="要不要做-判断框架"><a href="#要不要做-判断框架" class="headerlink" title="要不要做 &#x2F; 判断框架"></a>要不要做 &#x2F; 判断框架</h2><p><strong>值得做</strong>：</p><ul><li>Skill 被高频调用（每次写章节、每次生图都触发）</li><li>单个 reference 文件超过 50KB</li><li>同一个文件被不同 phase 消费，但 phase 之间需要的内容有明显差异</li></ul><p><strong>可以再等</strong>：</p><ul><li>Skill 调用频率低（每月几次）</li><li>所有 reference 文件都在 20KB 以下</li><li>文件内容高度内聚，没有明显的 phase 分离机会</li></ul><p><strong>不值得做</strong>：</p><ul><li>为了”显得更整洁”而拆分（增加维护成本，收益低）</li><li>把所有内容压到极限（模型需要足够的示例才能理解方法论，过度压缩反而降质量）</li></ul><hr><p>当前 fiction-site-builder skill 的总 reference 文件大约 830KB，按 phase 加载，单次任务实际加载通常在 100–200KB 区间。优化之后高频 phase（A2.5）少了 27KB，低频 phase（A2）靠新的文件拆分仍然能拿到完整 genre 库。</p><p>整体感受：token 优化不是”写少一点”，而是”用对粒度”。</p>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/c9a29cde60cd/</id>
    <link href="https://blog.zisheng.pro/posts/c9a29cde60cd/"/>
    <published>2026-07-04T14:00:00.000Z</published>
    <summary>记录一次给 Claude Code 自定义 Skill 做 token 瘦身的实践。核心方法是把 reference 文件按&quot;谁在哪个 phase 用&quot;拆分，把同一个合规说明的三份副本压缩成一份，把 21 个 costume 模板压缩为 5 个 exemplar。结果：最重的单个 reference 文件从 162KB 降到 117KB，每次插图生成额外省 27KB 上下文。适合正在构建 Claude Code 自定义 Skill 的开发者。</summary>
    <title>Claude Code Skill Token 优化实践：从 162KB 到 117KB</title>
    <updated>2026-09-18T08:15:28.629Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="AI" scheme="https://blog.zisheng.pro/categories/AI/"/>
    <category term="Google AdSense" scheme="https://blog.zisheng.pro/tags/Google-AdSense/"/>
    <category term="Facebook Pixel" scheme="https://blog.zisheng.pro/tags/Facebook-Pixel/"/>
    <category term="Facebook Ads" scheme="https://blog.zisheng.pro/tags/Facebook-Ads/"/>
    <category term="独立站" scheme="https://blog.zisheng.pro/tags/%E7%8B%AC%E7%AB%8B%E7%AB%99/"/>
    <category term="投流" scheme="https://blog.zisheng.pro/tags/%E6%8A%95%E6%B5%81/"/>
    <category term="流量变现" scheme="https://blog.zisheng.pro/tags/%E6%B5%81%E9%87%8F%E5%8F%98%E7%8E%B0/"/>
    <content>
      <![CDATA[<p>在给小说站接 Facebook Pixel 的过程中，我踩了一个坑：把 ScrollDepth25 和 ScrollDepth75 也想着加进 AEM（聚合事件衡量）的优先级队列——毕竟”用户滚动越深说明越感兴趣”，这个逻辑听起来很合理。</p><p>后来想清楚了，这是在做一件很蠢的事。</p><h2 id="一句话总结"><a href="#一句话总结" class="headerlink" title="一句话总结"></a>一句话总结</h2><p>Facebook Pixel 事件分两层——<strong>转化事件</strong>（优化广告用）和<strong>参与度事件</strong>（诊断 + 建受众用）——用错层等于给算法喂错信号，Facebook 会去找”爱滚动”的人，不是”爱读小说”的人。</p><h2 id="两层事件，本质是信号质量问题"><a href="#两层事件，本质是信号质量问题" class="headerlink" title="两层事件，本质是信号质量问题"></a>两层事件，本质是信号质量问题</h2><p>先看 Facebook 优化目标是怎么工作的：</p><p>你在 Campaign 里设置”优化 ViewContent”，Facebook 就会把你的广告展示给<strong>最可能触发 ViewContent 的人</strong>。算法本质是在学习：哪些人口特征、行为特征的人，最容易产生这个事件。</p><p>所以核心问题是：<strong>你希望 Facebook 去找什么样的人？</strong></p><p>这就是事件分层的本质——不同事件代表不同的用户行为，代表不同质量的人群信号。</p><table><thead><tr><th>层级</th><th>事件</th><th>信号含义</th><th>用法</th></tr></thead><tbody><tr><td><strong>转化事件</strong></td><td>ViewContent</td><td>打开了章节、开始读</td><td>AEM + Campaign 优化目标</td></tr><tr><td><strong>转化事件</strong></td><td>ChapterRead50</td><td>读到章节 50%，真读者</td><td>AEM 高优先级</td></tr><tr><td><strong>转化事件</strong></td><td>ChapterCompleted</td><td>读完整章，深度读者</td><td>AEM 最高优先级</td></tr><tr><td><strong>参与度事件</strong></td><td>PageView</td><td>点进了网站，包括 1 秒就走的</td><td>数据量积累</td></tr><tr><td><strong>参与度事件</strong></td><td>ScrollDepth25</td><td>滚了四分之一，可能在看</td><td>受众建模、行为诊断</td></tr><tr><td><strong>参与度事件</strong></td><td>ScrollDepth75</td><td>真的在读这一章</td><td>高质量 Lookalike 种子</td></tr></tbody></table><h2 id="AEM-的-8-个槽，是资源，不是垃圾桶"><a href="#AEM-的-8-个槽，是资源，不是垃圾桶" class="headerlink" title="AEM 的 8 个槽，是资源，不是垃圾桶"></a>AEM 的 8 个槽，是资源，不是垃圾桶</h2><p>iOS 14+ 之后，苹果的隐私政策让大部分 iOS 用户的广告追踪失效。Facebook 的解决方案是 AEM（Aggregated Event Measurement）：你预先申报最多 8 个事件，按优先级排列，Facebook 用差分隐私的方式帮你归因。</p><p>关键：<strong>只有 8 个槽</strong>，而且优先级顺序决定归因逻辑——当一个 iOS 用户触发了多个事件，Facebook 只把转化归因给其中优先级最高的一个。</p><p>正确的 AEM 配置应该是：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ChapterCompleted (1) &gt; ChapterRead50 (2) &gt; ViewContent (3) &gt; PageView (4)</span><br></pre></td></tr></table></figure><p>ScrollDepth25、ScrollDepth75 <strong>不应该在这个队列里</strong>。原因：</p><ol><li>它们不是转化信号，是过程信号</li><li>浪费宝贵的 AEM 槽位</li><li>如果 ScrollDepth75 的优先级高于 ViewContent，iOS 用户触发了两个事件，归因会错误地落在 ScrollDepth75 而不是 ViewContent</li></ol><h2 id="ScrollDepth-有用，但用法不是”优化”"><a href="#ScrollDepth-有用，但用法不是”优化”" class="headerlink" title="ScrollDepth 有用，但用法不是”优化”"></a>ScrollDepth 有用，但用法不是”优化”</h2><p>有用，但用法是<strong>数据诊断 + 受众分层</strong>，不是优化目标。三个具体用途：</p><p><strong>1. 诊断广告质量</strong></p><p>投一批广告 → 看落地章节的 ScrollDepth75 触发率：</p><ul><li>ScrollDepth75 率高 &#x3D; 这批人真的在读 → 广告找对人了</li><li>ScrollDepth75 率低但 ViewContent 高 &#x3D; 人进来了但不读 → 广告文案&#x2F;封面和内容不匹配</li></ul><p>没有 ScrollDepth，你只知道”有人进来了”，不知道”有没有人在读”。这个区别决定了你该换素材还是换受众，解法完全不同。</p><p><strong>2. 建高质量 Lookalike</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ScrollDepth75 的人 → 建自定义受众 → Lookalike 1%</span><br></pre></td></tr></table></figure><p>比用 PageView 或 ViewContent 建的 Lookalike 质量高得多——这批人是确认的真读者，Facebook 会去找特征相似的人，带来的也更可能是真读者。</p><p><strong>3. 归因分析</strong></p><p>对比 ScrollDepth25 vs ScrollDepth75 的转化漏斗：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">100 人进章节 → 60 人到 25% → 30 人到 75%</span><br></pre></td></tr></table></figure><p>留存率 50%，说明内容前段吸引力还行，后段掉人。这是<strong>内容问题</strong>，不是广告问题——没有 ScrollDepth 数据，这个判断很难做到。</p><hr><p><strong>简单说：ScrollDepth 是侦察兵，不是士兵。</strong></p><p>它不直接打仗（优化 Campaign），但告诉你谁在认真读、广告找的人对不对、内容哪里在掉人。没有它，你做投流决策是靠猜的。</p><h2 id="一个反直觉的坑：PageView-不是越多越好"><a href="#一个反直觉的坑：PageView-不是越多越好" class="headerlink" title="一个反直觉的坑：PageView 不是越多越好"></a>一个反直觉的坑：PageView 不是越多越好</h2><p>初期容易有个误区：PageView 数量越多，数据越充分，广告就越精准。</p><p>实际上 PageView 的信号质量是最低的——包含了点进来 1 秒就关掉的人、误点广告的人、爬虫（如果 Pixel 没做过滤的话）。用 PageView 做优化目标，Facebook 学到的是”什么样的人最容易点广告”，而不是”什么样的人最容易读小说”。</p><p>PageView 的唯一价值是<strong>数据量积累</strong>——新账号冷启动阶段，用它帮 Facebook 快速建立人群基础，之后该切的时候要切。</p><p>当 ViewContent 积累到 500 次，就应该把 Campaign 优化目标从”落地页点击”切换到”ViewContent”。这一步是投流的质变节点。</p><h2 id="我的判断框架"><a href="#我的判断框架" class="headerlink" title="我的判断框架"></a>我的判断框架</h2><p>遇到一个新事件，用两个问题判断它的定位：</p><p><strong>问题 1：这个事件代表的用户，是我真正想要找到的人吗？</strong></p><ul><li>ChapterCompleted 的人 &#x3D; 读完整章 &#x3D; 是我想要的人 ✓</li><li>ScrollDepth25 的人 &#x3D; 滚了一点 &#x3D; 不确定，太模糊 ✗</li></ul><p><strong>问题 2：如果 Facebook 去找更多”触发这个事件的人”，找来的人符合预期吗？</strong></p><ul><li>优化 ChapterRead50 → 找来的人更可能认真读小说 ✓</li><li>优化 ScrollDepth → 找来的人更可能”爱滚动”，不代表会读完 ✗</li></ul><p>通过这两个问题的事件 → 放进转化层，可以用作 AEM 和优化目标。<br>不通过的事件 → 放进参与度层，用来建受众和做诊断。</p><hr><p>具体到我们的小说站，最终实现的事件体系：</p><ul><li><strong>布置在 layout（全局）</strong>：PageView</li><li><strong>布置在章节页 mount（进章节触发）</strong>：ViewContent</li><li><strong>布置在章节页 scroll（读的过程触发）</strong>：ChapterRead50、ChapterCompleted、ScrollDepth25、ScrollDepth75</li></ul><p>前两层进 AEM 配置和 Campaign 优化目标，后面的 scroll 事件全部留在参与度层做受众和诊断用。这个分层是当前最合理的实现，不过随着数据积累，ChapterCompleted 进 AEM 的时机还需要观察。</p>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/2b2ef1726b7e/</id>
    <link href="https://blog.zisheng.pro/posts/2b2ef1726b7e/"/>
    <published>2026-07-04T14:00:00.000Z</published>
    <summary>给小说站接 Facebook Pixel 时，我发现一个反直觉的问题：不是所有自定义事件都该用来优化广告。PageView、ViewContent、ScrollDepth、ChapterCompleted——这些事件的信号质量天壤之别，用错了等于花真金白银训练 Facebook 去找&quot;爱点广告的人&quot;而不是&quot;爱读小说的人&quot;。这篇文章把事件分层讲清楚，给出判断框架。</summary>
    <title>Facebook Pixel 事件分层：用错事件等于训练算法找错人</title>
    <updated>2026-09-18T08:15:28.629Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="AI" scheme="https://blog.zisheng.pro/categories/AI/"/>
    <category term="Google AdSense" scheme="https://blog.zisheng.pro/tags/Google-AdSense/"/>
    <category term="Facebook Pixel" scheme="https://blog.zisheng.pro/tags/Facebook-Pixel/"/>
    <category term="Next.js" scheme="https://blog.zisheng.pro/tags/Next-js/"/>
    <category term="小说站" scheme="https://blog.zisheng.pro/tags/%E5%B0%8F%E8%AF%B4%E7%AB%99/"/>
    <category term="广告套利" scheme="https://blog.zisheng.pro/tags/%E5%B9%BF%E5%91%8A%E5%A5%97%E5%88%A9/"/>
    <category term="scroll tracking" scheme="https://blog.zisheng.pro/tags/scroll-tracking/"/>
    <content>
      <![CDATA[<p>做小说站投流，最容易踩的坑是：用 <code>PageView</code> 作为广告优化事件。</p><p><code>PageView</code> 的语义是「有人来了」，而不是「有人读了」。Facebook 算法拿这个信号去优化，会找到「擅长点广告」的人群，而不是「愿意读小说」的人群。结果就是 CTR 看着不错，但用户进来翻一页就走，AdSense 会话 RPM 极低，ROAS（Return on Ad Spend，广告花费回报率 &#x3D; 广告带来的收入 &#x2F; 广告花费）算下来根本不打平。</p><h2 id="一句话总结"><a href="#一句话总结" class="headerlink" title="一句话总结"></a>一句话总结</h2><p>在章节页实装 3 个 Pixel 事件：ViewContent（进入即触发）、ChapterRead50 和 ChapterCompleted（scroll depth 事件），让 Facebook 算法学会找「真正读完一章」的人，而不是「会点广告」的人。</p><h2 id="为什么-PageView-是最差的优化信号"><a href="#为什么-PageView-是最差的优化信号" class="headerlink" title="为什么 PageView 是最差的优化信号"></a>为什么 PageView 是最差的优化信号</h2><p>小说站的盈利公式：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Profit/visit = (会话 RPM × 翻页数/session) / 1000 − Facebook CPC</span><br></pre></td></tr></table></figure><p>CPC 固定的情况下，唯一能拉高 ROAS 的是「翻页数&#x2F;session」。而翻页数取决于用户是不是真的在读——这正是 <code>PageView</code> 衡量不到的。</p><p>Facebook 的优化逻辑是：给我 50 个「转化事件」的样本，我帮你找更多像这批人的用户。用 <code>PageView</code> 做样本，找到的是「会点广告的人」；用 <code>ChapterCompleted</code>（读完整章）做样本，找到的是「深度读者」。</p><h2 id="Pixel-事件层级设计"><a href="#Pixel-事件层级设计" class="headerlink" title="Pixel 事件层级设计"></a>Pixel 事件层级设计</h2><p>我给章节页设计了 6 层事件，覆盖从「进来了」到「读完了」的完整漏斗：</p><table><thead><tr><th>事件</th><th>触发</th><th>AEM 优先级</th><th>用途</th></tr></thead><tbody><tr><td><code>PageView</code></td><td>全局自动</td><td>4（最低）</td><td>数据量打底，冷启动用</td></tr><tr><td><code>ViewContent</code></td><td>进入章节即触发</td><td>3</td><td>广告优化主信号，积累 500+ 后切优化目标</td></tr><tr><td><code>ScrollDepth25</code></td><td>scroll ≥ 25%</td><td>漏斗分析</td><td>判断 ch1 开头 hook 是否抓住人</td></tr><tr><td><code>ChapterRead50</code></td><td>scroll ≥ 50%</td><td>2</td><td>强意图信号，AEM 次级优先</td></tr><tr><td><code>ScrollDepth75</code></td><td>scroll ≥ 75%</td><td>漏斗分析</td><td>章节中后段内容质量诊断</td></tr><tr><td><code>ChapterCompleted</code></td><td>scroll ≥ <strong>90%</strong></td><td>1（最高）</td><td>最强信号，Lookalike seed</td></tr></tbody></table><blockquote><p><strong>为什么用 90% 而不是 100%？</strong> 章节页底部有广告位、推荐书目、Share 栏。读完正文后用户视线就停了，不会继续滚到绝对底部。90% 才是实际的「读完」代理指标，100% 在现实中几乎永远不触发。</p></blockquote><h2 id="实现：ChapterPixel-组件"><a href="#实现：ChapterPixel-组件" class="headerlink" title="实现：ChapterPixel 组件"></a>实现：ChapterPixel 组件</h2><p>一个轻量的 client component，挂在 chapter page 里，用单个 passive scroll listener + <code>useRef</code> 防重复触发：</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br></pre></td><td class="code"><pre><span class="line"><span class="string">&#x27;use client&#x27;</span></span><br><span class="line"><span class="keyword">import</span> &#123; useEffect, useRef &#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">interface</span> <span class="title class_">Props</span> &#123;</span><br><span class="line">  <span class="attr">chapterTitle</span>: <span class="built_in">string</span></span><br><span class="line">  <span class="attr">chapterOrder</span>: <span class="built_in">number</span></span><br><span class="line">  <span class="attr">bookSlug</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">export</span> <span class="keyword">default</span> <span class="keyword">function</span> <span class="title function_">ChapterPixel</span>(<span class="params">&#123; chapterTitle, chapterOrder, bookSlug &#125;: Props</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> fired25 = <span class="title function_">useRef</span>(<span class="literal">false</span>)</span><br><span class="line">  <span class="keyword">const</span> fired50 = <span class="title function_">useRef</span>(<span class="literal">false</span>)</span><br><span class="line">  <span class="keyword">const</span> fired75 = <span class="title function_">useRef</span>(<span class="literal">false</span>)</span><br><span class="line">  <span class="keyword">const</span> fired90 = <span class="title function_">useRef</span>(<span class="literal">false</span>)</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="keyword">const</span> fbq = (<span class="variable language_">window</span> <span class="keyword">as</span> <span class="title class_">Window</span> &amp; &#123; fbq?: <span class="function">(<span class="params">...args: <span class="built_in">unknown</span>[]</span>) =&gt;</span> <span class="built_in">void</span> &#125;).<span class="property">fbq</span></span><br><span class="line">    <span class="comment">// ViewContent 进入即触发 — 区分「进来了」和「立刻离开」</span></span><br><span class="line">    fbq?.(<span class="string">&#x27;track&#x27;</span>, <span class="string">&#x27;ViewContent&#x27;</span>, &#123;</span><br><span class="line">      <span class="attr">content_type</span>: <span class="string">&#x27;chapter&#x27;</span>,</span><br><span class="line">      <span class="attr">content_name</span>: chapterTitle,</span><br><span class="line">      <span class="attr">content_ids</span>: [<span class="string">`<span class="subst">$&#123;bookSlug&#125;</span>-ch<span class="subst">$&#123;chapterOrder&#125;</span>`</span>],</span><br><span class="line">    &#125;)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">const</span> payload = &#123; <span class="attr">content_name</span>: chapterTitle, <span class="attr">chapter_order</span>: chapterOrder &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">const</span> <span class="title function_">onScroll</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">      <span class="keyword">const</span> total = <span class="variable language_">document</span>.<span class="property">body</span>.<span class="property">scrollHeight</span> - <span class="variable language_">window</span>.<span class="property">innerHeight</span></span><br><span class="line">      <span class="keyword">if</span> (total &lt;= <span class="number">0</span>) <span class="keyword">return</span></span><br><span class="line">      <span class="keyword">const</span> ratio = <span class="variable language_">window</span>.<span class="property">scrollY</span> / total</span><br><span class="line">      <span class="keyword">const</span> fire = (<span class="variable language_">window</span> <span class="keyword">as</span> <span class="title class_">Window</span> &amp; &#123; fbq?: <span class="function">(<span class="params">...args: <span class="built_in">unknown</span>[]</span>) =&gt;</span> <span class="built_in">void</span> &#125;).<span class="property">fbq</span></span><br><span class="line"></span><br><span class="line">      <span class="keyword">if</span> (!fired25.<span class="property">current</span> &amp;&amp; ratio &gt;= <span class="number">0.25</span>) &#123;</span><br><span class="line">        fired25.<span class="property">current</span> = <span class="literal">true</span></span><br><span class="line">        fire?.(<span class="string">&#x27;trackCustom&#x27;</span>, <span class="string">&#x27;ScrollDepth25&#x27;</span>, payload)</span><br><span class="line">      &#125;</span><br><span class="line">      <span class="keyword">if</span> (!fired50.<span class="property">current</span> &amp;&amp; ratio &gt;= <span class="number">0.5</span>) &#123;</span><br><span class="line">        fired50.<span class="property">current</span> = <span class="literal">true</span></span><br><span class="line">        fire?.(<span class="string">&#x27;trackCustom&#x27;</span>, <span class="string">&#x27;ChapterRead50&#x27;</span>, payload)</span><br><span class="line">      &#125;</span><br><span class="line">      <span class="keyword">if</span> (!fired75.<span class="property">current</span> &amp;&amp; ratio &gt;= <span class="number">0.75</span>) &#123;</span><br><span class="line">        fired75.<span class="property">current</span> = <span class="literal">true</span></span><br><span class="line">        fire?.(<span class="string">&#x27;trackCustom&#x27;</span>, <span class="string">&#x27;ScrollDepth75&#x27;</span>, payload)</span><br><span class="line">      &#125;</span><br><span class="line">      <span class="keyword">if</span> (!fired90.<span class="property">current</span> &amp;&amp; ratio &gt;= <span class="number">0.9</span>) &#123;</span><br><span class="line">        fired90.<span class="property">current</span> = <span class="literal">true</span></span><br><span class="line">        fire?.(<span class="string">&#x27;trackCustom&#x27;</span>, <span class="string">&#x27;ChapterCompleted&#x27;</span>, payload)</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_">window</span>.<span class="title function_">addEventListener</span>(<span class="string">&#x27;scroll&#x27;</span>, onScroll, &#123; <span class="attr">passive</span>: <span class="literal">true</span> &#125;)</span><br><span class="line">    <span class="keyword">return</span> <span class="function">() =&gt;</span> <span class="variable language_">window</span>.<span class="title function_">removeEventListener</span>(<span class="string">&#x27;scroll&#x27;</span>, onScroll)</span><br><span class="line">  &#125;, [chapterTitle, chapterOrder, bookSlug])</span><br><span class="line"></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><p>chapter page 里直接挂上去：</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// app/book/[slug]/chapter/[n]/page.tsx</span></span><br><span class="line">&lt;<span class="title class_">ChapterPixel</span></span><br><span class="line">  chapterTitle=&#123;chapter.<span class="property">title</span>&#125;</span><br><span class="line">  chapterOrder=&#123;chapter.<span class="property">order</span>&#125;</span><br><span class="line">  bookSlug=&#123;slug&#125;</span><br><span class="line">/&gt;</span><br></pre></td></tr></table></figure><p>几个设计细节值得注意：</p><ul><li><strong><code>useRef</code> 防重复</strong>：scroll 事件高频触发，必须用 ref 确保每个深度只上报一次</li><li><strong><code>passive: true</code></strong>：不阻塞渲染线程，对 Core Web Vitals 无影响</li><li><strong><code>fbq</code> optional chaining</strong>：Pixel 加载有延迟，防止脚本未就绪时报错</li><li><strong>ViewContent 在 mount 而非 scroll</strong>：「进入章节」本身就是有效的意图信号，不需要等到用户滚动</li></ul><h2 id="FB-后台还需要手动配置"><a href="#FB-后台还需要手动配置" class="headerlink" title="FB 后台还需要手动配置"></a>FB 后台还需要手动配置</h2><p>代码侧搞定后，还有两步后台操作才能真正用上这些信号：</p><p><strong>1. 创建自定义转化</strong>（ChapterRead50 &#x2F; ChapterCompleted）</p><p>Events Manager → 自定义转化 → 新建，选择「自定义事件」→ 选对应事件名。<code>PageView</code> 和 <code>ViewContent</code> 是 Facebook 标准事件，不需要这步。</p><p><strong>2. 配置 AEM（聚合事件衡量）</strong></p><p>iOS 14+ 之后大部分 iOS 用户拒绝追踪，Facebook 的解法是「申报最多 8 个事件 + 优先级」，信号丢失时归因到优先级最高的可用事件。</p><p>路径：Events Manager → 你的 Pixel → 聚合事件衡量 → 配置网络事件</p><p>推荐优先级：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ChapterCompleted (1) &gt; ChapterRead50 (2) &gt; ViewContent (3) &gt; PageView (4)</span><br></pre></td></tr></table></figure><p><code>ScrollDepth25</code> 和 <code>ScrollDepth75</code> 不加进 AEM——它们是漏斗分析用的，不是广告优化目标。AEM 只配置你真正用于 Campaign 优化的事件。</p><h2 id="要不要做-我的判断框架"><a href="#要不要做-我的判断框架" class="headerlink" title="要不要做 &#x2F; 我的判断框架"></a>要不要做 &#x2F; 我的判断框架</h2><p><strong>值得立刻做：</strong> 任何在跑 Facebook 流量的小说站都应该实装。几十行代码，对投流效果的影响是系统性的——算法找的人不一样，后续所有优化都站在更好的基础上。</p><p><strong>可以再等：</strong> CAPI（Conversions API，服务端回传）。iOS 信号丢失严重时 CAPI 能补回 30%+ 的归因，但实现复杂（静态导出站需要额外部署 serverless function）。当前阶段流量体量不大，先把浏览器 Pixel 跑稳，等 ViewContent 积累到 500+ 再评估是否上 CAPI。</p><p><strong>重点关注：</strong> ViewContent 积累到 500 次后，把 Campaign 优化目标从「落地页点击」切换为「ViewContent」，这是整个链路里 CPM 下降最明显的节点。</p>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/9738a32a70ad/</id>
    <link href="https://blog.zisheng.pro/posts/9738a32a70ad/"/>
    <published>2026-07-04T14:00:00.000Z</published>
    <summary>在小说站 Facebook 投流中，PageView 是最糟糕的优化信号——它把「看了 3 秒就走的人」和「读完整章的人」混在一起。这篇文章记录我如何用 ChapterPixel 组件实装 6 层 scroll depth 事件，让 Facebook 算法精准区分不同深度的读者，最终降低 CPM、提升会话 RPM。</summary>
    <title>小说站 Facebook Pixel 深度配置：用 scroll depth 事件训练算法找「真实读者」</title>
    <updated>2026-09-18T08:15:28.640Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="AI" scheme="https://blog.zisheng.pro/categories/AI/"/>
    <category term="Claude Code" scheme="https://blog.zisheng.pro/categories/AI/Claude-Code/"/>
    <category term="AI Infra" scheme="https://blog.zisheng.pro/tags/AI-Infra/"/>
    <category term="Claude Code" scheme="https://blog.zisheng.pro/tags/Claude-Code/"/>
    <category term="/btw" scheme="https://blog.zisheng.pro/tags/btw/"/>
    <category term="上下文管理" scheme="https://blog.zisheng.pro/tags/%E4%B8%8A%E4%B8%8B%E6%96%87%E7%AE%A1%E7%90%86/"/>
    <category term="工作流" scheme="https://blog.zisheng.pro/tags/%E5%B7%A5%E4%BD%9C%E6%B5%81/"/>
    <content>
      <![CDATA[<p>做 Agent 开发之后，我越来越在意一件事：<strong>上下文的纯净度</strong>。Claude Code 在处理一个大型重构任务时，我突然想确认一个之前讨论过的设计决策——如果直接在主对话里问，这个问题和答案会永远留在历史里，影响后续 Claude 的判断。如果开新 session，又要重新铺上下文。</p><p>我以前的做法是忍着不问，或者开一个单独的 Claude 窗口。直到我发现了 <code>/btw</code>。</p><h2 id="一句话总结"><a href="#一句话总结" class="headerlink" title="一句话总结"></a>一句话总结</h2><p><code>/btw</code> &#x3D; 不留痕的侧问（有上下文，无工具，答案不进历史）；<code>f to fork</code> &#x3D; 把这个侧问升级成有完整工具的新主战场。</p><hr><h2 id="一、-btw-是什么"><a href="#一、-btw-是什么" class="headerlink" title="一、&#x2F;btw 是什么"></a>一、&#x2F;btw 是什么</h2><p><code>/btw</code> 是 “by the way”（顺便问一句）的缩写，在 Claude Code <strong>v2.1.72</strong>（2026-03-10）引入。</p><p>它的核心设计哲学：<strong>快速提问，不污染历史</strong>。</p><p>执行 <code>/btw 你的问题</code> 之后，Claude 会在一个浮层面板里回答，这条问答<strong>不会作为 turn 添加到对话历史</strong>。主任务继续，上下文保持干净。</p><h3 id="与-subagent-的镜像关系"><a href="#与-subagent-的镜像关系" class="headerlink" title="与 subagent 的镜像关系"></a>与 subagent 的镜像关系</h3><p>官方文档有一句非常精准的描述：</p><blockquote><p><code>/btw</code> is the inverse of a subagent: it sees your full conversation but has no tools, while a subagent has full tools but starts with an empty context.</p></blockquote><table><thead><tr><th>维度</th><th><code>/btw</code></th><th>Subagent</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><hr><h2 id="二、结果面板：6-个键盘快捷键"><a href="#二、结果面板：6-个键盘快捷键" class="headerlink" title="二、结果面板：6 个键盘快捷键"></a>二、结果面板：6 个键盘快捷键</h2><p><code>/btw</code> 的答案出现后，浮层面板支持以下操作：</p><table><thead><tr><th>键</th><th>操作</th><th>说明</th></tr></thead><tbody><tr><td><code>↑</code> &#x2F; <code>↓</code></td><td>滚动答案</td><td>长答案时用</td></tr><tr><td><code>←</code> &#x2F; <code>→</code></td><td>切换历史侧问</td><td>v2.1.187+，在本 session 的多个 <code>/btw</code> 答案间翻页</td></tr><tr><td><code>c</code></td><td>复制为 Markdown</td><td>推荐方式，原始格式，见下文</td></tr><tr><td><code>f</code></td><td>Fork 成新 session</td><td>v2.1.161+，见下文</td></tr><tr><td><code>x</code></td><td>清除历史侧问列表</td><td>清掉面板上方显示的之前问答记录</td></tr><tr><td><code>Esc</code> &#x2F; <code>Enter</code> &#x2F; <code>Space</code></td><td>关闭面板</td><td>返回主对话提示符</td></tr></tbody></table><hr><h2 id="三、c-to-copy：为什么不用鼠标选？"><a href="#三、c-to-copy：为什么不用鼠标选？" class="headerlink" title="三、c to copy：为什么不用鼠标选？"></a>三、c to copy：为什么不用鼠标选？</h2><p>按 <code>c</code> 会把答案以<strong>原始 Markdown 格式</strong>复制到剪贴板。</p><p>这不是废话——终端渲染文本时会做”硬换行”（hard wrap），鼠标选中复制出来的是被截断的版本，粘贴到 Notion &#x2F; Obsidian 之后格式全乱。</p><p><code>c</code> 键直接拿的是 Claude 返回的源文本，<strong>代码块、标题、列表全部保留</strong>，粘贴即用。</p><hr><h2 id="四、f-to-fork：把侧问升级成主战场"><a href="#四、f-to-fork：把侧问升级成主战场" class="headerlink" title="四、f to fork：把侧问升级成主战场"></a>四、f to fork：把侧问升级成主战场</h2><p>这是整个 <code>/btw</code> 功能里最强的一个操作。</p><p>按 <code>f</code> 之后，Claude Code 会：</p><ol><li>把<strong>完整的主对话历史</strong> + <strong>这条 <code>/btw</code> 问答</strong>作为 transcript 继承</li><li>在此基础上开一个<strong>新的独立 session</strong></li><li>新 session 有<strong>完整的工具访问权限</strong>（可以读文件、运行命令、编辑代码）</li><li>原始 session 通过 <code>/resume</code> 可随时返回</li></ol><p><strong>典型工作流</strong>：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"># 正在做大型重构，Claude 还在工作</span><br><span class="line">/btw 这个 API 的响应格式对吗？</span><br><span class="line"></span><br><span class="line"># Claude 回答：不对，应该是 &#123; data: [], total: number &#125;</span><br><span class="line"># 你觉得需要深入修复，不想打断主任务</span><br><span class="line"></span><br><span class="line">f    # Fork 成新 session，继承完整上下文 + 有工具</span><br><span class="line"># 新 session 里：帮我修复这个 API 的响应格式</span><br><span class="line"></span><br><span class="line"># 修完之后 /resume 回主任务</span><br></pre></td></tr></table></figure><h3 id="Fork-vs-Branch"><a href="#Fork-vs-Branch" class="headerlink" title="Fork vs Branch"></a>Fork vs Branch</h3><table><thead><tr><th></th><th><code>/btw</code> + <code>f</code></th><th><code>/branch</code></th></tr></thead><tbody><tr><td>原 session</td><td>继续运行</td><td>暂停</td></tr><tr><td>新 session</td><td>独立开启，有完整工具</td><td>你切换过去继续</td></tr><tr><td>适合场景</td><td>侧问后发现需要深入处理</td><td>主动想探索另一条路径</td></tr></tbody></table><hr><h2 id="五、与直接在主对话输入的区别"><a href="#五、与直接在主对话输入的区别" class="headerlink" title="五、与直接在主对话输入的区别"></a>五、与直接在主对话输入的区别</h2><table><thead><tr><th>维度</th><th><code>/btw 侧问</code></th><th>主对话输入</th></tr></thead><tbody><tr><td>上下文污染</td><td>❌ 不进历史</td><td>✅ 永久写入</td></tr><tr><td>工具访问</td><td>❌ 无</td><td>✅ 完整</td></tr><tr><td>成本</td><td>低（复用 prompt cache）</td><td>标准</td></tr><tr><td>是否中断主任务</td><td>❌ 可在 Claude 工作时插入</td><td>✅ 等当前 turn 完成</td></tr><tr><td>适合提问类型</td><td>澄清、确认、上下文查询</td><td>新指令、代码修改</td></tr></tbody></table><p>关键点：<strong><code>/btw</code> 可以在 Claude 还在处理响应时插入</strong>，不需要等它完成。</p><hr><h2 id="六、5-个最有价值的实战场景"><a href="#六、5-个最有价值的实战场景" class="headerlink" title="六、5 个最有价值的实战场景"></a>六、5 个最有价值的实战场景</h2><h3 id="场景-1：长任务中途确认设计决策"><a href="#场景-1：长任务中途确认设计决策" class="headerlink" title="场景 1：长任务中途确认设计决策"></a>场景 1：长任务中途确认设计决策</h3><p>Claude 正在做 30 分钟的大型重构，你突然想想起来一个细节：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">/btw 我们之前说的是用 React Query 还是 SWR 管理数据缓存？</span><br></pre></td></tr></table></figure><p>快速得到答案，主任务继续，历史不受影响。</p><h3 id="场景-2：对话已很长，快速找回前面的信息"><a href="#场景-2：对话已很长，快速找回前面的信息" class="headerlink" title="场景 2：对话已很长，快速找回前面的信息"></a>场景 2：对话已很长，快速找回前面的信息</h3><p>50+ turns 的长 session，需要确认一个早期决策：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">/btw 我们在第几步决定不用 Redux 的？原因是什么？</span><br></pre></td></tr></table></figure><p>成本低（命中 prompt cache），不会因为问这个让后续对话偏题。</p><h3 id="场景-3：上下文检查，防止-Claude-跑偏"><a href="#场景-3：上下文检查，防止-Claude-跑偏" class="headerlink" title="场景 3：上下文检查，防止 Claude 跑偏"></a>场景 3：上下文检查，防止 Claude 跑偏</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">/btw 目前你对这个项目的技术栈理解是什么？列一下</span><br></pre></td></tr></table></figure><p>用来”探针”Claude 当前的理解是否和你一致，不对就及时修正，而不是等出了 bug 再发现。</p><h3 id="场景-4：侧问-fork-的黄金组合"><a href="#场景-4：侧问-fork-的黄金组合" class="headerlink" title="场景 4：侧问 + fork 的黄金组合"></a>场景 4：侧问 + fork 的黄金组合</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">/btw 这个 SQL 查询有没有 N+1 问题？</span><br><span class="line"></span><br><span class="line"># Claude 回答：有，在 getUserOrders 里</span><br><span class="line">f</span><br><span class="line"># 新 session 里处理这个问题，有完整文件访问权限</span><br></pre></td></tr></table></figure><h3 id="场景-5：多-subagent-并行时的状态检查"><a href="#场景-5：多-subagent-并行时的状态检查" class="headerlink" title="场景 5：多 subagent 并行时的状态检查"></a>场景 5：多 subagent 并行时的状态检查</h3><p>后台跑着几个 subagent，不想打断主对话：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">/btw 目前正在运行的 subagent 有哪些任务？</span><br></pre></td></tr></table></figure><hr><h2 id="七、配合个人工作流的实践建议"><a href="#七、配合个人工作流的实践建议" class="headerlink" title="七、配合个人工作流的实践建议"></a>七、配合个人工作流的实践建议</h2><p>我目前在 fwork（飞猪 Cloud GUI Agent）开发中的用法：</p><ul><li><strong>任何”确认类”问题</strong>优先 <code>/btw</code>，而不是直接问——问完即走，不留污染</li><li><strong>每隔 20-30 turns</strong> 用 <code>/btw 你对当前任务状态的理解是什么</code> 做一次”探针”</li><li>发现侧问答案值得深入时，<strong>立即 <code>f</code> fork</strong>，不要在主对话里展开</li><li>长对话结束前用 <code>/btw</code> 整理关键决策，再用 <code>c</code> 复制到 Obsidian 存档</li></ul><hr><h2 id="要不要用-我的判断框架"><a href="#要不要用-我的判断框架" class="headerlink" title="要不要用 &#x2F; 我的判断框架"></a>要不要用 &#x2F; 我的判断框架</h2><p><strong>现在就该用</strong>：</p><ul><li>你经常在长 session 中途想问一个细节，但又担心影响后续上下文</li><li>你有”问完发现要深入处理”的场景，<code>f</code> + fork 是完美解法</li><li>你在跑多个 subagent，需要快速旁问而不中断主流</li></ul><p><strong>注意这些限制</strong>：</p><ul><li><code>/btw</code> 没有工具——它不能帮你读文件、运行命令。只能基于<strong>已在上下文里的信息</strong>回答</li><li>如果问题本身需要 Claude 去查代码，还是得在主对话问（或者 fork 出去处理）</li><li><code>←</code>&#x2F;<code>→</code> 切换历史侧问需要 v2.1.187+，旧版本没有</li></ul><p><strong>重点关注</strong>：</p><ul><li><code>f to fork</code> 是 <code>/btw</code> 价值密度最高的功能，但大多数人不知道——记住这个键</li><li>复制答案永远用 <code>c</code>，不要鼠标选，格式更干净</li></ul>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/e263b0c1c425/</id>
    <link href="https://blog.zisheng.pro/posts/e263b0c1c425/"/>
    <published>2026-07-04T13:00:00.000Z</published>
    <summary>Claude Code 的 /btw 命令是一个&quot;顺便问一句&quot;的侧问功能，答案不会进入对话历史，不打断主任务，还能用 c 复制为原始 Markdown、用 f 一键 fork 成拥有完整工具的新 session。本文从原理到实战，带你把这个被大多数人忽视的小命令用到极致，从此告别&quot;为了问一个细节把上下文污染了&quot;的困境。</summary>
    <title>Claude Code /btw 实践指南</title>
    <updated>2026-09-18T08:15:28.629Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="广告投放" scheme="https://blog.zisheng.pro/categories/%E5%B9%BF%E5%91%8A%E6%8A%95%E6%94%BE/"/>
    <category term="Facebook" scheme="https://blog.zisheng.pro/categories/%E5%B9%BF%E5%91%8A%E6%8A%95%E6%94%BE/Facebook/"/>
    <category term="Facebook Pixel" scheme="https://blog.zisheng.pro/tags/Facebook-Pixel/"/>
    <category term="独立站" scheme="https://blog.zisheng.pro/tags/%E7%8B%AC%E7%AB%8B%E7%AB%99/"/>
    <category term="Meta Pixel" scheme="https://blog.zisheng.pro/tags/Meta-Pixel/"/>
    <category term="CAPI" scheme="https://blog.zisheng.pro/tags/CAPI/"/>
    <category term="转化追踪" scheme="https://blog.zisheng.pro/tags/%E8%BD%AC%E5%8C%96%E8%BF%BD%E8%B8%AA/"/>
    <category term="广告投放" scheme="https://blog.zisheng.pro/tags/%E5%B9%BF%E5%91%8A%E6%8A%95%E6%94%BE/"/>
    <content>
      <![CDATA[<p>我在帮飞猪做 Cloud GUI Agent 的同时，一直在用独立站项目练手 AI Infra 的落地。一个绕不开的问题是：<strong>广告数据怎么接</strong>？Google Analytics 之外，Facebook Pixel 几乎是独立站的标配。但大多数教程停留在”粘贴代码到 head 里”就完了，而真正让投放起效的细节——事件质量、CAPI 双打、iOS 14 之后的归因——这些从来没人说清楚。</p><p>这篇文章，我从前端工程师的角度把整条链路拆开来讲。</p><h2 id="一句话总结"><a href="#一句话总结" class="headerlink" title="一句话总结"></a>一句话总结</h2><p>Facebook Pixel &#x3D; 浏览器侧事件（Browser Events）+ 服务端转化 API（CAPI）双打，才能在隐私限制下保住数据质量，进而让算法有料可投。</p><hr><h2 id="一、Pixel-是什么，能做什么"><a href="#一、Pixel-是什么，能做什么" class="headerlink" title="一、Pixel 是什么，能做什么"></a>一、Pixel 是什么，能做什么</h2><p>Facebook Pixel（现在官方叫 <strong>Meta Pixel</strong>）是一段 JavaScript 代码，安装在你的网站后，负责把用户行为回传给 Meta，用于：</p><table><thead><tr><th>用途</th><th>说明</th></tr></thead><tbody><tr><td>转化追踪</td><td>记录购买、注册、加购等行为，衡量广告 ROI</td></tr><tr><td>再营销</td><td>对浏览过某页面但未购买的用户重新投放</td></tr><tr><td>相似受众</td><td>以转化用户为种子，扩展相似人群</td></tr><tr><td>广告优化</td><td>给算法信号，让 Campaign 朝高转化方向学习</td></tr></tbody></table><hr><h2 id="二、安装：最小可用配置"><a href="#二、安装：最小可用配置" class="headerlink" title="二、安装：最小可用配置"></a>二、安装：最小可用配置</h2><h3 id="2-1-获取-Pixel-ID"><a href="#2-1-获取-Pixel-ID" class="headerlink" title="2.1 获取 Pixel ID"></a>2.1 获取 Pixel ID</h3><p>登录 <a href="https://business.facebook.com/">Meta Business Manager</a> → <strong>Events Manager</strong> → 创建数据源 → 选 <strong>Web</strong> → 获得形如 <code>1234567890123456</code> 的 Pixel ID。</p><h3 id="2-2-基础代码"><a href="#2-2-基础代码" class="headerlink" title="2.2 基础代码"></a>2.2 基础代码</h3><p>把下面这段放进所有页面的 <code>&lt;head&gt;</code> 里：</p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">&lt;!-- Meta Pixel Code --&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">script</span>&gt;</span><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript">!<span class="keyword">function</span>(<span class="params">f,b,e,v,n,t,s</span>)</span></span><br><span class="line"><span class="language-javascript">&#123;<span class="keyword">if</span>(f.<span class="property">fbq</span>)<span class="keyword">return</span>;n=f.<span class="property">fbq</span>=<span class="keyword">function</span>(<span class="params"></span>)&#123;n.<span class="property">callMethod</span>?</span></span><br><span class="line"><span class="language-javascript">n.<span class="property">callMethod</span>.<span class="title function_">apply</span>(n,<span class="variable language_">arguments</span>):n.<span class="property">queue</span>.<span class="title function_">push</span>(<span class="variable language_">arguments</span>)&#125;;</span></span><br><span class="line"><span class="language-javascript"><span class="keyword">if</span>(!f.<span class="property">_fbq</span>)f.<span class="property">_fbq</span>=n;n.<span class="property">push</span>=n;n.<span class="property">loaded</span>=!<span class="number">0</span>;n.<span class="property">version</span>=<span class="string">&#x27;2.0&#x27;</span>;</span></span><br><span class="line"><span class="language-javascript">n.<span class="property">queue</span>=[];t=b.<span class="title function_">createElement</span>(e);t.<span class="property">async</span>=!<span class="number">0</span>;</span></span><br><span class="line"><span class="language-javascript">t.<span class="property">src</span>=v;s=b.<span class="title function_">getElementsByTagName</span>(e)[<span class="number">0</span>];</span></span><br><span class="line"><span class="language-javascript">s.<span class="property">parentNode</span>.<span class="title function_">insertBefore</span>(t,s)&#125;(<span class="variable language_">window</span>, <span class="variable language_">document</span>,<span class="string">&#x27;script&#x27;</span>,</span></span><br><span class="line"><span class="language-javascript"><span class="string">&#x27;https://connect.facebook.net/en_US/fbevents.js&#x27;</span>);</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"><span class="title function_">fbq</span>(<span class="string">&#x27;init&#x27;</span>, <span class="string">&#x27;YOUR_PIXEL_ID&#x27;</span>);</span></span><br><span class="line"><span class="language-javascript"><span class="title function_">fbq</span>(<span class="string">&#x27;track&#x27;</span>, <span class="string">&#x27;PageView&#x27;</span>);</span></span><br><span class="line"><span class="language-javascript"></span><span class="tag">&lt;/<span class="name">script</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">noscript</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">img</span> <span class="attr">height</span>=<span class="string">&quot;1&quot;</span> <span class="attr">width</span>=<span class="string">&quot;1&quot;</span> <span class="attr">style</span>=<span class="string">&quot;display:none&quot;</span></span></span><br><span class="line"><span class="tag">    <span class="attr">src</span>=<span class="string">&quot;https://www.facebook.com/tr?id=YOUR_PIXEL_ID&amp;ev=PageView&amp;noscript=1&quot;</span>/&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">noscript</span>&gt;</span></span><br><span class="line"><span class="comment">&lt;!-- End Meta Pixel Code --&gt;</span></span><br></pre></td></tr></table></figure><p><strong><code>fbq(&#39;init&#39;, ID)</code></strong> 初始化并绑定用户标识，<strong><code>fbq(&#39;track&#39;, &#39;PageView&#39;)</code></strong> 发送第一个事件。</p><h3 id="2-3-在-React-Next-js-中接入"><a href="#2-3-在-React-Next-js-中接入" class="headerlink" title="2.3 在 React &#x2F; Next.js 中接入"></a>2.3 在 React &#x2F; Next.js 中接入</h3><p>以 Next.js App Router 为例：</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><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_">Script</span> <span class="keyword">from</span> <span class="string">&#x27;next/script&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="variable constant_">PIXEL_ID</span> = process.<span class="property">env</span>.<span class="property">NEXT_PUBLIC_META_PIXEL_ID</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">head</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">Script</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">          <span class="attr">id</span>=<span class="string">&quot;meta-pixel&quot;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">          <span class="attr">strategy</span>=<span class="string">&quot;afterInteractive&quot;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">          <span class="attr">dangerouslySetInnerHTML</span>=<span class="string">&#123;&#123;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">            <span class="attr">__html:</span> `</span></span></span><br><span class="line"><span class="tag"><span class="language-xml">              !<span class="attr">function</span>(<span class="attr">f</span>,<span class="attr">b</span>,<span class="attr">e</span>,<span class="attr">v</span>,<span class="attr">n</span>,<span class="attr">t</span>,<span class="attr">s</span>)&#123;<span class="attr">...</span>&#125;(<span class="attr">window</span>,<span class="attr">document</span>,&#x27;<span class="attr">script</span>&#x27;,</span></span></span><br><span class="line"><span class="tag"><span class="language-xml">              &#x27;<span class="attr">https:</span>//<span class="attr">connect.facebook.net</span>/<span class="attr">en_US</span>/<span class="attr">fbevents.js</span>&#x27;);</span></span></span><br><span class="line"><span class="tag"><span class="language-xml">              <span class="attr">fbq</span>(&#x27;<span class="attr">init</span>&#x27;, &#x27;$&#123;<span class="attr">PIXEL_ID</span>&#125;&#x27;);</span></span></span><br><span class="line"><span class="tag"><span class="language-xml">              <span class="attr">fbq</span>(&#x27;<span class="attr">track</span>&#x27;, &#x27;<span class="attr">PageView</span>&#x27;);</span></span></span><br><span class="line"><span class="tag"><span class="language-xml">            `,</span></span></span><br><span class="line"><span class="tag"><span class="language-xml">          &#125;&#125;</span></span></span><br><span class="line"><span class="tag"><span class="language-xml">        /&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">head</span>&gt;</span></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">body</span>&gt;</span>&#123;children&#125;<span class="tag">&lt;/<span class="name">body</span>&gt;</span></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">html</span>&gt;</span></span></span></span></span><br><span class="line">  )</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><blockquote><p><code>strategy=&quot;afterInteractive&quot;</code> 让 Pixel 脚本不阻塞 LCP，是 Next.js 推荐方式。</p></blockquote><hr><h2 id="三、标准事件：必须掌握的-9-个"><a href="#三、标准事件：必须掌握的-9-个" class="headerlink" title="三、标准事件：必须掌握的 9 个"></a>三、标准事件：必须掌握的 9 个</h2><p>Meta 有一套预定义事件，算法对这些事件有专属优化模型，<strong>优先用标准事件，而不是自定义事件</strong>。</p><table><thead><tr><th>事件名</th><th>触发时机</th><th>代码</th></tr></thead><tbody><tr><td><code>PageView</code></td><td>每次页面加载</td><td>自动（init 后自动触发）</td></tr><tr><td><code>ViewContent</code></td><td>商品详情页</td><td><code>fbq(&#39;track&#39;, &#39;ViewContent&#39;, {...})</code></td></tr><tr><td><code>Search</code></td><td>搜索结果页</td><td><code>fbq(&#39;track&#39;, &#39;Search&#39;, {...})</code></td></tr><tr><td><code>AddToCart</code></td><td>加入购物车</td><td><code>fbq(&#39;track&#39;, &#39;AddToCart&#39;, {...})</code></td></tr><tr><td><code>AddToWishlist</code></td><td>收藏&#x2F;心愿单</td><td><code>fbq(&#39;track&#39;, &#39;AddToWishlist&#39;, {...})</code></td></tr><tr><td><code>InitiateCheckout</code></td><td>进入结算流程</td><td><code>fbq(&#39;track&#39;, &#39;InitiateCheckout&#39;, {...})</code></td></tr><tr><td><code>AddPaymentInfo</code></td><td>填写支付信息</td><td><code>fbq(&#39;track&#39;, &#39;AddPaymentInfo&#39;, {...})</code></td></tr><tr><td><code>Purchase</code></td><td>订单完成</td><td><code>fbq(&#39;track&#39;, &#39;Purchase&#39;, {...})</code></td></tr><tr><td><code>Lead</code></td><td>表单提交&#x2F;注册</td><td><code>fbq(&#39;track&#39;, &#39;Lead&#39;, {...})</code></td></tr></tbody></table><h3 id="Purchase-事件示例（最重要）"><a href="#Purchase-事件示例（最重要）" class="headerlink" title="Purchase 事件示例（最重要）"></a>Purchase 事件示例（最重要）</h3><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="title function_">fbq</span>(<span class="string">&#x27;track&#x27;</span>, <span class="string">&#x27;Purchase&#x27;</span>, &#123;</span><br><span class="line">  <span class="attr">value</span>: <span class="number">99.00</span>,          <span class="comment">// 订单金额，必填</span></span><br><span class="line">  <span class="attr">currency</span>: <span class="string">&#x27;USD&#x27;</span>,       <span class="comment">// 货币代码，必填</span></span><br><span class="line">  <span class="attr">content_ids</span>: [<span class="string">&#x27;SKU_001&#x27;</span>, <span class="string">&#x27;SKU_002&#x27;</span>],  <span class="comment">// 商品 ID 数组</span></span><br><span class="line">  <span class="attr">content_type</span>: <span class="string">&#x27;product&#x27;</span>,</span><br><span class="line">  <span class="attr">num_items</span>: <span class="number">2</span>,</span><br><span class="line">  <span class="attr">order_id</span>: <span class="string">&#x27;ORD_20260704_001&#x27;</span>,  <span class="comment">// 去重用，强烈建议加</span></span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><h3 id="ViewContent-事件示例"><a href="#ViewContent-事件示例" class="headerlink" title="ViewContent 事件示例"></a>ViewContent 事件示例</h3><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="title function_">fbq</span>(<span class="string">&#x27;track&#x27;</span>, <span class="string">&#x27;ViewContent&#x27;</span>, &#123;</span><br><span class="line">  <span class="attr">content_name</span>: <span class="string">&#x27;iPhone 16 Pro Case&#x27;</span>,</span><br><span class="line">  <span class="attr">content_ids</span>: [<span class="string">&#x27;CASE_IP16P_BLK&#x27;</span>],</span><br><span class="line">  <span class="attr">content_type</span>: <span class="string">&#x27;product&#x27;</span>,</span><br><span class="line">  <span class="attr">value</span>: <span class="number">19.90</span>,</span><br><span class="line">  <span class="attr">currency</span>: <span class="string">&#x27;USD&#x27;</span>,</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><hr><h2 id="四、自定义事件：超出标准事件的场景"><a href="#四、自定义事件：超出标准事件的场景" class="headerlink" title="四、自定义事件：超出标准事件的场景"></a>四、自定义事件：超出标准事件的场景</h2><p>当标准事件满足不了需求时，用 <code>fbq(&#39;trackCustom&#39;, &#39;EventName&#39;, params)</code>：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 视频播放 25% 时</span></span><br><span class="line"><span class="title function_">fbq</span>(<span class="string">&#x27;trackCustom&#x27;</span>, <span class="string">&#x27;VideoWatch25&#x27;</span>, &#123;</span><br><span class="line">  <span class="attr">video_title</span>: <span class="string">&#x27;产品介绍视频&#x27;</span>,</span><br><span class="line">  <span class="attr">watch_percentage</span>: <span class="number">25</span>,</span><br><span class="line">&#125;);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 用户点击了特定 CTA</span></span><br><span class="line"><span class="title function_">fbq</span>(<span class="string">&#x27;trackCustom&#x27;</span>, <span class="string">&#x27;ClickPricingButton&#x27;</span>, &#123;</span><br><span class="line">  <span class="attr">plan</span>: <span class="string">&#x27;pro&#x27;</span>,</span><br><span class="line">  <span class="attr">location</span>: <span class="string">&#x27;hero_section&#x27;</span>,</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><blockquote><p>自定义事件无法用于 Campaign 层级的标准优化目标（如”Purchase”优化），只能用于自定义转化。</p></blockquote><hr><h2 id="五、用户数据匹配：提升命中率的关键"><a href="#五、用户数据匹配：提升命中率的关键" class="headerlink" title="五、用户数据匹配：提升命中率的关键"></a>五、用户数据匹配：提升命中率的关键</h2><p>Pixel 默认靠 Cookie 识别用户，但可以传入哈希后的用户数据提升匹配率（<strong>Advanced Matching</strong>）：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="title function_">fbq</span>(<span class="string">&#x27;init&#x27;</span>, <span class="string">&#x27;YOUR_PIXEL_ID&#x27;</span>, &#123;</span><br><span class="line">  <span class="attr">em</span>: <span class="title function_">sha256</span>(user.<span class="property">email</span>),         <span class="comment">// 邮件（SHA-256）</span></span><br><span class="line">  <span class="attr">ph</span>: <span class="title function_">sha256</span>(user.<span class="property">phone</span>),         <span class="comment">// 电话（SHA-256）</span></span><br><span class="line">  <span class="attr">fn</span>: <span class="title function_">sha256</span>(user.<span class="property">firstName</span>.<span class="title function_">toLowerCase</span>()),</span><br><span class="line">  <span class="attr">ln</span>: <span class="title function_">sha256</span>(user.<span class="property">lastName</span>.<span class="title function_">toLowerCase</span>()),</span><br><span class="line">  <span class="attr">ct</span>: <span class="title function_">sha256</span>(user.<span class="property">city</span>.<span class="title function_">toLowerCase</span>()),</span><br><span class="line">  <span class="attr">st</span>: <span class="title function_">sha256</span>(user.<span class="property">state</span>),         <span class="comment">// 两字母缩写，美国适用</span></span><br><span class="line">  <span class="attr">zp</span>: <span class="title function_">sha256</span>(user.<span class="property">zipCode</span>),</span><br><span class="line">  <span class="attr">country</span>: <span class="title function_">sha256</span>(<span class="string">&#x27;us&#x27;</span>),</span><br><span class="line">  <span class="attr">external_id</span>: <span class="title function_">sha256</span>(user.<span class="property">userId</span>),  <span class="comment">// 自有用户 ID，最重要</span></span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p><strong>规范要求</strong>：所有字段必须先 SHA-256 哈希、lowercase、trim 后再传入。Meta 在服务端对比自己的数据库，不存储原始 PII。</p><p>简易哈希工具（浏览器端）：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">async</span> <span class="keyword">function</span> <span class="title function_">sha256</span>(<span class="params">message</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> msgBuffer = <span class="keyword">new</span> <span class="title class_">TextEncoder</span>().<span class="title function_">encode</span>(message.<span class="title function_">trim</span>().<span class="title function_">toLowerCase</span>());</span><br><span class="line">  <span class="keyword">const</span> hashBuffer = <span class="keyword">await</span> crypto.<span class="property">subtle</span>.<span class="title function_">digest</span>(<span class="string">&#x27;SHA-256&#x27;</span>, msgBuffer);</span><br><span class="line">  <span class="keyword">const</span> hashArray = <span class="title class_">Array</span>.<span class="title function_">from</span>(<span class="keyword">new</span> <span class="title class_">Uint8Array</span>(hashBuffer));</span><br><span class="line">  <span class="keyword">return</span> hashArray.<span class="title function_">map</span>(<span class="function"><span class="params">b</span> =&gt;</span> b.<span class="title function_">toString</span>(<span class="number">16</span>).<span class="title function_">padStart</span>(<span class="number">2</span>, <span class="string">&#x27;0&#x27;</span>)).<span class="title function_">join</span>(<span class="string">&#x27;&#x27;</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><hr><h2 id="六、Conversions-API（CAPI）：iOS-14-之后的救命稻草"><a href="#六、Conversions-API（CAPI）：iOS-14-之后的救命稻草" class="headerlink" title="六、Conversions API（CAPI）：iOS 14 之后的救命稻草"></a>六、Conversions API（CAPI）：iOS 14 之后的救命稻草</h2><h3 id="为什么需要-CAPI"><a href="#为什么需要-CAPI" class="headerlink" title="为什么需要 CAPI"></a>为什么需要 CAPI</h3><p>iOS 14.5+ 的 ATT 框架、Safari ITP、Chrome 第三方 Cookie 限制，导致<strong>浏览器侧 Pixel 信号大量丢失</strong>，典型症状：</p><ul><li>Events Manager 里 Purchase 事件数比实际订单少 30-60%</li><li>归因窗口缩短，ROAS 虚低</li><li>再营销受众缩水</li></ul><p>CAPI 是<strong>服务端直接调用 Meta Graph API</strong> 发送事件，绕过浏览器限制：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">用户行为 → 你的服务器 → Meta Graph API（CAPI）</span><br><span class="line">         ↘ 浏览器 Pixel（Browser Events）</span><br></pre></td></tr></table></figure><p>两路数据 Meta 会自动去重，合并后的信号质量远优于单路。</p><h3 id="CAPI-接入（Node-js-示例）"><a href="#CAPI-接入（Node-js-示例）" class="headerlink" title="CAPI 接入（Node.js 示例）"></a>CAPI 接入（Node.js 示例）</h3><p>安装官方 SDK：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm install facebook-nodejs-business-sdk</span><br></pre></td></tr></table></figure><p>在订单完成时，从服务端发送 Purchase 事件：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">FacebookAdsApi</span>, <span class="title class_">ServerSideApi</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;facebook-nodejs-business-sdk&#x27;</span>;</span><br><span class="line"><span class="keyword">const</span> bizSdk = <span class="built_in">require</span>(<span class="string">&#x27;facebook-nodejs-business-sdk&#x27;</span>);</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> &#123; <span class="title class_">EventRequest</span>, <span class="title class_">UserData</span>, <span class="title class_">CustomData</span>, <span class="title class_">ServerEvent</span> &#125; = bizSdk;</span><br><span class="line"></span><br><span class="line"><span class="title class_">FacebookAdsApi</span>.<span class="title function_">init</span>(process.<span class="property">env</span>.<span class="property">META_ACCESS_TOKEN</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_">sendPurchaseEvent</span>(<span class="params">order, req</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> userData = <span class="keyword">new</span> <span class="title class_">UserData</span>()</span><br><span class="line">    .<span class="title function_">setEmails</span>([<span class="keyword">await</span> <span class="title function_">sha256</span>(order.<span class="property">email</span>)])</span><br><span class="line">    .<span class="title function_">setPhones</span>([<span class="keyword">await</span> <span class="title function_">sha256</span>(order.<span class="property">phone</span>)])</span><br><span class="line">    .<span class="title function_">setExternalId</span>(<span class="keyword">await</span> <span class="title function_">sha256</span>(order.<span class="property">userId</span>))</span><br><span class="line">    .<span class="title function_">setClientIpAddress</span>(req.<span class="property">ip</span>)</span><br><span class="line">    .<span class="title function_">setClientUserAgent</span>(req.<span class="property">headers</span>[<span class="string">&#x27;user-agent&#x27;</span>])</span><br><span class="line">    .<span class="title function_">setFbc</span>(req.<span class="property">cookies</span>.<span class="property">_fbc</span>)   <span class="comment">// Facebook Click ID Cookie</span></span><br><span class="line">    .<span class="title function_">setFbp</span>(req.<span class="property">cookies</span>.<span class="property">_fbp</span>);  <span class="comment">// Facebook Browser ID Cookie</span></span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> customData = <span class="keyword">new</span> <span class="title class_">CustomData</span>()</span><br><span class="line">    .<span class="title function_">setValue</span>(order.<span class="property">totalAmount</span>)</span><br><span class="line">    .<span class="title function_">setCurrency</span>(<span class="string">&#x27;USD&#x27;</span>)</span><br><span class="line">    .<span class="title function_">setContentIds</span>(order.<span class="property">items</span>.<span class="title function_">map</span>(<span class="function"><span class="params">i</span> =&gt;</span> i.<span class="property">sku</span>))</span><br><span class="line">    .<span class="title function_">setContentType</span>(<span class="string">&#x27;product&#x27;</span>)</span><br><span class="line">    .<span class="title function_">setOrderId</span>(order.<span class="property">id</span>);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> serverEvent = <span class="keyword">new</span> <span class="title class_">ServerEvent</span>()</span><br><span class="line">    .<span class="title function_">setEventName</span>(<span class="string">&#x27;Purchase&#x27;</span>)</span><br><span class="line">    .<span class="title function_">setEventTime</span>(<span class="title class_">Math</span>.<span class="title function_">floor</span>(<span class="title class_">Date</span>.<span class="title function_">now</span>() / <span class="number">1000</span>))</span><br><span class="line">    .<span class="title function_">setUserData</span>(userData)</span><br><span class="line">    .<span class="title function_">setCustomData</span>(customData)</span><br><span class="line">    .<span class="title function_">setEventSourceUrl</span>(order.<span class="property">pageUrl</span>)</span><br><span class="line">    .<span class="title function_">setActionSource</span>(<span class="string">&#x27;website&#x27;</span>)</span><br><span class="line">    .<span class="title function_">setEventId</span>(<span class="string">`purchase_<span class="subst">$&#123;order.id&#125;</span>`</span>); <span class="comment">// 去重 ID，与浏览器事件一致</span></span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> eventRequest = <span class="keyword">new</span> <span class="title class_">EventRequest</span>(process.<span class="property">env</span>.<span class="property">META_PIXEL_ID</span>)</span><br><span class="line">    .<span class="title function_">setEvents</span>([serverEvent]);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> eventRequest.<span class="title function_">execute</span>();</span><br><span class="line">&#125;</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><code>_fbc</code></td><td>Cookie（<code>fbclid</code> 参数写入）</td><td>追踪来自 Facebook 广告的点击</td></tr><tr><td><code>_fbp</code></td><td>Cookie（Pixel 自动写入）</td><td>追踪浏览器会话</td></tr><tr><td><code>event_id</code></td><td>自定义（保持唯一）</td><td><strong>浏览器与 CAPI 去重的核心字段</strong></td></tr><tr><td><code>action_source</code></td><td>固定传 <code>&#39;website&#39;</code></td><td>告知 Meta 事件来源</td></tr><tr><td><code>client_ip_address</code></td><td>从请求头获取</td><td>提升匹配率</td></tr></tbody></table><hr><h2 id="七、事件去重：防止数据虚报"><a href="#七、事件去重：防止数据虚报" class="headerlink" title="七、事件去重：防止数据虚报"></a>七、事件去重：防止数据虚报</h2><p>当浏览器 Pixel 和 CAPI 同时发出同一事件，Meta 用 <code>event_id</code> + <code>event_name</code> 组合去重。<strong>两路必须传同一个 <code>event_id</code></strong>：</p><p>浏览器端：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> eventId = <span class="string">`purchase_<span class="subst">$&#123;orderId&#125;</span>_<span class="subst">$&#123;<span class="built_in">Date</span>.now()&#125;</span>`</span>;</span><br><span class="line"></span><br><span class="line"><span class="title function_">fbq</span>(<span class="string">&#x27;track&#x27;</span>, <span class="string">&#x27;Purchase&#x27;</span>, &#123;</span><br><span class="line">  <span class="attr">value</span>: <span class="number">99.00</span>,</span><br><span class="line">  <span class="attr">currency</span>: <span class="string">&#x27;USD&#x27;</span>,</span><br><span class="line">  <span class="attr">order_id</span>: orderId,</span><br><span class="line">&#125;, &#123;</span><br><span class="line">  <span class="attr">eventID</span>: eventId,  <span class="comment">// 注意是 eventID，大写 D</span></span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p>服务端 CAPI：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">serverEvent.<span class="title function_">setEventId</span>(eventId);  <span class="comment">// 与浏览器端完全一致</span></span><br></pre></td></tr></table></figure><hr><h2 id="八、事件质量评分（Event-Match-Quality）"><a href="#八、事件质量评分（Event-Match-Quality）" class="headerlink" title="八、事件质量评分（Event Match Quality）"></a>八、事件质量评分（Event Match Quality）</h2><p>Events Manager 里每个事件都有一个 <strong>EMQ 分数（0-10）</strong>，直接影响广告投放效果：</p><table><thead><tr><th>分数区间</th><th>判断</th><th>优化方向</th></tr></thead><tbody><tr><td>7-10</td><td>优秀</td><td>保持</td></tr><tr><td>4-6</td><td>一般</td><td>补充更多用户标识字段</td></tr><tr><td>0-3</td><td>差</td><td>检查是否有 external_id &#x2F; email 传入</td></tr></tbody></table><p>提分策略：</p><ol><li><strong>传 <code>external_id</code></strong>（登录用户的 userId 哈希）是最有效的单一操作</li><li>同时传 email + phone</li><li>接入 CAPI 补充 IP、UA、<code>_fbc</code>&#x2F;<code>_fbp</code></li><li>确保事件在正确时机触发（购买完成页，而非提交按钮点击时）</li></ol><hr><h2 id="九、聚合事件衡量（AEM）：iOS-14-之后必配"><a href="#九、聚合事件衡量（AEM）：iOS-14-之后必配" class="headerlink" title="九、聚合事件衡量（AEM）：iOS 14 之后必配"></a>九、聚合事件衡量（AEM）：iOS 14 之后必配</h2><p>iOS 14 之后，针对 iOS 用户的广告归因受 ATT 限制，必须在 <strong>Events Manager → 聚合事件衡量</strong> 里配置优先级：</p><ul><li>最多 <strong>8 个事件</strong>，按转化漏斗从低到高排优先级</li><li>推荐顺序：<code>Purchase</code> &gt; <code>InitiateCheckout</code> &gt; <code>AddToCart</code> &gt; <code>ViewContent</code> &gt; <code>PageView</code></li><li>没有配置 AEM 的事件，iOS 数据几乎为零</li></ul><hr><h2 id="十、与-GA4-协同：数据互补而非替代"><a href="#十、与-GA4-协同：数据互补而非替代" class="headerlink" title="十、与 GA4 协同：数据互补而非替代"></a>十、与 GA4 协同：数据互补而非替代</h2><table><thead><tr><th>维度</th><th>Meta Pixel</th><th>GA4</th></tr></thead><tbody><tr><td>归因模型</td><td>Meta 内部（助攻算法）</td><td>数据驱动 &#x2F; 线性</td></tr><tr><td>用户识别</td><td><code>_fbp</code> &#x2F; <code>external_id</code></td><td><code>client_id</code> &#x2F; <code>user_id</code></td></tr><tr><td>跨设备</td><td>依赖 Meta 账号登录</td><td>依赖 GA4 User-ID</td></tr><tr><td>适合看什么</td><td>广告 ROI、受众效果</td><td>全站行为漏斗、留存</td></tr></tbody></table><p>两套数据天然不会对齐，<strong>不要试图让两者数字一致</strong>，应该：</p><ul><li>用 Meta Pixel 判断广告素材&#x2F;受众效果</li><li>用 GA4 看站内用户路径和行为质量</li><li>订单数据以自己数据库为准（Source of Truth）</li></ul><hr><h2 id="十一、调试工具"><a href="#十一、调试工具" class="headerlink" title="十一、调试工具"></a>十一、调试工具</h2><table><thead><tr><th>工具</th><th>用途</th></tr></thead><tbody><tr><td><strong>Meta Pixel Helper</strong>（Chrome 插件）</td><td>实时查看页面触发了哪些事件，是否有错误</td></tr><tr><td><strong>Events Manager → Test Events</strong></td><td>输入网址，实时验证服务端 CAPI 数据</td></tr><tr><td><strong>Events Manager → Diagnostics</strong></td><td>查看事件错误、去重情况</td></tr><tr><td><strong>Graph API Explorer</strong></td><td>手动发测试 CAPI 请求</td></tr></tbody></table><p>用 Pixel Helper 检查时，常见问题：</p><ul><li><code>Pixel ID mismatch</code>：页面上多个 Pixel ID 冲突</li><li><code>No events detected</code>：脚本未正确加载（广告拦截器、CSP 限制）</li><li><code>Duplicate pixel</code>：同一事件触发了两次（SPA 路由切换时 <code>PageView</code> 重复）</li></ul><hr><h2 id="十二、隐私合规：不能忽略的法律红线"><a href="#十二、隐私合规：不能忽略的法律红线" class="headerlink" title="十二、隐私合规：不能忽略的法律红线"></a>十二、隐私合规：不能忽略的法律红线</h2><table><thead><tr><th>地区</th><th>法规</th><th>要点</th></tr></thead><tbody><tr><td>欧盟&#x2F;英国</td><td>GDPR</td><td>必须获得明确同意后才能加载 Pixel</td></tr><tr><td>加州</td><td>CCPA</td><td>提供 “Do Not Sell” 选项</td></tr><tr><td>中国大陆</td><td>个人信息保护法</td><td>境外传输需单独授权</td></tr></tbody></table><p>实现方式：接入 CMP（Consent Management Platform，如 Cookiebot、OneTrust），在用户同意后动态加载 Pixel：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 用户同意后再初始化</span></span><br><span class="line"><span class="variable language_">window</span>.<span class="title function_">addEventListener</span>(<span class="string">&#x27;CookieConsentGiven&#x27;</span>, <span class="function">() =&gt;</span> &#123;</span><br><span class="line">  <span class="title function_">fbq</span>(<span class="string">&#x27;init&#x27;</span>, <span class="variable constant_">PIXEL_ID</span>);</span><br><span class="line">  <span class="title function_">fbq</span>(<span class="string">&#x27;track&#x27;</span>, <span class="string">&#x27;PageView&#x27;</span>);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p>CAPI 在服务端运行，理论上不受客户端 Cookie 限制，但数据回传本身也需要有合法依据（合同履行、合法利益或明确同意）。</p><hr><h2 id="十三、内容站套利：优化事件才是最大杠杆"><a href="#十三、内容站套利：优化事件才是最大杠杆" class="headerlink" title="十三、内容站套利：优化事件才是最大杠杆"></a>十三、内容站套利：优化事件才是最大杠杆</h2><p>前面十二节的案例几乎都围绕电商（Purchase、AddToCart）。但如果你跑的是<strong>内容站 + AdSense&#x2F;AdX 广告套利模型</strong>——比如小说站、文章站——没有 Purchase 事件，优化目标怎么选？这是最容易踩坑的一环。</p><h3 id="套利的利润公式"><a href="#套利的利润公式" class="headerlink" title="套利的利润公式"></a>套利的利润公式</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">每次点击利润 = (平均每次会话 pageviews × RPM / 1000) − Facebook CPC</span><br></pre></td></tr></table></figure><p>比如 RPM $8、用户平均浏览 4 页（4 章）：</p><ul><li>每次会话广告收入 &#x3D; 4 × 8&#x2F;1000 &#x3D; $0.032 × 4 &#x3D; <strong>$0.128</strong></li><li>目标 CPC &lt; $0.10，才有稳定利润空间</li></ul><p>这个模型里，广告侧唯一能控制的变量是 <strong>CPC</strong> 和 <strong>受众质量</strong>（高质量受众 &#x3D; 每次会话浏览更多页）。优化事件的选择直接决定算法能不能找到”真实读者”。</p><h3 id="优化事件信号质量从低到高"><a href="#优化事件信号质量从低到高" class="headerlink" title="优化事件信号质量从低到高"></a>优化事件信号质量从低到高</h3><table><thead><tr><th>事件</th><th>捕捉的是谁</th><th>信号质量</th><th>可用时机</th></tr></thead><tbody><tr><td><code>PageView</code></td><td>任何点进来的人，包括 3 秒就走的</td><td>最差</td><td>立刻可用，但不建议</td></tr><tr><td><code>ViewContent</code>（页面加载时触发）</td><td>点进来且页面加载完成的</td><td>中</td><td>积累 500+ 后可用</td></tr><tr><td><code>ViewContent</code>（滚动 50% 触发）</td><td>真正读了半章的人</td><td>最好</td><td>积累 500+ 后可用</td></tr></tbody></table><p><strong>为什么不能用 <code>PageView</code> 做优化目标：</strong> <code>PageView</code> 在落地页加载完就触发，连内容还没看，可能就是广告点击后立刻返回的人。把这个信号喂给算法，等于教它”帮我找更多会点广告但不读文章的人”——会话深度低，每次会话 pageviews 低，套利失败。</p><h3 id="内容站的三段-Campaign-路线"><a href="#内容站的三段-Campaign-路线" class="headerlink" title="内容站的三段 Campaign 路线"></a>内容站的三段 Campaign 路线</h3><p><strong>第一阶段：冷启动（0–499 ViewContent 事件）</strong></p><p>Campaign 目标选 <strong>Traffic</strong>，优化目标选 <strong>Landing Page Views</strong>（不是 Link Clicks）。Landing Page Views 需要页面真正加载完成，比 Link Clicks 多过滤一层跳出。</p><p><strong>第二阶段：信号足够后切换（500+ ViewContent 事件）</strong></p><p>把 Campaign 目标切成 <strong>Engagement</strong> 或 <strong>Conversions</strong>，优化目标选 <code>ViewContent</code>。这时算法已经有足够样本，会去找”打开章节页”的人，而不只是”点广告”的人。</p><p>注意：<code>ViewContent</code> 触发越晚（比如滚动到 50% 才触发），信号质量越高，但积累速度越慢。可以先用”页面加载时触发”过渡到 500 个，再换成”滚动 50% 触发”重新积累相似受众。</p><p><strong>第三阶段：Lookalike 扩量（Custom Audience 500+ 人）</strong></p><p>以”触发过 ViewContent 的访客”为种子，建 1% Lookalike。这批人统计上和你最好的读者最像，CPM 可能比兴趣定向高，但 ROAS 通常高 2–4 倍。</p><h3 id="在-Next-js-章节页接入-scroll-50-ViewContent"><a href="#在-Next-js-章节页接入-scroll-50-ViewContent" class="headerlink" title="在 Next.js 章节页接入 scroll-50% ViewContent"></a>在 Next.js 章节页接入 scroll-50% ViewContent</h3><p>在章节页的 Client Component 里：</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="string">&#x27;use client&#x27;</span></span><br><span class="line"><span class="keyword">import</span> &#123; useEffect &#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">function</span> <span class="title function_">ChapterScrollPixel</span>(<span class="params">&#123; chapterTitle &#125;: &#123; chapterTitle: <span class="built_in">string</span> &#125;</span>) &#123;</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">let</span> fired = <span class="literal">false</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">const</span> <span class="title function_">onScroll</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">      <span class="keyword">const</span> scrolled =</span><br><span class="line">        <span class="variable language_">window</span>.<span class="property">scrollY</span> / (<span class="variable language_">document</span>.<span class="property">body</span>.<span class="property">scrollHeight</span> - <span class="variable language_">window</span>.<span class="property">innerHeight</span>)</span><br><span class="line">      <span class="keyword">if</span> (!fired &amp;&amp; scrolled &gt;= <span class="number">0.5</span>) &#123;</span><br><span class="line">        fired = <span class="literal">true</span></span><br><span class="line">        <span class="variable language_">window</span>.<span class="property">fbq</span>?.(<span class="string">&#x27;track&#x27;</span>, <span class="string">&#x27;ViewContent&#x27;</span>, &#123;</span><br><span class="line">          <span class="attr">content_type</span>: <span class="string">&#x27;chapter&#x27;</span>,</span><br><span class="line">          <span class="attr">content_name</span>: chapterTitle,</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_">window</span>.<span class="title function_">addEventListener</span>(<span class="string">&#x27;scroll&#x27;</span>, onScroll, &#123; <span class="attr">passive</span>: <span class="literal">true</span> &#125;)</span><br><span class="line">    <span class="keyword">return</span> <span class="function">() =&gt;</span> <span class="variable language_">window</span>.<span class="title function_">removeEventListener</span>(<span class="string">&#x27;scroll&#x27;</span>, onScroll)</span><br><span class="line">  &#125;, [chapterTitle])</span><br><span class="line"></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><p>在章节页 Server Component 里引用它：</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// app/book/[slug]/chapter/[ch]/page.tsx</span></span><br><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">ChapterScrollPixel</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;@/components/ChapterScrollPixel&#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_">ChapterPage</span>(<span class="params">&#123; params &#125;</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> chapter = <span class="title function_">getChapter</span>(params.<span class="property">slug</span>, params.<span class="property">ch</span>)</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">ChapterScrollPixel</span> <span class="attr">chapterTitle</span>=<span class="string">&#123;chapter.title&#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;/&gt;</span></span></span><br><span class="line">  )</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>用标准 <code>ViewContent</code> 事件名而不是自定义事件，是因为 Conversion 目标可以直接用它优化——用 <code>trackCustom</code> 就只能配成”自定义转化”，多一步且算法支持的优化模型更少。</p><h3 id="与电商-Pixel-的核心区别"><a href="#与电商-Pixel-的核心区别" class="headerlink" title="与电商 Pixel 的核心区别"></a>与电商 Pixel 的核心区别</h3><p>电商的转化漏斗有明确终点（Purchase），内容站没有。内容站的”转化”是<strong>会话深度</strong>：读者在站内翻了多少页，就决定了广告收入多少。因此：</p><ul><li>电商：把 Purchase 信号传得越准越好</li><li>内容站：把”真正读内容”的信号传得越准越好，<code>ViewContent</code> at scroll-50% 是当前最接近的近似</li></ul><hr><h2 id="要不要用-我的判断框架"><a href="#要不要用-我的判断框架" class="headerlink" title="要不要用 &#x2F; 我的判断框架"></a>要不要用 &#x2F; 我的判断框架</h2><p><strong>值得现在上</strong>：</p><ul><li>你在跑 Facebook &#x2F; Instagram 广告，或计划跑</li><li>独立站有真实交易，需要优化 Purchase 转化</li><li>想做再营销或相似受众扩量</li></ul><p><strong>先把基础做好再说</strong>：</p><ul><li>网站月 UV 不足 1 万，广告预算不足 $1000&#x2F;月——此时数据量太少，算法学不动</li><li>没有服务端能力接 CAPI——只装浏览器 Pixel 信号损失严重，建议用 Shopify&#x2F;WordPress 插件替代手写</li></ul><p><strong>重点关注</strong>：</p><ul><li>iOS 14+ AEM 配置是必选项，不是可选项</li><li><code>event_id</code> 去重是 CAPI 双打的基础，漏掉就会数据虚报</li><li>EMQ 分数低于 5 分，先补 <code>external_id</code> 和 email 再说其他优化</li></ul><hr><blockquote><p>我目前在 fwork 项目里做 AI Infra，Pixel 是独立站产品的基础设施之一。这篇文章把我踩过的坑和查过的文档整理成了一条可执行的链路——希望对同样在「前端转 Agent&#x2F;全栈」路上的你有用。</p></blockquote>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/36251c389fef/</id>
    <link href="https://blog.zisheng.pro/posts/36251c389fef/"/>
    <published>2026-07-04T12:00:00.000Z</published>
    <summary>本文从前端视角系统拆解 Facebook Pixel：从基础安装、标准事件到自定义事件、转化 API（CAPI）、再到与 GA4 协同、iOS 14+ 隐私限制应对、广告归因模型等进阶内容，覆盖独立站运营者和前端开发者在投放 Facebook 广告时必须掌握的全部技术细节，帮助你真正&quot;看懂数据、投好广告&quot;。</summary>
    <title>Facebook Pixel 从入门到精通</title>
    <updated>2026-09-18T08:15:28.629Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="营销" scheme="https://blog.zisheng.pro/categories/%E8%90%A5%E9%94%80/"/>
    <category term="Facebook Pixel" scheme="https://blog.zisheng.pro/tags/Facebook-Pixel/"/>
    <category term="Facebook Ads" scheme="https://blog.zisheng.pro/tags/Facebook-Ads/"/>
    <category term="广告投放" scheme="https://blog.zisheng.pro/tags/%E5%B9%BF%E5%91%8A%E6%8A%95%E6%94%BE/"/>
    <category term="Conversions API" scheme="https://blog.zisheng.pro/tags/Conversions-API/"/>
    <category term="Events Manager" scheme="https://blog.zisheng.pro/tags/Events-Manager/"/>
    <category term="数字营销" scheme="https://blog.zisheng.pro/tags/%E6%95%B0%E5%AD%97%E8%90%A5%E9%94%80/"/>
    <content>
      <![CDATA[<p>我在做小说 H5 站的 Facebook 投流时，踩了一个坑：Pixel 装了，PageView 和 ViewContent 也在触发，但广告优化目标一直跑不起来，CPM 高、CTR 低，算法像是在盲投。</p><p>排查了两天，发现根本问题是：<strong>我的关键行为事件没有喂给 Meta</strong>。读者翻了多少章、停留多久、是否完成了阅读——这些数据 Meta 完全不知道，自然没法找到”高价值读者”来投。</p><p>这让我认真研究了一遍 Events Manager。以下是我拆解后的完整认知。</p><h2 id="一句话总结"><a href="#一句话总结" class="headerlink" title="一句话总结"></a>一句话总结</h2><p>Events Manager 是 Meta 广告的”数据大脑”——<strong>你告诉它用户做了什么，它帮你找更多做同样事情的人。</strong></p><p>数据质量越高，算法学得越精准，广告效率越高。反过来，数据残缺或质量差，钱就烧在错误的人身上。</p><h2 id="Events-Manager-是什么"><a href="#Events-Manager-是什么" class="headerlink" title="Events Manager 是什么"></a>Events Manager 是什么</h2><p>Meta Events Manager（事件管理工具）是 Meta Business Suite 下的一个模块，入口在 <code>business.facebook.com/events_manager</code>。</p><p>它做三件事：</p><ol><li><strong>管理数据源</strong>：Pixel（网站追踪代码）和 Conversions API（服务端追踪接口）</li><li><strong>监控事件质量</strong>：帮你发现事件丢失、重复、匹配质量低等问题</li><li><strong>配置自定义转化</strong>：把原始事件组合成广告优化目标</li></ol><p>你在这里配置好事件，投放时就能选”以 ViewContent 优化”或”以 Purchase 优化”——Meta 算法会去找最可能完成该行为的用户。</p><h2 id="两条数据通路：Pixel-vs-Conversions-API"><a href="#两条数据通路：Pixel-vs-Conversions-API" class="headerlink" title="两条数据通路：Pixel vs Conversions API"></a>两条数据通路：Pixel vs Conversions API</h2><p>这是 Events Manager 最核心的架构概念。</p><h3 id="Pixel（浏览器端）"><a href="#Pixel（浏览器端）" class="headerlink" title="Pixel（浏览器端）"></a>Pixel（浏览器端）</h3><p>Pixel 是一段 JS 代码，运行在<strong>用户浏览器里</strong>。用户访问页面时，浏览器向 Meta 服务器发送事件请求。</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="title function_">fbq</span>(<span class="string">&#x27;track&#x27;</span>, <span class="string">&#x27;ViewContent&#x27;</span>, &#123;</span><br><span class="line">  <span class="attr">content_type</span>: <span class="string">&#x27;article&#x27;</span>,</span><br><span class="line">  <span class="attr">content_ids</span>: [<span class="string">&#x27;chapter-5&#x27;</span>],</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p><strong>优势</strong>：部署简单，实时触发，能捕捉页面行为。</p><p><strong>劣势</strong>：</p><ul><li>被广告拦截器屏蔽（iOS Safari、uBlock Origin 等）</li><li>iOS 14+ ATT 隐私限制，匹配率下降</li><li>浏览器关闭后事件丢失</li></ul><h3 id="Conversions-API（CAPI，服务端）"><a href="#Conversions-API（CAPI，服务端）" class="headerlink" title="Conversions API（CAPI，服务端）"></a>Conversions API（CAPI，服务端）</h3><p>CAPI 是从你的<strong>服务器直接向 Meta API 发送事件</strong>，绕过浏览器限制。</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 服务端（Node.js 示例）</span></span><br><span class="line"><span class="keyword">const</span> serverEvent = <span class="keyword">new</span> bizSdk.<span class="title class_">ServerEvent</span>()</span><br><span class="line">  .<span class="title function_">setEventName</span>(<span class="string">&#x27;Purchase&#x27;</span>)</span><br><span class="line">  .<span class="title function_">setEventTime</span>(<span class="title class_">Math</span>.<span class="title function_">floor</span>(<span class="title class_">Date</span>.<span class="title function_">now</span>() / <span class="number">1000</span>))</span><br><span class="line">  .<span class="title function_">setUserData</span>(userData)</span><br><span class="line">  .<span class="title function_">setCustomData</span>(customData);</span><br><span class="line"></span><br><span class="line"><span class="keyword">new</span> <span class="title class_">EventRequest</span>(<span class="variable constant_">PIXEL_ID</span>, [serverEvent]).<span class="title function_">execute</span>();</span><br></pre></td></tr></table></figure><p><strong>优势</strong>：不受广告拦截、iOS 限制影响，数据更完整可靠。</p><p><strong>劣势</strong>：需要后端开发；用户隐私数据需要哈希处理后发送。</p><h3 id="最佳实践：两者并用-去重"><a href="#最佳实践：两者并用-去重" class="headerlink" title="最佳实践：两者并用 + 去重"></a>最佳实践：两者并用 + 去重</h3><p>Meta 官方建议 <strong>Pixel + CAPI 同时部署</strong>，同一个事件从两条通路都发送：</p><ul><li>广告拦截器屏蔽了 Pixel？CAPI 兜底</li><li>服务端延迟高？Pixel 先到</li></ul><p>但两条通路都发，意味着同一个事件 Meta 会收到两次，必须加 <code>event_id</code> 去重：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Pixel 端</span></span><br><span class="line"><span class="title function_">fbq</span>(<span class="string">&#x27;track&#x27;</span>, <span class="string">&#x27;ViewContent&#x27;</span>, eventData, &#123; <span class="attr">eventID</span>: <span class="string">&#x27;chapter-5-view-1720051200&#x27;</span> &#125;);</span><br><span class="line"></span><br><span class="line"><span class="comment">// CAPI 端（传同一个 event_id）</span></span><br><span class="line">serverEvent.<span class="title function_">setEventId</span>(<span class="string">&#x27;chapter-5-view-1720051200&#x27;</span>);</span><br></pre></td></tr></table></figure><p>Meta 看到同一 <code>event_id</code> 的两条记录，只计一次。</p><h2 id="标准事件-vs-自定义转化"><a href="#标准事件-vs-自定义转化" class="headerlink" title="标准事件 vs 自定义转化"></a>标准事件 vs 自定义转化</h2><h3 id="标准事件（Standard-Events）"><a href="#标准事件（Standard-Events）" class="headerlink" title="标准事件（Standard Events）"></a>标准事件（Standard Events）</h3><p>Meta 预定义的 17 个事件，算法天然理解其含义：</p><table><thead><tr><th>事件名</th><th>典型场景</th></tr></thead><tbody><tr><td><code>PageView</code></td><td>任何页面访问</td></tr><tr><td><code>ViewContent</code></td><td>查看具体内容（文章、商品详情页）</td></tr><tr><td><code>Search</code></td><td>搜索</td></tr><tr><td><code>AddToCart</code></td><td>加购</td></tr><tr><td><code>InitiateCheckout</code></td><td>开始结算</td></tr><tr><td><code>Purchase</code></td><td>完成购买</td></tr><tr><td><code>Lead</code></td><td>提交表单 &#x2F; 注册</td></tr><tr><td><code>Subscribe</code></td><td>订阅</td></tr><tr><td><code>CompleteRegistration</code></td><td>完成注册</td></tr></tbody></table><p>用标准事件的好处：<strong>Meta 算法直接懂你在优化什么</strong>，不需要额外学习。</p><p>对我的小说站，我这样映射：</p><ul><li><code>PageView</code> — 所有页面</li><li><code>ViewContent</code> — 每个章节页</li><li><code>Lead</code> — 模拟”深度阅读”（翻了 3 章以上触发一次）</li></ul><h3 id="自定义转化（Custom-Conversions）"><a href="#自定义转化（Custom-Conversions）" class="headerlink" title="自定义转化（Custom Conversions）"></a>自定义转化（Custom Conversions）</h3><p>当标准事件粒度不够时，自定义转化可以组合条件。</p><p><strong>示例</strong>：想优化”读完第 5 章的用户”</p><p>在 Events Manager → Custom Conversions 里配置：</p><ul><li>事件：<code>ViewContent</code></li><li>条件：URL 包含 <code>/chapters/ch-005</code></li></ul><p>这样 Meta 会专门找”最可能读到第 5 章”的用户来投放，而不是泛泛地找”会浏览内容的人”。</p><p>自定义转化的本质是<strong>在标准事件上加筛选漏斗</strong>，让算法优化目标更精准。</p><h2 id="Event-Match-Quality：决定你的钱烧得值不值"><a href="#Event-Match-Quality：决定你的钱烧得值不值" class="headerlink" title="Event Match Quality：决定你的钱烧得值不值"></a>Event Match Quality：决定你的钱烧得值不值</h2><p>EMQ（事件匹配质量）是 Events Manager 里最容易忽视但最重要的指标，1–10 分制。</p><p><strong>它衡量什么</strong>：Meta 能把多少比例的事件匹配回真实用户账号。</p><table><thead><tr><th>EMQ 分段</th><th>含义</th><th>广告效果</th></tr></thead><tbody><tr><td>8–10</td><td>优秀</td><td>算法学习精准，广告 ROI 高</td></tr><tr><td>5–7</td><td>一般</td><td>约 30–50% 事件无法归因</td></tr><tr><td>1–4</td><td>差</td><td>算法几乎在盲投</td></tr></tbody></table><p><strong>提高 EMQ 的核心手段</strong>：发送更多用户标识符（都需要 SHA-256 哈希后传）：</p><table><thead><tr><th>字段</th><th>说明</th><th>权重</th></tr></thead><tbody><tr><td><code>em</code></td><td>邮箱哈希</td><td>最高</td></tr><tr><td><code>ph</code></td><td>手机号哈希</td><td>高</td></tr><tr><td><code>external_id</code></td><td>你系统里的用户 ID</td><td>中</td></tr><tr><td><code>client_ip_address</code></td><td>用户 IP（CAPI 需手动传，Pixel 自动收集）</td><td>中</td></tr><tr><td><code>client_user_agent</code></td><td>浏览器 UA</td><td>中</td></tr><tr><td><code>fbp</code></td><td>Pixel cookie（<code>_fbp</code>）</td><td>中</td></tr></tbody></table><p>小说站的匿名访客没有邮箱，我的做法是至少传 IP + UA + <code>fbp</code>，EMQ 能维持在 5–7 分。如果有登录体系，传邮箱哈希能直接拉到 8 分以上。</p><h2 id="Event-Testing-Tool：上线前必跑"><a href="#Event-Testing-Tool：上线前必跑" class="headerlink" title="Event Testing Tool：上线前必跑"></a>Event Testing Tool：上线前必跑</h2><p>入口：Events Manager → Data Sources → 选你的 Pixel → Test Events。</p><p>输入网页 URL，打开浏览器模拟用户行为，右侧会<strong>实时显示 Meta 收到的每个事件</strong>，包括：</p><ul><li>事件名是否正确</li><li>参数是否完整</li><li>是否被去重</li><li>匹配到了哪些用户标识符</li></ul><p>这是 Debug 神器。每次部署新事件，必须先在 Test Events 里验证，再推生产。</p><p>常见问题排查：</p><table><thead><tr><th>现象</th><th>原因</th></tr></thead><tbody><tr><td>事件触发了但 Test Events 没出现</td><td>Pixel 代码没正确加载，检查 <code>window.fbq</code> 是否存在</td></tr><tr><td>事件出现但参数为空</td><td><code>fbq(&#39;track&#39;, ...)</code> 第二个参数写法有误</td></tr><tr><td>两条记录只计一次</td><td>去重生效，正常</td></tr><tr><td>两条记录都被计入</td><td><code>event_id</code> 没传，或 Pixel 端和 CAPI 端的值不一致</td></tr><tr><td>EMQ 显示 0 或 N&#x2F;A</td><td>没传任何用户标识符，补上 IP + UA + fbp</td></tr></tbody></table><h2 id="要不要用-我的判断框架"><a href="#要不要用-我的判断框架" class="headerlink" title="要不要用 &#x2F; 我的判断框架"></a>要不要用 &#x2F; 我的判断框架</h2><p><strong>要不要上 CAPI？</strong></p><table><thead><tr><th>场景</th><th>建议</th></tr></thead><tbody><tr><td>纯内容站、无登录、主要靠 Facebook 投流</td><td><strong>要上</strong>。iOS 拦截 Pixel 的比例越来越高，CAPI 是补全数据的唯一手段</td></tr><tr><td>有服务端但没后端开发资源</td><td>先用 Meta 的 Conversions API Gateway（托管版，不用写代码）</td></tr><tr><td>电商 &#x2F; 有 Purchase 事件</td><td><strong>必上</strong>。Purchase 数据残缺直接影响 ROAS</td></tr><tr><td>纯品牌曝光、不优化转化</td><td>可以暂缓，Pixel 够用</td></tr></tbody></table><p><strong>自定义转化值得配吗？</strong></p><p>当你的标准事件粒度不够精细，或者想区分”浅度访问”和”深度参与”时，自定义转化值得投入 15 分钟配置。成本极低，收益是算法优化目标的精度。</p><p><strong>EMQ 多少够用？</strong></p><p>5 分以上可以跑，7 分以上才算健康。低于 5 先排查是不是 IP&#x2F;UA 没传，再考虑引入登录体系收集邮箱哈希。</p><hr><p>Events Manager 本质上是 Meta 算法的”喂料接口”。数据喂得越准、越完整，算法的学习曲线越陡，广告效率越高。作为开发者，这是我们能直接控制的少数几个广告变量之一，值得认真对待。</p>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/ddcdd7b9c6e2/</id>
    <link href="https://blog.zisheng.pro/posts/ddcdd7b9c6e2/"/>
    <published>2026-07-04T02:00:00.000Z</published>
    <summary>从 Pixel 到 Conversions API，从标准事件到自定义转化，系统拆解 Meta Events Manager 的核心机制。写给想把广告投放做精的开发者和运营，让你的每一个转化事件都能准确喂给 Meta 算法，把广告预算花在刀刃上。</summary>
    <title>Meta Events Manager 深度指南：让广告数据不再是黑盒</title>
    <updated>2026-09-18T08:15:28.629Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="AI" scheme="https://blog.zisheng.pro/categories/AI/"/>
    <category term="fiction" scheme="https://blog.zisheng.pro/tags/fiction/"/>
    <category term="arbitrage" scheme="https://blog.zisheng.pro/tags/arbitrage/"/>
    <category term="cold-traffic" scheme="https://blog.zisheng.pro/tags/cold-traffic/"/>
    <category term="Facebook" scheme="https://blog.zisheng.pro/tags/Facebook/"/>
    <category term="AI Agent" scheme="https://blog.zisheng.pro/tags/AI-Agent/"/>
    <content>
      <![CDATA[<p>我在做一个小项目：用 AI 生成英文言情小说，搭小说 H5 站，跑 Facebook 广告买流量，靠 AdSense&#x2F;AdX 广告变现，赚中间差价。</p><p>听起来很野，但本质是个套利模型：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">利润 = 单次 session RPM × 每次 session 产生的 pageview 数 / 1000 − Facebook CPC</span><br></pre></td></tr></table></figure><p>为了跑通这个模型，我需要解决三个杠杆：L1 是让封面 CTR 够高（广告素材质量）；L2 是让读者翻更多章节（内容深度）；L3 是让广告展示 RPM 够高（广告布局和页面性能）。</p><p>这篇讲 L2 里一个具体问题：<strong>如何写第一章</strong>。</p><hr><h2 id="一句话总结"><a href="#一句话总结" class="headerlink" title="一句话总结"></a>一句话总结</h2><p>Ch1 的读者是冷流量，他们在前 200 字内决定走还是留——把第一章当 Landing Page 来设计，而不是当小说章节来写。</p><hr><h2 id="冷流量读者和暖流量读者的区别"><a href="#冷流量读者和暖流量读者的区别" class="headerlink" title="冷流量读者和暖流量读者的区别"></a>冷流量读者和暖流量读者的区别</h2><p>写小说一般假设读者有背景——他们知道这本书的简介，看过评论，或者是老读者回来续读。这是暖流量。</p><p>Facebook 广告带来的是冷流量：读者点进来之前只看到了封面图和一句 tagline。他们不知道主角叫什么，不知道故事发生在哪，没有任何情感投入。他们在 200 字内——大概 60 秒——决定：继续读，还是点 Back。</p><p>这个决策场景和一个付费广告的 Landing Page 完全一样。区别只是产品换成了小说章节。</p><p>所以 ch1 的写作目标不是”交代背景”，而是”让读者感到必须往下读”。</p><hr><h2 id="200-字节拍结构"><a href="#200-字节拍结构" class="headerlink" title="200 字节拍结构"></a>200 字节拍结构</h2><p>我给 AI 写内容时用了一张节拍检查表，把 ch1 切成 6 个区间，每个区间有明确的交付物：</p><table><thead><tr><th>字数区间</th><th>必须完成的事</th><th>为什么</th></tr></thead><tbody><tr><td>0–30 字</td><td>落入动作中，主角已经在做某事</td><td>建立 POV 和能量，不给读者走神的机会</td></tr><tr><td>30–100 字</td><td>一个具体细节点出主角的核心问题</td><td>让读者有东西在乎，无需解释</td></tr><tr><td>100–200 字</td><td>引入张力来源（人&#x2F;情境&#x2F;物件）</td><td>读者必须感到”这要出问题了”</td></tr><tr><td>200–400 字</td><td>用动作（非叙述）展示她想要什么、什么在阻挡</td><td>情感代入；读者必须看到自己或某个原型</td></tr><tr><td>400–700 字</td><td>第一个意外转折</td><td>证明故事会有惊喜，不是慢热消耗</td></tr><tr><td>700–结尾</td><td>再推一个节拍，用未解决的逆转收尾</td><td>强迫读者翻下一章</td></tr></tbody></table><p>这张表的核心逻辑：<strong>每个区间都在回答”读者为什么不能现在停下来”。</strong></p><hr><h2 id="Ch1-的专属硬规则"><a href="#Ch1-的专属硬规则" class="headerlink" title="Ch1 的专属硬规则"></a>Ch1 的专属硬规则</h2><p>除了节拍结构，还有几条只适用于 ch1 的约束：</p><h3 id="首行禁令"><a href="#首行禁令" class="headerlink" title="首行禁令"></a>首行禁令</h3><p>第一句话不能是天气、时刻或场景描写。这是最常见的 AI 生成小说 bug，也是最致命的——冷流量读者在第一句就判断”这不是我要读的东西”。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">❌ &quot;城市灯光在 Elena 驻立的顶层窗户下闪烁。&quot;</span><br><span class="line">❌ &quot;自从她最后一次见到他，已经过了三年。&quot;</span><br><span class="line">✅ 某个已经在动作、冲突或决策中的暗示</span><br></pre></td></tr></table></figure><h3 id="回忆上限"><a href="#回忆上限" class="headerlink" title="回忆上限"></a>回忆上限</h3><p>前 500 字内最多 2 句回忆&#x2F;背景叙述，且只有当它制造反讽时才允许（她以为的 vs 现在的现实）。背景叙述在 ch1 里是转化杀手。</p><h3 id="角色上限"><a href="#角色上限" class="headerlink" title="角色上限"></a>角色上限</h3><p>ch1 最多引入 2 个具名角色（主角 + 另一人）。开篇群像场景让冷流量读者无法代入任何人，直接跳出。</p><h3 id="对话时机"><a href="#对话时机" class="headerlink" title="对话时机"></a>对话时机</h3><p>第一段对话必须在第 300 字前出现。对话意味着人与人之间有事发生，这是言情小说的核心承诺——越晚兑现，跳出率越高。</p><h3 id="Facebook-Promise-检查"><a href="#Facebook-Promise-检查" class="headerlink" title="Facebook Promise 检查"></a>Facebook Promise 检查</h3><p>这是我加的一条，纯粹从投放角度来的：定稿 ch1 之前，把广告里的 tagline 重读一遍，确认 ch1 开篇 200 字的情绪基调和 tagline 承诺的一致。</p><p>典型违规：tagline 写”一场不该发生的相遇”，ch1 却从主角的日常办公室生活开始，过了 800 字才见面。这不是”悬念”，这是让为”相遇”付了 CPC 的流量失望。</p><hr><h2 id="字数目标：更短比更长好"><a href="#字数目标：更短比更长好" class="headerlink" title="字数目标：更短比更长好"></a>字数目标：更短比更长好</h2><p>常规章节目标是 1,400–1,800 字。但 ch1 建议取下限（1,400–1,500 字）。</p><p>原因很直接：冷流量读者在 7 分钟内读完 ch1 → 立即点”下一章” → 产生第二个 pageview。更长的 ch1 不会让他们更投入，只是延迟了那次点击。</p><hr><h2 id="200-字止损测试"><a href="#200-字止损测试" class="headerlink" title="200 字止损测试"></a>200 字止损测试</h2><p>写完 ch1 后，我用这个测试验收：</p><blockquote><p>只读前 200 字。假设有人从 Facebook 广告来，除此之外一无所知，他们会<strong>感受到什么</strong>并想知道接下来发生什么吗？</p></blockquote><p>如果答案是”他们会<strong>理解</strong>场景设定”——从第 1 字重写。</p><p>理解不等于感受。感受才能转化。</p><hr><h2 id="要不要用-我的判断框架"><a href="#要不要用-我的判断框架" class="headerlink" title="要不要用 &#x2F; 我的判断框架"></a>要不要用 &#x2F; 我的判断框架</h2><p>这套规范有没有价值，取决于你的流量结构：</p><table><thead><tr><th>场景</th><th>建议</th></tr></thead><tbody><tr><td>跑付费广告（Facebook&#x2F;TikTok）带流量</td><td><strong>值得上</strong>。ch1 就是你的 landing page，这些规则是 CRO 原则的小说翻译版</td></tr><tr><td>SEO 自然流量为主</td><td><strong>可以再等</strong>。来自搜索的读者有更强的意图，200 字紧迫感要求没那么高</td></tr><tr><td>纯粹写给自己看&#x2F;文学创作</td><td><strong>重点关注其他维度</strong>。这套规范优化的是转化，不是艺术性</td></tr></tbody></table><p>AI 写小说这件事，我越做越觉得它本质是内容产品工程，而不是创作本身。写作规范是 prompt engineering，节拍结构是信息架构，CPC vs RPM 是单元经济——只是产品换成了阅读体验。</p><p>这个视角切换之后，很多”写作建议”就能被精确化：不是”第一章要有吸引力”，而是”前 200 字的读者感受必须和广告 tagline 的承诺对齐，否则冷流量 bounce”。</p><p>量化之后，才能优化。</p>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/bc743c6da846/</id>
    <link href="https://blog.zisheng.pro/posts/bc743c6da846/"/>
    <published>2026-07-04T02:00:00.000Z</published>
    <summary>我在做一个 Facebook 投流 + 小说站套利的 AI Agent 系统。这篇讲的是其中一个让我意识到&quot;写作即产品设计&quot;的规范——如何写第一章，让广告带来的冷流量读者在 200 字内决定继续读下去。不谈文学，只谈转化。</summary>
    <title>冷流量 Ch1 钩子：把第一章当 Landing Page 来写</title>
    <updated>2026-09-18T08:15:28.636Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="AI" scheme="https://blog.zisheng.pro/categories/AI/"/>
    <category term="前端开发" scheme="https://blog.zisheng.pro/tags/%E5%89%8D%E7%AB%AF%E5%BC%80%E5%8F%91/"/>
    <category term="Claude Code" scheme="https://blog.zisheng.pro/tags/Claude-Code/"/>
    <category term="Remotion" scheme="https://blog.zisheng.pro/tags/Remotion/"/>
    <category term="帧动画" scheme="https://blog.zisheng.pro/tags/%E5%B8%A7%E5%8A%A8%E7%94%BB/"/>
    <category term="RunCat" scheme="https://blog.zisheng.pro/tags/RunCat/"/>
    <category term="创意编程" scheme="https://blog.zisheng.pro/tags/%E5%88%9B%E6%84%8F%E7%BC%96%E7%A8%8B/"/>
    <content>
      <![CDATA[<p>macOS 菜单栏那只跟着 CPU 使用率越跑越快的小猫（<a href="https://github.com/Kyome22/menubar_runcat">RunCat</a>），大概是我用过最治愈的系统监控工具。作为一个从前端转 Agent 开发的人，我一直好奇：这种逐帧 sprite 动画，能不能用现代 React 工具链声明式地复刻出来？答案是 <strong>Remotion</strong>——一个用 React 写视频的框架。这篇文章我用 Claude Code + Remotion 把这只猫从头搭一遍，顺带把「帧动画」这件事讲透。</p><h2 id="一句话总结"><a href="#一句话总结" class="headerlink" title="一句话总结"></a>一句话总结</h2><p>RunCat 的猫本质只有 <strong>5 张 PNG</strong>，靠「取模循环 + 定时器间隔」跑起来。Remotion 的<strong>纯函数帧模型</strong>天生适合表达这种逐帧动画：把「第几帧显示哪张图」写成一个纯函数即可。但要认清边界——<strong>Remotion 产出的是渲染好的 GIF&#x2F;视频资产，不是常驻菜单栏的原生进程</strong>。想要真·菜单栏猫得靠 Tauri&#x2F;Electron，Remotion 负责的是把动画本身优雅地造出来。</p><h2 id="先扒开-RunCat：它到底怎么动"><a href="#先扒开-RunCat：它到底怎么动" class="headerlink" title="先扒开 RunCat：它到底怎么动"></a>先扒开 RunCat：它到底怎么动</h2><p>很多人以为菜单栏动画是什么黑魔法，扒完源码发现朴素得可爱。RunCat 的核心就三件事。</p><h3 id="1-5-帧-PNG-循环"><a href="#1-5-帧-PNG-循环" class="headerlink" title="1. 5 帧 PNG 循环"></a>1. 5 帧 PNG 循环</h3><p>仓库 <code>Assets.xcassets</code> 里躺着 <code>cat0.png</code> 到 <code>cat4.png</code>，一共 5 帧。帧循环逻辑一行搞定：</p><figure class="highlight swift"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">index <span class="operator">=</span> (index <span class="operator">+</span> <span class="number">1</span>) <span class="operator">%</span> frames.count   <span class="comment">// 4 之后回到 0，无限循环</span></span><br><span class="line">statusItem.button<span class="operator">?</span>.image <span class="operator">=</span> frames[index]</span><br></pre></td></tr></table></figure><p>这个 <code>% frames.count</code>（取模）就是所有 sprite 循环动画的灵魂——记住它，等下 Remotion 里还是它。</p><h3 id="2-CPU-使用率决定「跑多快」"><a href="#2-CPU-使用率决定「跑多快」" class="headerlink" title="2. CPU 使用率决定「跑多快」"></a>2. CPU 使用率决定「跑多快」</h3><p>猫跑多快，取决于定时器的触发间隔。RunCat 的速度公式是：</p><figure class="highlight swift"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">interval <span class="operator">=</span> <span class="number">0.2</span> <span class="operator">/</span> <span class="built_in">max</span>(<span class="number">1.0</span>, <span class="built_in">min</span>(<span class="number">20.0</span>, usage <span class="operator">/</span> <span class="number">5.0</span>))</span><br></pre></td></tr></table></figure><p>拆开看：</p><ul><li><code>usage / 5.0</code>：把 CPU 百分比缩放一下</li><li><code>min(20, max(1, ...))</code>：钳制在 1~20 倍之间，防止闲时卡成 PPT、满载时快到抽搐</li><li><code>0.2 / 倍率</code>：基础间隔 0.2 秒，倍率越大间隔越短</li></ul><p>所以 <strong>CPU 越高 → 倍率越大 → 间隔越短 → 换帧越快 → 猫跑得越急</strong>。空闲时 0.2 秒一帧，满载时能到 0.01 秒一帧。就这么个反比关系，体感却极其直观。</p><h3 id="3-定时器驱动"><a href="#3-定时器驱动" class="headerlink" title="3. 定时器驱动"></a>3. 定时器驱动</h3><figure class="highlight swift"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">Timer</span>(timeInterval: <span class="keyword">self</span>.interval, repeats: <span class="literal">true</span>) &#123; <span class="keyword">self</span><span class="operator">?</span>.next() &#125;</span><br></pre></td></tr></table></figure><p>每隔 <code>interval</code> 触发一次 <code>next()</code>，<code>next()</code> 里就是上面那句取模换帧。<strong>这是典型的「有状态命令式」动画</strong>：一个 <code>index</code> 变量，定时 <code>+1</code>。</p><p>记住这个「命令式、有状态」的特征——因为 Remotion 的模型跟它<strong>恰好相反</strong>，这是本文最值得玩味的地方。</p><h2 id="为什么用-Remotion-做帧动画"><a href="#为什么用-Remotion-做帧动画" class="headerlink" title="为什么用 Remotion 做帧动画"></a>为什么用 Remotion 做帧动画</h2><p>Remotion 的核心信条只有一句：<strong>每一帧都是 <code>useCurrentFrame()</code> 的纯函数</strong>。</p><p>官方原话是——把你的组件想象成一个「把帧号转换成图像」的函数。每一帧独立渲染，不依赖上一帧的状态。这意味着 CSS 动画、<code>setInterval</code>、<code>requestAnimationFrame</code> 那套在 Remotion 里几乎全部失效，你必须重新用「帧号 → 画面」的思路建模。</p><p>对比一下就懂了：</p><table><thead><tr><th></th><th>RunCat（命令式）</th><th>Remotion（声明式）</th></tr></thead><tbody><tr><td>状态</td><td>维护一个 <code>index</code> 变量</td><td>无状态，只读 <code>frame</code></td></tr><tr><td>换帧</td><td>定时器 <code>index++</code></td><td><code>Math.floor(frame / n) % 5</code></td></tr><tr><td>时间</td><td>真实时钟（<code>Timer</code>）</td><td>逻辑帧号（<code>useCurrentFrame</code>）</td></tr><tr><td>产物</td><td>实时菜单栏画面</td><td>渲染成 GIF&#x2F;视频</td></tr></tbody></table><p>看出来了吗？RunCat 那句 <code>(index + 1) % 5</code> 的「取模」精神，在 Remotion 里变成了<strong>用当前帧号直接算出该显示第几张 sprite</strong>——不需要状态，因为帧号本身就是「时间的坐标」。这就是纯函数帧模型的优雅之处。</p><h2 id="动手：Claude-Code-Remotion-复刻奔跑的猫"><a href="#动手：Claude-Code-Remotion-复刻奔跑的猫" class="headerlink" title="动手：Claude Code + Remotion 复刻奔跑的猫"></a>动手：Claude Code + Remotion 复刻奔跑的猫</h2><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="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">初始化一个 Remotion 项目，创建一个叫 RunningCat 的 Composition：</span><br><span class="line">- 144x144，30fps</span><br><span class="line">- 加载 public/cat/ 下的 0.png~4.png 五帧 sprite</span><br><span class="line">- 根据传入的 load（模拟 CPU 使用率）参数，用 RunCat 的速度公式</span><br><span class="line">  interval = 0.2 / clamp(load/5, 1, 20) 换算成换帧节奏</span><br><span class="line">- 像素图，用 imageRendering: pixelated 保持锐利</span><br></pre></td></tr></table></figure><p><code>npx create-video@latest</code> 起模板后，Claude Code 会把组件、<code>Root.tsx</code> 注册、<code>public/</code> 资源路径一次性搭好。<strong>帧素材</strong>可以直接扒 RunCat 仓库 <code>resources/cat/png/</code> 里的 5 张图，或者让 AI 图像工具生成一套你自己的 5 帧奔跑序列。</p><h3 id="第二步：核心-sprite-组件"><a href="#第二步：核心-sprite-组件" class="headerlink" title="第二步：核心 sprite 组件"></a>第二步：核心 sprite 组件</h3><p>这是全文的重点。RunCat 的「取模换帧」在 Remotion 里长这样：</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// src/RunningCat.tsx</span></span><br><span class="line"><span class="keyword">import</span> &#123;</span><br><span class="line">  <span class="title class_">AbsoluteFill</span>, <span class="title class_">Img</span>, staticFile,</span><br><span class="line">  useCurrentFrame, useVideoConfig,</span><br><span class="line">&#125; <span class="keyword">from</span> <span class="string">&#x27;remotion&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="variable constant_">CAT_FRAMES</span> = [<span class="number">0</span>, <span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>, <span class="number">4</span>].<span class="title function_">map</span>(<span class="function">(<span class="params">i</span>) =&gt;</span> <span class="title function_">staticFile</span>(<span class="string">`cat/<span class="subst">$&#123;i&#125;</span>.png`</span>));</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> <span class="title class_">RunningCat</span>: <span class="title class_">React</span>.<span class="property">FC</span>&lt;&#123; <span class="attr">load</span>: <span class="built_in">number</span> &#125;&gt; = <span class="function">(<span class="params">&#123; load &#125;</span>) =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">const</span> frame = <span class="title function_">useCurrentFrame</span>();</span><br><span class="line">  <span class="keyword">const</span> &#123; fps &#125; = <span class="title function_">useVideoConfig</span>();</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 照搬 RunCat 的速度公式</span></span><br><span class="line">  <span class="keyword">const</span> speed = <span class="title class_">Math</span>.<span class="title function_">max</span>(<span class="number">1</span>, <span class="title class_">Math</span>.<span class="title function_">min</span>(<span class="number">20</span>, load / <span class="number">5</span>));</span><br><span class="line">  <span class="keyword">const</span> secondsPerSprite = <span class="number">0.2</span> / speed;            <span class="comment">// 每张 sprite 停留的秒数</span></span><br><span class="line">  <span class="keyword">const</span> framesPerSprite = <span class="title class_">Math</span>.<span class="title function_">max</span>(               <span class="comment">// 换算成「视频帧」</span></span><br><span class="line">    <span class="number">1</span>,</span><br><span class="line">    <span class="title class_">Math</span>.<span class="title function_">round</span>(secondsPerSprite * fps),</span><br><span class="line">  );</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 灵魂取模：帧号 → 第几张 sprite</span></span><br><span class="line">  <span class="keyword">const</span> spriteIndex =</span><br><span class="line">    <span class="title class_">Math</span>.<span class="title function_">floor</span>(frame / framesPerSprite) % <span class="variable constant_">CAT_FRAMES</span>.<span class="property">length</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">AbsoluteFill</span> <span class="attr">style</span>=<span class="string">&#123;&#123;</span> <span class="attr">justifyContent:</span> &#x27;<span class="attr">center</span>&#x27;, <span class="attr">alignItems:</span> &#x27;<span class="attr">center</span>&#x27; &#125;&#125;&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">Img</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">        <span class="attr">src</span>=<span class="string">&#123;CAT_FRAMES[spriteIndex]&#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> <span class="attr">144</span>, <span class="attr">imageRendering:</span> &#x27;<span class="attr">pixelated</span>&#x27; &#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">    <span class="tag">&lt;/<span class="name">AbsoluteFill</span>&gt;</span></span></span><br><span class="line">  );</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>关键就那一行 <code>Math.floor(frame / framesPerSprite) % CAT_FRAMES.length</code>：</p><ul><li><code>frame / framesPerSprite</code>：当前帧对应到「第几个 sprite 周期」</li><li><code>Math.floor(...)</code>：取整，同一张图在多个视频帧里保持不变</li><li><code>% 5</code>：跑完 5 张回到第 0 张——<strong>和 RunCat 那句 <code>% frames.count</code> 是同一个灵魂</strong></li></ul><h3 id="第三步：注册-Composition"><a href="#第三步：注册-Composition" class="headerlink" title="第三步：注册 Composition"></a>第三步：注册 Composition</h3><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// src/Root.tsx</span></span><br><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">Composition</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;remotion&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">RunningCat</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;./RunningCat&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> <span class="title class_">RemotionRoot</span>: <span class="title class_">React</span>.<span class="property">FC</span> = <span class="function">() =&gt;</span> (</span><br><span class="line">  <span class="language-xml"><span class="tag">&lt;<span class="name">Composition</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">    <span class="attr">id</span>=<span class="string">&quot;RunningCat&quot;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">    <span class="attr">component</span>=<span class="string">&#123;RunningCat&#125;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">    <span class="attr">durationInFrames</span>=<span class="string">&#123;60&#125;</span>     // <span class="attr">见下方</span>「<span class="attr">无缝循环</span>」<span class="attr">的坑</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">    <span class="attr">fps</span>=<span class="string">&#123;30&#125;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">    <span class="attr">width</span>=<span class="string">&#123;144&#125;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">    <span class="attr">height</span>=<span class="string">&#123;144&#125;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">    <span class="attr">defaultProps</span>=<span class="string">&#123;&#123;</span> <span class="attr">load:</span> <span class="attr">50</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><br></pre></td></tr></table></figure><p><strong>无缝循环的坑</strong>：GIF 要循环不跳帧，<code>durationInFrames</code> 必须是「一个完整动画周期」的整数倍，也就是 <code>framesPerSprite * 5</code> 的倍数。比如 <code>load=50</code> 时 <code>speed=10</code>、<code>framesPerSprite = round(0.02*30)=1</code>，一个周期 5 帧，那 <code>durationInFrames</code> 取 5 的倍数（如 60）才能首尾对齐。这个细节 Claude Code 不一定主动帮你算，得自己盯一下。</p><h3 id="第四步：渲染成-GIF"><a href="#第四步：渲染成-GIF" class="headerlink" title="第四步：渲染成 GIF"></a>第四步：渲染成 GIF</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 基础渲染</span></span><br><span class="line">npx remotion render RunningCat out/cat.gif --codec=gif</span><br><span class="line"></span><br><span class="line"><span class="comment"># 透明背景（sprite 是抠好的 PNG 时必备）</span></span><br><span class="line">npx remotion render RunningCat out/cat.gif --codec=gif --image-format=png</span><br><span class="line"></span><br><span class="line"><span class="comment"># 降帧压体积：只渲染每 2 帧，30fps → 15fps</span></span><br><span class="line">npx remotion render RunningCat out/cat.gif --codec=gif --every-nth-frame=2</span><br><span class="line"></span><br><span class="line"><span class="comment"># 无限循环（GIF 默认就是无限循环，这里显式写出）</span></span><br><span class="line">npx remotion render RunningCat out/cat.gif --codec=gif --number-of-gif-loops=null</span><br></pre></td></tr></table></figure><p>跑完你就得到一只无限循环、背景透明的奔跑猫 GIF，可以塞进博客、README、PPT 或者任何网页。</p><h2 id="进阶：让猫「真的」随负载变速"><a href="#进阶：让猫「真的」随负载变速" class="headerlink" title="进阶：让猫「真的」随负载变速"></a>进阶：让猫「真的」随负载变速</h2><p>上面 <code>load</code> 是个固定值，猫匀速跑。想复刻 RunCat 那种「负载飙升猫就抽风」的动态感，有两条路。</p><p><strong>渲染成动态 GIF</strong>：用 <code>interpolate</code> 让 <code>load</code> 随帧号变化，模拟一次 CPU 尖峰：</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; interpolate &#125; <span class="keyword">from</span> <span class="string">&#x27;remotion&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> load = <span class="title function_">interpolate</span>(</span><br><span class="line">  frame,</span><br><span class="line">  [<span class="number">0</span>, <span class="number">45</span>, <span class="number">90</span>, <span class="number">135</span>, <span class="number">180</span>],</span><br><span class="line">  [<span class="number">10</span>, <span class="number">95</span>, <span class="number">30</span>, <span class="number">100</span>, <span class="number">10</span>],   <span class="comment">// 闲 → 爆 → 缓 → 再爆 → 闲</span></span><br><span class="line">  &#123; <span class="attr">extrapolateRight</span>: <span class="string">&#x27;clamp&#x27;</span> &#125;,</span><br><span class="line">);</span><br></pre></td></tr></table></figure><blockquote><p><strong>一个容易被忽略的深度坑</strong>：当速度随帧动态变化时，<code>Math.floor(frame / framesPerSprite)</code> 里的 <code>framesPerSprite</code> 每帧都在变，除法结果不再单调递增，sprite 会跳帧。严格正确的做法是<strong>对速度做相位累积</strong>（把每帧的换帧增量累加起来），而不是简单除法。RunCat 靠有状态的 <code>index++</code> 天然规避了这个问题，但 Remotion 的纯函数模型逼你把「累积」显式写出来——这正是命令式转声明式最反直觉的地方。做匀速 demo 可以偷懒用除法，做严肃动态动画就得老实累积。</p></blockquote><p><strong>驱动实时网页版</strong>：如果你要的不是视频而是一个活的网页挂件，用 <code>@remotion/player</code> 把同一个组件搬进浏览器，再把真实性能指标喂给 <code>load</code>，就是一个跑在网页里的实时 RunCat。同一份组件，既能渲染成 GIF，又能实时播放——这是 Remotion 相比纯 CSS 动画的一大优势。</p><h2 id="Claude-Code-在这条流程里加速了什么"><a href="#Claude-Code-在这条流程里加速了什么" class="headerlink" title="Claude Code 在这条流程里加速了什么"></a>Claude Code 在这条流程里加速了什么</h2><p>说句实在话，Remotion 的 API 不难，难的是「纯函数帧模型」的思维转换。Claude Code 的价值分三层：</p><ul><li><strong>脚手架层（省时间）</strong>：起项目、配 <code>Composition</code>、连 <code>public/</code> 资源、写渲染脚本，这些样板活它一把梭。</li><li><strong>翻译层（省脑子）</strong>：把 RunCat 那句 Swift 的命令式 <code>index++</code> 翻译成 Remotion 的声明式取模，这种「换范式重写」是 AI 的强项——你给它源码逻辑，它给你目标框架的地道实现。</li><li><strong>调试层（省试错）</strong>：无缝循环的帧数对齐、动态变速的相位累积这类坑，描述清楚现象后它能直接给修法，比自己翻文档快得多。</li></ul><p>但**「取模换帧」的原理、「相位累积」的坑、无缝循环的数学**——这些判断你得心里有数，否则 AI 给的代码你既 review 不了也调不动。这也印证了我一直的观点：<strong>AI 拉满执行力，判断框架还得是自己的</strong>。</p><h2 id="要不要这么搞：我的判断"><a href="#要不要这么搞：我的判断" class="headerlink" title="要不要这么搞：我的判断"></a>要不要这么搞：我的判断</h2><ul><li><strong>值得动手</strong>：你想给博客&#x2F;README&#x2F;演示做一个有辨识度的循环动画，或者想亲手理解「逐帧 sprite 动画」和「声明式帧模型」——RunCat 复刻是绝佳的练手项目，5 帧图、一个取模，麻雀虽小五脏俱全。</li><li><strong>别用错工具</strong>：你要的是一个<strong>常驻 macOS 菜单栏、读真实 CPU</strong> 的原生小猫——那 Remotion 不是答案，老老实实用 Swift（RunCat 本体）或 Tauri&#x2F;Electron。Remotion 造的是动画资产，不是系统进程。</li><li><strong>重点收获</strong>：真正值钱的不是这只猫，而是**「命令式有状态动画 → 声明式纯函数帧」的思维转换**。理解了 <code>useCurrentFrame</code> 这套模型，你能用 React 声明式地造出几乎任何数据驱动的视频——这才是 Remotion 值得前端认真学一次的原因。</li></ul><p>一只 5 帧的猫，串起了 sprite 动画原理、纯函数帧模型、和 AI 辅助的范式翻译。有时候最好的学习项目，就是把一个你每天都在看的小东西，亲手拆开再装回去。</p><blockquote><p>参考来源：<a href="https://github.com/Kyome22/menubar_runcat">menubar_runcat 源码</a> · <a href="https://www.remotion.dev/docs/use-current-frame">Remotion useCurrentFrame 文档</a> · <a href="https://www.remotion.dev/docs/render-as-gif">Remotion 渲染 GIF 文档</a></p></blockquote>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/5e4b74eb9d6f/</id>
    <link href="https://blog.zisheng.pro/posts/5e4b74eb9d6f/"/>
    <published>2026-07-03T12:00:00.000Z</published>
    <summary>RunCat 菜单栏那只随 CPU 使用率加速奔跑的小猫，本质是 5 帧 PNG 的循环 sprite 动画。这篇文章先扒开它的源码，拆解帧循环和速度公式，再用 Claude Code + Remotion 把这套逐帧动画在 React 里声明式复刻出来、渲染成可无限循环的 GIF，并讲清楚 Remotion「纯函数帧模型」的适用边界，以及这条工作流里 AI 到底加速了什么。</summary>
    <title>用 Claude Code + Remotion 复刻 RunCat 奔跑的猫帧动画</title>
    <updated>2026-09-18T08:15:28.640Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="AI" scheme="https://blog.zisheng.pro/categories/AI/"/>
    <category term="前端开发" scheme="https://blog.zisheng.pro/tags/%E5%89%8D%E7%AB%AF%E5%BC%80%E5%8F%91/"/>
    <category term="AI Infra" scheme="https://blog.zisheng.pro/tags/AI-Infra/"/>
    <category term="Claude" scheme="https://blog.zisheng.pro/tags/Claude/"/>
    <category term="Anthropic" scheme="https://blog.zisheng.pro/tags/Anthropic/"/>
    <category term="Claude Design" scheme="https://blog.zisheng.pro/tags/Claude-Design/"/>
    <category term="设计工具" scheme="https://blog.zisheng.pro/tags/%E8%AE%BE%E8%AE%A1%E5%B7%A5%E5%85%B7/"/>
    <content>
      <![CDATA[<p>Anthropic Labs 在 2026 年 4 月发布了 <strong>Claude Design</strong>——一个用对话生成设计稿、交互原型、PPT 和营销物料的 AI 工具，由 <strong>Claude Opus 4.7</strong> 驱动。它被普遍解读为「冲着 Figma 来的」，但作为一个天天在 design-to-code 链路上打转的前端，我更关心的是另一件事：<strong>它能读你的 codebase，反过来又能把产物交接给 Claude Code</strong>。这篇文章从前端视角讲讲它到底是什么、怎么用，以及要不要现在就上。</p><h2 id="一句话总结"><a href="#一句话总结" class="headerlink" title="一句话总结"></a>一句话总结</h2><p>Claude Design 的核心卖点不是「又一个 AI 画图工具」，而是<strong>把设计系统和代码库打通</strong>：它在 onboarding 时读你的 codebase 和设计文件，自动抽出 colors、typography、components 建成一套 design system，之后每个项目都自动套用。对前端来说，这意味着 design-to-code 这条链路第一次有可能从两端同时被 AI 咬合。</p><h2 id="Claude-Design-是什么"><a href="#Claude-Design-是什么" class="headerlink" title="Claude Design 是什么"></a>Claude Design 是什么</h2><p>一句话：<strong>你用自然语言描述要什么，Claude 生成第一版，然后你通过多种方式细调。</strong></p><p>它能生成的东西比想象中广：</p><ul><li>真实可交互的 prototype（不是静态图，是能点的）</li><li>产品 wireframe 和 mockup</li><li>多方向的设计探索（一次给你几套不同风格）</li><li>Pitch deck、PPT 演示</li><li>营销物料：landing page、社媒素材、campaign 视觉</li><li>代码驱动的原型，甚至能带 voice、video、shader、3D 和内置 AI</li></ul><h2 id="核心工作流：生成之后怎么改"><a href="#核心工作流：生成之后怎么改" class="headerlink" title="核心工作流：生成之后怎么改"></a>核心工作流：生成之后怎么改</h2><p>AI 生成设计的工具不少，Claude Design 真正的差异在<strong>细调环节</strong>。它给了四种并行的修改通道：</p><ol><li><strong>对话（Conversation）</strong>：直接跟 Claude 说「这个标题再大一点、配色冷一点」</li><li><strong>Inline comments</strong>：像在 Figma 里评论一样，圈住某个具体元素批注</li><li><strong>直接编辑（Direct editing）</strong>：文字可以直接改，不用绕一圈跟 AI 说</li><li><strong>自定义 slider</strong>：这是最有意思的——<strong>Claude 会自己生成调节旋钮</strong>，让你实时拖动 spacing、color、layout</li></ol><p>第 4 点值得展开。传统 AI 生成图是「抽卡」——不满意就重 roll，全凭运气。而 Claude Design 给你的是<strong>参数化的连续控制</strong>，而且这些 slider 是它根据当前设计动态生成的，不是固定几个。这在交互范式上比「反复 prompt 重生成」高了一个维度。</p><h2 id="对前端最有价值的三个点"><a href="#对前端最有价值的三个点" class="headerlink" title="对前端最有价值的三个点"></a>对前端最有价值的三个点</h2><p>抛开「取代设计师」这种标题党，我觉得对前端工程师真正有价值的是这三个能力：</p><h3 id="1-从-codebase-生成-design-system"><a href="#1-从-codebase-生成-design-system" class="headerlink" title="1. 从 codebase 生成 design system"></a>1. 从 codebase 生成 design system</h3><p>onboarding 时 Claude 会读你的代码库和设计文件，自动建一套 design system——colors、typography、components 全抽出来，之后每个新项目自动套用，保证品牌一致性。团队可以持续打磨这套系统，还能同时维护多套。</p><p>对前端来说这解决了一个老大难：<strong>设计稿和代码的 design token 对不齐</strong>。以前是设计师在 Figma 定一套、前端在代码里再实现一套，两边长期漂移。现在 AI 直接从你已有的代码里反推设计系统，源头就是统一的。</p><h3 id="2-Web-capture：抓真实产品的元素"><a href="#2-Web-capture：抓真实产品的元素" class="headerlink" title="2. Web capture：抓真实产品的元素"></a>2. Web capture：抓真实产品的元素</h3><p>它有个 web capture 工具，能直接从线上网站抓取元素，让原型「长得像真实产品」。做增量需求时特别实用——不用从零搭页面骨架，直接抓现有页面来改。</p><h3 id="3-导出-HTML-交接给-Claude-Code"><a href="#3-导出-HTML-交接给-Claude-Code" class="headerlink" title="3. 导出 HTML + 交接给 Claude Code"></a>3. 导出 HTML + 交接给 Claude Code</h3><p>导出选项里有两个对开发者关键：</p><ul><li><strong>standalone HTML 文件</strong>：产物可以直接是能跑的 HTML</li><li><strong>handoff bundle 到 Claude Code</strong>：设计稿能打包交接给 Claude Code 继续实现</li></ul><p>这就是我开头说的「两端咬合」：<strong>Claude Design 从 codebase 读设计系统 → 生成设计和原型 → 再把产物交回 Claude Code 落地成生产代码</strong>。整条 design-to-code 链路，AI 从设计端和工程端同时介入。</p><p>其他导出格式也齐全：组织内部 URL 共享、保存为文件夹、导出到 Canva、PDF、PPTX。导入同样灵活：text prompt、图片、DOCX&#x2F;PPTX&#x2F;XLSX 文档，或直接指向 codebase。</p><h2 id="规格与可用性"><a href="#规格与可用性" class="headerlink" title="规格与可用性"></a>规格与可用性</h2><table><thead><tr><th>项目</th><th>详情</th></tr></thead><tbody><tr><td>驱动模型</td><td>Claude Opus 4.7（Anthropic 最强的 vision 模型）</td></tr><tr><td>状态</td><td>Research preview（逐步放量）</td></tr><tr><td>可用套餐</td><td>Claude Pro &#x2F; Max &#x2F; Team &#x2F; Enterprise</td></tr><tr><td>计费</td><td>包含在现有订阅内，走标准用量额度，可选额外用量</td></tr><tr><td>企业策略</td><td>默认关闭，需管理员在 Organization settings 开启</td></tr></tbody></table><p>注意它用的是 <strong>Opus 4.7</strong> 而不是最新的 Fable 5——因为 4.7 是专门强化过 vision 能力的模型，处理设计这种视觉密集任务更合适，这也印证了 Anthropic「按任务选模型」而非「无脑上最新」的思路。</p><h2 id="和-Figma-Canva-是什么关系"><a href="#和-Figma-Canva-是什么关系" class="headerlink" title="和 Figma &#x2F; Canva 是什么关系"></a>和 Figma &#x2F; Canva 是什么关系</h2><p>媒体普遍把它定位成「挑战 Figma」，但我的判断更克制：</p><ul><li><strong>它不是 Figma 替代品</strong>：Figma 强在多人实时协作、精细的矢量控制、成熟的插件生态和 handoff 流程，这些 Claude Design 现阶段替代不了。</li><li><strong>它是 0→1 阶段的加速器</strong>：从想法到第一版可交互原型，Claude Design 快得多。适合验证概念、快速出多个方向、做 pitch。</li><li><strong>它和 Canva 是导出关系而非竞争</strong>：能直接导出到 Canva，说明 Anthropic 没打算做全链路闭环，而是嵌入现有工作流。</li></ul><p>所以更准确的说法是：<strong>它抢的是「打开 Figma 从空白画布开始」的那部分时间</strong>，而不是整个 Figma。</p><h2 id="前端视角：这对我们意味着什么"><a href="#前端视角：这对我们意味着什么" class="headerlink" title="前端视角：这对我们意味着什么"></a>前端视角：这对我们意味着什么</h2><p>结合我自己从前端转 Agent 开发的路径，谈几点判断：</p><p><strong>短期</strong>，它是个提效工具。做 landing page、内部工具原型、demo、给 PPM 讲方案的可交互 mockup，这些以前要拉设计师排期的活，现在能自己快速出。前端的「全栈化」又往设计端延伸了一步。</p><p><strong>中期</strong>，值得关注的是 design system 的自动化。如果 AI 能可靠地从代码反推、并维护设计系统，那设计和研发之间那道「token 对齐」的墙会越来越薄。这对 design system 维护者是个信号。</p><p><strong>长期</strong>，我更在意的是它作为 <strong>agentic workflow 一环</strong>的定位——从 codebase 输入到 Claude Code 输出，Claude Design 是把「设计」这个此前很难被 agent 编排的环节，接进了自动化流水线。这才是 AI Infra 视角下真正的看点：不是又多了个工具，而是又一个人类专业环节被纳入了 agent 可调度的范围。</p><h2 id="要不要现在就用"><a href="#要不要现在就用" class="headerlink" title="要不要现在就用"></a>要不要现在就用</h2><p>我的判断框架：</p><ul><li><strong>值得现在就上</strong>：你是 Pro&#x2F;Max 订阅用户，需要快速出原型、demo、pitch deck，或者想验证「从代码生成设计系统」在自己项目上的效果。反正包含在订阅里，试错成本几乎为零。</li><li><strong>可以再等等</strong>：你的核心诉求是精细的生产级设计协作、复杂的多人 handoff 流程——现阶段 research preview 还不够成熟，继续用 Figma。</li><li><strong>重点关注</strong>：如果你在做 AI Infra &#x2F; agentic workflow，Claude Design 的「codebase ↔ 设计 ↔ Claude Code」闭环值得深入研究，它可能是未来 design-to-code 自动化的一块关键拼图。</li></ul><p>Anthropic 把它放在 Labs 下、以 research preview 形式发布，本身就说明这是个「探索方向」而非「成熟产品」。但方向选得很准——<strong>用 AI 咬合设计和代码的两端</strong>，这件事一旦跑通，改变的不只是设计工具市场，而是整个前端的工作方式。</p><blockquote><p>参考来源：<a href="https://www.anthropic.com/news/claude-design-anthropic-labs">Introducing Claude Design by Anthropic Labs</a> · <a href="https://claude.com/product/design">Turn ideas into designs with Claude</a></p></blockquote>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/66115f19c248/</id>
    <link href="https://blog.zisheng.pro/posts/66115f19c248/"/>
    <published>2026-07-03T10:00:00.000Z</published>
    <summary>Anthropic Labs 发布了 Claude Design，一个用对话生成设计稿、原型和 PPT 的 AI 工具，由 Claude Opus 4.7 驱动，主打「读你的 codebase 生成设计系统」并能一键交接给 Claude Code。这篇文章从前端工程师的视角，梳理它的核心工作流、对 design-to-code 链路的冲击，以及要不要现在就用。</summary>
    <title>Claude Design 上手：前端视角看 Anthropic 的 AI 设计工具</title>
    <updated>2026-09-18T08:15:28.629Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="AI" scheme="https://blog.zisheng.pro/categories/AI/"/>
    <category term="LLM" scheme="https://blog.zisheng.pro/tags/LLM/"/>
    <category term="Agent" scheme="https://blog.zisheng.pro/tags/Agent/"/>
    <category term="Claude" scheme="https://blog.zisheng.pro/tags/Claude/"/>
    <category term="Anthropic" scheme="https://blog.zisheng.pro/tags/Anthropic/"/>
    <category term="Fable 5" scheme="https://blog.zisheng.pro/tags/Fable-5/"/>
    <content>
      <![CDATA[<p>Anthropic 发布了 <strong>Claude Fable 5</strong>（model ID：<code>claude-fable-5</code>），这是目前 Anthropic 能力最强的正式发布模型，定位是”最苛刻的推理任务和长程 agentic 工作”。我第一时间把 Claude Code 的默认模型切换了过去，这篇文章梳理一下它相比 Opus 4.8 的大更新，以及写代码接入时要注意的破坏性变更。</p><h2 id="一句话总结"><a href="#一句话总结" class="headerlink" title="一句话总结"></a>一句话总结</h2><p>Fable 5 的核心卖点不是”老任务做得更好”，而是<strong>能做以前模型做不了的事</strong>：单次请求自主运行几十分钟的长程任务、可靠的多 sub-agent 并行协作、端到端的企业级交付物。如果你的场景还是分类、摘要、单轮问答，Opus 4.8 依然是更划算的选择。</p><h2 id="能力升级"><a href="#能力升级" class="headerlink" title="能力升级"></a>能力升级</h2><h3 id="长程-agentic-任务（最大亮点）"><a href="#长程-agentic-任务（最大亮点）" class="headerlink" title="长程 agentic 任务（最大亮点）"></a>长程 agentic 任务（最大亮点）</h3><p>单次请求可以自主运行很长时间——复杂任务跑 15 分钟是正常现象。适合的场景包括：</p><ul><li>大型代码迁移、重构、通宵编码任务</li><li>端到端的企业交付物：财务分析、Excel 表格、PPT、文档</li><li>深度研究和长时间的自主工作流</li></ul><p>官方给的最佳实践是：<strong>在第一轮就给出完整、清晰的任务规格</strong>，然后放手让它执行，而不是挤牙膏式地多轮补充需求。</p><h3 id="多-sub-agent-协作"><a href="#多-sub-agent-协作" class="headerlink" title="多 sub-agent 协作"></a>多 sub-agent 协作</h3><p>以前的模型用 sub-agent 经常翻车，很多团队的 prompt 里都有”不要随便开 sub-agent”之类的压制性指令。Fable 5 反过来了：<strong>并行委派子任务非常可靠</strong>，能和长期运行的 sub-agent、peer agent 保持异步通信。官方甚至建议主动鼓励它委派：</p><blockquote><p>Delegate independent subtasks to sub-agents and keep working while they run.</p></blockquote><h3 id="其他提升"><a href="#其他提升" class="headerlink" title="其他提升"></a>其他提升</h3><ul><li><strong>Code review &#x2F; debugging</strong>：找真实 bug 的能力显著提升（recall 和 precision 都涨了）</li><li><strong>Vision</strong>：针对密集、模糊、翻转、有噪点的图像做了专门训练，会主动用 bash 和 crop 工具预处理图片</li><li><strong>Memory</strong>：给它一个可写的 memory 文件（哪怕就是个 <code>.md</code>），跨任务表现明显更好</li></ul><h2 id="规格与定价"><a href="#规格与定价" class="headerlink" title="规格与定价"></a>规格与定价</h2><table><thead><tr><th>项目</th><th>Claude Fable 5</th><th>Claude Opus 4.8</th></tr></thead><tbody><tr><td>Context window</td><td>1M tokens（默认即最大）</td><td>1M tokens</td></tr><tr><td>Max output</td><td>128K</td><td>128K</td></tr><tr><td>输入价格</td><td>$10 &#x2F; MTok</td><td>$5 &#x2F; MTok</td></tr><tr><td>输出价格</td><td>$50 &#x2F; MTok</td><td>$25 &#x2F; MTok</td></tr><tr><td>Tokenizer</td><td>与 Opus 4.8 相同</td><td>—</td></tr></tbody></table><p>价格是 Opus 4.8 的两倍。Tokenizer 没变，从 Opus 4.7&#x2F;4.8 迁移过来 token 数基本不变，只是单价不同。</p><h2 id="API-破坏性变更"><a href="#API-破坏性变更" class="headerlink" title="API 破坏性变更"></a>API 破坏性变更</h2><p>写代码接入时要注意这几点，都是会直接 400 的硬约束：</p><h3 id="1-Thinking-永远开启"><a href="#1-Thinking-永远开启" class="headerlink" title="1. Thinking 永远开启"></a>1. Thinking 永远开启</h3><p>不要传 <code>thinking</code> 参数（或只传 <code>{&quot;type&quot;: &quot;adaptive&quot;}</code>）。<code>{&quot;type&quot;: &quot;disabled&quot;}</code> 和 <code>budget_tokens</code> 都会返回 400。思考深度改用 <code>output_config.effort</code> 控制，档位从 <code>low</code> 到 <code>max</code>：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">client.messages.create(</span><br><span class="line">    model=<span class="string">&quot;claude-fable-5&quot;</span>,</span><br><span class="line">    max_tokens=<span class="number">16000</span>,</span><br><span class="line">    output_config=&#123;<span class="string">&quot;effort&quot;</span>: <span class="string">&quot;high&quot;</span>&#125;,  <span class="comment"># 不传 thinking 参数</span></span><br><span class="line">    messages=[...],</span><br><span class="line">)</span><br></pre></td></tr></table></figure><p>值得一提的是，Fable 5 的 <code>low</code> 档表现经常超过旧模型的 <code>max</code> 档，日常任务不用无脑拉满。</p><h3 id="2-原始思维链永不返回"><a href="#2-原始思维链永不返回" class="headerlink" title="2. 原始思维链永不返回"></a>2. 原始思维链永不返回</h3><p>默认 <code>display: &quot;omitted&quot;</code>，thinking 块的内容是空字符串。想看推理摘要要显式设置：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">thinking=&#123;<span class="string">&quot;type&quot;</span>: <span class="string">&quot;adaptive&quot;</span>, <span class="string">&quot;display&quot;</span>: <span class="string">&quot;summarized&quot;</span>&#125;</span><br></pre></td></tr></table></figure><h3 id="3-安全分类器与-refusal-fallback"><a href="#3-安全分类器与-refusal-fallback" class="headerlink" title="3. 安全分类器与 refusal fallback"></a>3. 安全分类器与 refusal fallback</h3><p>Fable 5 对生物研究和网络安全内容有前置安全分类器，被拒的请求返回 HTTP 200 + <code>stop_reason: &quot;refusal&quot;</code>。问题是<strong>良性的相邻工作也可能被误伤</strong>（比如安全工具开发、生命科学任务），所以官方建议默认开启 server-side fallbacks——被拒时同一个请求内自动降级到 Opus 4.8 重跑：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">response = client.beta.messages.create(</span><br><span class="line">    model=<span class="string">&quot;claude-fable-5&quot;</span>,</span><br><span class="line">    max_tokens=<span class="number">16000</span>,</span><br><span class="line">    betas=[<span class="string">&quot;server-side-fallback-2026-06-01&quot;</span>],</span><br><span class="line">    fallbacks=[&#123;<span class="string">&quot;model&quot;</span>: <span class="string">&quot;claude-opus-4-8&quot;</span>&#125;],</span><br><span class="line">    messages=[...],</span><br><span class="line">)</span><br></pre></td></tr></table></figure><h3 id="4-其他硬约束"><a href="#4-其他硬约束" class="headerlink" title="4. 其他硬约束"></a>4. 其他硬约束</h3><ul><li><strong>不支持 assistant prefill</strong>（和 4.6+ 系列一致），用 <code>output_config.format</code> 结构化输出替代</li><li><strong>Sampling 参数全部移除</strong>：<code>temperature</code> &#x2F; <code>top_p</code> &#x2F; <code>top_k</code> 都会 400</li><li><strong>要求 30 天数据保留</strong>：ZDR（零数据保留）组织的所有请求直接 400，排查时别只盯着请求体</li></ul><h2 id="Prompting-风格变化"><a href="#Prompting-风格变化" class="headerlink" title="Prompting 风格变化"></a>Prompting 风格变化</h2><p>这是迁移时最容易被忽略的一点：<strong>为旧模型写的 prompt 往往过于 prescriptive，反而会降低 Fable 5 的输出质量</strong>。</p><p>旧模型需要你把步骤掰开揉碎写清楚，Fable 5 更适合只描述目标和约束，把”怎么做”留给它自己规划。官方建议迁移后做一轮 A&#x2F;B 测试，把老的 step-by-step 脚手架删掉对比效果。</p><p>另外由于单轮任务可能跑很多分钟，工程上要提前规划好 timeout、streaming 和进度展示，把工作流设计成异步 check-in 的模式，而不是阻塞在一个请求里干等。</p><h2 id="要不要升级？"><a href="#要不要升级？" class="headerlink" title="要不要升级？"></a>要不要升级？</h2><p>我的判断框架：</p><ul><li><strong>值得上 Fable 5</strong>：长程自主任务、多 agent 编排、复杂重构、端到端交付物——这些是它独有的能力区间</li><li><strong>留在 Opus 4.8</strong>：常规编码、日常问答、成本敏感的高频调用——一半的价格，能力足够</li><li><strong>留在 Sonnet 4.6 &#x2F; Haiku 4.5</strong>：分类、摘要、大批量的简单任务</li></ul><p>对 Claude Code 用户来说，切到 Fable 5 最直观的感受是：单轮跑得更久但更完整，更愿意开 sub-agent 并行干活，最终总结更清晰。偶尔碰到 refusal 误伤时换个表述或临时降回 Opus 4.8 即可。</p>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/357aa7a7ce38/</id>
    <link href="https://blog.zisheng.pro/posts/357aa7a7ce38/"/>
    <published>2026-07-03T04:00:00.000Z</published>
    <summary>Anthropic 发布了目前能力最强的正式版模型 Claude Fable 5。这篇文章从能力升级、规格定价、API 破坏性变更和 Prompting 风格四个维度，梳理它相比 Opus 4.8 的所有大更新，帮你判断要不要升级、怎么迁移。</summary>
    <title>Claude Fable 5 发布：有哪些大更新？</title>
    <updated>2026-09-18T08:15:28.629Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="AI" scheme="https://blog.zisheng.pro/categories/AI/"/>
    <category term="AI" scheme="https://blog.zisheng.pro/tags/AI/"/>
    <category term="Agent" scheme="https://blog.zisheng.pro/tags/Agent/"/>
    <category term="学习路线" scheme="https://blog.zisheng.pro/tags/%E5%AD%A6%E4%B9%A0%E8%B7%AF%E7%BA%BF/"/>
    <category term="职业规划" scheme="https://blog.zisheng.pro/tags/%E8%81%8C%E4%B8%9A%E8%A7%84%E5%88%92/"/>
    <content>
      <![CDATA[<p>最近在整理 AI Agent 方向的知识体系时，我和 GPT 反复聊了几个来回，生成了一张非常系统的 <strong>Agentic System Engineer 专家能力架构图</strong>。这张图把从底层工程基础到顶层专家影响力，拆成了七个层次（L1-L7），同时辅以横向的工程素养和业务素养两条维度。</p><p>作为全栈开发者，我大部分时间在 JavaScript&#x2F;TypeScript 和 Node.js 生态里工作，正在往 AI Agent 方向拓展。这张图恰好给了我一个清晰的坐标系——知道自己现在在哪儿，接下来该往哪儿走。这篇文章既是学习笔记，也是一份面向全栈开发者的成长计划。</p><span id="more"></span><h2 id="能力架构总览"><a href="#能力架构总览" class="headerlink" title="能力架构总览"></a>能力架构总览</h2><p>这张图的核心路径是：<strong>LLM 理解 → Agent 核心 → Runtime &#x2F; Infra → Grounding &amp; Execution → Evolution → 战略影响力</strong>。七个层级层层递进，每个层级下面都有具体的能力点。</p><table><thead><tr><th>层级</th><th>名称</th><th>核心关键词</th></tr></thead><tbody><tr><td>L1</td><td>通用工程与系统基础</td><td>编程语言、算法、OS、网络、数据库、云原生</td></tr><tr><td>L2</td><td>AI &#x2F; LLM 能力基础</td><td>Transformer、Prompt Engineering、Function Calling、多模态、模型评估</td></tr><tr><td>L3</td><td>Agent 核心认知</td><td>Planning、Memory、Tool Use、State&#x2F;Context、长期任务、安全约束</td></tr><tr><td>L4</td><td>Runtime 与基础设施</td><td>Agent Runtime、工作流、调度、Sandbox、Checkpoint、可观测性</td></tr><tr><td>L5</td><td>环境交互与执行能力</td><td>Browser Agent、Vision&#x2F;DOM Grounding、Tool&#x2F;API 编排、RAG、多 Agent 协同、Human-in-the-loop</td></tr><tr><td>L6</td><td>自进化与优化系统</td><td>Eval 评测、Trajectory&#x2F;Trace、反馈闭环、Skill Library、Self-Evolve、RL</td></tr><tr><td>L7</td><td>专家影响力与战略</td><td>技术战略、系统架构、方法论、开源影响力、团队带教</td></tr></tbody></table><p>两个横向维度同样关键：</p><ul><li><strong>横向工程素养</strong>：可靠性、可维护性、性能、成本、安全</li><li><strong>横向业务素养</strong>：产品理解、领域建模、沟通表达、文档写作、业务落地</li></ul><p>下面结合我的实际情况，逐层拆解学习计划。</p><h2 id="L1：通用工程与系统基础（已具备，持续巩固）"><a href="#L1：通用工程与系统基础（已具备，持续巩固）" class="headerlink" title="L1：通用工程与系统基础（已具备，持续巩固）"></a>L1：通用工程与系统基础（已具备，持续巩固）</h2><p>作为全栈开发者，L1 的大部分能力已经覆盖。这里需要重点补充的是：<strong>面向 Agent 系统的工程思维</strong>。</p><ul><li><strong>编程语言</strong>：JavaScript&#x2F;TypeScript 是主力，正在强化 Python（FastAPI、AI Agent 生态），Java 也在学习中。Agent 系统开发中，Python 是 AI 侧的主力，JS&#x2F;TS 适合前端集成和工具链。</li><li><strong>数据结构与算法</strong>：基础已具备，需要补充的是<strong>图遍历、状态空间搜索</strong>等 Agent Planning 相关算法。</li><li><strong>操作系统与网络</strong>：已有基础，重点看<strong>进程隔离、容器化</strong>等与 Sandbox 安全执行相关的内容。</li><li><strong>数据库&#x2F;存储</strong>：关系型和非关系型都有使用经验，需要补充的是<strong>向量数据库</strong>（如 Milvus、Pinecone）和 Agent 记忆持久化方案。</li><li><strong>云原生&#x2F;分布式系统</strong>：Docker、K8s 有基础，需要深入的是<strong>Serverless（FaaS）<strong>和</strong>事件驱动架构</strong>，这对 Agent Runtime 的弹性调度很重要。</li></ul><p><strong>行动项</strong>：</p><ol><li>系统学习向量数据库原理与选型</li><li>深入 Docker 安全隔离机制，为后续 Sandbox 设计打基础</li><li>补充图算法（DFS&#x2F;BFS、A*）在 Agent 路径规划中的应用</li></ol><h2 id="L2：AI-LLM-能力基础（正在强化）"><a href="#L2：AI-LLM-能力基础（正在强化）" class="headerlink" title="L2：AI &#x2F; LLM 能力基础（正在强化）"></a>L2：AI &#x2F; LLM 能力基础（正在强化）</h2><p>这是从传统工程师转向 AI 工程师的关键一层。</p><ul><li><strong>Transformer &#x2F; 推理基础</strong>：理解 Attention 机制、KV Cache、Token 生成过程。不需要推导所有公式，但要理解<strong>模型为什么会产生幻觉、为什么长文本会丢失上下文</strong>。</li><li><strong>Prompt &#x2F; Context Engineering</strong>：已经大量使用，但要体系化。需要掌握 Chain-of-Thought、ReAct、Self-Consistency 等 Prompt 范式。</li><li><strong>Function Calling</strong>：这是 Agent 调用工具的基石。要深入理解不同模型（GPT、Claude、Llama）的 Function Calling 实现差异。</li><li><strong>多模态理解</strong>：Vision 能力正在快速发展，需要了解 GPT-4V、Claude 3 的视觉输入处理机制。</li><li><strong>模型评估</strong>：掌握 BLEU、ROUGE、BERTScore 等传统指标，更要关注<strong>Agent 专用评估方法</strong>（如成功率、任务完成率）。</li><li><strong>推理成本意识</strong>：Token 计费、上下文窗口成本、模型选型策略。生产环境中成本意识决定方案可行性。</li></ul><p><strong>行动项</strong>：</p><ol><li>精读《Attention Is All You Need》，理解 Transformer 核心机制</li><li>系统学习 Prompt Engineering 体系，整理一份 Prompt 模式库</li><li>动手实现一个支持 Function Calling 的 Agent 原型</li><li>对比 GPT-4、Claude 3.5、Llama 3 在相同任务上的成本与效果</li></ol><h2 id="L3：Agent-核心认知（重点突破层）"><a href="#L3：Agent-核心认知（重点突破层）" class="headerlink" title="L3：Agent 核心认知（重点突破层）"></a>L3：Agent 核心认知（重点突破层）</h2><p>这是 Agent 系统的”内核”。如果说 L2 是理解模型，L3 就是理解 Agent 的本质。</p><ul><li><strong>Planning</strong>：Agent 如何拆解任务、制定计划。关键概念包括 Task Decomposition（任务分解）、Hierarchical Planning（层次规划）、ReAct（推理+行动循环）。</li><li><strong>Memory</strong>：短期记忆（上下文窗口管理）、长期记忆（向量存储、知识图谱）。需要理解<strong>记忆的检索、压缩、遗忘机制</strong>。</li><li><strong>Tool Use</strong>：Agent 如何发现、选择、调用工具。MCP（Model Context Protocol）是新兴标准，值得关注。</li><li><strong>State &#x2F; Context 管理</strong>：Agent 的状态机设计、上下文窗口的滑动窗口策略、多轮对话的状态保持。</li><li><strong>长期任务执行</strong>：多步骤任务的持久化执行、错误恢复、断点续传。这是从 Demo 走向生产的关键。</li><li><strong>安全与约束</strong>：输入过滤、输出审查、权限控制、沙箱隔离。Agent 的自主性越高，安全风险越大。</li></ul><p><strong>行动项</strong>：</p><ol><li>用 Python 实现一个 ReAct Agent，包含 Planning + Tool Use + Memory</li><li>研究 LangChain、Mastra 的 Memory 实现，对比不同方案</li><li>学习 MCP 协议，实现一个自定义 MCP Server</li><li>设计一个带状态持久化的 Agent，支持任务中断和恢复</li></ol><h2 id="L4：Runtime-与基础设施（从原型到生产）"><a href="#L4：Runtime-与基础设施（从原型到生产）" class="headerlink" title="L4：Runtime 与基础设施（从原型到生产）"></a>L4：Runtime 与基础设施（从原型到生产）</h2><p>L3 关注的是单个 Agent 的认知能力，L4 关注的是<strong>如何让 Agent 系统在生产环境中稳定运行</strong>。</p><ul><li><strong>Agent Runtime</strong>：Agent 的执行引擎。需要了解 LangChain、Mastra、AutoGen 等框架的 Runtime 设计。</li><li><strong>工作流 &#x2F; 状态机</strong>：用状态机或 DAG 编排 Agent 的执行流程。如 LangGraph、Temporal 等。</li><li><strong>调度与队列</strong>：多 Agent 任务的排队调度、优先级管理、并发控制。</li><li><strong>Sandbox &#x2F; 隔离</strong>：代码执行的安全沙箱（如 E2B、Docker Sandbox）、文件系统隔离、网络隔离。</li><li><strong>Checkpoint &#x2F; Durable Execution</strong>：执行状态持久化，支持故障恢复。Temporal、Durable Objects 等技术。</li><li><strong>可观测性 &#x2F; 监控</strong>：Agent 的调用链路追踪、成本监控、效果评估指标收集。OpenTelemetry、LangSmith 等工具。</li></ul><p><strong>行动项</strong>：</p><ol><li>学习 LangGraph，用状态机方式重构 L3 的 ReAct Agent</li><li>研究 E2B 或 Docker 的代码执行沙箱方案，实现安全代码运行环境</li><li>集成 LangSmith 或 Helicone，建立 Agent 的监控和追踪体系</li><li>学习 Temporal 或类似框架，实现 Durable Execution</li></ol><h2 id="L5：环境交互与执行能力（从对话到行动）"><a href="#L5：环境交互与执行能力（从对话到行动）" class="headerlink" title="L5：环境交互与执行能力（从对话到行动）"></a>L5：环境交互与执行能力（从对话到行动）</h2><p>这是 Agent “落地”的关键层。Agent 不仅要会思考，还要能与真实世界交互。</p><ul><li><strong>Browser Agent</strong>：用 Agent 控制浏览器完成网页操作。browser-use、Playwright、Puppeteer 是核心工具。UI-TARS 是新兴的视觉驱动方案。</li><li><strong>Vision &#x2F; DOM Grounding</strong>：让 Agent 理解网页视觉内容和 DOM 结构。结合多模态模型的视觉能力。</li><li><strong>Tool &#x2F; API Orchestration</strong>：编排多个 API 调用，处理依赖关系、错误回退。MCP 协议在这里扮演重要角色。</li><li><strong>RAG &#x2F; 检索增强</strong>：结合向量检索和 LLM，让 Agent 访问外部知识。Sirchmunk（Embedding-Free RAG）是值得关注的新方向。</li><li><strong>多 Agent 协同</strong>：多个 Agent 分工协作，如经理 Agent 分配任务、专家 Agent 执行。AutoGen、CrewAI 等框架。</li><li><strong>Human-in-the-loop</strong>：关键决策点引入人工确认，平衡自动化和可控性。</li></ul><p><strong>行动项</strong>：</p><ol><li>用 browser-use 实现一个自动化的网页数据抓取 Agent</li><li>集成 RAG 能力，让 Agent 能查询文档知识库（尝试 Sirchmunk）</li><li>用 AutoGen 或 CrewAI 搭建一个多 Agent 协作系统</li><li>实现 Human-in-the-loop 机制，设计关键决策的人工确认流程</li></ol><h2 id="L6：自进化与优化系统（从静态到动态）"><a href="#L6：自进化与优化系统（从静态到动态）" class="headerlink" title="L6：自进化与优化系统（从静态到动态）"></a>L6：自进化与优化系统（从静态到动态）</h2><p>让 Agent 系统能够持续学习和改进，而不是每次都从零开始。</p><ul><li><strong>Eval 评测体系</strong>：建立 Agent 任务的评估数据集和指标。这是优化的前提。</li><li><strong>Trajectory &#x2F; Trace</strong>：记录 Agent 的完整执行轨迹，用于分析和调试。</li><li><strong>反馈闭环</strong>：将执行结果反馈给 Agent，形成学习循环。成功&#x2F;失败案例的积累。</li><li><strong>Skill Library</strong>：Agent 学习的技能库，可复用的工具组合和策略模板。</li><li><strong>Self-Evolve</strong>：Agent 自主改进自身 Prompt、工具选择策略。</li><li><strong>策略优化 &#x2F; RL</strong>：用强化学习优化 Agent 的决策策略。DPO、PPO 等算法在 Agent 优化中的应用。</li></ul><p><strong>行动项</strong>：</p><ol><li>为自己的 Agent 项目建立 Eval 数据集和评估流程</li><li>设计反馈收集机制，记录每次任务的执行轨迹和结果</li><li>实现 Skill Library，积累可复用的 Agent 技能模板</li><li>了解 DPO（Direct Preference Optimization）在 Agent 优化中的应用</li></ol><h2 id="L7：专家影响力与战略（长期目标）"><a href="#L7：专家影响力与战略（长期目标）" class="headerlink" title="L7：专家影响力与战略（长期目标）"></a>L7：专家影响力与战略（长期目标）</h2><p>这是从工程师到专家、从技术执行到技术领导的跨越。</p><ul><li><strong>技术战略</strong>：Agent 技术的演进方向判断、技术选型决策、团队技术路线规划。</li><li><strong>系统架构</strong>：设计大规模 Agent 系统的整体架构，考虑扩展性、可靠性、成本。</li><li><strong>方法论输出</strong>：沉淀 Agent 开发的工程方法论，形成可复用的框架和最佳实践。</li><li><strong>开源影响力</strong>：参与或主导开源 Agent 项目，建立技术影响力。FastGPT、Mastra 等。</li><li><strong>团队带教</strong>：培养团队成员的 Agent 开发能力，建立知识传承机制。</li><li><strong>跨部门协同</strong>：与产品、运营、业务团队协作，推动 Agent 技术在实际业务中落地。</li></ul><p><strong>行动项</strong>：</p><ol><li>在 GitHub 上持续贡献 Agent 相关开源项目</li><li>沉淀 Agent 开发方法论，整理成内部文档或开源项目</li><li>主动承担团队 Agent 方向的技术规划和选型工作</li><li>结合飞猪业务场景，探索 Agent 在旅游&#x2F;出行领域的落地机会</li></ol><h2 id="横向素养：贯穿始终的两条隐线"><a href="#横向素养：贯穿始终的两条隐线" class="headerlink" title="横向素养：贯穿始终的两条隐线"></a>横向素养：贯穿始终的两条隐线</h2><h3 id="横向工程素养"><a href="#横向工程素养" class="headerlink" title="横向工程素养"></a>横向工程素养</h3><p>Agent 系统对工程素养的要求比传统应用更高：</p><ul><li><strong>可靠性</strong>：Agent 的自主性意味着错误可能被放大，需要有完善的熔断、降级机制。</li><li><strong>可维护性</strong>：Agent 的 Prompt 和策略需要版本管理，配置变更需要可追踪。</li><li><strong>性能</strong>：LLM 调用是瓶颈，需要优化调用次数、使用缓存、并行化非依赖步骤。</li><li><strong>成本</strong>：Token 消耗是持续成本，需要在效果和成本间做权衡。</li><li><strong>安全</strong>：Agent 能执行代码、访问外部系统，安全风险成倍放大。</li></ul><h3 id="横向业务素养"><a href="#横向业务素养" class="headerlink" title="横向业务素养"></a>横向业务素养</h3><p>技术最终要服务于业务：</p><ul><li><strong>产品理解</strong>：理解 Agent 产品的用户场景和价值主张。</li><li><strong>领域建模</strong>：将业务领域知识结构化，供 Agent 理解和使用。</li><li><strong>沟通表达</strong>：Agent 系统的复杂性要求清晰的文档和沟通能力。</li><li><strong>文档写作</strong>：Prompt 工程本质上就是”面向模型的文档写作”。</li><li><strong>业务落地</strong>：从 POC 到生产环境，推动 Agent 在真实业务中产生价值。</li></ul><h2 id="个人学习路线与时间安排"><a href="#个人学习路线与时间安排" class="headerlink" title="个人学习路线与时间安排"></a>个人学习路线与时间安排</h2><p>基于以上分析，结合日常工作节奏，制定如下学习计划：</p><h3 id="第一阶段（1-3-个月）：夯实基础"><a href="#第一阶段（1-3-个月）：夯实基础" class="headerlink" title="第一阶段（1-3 个月）：夯实基础"></a>第一阶段（1-3 个月）：夯实基础</h3><p><strong>重点：L2 + L3</strong></p><ul><li>周 1-2：系统学习 Transformer 原理，精读 Attention Is All You Need</li><li>周 3-4：Prompt Engineering 体系化学习，整理 Prompt 模式库</li><li>周 5-6：Function Calling 实现，动手写 Agent 原型</li><li>周 7-8：Memory 系统设计，实现短期+长期记忆</li><li>周 9-10：ReAct &#x2F; Planning 模式深入</li><li>周 11-12：安全与约束机制设计</li></ul><p><strong>产出物</strong>：一个具备 Planning + Memory + Tool Use 的 Agent 原型</p><h3 id="第二阶段（4-6-个月）：走向生产"><a href="#第二阶段（4-6-个月）：走向生产" class="headerlink" title="第二阶段（4-6 个月）：走向生产"></a>第二阶段（4-6 个月）：走向生产</h3><p><strong>重点：L4 + L5</strong></p><ul><li>月 4：学习 LangGraph &#x2F; Mastra，用状态机重构 Agent</li><li>月 5：集成 Sandbox 安全执行环境</li><li>月 6：建立可观测性体系（追踪 + 监控 + 日志）</li><li>月 7：Browser Agent 实战（browser-use &#x2F; UI-TARS）</li><li>月 8：RAG 系统集成（向量检索 + Embedding-Free 方案）</li><li>月 9：多 Agent 协同系统搭建</li></ul><p><strong>产出物</strong>：一个具备完整 Runtime + 环境交互能力的生产级 Agent 系统</p><h3 id="第三阶段（7-9-个月）：持续进化"><a href="#第三阶段（7-9-个月）：持续进化" class="headerlink" title="第三阶段（7-9 个月）：持续进化"></a>第三阶段（7-9 个月）：持续进化</h3><p><strong>重点：L6 + 横向素养</strong></p><ul><li>月 10：建立 Eval 评测体系</li><li>月 11：实现反馈闭环和 Skill Library</li><li>月 12：成本优化与性能调优</li><li>持续：文档写作、方法论沉淀、开源贡献</li></ul><p><strong>产出物</strong>：可自进化的 Agent 系统 + 一套完整的方法论文档</p><h3 id="第四阶段（10-12-个月）：建立影响力"><a href="#第四阶段（10-12-个月）：建立影响力" class="headerlink" title="第四阶段（10-12 个月）：建立影响力"></a>第四阶段（10-12 个月）：建立影响力</h3><p><strong>重点：L7</strong></p><ul><li>主导团队 Agent 技术方向</li><li>开源项目持续贡献</li><li>结合业务场景推动 Agent 落地</li></ul><h2 id="学习资源推荐"><a href="#学习资源推荐" class="headerlink" title="学习资源推荐"></a>学习资源推荐</h2><h3 id="基础理论"><a href="#基础理论" class="headerlink" title="基础理论"></a>基础理论</h3><ul><li>《Attention Is All You Need》— Transformer 起源</li><li>《ReAct: Synergizing Reasoning and Acting in Language Models》— Agent 基础范式</li><li>《Reflexion: Self-Reflective Agents》— 自我反思 Agent</li><li>《Let’s Verify Step by Step》— 推理过程验证</li></ul><h3 id="框架与工具"><a href="#框架与工具" class="headerlink" title="框架与工具"></a>框架与工具</h3><ul><li><strong>LangChain &#x2F; LangGraph</strong>：Agent 开发主流框架</li><li><strong>Mastra</strong>：新兴的 TypeScript-first Agent 框架</li><li><strong>AgentScope</strong>：阿里巴巴开源的多 Agent 框架，支持多模态、多智能体协同，内置丰富的对话管理和工具编排能力</li><li><strong>browser-use</strong>：浏览器自动化 Agent</li><li><strong>MCP（Model Context Protocol）</strong>：工具调用标准协议</li><li><strong>E2B</strong>：代码执行沙箱</li><li><strong>Temporal</strong>：Durable Execution 框架</li></ul><h3 id="GitHub-课程与实战仓库"><a href="#GitHub-课程与实战仓库" class="headerlink" title="GitHub 课程与实战仓库"></a>GitHub 课程与实战仓库</h3><p>以下仓库按学习阶段分类，从入门到进阶，覆盖理论与实战：</p><h4 id="系统入门课程"><a href="#系统入门课程" class="headerlink" title="系统入门课程"></a>系统入门课程</h4><table><thead><tr><th>仓库</th><th>层级对应</th><th>简介</th></tr></thead><tbody><tr><td><a href="https://github.com/datawhalechina/hello-agents">datawhalechina&#x2F;hello-agents</a></td><td>L2-L5</td><td>Datawhale 出品的系统教程（6.3k+ Star），中文社区最完善的 Agent 入门课程。5 部分 16 章：Agent 基础 → 手写 Agent 框架（ReAct&#x2F;Plan-and-Solve）→ 低代码平台（Coze&#x2F;Dify&#x2F;n8n）→ 进阶主题（Memory&#x2F;RAG、MCP&#x2F;A2A&#x2F;ANP、Agentic-RL）→ 实战项目（智能出行助手、DeepResearch Agent、Cyber Town）。含自研 <code>hello_agents</code> 教学框架，从第一性原理构建 Agent</td></tr><tr><td><a href="https://github.com/microsoft/ai-agents-for-beginners">microsoft&#x2F;ai-agents-for-beginners</a></td><td>L2-L4</td><td>微软官方 12 课入门课程，含视频、文档和代码。覆盖 Tool Use、Agentic RAG、多 Agent 设计模式、生产部署、MCP&#x2F;A2A 协议。使用 Azure AI Foundry、Semantic Kernel、AutoGen</td></tr><tr><td><a href="https://github.com/GeneArnold/AI-Agent-Engineering-Course">GeneArnold&#x2F;AI-Agent-Engineering-Course</a></td><td>L2-L5</td><td>7 周渐进式课程，专为 Claude Code 辅助学习设计。从单 Agent → Memory Agent → 多工具 Agent → 多 Agent 系统 → 视觉识别 Agent，含 MCP 覆盖</td></tr><tr><td><a href="https://github.com/zkzkGamal/Agentic-AI-Tutorial">zkzkGamal&#x2F;Agentic-AI-Tutorial</a></td><td>L2-L4</td><td>5 章综合教程：LLM Provider 基础 → LangChain 编排 → RAG&#x2F;Memory 基础设施 → Agent Graph 模式（ReAct、Human-in-the-loop）→ LangGraph + MCP Runtime</td></tr><tr><td><a href="https://github.com/NirDiamant/GenAI_Agents">NirDiamant&#x2F;GenAI_Agents</a></td><td>L3-L5</td><td>45+ 个 Jupyter Notebook 项目，从基础到高级，每个项目含详细说明和可运行代码。覆盖 RAG、多 Agent、工具调用等场景</td></tr></tbody></table><h4 id="实战工作坊（项目驱动）"><a href="#实战工作坊（项目驱动）" class="headerlink" title="实战工作坊（项目驱动）"></a>实战工作坊（项目驱动）</h4><table><thead><tr><th>仓库</th><th>层级对应</th><th>简介</th></tr></thead><tbody><tr><td><a href="https://github.com/iusztinpaul/designing-real-world-ai-agents-workshop">iusztinpaul&#x2F;designing-real-world-ai-agents-workshop</a></td><td>L4-L5</td><td>生产级多 Agent 系统实战：构建 Deep Research Agent + LinkedIn 写作工作流，以 MCP Server 方式提供服务。含代码、PPT、视频和自学路径</td></tr><tr><td><a href="https://github.com/antonacio/intro-to-ai-agents">antonacio&#x2F;intro-to-ai-agents</a></td><td>L3-L4</td><td>手把手构建和编排 Agent 系统：Research Agent、RAG Agent、Web Search Agent、Math Agent，适合快速上手</td></tr><tr><td><a href="https://github.com/coleam00/ai-agents-masterclass">coleam00&#x2F;ai-agents-masterclass</a></td><td>L3-L5</td><td>YouTube 视频系列的配套代码，逐步构建实用 Agent，适合跟着视频边做边学</td></tr><tr><td><a href="https://github.com/ed-donner/agents">ed-donner&#x2F;agents</a></td><td>L2-L4</td><td>6 周 Agent 工程课程，从编码到部署的完整流程</td></tr></tbody></table><h4 id="从零实现（不依赖框架）"><a href="#从零实现（不依赖框架）" class="headerlink" title="从零实现（不依赖框架）"></a>从零实现（不依赖框架）</h4><table><thead><tr><th>仓库</th><th>层级对应</th><th>简介</th></tr></thead><tbody><tr><td><a href="https://github.com/pguso/ai-agents-from-scratch">pguso&#x2F;ai-agents-from-scratch</a></td><td>L2-L3</td><td>用 Node.js + 本地 LLM（node-llama-cpp）从零实现 Agent，理解底层原理。无需 API Key，完全本地运行。配套网站 <a href="https://agentsfromscratch.com/">agentsfromscratch.com</a></td></tr><tr><td><a href="https://github.com/proflead/how-to-build-ai-agents-from-scratch">proflead&#x2F;how-to-build-ai-agents-from-scratch</a></td><td>L3</td><td>基于 Google ADK 和 Gemini 2.5 Flash Lite，构建 Research → Summarizer → Coordinator 多 Agent 系统</td></tr><tr><td><a href="https://github.com/SRafi007/ai-tutorial-crew">SRafi007&#x2F;ai-tutorial-crew</a></td><td>L3-L4</td><td>CrewAI + Ollama 本地 LLM 实现多 Agent 协作：自动研究、写作、审阅 Python 教程。零 API 成本</td></tr></tbody></table><h4 id="Awesome-Lists（资源导航）"><a href="#Awesome-Lists（资源导航）" class="headerlink" title="Awesome Lists（资源导航）"></a>Awesome Lists（资源导航）</h4><table><thead><tr><th>仓库</th><th>用途</th></tr></thead><tbody><tr><td><a href="https://github.com/artnitolog/awesome-agent-learning">artnitolog&#x2F;awesome-agent-learning</a></td><td>Agent 学习资源汇总，含课程、论文、框架教程、评测基准</td></tr><tr><td><a href="https://github.com/e2b-dev/awesome-ai-agents">e2b-dev&#x2F;awesome-ai-agents</a></td><td>AI Agent 框架、库、研究论文的精选列表，区分开源和闭源方案</td></tr><tr><td><a href="https://github.com/punkpeye/awesome-mcp-servers">punkpeye&#x2F;awesome-mcp-servers</a></td><td>按类别整理的 MCP Server 集合（浏览器自动化、代码执行、云服务等）</td></tr><tr><td><a href="https://github.com/Shubhamsaboo/awesome-llm-apps">Shubhamsaboo&#x2F;awesome-llm-apps</a></td><td>LLM 应用集合，含 Agent、RAG、MCP 等实际项目</td></tr></tbody></table><h3 id="评估与优化"><a href="#评估与优化" class="headerlink" title="评估与优化"></a>评估与优化</h3><ul><li><strong>LangSmith</strong>：Agent 监控和评估平台</li><li><strong>Helicone</strong>：LLM 可观测性工具</li><li><strong>OpenAI Evals</strong>：模型评估框架</li></ul><h3 id="博主与频道推荐"><a href="#博主与频道推荐" class="headerlink" title="博主与频道推荐"></a>博主与频道推荐</h3><p>持续跟踪 Agent 领域的最新动态，除了文档和论文，关注一线从业者的分享同样重要。以下按方向分类推荐：</p><h4 id="X-Twitter"><a href="#X-Twitter" class="headerlink" title="X (Twitter)"></a>X (Twitter)</h4><table><thead><tr><th>博主</th><th>方向</th><th>简介</th></tr></thead><tbody><tr><td><a href="https://x.com/hwchase17">@hwchase17</a></td><td>Agent 框架</td><td>LangChain &#x2F; LangGraph 创始人，Agent 框架演进的权威声音</td></tr><tr><td><a href="https://x.com/karpathy">@karpathy</a></td><td>AI 基础</td><td>Andrej Karpathy，AI 教育内容标杆，讲解深入浅出</td></tr><tr><td><a href="https://x.com/swyx">@swyx</a></td><td>AI 工程化</td><td>Shawn Wang，Latent Space 播客主理人，AI Engineering 趋势追踪</td></tr><tr><td><a href="https://x.com/goodside">@goodside</a></td><td>Prompt Engineering</td><td>Riley Goodside，OpenAI Prompt Engineering 专家</td></tr><tr><td><a href="https://x.com/jxnlco">@jxnlco</a></td><td>LLM 工程化</td><td>Jason Liu，OpenAI 工程师，LLM 工程实践最佳</td></tr><tr><td><a href="https://x.com/gregpr07">@gregpr07</a></td><td>Browser Agent</td><td>browser-use 联合创始人，第一手更新和实现思路</td></tr><tr><td><a href="https://x.com/karpulix">@karpulix</a></td><td>多 Agent 协同</td><td>CrewAI 创始人，多 Agent 协作的实践者</td></tr><tr><td><a href="https://x.com/rlancemartin">@rlancemartin</a></td><td>RAG</td><td>LangChain RAG 负责人，RAG 技术演进第一手资料</td></tr><tr><td><a href="https://x.com/BeibinLi">@BeibinLi</a></td><td>多 Agent 框架</td><td>AgentScope（阿里开源）核心作者</td></tr><tr><td><a href="https://x.com/simonw">@simonw</a></td><td>工具集成</td><td>Simon Willison，AI 工程化和工具集成领域的多产作者</td></tr></tbody></table><h4 id="YouTube"><a href="#YouTube" class="headerlink" title="YouTube"></a>YouTube</h4><table><thead><tr><th>频道</th><th>方向</th><th>简介</th></tr></thead><tbody><tr><td><a href="https://www.youtube.com/@AndrejKarpathy">Andrej Karpathy</a></td><td>AI 基础</td><td>从 Transformer 到 GPT 的底层原理讲解，适合夯实 L2</td></tr><tr><td><a href="https://www.youtube.com/@Deeplearningai">DeepLearning.AI</a></td><td>系统课程</td><td>Andrew Ng 的短课程系列，Prompt Engineering、RAG、Agent 都有</td></tr><tr><td><a href="https://www.youtube.com/@SamWitteveen">Sam Witteveen</a></td><td>LangChain 实战</td><td>Google Developer Expert，大量 LangGraph 实战教程</td></tr><tr><td><a href="https://www.youtube.com/@jamesbriggs">James Briggs</a></td><td>RAG &#x2F; 向量检索</td><td>Pinecone 开发者，向量数据库和 RAG 最佳实践</td></tr><tr><td><a href="https://www.youtube.com/@mastra-ai">Mastra</a></td><td>TypeScript Agent</td><td>官方频道，TypeScript-first Agent 框架，技术栈匹配度高</td></tr><tr><td><a href="https://www.youtube.com/@LatentSpacePodcast">Latent Space</a></td><td>播客访谈</td><td>swyx 主理，每期访谈一线从业者，了解行业真实进展</td></tr><tr><td><a href="https://www.youtube.com/@aiexplained-official">AI Explained</a></td><td>行业动态</td><td>每日 AI 新闻速递，适合快速了解行业热点</td></tr></tbody></table><p><strong>关注策略建议</strong>：日常刷 X 追踪框架更新和行业热点（@hwchase17、@gregpr07、@swyx），周末看 YouTube 深度内容夯实基础（Andrej Karpathy、DeepLearning.AI），项目驱动时跟着实战教程边做边学（Sam Witteveen、James Briggs）。</p><h2 id="每日2小时学习规划"><a href="#每日2小时学习规划" class="headerlink" title="每日2小时学习规划"></a>每日2小时学习规划</h2><p>以上路线图覆盖 12 个月，但大多数在职开发者每天能抽出的时间有限。以下是一份基于<strong>每天 2 小时</strong>、以<strong>工作日为主</strong>的执行方案，核心原则是<strong>小步快跑、项目驱动、周末做整合</strong>。</p><h3 id="时间块分配"><a href="#时间块分配" class="headerlink" title="时间块分配"></a>时间块分配</h3><table><thead><tr><th>时段</th><th>时长</th><th>内容</th></tr></thead><tbody><tr><td><strong>理论学习</strong></td><td>40 min</td><td>读文档、课程章节或论文，理解概念并记关键笔记</td></tr><tr><td><strong>动手实践</strong></td><td>60 min</td><td>写代码、跑示例、调 Prompt，必须产出可运行的代码</td></tr><tr><td><strong>复盘整理</strong></td><td>20 min</td><td>写学习日志、整理代码片段、画思维导图，沉淀为可复用知识</td></tr></tbody></table><p>建议固定时间段（如早 7:00-9:00 或晚 21:00-23:00），降低启动成本。</p><h3 id="周末弹性安排"><a href="#周末弹性安排" class="headerlink" title="周末弹性安排"></a>周末弹性安排</h3><p>周末不强制加量，但建议完成两件事：</p><ol><li><strong>周项目冲刺</strong>（2-3 小时）：把本周零散实践整合成一个完整功能点</li><li><strong>补进度&#x2F;深度阅读</strong>：处理本周遗留，或精读一篇论文</li></ol><h3 id="第一阶段（1-3-月）：L2-L3，打地基"><a href="#第一阶段（1-3-月）：L2-L3，打地基" class="headerlink" title="第一阶段（1-3 月）：L2 + L3，打地基"></a>第一阶段（1-3 月）：L2 + L3，打地基</h3><p><strong>目标</strong>：产出一个具备 Planning + Memory + Tool Use 的 Agent 原型。</p><table><thead><tr><th>周次</th><th>理论学习（40 min）</th><th>动手实践（60 min）</th><th>关键产出</th></tr></thead><tbody><tr><td>1-2</td><td>跟读 <code>hello-agents</code> Part 1（Agent &amp; LLM 基础），配合 Andrej Karpathy 的 Transformer 视频</td><td>用 Python 复现简单的 Attention 计算；搭建本地开发环境（uv + Python 3.11）</td><td>环境就绪 + Attention 草稿</td></tr><tr><td>3-4</td><td>系统学习 Prompt Engineering，整理 Chain-of-Thought、ReAct、Self-Consistency 模板</td><td>给同一个任务写 3 种不同 Prompt，对比输出效果；记录到个人 Prompt 模式库</td><td>Prompt 模式库（10+ 模板）</td></tr><tr><td>5-6</td><td>精读 <code>hello-agents</code> Part 2（手写 Agent 框架），理解 ReAct 循环</td><td>实现一个最简单的 ReAct Agent：输入 → Thought → Action → Observation → 输出</td><td>ReAct Agent v0.1</td></tr><tr><td>7-8</td><td>学习 Memory 设计：短期记忆（上下文窗口）+ 长期记忆（向量存储）</td><td>给 Agent 加短期对话历史；接入 ChromaDB&#x2F;FAISS 做长期记忆检索</td><td>带记忆的 Agent v0.2</td></tr><tr><td>9-10</td><td>学习 Tool Use 和 MCP 协议；读 <code>hello-agents</code> 的 MCP&#x2F;A2A 章节</td><td>给 Agent 注册 2-3 个工具（计算器、搜索、文件读取）；尝试写一个简易 MCP Server</td><td>带工具调用的 Agent v0.3</td></tr><tr><td>11-12</td><td>学习安全约束：输入过滤、输出审查、权限控制</td><td>给 Agent 加输入校验、输出敏感词过滤、工具调用权限白名单</td><td>带安全约束的 Agent v0.4</td></tr></tbody></table><p><strong>阶段产出</strong>：一个可运行的 ReAct Agent（Python），支持 Memory + Tool Use + 基础安全。</p><h3 id="第二阶段（4-6-月）：L4-L5，走通生产"><a href="#第二阶段（4-6-月）：L4-L5，走通生产" class="headerlink" title="第二阶段（4-6 月）：L4 + L5，走通生产"></a>第二阶段（4-6 月）：L4 + L5，走通生产</h3><p><strong>目标</strong>：把原型升级为具备 Runtime + 环境交互能力的系统。</p><table><thead><tr><th>月次</th><th>理论学习（40 min）</th><th>动手实践（60 min）</th><th>关键产出</th></tr></thead><tbody><tr><td>4</td><td>学习 LangGraph 状态机；读 <code>microsoft/ai-agents-for-beginners</code> 的 Design Patterns</td><td>用 LangGraph 重写第一阶段的 Agent，用状态机管理执行流程</td><td>LangGraph 版 Agent</td></tr><tr><td>5</td><td>研究 E2B&#x2F;Docker Sandbox；学习 Temporal&#x2F;Durable Execution</td><td>给 Agent 加一个代码执行沙箱；实现任务断点续传</td><td>带 Sandbox 的 Agent</td></tr><tr><td>6</td><td>学习可观测性：OpenTelemetry、LangSmith&#x2F;Helicone</td><td>接入 LangSmith，记录每次调用的 Token 消耗、延迟、执行轨迹</td><td>可监控的 Agent 系统</td></tr><tr><td>7</td><td>学习 browser-use&#x2F;UI-TARS；读 <code>hello-agents</code> 的实战项目章节</td><td>用 browser-use 实现一个自动登录 + 抓数据的 Agent</td><td>Browser Agent</td></tr><tr><td>8</td><td>学习 RAG：向量检索 + Sirchmunk（Embedding-Free）</td><td>给 Agent 接入文档知识库，支持基于自有文档的问答</td><td>RAG-enabled Agent</td></tr><tr><td>9</td><td>学习多 Agent 协同：AutoGen&#x2F;CrewAI；读 <code>hello-agents</code> 的 Cyber Town</td><td>搭建 2-3 个 Agent 协作：Planner + Worker + Critic</td><td>多 Agent 协作系统</td></tr></tbody></table><p><strong>阶段产出</strong>：一个生产级 Agent 系统，具备状态机、沙箱、可观测性、Browser 能力、RAG、多 Agent 协同。</p><h3 id="第三阶段（7-9-月）：L6-横向素养，持续进化"><a href="#第三阶段（7-9-月）：L6-横向素养，持续进化" class="headerlink" title="第三阶段（7-9 月）：L6 + 横向素养，持续进化"></a>第三阶段（7-9 月）：L6 + 横向素养，持续进化</h3><p><strong>目标</strong>：让系统能自我改进，沉淀方法论。</p><table><thead><tr><th>月次</th><th>理论学习（40 min）</th><th>动手实践（60 min）</th><th>关键产出</th></tr></thead><tbody><tr><td>10</td><td>学习 Eval 体系：成功率、任务完成率、轨迹质量</td><td>为自己的 Agent 设计 10-20 个测试用例，建立自动化评估</td><td>Eval 数据集 + 评估脚本</td></tr><tr><td>11</td><td>学习反馈闭环和 Skill Library 设计</td><td>实现执行轨迹记录；把常用工具组合抽象成可复用的 Skill 模板</td><td>Skill Library（5+ 模板）</td></tr><tr><td>12</td><td>学习成本优化：缓存策略、模型降级、并行化</td><td>给系统加 Redis 缓存、非关键步骤用便宜模型、并行调用无依赖工具</td><td>成本优化后的系统</td></tr></tbody></table><p><strong>阶段产出</strong>：可自进化的 Agent 系统 + 一套内部方法论文档。</p><h3 id="第四阶段（10-12-月）：L7，建立影响力"><a href="#第四阶段（10-12-月）：L7，建立影响力" class="headerlink" title="第四阶段（10-12 月）：L7，建立影响力"></a>第四阶段（10-12 月）：L7，建立影响力</h3><p><strong>目标</strong>：从个人学习转向团队价值和开源贡献。</p><table><thead><tr><th>行动</th><th>时间投入</th><th>具体做法</th></tr></thead><tbody><tr><td>开源贡献</td><td>每天 60 min</td><td>给 <code>hello-agents</code>、<code>Mastra</code>、<code>browser-use</code> 等项目提 PR（文档改进、Bug 修复、小功能）</td></tr><tr><td>方法论沉淀</td><td>每天 40 min</td><td>把学习笔记整理成博客文章或内部技术分享</td></tr><tr><td>业务探索</td><td>周末 3-4 小时</td><td>结合飞猪业务，设计一个 Agent Demo（如智能行程规划、客服辅助）</td></tr></tbody></table><h3 id="关键执行原则"><a href="#关键执行原则" class="headerlink" title="关键执行原则"></a>关键执行原则</h3><ol><li><strong>先跑起来再完美</strong>：第一周就写出能运行的代码，不要花两周只读理论</li><li><strong>一课一代码</strong>：每学完一个概念（如 ReAct、Memory），当天就必须有代码产出</li><li><strong>用 Git 管理</strong>：从第一天就建一个 <code>agent-learning</code> 仓库，每周末 commit</li><li><strong>输出倒逼输入</strong>：每两周写一篇学习笔记（哪怕只是 README），强迫自己消化</li><li><strong>卡住先跳过</strong>：2 小时很宝贵，卡超过 30 分钟就记 TODO，先往下走</li></ol><h3 id="各阶段推荐资源组合"><a href="#各阶段推荐资源组合" class="headerlink" title="各阶段推荐资源组合"></a>各阶段推荐资源组合</h3><table><thead><tr><th>阶段</th><th>主力课程</th><th>辅助资源</th><th>备用</th></tr></thead><tbody><tr><td>第一阶段</td><td><code>hello-agents</code> Part 1-2</td><td>Andrej Karpathy Transformer 视频</td><td><code>microsoft/ai-agents-for-beginners</code> 前 5 课</td></tr><tr><td>第二阶段</td><td><code>microsoft/ai-agents-for-beginners</code> 后 7 课 + LangGraph 官方文档</td><td><code>iusztinpaul/designing-real-world-ai-agents-workshop</code></td><td><code>zkzkGamal/Agentic-AI-Tutorial</code></td></tr><tr><td>第三阶段</td><td><code>hello-agents</code> Part 3-4</td><td>LangSmith 官方文档</td><td>OpenAI Evals 源码</td></tr><tr><td>第四阶段</td><td>开源项目源码阅读</td><td>Latent Space 播客</td><td>X 上追踪 @hwchase17、@gregpr07</td></tr></tbody></table><p>按这个节奏，12 个月后你会有一条清晰的成长轨迹：<strong>从”能写 Agent”到”能设计 Agent 系统”再到”能带团队做 Agent 落地”</strong>。每天 2 小时，关键是<strong>不中断</strong>。</p><h2 id="写在最后"><a href="#写在最后" class="headerlink" title="写在最后"></a>写在最后</h2><p>成为 Agentic System Engineer 不是一蹴而就的，需要从工程基础到 AI 认知，再到系统架构，层层递进。这张图的价值在于它提供了一个完整的地图——你不需要一开始就掌握所有内容，但你需要知道每个能力点的位置和重要性。</p><p>作为全栈开发者，最大的优势是工程基础扎实、理解系统全貌。最大的挑战是从”确定性编程”转向”概率性系统”的思维转变。Agent 系统的行为不是完全可控的，需要在不确定性中做工程化。</p><p>这条路上没有捷径，但有地图。这张图就是我的地图。接下来，就是一步步走。</p>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/7e7297c52e91/</id>
    <link href="https://blog.zisheng.pro/posts/7e7297c52e91/"/>
    <published>2026-06-08T14:00:00.000Z</published>
    <summary>从这张能力架构图出发，结合全栈开发背景，规划一条系统化的 Agent 工程师成长路径。</summary>
    <title>Agentic System Engineer 成长路线图与学习指南</title>
    <updated>2026-09-18T08:15:28.629Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="云原生" scheme="https://blog.zisheng.pro/categories/%E4%BA%91%E5%8E%9F%E7%94%9F/"/>
    <category term="Docker" scheme="https://blog.zisheng.pro/tags/Docker/"/>
    <category term="容器" scheme="https://blog.zisheng.pro/tags/%E5%AE%B9%E5%99%A8/"/>
    <category term="阿里云" scheme="https://blog.zisheng.pro/tags/%E9%98%BF%E9%87%8C%E4%BA%91/"/>
    <category term="Kubernetes" scheme="https://blog.zisheng.pro/tags/Kubernetes/"/>
    <category term="PouchContainer" scheme="https://blog.zisheng.pro/tags/PouchContainer/"/>
    <content>
      <![CDATA[<p>如果你在阿里云或阿里内部系统中看到过”Pouch”这个词，可能会好奇它和 Docker 是什么关系。本文从历史、架构、核心特性三个维度，系统地介绍阿里巴巴开源的企业级容器运行时 PouchContainer。</p><h2 id="背景：从-T4-到-PouchContainer"><a href="#背景：从-T4-到-PouchContainer" class="headerlink" title="背景：从 T4 到 PouchContainer"></a>背景：从 T4 到 PouchContainer</h2><p>2011 年，阿里巴巴内部开始研发一套基于 LXC 的容器系统，代号 <strong>T4</strong>，用于支撑淘宝的海量在线交易。这比 Docker 公开发布（2013 年）还早了两年。</p><p>随着 Docker 的兴起和 OCI（Open Container Initiative）规范的确立，阿里将 Docker 的镜像格式和生态整合进 T4，逐步演化为兼容 OCI 规范的 <strong>PouchContainer</strong>，并于 2017 年 11 月正式对外开源（Apache 2.0）。</p><blockquote><p>GitHub 地址：<a href="https://github.com/AliyunContainerService/pouch">AliyunContainerService&#x2F;pouch</a></p></blockquote><p>Pouch 的设计目标不是”再做一个 Docker”，而是在<strong>阿里超大规模数据中心</strong>（几十万台机器、数亿容器实例）场景下，解决 Docker 难以满足的企业级痛点。</p><h2 id="与-Docker-的核心差异"><a href="#与-Docker-的核心差异" class="headerlink" title="与 Docker 的核心差异"></a>与 Docker 的核心差异</h2><table><thead><tr><th>维度</th><th>Docker</th><th>PouchContainer</th></tr></thead><tbody><tr><td>容器模型</td><td>单进程原则</td><td>支持多进程（Rich Container）</td></tr><tr><td>隔离手段</td><td>仅 cgroups + namespace</td><td>kernel + hypervisor 双轨</td></tr><tr><td>&#x2F;proc 视图</td><td>与宿主共享</td><td>通过 lxcfs 隔离</td></tr><tr><td>镜像分发</td><td>Registry 直拉</td><td>集成 Dragonfly P2P 分发</td></tr><tr><td>CRI 集成</td><td>需要 containerd-shim</td><td>CRI Manager 内嵌 Daemon</td></tr><tr><td>内核要求</td><td>较新内核</td><td>兼容低至 Linux 2.6.32</td></tr></tbody></table><h2 id="四大核心特性"><a href="#四大核心特性" class="headerlink" title="四大核心特性"></a>四大核心特性</h2><h3 id="1-Rich-Container（富容器模式）"><a href="#1-Rich-Container（富容器模式）" class="headerlink" title="1. Rich Container（富容器模式）"></a>1. Rich Container（富容器模式）</h3><p>Docker 强调”一个容器只跑一个进程”，这在云原生新应用上没问题。但阿里大量存量业务跑在传统 Linux 环境里，需要 <code>sshd</code>、<code>cron</code>、<code>supervisord</code> 等系统服务同时运行在同一个容器内，这就是 <strong>Rich Container</strong> 要解决的问题。</p><p>Rich Container 允许容器内运行完整的 init 系统（如 systemd），让传统应用无需改造就能容器化，极大降低了存量业务的迁移成本。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 以 Rich Container 模式启动容器（带 init 进程）</span></span><br><span class="line">pouch run -d --rich --initscript /tmp/initscript.sh ubuntu:16.04</span><br></pre></td></tr></table></figure><h3 id="2-LXCFS-提供更强的-proc-隔离"><a href="#2-LXCFS-提供更强的-proc-隔离" class="headerlink" title="2. LXCFS 提供更强的 &#x2F;proc 隔离"></a>2. LXCFS 提供更强的 &#x2F;proc 隔离</h3><p>Docker 容器在默认情况下，<code>cat /proc/meminfo</code> 显示的是<strong>宿主机</strong>的内存信息，而不是容器被分配的资源。这在多租户场景下会引发严重误判——Java 应用按宿主机内存大小设置 JVM 堆，实际分配却远低于此，直接 OOM。</p><p>PouchContainer 集成了 <strong>lxcfs</strong>（Linux Container Filesystem），通过 FUSE 在用户态提供虚拟 <code>/proc</code> 文件系统，让容器内的进程看到的 <code>meminfo</code>、<code>cpuinfo</code>、<code>uptime</code> 等都是<strong>容器视图</strong>，彻底解决 &#x2F;proc 隔离问题。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"># 容器内看到的是分配给它的 2G 内存，而不是宿主机的 256G</span><br><span class="line">cat /proc/meminfo</span><br><span class="line">MemTotal:        2097152 kB</span><br><span class="line">MemFree:         1048576 kB</span><br></pre></td></tr></table></figure><h3 id="3-P2P-镜像分发（Dragonfly）"><a href="#3-P2P-镜像分发（Dragonfly）" class="headerlink" title="3. P2P 镜像分发（Dragonfly）"></a>3. P2P 镜像分发（Dragonfly）</h3><p>在阿里双 11 场景下，需要在几分钟内向几十万台机器推送同一个容器镜像。如果走传统的 Registry 直拉模式，Registry 带宽会瞬间被打爆。</p><p>PouchContainer 内置集成了阿里开源的 <strong>Dragonfly</strong>（蜻蜓），一个基于 P2P 协议的智能文件分发系统：</p><ul><li>首台机器从 Registry 拉取后，后续机器从已有节点 P2P 获取</li><li>带宽消耗降低 60% 以上</li><li>支持秒级扩容数万容器</li></ul><p>Dragonfly 已捐给 CNCF，成为独立项目：<a href="https://d7y.io/">d7y.io</a></p><h3 id="4-Hypervisor-级隔离（双轨隔离）"><a href="#4-Hypervisor-级隔离（双轨隔离）" class="headerlink" title="4. Hypervisor 级隔离（双轨隔离）"></a>4. Hypervisor 级隔离（双轨隔离）</h3><p>普通容器（runc）本质上还是共享内核，对安全敏感场景（金融、多租户）有隐患。PouchContainer 支持接入 <strong>hypervisor 级容器运行时</strong>（如 runV &#x2F; Kata Containers），用轻量级 VM 提供更强隔离，同时保留容器的快速启动特性。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">内核级容器（runc）  → 启动快，隔离较弱</span><br><span class="line">VM 级容器（runV）  → 启动稍慢，隔离强，内核独立</span><br></pre></td></tr></table></figure><p>用户可以按业务场景选择不同的 runtime，同一套 Pouch Daemon 统一管理。</p><h2 id="架构概览"><a href="#架构概览" class="headerlink" title="架构概览"></a>架构概览</h2><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line">┌─────────────────────────────────────┐</span><br><span class="line">│            Kubernetes               │</span><br><span class="line">└──────────────┬──────────────────────┘</span><br><span class="line">               │ CRI (gRPC)</span><br><span class="line">┌──────────────▼──────────────────────┐</span><br><span class="line">│             Pouchd                  │</span><br><span class="line">│  ┌──────────┐  ┌──────────────────┐ │</span><br><span class="line">│  │CRI Manager│  │  Image Manager   │ │</span><br><span class="line">│  └──────────┘  └──────────────────┘ │</span><br><span class="line">│  ┌──────────┐  ┌──────────────────┐ │</span><br><span class="line">│  │Container  │  │   CNI Manager    │ │</span><br><span class="line">│  │ Manager  │  └──────────────────┘ │</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">   │  runc   │  runV / kata</span><br><span class="line">   └─────────┘</span><br></pre></td></tr></table></figure><p>与 Docker 需要 <code>dockerd → containerd → containerd-shim → runc</code> 多层链路不同，Pouchd 将 CRI Manager 内嵌，减少了进程层级和 IPC 开销。</p><h2 id="Kubernetes-集成"><a href="#Kubernetes-集成" class="headerlink" title="Kubernetes 集成"></a>Kubernetes 集成</h2><p>PouchContainer 原生实现了 Kubernetes CRI（Container Runtime Interface），启动时加上 <code>--enable-cri=true</code> 即可作为 kubelet 的容器运行时：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># kubelet 配置</span></span><br><span class="line"><span class="string">--container-runtime=remote</span></span><br><span class="line"><span class="string">--container-runtime-endpoint=unix:///var/run/pouchcri.sock</span></span><br></pre></td></tr></table></figure><p>这一点早于 Docker 通过 cri-dockerd 桥接的方案，架构上更简洁。</p><h2 id="现状与影响"><a href="#现状与影响" class="headerlink" title="现状与影响"></a>现状与影响</h2><p>PouchContainer 目前已是阿里巴巴内部最核心的容器基础设施之一，支撑了：</p><ul><li>淘宝、天猫、支付宝的核心交易链路</li><li>双 11 期间数十万节点的弹性扩缩容</li><li>阿里云 ECI（弹性容器实例）的底层支撑</li></ul><p>作为开源项目，Pouch 的 GitHub 活跃度近年有所下降（阿里内部逐步向 containerd&#x2F;Kata 生态收敛），但它在 2017-2020 年间对推动国内企业级容器化进程有重要贡献，也促成了 CNCF 多个项目（Dragonfly、Sealer）的诞生。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>PouchContainer 的价值不在于”又一个容器运行时”，而在于它系统性地解决了超大规模、存量业务迁移、多租户安全隔离三类企业痛点：</p><ul><li><strong>Rich Container</strong> → 存量业务低成本容器化</li><li><strong>LXCFS</strong> → &#x2F;proc 视图正确隔离，Java 应用友好</li><li><strong>Dragonfly P2P</strong> → 大规模镜像分发不打爆 Registry</li><li><strong>双轨隔离</strong> → 按安全需求灵活选择 runc 或 VM</li></ul><p>如果你在阿里云上做容器化，了解 Pouch 的设计思路能帮你更好地理解底层基础设施的行为；如果你在设计私有云容器平台，这些特性也值得借鉴。</p>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/1aca4422b4f2/</id>
    <link href="https://blog.zisheng.pro/posts/1aca4422b4f2/"/>
    <published>2026-05-11T02:00:00.000Z</published>
    <summary>深入了解阿里巴巴开源的企业级容器运行时 PouchContainer：从 T4 的历史沿革，到 Rich Container、LXCFS、P2P 镜像分发等核心特性，再到与 Docker 和 Kubernetes 的关系。</summary>
    <title>阿里云 PouchContainer 容器技术详解</title>
    <updated>2026-09-18T08:15:28.640Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="建站教程" scheme="https://blog.zisheng.pro/categories/%E5%BB%BA%E7%AB%99%E6%95%99%E7%A8%8B/"/>
    <category term="服务器运维" scheme="https://blog.zisheng.pro/categories/%E5%BB%BA%E7%AB%99%E6%95%99%E7%A8%8B/%E6%9C%8D%E5%8A%A1%E5%99%A8%E8%BF%90%E7%BB%B4/"/>
    <category term="LAMP" scheme="https://blog.zisheng.pro/tags/LAMP/"/>
    <category term="LNMP" scheme="https://blog.zisheng.pro/tags/LNMP/"/>
    <category term="宝塔面板" scheme="https://blog.zisheng.pro/tags/%E5%AE%9D%E5%A1%94%E9%9D%A2%E6%9D%BF/"/>
    <category term="WordPress" scheme="https://blog.zisheng.pro/tags/WordPress/"/>
    <category term="建站" scheme="https://blog.zisheng.pro/tags/%E5%BB%BA%E7%AB%99/"/>
    <category term="网站搭建" scheme="https://blog.zisheng.pro/tags/%E7%BD%91%E7%AB%99%E6%90%AD%E5%BB%BA/"/>
    <category term="服务器管理" scheme="https://blog.zisheng.pro/tags/%E6%9C%8D%E5%8A%A1%E5%99%A8%E7%AE%A1%E7%90%86/"/>
    <category term="域名绑定" scheme="https://blog.zisheng.pro/tags/%E5%9F%9F%E5%90%8D%E7%BB%91%E5%AE%9A/"/>
    <content>
      <![CDATA[<h2 id="环境准备"><a href="#环境准备" class="headerlink" title="环境准备"></a>环境准备</h2><p>确保安装了 Nginx、MySQL、PHP 环境。</p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1753541490034.png"></p><h2 id="新建站点"><a href="#新建站点" class="headerlink" title="新建站点"></a>新建站点</h2><p>进入宝塔面板，点击左侧的“网站”，然后点击“添加站点”。</p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1753542436855.png"></p><p>点击 “一键部署”，选择 WordPress 模版，输入域名。建议 www 和非 www 都部署。</p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1753542516497.png"></p><h2 id="SSL-证书"><a href="#SSL-证书" class="headerlink" title="SSL 证书"></a>SSL 证书</h2><p>网站部署完成后，点击 “SSL”，选择 “Let’s Encrypt”，点击 “提交”。</p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1753542750792.png"></p><p>记得勾选 “强制 HTTPS” 和到期提醒。</p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1753542996550.png"></p><h2 id="伪静态"><a href="#伪静态" class="headerlink" title="伪静态"></a>伪静态</h2><p>选择 wordpress 模版，点击 “提交”。</p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1753543586057.png"></p><h2 id="301-重定向"><a href="#301-重定向" class="headerlink" title="301 重定向"></a>301 重定向</h2><p>重定向设置，选择 “301 重定向”，点击 “提交”。</p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1753543929126.png"></p><h2 id="wordpress-初始化"><a href="#wordpress-初始化" class="headerlink" title="wordpress 初始化"></a>wordpress 初始化</h2><p>数据库名选择新建网站时的：</p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1753545846823.png"></p><h2 id="wordpress-设置"><a href="#wordpress-设置" class="headerlink" title="wordpress 设置"></a>wordpress 设置</h2><p>permalink 设置为 <code>/%category%/%postname%/</code>（电商、新闻门户） 或者 <code>/%postname%/</code>（博客、企业站）：</p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1753603318808.png"></p><h2 id="wordpress-主题"><a href="#wordpress-主题" class="headerlink" title="wordpress 主题"></a>wordpress 主题</h2><ul><li><a href="https://www.xhtheme.com/themes/meteor-theme">Meteor</a>：一款专为自媒体人、博主和内容创作者精心打造的WordPress主题</li></ul>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/a9b37883b6d3/</id>
    <link href="https://blog.zisheng.pro/posts/a9b37883b6d3/"/>
    <published>2025-07-26T10:07:05.000Z</published>
    <summary>详细介绍如何使用宝塔面板快速安装 WordPress，包括环境配置、数据库创建、域名绑定等完整建站流程。适合新手站长的宝塔 WordPress 搭建指南。</summary>
    <title>宝塔面板安装 WordPress 详细教程：从零开始搭建网站</title>
    <updated>2026-09-18T08:15:28.640Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="Ant Design Pro" scheme="https://blog.zisheng.pro/tags/Ant-Design-Pro/"/>
    <category term="中后台" scheme="https://blog.zisheng.pro/tags/%E4%B8%AD%E5%90%8E%E5%8F%B0/"/>
    <category term="React" scheme="https://blog.zisheng.pro/tags/React/"/>
    <category term="Umi" scheme="https://blog.zisheng.pro/tags/Umi/"/>
    <category term="实战记录" scheme="https://blog.zisheng.pro/tags/%E5%AE%9E%E6%88%98%E8%AE%B0%E5%BD%95/"/>
    <content>
      <![CDATA[<h2 id="简介"><a href="#简介" class="headerlink" title="简介"></a>简介</h2><p>Ant Design Pro 是基于 Ant Design 和 umi 的封装的一整套企业级中后台前端&#x2F;设计解决方案，致力于在设计规范和基础组件的基础上，继续向上构建，提炼出典型模板&#x2F;业务组件&#x2F;配套设计资源，进一步提升企业级中后台产品设计研发过程中的『用户』和『设计者』的体验。随着『设计者』的不断反馈。</p><h2 id="初始化"><a href="#初始化" class="headerlink" title="初始化"></a>初始化</h2><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">npm i @ant-design/pro-cli -g</span><br><span class="line">pro create myapp</span><br></pre></td></tr></table></figure><p>然后选择脚手架，simple 是基础模板，只提供了框架运行的基本内容，complete 包含所有区块，不太适合当基础模板来进行二次开发</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">? 🚀 要全量的还是一个简单的脚手架? (Use arrow keys)</span><br><span class="line">❯ simple</span><br><span class="line">  complete</span><br></pre></td></tr></table></figure><blockquote><p>我这里选择是 complete 脚手架，方便全量的最佳实践</p></blockquote><p>安装依赖：</p><p>脚手架初始化成功之后就可以开始进行开发了，Ant Design Pro 提供了一些命令来辅助开发。</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">cd</span> myapp &amp;&amp; pnpm install</span><br></pre></td></tr></table></figure><h2 id="运行"><a href="#运行" class="headerlink" title="运行"></a>运行</h2><p>complete 脚手架运行会报错：<a href="https://github.com/ant-design/ant-design-pro/issues/11189">🐛 [BUG] 路由错误 #11189</a>，需要修改 <code>routes.ts</code> 文件，注释掉 <code>/user</code> 路由下的 404 路由（全局有一个 404 可以兜底）：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">path</span>: <span class="string">&#x27;/user&#x27;</span>,</span><br><span class="line">  <span class="attr">layout</span>: <span class="literal">false</span>,</span><br><span class="line">  <span class="attr">routes</span>: [</span><br><span class="line">    <span class="comment">// &#123;</span></span><br><span class="line">    <span class="comment">//   component: &#x27;404&#x27;,</span></span><br><span class="line">    <span class="comment">//   path: &#x27;/*&#x27;,</span></span><br><span class="line">    <span class="comment">// &#125;,</span></span><br><span class="line">  ],</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>然后执行 <code>yarn start</code> 即可启动项目。</p><h3 id="修改调试-MOCK"><a href="#修改调试-MOCK" class="headerlink" title="修改调试 MOCK"></a>修改调试 MOCK</h3><p><code>app.tsx</code> 文件中的 <code>baseURL</code> 注释掉，或者根据 <code>isDev</code> 判断：</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@name</span> request 配置，可以配置错误处理</span></span><br><span class="line"><span class="comment"> * 它基于 axios 和 ahooks 的 useRequest 提供了一套统一的网络请求和错误处理方案。</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@doc</span> https://umijs.org/docs/max/request#配置</span></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="attr">request</span>: <span class="title class_">RequestConfig</span> = &#123;</span><br><span class="line">  <span class="attr">baseURL</span>: isDev ? <span class="string">&#x27;&#x27;</span> : <span class="string">&#x27;https://proapi.azurewebsites.net&#x27;</span>,</span><br><span class="line">  ...errorConfig,</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><ul><li><a href="https://github.com/ant-design/ant-design-pro/issues/11446">🧐[问题 | question] 如何修改调试Mock</a></li></ul><h2 id="登录"><a href="#登录" class="headerlink" title="登录"></a>登录</h2><p>在 Umi Max 框架中，登录后的 ​Token 存储方案​ 通常结合全局状态管理与浏览器本地存储机制实现。以下是具体实现方式及安全权衡：</p><p>1、在运行时定义 <code>getInitialState</code> 方法，用于获取初始状态：</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> <span class="title function_">getInitialState</span> = <span class="keyword">async</span> (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> token = <span class="variable language_">localStorage</span>.<span class="title function_">getItem</span>(<span class="string">&#x27;token&#x27;</span>);</span><br><span class="line">  <span class="keyword">if</span> (token) &#123;</span><br><span class="line">    <span class="keyword">const</span> userInfo = <span class="keyword">await</span> <span class="title function_">fetchUserInfo</span>();</span><br><span class="line">    <span class="keyword">return</span> &#123;</span><br><span class="line">      <span class="attr">currentUser</span>: &#123;...userInfo, token &#125; <span class="keyword">as</span> <span class="variable constant_">API</span>.<span class="property">CurrentUser</span>,</span><br><span class="line">    &#125;;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">return</span> &#123;&#125;;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>2、登录成功后，获取 Token 并存储到浏览器的 Local Storage 中，并使用 <code>setInitialState</code> 方法把 token 更新全局状态：</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; useModel &#125; <span class="keyword">from</span> <span class="string">&#x27;@umijs/max&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; flushSync &#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">const</span> <span class="title function_">Login</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> &#123; initialState &#125; = <span class="title function_">useModel</span>(<span class="string">&#x27;@@initialState&#x27;</span>);</span><br><span class="line">  <span class="keyword">const</span> <span class="title function_">fetchUserInfo</span> = <span class="keyword">async</span> (<span class="params">token: <span class="built_in">string</span></span>) =&gt; &#123;</span><br><span class="line">    <span class="keyword">const</span> userInfo = <span class="keyword">await</span> initialState?.<span class="property">fetchUserInfo</span>?.();</span><br><span class="line">    <span class="variable language_">localStorage</span>.<span class="title function_">setItem</span>(<span class="string">&#x27;token&#x27;</span>, token);</span><br><span class="line">    <span class="keyword">if</span> (userInfo) &#123;</span><br><span class="line">      <span class="title function_">flushSync</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">        <span class="title function_">setInitialState</span>(<span class="function">(<span class="params">s</span>) =&gt;</span> (&#123;</span><br><span class="line">          ...s,</span><br><span class="line">          <span class="attr">currentUser</span>: &#123; ...userInfo, token &#125; <span class="keyword">as</span> <span class="variable constant_">API</span>.<span class="property">CurrentUser</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"></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="keyword">const</span> token = <span class="keyword">await</span> <span class="title function_">login</span>(&#123; <span class="attr">account</span>: <span class="string">&#x27;admin&#x27;</span>, <span class="attr">password</span>: <span class="string">&#x27;123456&#x27;</span> &#125;);</span><br><span class="line">    <span class="keyword">if</span> (token) &#123;</span><br><span class="line">      message.<span class="title function_">success</span>(<span class="string">&#x27;登录成功！&#x27;</span>);</span><br><span class="line">      <span class="keyword">await</span> <span class="title function_">fetchUserInfo</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="keyword">export</span> <span class="keyword">default</span> <span class="title class_">Login</span>;</span><br></pre></td></tr></table></figure><h2 id="部署"><a href="#部署" class="headerlink" title="部署"></a>部署</h2><h3 id="netlify"><a href="#netlify" class="headerlink" title="netlify"></a>netlify</h3><p>如果不使用 github pages，需要把模版中 <code>gh-pages</code> 删掉：</p><ul><li>删除 <code>public</code> 目录下的 <code>CNAME</code> 文件</li><li>删除 <code>gh-pages</code> 依赖和 npm script 脚本</li></ul><p>然后在 <code>public</code> 目录下添加 <code>_redirects</code> 文件，内容：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">&lt;!-- https://gist.github.com/dance2die/acf9a148ec8025364bf0b6cf37367294 --&gt;</span><br><span class="line">/*    /index.html   200</span><br></pre></td></tr></table></figure><ul><li><a href="https://github.com/ant-design/ant-design-pro/issues/7227">🐛[BUG] Page not found when reloading Netlify</a></li><li><a href="https://www.sung.codes/blog/2018/page-not-found-on-netlify-with-react-router">Page Not Found on Netlify with React Router</a></li></ul>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/b800604eca6e/</id>
    <link href="https://blog.zisheng.pro/posts/b800604eca6e/"/>
    <published>2025-05-17T08:04:15.000Z</published>
    <summary>本文记录了使用 Ant Design Pro 开发中后台的一些经验和问题</summary>
    <title>Ant Design Pro 实战记录</title>
    <updated>2026-09-18T08:15:28.629Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="AI" scheme="https://blog.zisheng.pro/categories/AI/"/>
    <category term="Agent" scheme="https://blog.zisheng.pro/tags/Agent/"/>
    <category term="MCP" scheme="https://blog.zisheng.pro/tags/MCP/"/>
    <category term="TARS" scheme="https://blog.zisheng.pro/tags/TARS/"/>
    <category term="字节跳动" scheme="https://blog.zisheng.pro/tags/%E5%AD%97%E8%8A%82%E8%B7%B3%E5%8A%A8/"/>
    <category term="Electron" scheme="https://blog.zisheng.pro/tags/Electron/"/>
    <content>
      <![CDATA[<h2 id="依赖"><a href="#依赖" class="headerlink" title="依赖"></a>依赖</h2><ul><li><code>@tavily/core</code>：Tavily 的 JavaScript SDK 允许与 Tavily API 轻松交互，直接在 JavaScript 和 TypeScript 程序中使用 Tavily 的全部搜索和提取功能。利用强大的 Tavily Search 和 Tavily Extract API，轻松将智能搜索和内容提取功能集成到应用程序中。</li><li><code>@computer-use/node-mac-permissions</code>：用来管理 macOS 权限的 nodejs 包。</li><li><code>@agent-infra/mcp-server-commands</code>：执行任意命令的 MCP 服务器。</li><li><code>@agent-infra/mcp-server-filesystem</code>：访问文件系统的 MCP 服务器。</li><li><code>@agent-infra/mcp-server-browser</code>：操作浏览器的 MCP 服务器。</li><li><code>jotai</code>：轻量状态管理库</li></ul><h2 id="架构"><a href="#架构" class="headerlink" title="架构"></a>架构</h2><h3 id="Monorepo"><a href="#Monorepo" class="headerlink" title="Monorepo"></a>Monorepo</h3><ul><li>pnpm workspace</li></ul><h3 id="Electron"><a href="#Electron" class="headerlink" title="Electron"></a>Electron</h3><p>agent-tars 是一个基于 Electron 的桌面应用，Electron 的主进程（Main Process）和渲染进程（Renderer Process）是其双进程架构的核心设计，两者的职责、运行环境和交互方式决定了 Electron 应用的性能与安全性。</p><h4 id="主进程（Main-Process）"><a href="#主进程（Main-Process）" class="headerlink" title="主进程（Main Process）"></a><strong>主进程（Main Process）</strong></h4><ul><li><strong>​入口与生命周期管理</strong>：作为应用的入口（<code>src/main/index.ts</code>），负责启动&#x2F;关闭应用、管理窗口生命周期（如创建&#x2F;销毁 <code>BrowserWindow</code> 实例）。</li><li><strong>​系统级交互</strong>：直接访问 Node.js API 和操作系统资源（如文件系统、网络请求、系统通知），执行敏感操作（如加密数据处理）</li><li><strong>​全局状态控制</strong>：管理跨窗口的全局数据（如用户配置、缓存），协调多渲染进程的通信。</li></ul><h4 id="渲染进程（Renderer-Process）"><a href="#渲染进程（Renderer-Process）" class="headerlink" title="渲染进程（Renderer Process）"></a><strong>渲染进程（Renderer Process）</strong></h4><ul><li><strong>UI 渲染与交互</strong>：每个窗口对应一个独立的渲染进程，基于 Chromium 实现网页渲染，处理 HTML、CSS、JavaScript 的执行及用户交互（如点击事件）。</li><li><strong>部分 Node.js能力</strong>：默认不启用 Node.js 集成，但可通过 <code>preload</code> 脚本和 <code>contentBridge</code> 暴露有限 API（如 <code>fs</code> 模块的部分功能）。</li><li><strong>沙箱环境</strong>：出于安全考虑，渲染进程默认隔离，无法直接访问系统资源或执行危险操作。</li></ul><h4 id="进程间通信（IPC）机制"><a href="#进程间通信（IPC）机制" class="headerlink" title="进程间通信（IPC）机制"></a>进程间通信（IPC）机制</h4><p>主进程与渲染进程通过 IPC（Inter-Process Communication）实现安全通信：</p><p>1、<strong>异步通信</strong>：</p><ul><li>渲染进程通过 <code>ipcRenderer.send()</code> 发送消息，主进程通过 <code>ipcMain.on()</code> 监听并响应。</li><li>示例：渲染进程请求打开文件 → 主进程调用 <code>fs</code> 模块读取 → 结果返回渲染进程。</li></ul><p>2、<strong>同步通信</strong>：</p><ul><li>渲染进程通过 <code>ipcRenderer.sendSync()</code> 发送同步请求，主进程处理后返回结果（可能阻塞 UI 线程，慎用）</li></ul><p>3、<strong>预加载脚本（Preload Script）</strong></p><ul><li>在渲染进程加载前注入，通过 <code>contextBridge.exposeInMainWorld()</code> 安全暴露主进程 API（如 <code>sendMessageToMain</code>）</li><li>示例：预加载脚本桥接 <code>ipcRenderer</code>，避免直接暴露 Node.js 功能。</li></ul><h2 id="渲染进程（src-renderer）"><a href="#渲染进程（src-renderer）" class="headerlink" title="渲染进程（src&#x2F;renderer）"></a>渲染进程（src&#x2F;renderer）</h2><h3 id="main-tsx"><a href="#main-tsx" class="headerlink" title="main.tsx"></a>main.tsx</h3><p>渲染进程的入口在 <code>src/main.tsx</code>，主要作用：</p><ol><li>执行 <code>initMonacoWorkers()</code> 方法初始化 MonacoWorkers，具体分析看 <a href="#initMonacoWorkers">initMonacoWorkers</a></li><li>引入 <code>./components/App.tsx</code>，具体分析看 <a href="#App.tsx">App.tsx</a></li></ol><h3 id="App-tsx"><a href="#App-tsx" class="headerlink" title="App.tsx"></a>App.tsx</h3><p><code>main.tsx</code> 一般都是执行一些和 UI 无关的全局初始化操作，<code>App.tsx</code> 则是处理一些 UI 相关的全局初始化操作：</p><ol><li>调用 <code>useMainProcessErrorHandler()</code> 注册主进程错误处理程序，这不是我们关注的重点，请查看<a href="https://github.com/bytedance/UI-TARS-desktop/blob/main/apps/agent-tars/src/renderer/src/services/errorHandlerService.ts#L88">源码</a></li><li>引入 <code>./components/AgentApp</code> 进行展示</li></ol><h3 id="AgentApp-tsx"><a href="#AgentApp-tsx" class="headerlink" title="AgentApp.tsx"></a>AgentApp.tsx</h3><ol><li><a href="https://github.com/bytedance/UI-TARS-desktop/tree/main/apps/agent-tars/src/renderer/src/components/LeftSidebar"><code>&lt;LeftSidebar /&gt;</code></a>：放置主题切换、会话管理和设置，这不是本文关注的重点。</li><li><a href="#OpenAgentChatUI"><code>&lt;OpenAgentChatUI /&gt;</code></a>：负责指令输入和 Agent 执行链展示</li><li><a href="#CanvasPanel"><code>&lt;CanvasPanel&gt;</code></a>：负责展示 Agent 执行细节的展示</li></ol><h3 id="OpenAgentChatUI"><a href="#OpenAgentChatUI" class="headerlink" title="OpenAgentChatUI"></a>OpenAgentChatUI</h3><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">ChatUI</span> <span class="keyword">as</span> <span class="title class_">BaseChatUI</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;@vendor/chat-ui&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">MenuHeader</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;./MenuHeader&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">AgentStatusTip</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;./AgentStatusTip&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">BeforeInputContainer</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;./BeforeInputContainer&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; useAgentFlow &#125; <span class="keyword">from</span> <span class="string">&#x27;@renderer/hooks/useAgentFlow&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">WelcomeScreen</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;../WelcomeScreen&#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_">OpenAgentChatUI</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> launchAgentFlow = <span class="title function_">useAgentFlow</span>();</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> sendMessage = <span class="title function_">useCallback</span>(</span><br><span class="line">    <span class="keyword">async</span> (<span class="attr">inputText</span>: <span class="built_in">string</span>, <span class="attr">inputFiles</span>: <span class="title class_">InputFile</span>[]) =&gt; &#123;</span><br><span class="line">      <span class="title function_">setIsSending</span>(<span class="literal">true</span>);</span><br><span class="line">      <span class="comment">// 截止发稿日，暂不支持文件上传</span></span><br><span class="line">      <span class="keyword">await</span> <span class="title function_">launchAgentFlow</span>(inputText, inputFiles);</span><br><span class="line">      <span class="title function_">setIsSending</span>(<span class="literal">false</span>);</span><br><span class="line">    &#125;,</span><br><span class="line">    [addUserMessage, launchAgentFlow],</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;&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">BaseChatUI</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">        <span class="attr">onMessageSend</span>=<span class="string">&#123;sendMessage&#125;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">        <span class="attr">slots</span>=<span class="string">&#123;&#123;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">          <span class="attr">beforeMessageList:</span> (</span></span></span><br><span class="line"><span class="tag"><span class="language-xml">            &lt;&gt;</span></span></span><br><span class="line"><span class="language-xml">              <span class="tag">&lt;<span class="name">MenuHeader</span> /&gt;</span></span></span><br><span class="line"><span class="language-xml">              &#123;isInitialized &amp;&amp; messages.length &gt;= 0 &amp;&amp; <span class="tag">&lt;<span class="name">WelcomeScreen</span> /&gt;</span>&#125;</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></span><br><span class="line"><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">          beforeInputContainer: <span class="tag">&lt;<span class="name">BeforeInputContainer</span> /&gt;</span>,</span></span><br><span class="line"><span class="language-xml">          customFeatures: (</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">div</span> <span class="attr">className</span>=<span class="string">&quot;flex gap-2&quot;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">                &#123;isSending ? <span class="tag">&lt;<span class="name">AgentStatusTip</span> /&gt;</span> : null&#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;/&gt;</span></span></span><br><span class="line">          ),</span><br><span class="line">        &#125;&#125;</span><br><span class="line">      /&gt;</span><br><span class="line">    &lt;/&gt;</span><br><span class="line">  )</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>以上是去掉和 ChatUI 交互后的简化的核心代码，<code>@vender/chat-ui</code> 由于内部原因，暂未公开，可以关注 <a href="https://github.com/bytedance/UI-TARS-desktop/issues/417">issues#417</a>。核心逻辑如下：</p><ol><li>用户输入完成后，触发 <code>sendMessage</code>，将用户输入发送给 <code>launchAgentFlow</code></li><li><code>launchAgentFlow</code> 触发 Agent 执行，详细代码请查看 <a href="#useAgentFlow">useAgentFlow</a></li></ol><p>slots 插槽的配置分别是：</p><ol><li><code>beforeMessageList</code>，MessageList 前面的槽位，这里放了菜单栏和欢迎页</li><li><code>beforeInputContainer</code>，InputContainer 前面的槽位，这里放了 <a href="#BeforeInputContainer"><code>&lt;BeforeInputContainer /&gt;</code></a>，</li><li><code>customFeatures</code>，自定义功能槽位，这里放了 <code>&lt;AgentStatusTip /&gt;</code>，用来展示 Agent 的状态</li></ol><h4 id="BeforeInputContainer"><a href="#BeforeInputContainer" class="headerlink" title="BeforeInputContainer"></a>BeforeInputContainer</h4><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">PlanTaskStatus</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;./PlanTaskStatus&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">UserInterruptArea</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;./UserInterruptArea&#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_">BeforeInputContainer</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;&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">PlanTaskStatus</span> /&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">UserInterruptArea</span> <span class="attr">isDark</span>=<span class="string">&#123;isDarkMode.value&#125;</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><ol><li><code>&lt;PlanTaskStatus /&gt;</code>，计划任务状态，展示计划执行列表的进度</li><li><code>&lt;UserInterruptArea /&gt;</code>，用户中断区域，消息发送或执行中，允许用户发送新的消息或中断</li></ol><h3 id="CanvasPanel"><a href="#CanvasPanel" class="headerlink" title="CanvasPanel"></a>CanvasPanel</h3><h3 id="hooks"><a href="#hooks" class="headerlink" title="hooks"></a>hooks</h3><h4 id="useAgentFlow"><a href="#useAgentFlow" class="headerlink" title="useAgentFlow"></a>useAgentFlow</h4><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; useCallback &#125; <span class="keyword">from</span> <span class="string">&quot;react&quot;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">AgentFlow</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;../agent/AgentFlow&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; v4 <span class="keyword">as</span> uuid &#125; <span class="keyword">from</span> <span class="string">&#x27;uuid&#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_">useAgentFlow</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="title function_">useCallback</span>(</span><br><span class="line">    <span class="title function_">async</span>(<span class="attr">inputText</span>: <span class="built_in">string</span>; <span class="attr">inputFiles</span>: <span class="title class_">InputFile</span>[]) =&gt; &#123;</span><br><span class="line">      <span class="keyword">const</span> agentFlowId = <span class="title function_">uuid</span>();</span><br><span class="line">      <span class="keyword">const</span> agentFlow = <span class="keyword">new</span> <span class="title class_">AgentFlow</span>(&#123;</span><br><span class="line">        agentFlowId,</span><br><span class="line">        <span class="attr">request</span>: &#123;</span><br><span class="line">          inputText,</span><br><span class="line">          <span class="comment">// 截止发稿日，暂不支持文件上传</span></span><br><span class="line">          inputFiles</span><br><span class="line">        &#125;</span><br><span class="line">      &#125;);</span><br><span class="line">      <span class="keyword">await</span> agentFlow.<span class="title function_">run</span>();</span><br><span class="line">    &#125;,</span><br><span class="line">    []</span><br><span class="line">  )</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ol><li><code>useAgentFow</code> hook 接收用户输入后，创建一个 <a href="#AgentFlow"><code>AgentFlow</code></a> 实例，并调用 <code>agentFlow.run()</code> 触发 Agent 执行</li></ol><h3 id="agent"><a href="#agent" class="headerlink" title="agent"></a>agent</h3><h4 id="AgentFlow-ts"><a href="#AgentFlow-ts" class="headerlink" title="AgentFlow.ts"></a>AgentFlow.ts</h4><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">ToolCallType</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;@renderer/type/agent&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">Aware</span>, <span class="title class_">AwareResult</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;./Aware&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">EventManager</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;./EventManager&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">Executor</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;./Executor&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">Greeter</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;./Greeter&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; ipcClient &#125; <span class="keyword">from</span> <span class="string">&#x27;@renderer/api&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; <span class="variable constant_">SNAPSHOT_BROWSER_ACTIONS</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;@renderer/constants&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> <span class="title class_">AgentFlow</span> &#123;</span><br><span class="line">  <span class="keyword">private</span> <span class="attr">abortController</span>: <span class="title class_">AbortController</span>;</span><br><span class="line"></span><br><span class="line">  <span class="title function_">constructor</span>(<span class="params"><span class="keyword">private</span> appContext: AppContext</span>) &#123;</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">eventManager</span> = <span class="keyword">new</span> <span class="title class_">EventManager</span>(omegaHistoryEvents);</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">async</span> <span class="title function_">run</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="keyword">const</span> greeter = <span class="keyword">new</span> <span class="title class_">Greeter</span>(<span class="variable language_">this</span>.<span class="property">appContext</span>, <span class="variable language_">this</span>.<span class="property">abortController</span>.<span class="property">signal</span>);</span><br><span class="line">    <span class="keyword">const</span> aware = <span class="keyword">new</span> <span class="title class_">Aware</span>(</span><br><span class="line">      <span class="variable language_">this</span>.<span class="property">appContext</span>,</span><br><span class="line">      agentContext,</span><br><span class="line">      <span class="variable language_">this</span>.<span class="property">interruptController</span>.<span class="property">signal</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> executor = <span class="keyword">new</span> <span class="title class_">Executor</span>(</span><br><span class="line">      <span class="variable language_">this</span>.<span class="property">appContext</span>,</span><br><span class="line">      agentContext,</span><br><span class="line">      <span class="variable language_">this</span>.<span class="property">interruptController</span>.<span class="property">signal</span>,</span><br><span class="line">    );</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 展示 ai 打招呼消息</span></span><br><span class="line">    <span class="keyword">const</span> preparePromise = greeter.<span class="title function_">run</span>().<span class="title function_">then</span>(<span class="keyword">async</span> () =&gt; &#123;&#125;);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">await</span> <span class="title class_">Promise</span>.<span class="title function_">all</span>([</span><br><span class="line">      preparePromise,</span><br><span class="line">      <span class="comment">// 启动 Agent 循环</span></span><br><span class="line">      <span class="variable language_">this</span>.<span class="title function_">launchAgentLoop</span>(executor, aware, agentContext, preparePromise),</span><br><span class="line">    ]);</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">private</span> <span class="keyword">async</span> <span class="title function_">launchAgentLoop</span>(<span class="params"></span></span><br><span class="line"><span class="params">    executor: Executor,</span></span><br><span class="line"><span class="params">    aware: Aware,</span></span><br><span class="line"><span class="params">    agentContext: AgentContext,</span></span><br><span class="line"><span class="params">    preparePromise: <span class="built_in">Promise</span>&lt;<span class="built_in">void</span>&gt;</span></span><br><span class="line"><span class="params">  </span>) &#123;</span><br><span class="line">    <span class="comment">//  Agent 循环</span></span><br><span class="line">    <span class="keyword">while</span>(!<span class="variable language_">this</span>.<span class="property">abortController</span>.<span class="property">signal</span>.<span class="property">aborted</span> &amp;&amp; !<span class="variable language_">this</span>.<span class="property">hasFinished</span>) &#123;</span><br><span class="line">      <span class="comment">// 当任务没有被取消或完成时执行 Agent 循环</span></span><br><span class="line">      <span class="keyword">const</span> awareResult = <span class="keyword">await</span> aware.<span class="title function_">run</span>();</span><br><span class="line">      <span class="keyword">if</span> (<span class="variable language_">this</span>.<span class="property">abortController</span>.<span class="property">signal</span>.<span class="property">aborted</span>) &#123;</span><br><span class="line">        <span class="comment">// 如果任务被取消，则跳出 Agent 循环</span></span><br><span class="line">        <span class="keyword">break</span>;</span><br><span class="line">      &#125;</span><br><span class="line"></span><br><span class="line">      <span class="keyword">if</span> (</span><br><span class="line">        awareResult.<span class="property">plan</span> &amp;&amp;</span><br><span class="line">        awareResult.<span class="property">plan</span>.<span class="title function_">every</span>(</span><br><span class="line">          <span class="function">(<span class="params">task</span>) =&gt;</span> task.<span class="property">status</span> === <span class="title class_">PlanTaskStatus</span>.<span class="property">Done</span>,</span><br><span class="line">        )</span><br><span class="line">      ) &#123;</span><br><span class="line">        <span class="comment">// 如果所有任务都完成，则跳出 Agent 循环</span></span><br><span class="line">        <span class="variable language_">this</span>.<span class="property">hasFinished</span> = <span class="literal">true</span>;</span><br><span class="line">        <span class="keyword">break</span>;</span><br><span class="line">      &#125;</span><br><span class="line"></span><br><span class="line">      <span class="keyword">const</span> toolCallList = (<span class="keyword">await</span> executor.<span class="title function_">run</span>(awareResult.<span class="property">status</span>)).<span class="title function_">filter</span>(</span><br><span class="line">        <span class="title class_">Boolean</span>,</span><br><span class="line">      );</span><br><span class="line">      <span class="keyword">const</span> mcpTools = <span class="keyword">await</span> ipcClient.<span class="title function_">listMcpTools</span>();</span><br><span class="line">      <span class="keyword">const</span> customServerTools = <span class="keyword">await</span> ipcClient.<span class="title function_">listCustomTools</span>();</span><br><span class="line"></span><br><span class="line">      <span class="comment">// 执行 工具</span></span><br><span class="line">      <span class="keyword">for</span> (<span class="keyword">const</span> toolCall <span class="keyword">of</span> toolCallList) &#123;</span><br><span class="line">        <span class="keyword">const</span> toolName = toolCall.<span class="property">function</span>.<span class="property">name</span>;</span><br><span class="line">        <span class="keyword">const</span> isMCPToolCall = mcpTools.<span class="title function_">some</span>(</span><br><span class="line">          <span class="function">(<span class="params">tool</span>) =&gt;</span> tool.<span class="property">name</span> === toolCall.<span class="property">function</span>.<span class="property">name</span>,</span><br><span class="line">        );</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 如果是执行 MCP 工具</span></span><br><span class="line">        <span class="keyword">if</span> (isMCPToolCall) &#123;</span><br><span class="line">          <span class="keyword">if</span> (</span><br><span class="line">            <span class="comment">// 如果是编辑和写入文件</span></span><br><span class="line">            toolName === <span class="title class_">ToolCallType</span>.<span class="property">EditFile</span> ||</span><br><span class="line">            toolName === <span class="title class_">ToolCallType</span>.<span class="property">WriteFile</span></span><br><span class="line">          ) &#123;</span><br><span class="line">            <span class="keyword">const</span> params = <span class="title class_">JSON</span>.<span class="title function_">parse</span>(</span><br><span class="line">               toolCall.<span class="property">function</span>.<span class="property">arguments</span>,</span><br><span class="line">             ) <span class="keyword">as</span> <span class="title class_">ToolCallParam</span>[<span class="string">&#x27;edit_file&#x27;</span>];</span><br><span class="line">             originalFileContent = <span class="keyword">await</span> ipcClient.<span class="title function_">getFileContent</span>(&#123;</span><br><span class="line">               <span class="attr">filePath</span>: params.<span class="property">path</span>,</span><br><span class="line">             &#125;);</span><br><span class="line">          &#125;</span><br><span class="line">          <span class="keyword">const</span> callResult = (<span class="keyword">await</span> executor.<span class="title function_">executeTools</span>([toolCall]))[<span class="number">0</span>];</span><br><span class="line">          <span class="keyword">await</span> <span class="variable language_">this</span>.<span class="property">eventManager</span>.<span class="title function_">handleToolExecution</span>(&#123;</span><br><span class="line">            toolName,</span><br><span class="line">            <span class="attr">toolCallId</span>: toolCall.<span class="property">id</span>,</span><br><span class="line">            <span class="attr">params</span>: toolCall.<span class="property">function</span>.<span class="property">arguments</span>,</span><br><span class="line">            <span class="attr">result</span>: callResult.<span class="property">content</span>,</span><br><span class="line">            <span class="attr">isError</span>: callResult.<span class="property">isError</span> <span class="keyword">as</span> <span class="built_in">boolean</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">if</span> (<span class="variable constant_">SNAPSHOT_BROWSER_ACTIONS</span>.<span class="title function_">includes</span>(toolName)) &#123;</span><br><span class="line">          <span class="comment">//</span></span><br><span class="line">          <span class="keyword">const</span> screenshotPath = <span class="keyword">await</span> ipcClient.<span class="title function_">saveBrowserSnapshot</span>();</span><br><span class="line">          <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">&#x27;screenshotPath&#x27;</span>, screenshotPath);</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><h4 id="Aware"><a href="#Aware" class="headerlink" title="Aware"></a>Aware</h4><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br><span class="line">128</span><br><span class="line">129</span><br><span class="line">130</span><br><span class="line">131</span><br><span class="line">132</span><br><span class="line">133</span><br><span class="line">134</span><br><span class="line">135</span><br><span class="line">136</span><br><span class="line">137</span><br><span class="line">138</span><br><span class="line">139</span><br><span class="line">140</span><br><span class="line">141</span><br><span class="line">142</span><br><span class="line">143</span><br><span class="line">144</span><br><span class="line">145</span><br><span class="line">146</span><br><span class="line">147</span><br><span class="line">148</span><br><span class="line">149</span><br><span class="line">150</span><br><span class="line">151</span><br><span class="line">152</span><br><span class="line">153</span><br><span class="line">154</span><br><span class="line">155</span><br><span class="line">156</span><br><span class="line">157</span><br><span class="line">158</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> <span class="title class_">Aware</span> &#123;</span><br><span class="line">  <span class="keyword">private</span> systemPrompt = <span class="string">`You are an AI agent with the ability to analyze the current environment, decide the next task status, tell user the next specific action.</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">&lt;task_description&gt;</span></span><br><span class="line"><span class="string">You must call the aware_analysis tool.</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">You should give the insights of current environment according to the various context information, and then decide the next task status.</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">If the task is none or current step is done, you should increment the step number and update system status. Please return the json output in the tool call part:</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">\`\`\`json</span></span><br><span class="line"><span class="string">&#123;</span></span><br><span class="line"><span class="string">  &quot;reflection&quot;: &quot;[your reflection about current environment]&quot;,</span></span><br><span class="line"><span class="string">  &quot;step&quot;: &quot;[next step number]&quot;,</span></span><br><span class="line"><span class="string">  &quot;plan&quot;: &quot;[steps array with id and title fields]&quot;,</span></span><br><span class="line"><span class="string">  &quot;status&quot;: &quot;[next task description, a complete sentence tell user what to do next]&quot;,</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"></span></span><br><span class="line"><span class="string">You should output the reflection first.</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">You should not output any response text and only return the tool call.</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">Only when there is no existing plan in the current environment, you should return plan field with the following format:</span></span><br><span class="line"><span class="string">- id: string (format: &quot;step_XXX&quot; where XXX is a sequential number starting from 001)</span></span><br><span class="line"><span class="string">- title: string (clear, concise description of the step)</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">&lt;/task_description&gt;</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">&lt;think_steps&gt;</span></span><br><span class="line"><span class="string">For any given task or problem:</span></span><br><span class="line"><span class="string">1. Analyze the requirements thoroughly</span></span><br><span class="line"><span class="string">2. Create a systematic, step-by-step solution</span></span><br><span class="line"><span class="string">3. Ensure each step is concrete and actionable</span></span><br><span class="line"><span class="string">4. Maintain logical progression between steps</span></span><br><span class="line"><span class="string">&lt;/think_steps&gt;</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">&lt;limitation&gt;</span></span><br><span class="line"><span class="string">You must follow these limitations:</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">- If there is plan exist, you should not return the plan field.</span></span><br><span class="line"><span class="string">- Don&#x27;t ask user anything, just tell user what to do next. If some points is not very clear, you should tell user your solution. Remember, you are a agent for human.</span></span><br><span class="line"><span class="string">- Don&#x27;t output any response text and only return the tool call.</span></span><br><span class="line"><span class="string">- You should not repeat the same behavior or mean with previous steps.</span></span><br><span class="line"><span class="string">- Don&#x27;t output any file path in current machine and ensure the security in your message. Don&#x27;t output any absolute path in your message.</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">&lt;/limitation&gt;</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">&lt;update_plan_in_process&gt;</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">Only except user interrupt or start a new session, you CANNOT update the plan!</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">If you reset the plan to a new one, you should also reset the step to number 1.</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">&lt;/update_plan_in_process&gt;</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">&lt;status_field&gt;</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">In the \`status\` field, you should only return a sentence to tell user what you will do next, and don&#x27;t need to return the reason and other information.Please the the first person perspective to answer, indicating that you are work for the user.</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">&lt;/status_field&gt;</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">&lt;end_step&gt;</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">If in the last step, but we still have issues to solve, you cannot increment the step number and should continue to solve the issue.</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">&lt;/end_step&gt;</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">&lt;user_interrupt&gt;</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">For user interrupt input in the middle of the event stream, you should handle it in the first important level and handle it as soon as possible.If current plan tasks cannot match the new user input, you should reset the plan.</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">&lt;/user_interrupt&gt;</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">&lt;event_stream&gt;</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">The event stream result record the complete response of the agent, you should make next decision base on the history, if current step has not been done, please don&#x27;t increment the step number. If you meet the \`ended\` message, that means you entered a new session and you should reset the plan from scratch.</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">In the event stream, the \`observation\` type message is the observation of the tool use, you should attention to the field and judge the task status according to it.When the observer represent the error message, you should reflect the error and solve it in the next step.</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">&lt;/event_stream&gt;</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">&lt;after_web_search&gt;</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">After \`web_search\` called, then you must select web page from the search result, then you must see the detail of the page, call navigate to get detail. See the detail right away after get search result!</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">&lt;/after_web_search&gt;</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">&lt;write_file&gt;</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">When you want to write file, you should list allowed directories and write the file to the allowed directory.</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">&lt;/write_file&gt;</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">&lt;language&gt;</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">You should use the same language as the user input by default.</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">&lt;/language&gt;`</span>;</span><br><span class="line">  <span class="keyword">async</span> <span class="title function_">run</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> environmentInfo = <span class="keyword">await</span> <span class="variable language_">this</span>.<span class="property">agentContext</span>.<span class="title function_">getEnvironmentInfo</span>(</span><br><span class="line">      <span class="variable language_">this</span>.<span class="property">appContext</span>,</span><br><span class="line">      <span class="variable language_">this</span>.<span class="property">agentContext</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> defaultResult = &#123;</span><br><span class="line">      <span class="attr">reflection</span>: <span class="string">&#x27;No plan&#x27;</span>,</span><br><span class="line">      <span class="attr">step</span>: <span class="variable language_">this</span>.<span class="property">agentContext</span>.<span class="property">currentStep</span>,</span><br><span class="line">      <span class="attr">status</span>: <span class="string">&#x27;No plan&#x27;</span>,</span><br><span class="line">      <span class="attr">plan</span>: [],</span><br><span class="line">    &#125;;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">Promise</span>(resolve, reject) =&gt; &#123;</span><br><span class="line">      <span class="keyword">try</span> &#123;</span><br><span class="line">        <span class="comment">// 获取执行器工具列表</span></span><br><span class="line">        <span class="keyword">const</span> executorTools = <span class="keyword">await</span> ipcClient.<span class="title function_">listTools</span>();</span><br><span class="line">        <span class="comment">// 调用 LLM</span></span><br><span class="line">        <span class="keyword">const</span> result = <span class="keyword">await</span> ipcClient.<span class="title function_">askLLMTool</span>(&#123;</span><br><span class="line">          <span class="attr">messages</span>: [</span><br><span class="line">            <span class="title class_">Message</span>.<span class="title function_">systemMessage</span>(<span class="variable language_">this</span>.<span class="property">systemPrompt</span>),</span><br><span class="line">            <span class="title class_">Message</span>.<span class="title function_">systemMessage</span>(</span><br><span class="line">              <span class="string">`You are working with executor agent, here is the executor tools: <span class="subst">$&#123;executorTools</span></span></span><br><span class="line"><span class="subst"><span class="string">                .map((tool) =&gt; <span class="string">`<span class="subst">$&#123;tool.name&#125;</span>: <span class="subst">$&#123;tool.description&#125;</span>`</span>)</span></span></span><br><span class="line"><span class="subst"><span class="string">                .join(<span class="string">&#x27;, &#x27;</span>)&#125;</span>`</span>,</span><br><span class="line">            ),</span><br><span class="line">            <span class="title class_">Message</span>.<span class="title function_">userMessage</span>(environmentInfo),</span><br><span class="line">            <span class="title class_">Message</span>.<span class="title function_">userMessage</span>(</span><br><span class="line">              <span class="string">`Please call aware_analysis tool to give me next decision.`</span>,</span><br><span class="line">            ),</span><br><span class="line">          ],</span><br><span class="line">          <span class="attr">tools</span>: [</span><br><span class="line">            &#123;</span><br><span class="line">              <span class="attr">type</span>: <span class="string">&#x27;function&#x27;</span>,</span><br><span class="line">              <span class="attr">function</span>: &#123;</span><br><span class="line">                <span class="attr">name</span>: <span class="string">&#x27;aware_analysis&#x27;</span>,</span><br><span class="line">                <span class="attr">description</span>:</span><br><span class="line">                  <span class="string">&#x27;Analyze the current environment with user input, and decide the next task status&#x27;</span>,</span><br><span class="line">                <span class="attr">parameters</span>: <span class="variable language_">this</span>.<span class="property">awareSchema</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="attr">requestId</span>: streamId,</span><br><span class="line">        &#125;);</span><br><span class="line">        <span class="keyword">const</span> awareResult = <span class="title class_">JSON</span>.<span class="title function_">parse</span>(</span><br><span class="line">          result.<span class="property">tool_calls</span>.<span class="title function_">filter</span>(<span class="title class_">Boolean</span>)[<span class="number">0</span>].<span class="property">function</span>.<span class="property">arguments</span>,</span><br><span class="line">        ) <span class="keyword">as</span> <span class="title class_">AwareResult</span>;</span><br><span class="line">        <span class="title function_">resolve</span>(awareResult);</span><br><span class="line">      &#125; <span class="keyword">catch</span> (error) &#123;</span><br><span class="line">        <span class="title function_">reject</span>(error);</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><code>src/main/ipcRoutes/llm.ts</code></p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> llmRoute = t.<span class="title function_">route</span>(&#123;</span><br><span class="line">  <span class="attr">askLLMTool</span>: t.<span class="property">procedure</span>.<span class="title function_">input</span>().<span class="title function_">handle</span>(<span class="keyword">async</span> (&#123; input &#125;) =&gt; &#123;</span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">      <span class="keyword">const</span> messages = input.<span class="property">messages</span>.<span class="title function_">map</span>(<span class="function">(<span class="params">msg</span>) =&gt;</span> <span class="keyword">new</span> <span class="title class_">Message</span>(msg));</span><br><span class="line">      <span class="keyword">const</span> response = <span class="keyword">await</span> llm.<span class="title function_">askTool</span>(&#123;</span><br><span class="line">        messages,</span><br><span class="line">        <span class="attr">tools</span>: input.<span class="property">tools</span>,</span><br><span class="line">        <span class="attr">mcpServerKeys</span>: input.<span class="property">mcpServerKeys</span>,</span><br><span class="line">        <span class="attr">requestId</span>: input.<span class="property">requestId</span>,</span><br><span class="line">      &#125;);</span><br><span class="line">      <span class="keyword">return</span> response;</span><br><span class="line">    &#125; <span class="keyword">catch</span> (error) &#123;&#125;</span><br><span class="line">  &#125;)</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><p><code>src/main/llmProvider/index.ts</code></p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> <span class="title class_">LLM</span> &#123;</span><br><span class="line">  <span class="keyword">async</span> <span class="title function_">askTool</span>(<span class="params">&#123;</span></span><br><span class="line"><span class="params">    messages,</span></span><br><span class="line"><span class="params">    tools,</span></span><br><span class="line"><span class="params">    requestId,</span></span><br><span class="line"><span class="params">    toolChoice,</span></span><br><span class="line"><span class="params">  &#125;</span>) &#123;</span><br><span class="line">    <span class="keyword">const</span> allTools = [...tools];</span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">await</span> <span class="variable language_">this</span>.<span class="property">provider</span>.<span class="title function_">askTool</span>(&#123;</span><br><span class="line">      messages,</span><br><span class="line">      <span class="attr">tools</span>: allTools,</span><br><span class="line">      requestId,</span><br><span class="line">      <span class="attr">toolChoice</span>: toolChoice || <span class="string">&#x27;auto&#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><p><code>src/main/llmProvider/providers/OpenAIProvider.ts</code>：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> <span class="title class_">OpenAIProvider</span> <span class="keyword">extends</span> <span class="title class_ inherited__">BaseProvider</span> &#123;</span><br><span class="line">  <span class="comment">/**</span></span><br><span class="line"><span class="comment">   * Convert Message objects to OpenAI API format</span></span><br><span class="line"><span class="comment">   */</span></span><br><span class="line">  <span class="keyword">protected</span> <span class="title function_">formatMessages</span>(</span><br><span class="line">    <span class="attr">messages</span>: <span class="title class_">Message</span>[],</span><br><span class="line">  ): <span class="title class_">OpenAI</span>.<span class="property">Chat</span>.<span class="property">ChatCompletionMessageParam</span>[] &#123;</span><br><span class="line">    <span class="keyword">return</span> messages.<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">role</span>: item.<span class="property">role</span> <span class="keyword">as</span> <span class="built_in">any</span>,</span><br><span class="line">      <span class="attr">content</span>: item.<span class="property">content</span>,</span><br><span class="line">      ...(item.<span class="property">tool_call_id</span> &amp;&amp; &#123; <span class="attr">tool_call_id</span>: item.<span class="property">tool_call_id</span> &#125;),</span><br><span class="line">      ...(item.<span class="property">tool_calls</span> &amp;&amp; &#123;</span><br><span class="line">        <span class="attr">tool_calls</span>:</span><br><span class="line">          item.<span class="property">tool_calls</span> <span class="keyword">as</span> <span class="title class_">OpenAI</span>.<span class="property">Chat</span>.<span class="property">ChatCompletionMessageToolCall</span>[],</span><br><span class="line">      &#125;),</span><br><span class="line">      ...(item.<span class="property">name</span> &amp;&amp; &#123; <span class="attr">name</span>: item.<span class="property">name</span> &#125;),</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="comment">   * Send a message to the LLM with tools and get a response with potential tool calls</span></span><br><span class="line"><span class="comment">   */</span></span><br><span class="line">  <span class="keyword">async</span> <span class="title function_">askTool</span>(&#123;</span><br><span class="line">    messages,</span><br><span class="line">    tools,</span><br><span class="line">    requestId,</span><br><span class="line">    toolChoice = <span class="string">&#x27;auto&#x27;</span>,</span><br><span class="line">  &#125;: &#123;</span><br><span class="line">    <span class="attr">messages</span>: <span class="title class_">Message</span>[];</span><br><span class="line">    <span class="attr">tools</span>: <span class="title class_">ChatCompletionTool</span>[];</span><br><span class="line">    <span class="attr">requestId</span>: <span class="built_in">string</span>;</span><br><span class="line">    toolChoice?: <span class="title class_">ToolChoice</span>;</span><br><span class="line">  &#125;): <span class="title class_">Promise</span>&lt;<span class="title class_">LLMResponse</span>&gt; &#123;</span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">      <span class="keyword">const</span> formattedMessages = <span class="variable language_">this</span>.<span class="title function_">formatMessages</span>(messages);</span><br><span class="line">      <span class="variable language_">this</span>.<span class="property">activeRequests</span>.<span class="title function_">set</span>(requestId, controller);</span><br><span class="line"></span><br><span class="line">      <span class="keyword">const</span> response = <span class="keyword">await</span> <span class="variable language_">this</span>.<span class="property">client</span>.<span class="property">chat</span>.<span class="property">completions</span>.<span class="title function_">create</span>(</span><br><span class="line">        &#123;</span><br><span class="line">          <span class="attr">model</span>: <span class="variable language_">this</span>.<span class="property">model</span>,</span><br><span class="line">          <span class="attr">messages</span>: formattedMessages,</span><br><span class="line">          <span class="attr">temperature</span>: <span class="variable language_">this</span>.<span class="property">config</span>.<span class="property">temperature</span>,</span><br><span class="line">          <span class="attr">max_tokens</span>: <span class="variable language_">this</span>.<span class="property">config</span>.<span class="property">maxTokens</span>,</span><br><span class="line">          tools,</span><br><span class="line">          <span class="attr">tool_choice</span>: toolChoice,</span><br><span class="line">          <span class="attr">top_p</span>: <span class="variable language_">this</span>.<span class="property">config</span>.<span class="property">topP</span>,</span><br><span class="line">          <span class="attr">frequency_penalty</span>: <span class="variable language_">this</span>.<span class="property">config</span>.<span class="property">frequencyPenalty</span>,</span><br><span class="line">          <span class="attr">presence_penalty</span>: <span class="variable language_">this</span>.<span class="property">config</span>.<span class="property">presencePenalty</span>,</span><br><span class="line">        &#125;,</span><br><span class="line">        &#123;</span><br><span class="line">          <span class="attr">signal</span>: controller.<span class="property">signal</span>,</span><br><span class="line">        &#125;,</span><br><span class="line">      );</span><br><span class="line">      <span class="keyword">const</span> content = response.<span class="property">choices</span>[<span class="number">0</span>].<span class="property">message</span>.<span class="property">content</span>;</span><br><span class="line">      <span class="keyword">const</span> toolCalls = <span class="variable language_">this</span>.<span class="title function_">processToolCalls</span>(response);</span><br><span class="line">      <span class="keyword">return</span> &#123; content, <span class="attr">tool_calls</span>: toolCalls &#125;;</span><br><span class="line">    &#125; <span class="keyword">catch</span> (<span class="attr">error</span>: <span class="built_in">unknown</span>) &#123;&#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="Executor"><a href="#Executor" class="headerlink" title="Executor"></a>Executor</h4><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> <span class="title class_">Executor</span> &#123;</span><br><span class="line">  <span class="keyword">private</span> systemPrompt = <span class="string">`You are a tool use expert. You should call the appropriate tools according to the aware status and environment information.You should not output any response text and only return the JSON.</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">&lt;overall_principal&gt;</span></span><br><span class="line"><span class="string">- Must respond with a tool use (function calling); plain text responses are forbidden</span></span><br><span class="line"><span class="string">- Do not mention any specific tool names to users in messages</span></span><br><span class="line"><span class="string">- Carefully verify available tools; do not fabricate non-existent tools</span></span><br><span class="line"><span class="string">- Follow the instructions carefully in the aware status.</span></span><br><span class="line"><span class="string">- Don&#x27;t repeat the same mean with aware status, you should select the appropriate tool.</span></span><br><span class="line"><span class="string">- Don&#x27;t ask user anything, just tell user what you will do next.If some points is not very clear, you should tell user your solution.Don&#x27;t ask user anything, remember, you are a agent for user.</span></span><br><span class="line"><span class="string">- You should only respond chat message after you have finished some tools and return the summary in chat message.</span></span><br><span class="line"><span class="string">- You should not output any response text and only return the tool call.</span></span><br><span class="line"><span class="string">- Don&#x27;t output any file path in current machine and ensure the security in your message. Don&#x27;t output any absolute path in your message.</span></span><br><span class="line"><span class="string">&lt;/overall_principal&gt;</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">&lt;chat_message_tool&gt;</span></span><br><span class="line"><span class="string">message as summary in current step.Don&#x27;t return message first when the step just started.</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">Notice, you should not output a lot of words in chat message, because the chat message is always summary words. If you want to write something in detail, please use \`write_file\` to write in by markdown file by default.</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">In chat message tool, you should add the files that has been created in the past steps, and put the complete file path in the \`attachments\` param.</span></span><br><span class="line"><span class="string">&lt;chat_message_tool&gt;</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">&lt;file_system_tool&gt;</span></span><br><span class="line"><span class="string">If you meet the file system permission denied, you should check if the dir or file exists and create it if not.</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">Before you interact with filesystem, you must list the allowed dirs and files and check if the dir or file exists and create it if not.Don&#x27;t write file directly. You should write the file into a safe directory.</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">&lt;/file_system_tool&gt;</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">&lt;web_search_tool&gt;</span></span><br><span class="line"><span class="string">After \`web_search\` called, then you must select web page from the search result, then you see the detail of the page, please call browser tool to do it.</span></span><br><span class="line"><span class="string">&lt;/web_search_tool&gt;</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">&lt;browser_tools&gt;</span></span><br><span class="line"><span class="string">use \`browser_navigate\` to enter the page detail.</span></span><br><span class="line"><span class="string">use \`browser_scroll\` to scroll the page.When you use browser to enter the page detail, if the page content is partially visible, you should call browser tool to scroll to get more content, until the page content is fully visible.</span></span><br><span class="line"><span class="string">use \`browser_click\` to click the element.</span></span><br><span class="line"><span class="string">use \`browser_form_input_fill\` to fill the form.</span></span><br><span class="line"><span class="string">use \`browser_select\` to select the element.</span></span><br><span class="line"><span class="string">use \`browser_hover\` to hover the element.</span></span><br><span class="line"><span class="string">use \`browser_evaluate\` to evaluate the element.</span></span><br><span class="line"><span class="string">use \`browser_get_text\` to get the text of the element.</span></span><br><span class="line"><span class="string">&lt;/browser_tool&gt;</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">&lt;commands_tool&gt;</span></span><br><span class="line"><span class="string">When you use commands, you must cd the allowed dir instead of cwd.</span></span><br><span class="line"><span class="string">&lt;/commands_tool&gt;</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">&lt;language&gt;</span></span><br><span class="line"><span class="string">You should use the same language as the user input by default.</span></span><br><span class="line"><span class="string">&lt;/language&gt;`</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>构建出的 executor 结构，我们主要关注</p><h4 id="EventManager"><a href="#EventManager" class="headerlink" title="EventManager"></a>EventManager</h4><p><code>EventManager</code> 类主要是负责和 chat-ui 交互的，由于 chat-ui 没有开源，这块是黑盒的。后续开源之后详细讲解。</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> <span class="title class_">EventManager</span> &#123;</span><br><span class="line">  <span class="comment">/**</span></span><br><span class="line"><span class="comment">   * Get all events</span></span><br><span class="line"><span class="comment">   */</span></span><br><span class="line">  <span class="keyword">public</span> <span class="title function_">getAllEvents</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="keyword">return</span> [...<span class="variable language_">this</span>.<span class="property">events</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="comment">   * Notify subscribers about event updates</span></span><br><span class="line"><span class="comment">   */</span></span><br><span class="line">  <span class="keyword">private</span> <span class="title function_">notifyUpdate</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="keyword">if</span> (<span class="variable language_">this</span>.<span class="property">onEventsUpdate</span>) &#123;</span><br><span class="line">      <span class="variable language_">this</span>.<span class="title function_">onEventsUpdate</span>(<span class="variable language_">this</span>.<span class="title function_">getAllEvents</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="comment">   * Add a generic event</span></span><br><span class="line"><span class="comment">   */</span></span><br><span class="line">  <span class="keyword">private</span> <span class="keyword">async</span> <span class="title function_">addEvent</span>(<span class="params"></span></span><br><span class="line"><span class="params">    <span class="keyword">type</span>,</span></span><br><span class="line"><span class="params">    content,</span></span><br><span class="line"><span class="params">    willNotifyUpdate = <span class="literal">true</span>,</span></span><br><span class="line"><span class="params">  </span>) &#123;</span><br><span class="line">    <span class="keyword">const</span> event = &#123;</span><br><span class="line">      <span class="attr">id</span>: <span class="title function_">nanoid</span>(),</span><br><span class="line">      <span class="keyword">type</span>,</span><br><span class="line">      content,</span><br><span class="line">      <span class="attr">timestamp</span>: <span class="title class_">Date</span>.<span class="title function_">now</span>(),</span><br><span class="line">    &#125;;</span><br><span class="line"></span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">events</span>.<span class="title function_">push</span>(event);</span><br><span class="line">    willNotifyUpdate &amp;&amp; (<span class="keyword">await</span> <span class="variable language_">this</span>.<span class="title function_">notifyUpdate</span>());</span><br><span class="line">    <span class="keyword">return</span> event;</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="comment">   * Add an observation event</span></span><br><span class="line"><span class="comment">   */</span></span><br><span class="line">  <span class="keyword">public</span> <span class="keyword">async</span> <span class="title function_">addObservation</span>(<span class="params">content</span>) &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="variable language_">this</span>.<span class="title function_">addEvent</span>(<span class="title class_">EventType</span>.<span class="property">Observation</span>, content);</span><br><span class="line">  &#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="comment">   * Handle tool execution result and add related events</span></span><br><span class="line"><span class="comment">   */</span></span><br><span class="line">  <span class="keyword">public</span> <span class="keyword">async</span> <span class="title function_">handleToolExecution</span>(<span class="params">&#123;</span></span><br><span class="line"><span class="params">    toolName,</span></span><br><span class="line"><span class="params">    toolCallId,</span></span><br><span class="line"><span class="params">    params,</span></span><br><span class="line"><span class="params">    result,</span></span><br><span class="line"><span class="params">    isError,</span></span><br><span class="line"><span class="params">  &#125;</span>) &#123;</span><br><span class="line">    <span class="keyword">const</span> normalizedInfo = <span class="title function_">normalizeToolUsedInfo</span>(</span><br><span class="line">      toolName,</span><br><span class="line">      params,</span><br><span class="line">      isError ? <span class="title class_">ActionStatus</span>.<span class="property">Failed</span> : <span class="title class_">ActionStatus</span>.<span class="property">Success</span>,</span><br><span class="line">      result,</span><br><span class="line">    );</span><br><span class="line"></span><br><span class="line">    <span class="keyword">await</span> <span class="variable language_">this</span>.<span class="title function_">addEvent</span>(<span class="title class_">EventType</span>.<span class="property">ToolUsed</span>, &#123;</span><br><span class="line">      <span class="attr">actionId</span>: toolCallId,</span><br><span class="line">      ...normalizedInfo,</span><br><span class="line">    &#125;);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">await</span> <span class="variable language_">this</span>.<span class="title function_">addObservation</span>(<span class="title class_">JSON</span>.<span class="title function_">stringify</span>(result));</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>src/renderer/src/type/event.ts</code>：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">enum</span> <span class="title class_">EventType</span> &#123;</span><br><span class="line">  <span class="title class_">UserMessage</span> = <span class="string">&#x27;user-message&#x27;</span>,</span><br><span class="line">  <span class="title class_">LoadingStatus</span> = <span class="string">&#x27;loading-status&#x27;</span>,</span><br><span class="line">  <span class="title class_">ToolUsed</span> = <span class="string">&#x27;tool-used&#x27;</span>,</span><br><span class="line">  <span class="title class_">ToolCallStart</span> = <span class="string">&#x27;tool-call-start&#x27;</span>,</span><br><span class="line">  <span class="title class_">PlanUpdate</span> = <span class="string">&#x27;plan-update&#x27;</span>,</span><br><span class="line">  <span class="title class_">AgentStatus</span> = <span class="string">&#x27;agent-status&#x27;</span>,</span><br><span class="line">  <span class="title class_">ChatText</span> = <span class="string">&#x27;chat-text&#x27;</span>,</span><br><span class="line">  <span class="title class_">Observation</span> = <span class="string">&#x27;observation&#x27;</span>,</span><br><span class="line">  <span class="title class_">NewPlanStep</span> = <span class="string">&#x27;new-plan-step&#x27;</span>,</span><br><span class="line">  <span class="title class_">UserInterruption</span> = <span class="string">&#x27;user-interruption&#x27;</span>,</span><br><span class="line">  <span class="title class_">End</span> = <span class="string">&#x27;end&#x27;</span>,</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="api"><a href="#api" class="headerlink" title="api"></a>api</h3><h4 id="ipcClient"><a href="#ipcClient" class="headerlink" title="ipcClient"></a>ipcClient</h4><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; createClient &#125; <span class="keyword">from</span> <span class="string">&#x27;@ui-tars/electron-ipc/renderer&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> ipcClient = <span class="title function_">createClient</span>(&#123;</span><br><span class="line">  <span class="attr">ipcInvoke</span>: <span class="variable language_">window</span>.<span class="property">electron</span>.<span class="property">ipcRenderer</span>.<span class="property">invoke</span>,</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p>createClient 代码如下：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">IpcRenderer</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;electron&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> <span class="title function_">createClient</span> = (<span class="params">&#123; ipcInvoke &#125;</span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">Proxy</span>(&#123;&#125;, &#123;</span><br><span class="line">    <span class="attr">get</span>: <span class="function">(<span class="params">_, prop</span>) =&gt;</span> &#123;</span><br><span class="line">      <span class="keyword">const</span> <span class="title function_">invoke</span> = (<span class="params">input</span>) =&gt; &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="title function_">ipcInvoke</span>(prop.<span class="title function_">toString</span>(), input);</span><br><span class="line">      &#125;;</span><br><span class="line"></span><br><span class="line">      <span class="keyword">return</span> invoke;</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="utils"><a href="#utils" class="headerlink" title="utils"></a>utils</h3><h4 id="initMonacoWorkers"><a href="#initMonacoWorkers" class="headerlink" title="initMonacoWorkers"></a>initMonacoWorkers</h4><p><code>initMonacoWorkers</code> 方法位于 <code>src/utils/monacoConfig</code>：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> * <span class="keyword">as</span> monaco <span class="keyword">from</span> <span class="string">&#x27;monaco-editor&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> editorWorker <span class="keyword">from</span> <span class="string">&#x27;monaco-editor/esm/vs/editor/editor.worker?worker&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> jsonWorker <span class="keyword">from</span> <span class="string">&#x27;monaco-editor/esm/vs/language/json/json.worker?worker&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> cssWorker <span class="keyword">from</span> <span class="string">&#x27;monaco-editor/esm/vs/language/css/css.worker?worker&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> htmlWorker <span class="keyword">from</span> <span class="string">&#x27;monaco-editor/esm/vs/language/html/html.worker?worker&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> tsWorker <span class="keyword">from</span> <span class="string">&#x27;monaco-editor/esm/vs/language/typescript/ts.worker?worker&#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_">initMonacoWorkers</span>(<span class="params"></span>): <span class="built_in">void</span> &#123;</span><br><span class="line">  self.<span class="property">MonacoEnvironment</span> = &#123;</span><br><span class="line">    <span class="title function_">getWorker</span>(<span class="params">_, label</span>) &#123;</span><br><span class="line">      <span class="keyword">if</span> (label === <span class="string">&#x27;json&#x27;</span>) &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> <span class="title function_">jsonWorker</span>();</span><br><span class="line">      &#125;</span><br><span class="line">      <span class="keyword">if</span> (label === <span class="string">&#x27;css&#x27;</span> || label === <span class="string">&#x27;scss&#x27;</span> || label === <span class="string">&#x27;less&#x27;</span>) &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> <span class="title function_">cssWorker</span>();</span><br><span class="line">      &#125;</span><br><span class="line">      <span class="keyword">if</span> (label === <span class="string">&#x27;html&#x27;</span> || label === <span class="string">&#x27;handlebars&#x27;</span> || label === <span class="string">&#x27;razor&#x27;</span>) &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> <span class="title function_">htmlWorker</span>();</span><br><span class="line">      &#125;</span><br><span class="line">      <span class="keyword">if</span> (label === <span class="string">&#x27;typescript&#x27;</span> || label === <span class="string">&#x27;javascript&#x27;</span>) &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> <span class="title function_">tsWorker</span>();</span><br><span class="line">      &#125;</span><br><span class="line">      <span class="keyword">return</span> <span class="keyword">new</span> <span class="title function_">editorWorker</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="keyword">export</span> <span class="keyword">const</span> <span class="attr">defaultEditorOptions</span>: monaco.<span class="property">editor</span>.<span class="property">IStandaloneEditorConstructionOptions</span> =</span><br><span class="line">  &#123;</span><br><span class="line">    <span class="attr">readOnly</span>: <span class="literal">true</span>,</span><br><span class="line">    <span class="attr">minimap</span>: &#123; <span class="attr">enabled</span>: <span class="literal">false</span> &#125;,</span><br><span class="line">    <span class="attr">scrollBeyondLastLine</span>: <span class="literal">false</span>,</span><br><span class="line">    <span class="attr">fontSize</span>: <span class="number">14</span>,</span><br><span class="line">    <span class="attr">renderLineHighlight</span>: <span class="string">&#x27;all&#x27;</span>,</span><br><span class="line">    <span class="attr">scrollbar</span>: &#123;</span><br><span class="line">      <span class="attr">vertical</span>: <span class="string">&#x27;visible&#x27;</span>,</span><br><span class="line">      <span class="attr">horizontal</span>: <span class="string">&#x27;visible&#x27;</span>,</span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="attr">automaticLayout</span>: <span class="literal">true</span>,</span><br><span class="line">  &#125;;</span><br></pre></td></tr></table></figure><h2 id="主进程（src-main）"><a href="#主进程（src-main）" class="headerlink" title="主进程（src&#x2F;main）"></a>主进程（src&#x2F;main）</h2><h3 id="ipcRoutes"><a href="#ipcRoutes" class="headerlink" title="ipcRoutes"></a>ipcRoutes</h3><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; initIpc, createServer &#125; <span class="keyword">from</span> <span class="string">&#x27;@ui-tars/electron-ipc/main&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> ipcRoutes = t.<span class="title function_">router</span>(&#123;</span><br><span class="line">  ...agentRoute,</span><br><span class="line">  ...llmRoute,</span><br><span class="line">  <span class="comment">// 这里定义了 actionRoute</span></span><br><span class="line">  ...actionRoute,</span><br><span class="line">  ...browserRoute,</span><br><span class="line">  ...fileSystemRoute,</span><br><span class="line">  ...searchRoute,</span><br><span class="line">  ...settingsRoute,</span><br><span class="line">  ...mcpRoute,</span><br><span class="line">&#125;);</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> server = <span class="title function_">createServer</span>(ipcRoutes);</span><br></pre></td></tr></table></figure><h4 id="actionRoute"><a href="#actionRoute" class="headerlink" title="actionRoute"></a>actionRoute</h4><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">MCPServerName</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;@agent-infra/shared&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; initIpc &#125; <span class="keyword">from</span> <span class="string">&#x27;@ui-tars/electron-ipc/main&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> t = initIpc.<span class="title function_">create</span>();</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> actionRoute = t.<span class="title function_">router</span>(&#123;</span><br><span class="line">  <span class="attr">listTools</span>: t.<span class="property">procedure</span>.<span class="title function_">handle</span>(<span class="keyword">async</span> () =&gt; &#123;&#125;),</span><br><span class="line"></span><br><span class="line">  <span class="attr">listMcpTools</span>: t.<span class="property">procedure</span>.<span class="title function_">handle</span>(<span class="keyword">async</span> () =&gt; &#123;&#125;),</span><br><span class="line"></span><br><span class="line">  <span class="attr">listCustomTools</span>: t.<span class="property">procedure</span>.<span class="title function_">handle</span>(<span class="keyword">async</span> () =&gt; &#123;&#125;),</span><br><span class="line"></span><br><span class="line">  <span class="attr">executeTool</span>: t.<span class="property">procedure</span>.<span class="title function_">input</span>().<span class="title function_">handle</span>(<span class="keyword">async</span> () =&gt; &#123;&#125;),</span><br><span class="line"></span><br><span class="line">  <span class="attr">saveBrowserSnapshot</span>: t.<span class="property">procedure</span>.<span class="title function_">input</span>().<span class="title function_">handle</span>(<span class="keyword">async</span> () =&gt; &#123;</span><br><span class="line">    <span class="comment">// 通过 mcpClient 调用浏览器截图工具</span></span><br><span class="line">    <span class="keyword">const</span> result = <span class="keyword">await</span> mcpClient,<span class="title function_">callTool</span>(&#123;</span><br><span class="line">      <span class="attr">client</span>: <span class="title class_">MCPServerName</span>.<span class="property">Browser</span>,</span><br><span class="line">      <span class="attr">name</span>: <span class="string">&#x27;browser_screenshot&#x27;</span>,</span><br><span class="line">      <span class="attr">args</span>: &#123;</span><br><span class="line">        <span class="attr">highlight</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="keyword">const</span> screenshotMeta = (</span><br><span class="line">      result.<span class="property">content</span> <span class="keyword">as</span> [</span><br><span class="line">        &#123; <span class="attr">type</span>: <span class="string">&#x27;text&#x27;</span>; <span class="attr">text</span>: <span class="built_in">string</span> &#125;,</span><br><span class="line">        &#123; <span class="attr">type</span>: <span class="string">&#x27;image&#x27;</span>; <span class="attr">data</span>: <span class="built_in">string</span>; <span class="attr">mimeType</span>: <span class="built_in">string</span> &#125;,</span><br><span class="line">      ]</span><br><span class="line">    )[<span class="number">1</span>];</span><br><span class="line"></span><br><span class="line">    <span class="keyword">const</span> omegaDir = <span class="keyword">await</span> <span class="title function_">getOmegaDir</span>();</span><br><span class="line">    <span class="keyword">const</span> screenshotPath = path.<span class="title function_">join</span>(omegaDir, <span class="string">&#x27;screenshots&#x27;</span>);</span><br><span class="line">    <span class="keyword">await</span> fs.<span class="title function_">mkdirSync</span>(screenshotPath, &#123; <span class="attr">recursive</span>: <span class="literal">true</span> &#125;);</span><br><span class="line">    <span class="keyword">const</span> ext = screenshotMeta.<span class="property">mimeType</span>.<span class="title function_">split</span>(<span class="string">&#x27;/&#x27;</span>)[<span class="number">1</span>] || <span class="string">&#x27;png&#x27;</span>;</span><br><span class="line">    <span class="keyword">const</span> timestamp = <span class="keyword">new</span> <span class="title class_">Date</span>().<span class="title function_">getTime</span>();</span><br><span class="line">    <span class="keyword">const</span> filename = <span class="string">`screenshot_<span class="subst">$&#123;timestamp&#125;</span>.<span class="subst">$&#123;ext&#125;</span>`</span>;</span><br><span class="line">    <span class="keyword">const</span> filepath = path.<span class="title function_">join</span>(screenshotPath, filename);</span><br><span class="line">    <span class="keyword">const</span> imageBuffer = <span class="title class_">Buffer</span>.<span class="title function_">from</span>(screenshotMeta.<span class="property">data</span>, <span class="string">&#x27;base64&#x27;</span>);</span><br><span class="line">    <span class="keyword">await</span> fs.<span class="title function_">writeFile</span>(filepath, imageBuffer);</span><br><span class="line">    <span class="keyword">return</span> &#123; filepath &#125;;</span><br><span class="line">  &#125;),</span><br><span class="line"></span><br><span class="line">  <span class="attr">saveReportHtml</span>: t.<span class="property">procedure</span>.<span class="title function_">input</span>().<span class="title function_">handle</span>(<span class="keyword">async</span> (&#123; input &#125;) =&gt; &#123;&#125;),</span><br><span class="line"></span><br><span class="line">  <span class="attr">cleanup</span>: t.<span class="property">procedure</span>.<span class="title function_">handle</span>(<span class="keyword">async</span> () =&gt; &#123;&#125;)</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><h3 id="mcp-client"><a href="#mcp-client" class="headerlink" title="mcp client"></a>mcp client</h3><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">MCPServerName</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;@agent-infra/shared&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 获取OmegaDir</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> <span class="title function_">getOmegaDir</span> = <span class="keyword">async</span> (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">  <span class="comment">// Create working directory in user&#x27;s home directory.</span></span><br><span class="line">  <span class="keyword">const</span> omegaDir = path.<span class="title function_">join</span>(os.<span class="title function_">homedir</span>(), <span class="string">&#x27;.omega&#x27;</span>);</span><br><span class="line">  <span class="keyword">if</span> (!fs.<span class="title function_">existsSync</span>(omegaDir)) &#123;</span><br><span class="line">    <span class="keyword">await</span> fs.<span class="title function_">mkdir</span>(omegaDir, &#123; <span class="attr">recursive</span>: <span class="literal">true</span> &#125;);</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">return</span> omegaDir;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="comment">//  <span class="doctag">TODO:</span> 这里我很奇为什么不直接用 import，有空和三元大佬请教下</span></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">dynamicImport</span> = (<span class="params">url</span>) =&gt; <span class="keyword">new</span> <span class="title class_">Function</span>(<span class="string">`return import(&#x27;<span class="subst">$&#123;url&#125;</span>&#x27;)`</span>)();</span><br><span class="line"></span><br><span class="line"><span class="comment">// 实现一个基于 commands、filesystem 和 browser 的 MCP 客户端</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> <span class="title function_">createMcpClient</span> = <span class="keyword">async</span> (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">  <span class="comment">// 动态导入 mcp server 模块</span></span><br><span class="line">  <span class="keyword">const</span> commandModule = <span class="keyword">await</span> <span class="title function_">dynamicImport</span>(<span class="string">&#x27;@agent-infra/mcp-server-commands&#x27;</span>);</span><br><span class="line">  <span class="keyword">const</span> fsModule = <span class="keyword">await</span> <span class="title function_">dynamicImport</span>(<span class="string">&#x27;@agent-infra/mcp-server-filesystem&#x27;</span>);</span><br><span class="line">  <span class="keyword">const</span> browserModule = <span class="keyword">await</span> <span class="title function_">dynamicImport</span>(<span class="string">&#x27;@agent-infra/mcp-server-browser&#x27;</span>);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> &#123; <span class="attr">client</span>: commandClient &#125; = commandModule.<span class="property">default</span>;</span><br><span class="line">  <span class="keyword">const</span> &#123; <span class="attr">client</span>: fsClient, setAllowedDirectories &#125; = fsModule.<span class="property">default</span>;</span><br><span class="line">  <span class="keyword">const</span> &#123; <span class="attr">client</span>: browserClient &#125; = browserModule.<span class="property">default</span>;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 共享的 fClientModule 模块</span></span><br><span class="line">  fsClientModule = fsModule.<span class="property">default</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> omegaDir = <span class="keyword">await</span> <span class="title function_">getOmegaDir</span>();</span><br><span class="line">  <span class="title function_">setAllowedDirectories</span>([omegaDir]);</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 工具 map</span></span><br><span class="line">  <span class="keyword">const</span> toolsMap = &#123;</span><br><span class="line">    [<span class="title class_">MCPServerName</span>.<span class="property">FileSystem</span>]: &#123;</span><br><span class="line">      <span class="attr">name</span>: <span class="title class_">MCPServerName</span>.<span class="property">FileSystem</span>,</span><br><span class="line">      <span class="attr">description</span>: <span class="string">&#x27;filesystem tool&#x27;</span>,</span><br><span class="line">      <span class="attr">localClient</span>: fsClient,</span><br><span class="line">    &#125;,</span><br><span class="line">    [<span class="title class_">MCPServerName</span>.<span class="property">Commands</span>]: &#123;</span><br><span class="line">      <span class="attr">name</span>: <span class="title class_">MCPServerName</span>.<span class="property">Commands</span>,</span><br><span class="line">      <span class="attr">description</span>: <span class="string">&#x27;commands tool&#x27;</span>,</span><br><span class="line">      <span class="attr">localClient</span>: commandClient,</span><br><span class="line">    &#125;,</span><br><span class="line">    [<span class="title class_">MCPServerName</span>.<span class="property">Browser</span>]: &#123;</span><br><span class="line">      <span class="attr">name</span>: <span class="title class_">MCPServerName</span>.<span class="property">Browser</span>,</span><br><span class="line">      <span class="attr">local</span>: <span class="literal">true</span>,</span><br><span class="line">      <span class="attr">description</span>: <span class="string">&#x27;browser tools&#x27;</span>,</span><br><span class="line">      <span class="attr">localClient</span>: browserClient,</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;;</span><br><span class="line"></span><br><span class="line">  logger.<span class="title function_">info</span>(<span class="string">&#x27;toolsMap&#x27;</span>, toolsMap);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> client = <span class="keyword">new</span> <span class="title class_">MCPClient</span>(<span class="title class_">Object</span>.<span class="title function_">values</span>(toolsMap));</span><br><span class="line">  mapClientRef.<span class="property">current</span> = client;</span><br><span class="line">  <span class="keyword">return</span> client;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><h2 id="agent-infra-shared"><a href="#agent-infra-shared" class="headerlink" title="@agent-infra/shared"></a><code>@agent-infra/shared</code></h2><p>TODO</p><h2 id="agent-infra-mcp-client"><a href="#agent-infra-mcp-client" class="headerlink" title="@agent-infra/mcp-client"></a><code>@agent-infra/mcp-client</code></h2><p>基于 <code>@modelcontextprotocol/sdk/client/index.js</code> 和 <code>@modelcontextprotocol/sdk/client/sse.js</code> 实现的 MCP 客户端。负责管理 MCP 服务器和工具调用。</p><h2 id="agent-infra-mcp-server-browser"><a href="#agent-infra-mcp-server-browser" class="headerlink" title="@agent-infra/mcp-server-browser"></a><code>@agent-infra/mcp-server-browser</code></h2><blockquote><p>模块位于 <code>packages/agent-infra/mcp-servers/browser</code></p></blockquote><h3 id="server-ts"><a href="#server-ts" class="headerlink" title="server.ts"></a>server.ts</h3><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br><span class="line">128</span><br><span class="line">129</span><br><span class="line">130</span><br><span class="line">131</span><br><span class="line">132</span><br><span class="line">133</span><br><span class="line">134</span><br><span class="line">135</span><br><span class="line">136</span><br><span class="line">137</span><br><span class="line">138</span><br><span class="line">139</span><br><span class="line">140</span><br><span class="line">141</span><br><span class="line">142</span><br><span class="line">143</span><br><span class="line">144</span><br><span class="line">145</span><br><span class="line">146</span><br><span class="line">147</span><br><span class="line">148</span><br><span class="line">149</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123;</span><br><span class="line">  getBuildDomTreeScript,</span><br><span class="line">  parseNode,</span><br><span class="line">  <span class="keyword">type</span> <span class="title class_">RawDomTreeNode</span>,</span><br><span class="line">  <span class="title class_">DOMElementNode</span>,</span><br><span class="line">  createSelectorMap,</span><br><span class="line">  removeHighlights,</span><br><span class="line">  waitForPageAndFramesLoad,</span><br><span class="line">  locateElement,</span><br><span class="line">  scrollIntoViewIfNeeded,</span><br><span class="line">&#125; <span class="keyword">from</span> <span class="string">&#x27;@agent-infra/browser-use&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">let</span> <span class="attr">globalBrowser</span>: <span class="title class_">LocalBrowser</span>[<span class="string">&#x27;browser&#x27;</span>] | <span class="literal">undefined</span>;</span><br><span class="line"><span class="keyword">let</span> <span class="attr">globalPage</span>: <span class="title class_">Page</span> | <span class="literal">undefined</span>;</span><br><span class="line"><span class="keyword">let</span> <span class="attr">selectorMap</span>: <span class="title class_">Map</span>&lt;<span class="built_in">number</span>, <span class="title class_">DOMElementNode</span>&gt; | <span class="literal">undefined</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> screenshots = <span class="keyword">new</span> <span class="title class_">Map</span>&lt;<span class="built_in">string</span>, <span class="built_in">string</span>&gt;();</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> <span class="title function_">getScreenshots</span> = (<span class="params"></span>) =&gt; screenshots;</span><br><span class="line"></span><br><span class="line"><span class="keyword">async</span> <span class="keyword">function</span> <span class="title function_">buildDomTree</span>(<span class="params">page: Page</span>) &#123;</span><br><span class="line">  <span class="keyword">try</span> &#123;</span><br><span class="line">    <span class="keyword">const</span> rawDomTree = <span class="keyword">await</span> page.evaluate(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">      <span class="comment">// Access buildDomTree from the window context of the target page</span></span><br><span class="line">      <span class="keyword">return</span> <span class="variable language_">window</span>.<span class="title function_">buildDomTree</span>(&#123;</span><br><span class="line">        <span class="attr">doHighlightElements</span>: <span class="literal">true</span>,</span><br><span class="line">        <span class="attr">focusHighlightIndex</span>: -<span class="number">1</span>,</span><br><span class="line">        <span class="attr">viewportExpansion</span>: <span class="number">0</span>,</span><br><span class="line">      &#125;);</span><br><span class="line">    &#125;);</span><br><span class="line">    <span class="keyword">if</span> (rawDomTree !== <span class="literal">null</span>) &#123;</span><br><span class="line">      <span class="keyword">const</span> elementTree = <span class="title function_">parseNode</span>(rawDomTree <span class="keyword">as</span> <span class="title class_">RawDomTreeNode</span>);</span><br><span class="line">      <span class="keyword">if</span> (elementTree !== <span class="literal">null</span> &amp;&amp; elementTree <span class="keyword">instanceof</span> <span class="title class_">DOMElementNode</span>) &#123;</span><br><span class="line">        <span class="keyword">const</span> clickableElements = elementTree.<span class="title function_">clickableElementsToString</span>();</span><br><span class="line">        selectorMap = <span class="title function_">createSelectorMap</span>(elementTree);</span><br><span class="line"></span><br><span class="line">        <span class="keyword">return</span> &#123;</span><br><span class="line">          clickableElements,</span><br><span class="line">          elementTree,</span><br><span class="line">          selectorMap,</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> <span class="literal">null</span>;</span><br><span class="line">  &#125; <span class="keyword">catch</span> (error) &#123;</span><br><span class="line">    logger.<span class="title function_">error</span>(<span class="string">&#x27;Error building DOM tree:&#x27;</span>, error);</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">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="attr">handleToolCall</span>: <span class="title class_">Client</span>[<span class="string">&#x27;callTool&#x27;</span>] = <span class="keyword">async</span> (&#123;</span><br><span class="line">  name,</span><br><span class="line">  <span class="attr">arguments</span>: toolArgs,</span><br><span class="line">&#125;) =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> initialBrowser = <span class="keyword">await</span> <span class="title function_">setInitialBrowser</span>();</span><br><span class="line">  <span class="keyword">const</span> &#123; browser &#125; = initialBrowser;</span><br><span class="line">  <span class="keyword">let</span> &#123; page &#125; = initialBrowser;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (!page) &#123;</span><br><span class="line">    <span class="keyword">return</span> &#123;</span><br><span class="line">      <span class="attr">content</span>: [&#123; <span class="attr">type</span>: <span class="string">&#x27;text&#x27;</span>, <span class="attr">text</span>: <span class="string">&#x27;Page not found&#x27;</span> &#125;],</span><br><span class="line">      <span class="attr">isError</span>: <span class="literal">true</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> handlers = &#123;</span><br><span class="line">    <span class="comment">//  浏览器后退</span></span><br><span class="line">    <span class="attr">browser_go_back</span>: <span class="keyword">async</span> () =&gt; &#123;</span><br><span class="line">      <span class="keyword">await</span> <span class="title class_">Promise</span>.<span class="title function_">all</span>([<span class="title function_">waitForPageAndFramesLoad</span>(page), page.<span class="title function_">goBack</span>()]);</span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="comment">//  浏览器前进</span></span><br><span class="line">    <span class="attr">browser_go_forward</span>: <span class="keyword">async</span> () =&gt; &#123;</span><br><span class="line">      <span class="keyword">await</span> <span class="title class_">Promise</span>.<span class="title function_">all</span>([<span class="title function_">waitForPageAndFramesLoad</span>(page), page.<span class="title function_">goForward</span>()]);</span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="attr">browser_navigate</span>: <span class="keyword">async</span> () =&gt; &#123;</span><br><span class="line">      <span class="keyword">await</span> <span class="title class_">Promise</span>.<span class="title function_">all</span>([</span><br><span class="line">          <span class="title function_">waitForPageAndFramesLoad</span>(page),</span><br><span class="line">          page.<span class="title function_">goto</span>(args.<span class="property">url</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> &#123; clickableElements &#125; = (<span class="keyword">await</span> <span class="title function_">buildDomTree</span>(page)) || &#123;&#125;;</span><br><span class="line">      <span class="keyword">return</span> &#123;</span><br><span class="line">          <span class="attr">content</span>: [</span><br><span class="line">            &#123;</span><br><span class="line">              <span class="attr">type</span>: <span class="string">&#x27;text&#x27;</span>,</span><br><span class="line">              <span class="attr">text</span>: <span class="string">`Navigated to <span class="subst">$&#123;args.url&#125;</span>\nclickable elements: <span class="subst">$&#123;clickableElements&#125;</span>`</span>,</span><br><span class="line">            &#125;,</span><br><span class="line">          ],</span><br><span class="line">          <span class="attr">isError</span>: <span class="literal">false</span>,</span><br><span class="line">        &#125;;</span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="attr">browser_screenshot</span>: <span class="keyword">async</span> (args) =&gt; &#123;</span><br><span class="line">      <span class="keyword">if</span> (args.<span class="property">highlight</span>) &#123;</span><br><span class="line">        <span class="keyword">await</span> <span class="title function_">buildDomTree</span>(page);</span><br><span class="line">      &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        <span class="keyword">await</span> <span class="title function_">removeHighlights</span>(page);</span><br><span class="line">      &#125;</span><br><span class="line">      <span class="keyword">const</span> width = args.<span class="property">width</span> ?? page.<span class="title function_">viewport</span>()?.<span class="property">width</span> ?? <span class="number">800</span>;</span><br><span class="line">      <span class="keyword">const</span> height = args.<span class="property">height</span> ?? page.<span class="title function_">viewport</span>()?.<span class="property">height</span> ?? <span class="number">600</span>;</span><br><span class="line">      <span class="keyword">await</span> page.<span class="title function_">setViewport</span>(&#123; width, height &#125;);</span><br><span class="line">      <span class="keyword">const</span> screenshot = <span class="keyword">await</span> (args.<span class="property">selector</span></span><br><span class="line">        ? (<span class="keyword">await</span> page.$(args.<span class="property">selector</span>))?.<span class="title function_">screenshot</span>(&#123; <span class="attr">encoding</span>: <span class="string">&#x27;base64&#x27;</span> &#125;)</span><br><span class="line">        : page.<span class="title function_">screenshot</span>(&#123; <span class="attr">encoding</span>: <span class="string">&#x27;base64&#x27;</span>, <span class="attr">fullPage</span>: <span class="literal">false</span> &#125;));</span><br><span class="line">      screenshots.<span class="title function_">set</span>(args.<span class="property">name</span>, screenshot <span class="keyword">as</span> <span class="built_in">string</span>);</span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="attr">browser_get_clickable_elements</span>: <span class="keyword">async</span> () =&gt; &#123;</span><br><span class="line">      <span class="keyword">const</span> &#123; clickableElements &#125; = (<span class="keyword">await</span> <span class="title function_">buildDomTree</span>(page)) || &#123;&#125;;</span><br><span class="line">      <span class="keyword">return</span> &#123;</span><br><span class="line">        <span class="attr">content</span>: [</span><br><span class="line">          &#123;</span><br><span class="line">            <span class="attr">type</span>: <span class="string">&#x27;text&#x27;</span>,</span><br><span class="line">            <span class="attr">text</span>: clickableElements,</span><br><span class="line">          &#125;,</span><br><span class="line">        ],</span><br><span class="line">        <span class="attr">isError</span>: <span class="literal">false</span>,</span><br><span class="line">      &#125;;</span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="attr">browser_click</span>: <span class="title function_">async</span>() =&gt; &#123;</span><br><span class="line">      <span class="keyword">const</span> elementNode = selectorMap?.<span class="title function_">get</span>(<span class="title class_">Number</span>(args?.<span class="property">index</span>));</span><br><span class="line">      <span class="keyword">const</span> element = <span class="keyword">await</span> <span class="title function_">locateElement</span>(page, elementNode!);</span><br><span class="line">      <span class="keyword">await</span> <span class="title function_">scrollIntoViewIfNeeded</span>(element);</span><br><span class="line"></span><br><span class="line">      <span class="keyword">await</span> <span class="title class_">Promise</span>.<span class="title function_">race</span>([</span><br><span class="line">        element.<span class="title function_">click</span>(),</span><br><span class="line">        <span class="keyword">new</span> <span class="title class_">Promise</span>(<span class="function">(<span class="params">_, reject</span>) =&gt;</span></span><br><span class="line">          <span class="built_in">setTimeout</span>(<span class="function">() =&gt;</span> <span class="title function_">reject</span>(<span class="keyword">new</span> <span class="title class_">Error</span>(<span class="string">&#x27;Click timeout&#x27;</span>)), <span class="number">5000</span>),</span><br><span class="line">        ),</span><br><span class="line">      ]);</span><br><span class="line">      <span class="keyword">return</span> &#123;</span><br><span class="line">        <span class="attr">content</span>: [</span><br><span class="line">          &#123;</span><br><span class="line">            <span class="attr">type</span>: <span class="string">&#x27;text&#x27;</span>,</span><br><span class="line">            <span class="attr">text</span>: <span class="string">`Clicked element: <span class="subst">$&#123;args.index&#125;</span>`</span>,</span><br><span class="line">          &#125;,</span><br><span class="line">        ],</span><br><span class="line">        <span class="attr">isError</span>: <span class="literal">false</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="keyword">if</span> (handlers[name]) &#123;</span><br><span class="line">    <span class="keyword">return</span> handlers[name](toolArgs);</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">const</span> <span class="attr">client</span>: <span class="title class_">Pick</span>&lt;<span class="title class_">Client</span>, <span class="string">&#x27;callTool&#x27;</span> | <span class="string">&#x27;listTools&#x27;</span> | <span class="string">&#x27;close&#x27;</span> | <span class="string">&#x27;ping&#x27;</span>&gt; =</span><br><span class="line">  &#123;</span><br><span class="line">    <span class="attr">callTool</span>: handleToolCall,</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><ul><li><a href="https://www.rustc.cloud/mcp">基于 MCP 的 AI Agent 应用开发实践</a></li></ul>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/b9eba41ae7e3/</id>
    <link href="https://blog.zisheng.pro/posts/b9eba41ae7e3/"/>
    <published>2025-04-13T12:14:59.000Z</published>
    <summary>深入阅读字节跳动开源的 agent-tars 源码，剖析其基于 Electron 的双进程架构、Aware/Executor/AgentFlow 三层 Agent 循环设计，以及 MCP 工具调用与事件管理机制。</summary>
    <title>字节 agent-tars 技术揭秘（硬啃）</title>
    <updated>2026-09-18T08:15:28.640Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="投资理财" scheme="https://blog.zisheng.pro/categories/%E6%8A%95%E8%B5%84%E7%90%86%E8%B4%A2/"/>
    <category term="QDII" scheme="https://blog.zisheng.pro/tags/QDII/"/>
    <category term="理财" scheme="https://blog.zisheng.pro/tags/%E7%90%86%E8%B4%A2/"/>
    <category term="定投" scheme="https://blog.zisheng.pro/tags/%E5%AE%9A%E6%8A%95/"/>
    <category term="指数基金" scheme="https://blog.zisheng.pro/tags/%E6%8C%87%E6%95%B0%E5%9F%BA%E9%87%91/"/>
    <content>
      <![CDATA[<h2 id="QDII-基金是什么？"><a href="#QDII-基金是什么？" class="headerlink" title="QDII 基金是什么？"></a>QDII 基金是什么？</h2><p>QDII（Qualified Domestic Institutional Investor，合格境内机构投资者）是中国在资本项目未完全开放的背景下推出的跨境投资机制，允许境内投资者通过持牌机构间接参与境外证券市场（如美股、港股等）。其核心特点包括：</p><ol><li><strong>​投资范围广</strong>：可配置股票、债券、大宗商品等全球资产；</li><li><strong>汇率风险</strong>：需人民币兑换外币投资，收益受汇率波动影响；</li><li><strong>制度优势</strong>：突破个人换汇限额（5万美元&#x2F;年），提供合规境外投资渠道。</li></ol><h2 id="小白无脑定投产品推荐"><a href="#小白无脑定投产品推荐" class="headerlink" title="小白无脑定投产品推荐"></a>小白无脑定投产品推荐</h2><p>对于新手投资者，以下三类 QDII 基金及策略兼顾低风险与易操作性，适合长期定投：</p><h3 id="宽基指数型-QDII（50-）"><a href="#宽基指数型-QDII（50-）" class="headerlink" title="宽基指数型 QDII（50%）"></a>宽基指数型 QDII（50%）</h3><ul><li><strong>标普 500 指数基金</strong>（如易方达标普 500 指数基金）<br>追踪美股大盘，覆盖苹果、微软等龙头企业，管理费0.8%，2024年收益率达21.5%</li><li><strong>纳斯达克100 ETF</strong>（如华夏纳斯达克100 ETF）<br>聚焦科技巨头（如英伟达、亚马逊），受益于AI和数字经济浪潮，2024年溢价率需关注</li></ul><h3 id="中概股主题-QDII（30-）"><a href="#中概股主题-QDII（30-）" class="headerlink" title="中概股主题 QDII（30%）"></a>中概股主题 QDII（30%）</h3><ul><li><strong>中概互联网ETF</strong>（如易方达中概互联50）<br>一篮子持有阿里、腾讯等海外上市中国科技公司，估值处于历史低位，适合逢低布局</li></ul><h3 id="低波动稳健型QDII（20-）"><a href="#低波动稳健型QDII（20-）" class="headerlink" title="低波动稳健型QDII（20%）"></a>低波动稳健型QDII（20%）</h3><ul><li><strong>全球债券型QDII</strong>（如摩根全球新兴市场债券）<br>配置海外高评级债券，年化收益约4%-5%，波动率低于股票型基金。</li><li><strong>红利低波ETF</strong>（如招商中证红利ETF）<br>高股息策略，在降息周期中提供稳定现金流，适合风险厌恶型投资者。</li></ul><h3 id="定投实操建议"><a href="#定投实操建议" class="headerlink" title="定投实操建议"></a>定投实操建议</h3><ul><li>账户选择：通过支付宝、天天基金等平台直接购买，支持10元起投；</li><li>频率与金额：建议月定投，分配比例示例：50%宽基指数（如标普500）+30%中概股+20%债券基金；</li><li>​风险对冲：<ul><li>人民币升值周期加大换汇比例，降低汇率损失；</li><li>避免高溢价基金（部分原油、科技类QDII溢价超10%）。</li></ul></li></ul><h3 id="注意事项"><a href="#注意事项" class="headerlink" title="注意事项"></a>注意事项</h3><ul><li><strong>​申赎规则</strong>：多数 QDII 为 T+2 确认份额，赎回到账需 3-5 个工作日；</li><li><strong>​费用控制</strong>：选择管理费低于 0.5% 的产品，避免侵蚀长期收益；</li><li><strong>​长期视角</strong>：定投周期建议 3 年以上，穿越市场周期获取复利效应。</li></ul><h3 id="ETF-是什么？"><a href="#ETF-是什么？" class="headerlink" title="ETF 是什么？"></a>ETF 是什么？</h3><p>ETF（Exchange-Traded Fund），全称“交易型开放式指数基金”，是一种在证券交易所上市交易的开放式基金，结合了股票和基金的优点，通过跟踪特定指数、商品或资产组合的表现，为投资者提供高效、低成本的市场参与工具。</p>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/38c2eb16ce85/</id>
    <link href="https://blog.zisheng.pro/posts/38c2eb16ce85/"/>
    <published>2025-03-27T20:39:55.000Z</published>
    <summary>QDII（合格境内机构投资者）是普通投资者参与美股、港股等境外市场的合规渠道。本文系统介绍 QDII 基金的核心机制，并推荐适合新手的宽基指数、中概股及低波动三类定投组合。</summary>
    <title>QDII 是什么？懒人无脑定投的产品有哪些？</title>
    <updated>2026-09-18T08:15:28.632Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="后端" scheme="https://blog.zisheng.pro/categories/%E5%90%8E%E7%AB%AF/"/>
    <category term="Java" scheme="https://blog.zisheng.pro/categories/%E5%90%8E%E7%AB%AF/Java/"/>
    <category term="macOS" scheme="https://blog.zisheng.pro/tags/macOS/"/>
    <category term="Java" scheme="https://blog.zisheng.pro/tags/Java/"/>
    <category term="Maven" scheme="https://blog.zisheng.pro/tags/Maven/"/>
    <content>
      <![CDATA[<h2 id="Java"><a href="#Java" class="headerlink" title="Java"></a>Java</h2><h3 id="下载安装"><a href="#下载安装" class="headerlink" title="下载安装"></a>下载安装</h3><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">brew install --cask zulu@17</span><br></pre></td></tr></table></figure><h3 id="实用命令"><a href="#实用命令" class="headerlink" title="实用命令"></a>实用命令</h3><h4 id="查看-jdk-版本及其安装目录"><a href="#查看-jdk-版本及其安装目录" class="headerlink" title="查看 jdk 版本及其安装目录"></a>查看 jdk 版本及其安装目录</h4><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">$ /usr/libexec/java_home -V</span><br><span class="line">Matching Java Virtual Machines (1):</span><br><span class="line">    17.0.14 (arm64) <span class="string">&quot;Azul Systems, Inc.&quot;</span> - <span class="string">&quot;Zulu 17.56.15&quot;</span> /Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home</span><br><span class="line">/Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home</span><br></pre></td></tr></table></figure><h4 id="查看-jre-版本"><a href="#查看-jre-版本" class="headerlink" title="查看 jre 版本"></a>查看 jre 版本</h4><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">$ java -version</span><br><span class="line">openjdk 17.0.14 2025-01-21 LTS</span><br><span class="line">OpenJDK Runtime Environment Zulu17.56+15-CA (build 17.0.14+7-LTS)</span><br><span class="line">OpenJDK 64-Bit Server VM Zulu17.56+15-CA (build 17.0.14+7-LTS, mixed mode, sharing)</span><br></pre></td></tr></table></figure><h4 id="查看-jdk-版本"><a href="#查看-jdk-版本" class="headerlink" title="查看 jdk 版本"></a>查看 jdk 版本</h4><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">$ javac --version</span><br><span class="line">javac 17.0.14</span><br></pre></td></tr></table></figure><h2 id="Maven"><a href="#Maven" class="headerlink" title="Maven"></a>Maven</h2><h3 id="安装"><a href="#安装" class="headerlink" title="安装"></a>安装</h3><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ brew install maven</span><br></pre></td></tr></table></figure><h3 id="配置阿里镜像"><a href="#配置阿里镜像" class="headerlink" title="配置阿里镜像"></a>配置阿里镜像</h3><p><code>setting.xml</code> 路径为 <code>${Maven Home}/conf/settings.xml</code> ，Maven Home 可以通过 <code>mvn --version</code></p><p>可将 <code>settings.xml</code> 直接拷贝到 <code>.m2</code> 文件夹下，进行配置。</p><p>如果没有 <code>.m2</code> 文件夹时,运行命令</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ mvn <span class="built_in">help</span>:system</span><br></pre></td></tr></table></figure><p>然后打开当前用户的目录，可以在其中找到 <code>.m2</code> 文件夹，在 <code>settings.xml</code> 的 <code>&lt;mirrors&gt;</code> 标签内添加：</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">mirror</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">id</span>&gt;</span>alimaven<span class="tag">&lt;/<span class="name">id</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">name</span>&gt;</span>aliyun maven<span class="tag">&lt;/<span class="name">name</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">url</span>&gt;</span>http://maven.aliyun.com/nexus/content/groups/public/<span class="tag">&lt;/<span class="name">url</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">mirrorOf</span>&gt;</span>central<span class="tag">&lt;/<span class="name">mirrorOf</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">mirror</span>&gt;</span></span><br></pre></td></tr></table></figure><h3 id="升级-Maven"><a href="#升级-Maven" class="headerlink" title="升级 Maven"></a>升级 Maven</h3><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">$ brew update</span><br><span class="line">$ brew install maven</span><br></pre></td></tr></table></figure>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/93892f1c6e2d/</id>
    <link href="https://blog.zisheng.pro/posts/93892f1c6e2d/"/>
    <published>2025-03-06T23:38:26.000Z</published>
    <summary>介绍如何在 Mac（Apple Silicon）上通过 Homebrew 安装 Zulu JDK 17、配置 Maven 阿里云镜像，快速搭建 Java 开发环境。</summary>
    <title>在 Mac 上搭建 Java 开发环境</title>
    <updated>2026-09-18T08:15:28.636Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="浏览器" scheme="https://blog.zisheng.pro/categories/%E6%B5%8F%E8%A7%88%E5%99%A8/"/>
    <category term="macOS" scheme="https://blog.zisheng.pro/tags/macOS/"/>
    <category term="v8" scheme="https://blog.zisheng.pro/tags/v8/"/>
    <category term="安装" scheme="https://blog.zisheng.pro/tags/%E5%AE%89%E8%A3%85/"/>
    <content>
      <![CDATA[<blockquote><p>不要使用 <code>brew install v8</code>，因为可用命令是不完整的。</p></blockquote><h2 id="预备条件"><a href="#预备条件" class="headerlink" title="预备条件"></a>预备条件</h2><ul><li>安装 Xcode (Avaliable on the Mac App Store)</li><li>安装 Xcode Command Line Tools (Preferences &gt; Downloads)</li><li>安装 <a href="https://www.chromium.org/developers/how-tos/install-depot-tools">depot_tools</a><ul><li><code>cd ~ &amp;&amp; git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git</code></li><li><code>nano ~/.zshrc</code></li><li>添加 <code>export PATH=~/depot_tools:&quot;$PATH&quot;</code></li><li><code>source ~/.zshrc</code></li></ul></li><li>从你想要安装 V8 的目录中，运行 <code>gclient</code>。</li></ul><h2 id="Build-V8"><a href="#Build-V8" class="headerlink" title="Build V8"></a>Build V8</h2><ul><li><code>fetch v8</code></li><li><code>cd ~/v8</code></li><li><code>gclient sync</code></li></ul><p>Intel Macs:</p><ul><li><code>tools/dev/v8gen.py x64.optdebug</code></li><li><code>ninja -C out.gn/x64.optdebug</code></li></ul><p>ARM(M1) Macs:</p><ul><li><code>tools/dev/v8gen.py arm64.optdebug</code></li><li><code>ninja -C out/arm64.optdebug</code></li></ul><p>我建议你将这些添加到你的 <code>.zshrc</code> 文件中。</p><ul><li><code>sudo nano ~/.zshrc</code></li><li>添加 <code>alias d8=~/v8/out/arm64.optdebug/d8</code></li><li>添加 <code>alias tick-processor=~/v8/tools/mac-tick-processor</code></li><li>添加 <code>export D8_PATH=&quot;~/v8/out/arm64.optdebug&quot;</code></li><li><code>source ~/.zshrc</code></li></ul><h2 id="d8-shell-examples"><a href="#d8-shell-examples" class="headerlink" title="d8 shell examples"></a>d8 shell examples</h2><p>Note: many of these examples have become outdated as <code>v8</code> continues to evolve. Hoping to update them in the near future, but for now please be aware that some may not work as expected or may reference optimizations&#x2F;deoptimizations that no longer happen.</p><h3 id="Print-optimization-stats"><a href="#Print-optimization-stats" class="headerlink" title="Print optimization stats"></a>Print optimization stats</h3><p>Create <code>test.js</code> with the following code:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">test</span>(<span class="params"> obj </span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> obj.<span class="property">prop</span> + obj.<span class="property">prop</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> a = &#123; <span class="attr">prop</span>: <span class="string">&#x27;a&#x27;</span> &#125;, i = <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">while</span> ( i++ &lt; <span class="number">10000</span> ) &#123;</span><br><span class="line">  <span class="title function_">test</span>( a );</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>Run <code>$ d8 --trace-opt-verbose test.js</code></p><p>You should see that the <code>test</code> function was optimized by V8, along with an explanation of why. “ICs” stands for <a href="http://en.wikipedia.org/wiki/Inline_caching">inline caches</a> – and are one of the ways that V8 performs optimizations. Generally speaking, the more “ICs with typeinfo” the better.</p><p>Now modify <code>test.js</code> to include the following code:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">test</span>(<span class="params"> obj </span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> obj.<span class="property">prop</span> + obj.<span class="property">prop</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> a = &#123; <span class="attr">prop</span>: <span class="string">&#x27;a&#x27;</span> &#125;, b = &#123; <span class="attr">prop</span>: [] &#125;, i = <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">while</span> ( i++ &lt; <span class="number">10000</span> ) &#123;</span><br><span class="line">  <span class="title function_">test</span>( <span class="title class_">Math</span>.<span class="title function_">random</span>() &gt; <span class="number">0.5</span> ? a : b );</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>Run <code>$ d8 --trace-opt-verbose test.js</code></p><p>So, you’ll see that this time, the <code>test</code> function was never actually optimized. And the reason for that is because it’s being passed objects with different <a href="http://blog.twokul.io/hidden-classes-in-javascript-and-inline-caching/">hidden classes</a>. Try changing the value of <code>prop</code> in <code>a</code> to an integer and run it again. You should see that the function was able to be optimized.</p><h3 id="Print-deoptimization-stats"><a href="#Print-deoptimization-stats" class="headerlink" title="Print deoptimization stats"></a>Print deoptimization stats</h3><p>Modify the contents of <code>test.js</code>:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">test</span>(<span class="params"> obj </span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> obj.<span class="property">prop</span> + obj.<span class="property">prop</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> a = &#123; <span class="attr">prop</span>: <span class="string">&#x27;a&#x27;</span> &#125;, b = &#123; <span class="attr">prop</span>: [] &#125;, i = <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">while</span> ( i++ &lt; <span class="number">10000</span> ) &#123;</span><br><span class="line">  <span class="title function_">test</span>( i !== <span class="number">8000</span> ? a : b );</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>Run <code>$ d8 --trace-opt --trace-deopt test.js</code></p><p>You should see that the optimized code for the <code>test</code> function was thrown out. What happened here was that V8 kept seeing <code>test</code> being passed an object that looked like <code>{prop: &lt;String&gt;}</code>. But on the 8000th round of the while loop, we gave it something different. So V8 had to throw away the optimized code, because its initial assumptions were wrong.</p><h3 id="Profiling"><a href="#Profiling" class="headerlink" title="Profiling"></a>Profiling</h3><p>Modify <code>test.js</code>:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">factorial</span>(<span class="params"> n </span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> n === <span class="number">1</span> ? n : n * <span class="title function_">factorial</span>( --n );</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> i = <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">while</span> ( i++ &lt; <span class="number">1e7</span> ) &#123;</span><br><span class="line">  <span class="title function_">factorial</span>( <span class="number">10</span> );</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>Run <code>$ time d8 --prof test.js</code> (Generates <code>v8.log</code>)</p><p>Run <code>$ tick-processor</code> (Reads <code>v8.log</code> and <code>cat</code>s the parsed output)</p><p>This’ll show you where the program was spending most of its time, by function. Most of it should be under <code>LazyCompile: *factorial test.js:1:19</code>. The asterisk before the function name means that it was optimized.</p><p>Make a note of the execution time that was logged to the terminal. Now try modifying the code to this dumb, contrived example:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">factorial</span>(<span class="params"> n </span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="title function_">equal</span>( n, <span class="number">1</span> ) ? n : <span class="title function_">multiply</span>( n, <span class="title function_">factorial</span>( --n ) );</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_">multiply</span>(<span class="params"> x, y </span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> x * y;</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_">equal</span>(<span class="params"> a, b </span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> a === b;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> i = <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">while</span> ( i++ &lt; <span class="number">1e7</span> ) &#123;</span><br><span class="line">  <span class="title function_">factorial</span>( <span class="number">10</span> );</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>Run <code>$ time d8 --prof test.js</code></p><p>Run <code>$ tick-processor</code></p><p>Roughly the same execution time as the last function, which seems like it should have been faster. You’ll also notice that the <code>multiply</code> and <code>equal</code> functions are nowhere on the list. Weird, right?</p><p>Run <code>$ d8 --trace-inlining test.js</code></p><p>Okay. So, we can see that the optimizing compiler was smart here and completely eliminated the overhead of calling both of those functions by inlining them into the optimized code for <code>factorial</code>.</p><p>The optimized code for both versions ends up being basically identical (which you can check, if you know how to read assembly, by running <code>d8 --print-opt-code test.js</code>).</p><h3 id="Tracing-Garbage-Collection"><a href="#Tracing-Garbage-Collection" class="headerlink" title="Tracing Garbage Collection"></a>Tracing Garbage Collection</h3><p>Modify <code>test.js</code></p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">strToArray</span>(<span class="params"> str </span>) &#123;</span><br><span class="line">  <span class="keyword">var</span> i = <span class="number">0</span>,</span><br><span class="line">    len = str.<span class="property">length</span>,</span><br><span class="line">    arr = <span class="keyword">new</span> <span class="title class_">Uint16Array</span>( str.<span class="property">length</span> );</span><br><span class="line">  <span class="keyword">for</span> ( ; i &lt; len; ++i ) &#123;</span><br><span class="line">    arr[ i ] = str.<span class="title function_">charCodeAt</span>( i );</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">return</span> arr;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> i = <span class="number">0</span>, str = <span class="string">&#x27;V8 is the collest&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">while</span> ( i++ &lt; <span class="number">1e5</span> ) &#123;</span><br><span class="line">  <span class="title function_">strToArray</span>( str );</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>Run <code>$ d8 --trace-gc test.js</code></p><p>You’ll see a bunch of <code>Scavenge... [allocation failure]</code>.</p><p>Basically, V8’s GC heap has different “spaces”. Most objects are allocated in the “new space”. It’s super cheap to allocate here, but it’s also pretty small (usually somewhere between 1 and 8 MB). Once that space gets filled up, the GC does a “scavenge”.</p><p>Scavenging is the fast part of V8 garbage collection. Usually somewhere between 1 and 5ms from what I’ve seen – so it might not necessarily cause a noticeable GC pause.</p><p><strong>Scavenges can only be kicked off by allocations</strong>. If the “new space” never gets filled up, the GC never needs to reclaim space by scavenging.</p><p>Modify <code>test.js</code>:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">strToArray</span>(<span class="params"> str, bufferView </span>) &#123;</span><br><span class="line">  <span class="keyword">var</span> i = <span class="number">0</span>,</span><br><span class="line">    len = str.<span class="property">length</span>;</span><br><span class="line">  <span class="keyword">for</span> ( ; i &lt; len; ++i ) &#123;</span><br><span class="line">    bufferView[ i ] = str.<span class="title function_">charCodeAt</span>( i );</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">return</span> bufferView;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> i = <span class="number">0</span>,</span><br><span class="line">  str = <span class="string">&#x27;V8 is the coolest&#x27;</span>,</span><br><span class="line">  buffer = <span class="keyword">new</span> <span class="title class_">ArrayBuffer</span>( str.<span class="property">length</span> * <span class="number">2</span> ),</span><br><span class="line">  bufferView = <span class="keyword">new</span> <span class="title class_">Uint16Array</span>( buffer );</span><br><span class="line"></span><br><span class="line"><span class="keyword">while</span> ( i++ &lt; <span class="number">1e5</span> ) &#123;</span><br><span class="line">  <span class="title function_">strToArray</span>( str, bufferView );</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>Here, we use a preallocated <code>ArrayBuffer</code> and an associated <code>ArrayBufferView</code> (in this case a <code>Uint16Array</code>) in order to avoid reallocating a new object every time we run <code>strToArray()</code>. The result is that we’re hardly allocating anything.</p><p>Run <code>$ d8 --trace-gc test.js</code></p><p>Nothing. We never filled up the “new space”, so we never had to scavenge.</p><p>One more thing to try in <code>test.js</code>:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">strToArray</span>(<span class="params"> str </span>) &#123;</span><br><span class="line">  <span class="keyword">var</span> i = <span class="number">0</span>,</span><br><span class="line">    len = str.<span class="property">length</span>,</span><br><span class="line">    arr = <span class="keyword">new</span> <span class="title class_">Uint16Array</span>( str.<span class="property">length</span> );</span><br><span class="line">  <span class="keyword">for</span> ( ; i &lt; len; ++i ) &#123;</span><br><span class="line">    arr[ i ] = str.<span class="title function_">charCodeAt</span>( i );</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">return</span> arr;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> i = <span class="number">0</span>, str = <span class="string">&#x27;V8 is the coolest&#x27;</span>, arr = [];</span><br><span class="line"></span><br><span class="line"><span class="keyword">while</span> ( i++ &lt; <span class="number">1e6</span> ) &#123;</span><br><span class="line">  <span class="title function_">strToArray</span>( str );</span><br><span class="line">  <span class="keyword">if</span> ( i % <span class="number">100000</span> === <span class="number">0</span> ) &#123;</span><br><span class="line">    <span class="comment">// save a long-term reference to a random, huge object</span></span><br><span class="line">    arr.<span class="title function_">push</span>( <span class="keyword">new</span> <span class="title class_">Uint16Array</span>( <span class="number">100000000</span> ) );</span><br><span class="line">    <span class="comment">// release references about 5% of the time</span></span><br><span class="line">    <span class="title class_">Math</span>.<span class="title function_">random</span>() &gt; <span class="number">0.95</span> &amp;&amp; ( arr.<span class="property">length</span> = <span class="number">0</span> );</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>Run <code>$ d8 --trace-gc test.js</code></p><p>Lots of scavenges, which is expected since we’re no longer using a preallocated buffer. But there should also be a bunch of <code>Mark-sweep</code> lines.</p><p>Mark-sweep is the “full” GC. It gets run when the “old space” heap reaches a certain size, and it tends to take a lot longer than a scavenge. If you look at the logs, you’ll probably see <code>Scavenge</code> at around ~1.5ms and <code>Mark-sweep</code> closer to 25 or 30ms.</p><p>Since the frame budget in a web app is about 16ms, you’re pretty much guaranteed to drop at least 1 frame every time Mark-sweep runs.</p><h2 id="Random-stuff"><a href="#Random-stuff" class="headerlink" title="Random stuff"></a>Random stuff</h2><h3 id="d8-help-logs-all-available-d8-flags"><a href="#d8-help-logs-all-available-d8-flags" class="headerlink" title="$ d8 --help logs all available d8 flags"></a><code>$ d8 --help</code> logs all available d8 flags</h3><p>There’s a ton there, but you can usually find what you’re looking for with something like <code>d8 --help | grep memory</code> or whatever.</p><h3 id="d8-allow-natives-syntax-file-js"><a href="#d8-allow-natives-syntax-file-js" class="headerlink" title="$ d8 --allow-natives-syntax file.js"></a><code>$ d8 --allow-natives-syntax file.js</code></h3><p>This actually lets you call V8 internal methods from within your JS file, like this:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">factorial</span>(<span class="params"> n </span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> n === <span class="number">1</span> ? n : <span class="title function_">factorial</span>( --n );</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> i = <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">while</span> ( i++ &lt; <span class="number">1e8</span> ) &#123;</span><br><span class="line">  <span class="title function_">factorial</span>( <span class="number">10</span> );</span><br><span class="line">  <span class="comment">// run a full Mark-sweep pass every 10MM iterations</span></span><br><span class="line">  i % <span class="number">1e7</span> === <span class="number">0</span> &amp;&amp; %<span class="title class_">CollectGarbage</span>( <span class="literal">null</span> );</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>…and run <code>$ d8 --allow-natives-syntax --trace-gc test.js</code></p><p>Native functions are prefixed with the <code>%</code> symbol. A (somewhat incomplete) list of native functions are listed <a href="https://github.com/Nathanaela/v8-Natives/blob/master/lib/v8-native-calls.js">here</a>.</p><h3 id="Logging"><a href="#Logging" class="headerlink" title="Logging"></a>Logging</h3><p>d8 doesn’t have a <code>console</code> object (or a <code>window</code> object, for that matter). But you can log to the terminal using <code>print()</code>.</p><h3 id="Comparing-Hidden-Classes"><a href="#Comparing-Hidden-Classes" class="headerlink" title="Comparing Hidden Classes"></a>Comparing Hidden Classes</h3><p>This is probably my favorite one. I actually <em>just</em> found it.</p><p>So in V8, there’s this concept of <a href="http://www.html5rocks.com/en/tutorials/speed/v8/">“hidden classes”</a> (Good explanation a couple paragraphs in). You should read that article – but basically, hidden classes are how V8 (SpiderMonkey and JavaScript Core use similar techniques, too) determine whether or not two objects have the same “shape”.</p><p>All things considered, you always want to pass objects of the same hidden class as arguments to functions.</p><p>Anyway, you can actually <em>compare</em> the hidden classes of two objects:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">function Class( val ) &#123;</span><br><span class="line">  this.prop = val;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">var a = new Class(&#x27;foo&#x27;);</span><br><span class="line">var b = new Class(&#x27;bar&#x27;);</span><br><span class="line"></span><br><span class="line">print( %HaveSameMap( a, b ) );</span><br><span class="line"></span><br><span class="line">b.prop2 = &#x27;baz&#x27;;</span><br><span class="line"></span><br><span class="line">print( %HaveSameMap( a, b ) );</span><br></pre></td></tr></table></figure><p>Run <code>$ d8 --allow-natives-syntax test.js</code></p><p>You should see <code>true</code>, then <code>false</code>. By adding <code>b.prop2 = &#39;baz&#39;</code>, we modified its structure and created a new hidden class.</p><h3 id="Node-js"><a href="#Node-js" class="headerlink" title="Node.js"></a>Node.js</h3><p>A lot of these flags (but not all of them) work with Node, too. <code>--trace-opt</code>, <code>--prof</code>, <code>--allow-natives-syntax</code> are all supported.</p><p>That can be helpful if you want to test something that relies on another library, since you can use Node’s <code>require()</code>.</p><p>A list of supported V8 flags can be accessed with <code>node --v8-options</code>.</p><h3 id="Links"><a href="#Links" class="headerlink" title="Links"></a>Links</h3><p><a href="http://www.html5rocks.com/en/tutorials/speed/v8/">Performance Tips for JavaScript in V8</a> (Good basic intro to Hidden Classes)</p><p><a href="http://www.html5rocks.com/en/tutorials/performance/mystery/">Use forensics and detective work to solve JavaScript performance mysteries</a></p><p><a href="https://www.youtube.com/watch?v=UJPdhx5zTaw">Breaking the JavaScript Speed Limit with V8</a></p><p><a href="http://wingolog.org/archives/2011/07/05/v8-a-tale-of-two-compilers">V8 - A Tale of Two Compilers</a> (Good explanation of Inline Caches)</p><hr><p>Anyway, this is all still pretty new to me, and there’s a lot I haven’t figured out yet. But the stuff I’ve found so far is pretty cool, so I wanted to write something up and share it.</p><p>Oh, and I’m sure there’s stuff in here that I’m wrong about, because I’m honestly a little out of my depth here. Feedback is appreciated.</p><blockquote><p>使用 Demo 请参考 <a href="https://gist.github.com/kevincennis/0cd2138c78a07412ef21#d8-shell-examples">d8-shell-examples</a></p></blockquote><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ul><li><a href="https://github.com/nodejs/node-gyp/issues/569#issuecomment-94917337">xcode-select: error: tool ‘xcodebuild’ requires Xcode, but active developer directory ‘&#x2F;Library&#x2F;Developer&#x2F;CommandLineTools’ is a command line tools instance</a></li><li><a href="https://gist.github.com/kevincennis/0cd2138c78a07412ef21">Installing V8 on a Mac</a></li></ul>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/ddec6c95591a/</id>
    <link href="https://blog.zisheng.pro/posts/ddec6c95591a/"/>
    <published>2025-03-06T22:54:41.000Z</published>
    <summary>介绍如何在 Mac 上从源码编译安装 V8 引擎（d8 shell），并通过 --trace-opt、--prof、--trace-gc 等命令深入了解 V8 的优化、性能分析与垃圾回收机制。</summary>
    <title>在 Mac 上安装 V8</title>
    <updated>2026-09-18T08:15:28.636Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="前端工程化" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/"/>
    <category term="TypeScript" scheme="https://blog.zisheng.pro/tags/TypeScript/"/>
    <category term="Jest" scheme="https://blog.zisheng.pro/tags/Jest/"/>
    <category term="Coverage" scheme="https://blog.zisheng.pro/tags/Coverage/"/>
    <category term="项目初始化" scheme="https://blog.zisheng.pro/tags/%E9%A1%B9%E7%9B%AE%E5%88%9D%E5%A7%8B%E5%8C%96/"/>
    <content>
      <![CDATA[<h2 id="TypeScript"><a href="#TypeScript" class="headerlink" title="TypeScript"></a>TypeScript</h2><blockquote><p>参考文档：<a href="https://www.typescriptlang.org/docs/handbook/typescript-tooling-in-5-minutes.html">https://www.typescriptlang.org/docs/handbook/typescript-tooling-in-5-minutes.html</a></p></blockquote><p>初始化：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 初始化 npm 项目</span></span><br><span class="line">$ npm init -y</span><br><span class="line"><span class="comment"># 全局安装 TypeScript</span></span><br><span class="line">$ npm install -g typescript</span><br><span class="line"><span class="comment"># 项目安装 TypeScript</span></span><br><span class="line">$ pnpm add typescript -D</span><br><span class="line"><span class="comment"># 初始化 TypeScript 配置文件</span></span><br><span class="line">$ tsc --init</span><br></pre></td></tr></table></figure><p>配置 tsconfig.json：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;compilerOptions&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;target&quot;</span><span class="punctuation">:</span> <span class="string">&quot;es2016&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;module&quot;</span><span class="punctuation">:</span> <span class="string">&quot;commonjs&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;outDir&quot;</span><span class="punctuation">:</span> <span class="string">&quot;./dist&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;esModuleInterop&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;forceConsistentCasingInFileNames&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;strict&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;skipLibCheck&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><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><ul><li>target：用于指定 TypeScript 代码编译后生成的 JavaScript 代码的目标版本。</li><li>module：用于指定 TypeScript 模块的目标模式，即编译后的 JavaScript 代码所使用的模块系统。</li><li>outDir：用于指定编译后的 JavaScript 文件输出目录的配置选项。</li><li>esModuleInterop：主要用于处理 ECMAScript 模块（ES 模块）与 CommonJS 模块之间的交互问题<blockquote><p>当esModuleInterop设置为true时，TypeScript 编译器会在编译过程中生成一些额外的代码来处理 ES 模块和 CommonJS 模块之间的差异。具体来说，它会确保当从 ES 模块中导入 CommonJS 模块时，能够正确地获取到 CommonJS 模块的导出内容。</p></blockquote></li><li>forceConsistentCasingInFileNames：用于指定是否强制使用一致的大小写来命名文件。<blockquote><p>当forceConsistentCasingInFileNames设置为true时，TypeScript 编译器会在编译过程中严格检查所有导入和引用的文件名的大小写是否与实际文件系统中的文件名大小写一致。如果发现不一致，编译器将抛出错误，提示开发人员修正文件名的大小写问题。</p></blockquote></li><li>strict：用于指定是否启用严格模式。<blockquote><p>当strict设置为true时，TypeScript 编译器会启用严格模式，对代码进行更严格的类型检查和错误检测。这意味着编译器会对代码进行更严格的类型检查，以帮助开发人员发现潜在的类型错误和问题。</p></blockquote></li><li>skipLibCheck：主要用于控制 TypeScript 编译器对声明文件（.d.ts）的类型检查行为，以下是具体介绍：<blockquote><p>当编译器遇到skipLibCheck设置为true时，它会在编译过程中跳过对node_modules目录下以及types目录（如果有）中声明文件的类型检查。编译器仍然会处理这些声明文件，以便在代码中正确地使用它们提供的类型信息，但不会对其内部的类型定义进行严格的检查和验证。</p></blockquote></li></ul><p>配置文件优化：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;compilerOptions&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">     <span class="comment">/* 声明文件与源码映射 */</span></span><br><span class="line">    <span class="attr">&quot;declaration&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span><span class="punctuation">,</span>      <span class="comment">// 生成 .d.ts 类型声明</span></span><br><span class="line">    <span class="attr">&quot;declarationMap&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span><span class="punctuation">,</span>   <span class="comment">// 类型声明与源码映射</span></span><br><span class="line">    <span class="attr">&quot;sourceMap&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span><span class="punctuation">,</span>        <span class="comment">// 生成调试用 sourcemap</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">/* 路径与模块配置 */</span></span><br><span class="line">    <span class="attr">&quot;baseUrl&quot;</span><span class="punctuation">:</span> <span class="string">&quot;.&quot;</span><span class="punctuation">,</span>           <span class="comment">// 模块解析基准路径[6,9](@ref)</span></span><br><span class="line">    <span class="attr">&quot;paths&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span>                <span class="comment">// 路径别名映射[6,9](@ref)</span></span><br><span class="line">      <span class="attr">&quot;@/*&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span><span class="string">&quot;src/*&quot;</span><span class="punctuation">]</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 class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;include&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span><span class="string">&quot;src/**/*&quot;</span><span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;exclude&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">    <span class="string">&quot;dist&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="string">&quot;**/*.test.ts&quot;</span><span class="punctuation">,</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><p>添加 <code>scripts</code>：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><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;prebuild&quot;</span><span class="punctuation">:</span> <span class="string">&quot;rimraf dist&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;tsc&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;dev&quot;</span><span class="punctuation">:</span> <span class="string">&quot;tsc --watch&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;devDependencies&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;rimraf&quot;</span><span class="punctuation">:</span> <span class="string">&quot;^3.0.2&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><ul><li>rimraf：用于删除指定目录下的所有文件和子目录。</li></ul><p>创建第一个 TypeScript 文件：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// src/sum.ts</span></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">sum</span>(<span class="params">a: <span class="built_in">number</span>, b: <span class="built_in">number</span></span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> a + b;</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> sum;</span><br></pre></td></tr></table></figure><p>执行 <code>pnpm run build</code> 命令，生成 <code>dist</code> 文件夹：</p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1740441881437.png" alt="图 0"></p><h2 id="Jest"><a href="#Jest" class="headerlink" title="Jest"></a>Jest</h2><blockquote><p>参考文档：<a href="https://jestjs.io/zh-Hans/docs/getting-started">https://jestjs.io/zh-Hans/docs/getting-started</a></p></blockquote><h3 id="安装-jest"><a href="#安装-jest" class="headerlink" title="安装 jest"></a>安装 jest</h3><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 全局安装 Jest</span></span><br><span class="line">$ npm install -g jest</span><br><span class="line"><span class="comment"># 项目安装 Jest</span></span><br><span class="line">$ pnpm add jest -D</span><br></pre></td></tr></table></figure><h3 id="安装-ts-jest"><a href="#安装-ts-jest" class="headerlink" title="安装 ts-jest"></a>安装 ts-jest</h3><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">pnpm add ts-jest -D</span><br></pre></td></tr></table></figure><p>安装 ts-jest 后，Jest 会自动识别 TypeScript 文件，并使用 <code>ts-jest</code> 进行编译和测试。</p><h3 id="使用-ts-jest"><a href="#使用-ts-jest" class="headerlink" title="使用 ts-jest"></a>使用 ts-jest</h3><p>为了让 Jest 使用 ts-jest 来转换 TypeScript，你还需要使用 <code>jest --init</code> 命令创建一个配置文件。之后在 jest.config.ts 文件中添加一行：<code>preset: &quot;ts-jest&quot;</code>。</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">pnpm add ts-jest -D</span><br></pre></td></tr></table></figure><h3 id="安装-ts-node"><a href="#安装-ts-node" class="headerlink" title="安装 ts-node"></a>安装 ts-node</h3><p>使用 Typescript 配置文件的还需给 Jest 安装 ts-node， 请确保在项目中安装该依赖。</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">pnpm add ts-node -D</span><br></pre></td></tr></table></figure><h3 id="安装-types-jest"><a href="#安装-types-jest" class="headerlink" title="安装 @types&#x2F;jest"></a>安装 @types&#x2F;jest</h3><p>安装 <code>@types/jest</code> 包。它为 Jest 的全局变量提供类型，无需导入它们。</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">pnpm add @types/jest -D</span><br></pre></td></tr></table></figure><h3 id="测试"><a href="#测试" class="headerlink" title="测试"></a>测试</h3><p>创建一个测试文件，一般我们会把测试文件放在 <code>__tests__</code> 目录下：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// src/__tests__/sum.test.ts</span></span><br><span class="line"><span class="keyword">import</span> sum <span class="keyword">from</span> <span class="string">&#x27;./sum&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="title function_">test</span>(<span class="string">&#x27;adds 1 + 2 to equal 3&#x27;</span>, <span class="function">() =&gt;</span> &#123;</span><br><span class="line">  <span class="title function_">expect</span>(<span class="title function_">sum</span>(<span class="number">1</span>, <span class="number">2</span>)).<span class="title function_">toBe</span>(<span class="number">3</span>);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p>执行 <code>pnpm run test</code> 命令，测试通过：</p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1740443065042.png" alt="图 1"></p><p>会在项目根目录下生成 <code>coverage</code> 文件夹，里面包含了测试覆盖率的报告：</p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1740443117575.png" alt="图 2"></p><h2 id="Codecov"><a href="#Codecov" class="headerlink" title="Codecov"></a>Codecov</h2><h3 id="介绍"><a href="#介绍" class="headerlink" title="介绍"></a>介绍</h3><p>Codecov 是一款专注于代码覆盖率分析的在线服务平台，支持多种编程语言和测试框架，能够帮助开发者直观评估测试用例的有效性，并优化代码质量。</p><h3 id="初始化"><a href="#初始化" class="headerlink" title="初始化"></a>初始化</h3><p>访问 <code>codecov.io</code> 网站，点击 <code>Sign in with GitHub</code> 按钮，授权登录。选择具体仓库进行配置：</p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1740444392401.png" alt="图 4"></p><p>在仓库中添加 TOKEN：</p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1740443734805.png" alt="图 3"></p><p>在项目根目录添加 <code>.github/workflows/test.yaml</code> 文件：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">name:</span> <span class="string">Run</span> <span class="string">tests</span> <span class="string">and</span> <span class="string">upload</span> <span class="string">coverage</span></span><br><span class="line"></span><br><span class="line"><span class="attr">on:</span></span><br><span class="line">  <span class="string">push</span></span><br><span class="line"></span><br><span class="line"><span class="attr">jobs:</span></span><br><span class="line">  <span class="attr">test:</span></span><br><span class="line">    <span class="attr">name:</span> <span class="string">Run</span> <span class="string">tests</span> <span class="string">and</span> <span class="string">collect</span> <span class="string">coverage</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">steps:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Checkout</span></span><br><span class="line">        <span class="attr">uses:</span> <span class="string">actions/checkout@v4</span></span><br><span class="line">        <span class="attr">with:</span></span><br><span class="line">          <span class="attr">fetch-depth:</span> <span class="number">0</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">Node</span></span><br><span class="line">        <span class="attr">uses:</span> <span class="string">actions/setup-node@v4</span></span><br><span class="line"></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Install</span> <span class="string">dependencies</span></span><br><span class="line">        <span class="attr">run:</span> <span class="string">npm</span> <span class="string">install</span></span><br><span class="line"></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Run</span> <span class="string">tests</span></span><br><span class="line">        <span class="attr">run:</span> <span class="string">npx</span> <span class="string">jest</span> <span class="string">--coverage</span></span><br><span class="line"></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Upload</span> <span class="string">results</span> <span class="string">to</span> <span class="string">Codecov</span></span><br><span class="line">        <span class="attr">uses:</span> <span class="string">codecov/codecov-action@v5</span></span><br><span class="line">        <span class="attr">with:</span></span><br><span class="line">          <span class="attr">token:</span> <span class="string">$&#123;&#123;</span> <span class="string">secrets.CODECOV_TOKEN</span> <span class="string">&#125;&#125;</span></span><br><span class="line">          <span class="attr">slug:</span> <span class="string">youngjuning/fucking-js</span></span><br></pre></td></tr></table></figure><p>执行完后访问 <a href="https://app.codecov.io/gh/youngjuning/jsace">https://app.codecov.io/gh/youngjuning/jsace</a> 网站，查看测试覆盖率：</p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1740444533825.png" alt="图 5"></p><h2 id="eslint-flat-config"><a href="#eslint-flat-config" class="headerlink" title="eslint flat config"></a>eslint flat config</h2><h3 id="安装-youngjuning-eslint-config"><a href="#安装-youngjuning-eslint-config" class="headerlink" title="安装 @youngjuning&#x2F;eslint-config"></a>安装 @youngjuning&#x2F;eslint-config</h3><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">pnpm add @youngjuning/eslint-config eslint prettier typescript jest -D</span><br></pre></td></tr></table></figure><h3 id="添加-eslint-config-ts"><a href="#添加-eslint-config-ts" class="headerlink" title="添加 eslint.config.ts"></a>添加 eslint.config.ts</h3><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><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> youngjuning <span class="keyword">from</span> <span class="string">&#x27;@youngjuning/eslint-config&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> tseslint.<span class="title function_">config</span>(youngjuning, &#123;</span><br><span class="line">  <span class="attr">ignores</span>: [<span class="string">&#x27;**/dist/**&#x27;</span>],</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><h3 id="pnpm-Installation"><a href="#pnpm-Installation" class="headerlink" title="pnpm Installation"></a>pnpm Installation</h3><p>要在 pnpm 中使用 ESLint，我们建议设置一个 <code>.npmrc</code> 文件，并至少包含以下设置：</p><figure class="highlight txt"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">auto-install-peers=true</span><br><span class="line">node-linker=hoisted</span><br></pre></td></tr></table></figure>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/724224a79d85/</id>
    <link href="https://blog.zisheng.pro/posts/724224a79d85/"/>
    <published>2025-02-24T13:45:40.000Z</published>
    <summary>从零到一初始化一个 TypeScript 项目，使用最新的技术</summary>
    <title>TypeScript+Jest+Coverage+Eslint+Prettier 项目初始化</title>
    <updated>2026-09-18T08:15:28.632Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="方法论" scheme="https://blog.zisheng.pro/categories/%E6%96%B9%E6%B3%95%E8%AE%BA/"/>
    <category term="个人目标" scheme="https://blog.zisheng.pro/tags/%E4%B8%AA%E4%BA%BA%E7%9B%AE%E6%A0%87/"/>
    <category term="OKR" scheme="https://blog.zisheng.pro/tags/OKR/"/>
    <category term="SMART" scheme="https://blog.zisheng.pro/tags/SMART/"/>
    <content>
      <![CDATA[<p>个人学习目标适合使用 <strong>OKR</strong> 和 <strong>SMART</strong> 模型都可以，但它们的适用场景和侧重点有所不同。选择哪种模型取决于你的学习目标类型、时间跨度以及你希望如何跟踪进展。</p><hr><h2 id="1-OKR（目标与关键结果）"><a href="#1-OKR（目标与关键结果）" class="headerlink" title="1. OKR（目标与关键结果）"></a><strong>1. OKR（目标与关键结果）</strong></h2><h3 id="特点："><a href="#特点：" class="headerlink" title="特点："></a><strong>特点：</strong></h3><ul><li><strong>目标（Objective）</strong>：描述一个有激励性的方向或愿景。</li><li><strong>关键结果（Key Results）</strong>：具体的、可量化的指标，用于衡量目标是否达成。</li><li><strong>适用场景</strong>：适合中长期目标，尤其是需要明确方向并量化成果的学习计划。</li></ul><h3 id="优点："><a href="#优点：" class="headerlink" title="优点："></a><strong>优点：</strong></h3><ul><li>强调愿景驱动，能够激发动力。</li><li>通过关键结果提供清晰的衡量标准。</li><li>灵活调整，适合动态变化的目标。</li></ul><h3 id="缺点："><a href="#缺点：" class="headerlink" title="缺点："></a><strong>缺点：</strong></h3><ul><li>对初学者来说可能过于抽象，难以制定具体的关键结果。</li><li>需要定期回顾和调整，否则容易偏离目标。</li></ul><h3 id="示例：个人学习目标"><a href="#示例：个人学习目标" class="headerlink" title="示例：个人学习目标"></a><strong>示例：个人学习目标</strong></h3><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><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: 掌握 Python 编程基础</span></span><br><span class="line"><span class="bullet">-</span> <span class="strong">**关键结果 1**</span>: 完成一本 Python 入门书籍的学习（如《Python编程：从入门到实践》）。</span><br><span class="line"><span class="bullet">-</span> <span class="strong">**关键结果 2**</span>: 在 LeetCode 上完成 30 道简单难度的算法题。</span><br><span class="line"><span class="bullet">-</span> <span class="strong">**关键结果 3**</span>: 开发一个小型项目（如记账工具）并部署到 GitHub。</span><br><span class="line"></span><br><span class="line"><span class="section">## 目标 2: 提升英语口语能力</span></span><br><span class="line"><span class="bullet">-</span> <span class="strong">**关键结果 1**</span>: 每周进行 3 次英语对话练习（每次至少 30 分钟）。</span><br><span class="line"><span class="bullet">-</span> <span class="strong">**关键结果 2**</span>: 参加一次在线英语演讲活动并获得反馈。</span><br><span class="line"><span class="bullet">-</span> <span class="strong">**关键结果 3**</span>: 英语听力测试分数提升至 85%。</span><br></pre></td></tr></table></figure><h3 id="适用人群："><a href="#适用人群：" class="headerlink" title="适用人群："></a><strong>适用人群：</strong></h3><ul><li>希望设定中长期学习目标的人。</li><li>需要量化成果并定期检查进展的人。</li><li>喜欢挑战性目标并愿意不断调整的人。</li></ul><hr><h2 id="2-SMART-模型"><a href="#2-SMART-模型" class="headerlink" title="2. SMART 模型"></a><strong>2. SMART 模型</strong></h2><h3 id="特点：-1"><a href="#特点：-1" class="headerlink" title="特点："></a><strong>特点：</strong></h3><ul><li><strong>Specific（具体）</strong>：目标明确，避免模糊。</li><li><strong>Measurable（可衡量）</strong>：可以量化或评估目标完成情况。</li><li><strong>Achievable（可实现）</strong>：目标现实且符合自身能力。</li><li><strong>Relevant（相关性）</strong>：目标与个人需求或长期规划相关。</li><li><strong>Time-bound（有时限）</strong>：设置明确的时间节点。</li></ul><h3 id="优点：-1"><a href="#优点：-1" class="headerlink" title="优点："></a><strong>优点：</strong></h3><ul><li>结构化强，易于制定和执行。</li><li>适合短期目标或需要快速见效的任务。</li><li>强调可行性，减少挫败感。</li></ul><h3 id="缺点：-1"><a href="#缺点：-1" class="headerlink" title="缺点："></a><strong>缺点：</strong></h3><ul><li>过于关注细节可能导致视野狭窄。</li><li>对于复杂或长期目标，可能缺乏灵活性。</li></ul><h3 id="示例：个人学习目标-1"><a href="#示例：个人学习目标-1" class="headerlink" title="示例：个人学习目标"></a><strong>示例：个人学习目标</strong></h3><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><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: 完成 Python 数据分析基础学习</span></span><br><span class="line"><span class="bullet">-</span> <span class="strong">**Specific**</span>: 学习 Pandas 和 NumPy 的基本操作。</span><br><span class="line"><span class="bullet">-</span> <span class="strong">**Measurable**</span>: 完成 Udemy 上的《Python数据分析入门课程》，并通过所有课后练习。</span><br><span class="line"><span class="bullet">-</span> <span class="strong">**Achievable**</span>: 每天投入 1 小时学习，周末复习。</span><br><span class="line"><span class="bullet">-</span> <span class="strong">**Relevant**</span>: 数据分析技能有助于未来职业发展。</span><br><span class="line"><span class="bullet">-</span> <span class="strong">**Time-bound**</span>: 在 1 个月内完成课程。</span><br><span class="line"></span><br><span class="line"><span class="section">## 目标 2: 提高英语词汇量</span></span><br><span class="line"><span class="bullet">-</span> <span class="strong">**Specific**</span>: 每天记忆 10 个新单词。</span><br><span class="line"><span class="bullet">-</span> <span class="strong">**Measurable**</span>: 使用 Anki 软件记录单词，并确保每周掌握 70 个单词。</span><br><span class="line"><span class="bullet">-</span> <span class="strong">**Achievable**</span>: 每天花 15 分钟背单词，结合例句记忆。</span><br><span class="line"><span class="bullet">-</span> <span class="strong">**Relevant**</span>: 扩大词汇量有助于阅读和写作能力提升。</span><br><span class="line"><span class="bullet">-</span> <span class="strong">**Time-bound**</span>: 持续 3 个月，总计掌握 900 个单词。</span><br></pre></td></tr></table></figure><h3 id="适用人群：-1"><a href="#适用人群：-1" class="headerlink" title="适用人群："></a><strong>适用人群：</strong></h3><ul><li>希望设定短期、具体目标的人。</li><li>需要明确步骤和时间节点的人。</li><li>初学者或对目标管理不熟悉的人。</li></ul><hr><h2 id="对比总结"><a href="#对比总结" class="headerlink" title="对比总结"></a><strong>对比总结</strong></h2><table><thead><tr><th>特性</th><th>OKR</th><th>SMART</th></tr></thead><tbody><tr><td><strong>时间跨度</strong></td><td>中长期（季度、半年、年度）</td><td>短期（周、月）</td></tr><tr><td><strong>目标性质</strong></td><td>愿景驱动，强调方向和成果</td><td>细节导向，强调可执行性和可行性</td></tr><tr><td><strong>灵活性</strong></td><td>动态调整，适应变化</td><td>固定性强，需按计划执行</td></tr><tr><td><strong>适用场景</strong></td><td>需要量化成果的复杂目标</td><td>需要快速见效的具体任务</td></tr><tr><td><strong>学习者类型</strong></td><td>喜欢挑战性目标，善于自我激励</td><td>偏好结构化指导，注重执行力</td></tr></tbody></table><hr><h2 id="如何选择？"><a href="#如何选择？" class="headerlink" title="如何选择？"></a><strong>如何选择？</strong></h2><ol><li><p><strong>如果你是初学者</strong>：</p><ul><li>推荐使用 <strong>SMART 模型</strong>，因为它更具体、更容易上手。</li><li>示例：每天背 10 个单词，每周写一篇短文。</li></ul></li><li><p><strong>如果你已经有明确的学习方向</strong>：</p><ul><li>推荐使用 <strong>OKR 模型</strong>，因为它可以帮助你聚焦愿景并量化成果。</li><li>示例：目标是“成为数据科学家”，关键结果包括完成特定课程、完成项目等。</li></ul></li><li><p><strong>结合使用</strong>：</p><ul><li>可以用 OKR 设定中长期目标，用 SMART 模型细化每个关键结果的具体执行计划。</li><li>示例：<figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="section"># 季度目标（OKR）</span></span><br><span class="line"><span class="section">## 目标 1: 成为初级数据分析师</span></span><br><span class="line"><span class="bullet">-</span> <span class="strong">**关键结果 1**</span>: 完成 Python 数据分析课程（SMART 计划：每天学习 1 小时，1 个月内完成）。</span><br><span class="line"><span class="bullet">-</span> <span class="strong">**关键结果 2**</span>: 完成一个数据分析项目（SMART 计划：每周投入 5 小时，2 个月内完成）。</span><br></pre></td></tr></table></figure></li></ul></li></ol><hr><h2 id="最终建议"><a href="#最终建议" class="headerlink" title="最终建议"></a><strong>最终建议</strong></h2><ul><li>如果你的学习目标较为长期且需要激励自己，选择 <strong>OKR</strong>。</li><li>如果你的学习目标较为短期且需要明确的执行步骤，选择 <strong>SMART</strong>。</li><li>如果不确定，可以从 <strong>SMART 模型</strong>开始，逐步过渡到 <strong>OKR</strong>，或者将两者结合使用。</li></ul><p>如果你能分享你的具体学习目标，我可以为你设计一个更贴合的方案！</p>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/be3ad9c965b3/</id>
    <link href="https://blog.zisheng.pro/posts/be3ad9c965b3/"/>
    <published>2025-02-20T08:47:14.000Z</published>
    <summary>个人学习目标适合使用 **OKR** 和 **SMART** 模型都可以，但它们的适用场景和侧重点有所不同。选择哪种模型取决于你的学习目标类型、时间跨度以及你希望如何跟踪进展。</summary>
    <title>个人目标管理模型 OKR 和 SMART 的区别</title>
    <updated>2026-09-18T08:15:28.636Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="数据结构" scheme="https://blog.zisheng.pro/categories/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/"/>
    <category term="JavaScript" scheme="https://blog.zisheng.pro/tags/JavaScript/"/>
    <category term="算法" scheme="https://blog.zisheng.pro/tags/%E7%AE%97%E6%B3%95/"/>
    <content>
      <![CDATA[<h1 id="数组"><a href="#数组" class="headerlink" title="数组"></a>数组</h1><ul><li>跟其他编程语言一样，JS 数组也是一组有序的数据，但跟其他语言不同的是，数组中的每个槽位可以存储任意类型的数组。</li><li>JS 数组是动态大小的，会随着数据添加而自动增长。</li></ul><h2 id="创建数组"><a href="#创建数组" class="headerlink" title="创建数组"></a>创建数组</h2><h3 id="使用-Array-构造函数"><a href="#使用-Array-构造函数" class="headerlink" title="使用 Array 构造函数"></a>使用 Array 构造函数</h3><p>创建一个空数组：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 创建一个空数组</span></span><br><span class="line"><span class="keyword">const</span> arr = <span class="keyword">new</span> <span class="title class_">Array</span>();</span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(arr); <span class="comment">// []</span></span><br></pre></td></tr></table></figure><p>创建一个指定长度的数组，元素为 <code>undefined</code>：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 创建一个长度为 5 的数组，元素为 undefined</span></span><br><span class="line"><span class="keyword">const</span> arr = <span class="keyword">new</span> <span class="title class_">Array</span>(<span class="number">5</span>);</span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(arr); <span class="comment">// [undefined, undefined, undefined, undefined, undefined]</span></span><br></pre></td></tr></table></figure><p>创建并初始化数组：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 创建一个包含 [1, 2, 3] 的数组</span></span><br><span class="line"><span class="keyword">const</span> arr = <span class="keyword">new</span> <span class="title class_">Array</span>(<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>);</span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(arr); <span class="comment">// [1, 2, 3]</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="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> arr = []; <span class="comment">// 创建一个空数组</span></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(arr); <span class="comment">// []</span></span><br></pre></td></tr></table></figure><p>直接定义数组元素：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 创建一个包含 [1, 2, 3] 的数组</span></span><br><span class="line"><span class="keyword">const</span> arr = [<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>];</span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(arr); <span class="comment">// [1, 2, 3]</span></span><br></pre></td></tr></table></figure><h3 id="使用-Array-from-方法"><a href="#使用-Array-from-方法" class="headerlink" title="使用 Array.from 方法"></a>使用 <code>Array.from</code> 方法</h3><p>从类数组对象或可迭代对象创建数组：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 创建一个包含 [1, 2, 3] 的数组</span></span><br><span class="line"><span class="keyword">let</span> arr = <span class="title class_">Array</span>.<span class="title function_">from</span>(&#123; <span class="attr">length</span>: <span class="number">3</span> &#125;, <span class="function">(<span class="params">_, i</span>) =&gt;</span> i + <span class="number">1</span>);</span><br></pre></td></tr></table></figure><p>从字符串转换为数组：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 创建一个包含 [&#x27;h&#x27;, &#x27;e&#x27;, &#x27;l&#x27;, &#x27;l&#x27;, &#x27;o&#x27;] 的数组</span></span><br><span class="line"><span class="keyword">let</span> arr = <span class="title class_">Array</span>.<span class="title function_">from</span>(<span class="string">&quot;hello&quot;</span>);</span><br></pre></td></tr></table></figure><h3 id="使用-Array-of-方法"><a href="#使用-Array-of-方法" class="headerlink" title="使用 Array.of 方法"></a>使用 <code>Array.of</code> 方法</h3><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 创建一个包含 [1, 2, 3] 的数组</span></span><br><span class="line"><span class="keyword">let</span> arr = <span class="title class_">Array</span>.<span class="title function_">of</span>(<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>);</span><br></pre></td></tr></table></figure><h3 id="使用-Array-fill-方法"><a href="#使用-Array-fill-方法" class="headerlink" title="使用 Array.fill 方法"></a>使用 <code>Array.fill</code> 方法</h3><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 创建一个长度为 5 的数组，元素全为 0。时间复杂度为 O(N)</span></span><br><span class="line"><span class="keyword">let</span> arr = <span class="keyword">new</span> <span class="title class_">Array</span>(<span class="number">5</span>).<span class="title function_">fill</span>(<span class="number">0</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><blockquote><p>时间复杂度为 O(1)</p></blockquote><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> arr = [<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>];</span><br><span class="line">arr.<span class="title function_">push</span>(<span class="number">4</span>); <span class="comment">// 在末尾插入元素 4</span></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(arr) <span class="comment">// [1, 2, 3, 4]</span></span><br></pre></td></tr></table></figure><h3 id="在开头插入元素"><a href="#在开头插入元素" class="headerlink" title="在开头插入元素"></a>在开头插入元素</h3><blockquote><p>时间复杂度为 O(N)</p></blockquote><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> arr = [<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>];</span><br><span class="line">arr.<span class="title function_">unshift</span>(<span class="number">0</span>); <span class="comment">// 在开头插入元素 0</span></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(arr); <span class="comment">// [0, 1, 2, 3]</span></span><br></pre></td></tr></table></figure><h3 id="在任意位置插入元素"><a href="#在任意位置插入元素" class="headerlink" title="在任意位置插入元素"></a>在任意位置插入元素</h3><blockquote><p>时间复杂度为 O(N)</p></blockquote><p><code>splice()</code> 方法可以在数组的任意位置插入元素。它接受三个参数：</p><ul><li>起始索引</li><li>要删除的元素的数量（如果不需要删除元素，设置为 <code>0</code>）</li><li>要插入的元素（可以是一个或多个）</li></ul><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> arr = [<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>];</span><br><span class="line">arr.<span class="title function_">splice</span>(<span class="number">1</span>, <span class="number">0</span>, <span class="number">1.5</span>); <span class="comment">// 在索引 1 的位置插入元素 1.5。</span></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(arr) <span class="comment">// [1, 1.5, 2, 3]</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><blockquote><p>时间复杂度为 O(1)</p></blockquote><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> arr = [<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>];</span><br><span class="line">arr.<span class="title function_">pop</span>(); <span class="comment">// 删除末尾元素 3。</span></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(arr) <span class="comment">// [1, 2]</span></span><br></pre></td></tr></table></figure><h3 id="从开头删除元素"><a href="#从开头删除元素" class="headerlink" title="从开头删除元素"></a>从开头删除元素</h3><blockquote><p>时间复杂度为 O(N)</p></blockquote><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> arr = [<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>];</span><br><span class="line">arr.<span class="title function_">shift</span>(); <span class="comment">// 删除开头元素 1。</span></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(arr); <span class="comment">// [2, 3]</span></span><br></pre></td></tr></table></figure><h3 id="在任意位置删除元素"><a href="#在任意位置删除元素" class="headerlink" title="在任意位置删除元素"></a>在任意位置删除元素</h3><blockquote><p>时间复杂度为 O(N)</p></blockquote><p><code>splice()</code> 方法可以删除数组的任意位置元素。它接受两个参数：</p><ul><li>起始索引</li><li>要删除的元素的数量（如果不需要删除元素，设置为 <code>0</code>）</li><li>要插入的元素（可以是一个或多个）</li></ul><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> arr = [<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>];</span><br><span class="line">arr.<span class="title function_">splice</span>(<span class="number">1</span>, <span class="number">1</span>); <span class="comment">// 删除索引 1 的元素 2。</span></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(arr) <span class="comment">// [1, 3]</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><blockquote><p>时间复杂度为 O(1)</p></blockquote><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> arr = [<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>];</span><br><span class="line">arr[<span class="number">0</span>] = <span class="number">100</span>;</span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(arr); <span class="comment">// [100, 2, 3]</span></span><br></pre></td></tr></table></figure><h3 id="使用-splice-方法"><a href="#使用-splice-方法" class="headerlink" title="使用 splice 方法"></a>使用 <code>splice</code> 方法</h3><blockquote><p>时间复杂度为 O(N)</p></blockquote><p><code>splice()</code> 方法可以删除数组的任意位置元素。它接受两个参数：</p><ul><li>起始索引</li><li>要删除的元素的数量（如果不需要删除元素，设置为 <code>0</code>）</li><li>要插入的元素（可以是一个或多个）</li></ul><p>当你需要修改数组中连续的多个元素时，<code>splice()</code> 方法非常有用。</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> arr = [<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>];</span><br><span class="line">arr.<span class="title function_">splice</span>(<span class="number">1</span>, <span class="number">1</span>, <span class="number">6</span>); <span class="comment">// 从索引 2 开始，删除 1 个元素，并插入新元素 6。</span></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(arr); <span class="comment">// [1, 6, 3]</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><blockquote><p>时间复杂度为 O(1)</p></blockquote><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> arr = [<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>];</span><br><span class="line"><span class="keyword">const</span> element = arr[<span class="number">1</span>]; <span class="comment">// 查询索引 1 的元素 2。</span></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(element); <span class="comment">// 2</span></span><br></pre></td></tr></table></figure><h3 id="根据元素值查找索引"><a href="#根据元素值查找索引" class="headerlink" title="根据元素值查找索引"></a>根据元素值查找索引</h3><blockquote><p>时间复杂度为 O(N)</p></blockquote><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> arr = [<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>];</span><br><span class="line"><span class="keyword">const</span> index = arr.<span class="title function_">indexOf</span>(<span class="number">2</span>); <span class="comment">// 查询元素 2 的索引 1。</span></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(index); <span class="comment">// 1</span></span><br></pre></td></tr></table></figure><h1 id="链表"><a href="#链表" class="headerlink" title="链表"></a>链表</h1><p>链表（Linked List）是一种常见的线性数据结构，它由一系列节点（Node）组成，每个节点包含两部分：数据域和指针域。数据域用于存储实际的数据，指针域则存储指向下一个节点的引用（在单链表中），通过这些指针将各个节点链接起来形成一个链式结构。</p><p>链表存储有序的元素集合，但不同于数组，链表中的元素在内存中不是连续放置的，而是通过指针相互关联。</p><h2 id="链表的特点"><a href="#链表的特点" class="headerlink" title="链表的特点"></a>链表的特点</h2><ul><li>插入和删除的效率高：在链表中插入或删除节点，只需要修改指针，时间复杂度为 O(1)。</li><li>随机访问效率低：由于链表中的节点在内存中不是连续存储的，不能像数组那样通过下标直接访问元素，需要从头节点开始依次遍历，时间复杂度为 O(n)。</li></ul><h2 id="链表的类型"><a href="#链表的类型" class="headerlink" title="链表的类型"></a>链表的类型</h2><ul><li>单链表：每个节点包含一个数据元素和一个指向下一个节点的指针。链表的最后一个节点的指针通常指向 <code>null</code>，表示链表的结束。</li><li>双向链表：每个节点除了包含数据元素和指向下一个节点的指针外，还包含一个指向前一个节点的指针。这种结构允许在链表中双向遍历。</li><li>循环链表：单循环链表的最后一个节点的指针指向头节点，形成一个闭环；双循环链表则是双向链表的首尾相连形成闭环。</li></ul><h2 id="创建链表"><a href="#创建链表" class="headerlink" title="创建链表"></a>创建链表</h2><h3 id="节点类的实现"><a href="#节点类的实现" class="headerlink" title="节点类的实现"></a>节点类的实现</h3><ul><li>链表最后一个节点的下一个元素始终是 <code>undefined</code> 或 <code>null</code>。</li><li>当一个 Node 实例被创建时，它的 <code>next</code> 指针总是 <code>undefined</code>。这没问题，因为我们知道它会是链表的最后一项。</li></ul><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">ListNode</span> &#123;</span><br><span class="line">  <span class="title function_">constructor</span>(<span class="params">element</span>) &#123;</span><br><span class="line">    <span class="comment">// 数据域，存储节点的值</span></span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">element</span> = element;</span><br><span class="line">    <span class="comment">// 指针域，指向下一个节点</span></span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">next</span> = <span class="literal">undefined</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><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">LinkedList</span> &#123;</span><br><span class="line">  <span class="title function_">constructor</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="comment">// 存储链表中的元素数量</span></span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">count</span> = <span class="number">0</span>;</span><br><span class="line">    <span class="comment">// 头节点，初始化为 undefined</span></span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">head</span> = <span class="literal">undefined</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><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">LinkedList</span> &#123;</span><br><span class="line">  <span class="title function_">constructor</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">count</span> = <span class="number">0</span>;</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">head</span> = <span class="literal">undefined</span>;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="title function_">push</span>(<span class="params">element</span>) &#123;</span><br><span class="line">    <span class="keyword">const</span> node = <span class="keyword">new</span> <span class="title class_">ListNode</span>(element);</span><br><span class="line">    <span class="keyword">let</span> current;</span><br><span class="line">    <span class="keyword">if</span> (<span class="variable language_">this</span>.<span class="property">head</span> == <span class="literal">null</span>) &#123;</span><br><span class="line">      <span class="comment">// 如果链表为空，将头节点指向新节点</span></span><br><span class="line">      <span class="variable language_">this</span>.<span class="property">head</span> = node;</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">      current = <span class="variable language_">this</span>.<span class="property">head</span>;</span><br><span class="line">      <span class="comment">// 遍历链表，找到最后一个节点</span></span><br><span class="line">      <span class="keyword">while</span> (current.<span class="property">next</span> != <span class="literal">null</span>) &#123;</span><br><span class="line">        current = current.<span class="property">next</span>;</span><br><span class="line">      &#125;</span><br><span class="line">      <span class="comment">// 将最后一个节点的 next 指针指向新节点</span></span><br><span class="line">      current.<span class="property">next</span> = node;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 递增链表的长度</span></span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">count</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>循环到目标 index 的代码片段在 LinkedList 类的方法中很常见、因此我们将其提取到一个单独的方法中，这样就可以在不同的地方复用它。</p><blockquote><p>注意：循环停止条件是小于目标 index，而不是小于等。这是因为目标的前一个节点的 next 就是目标节点。</p></blockquote><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">LinkedList</span> &#123;</span><br><span class="line">  <span class="title function_">constructor</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">count</span> = <span class="number">0</span>;</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">head</span> = <span class="literal">undefined</span>;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="title function_">getElementAt</span>(<span class="params">index</span>) &#123;</span><br><span class="line">    <span class="comment">// 检查越界值</span></span><br><span class="line">    <span class="keyword">if</span> (index &gt;= <span class="number">0</span> &amp;&amp; index &lt;= <span class="variable language_">this</span>.<span class="property">count</span>) &#123;</span><br><span class="line">      <span class="keyword">let</span> current = <span class="variable language_">this</span>.<span class="property">head</span>;</span><br><span class="line">      <span class="keyword">for</span> (<span class="keyword">let</span> i = <span class="number">0</span>; i &lt; index &amp;&amp; current != <span class="literal">null</span>; i++) &#123;</span><br><span class="line">        current = current.<span class="property">next</span>;</span><br><span class="line">      &#125;</span><br><span class="line">      <span class="keyword">return</span> current;</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">return</span> <span class="literal">undefined</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><ul><li>移除第一个元素：要做的就是让 <code>head</code> 指向列表的第二个元素。我们将用 <code>current</code> 变量创建一个对链表第一个元素的引用。这样 <code>current</code> 变量就是对链表中第一个元素的引用。如果把 <code>head</code> 赋为 <code>current.next</code>，就会移除第一个元素。</li><li>移除最后一个或中间某个元素：需要迭代链表的节点，直到找到要移除元素的前一个元素，我们用 <code>previous</code> 变量表示对当前元素的前一个元素的引用。在迭代到目标元素之后，<code>current</code> 变量会持有我们想从链表中移除的节点。因此，要从链表中移除当前元素，要做的就是将 <code>previous.next</code> 赋值为 <code>current.next</code>。</li></ul><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">LinkedList</span> &#123;</span><br><span class="line">  <span class="title function_">constructor</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">count</span> = <span class="number">0</span>;</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">head</span> = <span class="literal">undefined</span>;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="title function_">getElementAt</span>(<span class="params">index</span>) &#123;</span><br><span class="line">    <span class="comment">// 检查越界值</span></span><br><span class="line">    <span class="keyword">if</span> (index &gt;= <span class="number">0</span> &amp;&amp; index &lt;= <span class="variable language_">this</span>.<span class="property">count</span>) &#123;</span><br><span class="line">      <span class="keyword">let</span> current = <span class="variable language_">this</span>.<span class="property">head</span>;</span><br><span class="line">      <span class="comment">// 迭代整个链表直到目标 index。结束循环时，node 元素将是 index 位置元素的引用。</span></span><br><span class="line">      <span class="keyword">for</span> (<span class="keyword">let</span> i = <span class="number">0</span>; i &lt; index &amp;&amp; current != <span class="literal">null</span>; i++) &#123;</span><br><span class="line">        current = current.<span class="property">next</span>;</span><br><span class="line">      &#125;</span><br><span class="line">      <span class="keyword">return</span> current;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">undefined</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="comment">   * 从链表的特定位置移除一个元素。</span></span><br><span class="line"><span class="comment">   * <span class="doctag">@param</span> &#123;<span class="type">number</span>&#125; <span class="variable">index</span> - 要移除的元素的索引。</span></span><br><span class="line"><span class="comment">   * <span class="doctag">@returns</span> &#123;<span class="type">any</span>&#125; - 被移除的元素的值。</span></span><br><span class="line"><span class="comment">   */</span></span><br><span class="line">  <span class="title function_">removeAt</span>(<span class="params">index</span>) &#123;</span><br><span class="line">    <span class="comment">// 检查越界值</span></span><br><span class="line">    <span class="keyword">if</span> (index &gt;= <span class="number">0</span> &amp;&amp; index &lt; <span class="variable language_">this</span>.<span class="property">count</span>) &#123;</span><br><span class="line">      <span class="keyword">let</span> current = <span class="variable language_">this</span>.<span class="property">head</span>;</span><br><span class="line">      <span class="comment">// 移除第一项</span></span><br><span class="line">      <span class="keyword">if</span> (index === <span class="number">0</span>) &#123;</span><br><span class="line">        <span class="variable language_">this</span>.<span class="property">head</span> = current.<span class="property">next</span>;</span><br><span class="line">      &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        <span class="keyword">const</span> previous = <span class="variable language_">this</span>.<span class="title function_">getElementAt</span>(index - <span class="number">1</span>);</span><br><span class="line">        current = previous.<span class="property">next</span>;</span><br><span class="line">        <span class="comment">// 将 previous 与 current 的下一项链接起来：跳过 current，从而移除它</span></span><br><span class="line">        previous.<span class="property">next</span> = current.<span class="property">next</span>;</span><br><span class="line">      &#125;</span><br><span class="line">      <span class="variable language_">this</span>.<span class="property">count</span>--;</span><br><span class="line">      <span class="keyword">return</span> current.<span class="property">element</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">undefined</span>; <span class="comment">// 如果索引无效，则返回 undefined</span></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="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> list = <span class="keyword">new</span> <span class="title class_">LinkedList</span>();</span><br><span class="line">list.<span class="title function_">push</span>(<span class="number">5</span>);</span><br><span class="line">list.<span class="title function_">push</span>(<span class="number">10</span>);</span><br><span class="line">list.<span class="title function_">push</span>(<span class="number">15</span>);</span><br><span class="line">list.<span class="title function_">push</span>(<span class="number">20</span>);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 验证删除指定元素</span></span><br><span class="line">list.<span class="title function_">removeAt</span>(<span class="number">1</span>);</span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">info</span>(list);</span><br></pre></td></tr></table></figure><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1739283779711.png" alt="移除指定位置的元素"></p><p>如图可以看到，10 被成功移除了。</p><h2 id="在任意位置插入元素-1"><a href="#在任意位置插入元素-1" class="headerlink" title="在任意位置插入元素"></a>在任意位置插入元素</h2><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">LinkedList</span> &#123;</span><br><span class="line">  <span class="title function_">constructor</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">count</span> = <span class="number">0</span>;</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">head</span> = <span class="literal">undefined</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="comment">   * 在任意位置插入元素</span></span><br><span class="line"><span class="comment">   */</span></span><br><span class="line">  <span class="title function_">insert</span>(<span class="params">element, index</span>) &#123;</span><br><span class="line">    <span class="comment">// 由于我们处理的是位置，就需要检查越界值</span></span><br><span class="line">    <span class="keyword">if</span> (index &gt;= <span class="number">0</span> &amp;&amp; index &lt;= <span class="variable language_">this</span>.<span class="property">count</span>) &#123;</span><br><span class="line">      <span class="keyword">const</span> node = <span class="keyword">new</span> <span class="title class_">ListNode</span>(element);</span><br><span class="line">      <span class="keyword">if</span> (index === <span class="number">0</span>) &#123;</span><br><span class="line">        <span class="comment">// 在第一个位置添加</span></span><br><span class="line">        <span class="keyword">const</span> current = <span class="variable language_">this</span>.<span class="property">head</span>;</span><br><span class="line">        node.<span class="property">next</span> = current;</span><br><span class="line">        <span class="variable language_">this</span>.<span class="property">head</span> = node;</span><br><span class="line">      &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        <span class="keyword">const</span> previous = <span class="variable language_">this</span>.<span class="title function_">getElementAt</span>(index - <span class="number">1</span>);</span><br><span class="line">        <span class="keyword">const</span> current = previous.<span class="property">next</span>;</span><br><span class="line">        node.<span class="property">next</span> = current;</span><br><span class="line">        previous.<span class="property">next</span> = node;</span><br><span class="line">      &#125;</span><br><span class="line">      <span class="variable language_">this</span>.<span class="property">count</span>++; <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"></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">&#125;</span><br></pre></td></tr></table></figure><blockquote><p>注意：使用变量引用我们需要控制的节点非常重要，这样就不会丢失节点之间的链接。我们可以只使用一个变量（previous），但那样会很难控制节点之间的链接。<br>因此，最后声明一个额外的 current 变量来帮助我们处理这些引用。</p></blockquote><p>验证：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> list = <span class="keyword">new</span> <span class="title class_">LinkedList</span>();</span><br><span class="line">list.<span class="title function_">push</span>(<span class="string">&quot;张三&quot;</span>);</span><br><span class="line">list.<span class="title function_">push</span>(<span class="string">&quot;王五&quot;</span>);</span><br><span class="line">list.<span class="title function_">insert</span>(<span class="string">&quot;李四&quot;</span>, <span class="number">1</span>);</span><br></pre></td></tr></table></figure><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1739287449033.png" alt="在任意位置插入元素"></p><p>如图，我们成功在索引 1 的位置插入了 15。</p><h2 id="返回一个元素的位置"><a href="#返回一个元素的位置" class="headerlink" title="返回一个元素的位置"></a>返回一个元素的位置</h2><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="title function_">defaultEquals</span> = (<span class="params">a, b</span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">return</span> a === b;</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">class</span> <span class="title class_">LinkedList</span> &#123;</span><br><span class="line">  <span class="title function_">constructor</span>(<span class="params">equalsFn = defaultEquals</span>) &#123;</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">count</span> = <span class="number">0</span>;</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">head</span> = <span class="literal">undefined</span>;</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">equalsFn</span> = equalsFn;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="title function_">indexOf</span>(<span class="params">element</span>) &#123;</span><br><span class="line">    <span class="keyword">let</span> current = <span class="variable language_">this</span>.<span class="property">head</span>;</span><br><span class="line">    <span class="keyword">for</span> (<span class="keyword">let</span> i = <span class="number">0</span>; i &lt; <span class="variable language_">this</span>.<span class="property">count</span> &amp;&amp; current != <span class="literal">null</span>; i++) &#123;</span><br><span class="line">      <span class="keyword">if</span> (<span class="variable language_">this</span>.<span class="title function_">equalsFn</span>(element, current.<span class="property">element</span>)) &#123;</span><br><span class="line">        <span class="keyword">return</span> i;</span><br><span class="line">      &#125;</span><br><span class="line">      current = current.<span class="property">next</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> -<span class="number">1</span>;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li>一如既往，需要一个变量来帮助我们循环访问列表。该变量是 current，它的初始值是 head。</li><li>然后迭代元素，从 head（索引 0） 开始，直到链表长度（count 变量）为止。为了确保不会发生运行时错误，我们可以验证下 current 变量是否为 null 或 undefined。</li></ul><h2 id="从链表中移除元素"><a href="#从链表中移除元素" class="headerlink" title="从链表中移除元素"></a>从链表中移除元素</h2><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">LinkedList</span> &#123;</span><br><span class="line">  <span class="title function_">constructor</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">count</span> = <span class="number">0</span>;</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">head</span> = <span class="literal">undefined</span>;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="title function_">remove</span>(<span class="params">element</span>) &#123;</span><br><span class="line">    <span class="keyword">const</span> index = <span class="variable language_">this</span>.<span class="title function_">indexOf</span>(element);</span><br><span class="line">    <span class="keyword">return</span> <span class="variable language_">this</span>.<span class="title function_">removeAt</span>(index);</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="isEmpty、size-和-getHead-方法"><a href="#isEmpty、size-和-getHead-方法" class="headerlink" title="isEmpty、size 和 getHead 方法"></a>isEmpty、size 和 getHead 方法</h2><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">LinkedList</span> &#123;</span><br><span class="line">  <span class="title function_">constructor</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">count</span> = <span class="number">0</span>;</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">head</span> = <span class="literal">undefined</span>;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="title function_">isEmpty</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="variable language_">this</span>.<span class="property">count</span> === <span class="number">0</span>;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="title function_">size</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="variable language_">this</span>.<span class="property">count</span>;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="title function_">getHead</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="variable language_">this</span>.<span class="property">head</span>;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li>size：返回链表包含的元素个数，与数组的 length 属性类似。</li><li>getHead：返回链表包含的元素个数，与数组的 length 属性类似。</li><li>isEmpty：在链表中不包含元素时，返回 true，如果链表长度大于 0 则返回 false。</li></ul><h2 id="toString-方法"><a href="#toString-方法" class="headerlink" title="toString 方法"></a>toString 方法</h2><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">LinkedList</span> &#123;</span><br><span class="line">  <span class="title function_">constructor</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">count</span> = <span class="number">0</span>;</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">head</span> = <span class="literal">undefined</span>;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="title function_">toString</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="keyword">if</span> (<span class="variable language_">this</span>.<span class="property">head</span> === <span class="literal">null</span>) &#123;</span><br><span class="line">      <span class="keyword">return</span> <span class="string">&#x27;&#x27;</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">let</span> objString = <span class="string">`<span class="subst">$&#123;<span class="variable language_">this</span>.head.element&#125;</span>`</span>;</span><br><span class="line">    <span class="keyword">let</span> current = <span class="variable language_">this</span>.<span class="property">head</span>.<span class="property">next</span>;</span><br><span class="line">    <span class="keyword">for</span> (<span class="keyword">let</span> i = <span class="number">1</span>; i &lt; <span class="variable language_">this</span>.<span class="title function_">size</span>() &amp;&amp; current !== <span class="literal">null</span>; i++) &#123;</span><br><span class="line">      objString = <span class="string">`<span class="subst">$&#123;objString&#125;</span>,<span class="subst">$&#123;current.element&#125;</span>`</span>;</span><br><span class="line">      current = current.<span class="property">next</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> objString;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li>首先，列表为空（<code>head</code> 为 <code>null</code> 或 <code>undefined</code>），我们就返回一个空字符串，我们也可以用 <code>this.isEmpty()</code> 来判断。</li></ul>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/71833ca04b6f/</id>
    <link href="https://blog.zisheng.pro/posts/71833ca04b6f/"/>
    <published>2025-01-21T15:57:59.000Z</published>
    <summary>探索 JavaScript 数据结构的奥秘，掌握数组、链表、树等多种结构，为高效编程与算法实现筑牢根基。</summary>
    <title>JavaScript 数据结构</title>
    <updated>2026-09-18T08:15:28.629Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="英语" scheme="https://blog.zisheng.pro/categories/%E8%8B%B1%E8%AF%AD/"/>
    <category term="all,port,纠音" scheme="https://blog.zisheng.pro/tags/all-port-%E7%BA%A0%E9%9F%B3/"/>
    <content>
      <![CDATA[<h2 id="All-纠音"><a href="#All-纠音" class="headerlink" title="All 纠音"></a>All 纠音</h2><h3 id="音标分析"><a href="#音标分析" class="headerlink" title="音标分析"></a>音标分析</h3><ul><li><code>[ɔː]</code>：这是一个长元音，发音时口型要张大，舌身压低并后缩，舌尖不抵下齿，双唇稍收圆。可以先发 <code>[ɔ]</code> 的音，然后将音拉长，就得到了 <code>[ɔː]</code> 的发音。</li><li><code>[l]</code>：这个音是舌边音，当它位于元音后时，舌尖抵住上齿龈，舌面稍凹，舌后部向软腭抬起，气流从舌两侧通过，发出类似 “欧” 的音，但要注意舌尖始终抵住上齿龈，不要离开。</li></ul><h3 id="发音技巧"><a href="#发音技巧" class="headerlink" title="发音技巧"></a>发音技巧</h3><ul><li>先发 <code>[ɔː]</code> 的音，要保证发音的长度和饱满度，然后自然地滑向 <code>[l]</code> 的音，将二者连贯地发出，不要有明显的停顿。</li><li>注意整体发音的流畅性和连贯性，不要把 <code>[ɔː]</code> 和 <code>[l]</code> 的发音割裂开来，同时要注意发音的力度和清晰度，不要含糊不清。</li></ul><h3 id="常见错误及纠正"><a href="#常见错误及纠正" class="headerlink" title="常见错误及纠正"></a>常见错误及纠正</h3><ul><li>错误一：把 <code>[ɔː]</code> 发成 <code>[ɒ]</code>，这是比较常见的错误，<code>[ɒ]</code> 的口型比 <code>[ɔː]</code> 要小，发音也更短促。纠正方法是张大嘴巴，尽量将舌身压低后缩，发出更饱满的 <code>[ɔː]</code> 音。</li><li>错误二：忽略 <code>[l]</code> 的发音，或者发音不清晰。这可能导致单词的发音不准确，听起来像 <code>[ɔː]</code> 单独的音。要注意舌尖抵住上齿龈，清晰地发出 <code>[l]</code> 的音，使整个单词的发音完整准确。</li><li>错误三：发音时口型变化不自然，导致发音生硬。发音过程中要注意口型的逐渐过渡，从 <code>[ɔː]</code> 的张大嘴巴到 <code>[l]</code> 的舌尖抵住上齿龈，动作要连贯自然，才能发出准确流畅的 “all” 的音。<br>可以多听一些标准的发音音频，模仿练习，不断纠正自己的发音，逐渐掌握 “all” 的正确发音。</li></ul><h2 id="port-词根"><a href="#port-词根" class="headerlink" title="port 词根"></a>port 词根</h2><h3 id="词根-port-的来源和历史演变"><a href="#词根-port-的来源和历史演变" class="headerlink" title="词根 port 的来源和历史演变"></a>词根 port 的来源和历史演变</h3><p>词根 “port” 来源于拉丁语动词 “portare”，其基本含义是 “to carry”，即 “搬运”“携带” 之意。</p><p>在拉丁语中，“portare” 是一个常用的动词，通过词形变化等方式衍生出了众多与 “搬运”“携带” 相关的词汇。后来，随着语言的发展和演变，这些词汇被借入到英语中，并在英语中保留了 “port” 这个词根，且其含义也基本保持了与 “搬运”“携带” 相关的意义，如 “import”“export”“transport” 等词都体现了这一基本含义。</p><p>同时，从 “搬运” 这一动作延伸，与货物的装卸、运输等相关的地点也用 “port” 来表示，进而有了 “港口” 的含义，像 “seaport”“airport” 等词都是由此演变而来。这体现了语言在发展过程中，词汇的意义会根据人们的认知和社会发展的需要逐渐扩展和演变。</p><h3 id="表示-“搬运”"><a href="#表示-“搬运”" class="headerlink" title="表示 “搬运”"></a>表示 “搬运”</h3><ul><li>import：im - 表示 “进入”，import 即把物品搬运进入一个国家或地区，意为 “进口”，如：The country imports a large amount of oil.（这个国家进口大量石油。）</li><li>export：ex - 表示 “出去”，export 则是把物品搬运出去，即 “出口”，如：China exports many kinds of products to the world.（中国向世界出口多种产品。）</li><li>transport：trans - 表示 “转移”“跨越”，transport 就是把人或物从一处搬运到另一处，意为 “运输”“运送”，如：The goods were transported by train.（货物通过火车运输。）</li></ul><h3 id="表示-“携带”"><a href="#表示-“携带”" class="headerlink" title="表示 “携带”"></a>表示 “携带”</h3><ul><li>portable：-able 表示 “可…… 的”，portable 即可以携带的，意为 “便携式的”“轻便的”，如：I bought a - portable computer.（我买了一台便携式电脑。）</li><li>porter：表示搬运东西的人，即 “搬运工”“行李员”，如：The porter carried our luggage to the room.（行李员把我们的行李搬到房间。）</li></ul><h3 id="表示-“港口”"><a href="#表示-“港口”" class="headerlink" title="表示 “港口”"></a>表示 “港口”</h3><ul><li>port：本身可作名词表示 “港口”“口岸”，如：The ship arrived at the port safely.（船安全抵达港口。）</li><li>airport：air 表示 “空气”“天空”，airport 即飞机起降的港口，意为 “机场”，如：I will meet you at the airport.（我会在机场接你。）</li><li>seaport：sea 表示 “海”“海洋”，seaport 就是海边的港口，意为 “海港”“港口城市”，如：This city is a famous seaport.（这座城市是一个著名的海港。）</li></ul>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/b8cca4512b3f/</id>
    <link href="https://blog.zisheng.pro/posts/b8cca4512b3f/"/>
    <published>2024-11-18T23:33:25.000Z</published>
    <summary>“all” 的读音是 [ɔːl]，“port” 的读音是 [pɔːrt]</summary>
    <title>
      <![CDATA[All 纠音 & port 词根]]>
    </title>
    <updated>2026-09-18T08:15:28.629Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="英语" scheme="https://blog.zisheng.pro/categories/%E8%8B%B1%E8%AF%AD/"/>
    <category term="priv,Hello" scheme="https://blog.zisheng.pro/tags/priv-Hello/"/>
    <content>
      <![CDATA[<h2 id="Hello-纠音"><a href="#Hello-纠音" class="headerlink" title="Hello 纠音"></a>Hello 纠音</h2><p>Hello 的常见发音是 <code>[həˈləʊ]</code>，以下是对这个单词发音的详细纠音讲解：</p><h3 id="音标分析"><a href="#音标分析" class="headerlink" title="音标分析"></a>音标分析</h3><ul><li><code>[h]</code>：这是一个清辅音，发音时气流从肺部呼出，通过声门时不受阻碍，仅在通过声门时形成轻微的摩擦，声带不振动。发音时，嘴巴张开，舌头自然放松，让气流自然地从口中呼出，同时发出 “喝” 的音，但不要过于用力。例如，在单词 hat <code>[hæt]</code>、hot <code>[hɒt]</code> 中，都有这个音素。</li><li><code>[ə]</code>：这是一个中央元音，发音时舌身中部稍抬起，舌尖微抵下齿龈，双唇向两旁平伸，成扁平形。发音时，口腔肌肉放松，发出类似 “呃” 的音，是一个比较弱的音。比如在单词 about <code>[əˈbaʊt]</code>、again <code>[əˈɡen]</code> 中都有此音。</li><li><code>[ˈləʊ]</code>：这是一个双元音，由 <code>[l]</code> 和 <code>[əʊ]</code> 两个音素组成。首先发 <code>[l]</code> 音，舌尖抵住上齿龈，舌前部向硬腭尽量抬起，气流从舌的两侧泄出，声带振动，这个音类似于汉语中的 “了”，但发音位置更靠后。然后迅速向 <code>[əʊ]</code> 滑动，[əʊ] 的发音是由 <code>[ə]</code> 向 <code>[ʊ]</code> 滑动，双唇由扁平到收圆，声带振动，发出类似 “欧” 的音。在单词 low <code>[ləʊ]</code>、go <code>[ɡəʊ]</code> 中都含有 <code>[əʊ]</code> 这个音素。</li></ul><h3 id="发音技巧"><a href="#发音技巧" class="headerlink" title="发音技巧"></a>发音技巧</h3><ul><li>注意重音在第二个音节，即 <code>[ləʊ]</code> 上，发音时要适当加重这个音节的读音，突出重音，使发音更准确自然。</li><li>整个单词发音要连贯流畅，各个音素之间的过渡要自然，不要有明显的停顿或割裂感。特别是在发双元音 <code>[ˈləʊ]</code> 时，从 <code>[l]</code> 到 <code>[əʊ]</code> 的滑动要迅速而自然，避免生硬。</li></ul><h2 id="priv-词根"><a href="#priv-词根" class="headerlink" title="priv 词根"></a>priv 词根</h2><h3 id="单个"><a href="#单个" class="headerlink" title="单个"></a>单个</h3><p>含义解释：强调个体的、独自的，与集体相对。</p><p>例词：</p><ul><li>private <code>[&#39;praɪvət]</code>：adj. 私人的；个人的；私下的；私有的。<ul><li>This is my private car.（这是我的私人汽车。）</li></ul></li><li>privacy <code>[&#39;prɪvəsi]</code>： n. 隐私；秘密；隐居；隐居处。<ul><li>We should respect everyone’s privacy.（我们应该尊重每个人的隐私。）</li></ul></li><li>privilege <code>[ˈprɪvəlɪdʒ]</code>：n. 特权，优待。&#x3D; priv（单个） + i + lege（法律）<ul><li>Gold card holders enjoy more privileges.（金牌持有者享有更多的特权。）</li></ul></li></ul><h3 id="使丧失"><a href="#使丧失" class="headerlink" title="使丧失"></a>使丧失</h3><p>含义解释：有剥夺、使失去某种权利或东西的意思。</p><p>例词：</p><ul><li>deprive <code>[dɪ&#39;praɪv]</code>：vt. 使丧失，剥夺。<ul><li>The accident deprived him of his legs.（这场事故使他失去了双腿。）</li></ul></li><li>privation <code>[praɪ&#39;veɪʃn]</code>：n. 贫困；丧失；被剥夺了某物的状态。<ul><li>He suffered great privation during the war.（他在战争期间遭受了极大的困苦。）</li></ul></li></ul>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/bad34c984261/</id>
    <link href="https://blog.zisheng.pro/posts/bad34c984261/"/>
    <published>2024-11-14T04:45:44.000Z</published>
    <summary>Hello 并不是哈喽，每日震惊</summary>
    <title>
      <![CDATA[Hello 纠音 & priv 词根]]>
    </title>
    <updated>2026-09-18T08:15:28.629Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="问题排查" scheme="https://blog.zisheng.pro/categories/%E9%97%AE%E9%A2%98%E6%8E%92%E6%9F%A5/"/>
    <category term="Node.js" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/Node-js/"/>
    <category term="Node.js" scheme="https://blog.zisheng.pro/tags/Node-js/"/>
    <category term="OpenSSL" scheme="https://blog.zisheng.pro/tags/OpenSSL/"/>
    <category term="Webpack" scheme="https://blog.zisheng.pro/tags/Webpack/"/>
    <category term="构建" scheme="https://blog.zisheng.pro/tags/%E6%9E%84%E5%BB%BA/"/>
    <category term="排障" scheme="https://blog.zisheng.pro/tags/%E6%8E%92%E9%9A%9C/"/>
    <content>
      <![CDATA[<p><code>error:0308010C:digital envelope routines::unsupported</code> 常出现在把旧前端项目切到 Node.js 17 或更高版本之后。它看起来像业务代码报错，实际是构建工具请求了 OpenSSL 3 默认不再支持的旧算法。</p><h2 id="一句话总结"><a href="#一句话总结" class="headerlink" title="一句话总结"></a>一句话总结</h2><p><code>--openssl-legacy-provider</code> 只能救急。长期方案是升级 Webpack&#x2F;相关 loader，或把项目固定在兼容的 Node LTS；不要把这个环境变量当作永久配置。</p><h2 id="如何确认根因"><a href="#如何确认根因" class="headerlink" title="如何确认根因"></a>如何确认根因</h2><p>如果堆栈里包含 <code>webpack/lib/util/createHash</code>、<code>ERR_OSSL_EVP_UNSUPPORTED</code>，且故障发生在 <code>dev</code> 或 <code>build</code> 初期，基本就是这个问题。常见组合是旧版 Webpack 4 与 Node.js 17+。</p><h2 id="临时恢复构建"><a href="#临时恢复构建" class="headerlink" title="临时恢复构建"></a>临时恢复构建</h2><p>macOS &#x2F; Linux：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">export</span> NODE_OPTIONS=--openssl-legacy-provider</span><br><span class="line">pnpm build</span><br></pre></td></tr></table></figure><p>Windows PowerShell：</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="variable">$env:NODE_OPTIONS</span>=<span class="string">&#x27;--openssl-legacy-provider&#x27;</span></span><br><span class="line">pnpm build</span><br></pre></td></tr></table></figure><p>也可以仅给某个 script 设置该变量；重点是把它视为短期兼容层，并记录清理期限。</p><h2 id="优先的长期修复"><a href="#优先的长期修复" class="headerlink" title="优先的长期修复"></a>优先的长期修复</h2><ol><li>升级 Webpack、React Scripts 或框架脚手架到支持 OpenSSL 3 的版本。</li><li>同时升级依赖它们的 plugin、loader，避免 lockfile 把旧依赖重新拉回来。</li><li>对短期无法升级的遗留项目，使用项目约定的 Node LTS，并通过 <code>.nvmrc</code>、Volta 或 CI 固定版本。</li></ol><p>例如：</p><figure class="highlight txt"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"># .nvmrc</span><br><span class="line">16</span><br></pre></td></tr></table></figure><p>需要注意：选择旧 LTS 是为了给升级争取窗口，不是长期停留在已停止维护的运行时。</p><h2 id="不要混淆的场景"><a href="#不要混淆的场景" class="headerlink" title="不要混淆的场景"></a>不要混淆的场景</h2><p>如果错误发生在运行时的加密、签名或 TLS 逻辑中，而不是构建阶段，不能简单套用 legacy provider。应先确认算法、证书或上游 SDK 是否需要迁移，否则可能降低安全性。</p><h2 id="我的判断框架"><a href="#我的判断框架" class="headerlink" title="我的判断框架"></a>我的判断框架</h2><p>可以临时用：旧项目本地开发或一次性构建恢复。</p><p>优先修：持续交付、生产镜像和即将升级 Node 的项目；把工具链升级纳入技术债计划。</p>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/632fe69d082f/</id>
    <link href="https://blog.zisheng.pro/posts/632fe69d082f/"/>
    <published>2024-10-24T09:48:38.000Z</published>
    <summary>Node.js 17+ 升级 OpenSSL 3 后，旧版 Webpack 等构建工具可能触发 error:0308010C。本文说明根因、临时绕过方案，以及应当优先做的长期修复。</summary>
    <title>「已解决」error:0308010C:digital envelope routines::unsupported</title>
    <updated>2026-09-18T08:15:28.629Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="紫升翻译计划" scheme="https://blog.zisheng.pro/categories/%E7%B4%AB%E5%8D%87%E7%BF%BB%E8%AF%91%E8%AE%A1%E5%88%92/"/>
    <category term="Node.js" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/Node-js/"/>
    <category term="Node.js" scheme="https://blog.zisheng.pro/tags/Node-js/"/>
    <category term="服务器创建" scheme="https://blog.zisheng.pro/tags/%E6%9C%8D%E5%8A%A1%E5%99%A8%E5%88%9B%E5%BB%BA/"/>
    <category term="express" scheme="https://blog.zisheng.pro/tags/express/"/>
    <category term="ws" scheme="https://blog.zisheng.pro/tags/ws/"/>
    <category term="multer" scheme="https://blog.zisheng.pro/tags/multer/"/>
    <category term="后端开发基础" scheme="https://blog.zisheng.pro/tags/%E5%90%8E%E7%AB%AF%E5%BC%80%E5%8F%91%E5%9F%BA%E7%A1%80/"/>
    <category term="服务器工作原理" scheme="https://blog.zisheng.pro/tags/%E6%9C%8D%E5%8A%A1%E5%99%A8%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86/"/>
    <content>
      <![CDATA[<blockquote><p>原文地址：<a href="https://dev.to/trixsec/how-to-turn-your-device-into-a-simple-server-using-nodejs-1460">How to Turn Your Device Into a Simple Server Using Node.js</a><br>原文作者：<a href="https://dev.to/trixsec">Trix Cyrus</a></p></blockquote><p>让我们一起在你的设备上部署一个简单的 Node.js。</p><h2 id="入门"><a href="#入门" class="headerlink" title="入门"></a>入门</h2><p>首先，让我们使用 Node.js 创建一个基础的服务。步骤如下：</p><p>创建一个 <code>node-server</code> 目录：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">mkdir</span> node-server</span><br><span class="line"><span class="built_in">cd</span> node-server</span><br></pre></td></tr></table></figure><p>初始化一个 Node.js 项目：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm init -y</span><br></pre></td></tr></table></figure><p>安装依赖：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm install express ws multer mongoose dotenv helmet morgan cluster compression</span><br></pre></td></tr></table></figure><ul><li>express：处理 HTTP 请求</li><li>ws：支持 Websocket</li><li>multer：处理文件上传</li><li>mongoose：MongoDB 链接</li><li>dotenv：读取环境变量</li><li>helmet：保护服务器安全</li><li>morgan：日志</li><li>cluster：多线程</li><li>compression：用于压缩响应数据</li></ul><h2 id="创建一个-env-文件"><a href="#创建一个-env-文件" class="headerlink" title="创建一个 .env 文件"></a>创建一个 .env 文件</h2><p>在项目根目录，创建一个 <code>.env</code> 文件，它的作用是用来存储环境变量：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">PORT=8080</span><br><span class="line">MONGO_URI=mongodb://localhost:27017/serverdb</span><br></pre></td></tr></table></figure><p>创建一个 <code>server.js</code> 文件：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">touch</span> server.js</span><br><span class="line">nano server.js</span><br></pre></td></tr></table></figure><p>粘贴下面的代码：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">require</span>(<span class="string">&#x27;dotenv&#x27;</span>).<span class="title function_">config</span>();</span><br><span class="line"><span class="keyword">const</span> express = <span class="built_in">require</span>(<span class="string">&#x27;express&#x27;</span>);</span><br><span class="line"><span class="keyword">const</span> ws = <span class="built_in">require</span>(<span class="string">&#x27;ws&#x27;</span>);</span><br><span class="line"><span class="keyword">const</span> http = <span class="built_in">require</span>(<span class="string">&#x27;http&#x27;</span>);</span><br><span class="line"><span class="keyword">const</span> multer = <span class="built_in">require</span>(<span class="string">&#x27;multer&#x27;</span>);</span><br><span class="line"><span class="keyword">const</span> mongoose = <span class="built_in">require</span>(<span class="string">&#x27;mongoose&#x27;</span>);</span><br><span class="line"><span class="keyword">const</span> helmet = <span class="built_in">require</span>(<span class="string">&#x27;helmet&#x27;</span>);</span><br><span class="line"><span class="keyword">const</span> morgan = <span class="built_in">require</span>(<span class="string">&#x27;morgan&#x27;</span>);</span><br><span class="line"><span class="keyword">const</span> cluster = <span class="built_in">require</span>(<span class="string">&#x27;cluster&#x27;</span>);</span><br><span class="line"><span class="keyword">const</span> os = <span class="built_in">require</span>(<span class="string">&#x27;os&#x27;</span>);</span><br><span class="line"><span class="keyword">const</span> compression = <span class="built_in">require</span>(<span class="string">&#x27;compression&#x27;</span>);</span><br><span class="line"><span class="keyword">const</span> fs = <span class="built_in">require</span>(<span class="string">&#x27;fs&#x27;</span>);</span><br><span class="line"><span class="keyword">const</span> path = <span class="built_in">require</span>(<span class="string">&#x27;path&#x27;</span>);</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="variable constant_">PORT</span> = process.<span class="property">env</span>.<span class="property">PORT</span> || <span class="number">8080</span>;</span><br><span class="line"><span class="keyword">const</span> <span class="variable constant_">MONGO_URI</span> = process.<span class="property">env</span>.<span class="property">MONGO_URI</span>;</span><br><span class="line"><span class="keyword">const</span> <span class="variable constant_">NUM_CPUS</span> = os.<span class="title function_">cpus</span>().<span class="property">length</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> (cluster.<span class="property">isMaster</span>) &#123;</span><br><span class="line">  <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`Master process is running. Spawning <span class="subst">$&#123;NUM_CPUS&#125;</span> workers...`</span>);</span><br><span class="line">  <span class="keyword">for</span> (<span class="keyword">let</span> i = <span class="number">0</span>; i &lt; <span class="variable constant_">NUM_CPUS</span>; i++) &#123;</span><br><span class="line">    cluster.<span class="title function_">fork</span>();</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  cluster.<span class="title function_">on</span>(<span class="string">&#x27;exit&#x27;</span>, <span class="function">(<span class="params">worker, code, signal</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`Worker <span class="subst">$&#123;worker.process.pid&#125;</span> died. Spawning a new worker.`</span>);</span><br><span class="line">    cluster.<span class="title function_">fork</span>();</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="keyword">const</span> app = <span class="title function_">express</span>();</span><br><span class="line">  <span class="keyword">const</span> server = http.<span class="title function_">createServer</span>(app);</span><br><span class="line">  <span class="keyword">const</span> wss = <span class="keyword">new</span> ws.<span class="title class_">Server</span>(&#123; server &#125;);</span><br><span class="line"></span><br><span class="line">  mongoose.<span class="title function_">connect</span>(<span class="variable constant_">MONGO_URI</span>, &#123; <span class="attr">useNewUrlParser</span>: <span class="literal">true</span>, <span class="attr">useUnifiedTopology</span>: <span class="literal">true</span> &#125;)</span><br><span class="line">    .<span class="title function_">then</span>(<span class="function">() =&gt;</span> <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">&#x27;Connected to MongoDB&#x27;</span>))</span><br><span class="line">    .<span class="title function_">catch</span>(<span class="function"><span class="params">err</span> =&gt;</span> <span class="variable language_">console</span>.<span class="title function_">error</span>(<span class="string">&#x27;MongoDB connection error:&#x27;</span>, err));</span><br><span class="line"></span><br><span class="line">  app.<span class="title function_">use</span>(express.<span class="title function_">json</span>());</span><br><span class="line">  app.<span class="title function_">use</span>(<span class="title function_">helmet</span>());</span><br><span class="line">  app.<span class="title function_">use</span>(<span class="title function_">morgan</span>(<span class="string">&#x27;common&#x27;</span>));</span><br><span class="line">  app.<span class="title function_">use</span>(<span class="title function_">compression</span>());</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> upload = <span class="title function_">multer</span>(&#123; <span class="attr">dest</span>: <span class="string">&#x27;uploads/&#x27;</span> &#125;);</span><br><span class="line"></span><br><span class="line">  app.<span class="title function_">use</span>(express.<span class="title function_">static</span>(path.<span class="title function_">join</span>(__dirname, <span class="string">&#x27;public&#x27;</span>)));</span><br><span class="line"></span><br><span class="line">  app.<span class="title function_">use</span>(<span class="function">(<span class="params">req, res, next</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`Received <span class="subst">$&#123;req.method&#125;</span> request for <span class="subst">$&#123;req.url&#125;</span>`</span>);</span><br><span class="line">    <span class="title function_">next</span>();</span><br><span class="line">  &#125;);</span><br><span class="line"></span><br><span class="line">  wss.<span class="title function_">on</span>(<span class="string">&#x27;connection&#x27;</span>, <span class="function">(<span class="params">socket</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;WebSocket connection established.&#x27;</span>);</span><br><span class="line">    socket.<span class="title function_">on</span>(<span class="string">&#x27;message&#x27;</span>, <span class="function">(<span class="params">message</span>) =&gt;</span> &#123;</span><br><span class="line">      <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`Received WebSocket message: <span class="subst">$&#123;message&#125;</span>`</span>);</span><br><span class="line">      socket.<span class="title function_">send</span>(<span class="string">`Message received: <span class="subst">$&#123;message&#125;</span>`</span>);</span><br><span class="line">    &#125;);</span><br><span class="line">  &#125;);</span><br><span class="line"></span><br><span class="line">  app.<span class="title function_">get</span>(<span class="string">&#x27;/&#x27;</span>, <span class="function">(<span class="params">req, res</span>) =&gt;</span> &#123;</span><br><span class="line">    res.<span class="title function_">sendFile</span>(path.<span class="title function_">join</span>(__dirname, <span class="string">&#x27;public&#x27;</span>, <span class="string">&#x27;index.html&#x27;</span>));</span><br><span class="line">  &#125;);</span><br><span class="line"></span><br><span class="line">  app.<span class="title function_">get</span>(<span class="string">&#x27;/api/data&#x27;</span>, <span class="keyword">async</span> (req, res) =&gt; &#123;</span><br><span class="line">    res.<span class="title function_">json</span>(&#123; <span class="attr">message</span>: <span class="string">&#x27;Server API Response&#x27;</span>, <span class="attr">data</span>: [] &#125;);</span><br><span class="line">  &#125;);</span><br><span class="line"></span><br><span class="line">  app.<span class="title function_">post</span>(<span class="string">&#x27;/upload&#x27;</span>, upload.<span class="title function_">single</span>(<span class="string">&#x27;file&#x27;</span>), <span class="function">(<span class="params">req, res</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;File uploaded:&#x27;</span>, req.<span class="property">file</span>);</span><br><span class="line">    res.<span class="title function_">status</span>(<span class="number">201</span>).<span class="title function_">json</span>(&#123; <span class="attr">message</span>: <span class="string">&#x27;File uploaded successfully&#x27;</span>, <span class="attr">file</span>: req.<span class="property">file</span> &#125;);</span><br><span class="line">  &#125;);</span><br><span class="line"></span><br><span class="line">  app.<span class="title function_">get</span>(<span class="string">&#x27;/greet/:name&#x27;</span>, <span class="function">(<span class="params">req, res</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">const</span> name = req.<span class="property">params</span>.<span class="property">name</span> || <span class="string">&#x27;stranger&#x27;</span>;</span><br><span class="line">    res.<span class="title function_">send</span>(<span class="string">`&lt;h1&gt;Hello, <span class="subst">$&#123;name&#125;</span>!&lt;/h1&gt;`</span>);</span><br><span class="line">  &#125;);</span><br><span class="line"></span><br><span class="line">  app.<span class="title function_">use</span>(<span class="function">(<span class="params">req, res, next</span>) =&gt;</span> &#123;</span><br><span class="line">    res.<span class="title function_">status</span>(<span class="number">404</span>).<span class="title function_">sendFile</span>(path.<span class="title function_">join</span>(__dirname, <span class="string">&#x27;public&#x27;</span>, <span class="string">&#x27;404.html&#x27;</span>));</span><br><span class="line">  &#125;);</span><br><span class="line"></span><br><span class="line">  app.<span class="title function_">use</span>(<span class="function">(<span class="params">err, req, res, next</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;Error encountered:&#x27;</span>, err);</span><br><span class="line">    res.<span class="title function_">status</span>(<span class="number">500</span>).<span class="title function_">json</span>(&#123; <span class="attr">error</span>: <span class="string">&#x27;An internal server error occurred&#x27;</span> &#125;);</span><br><span class="line">  &#125;);</span><br><span class="line"></span><br><span class="line">  server.<span class="title function_">listen</span>(<span class="variable constant_">PORT</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">`Worker process <span class="subst">$&#123;process.pid&#125;</span> is running on port <span class="subst">$&#123;PORT&#125;</span>`</span>);</span><br><span class="line">  &#125;);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>创建一个 <code>public</code> 文件夹：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">mkdir</span> public</span><br><span class="line"><span class="built_in">cd</span> public</span><br></pre></td></tr></table></figure><h2 id="index-html"><a href="#index-html" class="headerlink" title="index.html"></a>index.html</h2><p>用作的首页的基本的 HTML 文件：</p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">&lt;!DOCTYPE <span class="keyword">html</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">html</span> <span class="attr">lang</span>=<span class="string">&quot;en&quot;</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">head</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">meta</span> <span class="attr">charset</span>=<span class="string">&quot;UTF-8&quot;</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">meta</span> <span class="attr">name</span>=<span class="string">&quot;viewport&quot;</span> <span class="attr">content</span>=<span class="string">&quot;width=device-width, initial-scale=1.0&quot;</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">title</span>&gt;</span>Node.js Server<span class="tag">&lt;/<span class="name">title</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">head</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">body</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">h1</span>&gt;</span>Welcome to the node.js Server!<span class="tag">&lt;/<span class="name">h1</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">p</span>&gt;</span>This server is a simple server.<span class="tag">&lt;/<span class="name">p</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">body</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">html</span>&gt;</span></span><br></pre></td></tr></table></figure><h3 id="404-html"><a href="#404-html" class="headerlink" title="404.html"></a>404.html</h3><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">&lt;!DOCTYPE <span class="keyword">html</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">html</span> <span class="attr">lang</span>=<span class="string">&quot;en&quot;</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">head</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">meta</span> <span class="attr">charset</span>=<span class="string">&quot;UTF-8&quot;</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">meta</span> <span class="attr">name</span>=<span class="string">&quot;viewport&quot;</span> <span class="attr">content</span>=<span class="string">&quot;width=device-width, initial-scale=1.0&quot;</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">title</span>&gt;</span>404 - Page Not Found<span class="tag">&lt;/<span class="name">title</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">head</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">body</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">h1</span>&gt;</span>404 - Page Not Found<span class="tag">&lt;/<span class="name">h1</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">p</span>&gt;</span>The page you are looking for doesn&#x27;t exist in this universe.<span class="tag">&lt;/<span class="name">p</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">body</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">html</span>&gt;</span></span><br></pre></td></tr></table></figure><h2 id="启动服务器"><a href="#启动服务器" class="headerlink" title="启动服务器"></a>启动服务器</h2><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">node server.js</span><br></pre></td></tr></table></figure><h2 id="测试服务器功能"><a href="#测试服务器功能" class="headerlink" title="测试服务器功能"></a>测试服务器功能</h2><ul><li>实时通信：您可以设置一个 WebSocket 客户端向服务器发送消息。</li><li>文件上传：向“<a href="http://localhost:8080/upload%E2%80%9D%E5%8F%91%E9%80%81">http://localhost:8080/upload”发送</a> POST 请求并附上一个文件，它将被保存到上传文件夹中。</li><li>RESTful API：访问“<a href="http://localhost:8080/api/data%E2%80%9D%E4%BB%A5%E6%9F%A5%E7%9C%8B">http://localhost:8080/api/data”以查看</a> API 响应。</li><li>错误处理：访问任何不存在的 URL（例如“<a href="http://localhost:8080/nonexistent%E2%80%9D%EF%BC%89%E4%BB%A5%E6%9F%A5%E7%9C%8B%E8%87%AA%E5%AE%9A%E4%B9%89%E7%9A%84">http://localhost:8080/nonexistent”）以查看自定义的</a> 404 页面。</li><li>数据库：将您的服务器连接到一个 MongoDB 实例，并修改&#x2F;api&#x2F;data 路由以查询并返回实际数据。</li></ul><p>最终目录：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">node-server/</span><br><span class="line">├── public/</span><br><span class="line">│   ├── index.html</span><br><span class="line">│   ├── 404.html</span><br><span class="line">├── server.js</span><br></pre></td></tr></table></figure><h2 id="一切就绪"><a href="#一切就绪" class="headerlink" title="一切就绪"></a>一切就绪</h2><p>你已经在你的设备创建并运行一个简单的 Node.js 服务器！这是学习后端开发基础知识以及了解服务器如何工作的好方法。</p>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/421dcbacb7e8/</id>
    <link href="https://blog.zisheng.pro/posts/421dcbacb7e8/"/>
    <published>2024-09-25T10:27:35.000Z</published>
    <summary>本文是一篇关于在设备上使用 Node.js 创建简单服务器的教程。作者首先详细介绍了创建服务器的具体步骤，包括创建目录、初始化项目、安装依赖、创建配置文件和服务器脚本等。接着展示了最终的目录结构，并对服务器脚本进行了详细分析，解释了每个安装的依赖的用途。此外，还介绍了测试服务器的多种方式，如实时通信、文件上传、访问 RESTful API、触发错误处理以及连接数据库等。最后强调了通过创建这个简单服务器来学习后端开发基础和了解服务器工作原理的价值。</summary>
    <title>如何使用 Node.js 将你的设备变成一个简单的服务器</title>
    <updated>2026-09-18T08:15:28.636Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="Google" scheme="https://blog.zisheng.pro/categories/Google/"/>
    <category term="Google AdSense" scheme="https://blog.zisheng.pro/categories/Google/Google-AdSense/"/>
    <category term="站点运营" scheme="https://blog.zisheng.pro/categories/%E7%AB%99%E7%82%B9%E8%BF%90%E8%90%A5/"/>
    <category term="广告" scheme="https://blog.zisheng.pro/categories/%E7%AB%99%E7%82%B9%E8%BF%90%E8%90%A5/%E5%B9%BF%E5%91%8A/"/>
    <category term="Google AdSense" scheme="https://blog.zisheng.pro/tags/Google-AdSense/"/>
    <category term="google adsense eCPM" scheme="https://blog.zisheng.pro/tags/google-adsense-eCPM/"/>
    <category term="google adsense eCPM 优化" scheme="https://blog.zisheng.pro/tags/google-adsense-eCPM-%E4%BC%98%E5%8C%96/"/>
    <content>
      <![CDATA[<h2 id="保险（Insurance）"><a href="#保险（Insurance）" class="headerlink" title="保险（Insurance）"></a>保险（Insurance）</h2><p>保险行业广告费投入多，主要有以下几方面原因：</p><p>1、行业竞争激烈</p><ul><li>市场主体众多：保险市场上的公司数量不断增加，无论是大型保险公司还是小型保险企业，都希望在市场中占据一席之地。截至2023年，国内的保险公司数量众多，各公司在产品、服务、价格等方面的差异较小，所以需要通过大量的广告宣传来突出自己的品牌和优势，吸引消费者的关注。例如，在车险市场，有多家大型保险公司以及众多中小保险公司竞争，为了争夺客户，都加大了广告投入。</li><li>产品同质化严重：保险产品在条款、保障范围等方面具有较高的相似性。对于消费者来说，很难从产品本身直观地分辨出不同公司产品的差异。因此，保险公司需要通过广告宣传来塑造品牌形象，让消费者对其品牌产生认知和信任，从而在众多竞品中选择自己的产品。</li></ul><p>2、品牌建设需求高：</p><ul><li>建立信任基础：保险产品是一种基于信任的金融服务，消费者在购买保险时，对保险公司的信誉和可靠性非常关注。通过大规模的广告宣传，保险公司可以向消费者传递自己的专业、可靠、稳健的形象，增强消费者对公司的信任。特别是对于一些新成立的保险公司或者知名度较低的公司，更需要通过广告来快速提升品牌知名度和信任度。</li><li>提升品牌影响力：在保险行业，品牌影响力对于公司的发展至关重要。具有较高品牌影响力的公司能够吸引更多的客户、合作伙伴和优秀的人才。广告宣传是提升品牌影响力的重要手段之一，通过在各种媒体上的广泛传播，保险公司的品牌能够被更多的人所了解和认可。</li></ul><p>3、客户获取成本高：</p><ul><li>销售模式的特点：保险产品的销售通常需要通过代理人、经纪人等中介渠道，或者通过电话销售、网络销售等方式进行。这些销售方式都需要投入大量的人力、物力和财力。相比其他行业，保险产品的销售周期较长，客户的决策过程也较为复杂，需要不断地进行沟通和解释。因此，保险公司需要通过广告宣传来提高客户的认知度和兴趣度，降低销售成本。</li><li>客户群体广泛：保险产品的潜在客户群体非常广泛，包括个人客户和企业客户，涵盖了不同年龄、性别、职业、收入水平的人群。为了覆盖到尽可能多的潜在客户，保险公司需要在各种媒体平台上进行广告投放，这也导致了广告费用的增加。</li></ul><p>4、业务拓展的需要：</p><ul><li>新产品推广：保险公司不断推出新的保险产品和服务，以满足市场需求和客户的个性化需求。新产品的推出需要进行大量的宣传推广，让消费者了解产品的特点、优势和适用场景。广告是新产品推广的重要手段之一，能够快速地将产品信息传递给潜在客户，提高产品的知名度和市场占有率。</li><li>开拓新市场：随着经济的发展和人们保险意识的提高，保险市场的潜力不断被挖掘。保险公司需要不断开拓新的市场领域，如农村市场、互联网保险市场等。在开拓新市场的过程中，广告宣传能够帮助保险公司快速地进入市场，提高品牌知名度和市场份额。</li></ul><p>5、监管要求和信息披露：</p><ul><li>合规宣传的需要：保险行业受到严格的监管，保险公司需要按照监管要求进行信息披露和宣传。为了确保广告内容的合规性和准确性，保险公司需要投入一定的资源进行广告策划、制作和审核。同时，监管部门也要求保险公司在广告宣传中不得进行虚假宣传、误导消费者等行为，这也增加了保险公司的广告成本。</li><li>风险提示和教育：保险产品涉及到复杂的条款和风险，消费者在购买保险时需要了解相关的风险和注意事项。保险公司需要通过广告宣传来进行风险提示和保险知识教育，帮助消费者正确理解保险产品，提高消费者的风险意识和保险素养。这也需要保险公司投入一定的广告费用。</li></ul><h3 id="推荐关键词"><a href="#推荐关键词" class="headerlink" title="推荐关键词"></a>推荐关键词</h3><ul><li>Insurance for employees 2024</li><li>Guardian Term Life Insurance</li><li>Recommend a nearby auto insurance company</li><li>Auto insurance comparison</li><li>Best car insurance policies</li><li>Insurance claim process</li><li>Affordable auto insurance</li><li>Comprehensive car insurance</li><li>High coverage auto insurance</li><li>Reliable insurance companies</li><li>Insurance discounts for safe drivers</li><li>Long-term car insurance plans</li><li>Insurance for luxury cars</li></ul><h2 id="法律（Lawyer）"><a href="#法律（Lawyer）" class="headerlink" title="法律（Lawyer）"></a>法律（Lawyer）</h2><ul><li>Medical Malpractice Lawyer</li><li>Legal advice</li><li>Business law</li><li>Criminal law</li><li>Family law</li><li>Contract law</li><li>Intellectual property law</li><li>Employment law</li><li>Real estate law</li><li>Environmental law</li><li>International law</li></ul><h2 id="教育（education）"><a href="#教育（education）" class="headerlink" title="教育（education）"></a>教育（education）</h2><ul><li>mba best colleges</li><li>1 year Master’s degrees</li><li>Recommend a nearby scholarships</li><li>Quality education resources</li><li>Innovative teaching methods</li><li>Best educational institutions</li><li>Online education platforms</li><li>Distance learning opportunities</li><li>Educational technology tools</li><li>Early childhood education</li><li>Higher education scholarships</li><li>Lifelong learning programs</li><li>Special education needs</li></ul><h2 id="律师（Attorney）"><a href="#律师（Attorney）" class="headerlink" title="律师（Attorney）"></a>律师（Attorney）</h2><ul><li>Top lawyers nearby</li><li>Best law firms for economy</li><li>Experienced criminal defense lawyers</li><li>Family law attorneys near me</li><li>Business litigation lawyers</li><li>Intellectual property lawyers</li><li>Personal injury lawyers reviews</li><li>Immigration lawyers services</li><li>Real estate lawyers advice</li><li>Bankruptcy lawyers assistance</li></ul><h2 id="AI-推荐"><a href="#AI-推荐" class="headerlink" title="AI 推荐"></a>AI 推荐</h2><ul><li>Financial planning（财务规划）：Deals with personal finance which attracts advertisers in the banking and investment sectors.</li><li>Health insurance comparison（健康保险比较）：High demand as people are constantly looking for the best health insurance options.</li><li>Luxury travel destinations（豪华旅游目的地）：Appeals to high-end travel companies and luxury brands.</li><li>Online business courses（在线商业课程）：Reason: Targets entrepreneurs and professionals looking to enhance their skills.</li><li>Smart home technology（智能家居技术）：A growing market with many companies advertising their smart home products.</li><li>Organic skincare products（有机护肤品）：Popular in the beauty and wellness industry.</li><li>Pet care services（宠物护理服务）：Pet owners are always looking for good care options for their furry friends.</li><li>Fitness equipment reviews（健身器材评价）：People interested in fitness are likely to click on ads related to fitness equipment.</li><li>Wedding planning ideas（婚礼策划创意）：A lucrative market with many advertisers in the wedding industry.</li><li>Green energy solutions（绿色能源解决方案）：As the demand for sustainable energy grows, advertisers in this field are likely to pay well.</li></ul><h2 id="Youtuber-推荐"><a href="#Youtuber-推荐" class="headerlink" title="Youtuber 推荐"></a>Youtuber 推荐</h2><ul><li>PaloAltoNetworks</li><li>Allergan</li><li>Avemco</li></ul>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/fe9898219413/</id>
    <link href="https://blog.zisheng.pro/posts/fe9898219413/"/>
    <published>2024-09-15T02:12:11.000Z</published>
    <summary>Google AdSense Shifts To eCPM Payment Model</summary>
    <title>Google Adsense High eCPM</title>
    <updated>2026-09-18T08:15:28.629Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="方法论" scheme="https://blog.zisheng.pro/categories/%E6%96%B9%E6%B3%95%E8%AE%BA/"/>
    <category term="英语" scheme="https://blog.zisheng.pro/tags/%E8%8B%B1%E8%AF%AD/"/>
    <content>
      <![CDATA[<h2 id="学习方法"><a href="#学习方法" class="headerlink" title="学习方法"></a>学习方法</h2><h3 id="词汇积累"><a href="#词汇积累" class="headerlink" title="词汇积累"></a>词汇积累</h3><ol><li>制定计划：确定每天要学习的词汇量，比如每天学习 20 个新单词。可以利用单词书、手机应用（如百词斩、扇贝单词等）来辅助学习。</li><li>语境记忆：将单词放在具体的语境中去理解和记忆，比如阅读英文文章、观看英语电影或电视剧。遇到生词时，结合上下文猜测其含义，然后再查阅词典进行确认。</li><li>词根词缀法：学习常见的词根、词缀，这样可以帮助你推断出许多陌生单词的含义。例如，“un-”表示否定，“re-”表示再次等。</li><li>联想记忆：将新学的单词与已知的事物、场景或概念联系起来，创造有趣的联想，以便更好地记住它们。</li></ol><h3 id="听力训练"><a href="#听力训练" class="headerlink" title="听力训练"></a>听力训练</h3><ol><li>多听英语广播：如 BBC、CNN 等，让自己沉浸在英语环境中。可以在早上起床后、做家务时或者乘坐交通工具时收听。</li><li>观看英语电影和电视剧：选择有英文字幕的影片，先观看一遍了解剧情，然后再反复观看，尝试不看字幕理解对话内容。</li><li>听英语歌曲：选择自己喜欢的英语歌曲，边听边看歌词，学习其中的词汇和表达方式。还可以跟着哼唱，提高口语发音。</li><li>做听力练习：可以使用英语学习教材配套的听力材料，或者在网上搜索专门的听力练习题。做完练习后，对照答案分析错误原因，逐步提高听力水平。</li></ol><h3 id="口语练习"><a href="#口语练习" class="headerlink" title="口语练习"></a>口语练习</h3><ol><li>模仿跟读：选择英语原声材料，如英语电影片段、英语演讲等，进行模仿跟读。注意模仿语音、语调、语速和停顿，尽量让自己的发音更标准。</li><li>口语交流：寻找英语学习伙伴或者参加英语角，与他人进行口语交流。不要害怕犯错，勇敢地表达自己的想法。</li><li>录制自己的声音：用手机或录音设备录制自己的英语口语练习，然后回放听一听，找出不足之处并加以改进。</li><li>参加口语课程：报名参加线上或线下的英语口语课程，有专业的老师指导可以更快地提高口语水平。</li></ol><h3 id="阅读提升"><a href="#阅读提升" class="headerlink" title="阅读提升"></a>阅读提升</h3><ol><li>选择适合的阅读材料：根据自己的英语水平选择难度适中的英文书籍、报纸、杂志或文章。可以从简单的儿童读物开始，逐渐过渡到更复杂的文学作品。</li><li>精读与泛读结合：精读时，仔细分析文章的结构、语法和词汇，理解每一个句子的含义。泛读时，快速浏览文章，了解大意即可，主要是提高阅读速度和语感。</li><li>做阅读笔记：在阅读过程中，遇到好的词汇、短语和句子可以记录下来，方便日后复习和运用。</li><li>阅读挑战：给自己设定阅读目标，比如每周读完一本英文书或者每天阅读一定数量的文章，坚持下去可以提高阅读能力。</li></ol><h3 id="写作锻炼"><a href="#写作锻炼" class="headerlink" title="写作锻炼"></a>写作锻炼</h3><ol><li>仿写练习：选择优秀的英语范文，模仿其结构、风格和表达方式进行写作练习。可以从简单的句子仿写开始，逐渐过渡到段落和文章的仿写。</li><li>坚持写作日记：用英语记录自己的日常生活、想法和感受。这不仅可以提高写作能力，还能帮助你更好地掌握词汇和语法。</li><li>找老师或伙伴批改：将自己写的文章交给老师、英语学习伙伴或者使用在线写作批改工具，让他们指出其中的错误和不足之处，以便及时改进。</li><li>参加写作比赛：参加英语写作比赛可以激发自己的学习动力，同时也能从其他参赛者的作品中学习到很多写作技巧和经验。</li></ol><h2 id="英文环境搭建"><a href="#英文环境搭建" class="headerlink" title="英文环境搭建"></a>英文环境搭建</h2><p>为了更好地耳濡目染，将每天使用频率最高的工具设置成英文版本。</p><ul><li>vscode studio 不安装汉化插件</li><li>手机系统设置成英文</li></ul><h2 id="英语学习资源"><a href="#英语学习资源" class="headerlink" title="英语学习资源"></a>英语学习资源</h2><p>以下是一些适合程序员学习英语的学习资源：</p><h3 id="在线学习平台"><a href="#在线学习平台" class="headerlink" title="在线学习平台"></a>在线学习平台</h3><ul><li><strong>Codecademy</strong>：这是一个在线的交互式编程学习平台，提供多种编程语言课程。在学习编程语言的同时，注重教授与编程相关的英语技能，用户可以通过项目和任务在真实编程场景中使用英语，加深记忆。</li><li><strong>Udemy</strong>：有大量由专业人士或机构开设的编程课程，部分课程是全英文教学，课程内容丰富多样，涵盖了从基础到高级的各种编程技能和知识，可以根据自己的需求选择适合的课程来学习英语和编程。</li></ul><h3 id="技术文档和官方网站"><a href="#技术文档和官方网站" class="headerlink" title="技术文档和官方网站"></a>技术文档和官方网站</h3><ul><li><a href="https://react.dev/">react.dev</a></li><li><a href="https://reactflow.dev/">reactflow.dev</a></li><li><a href="https://www.patterns.dev/">patterns.dev</a></li><li><a href="https://nodejs.org/en/learn/getting-started/nodejs-with-typescript">nodejs.org</a></li><li><a href="https://javascript.info/">javascript.info</a></li></ul><h3 id="英语学习应用"><a href="#英语学习应用" class="headerlink" title="英语学习应用"></a>英语学习应用</h3><ul><li>百词斩：一个专注于英语学习的单词记忆应用，可以帮助你记忆单词、复习单词、查找单词、背单词等。</li><li>豆包：字节跳动大模型英语，划词翻译、收藏、AI 助教等功能很适合日常使用。</li></ul><h3 id="英文技术博客和论坛"><a href="#英文技术博客和论坛" class="headerlink" title="英文技术博客和论坛"></a>英文技术博客和论坛</h3><ul><li><strong>Stack Overflow</strong>：全球最知名的技术问答社区，程序员可以在这里找到各种技术问题的解答和讨论。阅读和参与 Stack Overflow 上的问答，能够接触到大量的专业英语表述和技术讨论，提高英语阅读和写作能力。</li><li><strong>Medium</strong>：有许多程序员和技术专家在上面分享自己的编程经验、技术见解和学习心得等文章。阅读 Medium 上的英文技术文章，可以了解行业的最新动态和技术趋势，同时提升英语阅读和理解能力。</li><li><strong>Reddit</strong> 的相关技术子论坛，如 &#x2F;r&#x2F;programming、&#x2F;r&#x2F;learnprogramming 等，程序员们可以在这些论坛上交流技术问题，阅读和参与讨论也能提升英语水平。</li></ul><h3 id="视频教程平台"><a href="#视频教程平台" class="headerlink" title="视频教程平台"></a>视频教程平台</h3><ul><li><strong>YouTube</strong>：有许多程序员上传的英文编程教程、技术分享、项目演示等视频，内容丰富多样，可以根据自己的兴趣和需求选择观看，锻炼英语听力和理解能力，同时学习编程知识。</li><li><strong>B 站</strong>：也有一些搬运或原创的英文编程教学视频，部分视频还会配有中文字幕，方便学习和理解。</li></ul><h2 id="英语学习计划"><a href="#英语学习计划" class="headerlink" title="英语学习计划"></a>英语学习计划</h2><p>英文写作可以使用人工智能解决，但是使用频次高的词汇、阅读、听力和口语就必须熟练掌握了。毕竟，借助人工智能会打断你的阅读，更别提人工智能没法在实时交流是帮上忙了。</p><ul><li>翻译英文文档一篇：锻炼词汇、阅读能力</li><li>精听一篇英文文章：锻炼听力能力</li><li>TalkAI 进行模拟面试练习</li></ul>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/e32a5cc21917/</id>
    <link href="https://blog.zisheng.pro/posts/e32a5cc21917/"/>
    <published>2024-09-11T11:34:35.000Z</published>
    <summary>本文旨在探索一个适合程序员的学习英语的方法体系。</summary>
    <title>程序员如何学习英语</title>
    <updated>2026-09-18T08:15:28.640Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="TypeScript" scheme="https://blog.zisheng.pro/tags/TypeScript/"/>
    <category term="数据结构" scheme="https://blog.zisheng.pro/tags/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/"/>
    <content>
      <![CDATA[<p>例如，下面是一个使用递归定义的树形结构的例子：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> <span class="title class_">TreeNode</span>&lt;T&gt; = &#123;</span><br><span class="line">  <span class="attr">value</span>: T;</span><br><span class="line">  <span class="attr">children</span>: <span class="title class_">TreeNode</span>&lt;T&gt;[];</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="attr">tree</span>: <span class="title class_">TreeNode</span>&lt;<span class="built_in">string</span>&gt; = &#123;</span><br><span class="line">  <span class="attr">value</span>: <span class="string">&quot;root&quot;</span>,</span><br><span class="line">  <span class="attr">children</span>: [</span><br><span class="line">    &#123;</span><br><span class="line">      <span class="attr">value</span>: <span class="string">&quot;child1&quot;</span>,</span><br><span class="line">      <span class="attr">children</span>: [</span><br><span class="line">        &#123;</span><br><span class="line">          <span class="attr">value</span>: <span class="string">&quot;grandchild1&quot;</span>,</span><br><span class="line">          <span class="attr">children</span>: [],</span><br><span class="line">        &#125;,</span><br><span class="line">        &#123;</span><br><span class="line">          <span class="attr">value</span>: <span class="string">&quot;grandchild2&quot;</span>,</span><br><span class="line">          <span class="attr">children</span>: [],</span><br><span class="line">        &#125;,</span><br><span class="line">      ],</span><br><span class="line">    &#125;,</span><br><span class="line">    &#123;</span><br><span class="line">      <span class="attr">value</span>: <span class="string">&quot;child2&quot;</span>,</span><br><span class="line">      <span class="attr">children</span>: [],</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>在这个例子中，我们定义了一个TreeNode类型，它包含一个值和一个子节点数组。子节点数组也是一个 TreeNode 类型，这就形成了一个递归结构。我们可以使用这个类型来描述一个树形结构，其中每个节点可以有任意数量的子节点，也可以没有子节点。</p><p>这种递归结构特性可以用于解决很多问题，例如遍历树形结构、计算树的深度、判断树是否平衡等问题。同时，递归结构也可能会导致性能问题，因此在实际使用中需要注意递归深度的限制和性能优化。</p>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/56b3e1b7f6b1/</id>
    <link href="https://blog.zisheng.pro/posts/56b3e1b7f6b1/"/>
    <published>2024-07-10T03:21:12.000Z</published>
    <summary>TypeScript的递归结构特性是指在TypeScript中，可以使用递归定义来描述具有递归结构的数据类型。这种特性可以用于定义树形结构、嵌套数据、递归算法等问题。</summary>
    <title>TypeScript 描述树结构</title>
    <updated>2026-09-18T08:15:28.632Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="AI" scheme="https://blog.zisheng.pro/categories/AI/"/>
    <category term="LLM" scheme="https://blog.zisheng.pro/tags/LLM/"/>
    <category term="大语言模型" scheme="https://blog.zisheng.pro/tags/%E5%A4%A7%E8%AF%AD%E8%A8%80%E6%A8%A1%E5%9E%8B/"/>
    <category term="AI" scheme="https://blog.zisheng.pro/tags/AI/"/>
    <category term="ML" scheme="https://blog.zisheng.pro/tags/ML/"/>
    <category term="机器学习" scheme="https://blog.zisheng.pro/tags/%E6%9C%BA%E5%99%A8%E5%AD%A6%E4%B9%A0/"/>
    <content>
      <![CDATA[<h2 id="人工智能（AI）"><a href="#人工智能（AI）" class="headerlink" title="人工智能（AI）"></a>人工智能（AI）</h2><blockquote><p>AI，全称 Artificial Intelligence，人工智能。</p></blockquote><p>人工智能是一种模拟人类智能的技术和系统。人工智能的目标是使机器能够像人类一样思考、学习、推理和解决问题。它涉及到多个领域，包括机器学习、深度学习、自然语言处理、计算机视觉等。</p><p>人工智能技术可以分为弱人工智能和强人工智能两种类型。弱人工智能是针对特定任务的智能系统，如语音识别、图像识别等。而强人工智能则是具有与人类相似的智能水平，能够在各种任务上表现出类似甚至超越人类的智能。</p><p>人工智能技术在各个领域都有广泛的应用，如医疗保健、金融、交通、教育等。在医疗领域，人工智能可用于辅助医生进行诊断和制定治疗方案；在金融领域，人工智能可用于风险管理和交易预测；在交通领域，人工智能可用于交通流量优化和自动驾驶技术；在教育领域，人工智能可用于个性化教学和学习辅助等。</p><p>随着深度学习等技术的发展，人工智能在各个领域取得了巨大进展，并为社会带来了许多便利和创新。人工智能技术的不断发展和应用将继续推动科技进步和社会发展。</p><h2 id="机器学习（ML）"><a href="#机器学习（ML）" class="headerlink" title="机器学习（ML）"></a>机器学习（ML）</h2><blockquote><p>ML，全称 Machine Learning，机器学习。</p></blockquote><p>机器学习是一门开发算法和统计模型的科学，计算机系统使用这些算法和模型，在没有明确指令的情况下，依靠既有模式和推理来执行任务。计算机系统使用机器学习算法来处理大量历史数据。并识别数据模式。还可让计算机系统根据给出的输入数据集更准确地预测结果。例如，数据科学家可以通过存储数百万的扫描图片和相应诊断来训练医学应用程序，使该程序能够根据 X 光图片诊断癌症。</p><h3 id="机器学习的工作原理是什么？"><a href="#机器学习的工作原理是什么？" class="headerlink" title="机器学习的工作原理是什么？"></a>机器学习的工作原理是什么？</h3><p>机器学习的核心思想是任意输入和输出数据组合之间的现有数学关系。机器学习模型事先不知道这种关系，但如果给出足够的数据集，它可以猜测出来。这意味着每个机器学习算法都是围绕一个可修改的数学函数构建的。基本原理可以这样理解：</p><ol><li>我们为算法提供了以下输入&#x2F;输出<code>(i,o)</code>组合来“训练”算法：<code>(2,10)</code>、<code>(5,19)</code>、<code>(9,31)</code></li><li>算法计算出输入和输出之间的关系为：<code>o=3*i+4</code></li><li>我们为算法提供输入 7，并要求算法预测输出。算法会自动确定输出位 25。</li></ol><p>虽然这只是机器学习的基本理解，但机器学习关注的是一个原则，即所有复杂的数据点都可以通过计算机系统建立数学关联，只要它们有足够的数据和计算能力来处理这些数据。因此，输出的准确度与给定的输入大小直接相关。</p><h3 id="机器学习算法有哪些类型？"><a href="#机器学习算法有哪些类型？" class="headerlink" title="机器学习算法有哪些类型？"></a>机器学习算法有哪些类型？</h3><p>机器学习包括传统机器学习和深度学习，有以下几种类型：</p><ul><li>监督学习（Supervised Learning）：每个样本都有对应的目标值，通过搭建模型实现从输入特征向量到目标值的映射，例如解决回归和分类问题。</li><li>无监督学习（Unsupervised Learning）：所有样本没有目标值，期望从数据本身发下一些潜在规律，例如解决聚类问题。</li><li>强化学习（Reinforcement Learning）：相对较为复杂，系统和外界环境不断交互，在外界反馈的基础上决定自身行为，已达到目标最优化。例如阿尔法为其和无人驾驶。</li></ul><h3 id="机器学习和人工智能的关系"><a href="#机器学习和人工智能的关系" class="headerlink" title="机器学习和人工智能的关系"></a>机器学习和人工智能的关系</h3><p>机器学习是人工智能的一个重要分支，属于实现人工智能的技术手段之一。人工智能是一门研究如何使计算机能够模拟人类智能行为的学科，而机器学习则是实现这一目标的关键方法之一。</p><p>具体来说，机器学习是让计算机系统从数据中学习并改进性能的技术，而人工智能则包括了更广泛的概念，涵盖了各种技术和方法，如专家系统、自然语言处理、计算机视觉等。机器学习作为实现人工智能的重要手段之一，通过让计算机系统从数据中学习规律和模式，使其具备类似人类智能的能力。</p><p>在实际应用中，机器学习技术被广泛应用于各种人工智能任务中，如语音识别、图像识别、自然语言处理等。通过不断优化算法和模型，机器学习使得计算机系统能够逐渐展现出类似人类智能的行为和能力，推动了人工智能领域的发展和进步。</p><h2 id="深度学习（DL）"><a href="#深度学习（DL）" class="headerlink" title="深度学习（DL）"></a>深度学习（DL）</h2><blockquote><p>DL，全称 Deep Learning，深度学习。</p></blockquote><p><strong>深度学习是机器学习的一个分支，它是一种通过模仿人类大脑的神经网络结构来实现学习和推断的技术</strong>。深度学习利用多层神经网络来学习数据的特征表示，从而可以在大规模数据集上进行复杂的模式识别和预测任务。机器学习则是一种更广泛的概念，涵盖了各种通过算法让计算机系统从数据中学习和改进性能的技术。深度学习是机器学习的一个重要分支，它在图像识别、语音识别、自然语言处理等领域取得了显著的成就。</p><h3 id="神经网络（Neural-Network）"><a href="#神经网络（Neural-Network）" class="headerlink" title="神经网络（Neural Network）"></a>神经网络（Neural Network）</h3><blockquote><p>NN，全称 Neural Network，神经网络。</p></blockquote><p>神经网络是一种受到生物神经系统启发而设计的人工智能模型，用于模拟人类大脑的学习和推断过程。神经网络由多个神经元（节点）组成，这些神经元通过连接（权重）相互关联，形成多层网络结构。</p><p>在神经网络中，通常包括输入层、隐藏层和输出层。输入层接收原始数据输入，隐藏层通过学习数据中的特征表示来提取数据的高级特征，输出层根据学习到的特征进行预测或分类。</p><p>神经网络的训练过程通常通过反向传播算法来实现。在训练过程中，神经网络通过不断调整连接权重，使得模型能够逐渐优化并学习到数据中的模式和规律。这样，神经网络就能够在给定输入数据的情况下做出准确的预测或分类。</p><h4 id="神经网络有哪些类型？"><a href="#神经网络有哪些类型？" class="headerlink" title="神经网络有哪些类型？"></a>神经网络有哪些类型？</h4><ul><li>前馈神经网络（Feedforward Neural Network，FNN）：前馈神经网络是最简单的神经网络形式，信息在网络中单向传递，不会形成循环。它包括输入层、隐藏层和输出层，常用于分类和回归任务。</li><li>卷积神经网络（Convolutional Neural Network，CNN）：卷积神经网络主要用于处理图像数据，具有共享权重和局部连接的特点，能够有效提取图像特征。CNN在计算机视觉领域取得了巨大成功。</li><li>循环神经模型（Recurrent Neural Network，RNN）：循环神经网络具有循环结构，能够处理序列数据，如自然语言处理任务。RNN具有记忆功能，能够保留先前的信息，但存在梯度消失和梯度爆炸问题。</li><li>长短期记忆网络（Long Short-Term Memory，LSTM）：LSTM 是 RNN 的一种变体，通过门控机制解决了梯度消失和梯度爆炸问题，适用于处理长序列数据。</li><li>生成对抗网络（Generative Adversarial Network，GAN）：生成对抗网络由生成器和判别器组成，通过对抗训练实现生成逼真数据的目标。GAN 在图像生成、风格迁移等任务中表现出色。</li><li>自编码器（Autoencoder）：自编码器是一种无监督学习模型，通过将输入数据压缩表示并重建输出数据来学习数据的有效表示。自编码器常用于降维和特征提取。</li></ul><h4 id="神经网络的机器学习的关系"><a href="#神经网络的机器学习的关系" class="headerlink" title="神经网络的机器学习的关系"></a>神经网络的机器学习的关系</h4><p>神经网络是机器学习的一种技术手段，属于机器学习的范畴。机器学习是一种人工智能的分支领域，旨在让计算机系统能够从数据中学习和改进性能，而神经网络则是实现这一目标的重要工具之一。</p><p>在机器学习中，除了神经网络外，还包括传统的机器学习算法，如决策树、支持向量机、K近邻等。这些算法通常用于处理结构化数据和特定任务，如分类、回归、聚类等。神经网络则更适用于处理复杂的非线性关系和大规模数据集，具有强大的表征学习能力。</p><h4 id="神经网络和深度学习的关系"><a href="#神经网络和深度学习的关系" class="headerlink" title="神经网络和深度学习的关系"></a>神经网络和深度学习的关系</h4><p>神经网络是深度学习的基础，是实现深度学习的重要工具之一。</p><p>具体来说，神经网络是受到生物神经系统启发而设计的人工智能模型，由多个神经元（节点）组成，通过连接（权重）相互关联，形成多层网络结构。深度学习通过构建多层神经网络（即深层网络）来实现对数据的高级抽象和表征，从而提高对数据的理解和预测能力。</p><p>在深度学习中，神经网络扮演着核心角色，通过多层神经元之间的连接和权重调整来实现对数据的学习和建模。深度学习技术基于神经网络结构，能够处理复杂的数据模式和关系，取得了在计算机视觉、自然语言处理、语音识别等领域的显著进展。</p><p>因此，可以说深度学习是建立在神经网络基础上的一种机器学习技术，通过构建多层神经网络结构实现对数据的高级抽象和表征，从而提高对数据的理解和预测能力。神经网络作为深度学习的基础组件，在人工智能领域发挥着重要作用。</p><h3 id="Transformer"><a href="#Transformer" class="headerlink" title="Transformer"></a>Transformer</h3><p>Transformer 是一种基于注意力机制的深度学习模型架构，最初由 Google 的研究人员提出，被广泛应用于自然语言处理领域，尤其是机器翻译任务。Transformer 模型的提出对传统的循环神经网络（RNN）和长短时记忆网络（LSTM）等模型进行了革命性的改进。</p><p>Transformer 模型的核心是自注意力机制（Self-Attention Mechanism），通过自注意力机制，模型能够同时考虑输入序列中不同位置的词语之间的关联性，从而捕捉长距离依赖关系。此外，Transformer 模型还包括位置编码（Positional Encoding）和多头注意力机制（Multi-Head Attention），使其能够更好地处理序列数据。</p><p>相比于传统的循环神经网络，Transformer 模型具有以下优势：</p><ol><li>并行计算：Transformer 能够并行计算，加快训练速度；</li><li>长距离依赖：通过自注意力机制，Transformer 能够捕捉长距离依赖关系；</li><li>更好的表征学习：Transformer 能够学习到更加抽象和有效的特征表示。</li></ol><p>Transformer 模型已经被广泛应用于自然语言处理任务，如机器翻译、文本生成、文本分类等，取得了显著的成就。其成功应用推动了深度学习领域的发展，并为处理序列数据提供了一种高效且强大的模型架构。</p><h4 id="Transformer-和神经网络的关系"><a href="#Transformer-和神经网络的关系" class="headerlink" title="Transformer 和神经网络的关系"></a>Transformer 和神经网络的关系</h4><p>Transformer 是一种基于注意力机制的深度学习模型，而神经网络是深度学习的基础组件之一。具体来说，Transformer 可以被视为一种神经网络架构，它使用注意力机制来处理序列数据，如自然语言文本。</p><p>在传统的神经网络中，信息的传递是通过循环结构实现的，存在梯度消失和梯度爆炸问题。相比之下，Transformer 利用注意力机制实现了更为高效的信息传递和建模，能够并行处理序列数据，从而提高了训练速度和模型性能。</p><p>因此，Transformer 可以被看作是一种神经网络架构的演进和创新，它在机器翻译、文本生成、语音识别等领域取得了显著成就。通过引入自注意力机制和位置编码，Transformer 模型能够更好地捕捉序列数据中的长距离依赖关系和特征表示，成为深度学习领域的重要里程碑之一。</p><h2 id="大语言模型（LLM）"><a href="#大语言模型（LLM）" class="headerlink" title="大语言模型（LLM）"></a>大语言模型（LLM）</h2><blockquote><p>LLM，全称 Large Language Model，大语言模型。</p></blockquote><p>大型语言模型（LLM）是基于大量数据进行预训练的超大型深度学习模型，旨在理解和生成人类语言。它们在海量的文本数据上进行训练，通常具有至少几十亿参数，可以执行多样性的下游任务。。底层转换器是一组神经网络，这些神经网络由具有自注意力功能的编码器和解码器组成。编码器和解码器从一系列文本中提取含义，并理解其中的单词和短语之间的关系。</p><p>Transformer 能够进行无监督的训练，但更精确的解释使转换器可以执行自主学习。通过此过程，Transformer 可学会理解基本的语法、语言和知识。</p><p>与早期按顺序处理输入的循环神经网络（RNN）不同，Transformer 并行处理整个序列。这可让数据科学家使用 GPU 训练基于 Transformer 的 LLM，从而大幅度缩短训练时间。</p><p>借助 Transformer 神经网络架构，你可使用非常大规模的模型，其中通常具有数千亿个参数。这种大规模模型可以摄取通常来自互联网的大量数据，但也可以从包含 500 多亿个网页的 Common Crawl 和拥有约 5700 万个页面的 Wikipedia 等来源摄取数据。</p><h3 id="大型语言模型有哪些应用？"><a href="#大型语言模型有哪些应用？" class="headerlink" title="大型语言模型有哪些应用？"></a>大型语言模型有哪些应用？</h3><ul><li>文案写作：除了 GPT-3 和 ChatGPT 之外，Claude、Llama 2、Cohere Command 和 Jurassic 也可编写原件。AI21 Wordspice 建议修改原始语句以改善风格和语音。</li><li>知识库回答：该技术通常称为知识密集型自然语言处理（KI-NLP），是指可以根据数字存档中的信息帮助回答特定问题的 LLM。AI21 Studio playground 能够回答常识性问题就是此类示例。</li><li>文本分类：使用集群，LLM 可以对含义或情绪相似的文本进行分类。用途包括衡量客户情绪、确定文本之间的关系和文档搜索。</li><li>代码生成：LLM 擅长根据自然语言提示生成代码。示例包括 Amazon CodeWhisperer 和 GitHub Copilot 中使用的 Open AI Codex，它们可以用 Python、JavaScript、Ruby 和其他几种编程语言编码。其他编码应用包括创建 SQL 查询、编写 Shell 命令和进行网站设计。</li><li>文本生成：与代码生成类似，文本生成可以完成不完整的语句，编写产品文档，或者像 Alexa Create 一样创作简短的儿童故事。</li></ul><h3 id="GPT"><a href="#GPT" class="headerlink" title="GPT"></a>GPT</h3><blockquote><p>GPT 全称 Generative Pre-trained Transformer，生成预训练转换器。</p></blockquote><p>GPT 是由 OpenAI 开发的一系列基于 Transformer 架构的大型语言模型。GPT 模型采用了自回归的方式进行训练，能够生成文本、回答问题、完成翻译等自然语言处理任务。</p><h3 id="开源-LLM"><a href="#开源-LLM" class="headerlink" title="开源 LLM"></a>开源 LLM</h3><ul><li>通义千问：通义千问-7B（Qwen-7B） 是阿里云研发的通义千问大模型系列的70亿参数规模的模型。Qwen-7B是基于Transformer的大语言模型, 在超大规模的预训练数据上进行训练得到。预训练数据类型多样，覆盖广泛，包括大量网络文本、专业书籍、代码等。同时，在Qwen-7B的基础上，使用对齐机制打造了基于大语言模型的AI助手Qwen-7B-Chat。</li><li>智谱·AI：ChatGLM-6B 是一个开源的、支持中英双语问答的对话语言模型，基于 General Language Model (GLM) 架构，具有 62 亿参数</li></ul><h2 id="AIGC-Generative-AI"><a href="#AIGC-Generative-AI" class="headerlink" title="AIGC&#x2F;Generative AI"></a>AIGC&#x2F;Generative AI</h2><blockquote><p>AIGC，全称 Artificial Intelligence Generative Content，人工智能生成内容。<br>Generative AI，生成式 AI。</p></blockquote><p>AIGC 又称生成式 AI (Generative AI)，是继专业生产内容（PGC， Professional-generated Content）、用户生产内容（UGC， User-generated Content）之后的新型内容创作方式，可以在对话、故事、图像、视频和音乐制作等方面，打造新的数字内容生成与交互形式。</p><p>与所有人工智能技术一样，AIGC 的能力由机器学习模型提供，这些模型是基于大量数据进行预先训练的大模型，通常被称为基础模型（Foundation Models）。如今以基础模型为驱动的 AIGC 应用迭代速度呈现指数级发展，从由 Stable Diffusion 文生图模型驱动的 AI 作画应用，再到以大语言模型（LLM）驱动的智能聊天机器人，深度学习模型不断完善、开源预训练基础模型的推动以及大模型探索商业化的可能，都在成为这场人工智能颠覆性革命的主要驱动力。</p><h2 id="论文摘要"><a href="#论文摘要" class="headerlink" title="论文摘要"></a>论文摘要</h2><h3 id="《Neural-Machine-Translation-by-Jointly-Learning-to-Align-and-Translate》"><a href="#《Neural-Machine-Translation-by-Jointly-Learning-to-Align-and-Translate》" class="headerlink" title="《Neural Machine Translation by Jointly Learning to Align and Translate》"></a>《Neural Machine Translation by Jointly Learning to Align and Translate》</h3><p>《基于联合学习对齐和翻译的神经机器翻译》是一篇经典的机器翻译论文，由 Google Brain 团队的研究人员 Bahdanau（ [bɑːdɑˈnoʊ]）等人于 2014 年提出。该论文提出了一种新颖的神经机器翻译模型，引入了注意力机制，极大地改进了传统的基于统计机器翻译的方法。</p><p>该论文提出了一种称为 Seq2Seq 的神经网络架构，用于进行机器翻译任务。该架构包括编码器和解码器两部分，通过将源语言句子编码为固定长度的向量表示，然后将此向量解码为目标语言句子。与传统的固定长度短语表示不同，该模型能够根据输入序列中的不同部分动态地调整注意力，以便更好地对齐源语言和目标语言之间的单词。</p><p>通过引入注意力机制，该模型能够在生成目标语言句子的每个单词时动态地关注源语言句子的不同部分，从而更好地处理长距离依赖关系和词序问题。实验结果表明，这种基于注意力机制的神经机器翻译模型在翻译质量和效率上都取得了显著的提升，成为了后续神经机器翻译模型发展的重要里程碑。</p><p>这篇论文为后续的神经机器翻译研究奠定了基础，并启发了许多后续模型的发展，如 Transformer 等。</p><h3 id="《Attention-is-All-You-Need》"><a href="#《Attention-is-All-You-Need》" class="headerlink" title="《Attention is All You Need》"></a>《Attention is All You Need》</h3><p>《Attention is All You Need》是一篇由 Google Brain 团队提出的论文，介绍了 Transformer 模型，这是一种基于注意力机制的深度学习模型。</p><p>Transformer 模型摒弃了传统的循环神经网络（RNN）和长短期记忆网络（LSTM），完全基于自注意力机制来处理序列数据。该模型通过引入多头注意力机制和位置编码，实现了并行计算、更好的序列建模能力以及更快的训练速度。Transformer 模型在机器翻译等自然语言处理任务中取得了优异的性能，证明了注意力机制在序列建模中的有效性。</p><p>该论文的提出对深度学习领域产生了深远影响，激发了后续许多基于 Transformer 架构的模型的发展，如 BERT、GPT 等。Transformer 模型的成功证明了注意力机制在处理序列数据时的重要性，为自然语言处理领域带来了革命性的变革。</p><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ul><li><a href="https://aws.amazon.com/cn/what-is/artificial-intelligence/">什么是人工智能？</a></li><li><a href="https://aws.amazon.com/cn/what-is/machine-learning/">什么是机器学习？</a></li><li><a href="https://aws.amazon.com/cn/what-is/deep-learning/">什么是深度学习？</a></li></ul>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/456d17dc39c7/</id>
    <link href="https://blog.zisheng.pro/posts/456d17dc39c7/"/>
    <published>2024-04-05T15:12:43.000Z</published>
    <summary>本文介绍了人工智能领域的各种概念，旨在建立起人工智能知识架构，帮助大家更好地理解人工智能</summary>
    <title>人工智能概念学习</title>
    <updated>2026-09-18T08:15:28.629Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="算法" scheme="https://blog.zisheng.pro/categories/%E7%AE%97%E6%B3%95/"/>
    <category term="计划" scheme="https://blog.zisheng.pro/tags/%E8%AE%A1%E5%88%92/"/>
    <content>
      <![CDATA[<h2 id="第一阶段"><a href="#第一阶段" class="headerlink" title="第一阶段"></a>第一阶段</h2><h3 id="基础知识学习"><a href="#基础知识学习" class="headerlink" title="基础知识学习"></a>基础知识学习</h3><ul><li>学习基本数据结构（数组、链表、栈、队列、树等）和基本算法（排序、查找、递归等）。</li><li>刷题巩固基础，解决大量简单和中等难度的算法问题。</li></ul><h3 id="算法思想学习"><a href="#算法思想学习" class="headerlink" title="算法思想学习"></a>算法思想学习</h3><ul><li>深入学习常用算法思想，如贪心算法、动态规划、回溯算法等。</li><li>练习相关算法题目，加深理解和掌握应用。</li></ul><h3 id="LeetCode刷题阶段"><a href="#LeetCode刷题阶段" class="headerlink" title="LeetCode刷题阶段"></a>LeetCode刷题阶段</h3><ul><li>刷LeetCode题库，重点解决中等和困难难度的算法问题。</li><li>注重多种解题思路的探索，提升解决问题的能力和速度。</li></ul><h2 id="第二阶段"><a href="#第二阶段" class="headerlink" title="第二阶段"></a>第二阶段</h2><h3 id="算法进阶与实战"><a href="#算法进阶与实战" class="headerlink" title="算法进阶与实战"></a>算法进阶与实战</h3><ul><li>学习高级数据结构（图、堆、并查集等）和高级算法（网络流、字符串匹配等）。</li><li>解决复杂算法问题，注重优化算法效率和空间复杂度。</li></ul><h3 id="项目实践与总结"><a href="#项目实践与总结" class="headerlink" title="项目实践与总结"></a>项目实践与总结</h3><ul><li>将所学算法应用到实际项目中，解决实际问题。</li><li>总结学习经验，复习重要知识点，填补知识漏洞。</li></ul>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/06239a00ff4d/</id>
    <link href="https://blog.zisheng.pro/posts/06239a00ff4d/"/>
    <published>2024-03-13T14:56:33.000Z</published>
    <summary>算法学习计划</summary>
    <title>算法学习计划</title>
    <updated>2026-09-18T08:15:28.640Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="全栈开发" scheme="https://blog.zisheng.pro/categories/%E5%85%A8%E6%A0%88%E5%BC%80%E5%8F%91/"/>
    <category term="JavaScript" scheme="https://blog.zisheng.pro/tags/JavaScript/"/>
    <category term="Puppeteer" scheme="https://blog.zisheng.pro/tags/Puppeteer/"/>
    <content>
      <![CDATA[<h2 id="拦截所有图片资源"><a href="#拦截所有图片资源" class="headerlink" title="拦截所有图片资源"></a>拦截所有图片资源</h2><p>在 Puppeteer 中，如果你想禁止加载图片，可以通过拦截请求并阻止图片资源类型的请求来实现。这可以通过使用 <code>page.setRequestInterception(true)</code> 来启用请求拦截，然后在 <code>request</code> 事件监听器中检查请求类型，如果是图片，则调用 <code>request.abort()</code> 方法。</p><p>以下是一个示例代码，展示了如何在 Puppeteer 中禁止加载图片：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> puppeteer = <span class="built_in">require</span>(<span class="string">&#x27;puppeteer&#x27;</span>);</span><br><span class="line"></span><br><span class="line">(<span class="keyword">async</span> () =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> browser = <span class="keyword">await</span> puppeteer.<span class="title function_">launch</span>();</span><br><span class="line">  <span class="keyword">const</span> page = <span class="keyword">await</span> browser.<span class="title function_">newPage</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> page.<span class="title function_">setRequestInterception</span>(<span class="literal">true</span>);</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 添加请求拦截器</span></span><br><span class="line">  page.<span class="title function_">on</span>(<span class="string">&#x27;request&#x27;</span>, <span class="function"><span class="params">request</span> =&gt;</span> &#123;</span><br><span class="line">    <span class="comment">// 如果请求的资源类型是图片，则阻止该请求</span></span><br><span class="line">    <span class="keyword">if</span> (request.<span class="title function_">resourceType</span>() === <span class="string">&#x27;image&#x27;</span>) &#123;</span><br><span class="line">      request.<span class="title function_">abort</span>();</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">      request.<span class="title function_">continue</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">await</span> page.<span class="title function_">goto</span>(<span class="string">&#x27;https://example.com&#x27;</span>);</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 其他操作...</span></span><br><span class="line"></span><br><span class="line">  <span class="keyword">await</span> browser.<span class="title function_">close</span>();</span><br><span class="line">&#125;)();</span><br></pre></td></tr></table></figure><p>在这段代码中，我们首先通过 <code>setRequestInterception(true)</code> 启用了请求拦截。然后，我们监听 <code>request</code> 事件，并检查每个请求的资源类型。如果资源类型是 <code>image</code>，我们就调用 <code>request.abort()</code> 来阻止这个请求。对于其他类型的请求，我们调用 <code>request.continue()</code> 来允许请求继续。</p><p>这种方法可以有效减少页面加载时间和带宽使用，特别是在你不需要处理图片数据时。</p><h2 id="拦截指定域名的图片资源"><a href="#拦截指定域名的图片资源" class="headerlink" title="拦截指定域名的图片资源"></a>拦截指定域名的图片资源</h2><p>在 Puppeteer 中，如果你想禁止加载来自特定域名的图片，你可以在启用请求拦截后，检查请求的 URL，并根据域名决定是否要阻止该请求。以下是一个示例代码，展示了如何根据域名禁止加载图片：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> puppeteer = <span class="built_in">require</span>(<span class="string">&#x27;puppeteer&#x27;</span>);</span><br><span class="line"></span><br><span class="line">(<span class="keyword">async</span> () =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> browser = <span class="keyword">await</span> puppeteer.<span class="title function_">launch</span>();</span><br><span class="line">  <span class="keyword">const</span> page = <span class="keyword">await</span> browser.<span class="title function_">newPage</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> page.<span class="title function_">setRequestInterception</span>(<span class="literal">true</span>);</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 添加请求拦截器</span></span><br><span class="line">  page.<span class="title function_">on</span>(<span class="string">&#x27;request&#x27;</span>, <span class="function"><span class="params">request</span> =&gt;</span> &#123;</span><br><span class="line">    <span class="comment">// 获取请求的 URL</span></span><br><span class="line">    <span class="keyword">const</span> url = request.<span class="title function_">url</span>();</span><br><span class="line">    <span class="comment">// 解析 URL 以获取域名</span></span><br><span class="line">    <span class="keyword">const</span> hostname = <span class="keyword">new</span> <span class="title function_">URL</span>(url).<span class="property">hostname</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> (request.<span class="title function_">resourceType</span>() === <span class="string">&#x27;image&#x27;</span> &amp;&amp; hostname === <span class="string">&#x27;example.com&#x27;</span>) &#123;</span><br><span class="line">      <span class="comment">// 阻止来自特定域名的图片请求</span></span><br><span class="line">      request.<span class="title function_">abort</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">      request.<span class="title function_">continue</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">await</span> page.<span class="title function_">goto</span>(<span class="string">&#x27;https://example.com&#x27;</span>);</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 其他操作...</span></span><br><span class="line"></span><br><span class="line">  <span class="keyword">await</span> browser.<span class="title function_">close</span>();</span><br><span class="line">&#125;)();</span><br></pre></td></tr></table></figure><p>在这个例子中，我们首先通过 <code>setRequestInterception(true)</code> 启用了请求拦截。然后，我们监听 <code>request</code> 事件，对于每个请求，我们获取它的 URL 并解析出域名。如果请求的资源类型是 <code>image</code> 并且域名与我们指定的域名匹配，我们就调用 <code>request.abort()</code> 来阻止这个请求。对于其他类型的请求或来自其他域名的请求，我们调用 <code>request.continue()</code> 来允许请求继续。</p><p>请确保将 <code>&#39;example.com&#39;</code> 替换为你想要阻止图片加载的特定域名。这种方法可以帮助你减少不必要的网络流量，特别是当你只想屏蔽来自某些域名的图片时。</p>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/020a842650e4/</id>
    <link href="https://blog.zisheng.pro/posts/020a842650e4/"/>
    <published>2024-03-07T09:29:02.000Z</published>
    <summary>在 Puppeteer 中，如果你想禁止加载图片，可以通过拦截请求并阻止图片资源类型的请求来实现。这可以通过使用 page.setRequestInterception(true) 来启用请求拦截，然后在 request 事件监听器中检查请求类型，如果是图片，则调用 request.abort() 方法。</summary>
    <title>Puppeteer 学习笔记（示例）: 禁止加载图片资源</title>
    <updated>2026-09-18T08:15:28.629Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="JavaScript" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/JavaScript/"/>
    <category term="Promise" scheme="https://blog.zisheng.pro/tags/Promise/"/>
    <category term="setTimeout" scheme="https://blog.zisheng.pro/tags/setTimeout/"/>
    <content>
      <![CDATA[<p>在 JavaScript 中实现一个延迟函数（通常称为 <code>delay</code> 或 <code>sleep</code> 函数）非常简单，你可以使用 Promise 和 <code>setTimeout</code> 函数来实现。以下是一个 <code>delay</code> 函数的示例：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">delay</span>(<span class="params">time</span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">Promise</span>(<span class="function"><span class="params">resolve</span> =&gt;</span> <span class="built_in">setTimeout</span>(resolve, time));</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="keyword">function</span> <span class="title function_">run</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;Delay start&#x27;</span>);</span><br><span class="line">  <span class="keyword">await</span> <span class="title function_">delay</span>(<span class="number">2000</span>); <span class="comment">// 延迟 2000 毫秒（2秒）</span></span><br><span class="line">  <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">&#x27;Delay end&#x27;</span>);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="title function_">run</span>();</span><br></pre></td></tr></table></figure><p>在这个例子中，<code>delay</code> 函数接收一个时间参数（以毫秒为单位），然后返回一个 Promise。这个 Promise 将在指定的时间后通过 <code>setTimeout</code> 函数解决。在 <code>run</code> 函数中，我们使用 <code>await</code> 关键字等待 <code>delay</code> 函数，这样代码的执行就会在这里暂停指定的时间。</p><p>你可以根据需要调用 <code>delay</code> 函数来在任何地方添加延迟。这在模拟网络请求、动画等场景中非常有用。</p>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/d5391f511e4c/</id>
    <link href="https://blog.zisheng.pro/posts/d5391f511e4c/"/>
    <published>2024-03-07T09:27:07.000Z</published>
    <summary>在 JavaScript 中实现一个延迟函数（通常称为 delay 或 sleep 函数）非常简单，你可以使用 Promise 和 setTimeout 函数来实现。</summary>
    <title>在 JavaScript 中实现一个延迟函数</title>
    <updated>2026-09-18T08:15:28.636Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="全栈开发" scheme="https://blog.zisheng.pro/categories/%E5%85%A8%E6%A0%88%E5%BC%80%E5%8F%91/"/>
    <category term="Node.js" scheme="https://blog.zisheng.pro/tags/Node-js/"/>
    <category term="Puppeteer" scheme="https://blog.zisheng.pro/tags/Puppeteer/"/>
    <category term="爬虫" scheme="https://blog.zisheng.pro/tags/%E7%88%AC%E8%99%AB/"/>
    <content>
      <![CDATA[<p>在 Puppeteer 中，”tab”通常指的是浏览器窗口中的页面（Page）。如果你想同时打开多个页面（即多个tab），然后依次在它们之间切换，你可以按照以下步骤进行：</p><ol><li>创建多个页面对象。</li><li>同时打开所有页面。</li><li>依次切换到每个页面并执行操作。</li></ol><p>以下是一个示例代码，展示了如何同时打开多个页面，并在它们都加载完成后依次切换和操作：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> puppeteer = <span class="built_in">require</span>(<span class="string">&#x27;puppeteer&#x27;</span>);</span><br><span class="line"></span><br><span class="line">(<span class="keyword">async</span> () =&gt; &#123;</span><br><span class="line">  <span class="comment">// 启动浏览器</span></span><br><span class="line">  <span class="keyword">const</span> browser = <span class="keyword">await</span> puppeteer.<span class="title function_">launch</span>(&#123; <span class="attr">headless</span>: <span class="literal">false</span> &#125;); <span class="comment">// 设置为非无头模式以便看到浏览器标签切换</span></span><br><span class="line"></span><br><span class="line">  <span class="comment">// 定义要访问的 URL 列表</span></span><br><span class="line">  <span class="keyword">const</span> urls = [<span class="string">&#x27;https://example.com&#x27;</span>, <span class="string">&#x27;https://example.org&#x27;</span>, <span class="string">&#x27;https://example.net&#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> pages = <span class="keyword">await</span> <span class="title class_">Promise</span>.<span class="title function_">all</span>(urls.<span class="title function_">map</span>(<span class="keyword">async</span> (url) =&gt; &#123;</span><br><span class="line">    <span class="keyword">const</span> page = <span class="keyword">await</span> browser.<span class="title function_">newPage</span>();</span><br><span class="line">    <span class="keyword">await</span> page.<span class="title function_">goto</span>(url);</span><br><span class="line">    <span class="keyword">return</span> page;</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">for</span> (<span class="keyword">const</span> page <span class="keyword">of</span> pages) &#123;</span><br><span class="line">    <span class="comment">// 将焦点切换到当前页面</span></span><br><span class="line">    <span class="keyword">await</span> page.<span class="title function_">bringToFront</span>();</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 在这里可以执行页面操作，例如截图、提取页面内容等</span></span><br><span class="line">    <span class="comment">// 示例：打印当前页面的 URL</span></span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="keyword">await</span> page.<span class="title function_">url</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> randomDelay = <span class="title class_">Math</span>.<span class="title function_">floor</span>(<span class="title class_">Math</span>.<span class="title function_">random</span>() * (<span class="number">180000</span> - <span class="number">60000</span> + <span class="number">1</span>)) + <span class="number">60000</span>;</span><br><span class="line">    <span class="keyword">await</span> page.<span class="title function_">waitForTimeout</span>(randomDelay);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 执行完操作后，可以选择关闭当前页面或保持打开状态</span></span><br><span class="line">    <span class="comment">// await page.close();</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="comment">// await browser.close();</span></span><br><span class="line">&#125;)();</span><br></pre></td></tr></table></figure><p>在这个脚本中：</p><ul><li><code>browser.newPage()</code> 用于创建新页面。</li><li><code>page.goto(url)</code> 用于导航到指定的 URL。</li><li><code>page.bringToFront()</code> 用于将当前页面切换到前台，这样你就可以模拟用户在不同的标签页之间切换。</li></ul><p>请注意，如果你不关闭每个页面，那么在脚本的最后你应该关闭浏览器，以释放资源。如果你关闭了每个页面，那么关闭浏览器的步骤可以省略，因为当最后一个页面关闭时，Puppeteer 通常会自动关闭浏览器。</p><p>此外，由于这个脚本是在非无头模式下运行的（<code>headless: false</code>），你可以看到浏览器窗口和标签页的切换。如果你不需要看到浏览器界面，可以将其设置为无头模式（<code>headless: true</code>），这通常会稍微提高性能。</p>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/2cb484464818/</id>
    <link href="https://blog.zisheng.pro/posts/2cb484464818/"/>
    <published>2024-03-07T09:25:16.000Z</published>
    <summary>在 Puppeteer 中，&quot;tab&quot;通常指的是浏览器窗口中的页面（Page）。如果你想同时打开多个页面（即多个tab），然后依次在它们之间切换，你可以按照以下步骤进行。</summary>
    <title>Puppeteer 学习笔记（示例）: 打开多个页面并依次切换tab</title>
    <updated>2026-09-18T08:15:28.629Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="全栈开发" scheme="https://blog.zisheng.pro/categories/%E5%85%A8%E6%A0%88%E5%BC%80%E5%8F%91/"/>
    <category term="Puppeteer" scheme="https://blog.zisheng.pro/tags/Puppeteer/"/>
    <category term="学习笔记" scheme="https://blog.zisheng.pro/tags/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/"/>
    <content>
      <![CDATA[<p>在 Puppeteer 中，要打开一个页面并滑动到底部，你可以使用页面提供的滚动功能。这通常通过执行页面上下文中的 JavaScript 代码来实现。以下是一个示例，展示了如何使用 Puppeteer 打开一个页面并滚动到底部：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> puppeteer = <span class="built_in">require</span>(<span class="string">&#x27;puppeteer&#x27;</span>);</span><br><span class="line"></span><br><span class="line">(<span class="keyword">async</span> () =&gt; &#123;</span><br><span class="line">  <span class="comment">// 启动浏览器</span></span><br><span class="line">  <span class="keyword">const</span> browser = <span class="keyword">await</span> puppeteer.<span class="title function_">launch</span>();</span><br><span class="line">  <span class="comment">// 创建一个新页面</span></span><br><span class="line">  <span class="keyword">const</span> page = <span class="keyword">await</span> browser.<span class="title function_">newPage</span>();</span><br><span class="line">  <span class="comment">// 导航到指定的 URL</span></span><br><span class="line">  <span class="keyword">await</span> page.<span class="title function_">goto</span>(<span class="string">&#x27;https://example.com&#x27;</span>);</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="keyword">function</span> <span class="title function_">scrollToBottom</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="keyword">await</span> page.evaluate(<span class="keyword">async</span> () =&gt; &#123;</span><br><span class="line">      <span class="keyword">await</span> <span class="keyword">new</span> <span class="title class_">Promise</span>(<span class="function">(<span class="params">resolve, reject</span>) =&gt;</span> &#123;</span><br><span class="line">        <span class="keyword">var</span> totalHeight = <span class="number">0</span>;</span><br><span class="line">        <span class="keyword">var</span> distance = <span class="number">100</span>; <span class="comment">// 每次向下滚动的距离</span></span><br><span class="line">        <span class="keyword">var</span> timer = <span class="built_in">setInterval</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">          <span class="keyword">var</span> scrollHeight = <span class="variable language_">document</span>.<span class="property">body</span>.<span class="property">scrollHeight</span>;</span><br><span class="line">          <span class="variable language_">window</span>.<span class="title function_">scrollBy</span>(<span class="number">0</span>, distance);</span><br><span class="line">          totalHeight += distance;</span><br><span class="line"></span><br><span class="line">          <span class="keyword">if</span> (totalHeight &gt;= scrollHeight) &#123;</span><br><span class="line">            <span class="built_in">clearInterval</span>(timer);</span><br><span class="line">            <span class="title function_">resolve</span>();</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">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 调用滑动到页面底部的函数</span></span><br><span class="line">  <span class="keyword">await</span> <span class="title function_">scrollToBottom</span>();</span><br><span class="line"></span><br><span class="line">  <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">await</span> page.<span class="title function_">close</span>();</span><br><span class="line">  <span class="comment">// 关闭浏览器</span></span><br><span class="line">  <span class="keyword">await</span> browser.<span class="title function_">close</span>();</span><br><span class="line">&#125;)();</span><br></pre></td></tr></table></figure><p>在这个脚本中，<code>scrollToBottom</code> 函数通过设置一个定时器来模拟用户滚动行为，每隔一段时间滚动一小段距离，直到达到页面底部。这种方法可以模拟真实用户的滚动行为，对于需要加载懒加载内容（如图片或者无限滚动的社交媒体帖子）的页面尤其有用。</p><p>请注意，滚动到底部的速度和每次滚动的距离可以根据页面的实际情况进行调整。如果页面有很多懒加载内容，可能需要减慢滚动速度或增加每次滚动的距离，以确保内容能够正确加载。</p>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/d86e73bee997/</id>
    <link href="https://blog.zisheng.pro/posts/d86e73bee997/"/>
    <published>2024-03-07T02:47:39.000Z</published>
    <summary>在 Puppeteer 中，要打开一个页面并滑动到底部，你可以使用页面提供的滚动功能。这通常通过执行页面上下文中的 JavaScript 代码来实现。本文是介绍了一个示例，展示了如何使用 Puppeteer 打开一个页面并滚动到底部</summary>
    <title>Puppeteer 学习笔记（示例）: 打开页面并滑动到底部</title>
    <updated>2026-09-18T08:15:28.629Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="全栈开发" scheme="https://blog.zisheng.pro/categories/%E5%85%A8%E6%A0%88%E5%BC%80%E5%8F%91/"/>
    <category term="Puppeteer" scheme="https://blog.zisheng.pro/tags/Puppeteer/"/>
    <category term="学习笔记" scheme="https://blog.zisheng.pro/tags/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/"/>
    <content>
      <![CDATA[<p>要使用 Puppeteer 并行打开多个页面，你可以使用 <code>Promise.all()</code> 来同时处理多个页面。这样，你可以在同一个浏览器实例中并行打开多个标签页。下面是一个示例代码，展示了如何实现这一点：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> puppeteer = <span class="built_in">require</span>(<span class="string">&#x27;puppeteer&#x27;</span>);</span><br><span class="line"></span><br><span class="line">(<span class="keyword">async</span> () =&gt; &#123;</span><br><span class="line">  <span class="comment">// 启动浏览器</span></span><br><span class="line">  <span class="keyword">const</span> browser = <span class="keyword">await</span> puppeteer.<span class="title function_">launch</span>();</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 定义要访问的 URL 列表</span></span><br><span class="line">  <span class="keyword">const</span> urls = [<span class="string">&#x27;https://example.com&#x27;</span>, <span class="string">&#x27;https://example.org&#x27;</span>, <span class="string">&#x27;https://example.net&#x27;</span>];</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 使用 map() 方法将每个 URL 映射到打开页面的 Promise</span></span><br><span class="line">  <span class="keyword">const</span> pagePromises = urls.<span class="title function_">map</span>(<span class="keyword">async</span> (url) =&gt; &#123;</span><br><span class="line">    <span class="comment">// 创建一个新页面</span></span><br><span class="line">    <span class="keyword">const</span> page = <span class="keyword">await</span> browser.<span class="title function_">newPage</span>();</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 导航到指定的 URL</span></span><br><span class="line">    <span class="keyword">await</span> page.<span class="title function_">goto</span>(url);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 在这里可以执行其他页面操作，例如截图、提取页面内容等</span></span><br><span class="line">    <span class="comment">// 例如：await page.screenshot(&#123; path: `screenshot-$&#123;url&#125;.png` &#125;);</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> page.<span class="title function_">close</span>();</span><br><span class="line">  &#125;);</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 使用 Promise.all() 等待所有页面的 Promise 完成</span></span><br><span class="line">  <span class="keyword">await</span> <span class="title class_">Promise</span>.<span class="title function_">all</span>(pagePromises);</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 所有页面处理完毕后关闭浏览器</span></span><br><span class="line">  <span class="keyword">await</span> browser.<span class="title function_">close</span>();</span><br><span class="line">&#125;)();</span><br></pre></td></tr></table></figure><p>在这个脚本中，<code>urls.map()</code> 会为每个 URL 创建一个新的 Promise，这些 Promise 会开始执行页面的打开和操作过程。<code>Promise.all()</code> 会等待所有这些 Promise 完成，这意味着所有页面的操作都是并行执行的。</p><p>请注意，虽然这种方法可以提高效率，但是如果同时打开的页面太多，可能会消耗大量的系统资源，导致性能下降。因此，如果要处理大量页面，可能需要实现一种队列或者限制并发数量的机制。</p>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/a581be17d009/</id>
    <link href="https://blog.zisheng.pro/posts/a581be17d009/"/>
    <published>2024-03-07T02:44:16.000Z</published>
    <summary>要使用 Puppeteer 并行打开多个页面，你可以使用 `Promise.all()` 来同时处理多个页面。这样，你可以在同一个浏览器实例中并行打开多个标签页。</summary>
    <title>Puppeteer 学习笔记（示例）: 并行打开多个页面</title>
    <updated>2026-09-18T08:15:28.632Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="全栈开发" scheme="https://blog.zisheng.pro/categories/%E5%85%A8%E6%A0%88%E5%BC%80%E5%8F%91/"/>
    <category term="Puppeteer" scheme="https://blog.zisheng.pro/tags/Puppeteer/"/>
    <category term="学习笔记" scheme="https://blog.zisheng.pro/tags/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/"/>
    <content>
      <![CDATA[<p>在 Puppeteer 中，你可以使用 <code>browser.newPage()</code> 方法来打开多个页面（也称为标签页或 tabs）。以下是一个基本的示例，展示了如何使用 Puppeteer 打开多个页面并逐个访问不同的 URL：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> puppeteer = <span class="built_in">require</span>(<span class="string">&#x27;puppeteer&#x27;</span>);</span><br><span class="line"></span><br><span class="line">(<span class="keyword">async</span> () =&gt; &#123;</span><br><span class="line">  <span class="comment">// 启动浏览器</span></span><br><span class="line">  <span class="keyword">const</span> browser = <span class="keyword">await</span> puppeteer.<span class="title function_">launch</span>();</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 定义要访问的 URL 列表</span></span><br><span class="line">  <span class="keyword">const</span> urls = [<span class="string">&#x27;https://example.com&#x27;</span>, <span class="string">&#x27;https://example.org&#x27;</span>, <span class="string">&#x27;https://example.net&#x27;</span>];</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 遍历 URL 列表，为每个 URL 打开一个新页面</span></span><br><span class="line">  <span class="keyword">for</span> (<span class="keyword">const</span> url <span class="keyword">of</span> urls) &#123;</span><br><span class="line">    <span class="comment">// 创建一个新页面</span></span><br><span class="line">    <span class="keyword">const</span> page = <span class="keyword">await</span> browser.<span class="title function_">newPage</span>();</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 导航到指定的 URL</span></span><br><span class="line">    <span class="keyword">await</span> page.<span class="title function_">goto</span>(url);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 在这里可以执行其他页面操作，例如截图、提取页面内容等</span></span><br><span class="line">    <span class="comment">// 例如：await page.screenshot(&#123; path: `screenshot-$&#123;url&#125;.png` &#125;);</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> page.<span class="title function_">close</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">await</span> browser.<span class="title function_">close</span>();</span><br><span class="line">&#125;)();</span><br></pre></td></tr></table></figure><p>这个脚本首先启动了一个新的浏览器实例，然后遍历一个包含 URL 的数组，为每个 URL 创建一个新页面，并导航到相应的地址。在每个页面上，你可以执行所需的操作，比如截图、页面内容分析等。完成操作后，脚本会关闭当前页面，并在处理完所有页面后关闭浏览器实例。</p><p>请注意，这个脚本是按顺序打开和处理每个页面的。如果你想同时打开多个页面，你可以创建所有页面的 Promise，然后使用 <code>Promise.all()</code> 来等待所有页面操作完成。但是，这样做可能会对计算机的性能造成较大压力，尤其是当打开的页面数量较多时。</p>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/7c063e00d3c1/</id>
    <link href="https://blog.zisheng.pro/posts/7c063e00d3c1/"/>
    <published>2024-03-07T02:35:27.000Z</published>
    <summary>在 Puppeteer 中，你可以使用 `browser.newPage()` 方法来打开多个页面（也称为标签页或 tabs）</summary>
    <title>Puppeteer 学习笔记（示例）: 逐个打开页面并滑动到底部</title>
    <updated>2026-09-18T08:15:28.632Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="全栈开发" scheme="https://blog.zisheng.pro/categories/%E5%85%A8%E6%A0%88%E5%BC%80%E5%8F%91/"/>
    <category term="前端" scheme="https://blog.zisheng.pro/tags/%E5%89%8D%E7%AB%AF/"/>
    <category term="Node.js" scheme="https://blog.zisheng.pro/tags/Node-js/"/>
    <category term="Puppeteer" scheme="https://blog.zisheng.pro/tags/Puppeteer/"/>
    <content>
      <![CDATA[<h2 id="Puppeteer-是什么？"><a href="#Puppeteer-是什么？" class="headerlink" title="Puppeteer 是什么？"></a>Puppeteer 是什么？</h2><p>Puppeteer 是一个 Node 库，它提供了一个高级 API 来通过 DevTools 协议控制 Chromium 或 Chrome。Puppeteer 默认以 headless 模式运行，但是可以通过修改配置文件运行“有头”模式。</p><h2 id="Puppeteer-能做什么"><a href="#Puppeteer-能做什么" class="headerlink" title="Puppeteer 能做什么?"></a>Puppeteer 能做什么?</h2><p>你可以在浏览器中手动执行的绝大多数操作都可以使用 Puppeteer 来完成！ 下面是一些示例：</p><ul><li>生成页面 PDF。</li><li>抓取 SPA（单页应用）并生成预渲染内容（即“SSR”（服务器端渲染））。</li><li>自动提交表单，进行 UI 测试，键盘输入等。</li><li>创建一个时时更新的自动化测试环境。 使用最新的 JavaScript 和浏览器功能直接在最新版本的Chrome中执行测试。</li><li>捕获网站的 timeline trace，用来帮助分析性能问题。</li><li>测试浏览器扩展。</li></ul><h2 id="如何安装-puppeteer？"><a href="#如何安装-puppeteer？" class="headerlink" title="如何安装 puppeteer？"></a>如何安装 puppeteer？</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm install puppeteer</span><br></pre></td></tr></table></figure><h2 id="使用-puppeteer"><a href="#使用-puppeteer" class="headerlink" title="使用 puppeteer"></a>使用 puppeteer</h2><p>Puppeteer 使用起来和其他测试框架类似。你需要创建一个 Browser 实例，打开页面，然后使用 Puppeteer 的 API。</p><p>Example - 跳转到 <a href="https://example.com/">https://example.com</a> 并保存截图至 example.png:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// example.js</span></span><br><span class="line"><span class="keyword">const</span> puppeteer = <span class="built_in">require</span>(<span class="string">&#x27;puppeteer&#x27;</span>);</span><br><span class="line"></span><br><span class="line">(<span class="keyword">async</span> () =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> browser = <span class="keyword">await</span> puppeteer.<span class="title function_">launch</span>();</span><br><span class="line">  <span class="keyword">const</span> page = <span class="keyword">await</span> browser.<span class="title function_">newPage</span>();</span><br><span class="line">  <span class="keyword">await</span> page.<span class="title function_">goto</span>(<span class="string">&#x27;https://www.otemeta.com&#x27;</span>);</span><br><span class="line">  <span class="keyword">await</span> page.<span class="title function_">screenshot</span>(&#123;<span class="attr">path</span>: <span class="string">&#x27;otemeta.png&#x27;</span>&#125;);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">await</span> browser.<span class="title function_">close</span>();</span><br><span class="line">&#125;)();</span><br></pre></td></tr></table></figure><p>在命令行中执行：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">node example.<span class="property">js</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><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> puppeteer = <span class="built_in">require</span>(<span class="string">&#x27;puppeteer&#x27;</span>);</span><br><span class="line"></span><br><span class="line">(<span class="keyword">async</span> () =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> browser = <span class="keyword">await</span> puppeteer.<span class="title function_">launch</span>(&#123; <span class="attr">headless</span>: <span class="literal">false</span> &#125;);</span><br><span class="line">  <span class="keyword">const</span> page = <span class="keyword">await</span> browser.<span class="title function_">newPage</span>();</span><br><span class="line">  <span class="keyword">await</span> page.<span class="title function_">goto</span>(<span class="string">&#x27;https://www.otemeta.com&#x27;</span>);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">await</span> browser.<span class="title function_">close</span>();</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 js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> puppeteer = <span class="built_in">require</span>(<span class="string">&#x27;puppeteer&#x27;</span>);</span><br><span class="line"></span><br><span class="line">(<span class="keyword">async</span> () =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> browser = <span class="keyword">await</span> puppeteer.<span class="title function_">launch</span>(&#123;</span><br><span class="line">    <span class="attr">headless</span>: <span class="literal">false</span>,</span><br><span class="line">    <span class="attr">defaultViewport</span>: &#123;</span><br><span class="line">      <span class="attr">width</span>: <span class="number">1280</span>,</span><br><span class="line">      <span class="attr">height</span>: <span class="number">720</span></span><br><span class="line">    &#125;</span><br><span class="line">  &#125;);</span><br><span class="line">  <span class="keyword">const</span> page = <span class="keyword">await</span> browser.<span class="title function_">newPage</span>();</span><br><span class="line">  <span class="keyword">await</span> page.<span class="title function_">goto</span>(<span class="string">&#x27;https://www.otemeta.com&#x27;</span>);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">await</span> browser.<span class="title function_">close</span>();</span><br><span class="line">&#125;)();</span><br></pre></td></tr></table></figure><h3 id="在导航期间忽略-HTTPS-错误"><a href="#在导航期间忽略-HTTPS-错误" class="headerlink" title="在导航期间忽略 HTTPS 错误."></a>在导航期间忽略 HTTPS 错误.</h3><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> puppeteer = <span class="built_in">require</span>(<span class="string">&#x27;puppeteer&#x27;</span>);</span><br><span class="line"></span><br><span class="line">(<span class="keyword">async</span> () =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> browser = <span class="keyword">await</span> puppeteer.<span class="title function_">launch</span>(&#123;</span><br><span class="line">    <span class="attr">headless</span>: <span class="literal">false</span>,</span><br><span class="line">    <span class="attr">ignoreHTTPSErrors</span>: <span class="literal">false</span>,</span><br><span class="line">    <span class="attr">defaultViewport</span>: &#123;</span><br><span class="line">      <span class="attr">width</span>: <span class="number">1280</span>,</span><br><span class="line">      <span class="attr">height</span>: <span class="number">720</span></span><br><span class="line">    &#125;</span><br><span class="line">  &#125;);</span><br><span class="line">  <span class="keyword">const</span> page = <span class="keyword">await</span> browser.<span class="title function_">newPage</span>();</span><br><span class="line">  <span class="keyword">await</span> page.<span class="title function_">goto</span>(<span class="string">&#x27;https://www.otemeta.com&#x27;</span>);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">await</span> browser.<span class="title function_">close</span>();</span><br><span class="line">&#125;)();</span><br></pre></td></tr></table></figure><h2 id="Browser-实例"><a href="#Browser-实例" class="headerlink" title="Browser 实例"></a>Browser 实例</h2><h3 id="常用方法"><a href="#常用方法" class="headerlink" title="常用方法"></a>常用方法</h3><ul><li><code>browser.newPage()</code>：返回一个新的 Page 对象。Page 在一个默认的浏览器上下文中被创建。</li><li><code>browser.close()</code>：关闭 Chromium 及其所有页面(如果页面被打开的话)。</li><li><code>browser.pages()</code>：返回一个包含所有打开的页面的数组。</li></ul><h2 id="Page-实例"><a href="#Page-实例" class="headerlink" title="Page 实例"></a>Page 实例</h2><h3 id="page-goto-url-options"><a href="#page-goto-url-options" class="headerlink" title="page.goto(url[, options])"></a>page.goto(url[, options])</h3><p>导航到相应地址. 地址应该带有 http 协议, 比如 <code>https://</code>。options：</p><ul><li>timeout：跳转等待时间，单位是毫秒, 默认是30秒, 传 0 表示无限等待。可以通过 <code>page.setDefaultNavigationTimeout(timeout)</code> 方法修改默认值</li><li>waitUntil：满足什么条件认为页面跳转完成，默认是 load 事件触发时。指定事件数组，那么所有事件触发后才认为是跳转完成。事件包括：<ul><li>load - 页面的load事件触发时</li><li>domcontentloaded - 页面的 DOMContentLoaded 事件触发时</li><li>networkidle0 - 不再有网络连接时触发（至少500毫秒后）</li><li>networkidle2 - 只有2个网络连接时触发（至少500毫秒后）</li></ul></li></ul><h3 id="常用方法-1"><a href="#常用方法-1" class="headerlink" title="常用方法"></a>常用方法</h3><ul><li><code>page.$(selector)</code>：此方法在页面内执行 <code>document.querySelector</code>。如果没有元素匹配指定选择器，返回值是 <code>null</code>。</li><li><code>page.$$(selector)</code>：此方法在页面内执行 <code>document.querySelectorAll</code>。如果没有元素匹配指定选择器，返回值是 <code>[]</code>。</li><li><code>page.close([options])</code>：关闭页面</li><li><code>page.evaluate(pageFunction[, ...args])</code>： 在页面实例上下文中执行方法</li><li><code>page.setCookie(...cookies)</code>：设置 cookie</li><li><code>page.setViewport(viewport)</code>：如果是一个浏览器多个页面的情况，每个页面都可以有单独的 viewport</li><li><code>page.waitFor()</code>：设置页面停留时间</li><li><code>page.url()</code>：返回当前页面的 URL</li></ul><h2 id="示例"><a href="#示例" class="headerlink" title="示例"></a>示例</h2><h3 id="创建一个-PDF。"><a href="#创建一个-PDF。" class="headerlink" title="创建一个 PDF。"></a>创建一个 PDF。</h3><p>文件为 <code>hn.js</code></p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> puppeteer = <span class="built_in">require</span>(<span class="string">&#x27;puppeteer&#x27;</span>);</span><br><span class="line"></span><br><span class="line">(<span class="keyword">async</span> () =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> browser = <span class="keyword">await</span> puppeteer.<span class="title function_">launch</span>();</span><br><span class="line">  <span class="keyword">const</span> page = <span class="keyword">await</span> browser.<span class="title function_">newPage</span>();</span><br><span class="line">  <span class="keyword">await</span> page.<span class="title function_">goto</span>(<span class="string">&#x27;https://www.otemeta.com&#x27;</span>, &#123; <span class="attr">waitUntil</span>: <span class="string">&#x27;networkidle2&#x27;</span> &#125;);</span><br><span class="line">  <span class="keyword">await</span> page.<span class="title function_">pdf</span>(&#123;<span class="attr">path</span>: <span class="string">&#x27;hn.pdf&#x27;</span>, <span class="attr">format</span>: <span class="string">&#x27;A4&#x27;</span>&#125;);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">await</span> browser.<span class="title function_">close</span>();</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 js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> puppeteer = <span class="built_in">require</span>(<span class="string">&#x27;puppeteer&#x27;</span>);</span><br><span class="line"></span><br><span class="line">(<span class="keyword">async</span> () =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> browser = <span class="keyword">await</span> puppeteer.<span class="title function_">launch</span>();</span><br><span class="line">  <span class="keyword">const</span> page = <span class="keyword">await</span> browser.<span class="title function_">newPage</span>();</span><br><span class="line">  <span class="keyword">await</span> page.<span class="title function_">goto</span>(<span class="string">&#x27;https://www.otemeta.com&#x27;</span>);</span><br><span class="line"></span><br><span class="line">  <span class="comment">// Get the &quot;viewport&quot; of the page, as reported by the page.</span></span><br><span class="line">  <span class="keyword">const</span> dimensions = <span class="keyword">await</span> page.evaluate(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> &#123;</span><br><span class="line">      <span class="attr">width</span>: <span class="variable language_">document</span>.<span class="property">documentElement</span>.<span class="property">clientWidth</span>,</span><br><span class="line">      <span class="attr">height</span>: <span class="variable language_">document</span>.<span class="property">documentElement</span>.<span class="property">clientHeight</span>,</span><br><span class="line">      <span class="attr">deviceScaleFactor</span>: <span class="variable language_">window</span>.<span class="property">devicePixelRatio</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="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">&#x27;Dimensions:&#x27;</span>, dimensions);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">await</span> browser.<span class="title function_">close</span>();</span><br><span class="line">&#125;)();</span><br></pre></td></tr></table></figure><h2 id="puppeteer-FAQ"><a href="#puppeteer-FAQ" class="headerlink" title="puppeteer FAQ"></a>puppeteer FAQ</h2><h3 id="puppeteer-和-puppeteer-core-的区别"><a href="#puppeteer-和-puppeteer-core-的区别" class="headerlink" title="puppeteer 和 puppeteer-core 的区别"></a>puppeteer 和 puppeteer-core 的区别</h3><p>当你安装 <code>puppeteer</code> 时，它会下载最新版本的 Chromium（<del>170MB Mac，</del>282MB Linux，~280MB Win），以保证可以使用 API。 如果想要跳过下载，请阅读环境变量。</p><p>自 1.7.0 版本以来，我们都会发布一个 <code>puppeteer-core</code> 包，这个包默认不会下载 Chromium。</p><p><code>puppeteer-core</code> 是一个的轻量级的 Puppeteer 版本，用于启动现有浏览器安装或连接到远程安装。</p><h3 id="puppeteer-launch-和-puppeteer-connect-的区别"><a href="#puppeteer-launch-和-puppeteer-connect-的区别" class="headerlink" title="puppeteer.launch 和 puppeteer.connect 的区别"></a>puppeteer.launch 和 puppeteer.connect 的区别</h3><p>puppeteer 提供了两种方式来启动浏览器：<code>puppeteer.launch</code> 和 <code>puppeteer.connect</code>。</p><ol><li><p><code>puppeteer.launch</code>: 这个方法会启动一个全新的浏览器实例，每次调用 <code>puppeteer.launch</code> 都会启动一个新的浏览器进程。这种方式适合于需要独立的、干净的浏览器环境的场景，比如进行网页截图、自动化测试等。</p></li><li><p><code>puppeteer.connect</code>: 这个方法允许连接到一个已经存在的浏览器实例。这种方式适合于需要在已有浏览器实例上进行操作的场景，比如远程调试、复用已有的浏览器环境等。</p></li></ol><p>因此，<code>puppeteer.launch</code> 用于启动新的浏览器实例，而 <code>puppeteer.connect</code> 用于连接到已有的浏览器实例。根据具体的需求和场景，选择合适的方法来启动或连接浏览器。</p><h3 id="networkidle2-和-networkidle0-的区别"><a href="#networkidle2-和-networkidle0-的区别" class="headerlink" title="networkidle2 和 networkidle0 的区别"></a>networkidle2 和 networkidle0 的区别</h3><p>在 Puppeteer 中，<code>networkidle0</code> 和 <code>networkidle2</code> 是两种网络空闲检测的事件，它们用于在 <code>page.goto()</code> 或 <code>page.waitForNavigation()</code> 等函数中作为选项，以便确定何时认为页面导航和加载已经完成。</p><p>这两个事件的区别在于它们对网络空闲状态的定义：</p><ul><li><code>networkidle0</code>：在至少 500 毫秒内没有任何网络连接时触发。这意味着页面在指定时间内没有任何网络请求，这通常表示页面加载已经完成，并且没有任何长轮询请求或其他持续的网络活动。</li><li><code>networkidle2</code>：在至少 500 毫秒内网络连接数不超过 2 个时触发。这意味着页面可能仍然在进行一些轻量级的网络活动，但基本上是空闲的。这对于有一些小的、持续的网络请求（如 WebSockets 或长轮询请求）的页面来说是有用的。</li></ul><p>选择哪一个取决于你的用例。如果你需要等待所有网络活动完全停止，使用 <code>networkidle0</code> 可能更合适。如果页面预期会有一些持续的网络活动，但你想在大部分资源加载完毕时继续操作，那么 <code>networkidle2</code> 可能是更好的选择。</p><p>请注意，从 Puppeteer v3.0.0 开始，<code>networkidle0</code> 和 <code>networkidle2</code> 事件已被弃用，取而代之的是 <code>networkidle</code>。如果你使用的是更新的 Puppeteer 版本，请查阅最新的文档以获取正确的使用方法。</p>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/3ed9d91bc806/</id>
    <link href="https://blog.zisheng.pro/posts/3ed9d91bc806/"/>
    <published>2024-03-07T02:19:57.000Z</published>
    <summary>Puppeteer 是一个 Node 库，它提供了一个高级 API 来通过 DevTools 协议控制 Chromium 或 Chrome。Puppeteer 默认以 headless 模式运行，但是可以通过修改配置文件运行“有头”模式。</summary>
    <title>Puppeteer 学习笔记（一）: 入门</title>
    <updated>2026-09-18T08:15:28.629Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="JavaScript" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/JavaScript/"/>
    <category term="JavaScript" scheme="https://blog.zisheng.pro/tags/JavaScript/"/>
    <category term="IIFE" scheme="https://blog.zisheng.pro/tags/IIFE/"/>
    <content>
      <![CDATA[<p>在 JavaScript 中，IIFE（Immediately Invoked Function Expression）是一种重要且强大的编程模式。它能够帮助我们创建私有作用域，避免全局命名冲突，并提供了一种模块化的编程方式。本文将深入探讨 IIFE 的概念、用途和最佳实践。</p><h2 id="IIFE-的定义和语法"><a href="#IIFE-的定义和语法" class="headerlink" title="IIFE 的定义和语法"></a>IIFE 的定义和语法</h2><p>IIFE 是一个立即调用的函数表达式，它由两部分组成：函数表达式和立即调用运算符。其基本语法如下：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">(<span class="keyword">function</span>(<span class="params"></span>)&#123;</span><br><span class="line">  <span class="comment">// 代码块</span></span><br><span class="line">&#125;)();</span><br></pre></td></tr></table></figure><p>或</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">(<span class="keyword">function</span>(<span class="params"></span>)&#123;</span><br><span class="line">  <span class="comment">// 代码块</span></span><br><span class="line">&#125;());</span><br></pre></td></tr></table></figure><p>或</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">!<span class="keyword">function</span>(<span class="params"></span>)&#123;</span><br><span class="line">  <span class="comment">// 代码块</span></span><br><span class="line">&#125;();</span><br></pre></td></tr></table></figure><p>这样定义和调用的函数会立即执行，而不需要通过函数名来调用。</p><h2 id="IIFE-的作用和用途"><a href="#IIFE-的作用和用途" class="headerlink" title="IIFE 的作用和用途"></a>IIFE 的作用和用途</h2><p>IIFE 的一个主要作用是创建一个私有作用域，避免全局变量的污染。通过将代码封装在 IIFE 内部，所有的变量和函数都将存在于 IIFE 的作用域内，不会与全局作用域发生冲突。这种模式特别适用于插件和库的开发，可以将插件的代码封装在 IIFE 中，只暴露需要的接口给外部使用。</p><h2 id="IIFE-的参数传递和返回值"><a href="#IIFE-的参数传递和返回值" class="headerlink" title="IIFE 的参数传递和返回值"></a>IIFE 的参数传递和返回值</h2><p>IIFE 可以接受参数，并返回一个值。通过传递参数，我们可以向 IIFE 内部传递外部的变量和值，进一步增强了其灵活性。同时，通过返回一个值，我们可以将内部的数据和功能暴露给外部使用。</p><h2 id="IIFE-的最佳实践"><a href="#IIFE-的最佳实践" class="headerlink" title="IIFE 的最佳实践"></a>IIFE 的最佳实践</h2><p>在使用 IIFE 时，有几个最佳实践值得注意：</p><ul><li>使用分号结尾：在 IIFE 的结尾加上分号，以避免与其他代码的连锁执行发生问题。</li><li>使用严格模式：在 IIFE 内部开启严格模式，以确保代码的安全性和可靠性。</li><li>适度使用 IIFE：不要滥用 IIFE，只在需要创建私有作用域或模块化代码时使用它。</li></ul><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>IIFE 是 JavaScript 中一种强大且常用的编程模式，它可以帮助我们避免全局命名冲突，创建私有作用域，并提供模块化的编程方式。通过合理地运用 IIFE，我们可以写出更加安全、可维护和可扩展的代码。</p><p>希望本文能够帮助你更好地了解和应用 IIFE，在 JavaScript 的开发中发挥更大的作用。</p>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/8e6c60fd4747/</id>
    <link href="https://blog.zisheng.pro/posts/8e6c60fd4747/"/>
    <published>2024-02-08T06:33:23.000Z</published>
    <summary>当提到 IIFE（Immediately Invoked Function Expression）时，它是一种 JavaScript 中的函数表达式。这种表达式定义了一个函数，并立即调用它。以下是一篇关于 IIFE 的文章。</summary>
    <title>深入理解 JavaScript 中的 IIFE</title>
    <updated>2026-09-18T08:15:28.640Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="工具" scheme="https://blog.zisheng.pro/categories/%E5%B7%A5%E5%85%B7/"/>
    <category term="picgo" scheme="https://blog.zisheng.pro/tags/picgo/"/>
    <content>
      <![CDATA[<h2 id="安装方式"><a href="#安装方式" class="headerlink" title="安装方式"></a>安装方式</h2><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">brew install picgo --cask</span><br></pre></td></tr></table></figure><h2 id="问题1-安装后打开-picgo-报错：xxx-已损坏，无法打开。您应该将它移到废纸篓"><a href="#问题1-安装后打开-picgo-报错：xxx-已损坏，无法打开。您应该将它移到废纸篓" class="headerlink" title="问题1: 安装后打开 picgo 报错：xxx 已损坏，无法打开。您应该将它移到废纸篓"></a>问题1: 安装后打开 picgo 报错：xxx 已损坏，无法打开。您应该将它移到废纸篓</h2><p>终端输入：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sudo xattr -r -d com.apple.quarantine 你的APP路径</span><br></pre></td></tr></table></figure><p>APP 路径的获取方法：</p><p>打开 “访达”（Finder）进入 “应用程序” 目录，找到该软件图标，将图标拖到终端窗口，最终的命令类似</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sudo xattr -r -d com.apple.quarantine <span class="string">&#x27;/Applications/PicGo.app&#x27;</span></span><br></pre></td></tr></table></figure><h2 id="问题2-无法从剪贴板上传"><a href="#问题2-无法从剪贴板上传" class="headerlink" title="问题2: 无法从剪贴板上传"></a>问题2: 无法从剪贴板上传</h2><p>现象：配置好 cos 之后，可以正常从本地推动到主窗口上传，但在剪贴板上的“等待上传” 则点击无效。</p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/202310302151692.png"></p><p>排查方式：打开 picgo 的日志文件， 在<strong>主窗口 -&gt; PicGo设置 -&gt; 设置日志文件（点击设置） -&gt; 日志文件（点击打开）</strong>，如果其中的报错是如下格式，可以检查一下对应目录 <code>picgo-clipboard-images</code> 是否存在，不存在手动创建一个：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Error: ENOENT: no such file or directory, open <span class="string">&#x27;/Users/zizhu/Library/Application Support/picgo/picgo-clipboard-images/202306272131465.png.3840258425&#x27;</span></span><br></pre></td></tr></table></figure>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/ddf5c735f6e0/</id>
    <link href="https://blog.zisheng.pro/posts/ddf5c735f6e0/"/>
    <published>2023-12-31T16:00:00.000Z</published>
    <summary>本文介绍了 Mac(M2) 正确安装 picgo 姿势</summary>
    <title>Mac(M2) 正确安装 picgo 姿势</title>
    <updated>2026-09-18T08:15:28.629Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="项目管理" scheme="https://blog.zisheng.pro/categories/%E9%A1%B9%E7%9B%AE%E7%AE%A1%E7%90%86/"/>
    <category term="项目启动" scheme="https://blog.zisheng.pro/tags/%E9%A1%B9%E7%9B%AE%E5%90%AF%E5%8A%A8/"/>
    <content>
      <![CDATA[<h2 id="KO-会议是什么？"><a href="#KO-会议是什么？" class="headerlink" title="KO 会议是什么？"></a>KO 会议是什么？</h2><p>Kick-off Meeting 有的翻译为项目启动会议，也有的翻译成开工或者开踢会议，这不重要，明确这个英文名字即可，不用管如何翻译。会议是 PM 激励其团队的最佳机会。在这次会议上，项目管理人员可以建立共同目标，并开始了解每个人。</p><h2 id="KO-会议过程"><a href="#KO-会议过程" class="headerlink" title="KO 会议过程"></a>KO 会议过程</h2><p><strong>1、项目背景</strong></p><p>我们项目在哪里？说过去，做项目之前的“悲惨境地”，明确问题根源在哪里，明白为什么要做这个项目。</p><p><strong>2、项目意义、目的与目标</strong></p><p>我们项目去哪里？说将来，项目完成之后的美好前景，解决那些问题就算成功，达到什么样的效果就算完成，让听众知道终点在哪里。</p><p><strong>3、需求和功能点概述</strong></p><p>我们怎么做？说现状，说问题，具体用什么方法促使“过去”到“将来”的转变。讲解方法和任务概况，让听众为终极目标跃跃欲试。</p><p><strong>4、项目组织架构</strong></p><p>明确项目组成员工作范围和在项目中的职位，目的是让项目组成员互相认识，明确什么事情应该找谁。</p><p>KO时，最好是所有项目干系人都要到场，至少关键任务都要到场，比如：项目启动者或发起人，需求方，PMO人员。他们可以高屋建甄的给项目成员描绘项目的重要性和伟大前景，非常鼓舞士气。项目早起，务必多让项目启动者或发起人，需求方，PMO人员多多参与，反复确认方向正确，因为这时候，做各种调整的成本比较低。</p><p>注意不要遗漏工作不多的项目干系人，包括服务部门、配合部门的接口人等。这样会给后续项目实施、上线、运维带来很大的便利。</p><p>介绍成员的时候，最好能够让大家介绍一下自己，和大家站起来示意，自己做个简单自我介绍。</p><p><strong>5、项目计划（WBS）</strong></p><p>让所有人了解两个关键点：</p><p>第一、项目的时间点和里程碑；</p><p>第二、项目各个时段需要的资源，即每个人要在各个阶段做什么事情；</p><p>项目管理的计划、进度、资源、质量、风险，永恒不变的话题，只能自己体会。</p><p><strong>6、沟通计划</strong></p><p>沟通在项目管理中的重要地位不言而喻。太多的事情都是因为沟通不顺利，导致的产生一系列的问题。</p><p>做好沟通计划，强迫项目组成员定期沟通，降低问题发生的几率。</p><p>注意：定期与项目启动者或发起人，需求方，PMO人员回报和沟通，是促使项目顺利进行的必要因素。让他们了解项目进度，知道项目遇到的困难，更有助于申请资源。</p>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/b5b1c16bbb84/</id>
    <link href="https://blog.zisheng.pro/posts/b5b1c16bbb84/"/>
    <published>2023-11-10T15:00:00.000Z</published>
    <summary>Kick-off Meeting 有的翻译为项目启动会议，也有的翻译成开工或者开踢会议，这不重要，明确这个英文名字即可，不用管如何翻译。会议是 PM 激励其团队的最佳机会。在这次会议上，项目管理人员可以建立共同目标，并开始了解每个人。</summary>
    <title>KO 会议是什么？</title>
    <updated>2026-09-18T08:15:28.629Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="Node.js" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/Node-js/"/>
    <category term="Node.js" scheme="https://blog.zisheng.pro/tags/Node-js/"/>
    <category term="排障" scheme="https://blog.zisheng.pro/tags/%E6%8E%92%E9%9A%9C/"/>
    <category term="package.json" scheme="https://blog.zisheng.pro/tags/package-json/"/>
    <category term="依赖管理" scheme="https://blog.zisheng.pro/tags/%E4%BE%9D%E8%B5%96%E7%AE%A1%E7%90%86/"/>
    <content>
      <![CDATA[<p>安装依赖时看到 <code>incompatible engine</code>，不代表项目一定跑不起来；它表示某个包声明了经过验证的 Node.js 版本范围，而当前运行时不在这个范围内。它可能只是历史包写得过窄，也可能是在提醒你 API、ABI 或构建链已不兼容。</p><h2 id="一句话总结"><a href="#一句话总结" class="headerlink" title="一句话总结"></a>一句话总结</h2><p>把 <code>engines</code> 当作兼容性信号，不是无条件的安装开关。临时忽略可以 unblock 本地排查；要进入 CI 或生产，应该升级依赖、固定兼容 Node LTS，或用版本管理工具切换运行时。</p><h2 id="先看它是-warning-还是失败"><a href="#先看它是-warning-还是失败" class="headerlink" title="先看它是 warning 还是失败"></a>先看它是 warning 还是失败</h2><p>检查根目录和报错包的 <code>package.json</code>：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;engines&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;node&quot;</span><span class="punctuation">:</span> <span class="string">&quot;&gt;=18 &lt;23&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>如果项目锁定 Node 16，而你正在使用 Node 22，先在项目 README、<code>.nvmrc</code>、CI 配置里寻找约定版本。不要仅凭本机能安装成功，就把新运行时带进团队环境。</p><h2 id="临时绕过的方法"><a href="#临时绕过的方法" class="headerlink" title="临时绕过的方法"></a>临时绕过的方法</h2><p>Yarn Classic 可以单次忽略：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">yarn install --ignore-engines</span><br></pre></td></tr></table></figure><p>或写入本机配置：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">yarn config <span class="built_in">set</span> ignore-engines <span class="literal">true</span></span><br></pre></td></tr></table></figure><p>npm 的 <code>engine-strict</code> 为 <code>false</code> 时通常只警告；pnpm 则同样受 <code>engine-strict</code> 配置影响。团队项目不建议把忽略选项提交到仓库，因为它会掩盖其他开发机和 CI 的真实问题。</p><h2 id="更稳妥的修复顺序"><a href="#更稳妥的修复顺序" class="headerlink" title="更稳妥的修复顺序"></a>更稳妥的修复顺序</h2><ol><li>用 <code>nvm</code>, <code>fnm</code> 或 Volta 切到项目声明的 Node LTS，重新安装依赖。</li><li>确认上游包是否已有支持当前 Node 的版本；升级并重新生成 lockfile。</li><li>若是自有包，补齐准确的 <code>engines</code> 范围和 CI 矩阵。</li><li>只有为了验证业务逻辑、且测试全绿时，才临时忽略限制。</li></ol><p>例如 <code>.nvmrc</code> 固定团队版本：</p><figure class="highlight txt"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">20</span><br></pre></td></tr></table></figure><p>然后在 CI 中显式使用相同主版本，比让每个开发者各自猜测可靠得多。</p><h2 id="不能忽略的信号"><a href="#不能忽略的信号" class="headerlink" title="不能忽略的信号"></a>不能忽略的信号</h2><p>涉及 native addon、Node API、OpenSSL、ESM&#x2F;CJS 加载方式或构建工具时，版本不匹配经常会在安装后才爆炸。此时 <code>--ignore-engines</code> 只会延后问题，不能解决问题。</p><h2 id="我的判断框架"><a href="#我的判断框架" class="headerlink" title="我的判断框架"></a>我的判断框架</h2><p>值得忽略：第三方包版本范围明显陈旧，且在目标 Node LTS 上通过构建与测试。</p><p>必须修复：CI、线上或含原生模块的项目出现 engine 冲突。运行时版本应是可复现的工程约束。</p>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/bf86f69dbf94/</id>
    <link href="https://blog.zisheng.pro/posts/bf86f69dbf94/"/>
    <published>2023-08-16T13:53:00.000Z</published>
    <summary>遇到 package.json 的 engines 与本地 Node.js 版本不匹配时，先区分 warning 与 hard failure。本文说明 npm、Yarn、pnpm 的处理方式，以及何时绝不能忽略。</summary>
    <title>Node.js 的 incompatible engine：该忽略，还是该修依赖？</title>
    <updated>2026-09-18T08:15:28.640Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="Ant Design" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/Ant-Design/"/>
    <category term="Ant Design Pro" scheme="https://blog.zisheng.pro/tags/Ant-Design-Pro/"/>
    <category term="组件" scheme="https://blog.zisheng.pro/tags/%E7%BB%84%E4%BB%B6/"/>
    <category term="企业级" scheme="https://blog.zisheng.pro/tags/%E4%BC%81%E4%B8%9A%E7%BA%A7/"/>
    <category term="中后台" scheme="https://blog.zisheng.pro/tags/%E4%B8%AD%E5%90%8E%E5%8F%B0/"/>
    <category term="textinput" scheme="https://blog.zisheng.pro/tags/textinput/"/>
    <content>
      <![CDATA[<h2 id="TextArea"><a href="#TextArea" class="headerlink" title="TextArea"></a>TextArea</h2><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">React</span> <span class="keyword">from</span> <span class="string">&#x27;react&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">Input</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;antd&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> &#123; <span class="title class_">TextArea</span> &#125; = <span class="title class_">Input</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="title class_">App</span>: <span class="title class_">React</span>.<span class="property">FC</span> = <span class="function">() =&gt;</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">TextArea</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">      <span class="attr">rows</span>=<span class="string">&#123;4&#125;</span> </span></span></span><br><span class="line"><span class="tag"><span class="language-xml">      <span class="attr">placeholder</span>=<span class="string">&quot;maxLength is 6&quot;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">      <span class="attr">maxLength</span>=<span class="string">&#123;6&#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></span></span><br><span class="line"><span class="tag"><span class="language-xml">        <span class="attr">resize:</span> &#x27;<span class="attr">none</span>&#x27;</span></span></span><br><span class="line"><span class="tag"><span class="language-xml">      &#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">  <span class="tag">&lt;/&gt;</span></span></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> <span class="title class_">App</span>;</span><br></pre></td></tr></table></figure><h2 id="ProFormTextArea"><a href="#ProFormTextArea" class="headerlink" title="ProFormTextArea"></a>ProFormTextArea</h2><p>ProFormTextArea 组件本质上是 Form.Item 和 Input.TextArea 的结合，我们可以把他们当成一个 FormItem 来使用，并且支持各种 props。每个表单项都支持 fieldProps 属性来支持设置输入组件的 props。 我们支持了 placeholder 的透传，你可以直接在组件上设置 placeholder。</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">React</span> <span class="keyword">from</span> <span class="string">&#x27;react&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">ProForm</span>, <span class="title class_">ProFormTextArea</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;@ant-design/pro-components&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="title class_">App</span>: <span class="title class_">React</span>.<span class="property">FC</span> = <span class="function">() =&gt;</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">ProForm</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">      <span class="attr">onValuesChange</span>=<span class="string">&#123;(changeValues)</span> =&gt;</span> console.log(changeValues)&#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">ProFormTextArea</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">        <span class="attr">name</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">label</span>=<span class="string">&quot;名称&quot;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">        <span class="attr">placeholder</span>=<span class="string">&quot;请输入名称&quot;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">        <span class="attr">fieldProps</span>=<span class="string">&#123;&#123;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">          <span class="attr">style:</span> &#123;</span></span></span><br><span class="line"><span class="tag"><span class="language-xml">            <span class="attr">resize:</span> &#x27;<span class="attr">none</span>&#x27;</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">        &#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">    <span class="tag">&lt;/<span class="name">ProForm</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"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="title class_">App</span>;</span><br></pre></td></tr></table></figure>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/141618e1f8a1/</id>
    <link href="https://blog.zisheng.pro/posts/141618e1f8a1/"/>
    <published>2023-08-05T07:00:00.000Z</published>
    <summary>Antd TextArea 用于多行输入。</summary>
    <title>Antd form TextArea 隐藏右下角</title>
    <updated>2026-09-18T08:15:28.629Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="Ant Design" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/Ant-Design/"/>
    <category term="Ant Design Pro" scheme="https://blog.zisheng.pro/tags/Ant-Design-Pro/"/>
    <category term="组件" scheme="https://blog.zisheng.pro/tags/%E7%BB%84%E4%BB%B6/"/>
    <category term="企业级" scheme="https://blog.zisheng.pro/tags/%E4%BC%81%E4%B8%9A%E7%BA%A7/"/>
    <category term="中后台" scheme="https://blog.zisheng.pro/tags/%E4%B8%AD%E5%90%8E%E5%8F%B0/"/>
    <content>
      <![CDATA[<p>ModalForm 是 ProForm 的一个变体，本质上仍然是个表单。所以无法通过 footer 来自定义页脚,如果要定义页脚需要使用 submitter.render 来进行自定义。这两个表单的表现与 ProForm 相同，可以从 ProForm 直接修改而来。本文由浅入深一步一步记录 ModalForm 使用过程。</p><h2 id="最小使用"><a href="#最小使用" class="headerlink" title="最小使用"></a>最小使用</h2><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><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;antd&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">ModalForm</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;@ant-design/pro-components&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">PlusOutlined</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;@ant-design/icons&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> () =&gt; &#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">ModalForm</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">        <span class="attr">title</span>=<span class="string">&quot;新建项目&quot;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">        <span class="attr">trigger</span>=<span class="string">&#123;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">          &lt;<span class="attr">Button</span> <span class="attr">type</span>=<span class="string">&quot;primary&quot;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">            <span class="tag">&lt;<span class="name">PlusOutlined</span> /&gt;</span>新建项目</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">        &#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">ModalForm</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><ul><li><code>title</code>: 弹框的标题</li><li><code>trigger</code>: 用于触发 Modal 打开的 dom，一般是 button</li></ul><p><img src="https://s2.loli.net/2023/08/04/9vINFYMud2xZtJe.png"></p><h2 id="表单项"><a href="#表单项" class="headerlink" title="表单项"></a>表单项</h2><p>一个表单除了 Form 之外还是需要一系列的表单项，ProForm 自带了数量可观的表单项, 这些组件本质上是 Form.Item 和 组件的结合，我们可以把他们当成一个 FormItem 来使用，并且支持各种 props。每个表单项都支持 fieldProps 属性来支持设置输入组件的props。 我们支持了 placeholder 的透传，你可以直接在组件上设置 placeholder。</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br></pre></td><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;antd&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">ModalForm</span>, <span class="title class_">ProForm</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;@ant-design/pro-components&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">PlusOutlined</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;@ant-design/icons&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> () =&gt; &#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">ModalForm</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">        <span class="attr">title</span>=<span class="string">&quot;新建项目&quot;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">        <span class="attr">trigger</span>=<span class="string">&#123;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">          &lt;<span class="attr">Button</span> <span class="attr">type</span>=<span class="string">&quot;primary&quot;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">            <span class="tag">&lt;<span class="name">PlusOutlined</span> /&gt;</span>新建项目</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">        &#125;</span></span><br><span class="line"><span class="language-xml">        onFinish=&#123;async (values) =&gt; &#123;</span></span><br><span class="line"><span class="language-xml">          console.log(values.name);</span></span><br><span class="line"><span class="language-xml">          message.success(&#x27;提交成功&#x27;);</span></span><br><span class="line"><span class="language-xml">          return true;</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">        <span class="tag">&lt;<span class="name">ProFormText</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">          <span class="attr">width</span>=<span class="string">&quot;xl&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">label</span>=<span class="string">&quot;项目名称&quot;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">          <span class="attr">tooltip</span>=<span class="string">&quot;最长为 24 位&quot;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">          <span class="attr">placeholder</span>=<span class="string">&quot;请输入名称&quot;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">          <span class="attr">fieldProps</span>=<span class="string">&#123;&#123;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">            <span class="attr">maxLength:</span> <span class="attr">50</span>,</span></span></span><br><span class="line"><span class="tag"><span class="language-xml">            <span class="attr">showCount:</span> <span class="attr">true</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">          &#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">      <span class="tag">&lt;/<span class="name">ModalForm</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><ul><li><code>onFinish</code>: 提交数据时触发，如果返回一个 true。会关掉抽屉,如果配置了 <code>destroyOnClose</code> 还会重置表单。</li><li><code>ProFormText</code>: ProFormText 是 FormItem + Input 的产物，用于输入各类文本</li></ul><blockquote><p>注意：onFinish 是提交表单且数据验证成功后回调事件，同 antd 4 Form 组件 API</p></blockquote><p><img src="https://s2.loli.net/2023/08/04/t4LwjbKHGNSP8Rn.png"></p><h2 id="表单校验"><a href="#表单校验" class="headerlink" title="表单校验"></a>表单校验</h2><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br></pre></td><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;antd&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">ModalForm</span>, <span class="title class_">ProForm</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;@ant-design/pro-components&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">PlusOutlined</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;@ant-design/icons&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> () =&gt; &#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">ModalForm</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">        <span class="attr">title</span>=<span class="string">&quot;新建项目&quot;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">        <span class="attr">trigger</span>=<span class="string">&#123;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">          &lt;<span class="attr">Button</span> <span class="attr">type</span>=<span class="string">&quot;primary&quot;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">            <span class="tag">&lt;<span class="name">PlusOutlined</span> /&gt;</span>新建项目</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">        &#125;</span></span><br><span class="line"><span class="language-xml">        onFinish=&#123;async (values) =&gt; &#123;</span></span><br><span class="line"><span class="language-xml">          console.log(values.name);</span></span><br><span class="line"><span class="language-xml">          message.success(&#x27;提交成功&#x27;);</span></span><br><span class="line"><span class="language-xml">          return true;</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">        <span class="tag">&lt;<span class="name">ProFormText</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">          <span class="attr">width</span>=<span class="string">&quot;xl&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">label</span>=<span class="string">&quot;项目名称&quot;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">          <span class="attr">tooltip</span>=<span class="string">&quot;最长为 24 位&quot;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">          <span class="attr">placeholder</span>=<span class="string">&quot;请输入名称&quot;</span>,</span></span></span><br><span class="line"><span class="tag"><span class="language-xml">          <span class="attr">rules</span>=<span class="string">&#123;[</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">            &#123;</span></span></span><br><span class="line"><span class="tag"><span class="language-xml">              <span class="attr">required:</span> <span class="attr">true</span>,</span></span></span><br><span class="line"><span class="tag"><span class="language-xml">              <span class="attr">message:</span> &quot;<span class="attr">请输入项目名称</span>&quot;</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">          ]&#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">ModalForm</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>我们只需要关心业务规则，表单校验通过之后 onFinish 才会被调用。</p><p><img src="https://s2.loli.net/2023/08/04/ex4aNfWPXJ28oCp.png"></p><h2 id="默认值"><a href="#默认值" class="headerlink" title="默认值"></a>默认值</h2><p>优先级：request(ProForm) &gt; initialValues(ProForm) &gt; request(ProFormItem) &gt; initialValue(ProFormItem)</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br></pre></td><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;antd&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">ModalForm</span>, <span class="title class_">ProForm</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;@ant-design/pro-components&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">PlusOutlined</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;@ant-design/icons&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> () =&gt; &#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">ModalForm</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">        <span class="attr">title</span>=<span class="string">&quot;新建项目&quot;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">        <span class="attr">trigger</span>=<span class="string">&#123;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">          &lt;<span class="attr">Button</span> <span class="attr">type</span>=<span class="string">&quot;primary&quot;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">            <span class="tag">&lt;<span class="name">PlusOutlined</span> /&gt;</span>新建项目</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">        &#125;</span></span><br><span class="line"><span class="language-xml">        request=&#123;async () =&gt; (&#123;</span></span><br><span class="line"><span class="language-xml">          name: &quot;紫升的项目1&quot;</span></span><br><span class="line"><span class="language-xml">        &#125;&#125;&#125;</span></span><br><span class="line"><span class="language-xml">        initialValues=&#123;&#123;</span></span><br><span class="line"><span class="language-xml">          name: &quot;紫升的项目2&quot;</span></span><br><span class="line"><span class="language-xml">        &#125;&#125;</span></span><br><span class="line"><span class="language-xml">        onFinish=&#123;async (values) =&gt; &#123;</span></span><br><span class="line"><span class="language-xml">          console.log(values.name);</span></span><br><span class="line"><span class="language-xml">          message.success(&#x27;提交成功&#x27;);</span></span><br><span class="line"><span class="language-xml">          return true;</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">        <span class="tag">&lt;<span class="name">ProFormText</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">          <span class="attr">width</span>=<span class="string">&quot;xl&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">label</span>=<span class="string">&quot;项目名称&quot;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">          <span class="attr">tooltip</span>=<span class="string">&quot;最长为 24 位&quot;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">          <span class="attr">placeholder</span>=<span class="string">&quot;请输入名称&quot;</span>,</span></span></span><br><span class="line"><span class="tag"><span class="language-xml">          <span class="attr">request</span>=<span class="string">&#123;async</span> () =&gt;</span> &quot;紫升的项目3&quot;&#125;</span></span><br><span class="line"><span class="language-xml">          initialValue=&#123;&quot;紫升的项目4&quot;&#125;</span></span><br><span class="line"><span class="language-xml">          rules=&#123;[</span></span><br><span class="line"><span class="language-xml">            &#123;</span></span><br><span class="line"><span class="language-xml">              required: true,</span></span><br><span class="line"><span class="language-xml">              message: &quot;请输入项目名称&quot;</span></span><br><span class="line"><span class="language-xml">            &#125;</span></span><br><span class="line"><span class="language-xml">          ]&#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">ModalForm</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><h2 id="modalProps"><a href="#modalProps" class="headerlink" title="modalProps"></a>modalProps</h2><p>Modal 的 props，使用方式与 antd 相同。注意：不支持 ‘visible’，请使用全局的 visible，比如关闭时重置表单的 <code>destroyOnClose</code> 就在这里配置。</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><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;antd&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">ModalForm</span>, <span class="title class_">ProForm</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;@ant-design/pro-components&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">PlusOutlined</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;@ant-design/icons&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> () =&gt; &#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">ModalForm</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">        <span class="attr">title</span>=<span class="string">&quot;新建项目&quot;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">        <span class="attr">trigger</span>=<span class="string">&#123;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">          &lt;<span class="attr">Button</span> <span class="attr">type</span>=<span class="string">&quot;primary&quot;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">            <span class="tag">&lt;<span class="name">PlusOutlined</span> /&gt;</span>新建项目</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">        &#125;</span></span><br><span class="line"><span class="language-xml">        modalProps=&#123;&#123;</span></span><br><span class="line"><span class="language-xml">          destroyOnClose: true</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">        <span class="tag">&lt;<span class="name">ProFormText</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">          <span class="attr">width</span>=<span class="string">&quot;xl&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">label</span>=<span class="string">&quot;项目名称&quot;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">          <span class="attr">tooltip</span>=<span class="string">&quot;最长为 24 位&quot;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">          <span class="attr">placeholder</span>=<span class="string">&quot;请输入名称&quot;</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">ModalForm</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><ul><li><code>destroyOnClose</code>: 默认 ModalFrom 关闭后，表单不会重置，需要单独配置。</li></ul><h2 id="其他属性"><a href="#其他属性" class="headerlink" title="其他属性"></a>其他属性</h2><ul><li><code>autoFocusFirstInput</code>: 自动聚焦第一个输入框</li><li><code>submitTimeout</code>: 提交数据时，禁用取消按钮的超时时间（毫秒）。</li></ul>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/18fc5bd5e9a2/</id>
    <link href="https://blog.zisheng.pro/posts/18fc5bd5e9a2/"/>
    <published>2023-08-04T07:00:00.000Z</published>
    <summary>ModalForm 是 ProForm 的一个变体，本质上仍然是个表单。所以无法通过 footer 来自定义页脚,如果要定义页脚需要使用 submitter.render 来进行自定义。这两个表单的表现与 ProForm 相同，可以从 ProForm 直接修改而来。</summary>
    <title>Antd Pro ModalFrom 使用记录</title>
    <updated>2026-09-18T08:15:28.632Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="全栈开发" scheme="https://blog.zisheng.pro/categories/%E5%85%A8%E6%A0%88%E5%BC%80%E5%8F%91/"/>
    <category term="数据库" scheme="https://blog.zisheng.pro/categories/%E6%95%B0%E6%8D%AE%E5%BA%93/"/>
    <category term="分页" scheme="https://blog.zisheng.pro/tags/%E5%88%86%E9%A1%B5/"/>
    <category term="sequelize" scheme="https://blog.zisheng.pro/tags/sequelize/"/>
    <content>
      <![CDATA[<h2 id="findAndCountAll-方法"><a href="#findAndCountAll-方法" class="headerlink" title="findAndCountAll 方法"></a>findAndCountAll 方法</h2><p><code>findAndCountAll</code> 会在查出列表记录的同时, 还会返回符合你查询列表数据的条数。</p><h2 id="分页"><a href="#分页" class="headerlink" title="分页"></a>分页</h2><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">async</span> <span class="title function_">queryProjectList</span>(<span class="params">params: &#123; currentPage: <span class="built_in">number</span>, pageSize: <span class="built_in">number</span> &#125;</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> &#123; currentPage, pageSize &#125; = params;</span><br><span class="line">  <span class="keyword">const</span> result = <span class="keyword">await</span> <span class="title class_">UserModel</span>.<span class="title function_">findAndCountAll</span>(&#123;</span><br><span class="line">    <span class="attr">limit</span>: params.<span class="property">pageSize</span>, <span class="comment">// 确保是 number 类型</span></span><br><span class="line">    <span class="attr">offset</span>: (currentPage - <span class="number">1</span>) * pageSize, <span class="comment">// 计算偏移量算法</span></span><br><span class="line">  &#125;);</span><br><span class="line">  <span class="keyword">return</span> &#123;</span><br><span class="line">    <span class="attr">data</span>: result.<span class="property">rows</span>,</span><br><span class="line">    <span class="attr">total</span>: result.<span class="property">count</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><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">async</span> <span class="title function_">queryProjectList</span>(<span class="params">params: &#123; pid: <span class="built_in">string</span>, <span class="keyword">type</span>: ProjectType &#125;</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> &#123; pid, <span class="keyword">type</span> &#125; = params;</span><br><span class="line">  <span class="keyword">const</span> <span class="attr">condition</span>: <span class="built_in">any</span> = &#123;</span><br><span class="line">    <span class="attr">deletedAt</span>: <span class="literal">null</span>, <span class="comment">// deletedAt 为 null 表示未删除，需要配合 paranoid 模型配置</span></span><br><span class="line">  &#125;</span><br><span class="line">  <span class="comment">// 模糊查询</span></span><br><span class="line">  <span class="keyword">if</span> (pid) &#123;</span><br><span class="line">    condition.<span class="property">pid</span> = &#123; [<span class="title class_">Op</span>.<span class="property">like</span>]: <span class="string">`%<span class="subst">$&#123;pid&#125;</span>%`</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">if</span>(<span class="keyword">type</span>) &#123;</span><br><span class="line">    condition.<span class="property">type</span> = &#123; [<span class="title class_">Op</span>.<span class="property">eq</span>]: <span class="keyword">type</span> &#125;</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><h2 id="distinct"><a href="#distinct" class="headerlink" title="distinct"></a>distinct</h2><p>如果使用了 <code>include</code> 参数，需要额外传入 <code>distinct: true</code> 配置来去除 <code>include</code> 的数量。</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> result = <span class="keyword">await</span> <span class="title class_">Model</span>.<span class="title function_">findAndCountAll</span>(&#123;</span><br><span class="line">  <span class="attr">limit</span>: params.<span class="property">pageSize</span>, <span class="comment">// 确保是 number 类型</span></span><br><span class="line">  <span class="attr">offset</span>: (currentPage - <span class="number">1</span>) * pageSize, <span class="comment">// 计算偏移量算法</span></span><br><span class="line">  <span class="attr">include</span>: [</span><br><span class="line">    &#123; <span class="attr">model</span>: <span class="title class_">HobbyModel</span>&#125;,</span><br><span class="line">    &#123; <span class="attr">model</span>: <span class="title class_">FriendModel</span>&#125;,</span><br><span class="line">  ],</span><br><span class="line">  <span class="attr">distinct</span>: <span class="literal">true</span>,</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><ul><li><a href="https://github.com/sequelize/sequelize/issues/6418">result.count for findAndCountAll is incorrect with include</a></li></ul><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ul><li><a href="https://blog.csdn.net/qq_23521659/article/details/124871838">sequelize模糊查询及分页</a></li></ul>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/e6c3e5bafb7d/</id>
    <link href="https://blog.zisheng.pro/posts/e6c3e5bafb7d/"/>
    <published>2023-08-02T04:30:00.000Z</published>
    <summary>最近在开发一个全栈应用，起手就是一套增删改查，写到列表查询时，分页接口如何实现费了一番功夫，遂记录一下，以便后续使用。</summary>
    <title>实现 sequelize 分页和条件查询</title>
    <updated>2026-09-18T08:15:28.632Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="单元测试" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/%E5%8D%95%E5%85%83%E6%B5%8B%E8%AF%95/"/>
    <category term="Node.js" scheme="https://blog.zisheng.pro/tags/Node-js/"/>
    <category term="TypeScript" scheme="https://blog.zisheng.pro/tags/TypeScript/"/>
    <category term="jest" scheme="https://blog.zisheng.pro/tags/jest/"/>
    <content>
      <![CDATA[<h2 id="安装-jest"><a href="#安装-jest" class="headerlink" title="安装 jest"></a>安装 jest</h2><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ yarn add jest -D</span><br></pre></td></tr></table></figure><p>然后，将下列配置内容添加到您的 <code>package.json</code>：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><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;test&quot;</span><span class="punctuation">:</span> <span class="string">&quot;jest&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="安装-ts-jest"><a href="#安装-ts-jest" class="headerlink" title="安装 ts-jest"></a>安装 ts-jest</h2><p>ts-jest 是一个支持 sourcemap 的 TypeScript 预处理器，让你使用 TypeScript 编写 Jest 测试项目</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ yarn add --dev ts-jest</span><br></pre></td></tr></table></figure><h2 id="创建配置文件"><a href="#创建配置文件" class="headerlink" title="创建配置文件"></a>创建配置文件</h2><p>执行 <code>jest --init</code> 回答几个简单的问题之后会生成一个 <code>jest.config.ts</code> 文件，然后在这个文件中添加 <code>preset: &#39;ts-jest&#39;</code> 配置。</p><h2 id="安装-ts-node"><a href="#安装-ts-node" class="headerlink" title="安装 ts-node"></a>安装 ts-node</h2><p>如果配置文件是 <code>jest.config.ts</code>，那就需要安装 ts-node 支持它：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ yarn add ts-node -D</span><br></pre></td></tr></table></figure><h2 id="安装-types-jest"><a href="#安装-types-jest" class="headerlink" title="安装 @types&#x2F;jest"></a>安装 @types&#x2F;jest</h2><p>如果使用 typescript 写测试用例，那就需要安装 @types&#x2F;jest：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ yarn add @types/jest -D</span><br></pre></td></tr></table></figure><h2 id="踩坑"><a href="#踩坑" class="headerlink" title="踩坑"></a>踩坑</h2><h3 id="Error-Jest-‘ts-node’-is-required-for-the-TypeScript-configuration-files-Make-sure-it-is-installed"><a href="#Error-Jest-‘ts-node’-is-required-for-the-TypeScript-configuration-files-Make-sure-it-is-installed" class="headerlink" title="Error: Jest: ‘ts-node’ is required for the TypeScript configuration files. Make sure it is installed"></a>Error: Jest: ‘ts-node’ is required for the TypeScript configuration files. Make sure it is installed</h3><p>在全局执行 <code>jest</code>，会报错，意思是解析 typescript 配置文件需要安装 ts-node，</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 全局</span></span><br><span class="line">$ npm install ts-node -g</span><br></pre></td></tr></table></figure>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/a95721d19976/</id>
    <link href="https://blog.zisheng.pro/posts/a95721d19976/"/>
    <published>2023-07-28T12:00:00.000Z</published>
    <summary>本文介绍了如何在 Typescript 项目中初始化 Jest 并正确配置</summary>
    <title>Typescript 项目使用 Jest 入门</title>
    <updated>2026-09-18T08:15:28.632Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="工具" scheme="https://blog.zisheng.pro/categories/%E5%B7%A5%E5%85%B7/"/>
    <category term="问题排查" scheme="https://blog.zisheng.pro/categories/%E5%B7%A5%E5%85%B7/%E9%97%AE%E9%A2%98%E6%8E%92%E6%9F%A5/"/>
    <category term="排障" scheme="https://blog.zisheng.pro/tags/%E6%8E%92%E9%9A%9C/"/>
    <category term="Git" scheme="https://blog.zisheng.pro/tags/Git/"/>
    <category term="GitHub" scheme="https://blog.zisheng.pro/tags/GitHub/"/>
    <category term="网络" scheme="https://blog.zisheng.pro/tags/%E7%BD%91%E7%BB%9C/"/>
    <category term="代理" scheme="https://blog.zisheng.pro/tags/%E4%BB%A3%E7%90%86/"/>
    <content>
      <![CDATA[<p>执行 <code>git push</code> 或 <code>git clone</code> 时出现 <code>Failed to connect to github.com port 443</code>，表示连接还没到 GitHub 的认证阶段，就在 HTTPS 建连前失败了。过去常见的“写死 hosts IP”现在并不可靠：GitHub 的 CDN、证书校验和 IP 都可能变化，错误映射还会制造更难排的 TLS 问题。</p><h2 id="一句话总结"><a href="#一句话总结" class="headerlink" title="一句话总结"></a>一句话总结</h2><p>按“DNS 解析 → 本机代理 → 网络出口 → Git 配置”的顺序排查。hosts 只应作为明确、临时且可回滚的诊断手段，不是长期方案。</p><h2 id="先确认浏览器和-Git-是否同样失败"><a href="#先确认浏览器和-Git-是否同样失败" class="headerlink" title="先确认浏览器和 Git 是否同样失败"></a>先确认浏览器和 Git 是否同样失败</h2><p>在终端执行：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">curl -I https://github.com</span><br><span class="line">git ls-remote https://github.com/git/git.git HEAD</span><br></pre></td></tr></table></figure><p>如果浏览器能打开而 Git 失败，重点检查 Git 的代理配置；如果两者都失败，则更可能是 DNS、代理客户端或网络出口的问题。</p><h2 id="检查-Git-是否保留了旧代理"><a href="#检查-Git-是否保留了旧代理" class="headerlink" title="检查 Git 是否保留了旧代理"></a>检查 Git 是否保留了旧代理</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">git config --global --get http.proxy</span><br><span class="line">git config --global --get https.proxy</span><br></pre></td></tr></table></figure><p>代理客户端退出、端口变化后，Git 仍会尝试连接旧地址。确认不需要代理时，清掉旧配置：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">git config --global --<span class="built_in">unset</span> http.proxy</span><br><span class="line">git config --global --<span class="built_in">unset</span> https.proxy</span><br></pre></td></tr></table></figure><p>如果需要使用本地代理，确保地址、协议和端口与客户端实际监听一致；同时检查 shell 环境变量中的 <code>HTTP_PROXY</code>、<code>HTTPS_PROXY</code>。</p><h2 id="检查-DNS-和网络出口"><a href="#检查-DNS-和网络出口" class="headerlink" title="检查 DNS 和网络出口"></a>检查 DNS 和网络出口</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">nslookup github.com</span><br><span class="line">curl -v https://github.com</span><br></pre></td></tr></table></figure><p><code>nslookup</code> 异常说明先处理 DNS；DNS 正常但 <code>curl -v</code> 卡在连接阶段，则检查代理模式、防火墙或公司网络策略。公司网络下应优先使用合规的网络出口或咨询网络管理员，不要把公共 IP 固化进 hosts。</p><h2 id="一个更稳定的替代：SSH"><a href="#一个更稳定的替代：SSH" class="headerlink" title="一个更稳定的替代：SSH"></a>一个更稳定的替代：SSH</h2><p>HTTPS 网络策略不稳定、且账户已配置 SSH key 时，可以将远程地址切到 SSH：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">git remote set-url origin git@github.com:OWNER/REPO.git</span><br><span class="line">ssh -T git@github.com</span><br></pre></td></tr></table></figure><p>这不会绕过网络限制，但能避免 HTTPS 凭据与代理配置的额外变量。修改前可先执行 <code>git remote -v</code> 记录当前地址。</p><h2 id="我的判断框架"><a href="#我的判断框架" class="headerlink" title="我的判断框架"></a>我的判断框架</h2><p>优先修复：代理配置、DNS 和受控网络出口，它们才是可维护的根因。</p><p>避免采用：从第三方网站抄 IP 后长期写 hosts。它可能短暂恢复访问，却会在下一次基础设施变更时再次失效。</p>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/995252c5296a/</id>
    <link href="https://blog.zisheng.pro/posts/995252c5296a/"/>
    <published>2023-07-17T19:20:35.047Z</published>
    <summary>git push 或访问 GitHub 时出现 Failed to connect to github.com port 443，优先按 DNS、代理与网络出口逐层定位。不要再依赖易失效的 hosts IP。</summary>
    <title>「已解决」Failed to connect to github.com port 443</title>
    <updated>2026-09-18T08:15:28.636Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="工具" scheme="https://blog.zisheng.pro/categories/%E5%B7%A5%E5%85%B7/"/>
    <category term="hosts" scheme="https://blog.zisheng.pro/tags/hosts/"/>
    <category term="macos" scheme="https://blog.zisheng.pro/tags/macos/"/>
    <category term="chrome" scheme="https://blog.zisheng.pro/tags/chrome/"/>
    <content>
      <![CDATA[<h2 id="SwitchHosts"><a href="#SwitchHosts" class="headerlink" title="SwitchHosts"></a>SwitchHosts</h2><p>SwitchHosts是一款免费的跨平台hosts管理工具，可以帮助用户轻松快捷地管理多组hosts，方便地切换各种网络环境。它支持自定义规则、正则表达式以及备份和恢复等功能，是一款非常实用的网络工具。</p><p><img src="https://raw.githubusercontent.com/oldj/SwitchHosts/master/screenshots/sh_light.png" alt="SwitchHosts"></p><p>你可以下载源代码并自行构建，或从以下链接下载已构建的版本：</p><ul><li><a href="https://github.com/oldj/SwitchHosts/releases">SwitchHosts 下载页面（GitHub Release）</a></li></ul><p>您还可以使用软件包管理器 Chocolatey 安装已构建的版本：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">choco install switchhosts</span><br></pre></td></tr></table></figure><h2 id="iHosts"><a href="#iHosts" class="headerlink" title="iHosts"></a>iHosts</h2><p>iHosts 是一款 Mac OS X 上的 <code>/etc/hosts</code> 编辑器，可以通过使用组来管理 hosts，实现部分更新而不是替换整个 hosts 文件。用户可以在编辑 hosts 时使用“实时查看 hosts”的功能，也可以添加或删除 hosts 或 hosts 组。此应用程序以沙盒模式运行，并在编辑 hosts 时更新 <code>/etc/hosts</code> 文件。</p><p><img src="https://github.com/toolinbox/iHosts/raw/master/images/EditHosts.png" alt="iHosts"></p><p><a href="https://itunes.apple.com/app/id1102004240?ls=1&mt=12">&gt; &gt; &gt; 点击免费从 Mac App Store 下载 iHosts &lt;&lt;&lt;</a></p><h2 id="LightProxy"><a href="#LightProxy" class="headerlink" title="LightProxy"></a>LightProxy</h2><p>LightProxy 是阿里巴巴吧开源的一个跨平台的Web调试代理工具，具有证书安装和系统代理设置的完全自动化功能。它基于 Whistle，提供了强大的 <code>hosts/proxy/capture</code> 功能，并支持自定义 Node.js 代理规则。</p><p><img src="https://camo.githubusercontent.com/46a576f77303209ba2eb4df022a2520717064f22d7e297ff88ee7b07635b46f0/68747470733a2f2f696d672e616c6963646e2e636f6d2f7466732f5442313537624a462e5431674b306a535a46725858634e435858612d313339332d3932312e706e67" alt="LightProxy"></p><ul><li><a href="https://gw.alipayobjects.com/os/LightProxy/LightProxy.dmg">macOS Version Download</a></li><li><a href="https://gw.alipayobjects.com/os/LightProxy/LightProxy-Setup.exe">Windows Version Download</a></li></ul>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/7ac16e45f626/</id>
    <link href="https://blog.zisheng.pro/posts/7ac16e45f626/"/>
    <published>2023-07-17T18:47:19.466Z</published>
    <summary>SwitchHosts SwitchHosts是一款免费的跨平台hosts管理工具，可以帮助用户轻松快捷地管理多组hosts，方便地切换各种网络环境。它支持自定义规则、正则表达式以及备份和恢复等...</summary>
    <title>2023 hosts 切换工具推荐</title>
    <updated>2026-09-18T08:15:28.629Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="站点运营" scheme="https://blog.zisheng.pro/categories/%E7%AB%99%E7%82%B9%E8%BF%90%E8%90%A5/"/>
    <category term="Hexo" scheme="https://blog.zisheng.pro/tags/Hexo/"/>
    <category term="vuepress" scheme="https://blog.zisheng.pro/tags/vuepress/"/>
    <category term="qexo" scheme="https://blog.zisheng.pro/tags/qexo/"/>
    <content>
      <![CDATA[<h2 id="部署"><a href="#部署" class="headerlink" title="部署"></a>部署</h2><p>你可以通过 Vercel 提供的免费数据库。但请注意这是个 Beta 功能</p><a class="btn-beautify " href="https://vercel.com/new/clone?repository-url=https://github.com/am-abudu/Qexo" title="一键部署"><span>一键部署</span></a><p>首次部署会报错, 请无视并进行接下来的步骤</p><h3 id="申请-Vercel-数据库"><a href="#申请-Vercel-数据库" class="headerlink" title="申请 Vercel 数据库"></a>申请 Vercel 数据库</h3><p>进入 <a href="https://vercel.com/dashboard/stores">Vercel Storage</a> 界面 然后点击右上角的 Create Database 并选择 Postgres 创建免费 PostgreSQL 数据库, 在 Connect 页面获取数据库连接信息。请注意在地区选择的位置选择与你上一步项目对应的地区（通常为 Washington, D.C., USA (East) - iad1）。</p><h3 id="绑定项目"><a href="#绑定项目" class="headerlink" title="绑定项目"></a>绑定项目</h3><p>在左侧边栏选择 Projects 点击 Connect Project 连接到你第一步创建的项目</p><h3 id="部署-1"><a href="#部署-1" class="headerlink" title="部署"></a>部署</h3><p>回到你的项目页面，在 Deployments 点击 Redeploy 开始部署, 若没有 Error 信息即可打开域名进入初始化引导</p><h2 id="初始化"><a href="#初始化" class="headerlink" title="初始化"></a>初始化</h2><p>部署成功后，打开域名进入初始化引导：</p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1689507026731.png" alt="初始化 qexo"></p><h3 id="博客配置"><a href="#博客配置" class="headerlink" title="博客配置"></a>博客配置</h3><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1689507164192.png" alt="博客配置"></p><ul><li>服务商：github&#x2F;gitlab&#x2F;本地</li><li>使用配置：Hexo&#x2F;Hugo&#x2F;Valaxy&#x2F;Vuepress&#x2F;Vitepress</li><li>Github 密钥：于 <a href="https://github.com/settings/tokens">Github 设置</a> 生成的 Token (建议使用 Classical) 需要 Repo &amp; Workflow 下的权限 不建议给出所有权限</li><li>Github 仓库</li><li>项目分支</li><li>博客路径</li></ul><h3 id="Vercel-配置"><a href="#Vercel-配置" class="headerlink" title="Vercel 配置"></a>Vercel 配置</h3><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1689507547316.png" alt="Vercel 配置"></p><ul><li>Vercel 密钥：您的 Vercel 账户密钥 在 <a href="https://vercel.com/account/tokens">此处</a> 生成</li><li>项目ID：您 Qexo 部署所在项目的 ID 位于 Project Settings -&gt; General -&gt; Project ID</li></ul><h2 id="图床设置"><a href="#图床设置" class="headerlink" title="图床设置"></a>图床设置</h2><p>点击后台右上角设置按钮，进入设置页</p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1689508911802.png" alt="设置页面"></p><p>滑动到图床设置部分：</p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1689508861897.png" alt="图床设置"></p><ul><li>图床类型: 远程 API</li><li>API 地址: <a href="https://sm.ms/api/v2/upload">https://sm.ms/api/v2/upload</a></li><li>POST 参数名: <code>smfile</code></li><li>JSON 路径: <code>data.url</code></li><li>自定义请求头: <code>&#123;&quot;Authorization&quot;: &quot;sm.ms 密钥&quot;&#125;</code></li></ul><blockquote><p>sm.ms 密钥在 <a href="https://smms.app/home/apitoken">https://smms.app/home/apitoken</a> 生成</p></blockquote>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/c5e7f4e67be8/</id>
    <link href="https://blog.zisheng.pro/posts/c5e7f4e67be8/"/>
    <published>2023-07-16T11:06:44.000Z</published>
    <summary>Qexo 是一个美观、强大的在线静态博客管理器</summary>
    <title>静态博客管理器 qexo 搭建指南</title>
    <updated>2026-09-18T08:15:28.640Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="工具" scheme="https://blog.zisheng.pro/categories/%E5%B7%A5%E5%85%B7/"/>
    <category term="mac" scheme="https://blog.zisheng.pro/tags/mac/"/>
    <category term="github" scheme="https://blog.zisheng.pro/tags/github/"/>
    <content>
      <![CDATA[<p>github 提交的账户信息包括两部分 ：<code>git commit</code> 时日志中记录的用户名和邮箱信息、<code>git push</code>提交到远程时的账号密码验证信息。必须两者同时切换才能实现完美的切换账号的目的</p><h2 id="删除钥匙串"><a href="#删除钥匙串" class="headerlink" title="删除钥匙串"></a>删除钥匙串</h2><p>打开”钥匙串访问“。在右上方搜索 git，找到所有 github 相关的账号信息。按删除键，删除所有名称为 github.com 的条目。放心，这个不会影响 Chrome 浏览器保存的用户名和密码。</p><h2 id="执行命令"><a href="#执行命令" class="headerlink" title="执行命令"></a>执行命令</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">cp</span> ~/.gitconfig ~/.gitconfig_a</span><br><span class="line"><span class="built_in">cp</span> ~/.gitconfig ~/.gitconfig_b</span><br></pre></td></tr></table></figure><p>在 <code>gitconfig\_a</code> 和 <code>gitconfig\_b</code> 两个文件中分别填写提交日志的用户信息。</p><h2 id="切换账号"><a href="#切换账号" class="headerlink" title="切换账号"></a>切换账号</h2><p>需要用 <code>user\_a</code> 的时候：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">cp</span> ~/.gitconfig_a ~/.gitconfig</span><br></pre></td></tr></table></figure><p>然后执行 <code>git push</code> 输入 <code>a</code> 的登录信息即可以提交。</p><h2 id="zsh-设置alias"><a href="#zsh-设置alias" class="headerlink" title="zsh 设置alias"></a>zsh 设置alias</h2><p>在 <code>~/.zshrc</code> 添加别名</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">alias</span> cg=<span class="string">&quot;cat ~/.gitconfig&quot;</span></span><br><span class="line"><span class="built_in">alias</span> cga=<span class="string">&quot;cp ~/.gitconfig_a ~/.gitconfig&quot;</span></span><br><span class="line"><span class="built_in">alias</span> cgb=<span class="string">&quot;cp ~/.gitconfig_b ~/.gitconfig&quot;</span></span><br></pre></td></tr></table></figure><p>然后保存后执行 <code>source ~/.zshrc</code> 使之生效。</p><p>以后每次想切换时即可先用 <code>cg</code> 命令查询当前账户，根据情况执行 <code>cga</code> 或 <code>cgb</code> 切换账户。</p>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/5a54a3be5fa0/</id>
    <link href="https://blog.zisheng.pro/posts/5a54a3be5fa0/"/>
    <published>2023-07-06T21:39:05.142Z</published>
    <summary>本文详细讲述了如何在 Mac 环境下 切换 github 账号</summary>
    <title>Mac 切换 github 账号</title>
    <updated>2026-09-18T08:15:28.629Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="React" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/React/"/>
    <category term="React" scheme="https://blog.zisheng.pro/tags/React/"/>
    <content>
      <![CDATA[<p>React 是一种流行的 JavaScript 库，用于构建用户界面。自 React 在 2013 年首次发布以来，它已经成为了前端开发中不可或缺的一部分。在本文中，我们将按年回顾 React 的发展历史。</p><h2 id="童年"><a href="#童年" class="headerlink" title="童年"></a>童年</h2><p>React 最初是由 Facebook 的软件工程师 Jordan Walke 开发的。他在2010年为Facebook的广告平台开发了一种新的模型，这个模型可以根据用户的操作来动态地更新广告。这个模型后来演变成了React框架，并于2013年首次发布。</p><h2 id="幼儿园"><a href="#幼儿园" class="headerlink" title="幼儿园"></a>幼儿园</h2><h3 id="2013-年"><a href="#2013-年" class="headerlink" title="2013 年"></a>2013 年</h3><p>React 首次发布。它由 Facebook 的工程师们开发，旨在提高应用程序的性能和可维护性。React 的主要特点是虚拟 DOM，这是一种在内存中表示 DOM 的方式。通过使用虚拟 DOM，React 可以减少 DOM 操作的次数，从而提高应用程序的性能。</p><h3 id="2014-年"><a href="#2014-年" class="headerlink" title="2014 年"></a>2014 年</h3><p>React 发布了 0.12 版本。这个版本引入了 Mixins，这是一种在组件之间共享代码的方式。Mixins 很快成为了 React 中一个受欢迎的特性，但是在 2016 年被废弃了。</p><h3 id="2015-年"><a href="#2015-年" class="headerlink" title="2015 年"></a>2015 年</h3><p>React 发布了 0.14 版本。这个版本引入了 stateless 函数组件，这是一种更简单的组件形式，它没有状态或生命周期方法。stateless 函数组件很快成为了 React 中一个流行的特性，因为它们更易于测试和理解。</p><h2 id="小学"><a href="#小学" class="headerlink" title="小学"></a>小学</h2><h3 id="2016-年"><a href="#2016-年" class="headerlink" title="2016 年"></a>2016 年</h3><p>React 发布了 15.0 版本。这个版本引入了 PropTypes，这是一种用于验证组件属性的机制。PropTypes 很快成为了 React 中一个受欢迎的特性，因为它可以帮助开发人员捕获错误并提高代码质量。</p><h3 id="2017-年"><a href="#2017-年" class="headerlink" title="2017 年"></a>2017 年</h3><p>React 发布了 16.0 版本。这个版本引入了 Fiber，这是一个重新设计的渲染引擎，旨在提高 React 的性能和可维护性。Fiber 的主要特点是可以中断和恢复渲染过程，从而使 React 更加灵活和响应。</p><h3 id="2018-年"><a href="#2018-年" class="headerlink" title="2018 年"></a>2018 年</h3><p>React 发布了 16.3 版本。这个版本引入了 Context API，这是一种用于在组件之间共享数据的机制。Context API 可以帮助开发人员避免使用 prop drilling，并使代码更加简洁和可维护。</p><h3 id="2019-年"><a href="#2019-年" class="headerlink" title="2019 年"></a>2019 年</h3><p>React 发布了 16.8 版本。这个版本引入了 Hooks，这是一种用于在函数组件中使用状态和其他 React 功能的机制。Hooks 可以帮助开发人员编写更简洁、更易于理解和测试的代码。</p><h3 id="2020-年"><a href="#2020-年" class="headerlink" title="2020 年"></a>2020 年</h3><p>React 发布了 17.0 版本。这个版本没有引入太多新功能，而是主要关注向后兼容性和稳定性。React 17.0 的主要目标是使迁移更加容易，并为未来的版本打下基础。</p><p>总的来说，React 在过去的几年中经历了很多变化和进步。从虚拟 DOM 到 Hooks，每个版本都引入了新的功能和改进，并使 React 变得更加强大、灵活和易于使用。作为前端开发人员，我们可以期待着未来更多令人兴奋的变化和进步。</p><h3 id="2021-年"><a href="#2021-年" class="headerlink" title="2021 年"></a>2021 年</h3><ul><li>React 18: 它引入了许多新功能，例如自动批量更新、新的上下文API和React响应模式等。</li><li>React Server Components: React Server Components是一个实验性的功能，它允许React组件在服务器上渲染，从而提高性能和可靠性。</li></ul><h2 id="中学"><a href="#中学" class="headerlink" title="中学"></a>中学</h2><h3 id="2022-年"><a href="#2022-年" class="headerlink" title="2022 年"></a>2022 年</h3><p>2022 年的 React 更新包括发布了 React 18 正式版，其中包含了新的客户端渲染 API 和更新的服务端渲染 API，完全支持了服务端的 Suspense 和流式 SSR。</p><h3 id="2023-年"><a href="#2023-年" class="headerlink" title="2023 年"></a>2023 年</h3><ul><li>React 进阶版文档的发布，其中介绍了组件状态的保留和重置机制，以及不同位置渲染组件时状态的处理方式。</li><li>React 团队推出了一种新型组件——Server Components，它们可以提前运行，并且不包含在你的 JavaScript bundle 中。这些组件可以在构建过程中运行，让你从文件系统中读取或获取静态内容，也可以在服务器上运行，让你无需构建 API 即可访问数据层。</li></ul>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/36fe8cde4212/</id>
    <link href="https://blog.zisheng.pro/posts/36fe8cde4212/"/>
    <published>2023-06-02T08:48:23.000Z</published>
    <summary>2013年5月29日，React 迎来它的十周年纪念日。在这个值得纪念的日子里，我们回顾一下 React 的历史和发展。</summary>
    <title>React 发布 10 周年</title>
    <updated>2026-09-18T08:15:28.632Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="站点运营" scheme="https://blog.zisheng.pro/categories/%E7%AB%99%E7%82%B9%E8%BF%90%E8%90%A5/"/>
    <category term="广告" scheme="https://blog.zisheng.pro/categories/%E7%AB%99%E7%82%B9%E8%BF%90%E8%90%A5/%E5%B9%BF%E5%91%8A/"/>
    <category term="广告" scheme="https://blog.zisheng.pro/tags/%E5%B9%BF%E5%91%8A/"/>
    <category term="RPM" scheme="https://blog.zisheng.pro/tags/RPM/"/>
    <category term="站点运营" scheme="https://blog.zisheng.pro/tags/%E7%AB%99%E7%82%B9%E8%BF%90%E8%90%A5/"/>
    <category term="网站运营" scheme="https://blog.zisheng.pro/tags/%E7%BD%91%E7%AB%99%E8%BF%90%E8%90%A5/"/>
    <category term="CPM" scheme="https://blog.zisheng.pro/tags/CPM/"/>
    <content>
      <![CDATA[<p>RPM（Revenue per mille）广告是指每千次广告展示可以获得的收入。它是根据广告的展示次数、点击率和广告主出价等因素计算出来的。</p><p>在数字化时代，互联网广告已经成为了企业营销的重要手段。而对于广告主来说，他们最关心的就是广告的效果，即广告投放所带来的收益。而每千次展示收入（RPM）就是一个衡量广告效果的指标。</p><p>RPM是指广告主在每千次展示中所获得的收入。它是通过将广告收入除以展示次数，再乘以 1000 得出的。这个指标可以帮助广告主更好地了解他们的广告效果，并作出相应的调整。</p><p>RPM是一个非常重要的指标，因为它可以帮助广告主了解他们的广告投放是否能够带来足够的收益。如果RPM较低，可能意味着广告主需要重新考虑他们的广告策略，或者优化他们的广告内容，以提高点击率和转化率。</p><p>另外，RPM还可以帮助广告主评估不同媒体之间的效果差异。通过比较不同媒体的RPM，广告主可以更好地了解哪些媒体对他们的广告投放最有效，并将更多的预算投入到这些媒体上。</p><p>但是，需要注意的是，RPM并不是衡量广告效果的唯一指标。其他指标，如点击率、转化率、ROI等也同样重要。因此，在评估广告效果时，需要综合考虑多个指标，并结合实际情况进行分析。</p><p>总之，每千次展示收入（RPM）是一个非常重要的指标，可以帮助广告主更好地了解他们的广告效果，并作出相应的调整。但是需要注意的是，它并不是衡量广告效果的唯一指标，需要综合考虑多个指标，并结合实际情况进行分析。</p>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/06449303494e/</id>
    <link href="https://blog.zisheng.pro/posts/06449303494e/"/>
    <published>2023-05-11T06:12:57.000Z</published>
    <summary>RPM（Revenue per mille）广告是指每千次广告展示可以获得的收入。它是根据广告的展示次数、点击率和广告主出价等因素计算出来的。</summary>
    <title>RPM 广告是什么?</title>
    <updated>2026-09-18T08:15:28.632Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="VS Code" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/VS-Code/"/>
    <category term="VS Code" scheme="https://blog.zisheng.pro/tags/VS-Code/"/>
    <category term="光标" scheme="https://blog.zisheng.pro/tags/%E5%85%89%E6%A0%87/"/>
    <content>
      <![CDATA[<p>这是一种非常方便的操作方法，特别是在需要批量添加字符的时候。以下是这个方法的详细步骤：</p><ol><li>首先，在 Mac 上打开文本编辑器或其他编程工具。</li><li>选中你想要添加字符的多行文本。</li><li>按住 <kbd>shift</kbd> 和 <kbd>option</kbd> 键，并用鼠标点击其中一行的行首或行尾。</li><li>这样就可以在所有选中的行的行首或行尾添加同样的字符了。</li></ol><kbd>shift</kbd> 和 <kbd>option</kbd> 键需要同时按住，且在点击行首或行尾之前不要释放它们。<p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1683688081979.png" alt="vscode 多行光标"></p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1683688107699.png" alt="vscode 多行光标"></p>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/bb4c7efe8b3e/</id>
    <link href="https://blog.zisheng.pro/posts/bb4c7efe8b3e/"/>
    <published>2023-05-10T03:06:14.000Z</published>
    <summary>这是一种方便的方法，可用于在 Mac 上批量添加字符。选中多行文本后，按住 shift + option 键，然后点击其中一行的行首或行尾即可实现。</summary>
    <title>vscode 选择多行，将光标定位至每行行首或行尾</title>
    <updated>2026-09-18T08:15:28.636Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="Gatsby" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/Gatsby/"/>
    <category term="React" scheme="https://blog.zisheng.pro/tags/React/"/>
    <category term="TypeScript" scheme="https://blog.zisheng.pro/tags/TypeScript/"/>
    <category term="Gatsby" scheme="https://blog.zisheng.pro/tags/Gatsby/"/>
    <content>
      <![CDATA[<p>在这篇文章中，我们将逐步了解如何向我们的 gatsby 站点添加导入别名。</p><p>我们设置导入别名的原因更多是为了代码的可读性和导入组件时代码外观的美观。</p><p>我的意思是，如果我们看一下以下示例：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">Subscribe</span> <span class="keyword">from</span> <span class="string">&#x27;../../../../../../../core/modules/newsletter/mixins/Subscribe&#x27;</span></span><br></pre></td></tr></table></figure><p>在我看来，这样看起来非常丑陋，那么我们该如何改进呢？</p><p>我们可以通过更新 webpack 配置来包含针对我们已知将成为组件基础的主目录的别名来实现这一点。</p><p>一旦我们完成了这篇文章中的所有步骤，最终结果将如下所示：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">Subscribe</span> <span class="keyword">from</span> <span class="string">&#x27;@/core/modules/newsletter/mixins/Subscribe&#x27;</span></span><br></pre></td></tr></table></figure><h2 id="给-Gatsby-添加依赖"><a href="#给-Gatsby-添加依赖" class="headerlink" title="给 Gatsby 添加依赖"></a>给 Gatsby 添加依赖</h2><p>考虑到 Gatsby 使用 Webpack 作为其核心，并且默认情况下不会公开配置，因此我们可以使用 Gatsby 的 <code>onCreateWebpackConfig</code> API 添加自定义 Webpack 配置，这将导致自定义配置被合并，使您能够修改默认的 webpack 配置。</p><p>要添加自定义Webpack配置，我们需要编辑（或在项目根目录中创建文件，如果不存在）gatsby-node.js，并将新的配置添加到其中。</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> path = <span class="built_in">require</span>(<span class="string">&quot;path&quot;</span>);</span><br><span class="line"><span class="built_in">exports</span>.<span class="property">onCreateWebpackConfig</span> = <span class="function">(<span class="params">&#123; actions &#125;</span>) =&gt;</span> &#123;</span><br><span class="line">  actions.<span class="title function_">setWebpackConfig</span>(&#123;</span><br><span class="line">    <span class="attr">resolve</span>: &#123;</span><br><span class="line">      <span class="attr">alias</span>: &#123;</span><br><span class="line">        <span class="string">&quot;@/components&quot;</span>: path.<span class="title function_">resolve</span>(__dirname, <span class="string">&quot;src/components&quot;</span>),</span><br><span class="line">        <span class="string">&quot;@/static&quot;</span>: path.<span class="title function_">resolve</span>(__dirname, <span class="string">&quot;static&quot;</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>正如我们在上面的代码片段中看到的那样，actions 对象提供了使用 <code>setWebpackConfig</code> 选项的选项，该选项接受我们的自定义 webpack 配置并将其合并到 Gatsby 的 webpack 配置中。</p><p>为了添加新的别名，我们将使用 webpack 的 <code>resolve.alias</code>，这将允许我们在组件内部使用新创建的导入别名。</p><p>如下所示，在添加新别名后的最终结果将如下所示：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">Layout</span> <span class="keyword">from</span> <span class="string">&#x27;@components/Layout&#x27;</span>;</span><br></pre></td></tr></table></figure><h2 id="解析-ESlint-import"><a href="#解析-ESlint-import" class="headerlink" title="解析 ESlint import"></a>解析 ESlint import</h2><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">settings</span>: &#123;</span><br><span class="line">    <span class="string">&#x27;import/parsers&#x27;</span>: &#123;</span><br><span class="line">      <span class="string">&#x27;@typescript-eslint/parser&#x27;</span>: [<span class="string">&#x27;.ts&#x27;</span>],</span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="string">&#x27;import/resolver&#x27;</span>: &#123;</span><br><span class="line">      <span class="comment">// use &lt;root&gt;/tsconfig.json</span></span><br><span class="line">      <span class="attr">typescript</span>: &#123;</span><br><span class="line">        <span class="attr">alwaysTryTypes</span>: <span class="literal">true</span>, <span class="comment">// always try to resolve types under `&lt;roo/&gt;@types` directory even it doesn&#x27;t contain any source code, like `@types/unist`</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><h2 id="Typescript"><a href="#Typescript" class="headerlink" title="Typescript"></a>Typescript</h2><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;compilerOptions&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;baseUrl&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;paths&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">      <span class="attr">&quot;@/components/*&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span><span class="string">&quot;src/components/*&quot;</span><span class="punctuation">]</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></pre></td></tr></table></figure>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/d36cd5be8989/</id>
    <link href="https://blog.zisheng.pro/posts/d36cd5be8989/"/>
    <published>2023-05-08T10:33:33.000Z</published>
    <summary>在这篇文章中，我们将逐步了解如何向我们的 gatsby 站点添加导入别名。</summary>
    <title>如何设置 Gatsby 绝对路径引用别名</title>
    <updated>2026-09-18T08:15:28.640Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="React" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/React/"/>
    <category term="React" scheme="https://blog.zisheng.pro/tags/React/"/>
    <category term="TypeScript" scheme="https://blog.zisheng.pro/tags/TypeScript/"/>
    <category term="compilerOptions" scheme="https://blog.zisheng.pro/tags/compilerOptions/"/>
    <content>
      <![CDATA[<p>本文讲解了如何设置 React Typescript 项目绝对路径引用别名</p><h2 id="craco"><a href="#craco" class="headerlink" title="craco"></a>craco</h2><p>1、安装修改 CRA 配置的包：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ yarn add @craco/craco</span><br></pre></td></tr></table></figure><p>2、在项目根目录中创建 craco 的配置文件：<code>craco.config.js</code>，并在配置文件中配置路径别名</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> path = <span class="built_in">require</span>(<span class="string">&#x27;path&#x27;</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">webpack</span>: &#123;</span><br><span class="line">    <span class="attr">alias</span>: &#123;</span><br><span class="line">      <span class="comment">// 约定：使用 @ 表示 src 文件所在路径</span></span><br><span class="line">      <span class="string">&#x27;@&#x27;</span>: path.<span class="title function_">resolve</span>(__dirname, <span class="string">&#x27;src&#x27;</span>),</span><br><span class="line">      <span class="comment">// 约定：使用 @scss 表示 样式 文件所在路径</span></span><br><span class="line">      <span class="string">&#x27;@scss&#x27;</span>: path.<span class="title function_">resolve</span>(__dirname, <span class="string">&#x27;src&#x27;</span>, <span class="string">&#x27;assets&#x27;</span>, <span class="string">&#x27;styles&#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><p>3、修改 package.json 中的脚本命令</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="comment">// 将 start/build/test 三个命令修改为 craco 方式</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;start&quot;</span><span class="punctuation">:</span> <span class="string">&quot;craco start&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;craco build&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;test&quot;</span><span class="punctuation">:</span> <span class="string">&quot;craco test&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;eject&quot;</span><span class="punctuation">:</span> <span class="string">&quot;react-scripts eject&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="Typescript"><a href="#Typescript" class="headerlink" title="Typescript"></a>Typescript</h2><p>1、创建 path.tsconfig.json 配置文件</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;compilerOptions&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;baseUrl&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;paths&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">      <span class="attr">&quot;@/*&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span><span class="string">&quot;src/*&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>2、在 <code>tsconfig.json</code> 中导入该配置文件，让配置生效</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><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;extends&quot;</span><span class="punctuation">:</span> <span class="string">&quot;./path.tsconfig.json&quot;</span><span class="punctuation">,</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p>配置完这一步，vscode 也会有目录提示了</p><h2 id="Eslint"><a href="#Eslint" class="headerlink" title="Eslint"></a>Eslint</h2><p>配置 <code>@typescript-eslint/parser</code> 并在 <code>parserOptions</code> 中指定 <code>project</code> 选项，以便在 Eslint 中使用 TypeScript 的类型检查。</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="variable language_">module</span>.<span class="property">exports</span> = &#123;</span><br><span class="line">  <span class="attr">parser</span>: <span class="string">&#x27;@typescript-eslint/parser&#x27;</span>,</span><br><span class="line">  <span class="attr">parserOptions</span>: &#123;</span><br><span class="line">    <span class="attr">project</span>: [<span class="string">&#x27;./tsconfig.json&#x27;</span>], <span class="comment">// Specify it only for TypeScript files</span></span><br><span class="line">  &#125;,</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/94e8b5cabf01/</id>
    <link href="https://blog.zisheng.pro/posts/94e8b5cabf01/"/>
    <published>2023-05-08T10:18:10.000Z</published>
    <summary>本文讲解了如何设置 React Typescript 项目绝对路径引用别名</summary>
    <title>如何设置 React Typescript 项目绝对路径引用别名</title>
    <updated>2026-09-18T08:15:28.640Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="React Native" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/React-Native/"/>
    <category term="React Native" scheme="https://blog.zisheng.pro/tags/React-Native/"/>
    <category term="TypeScript" scheme="https://blog.zisheng.pro/tags/TypeScript/"/>
    <category term="export" scheme="https://blog.zisheng.pro/tags/export/"/>
    <category term="全局类型" scheme="https://blog.zisheng.pro/tags/%E5%85%A8%E5%B1%80%E7%B1%BB%E5%9E%8B/"/>
    <category term="global" scheme="https://blog.zisheng.pro/tags/global/"/>
    <content>
      <![CDATA[<p>要在 TypeScript 中声明全局类型：</p><ol><li>创建一个 <code>global.d.ts</code> 文件并在全局命名空间中声明类型。</li><li>添加需要全局访问的类型或接口。</li><li>使用 <code>export {}</code> 使文件成为模块。</li></ol><p>在项目的 src 目录中，创建一个包含以下 <code>global.d.ts</code> 文件的 <code>types</code> 目录。</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> &#123;&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">declare</span> <span class="variable language_">global</span> &#123;</span><br><span class="line">  <span class="comment">/**</span></span><br><span class="line"><span class="comment">   * 现在声明进入全局命名空间的类型，或者增加全局命名空间中的现有声明。</span></span><br><span class="line"><span class="comment">   */</span></span><br><span class="line">  <span class="keyword">interface</span> <span class="title class_">Employee</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">name</span>: <span class="built_in">string</span>;</span><br><span class="line">    <span class="attr">salary</span>: <span class="built_in">number</span>;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">type</span> <span class="title class_">Person</span> = &#123;</span><br><span class="line">    <span class="attr">name</span>: <span class="built_in">string</span>;</span><br><span class="line">    <span class="attr">age</span>: <span class="built_in">number</span>;</span><br><span class="line">  &#125;;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>上面的例子展示了如何创建一个修改全局命名空间的模块。 我们创建了一个全局可访问的 Employee 和 Person 类型。</p><p>现在我可以访问我项目中的类型，而无需导入它们。</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="attr">emp</span>: <span class="title class_">Employee</span> = &#123;</span><br><span class="line">  <span class="attr">id</span>: <span class="number">1</span>,</span><br><span class="line">  <span class="attr">name</span>: <span class="string">&#x27;James&#x27;</span>,</span><br><span class="line">  <span class="attr">salary</span>: <span class="number">100</span>,</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(emp);</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="attr">person</span>: <span class="title class_">Person</span> = &#123;</span><br><span class="line">  <span class="attr">name</span>: <span class="string">&#x27;Tom&#x27;</span>,</span><br><span class="line">  <span class="attr">age</span>: <span class="number">30</span>,</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(person);</span><br></pre></td></tr></table></figure><p>如果我们在 IDE 中遇到错误，请尝试将类型目录的路径添加到 <code>tsconfig.json</code> 文件中。</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;compilerOptions&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;typeRoots&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span><span class="string">&quot;./types&quot;</span><span class="punctuation">,</span> <span class="string">&quot;./node_modules/@types&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>我们在 <code>global.d.ts</code> 文件中使用 <code>export {}</code> 行将其标记为外部模块。 模块是包含至少 1 个导入或导出语句的文件。 我们必须这样做才能扩大全局范围。</p>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/d46ebc530d48/</id>
    <link href="https://blog.zisheng.pro/posts/d46ebc530d48/"/>
    <published>2023-05-08T10:10:31.000Z</published>
    <summary>本文讲解了如何在 React Native 中声明全局类型</summary>
    <title>如何在 React Native 中声明全局类型</title>
    <updated>2026-09-18T08:15:28.636Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="问题排查" scheme="https://blog.zisheng.pro/categories/%E9%97%AE%E9%A2%98%E6%8E%92%E6%9F%A5/"/>
    <category term="Ant Design" scheme="https://blog.zisheng.pro/categories/%E9%97%AE%E9%A2%98%E6%8E%92%E6%9F%A5/Ant-Design/"/>
    <category term="Ant Design" scheme="https://blog.zisheng.pro/tags/Ant-Design/"/>
    <category term="datepicker" scheme="https://blog.zisheng.pro/tags/datepicker/"/>
    <category term="date.clone" scheme="https://blog.zisheng.pro/tags/date-clone/"/>
    <content>
      <![CDATA[<p>本文介绍了 Antd <DatePicker /> 组件报 date.clone is not a function 错误的解决方法</p><p>当我们给 Antd DatePicker 组件设置默认值或通过 <code>form.setFieldsValue</code> 设置值时，可能会遇到如下错误：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&lt;DatePicker /&gt; date.clone is not a <span class="keyword">function</span></span><br></pre></td></tr></table></figure><p>这是因为 Antd DatePicker 组件的默认值必须是 moment 对象，而我们设置的值可能是字符串或者 Date 对象，所以需要将其转换为 moment 对象：</p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1683531634144.png" alt="&lt;DatePicker &#x2F;&gt; date.clone is not a function"></p>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/f6ee9e277c41/</id>
    <link href="https://blog.zisheng.pro/posts/f6ee9e277c41/"/>
    <published>2023-05-08T07:36:04.000Z</published>
    <summary>
      <![CDATA[本文介绍了 Antd <DatePicker /> 组件报 date.clone is not a function 错误的解决方法]]>
    </summary>
    <title>
      <![CDATA[Antd <DatePicker /> date.clone is not a function]]>
    </title>
    <updated>2026-09-18T08:15:28.629Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="Google" scheme="https://blog.zisheng.pro/categories/Google/"/>
    <category term="Google AdSense" scheme="https://blog.zisheng.pro/categories/Google/Google-AdSense/"/>
    <category term="站点运营" scheme="https://blog.zisheng.pro/categories/%E7%AB%99%E7%82%B9%E8%BF%90%E8%90%A5/"/>
    <category term="广告" scheme="https://blog.zisheng.pro/categories/%E7%AB%99%E7%82%B9%E8%BF%90%E8%90%A5/%E5%B9%BF%E5%91%8A/"/>
    <category term="Google AdSense" scheme="https://blog.zisheng.pro/tags/Google-AdSense/"/>
    <category term="Google" scheme="https://blog.zisheng.pro/tags/Google/"/>
    <category term="W-8BEN" scheme="https://blog.zisheng.pro/tags/W-8BEN/"/>
    <content>
      <![CDATA[<p>本文讲解了如何填写 Google Adsense 税务信息可以免税，以及如何填写 W-8 BEN 表格。</p><h2 id="开始填表"><a href="#开始填表" class="headerlink" title="开始填表"></a>开始填表</h2><p>以此点击 <code>收款</code> -&gt; <code>收款信息</code> -&gt; <code>美国税务信息</code> -&gt; <code>开始填表</code></p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1683361664395.png" alt="Google Adsense 税务信息"><br><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1683361691097.png" alt="Google Adsense 税务信息"><br><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1683361711328.png" alt="Google Adsense 税务信息"></p><h2 id="基本信息"><a href="#基本信息" class="headerlink" title="基本信息"></a>基本信息</h2><ol><li>您使用的是哪种类型的账号？：个人</li><li>您是否是美国公民或居民？：否</li><li>请选择 W-8BEN：选择</li></ol><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1683361782277.png" alt="Google Adsense 税务信息"></p><h2 id="W-8BEN-表单"><a href="#W-8BEN-表单" class="headerlink" title="W-8BEN 表单"></a>W-8BEN 表单</h2><p>按照下列图中的信息填写即可，需要注意的有：</p><ol><li>个人姓名填写拼音大写，比如 <code>LI MEI</code></li><li>外国的纳税人税号填写身份证号</li><li>报税选择无纸化</li></ol><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1683362043300.png" alt="Google Adsense 税务信息"><br><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1683362207801.png" alt="Google Adsense 税务信息"><br><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1683362383338.png" alt="Google Adsense 税务信息"><br><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1683362398811.png" alt="Google Adsense 税务信息"><br><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1683362490608.png" alt="Google Adsense 税务信息"><br><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1683362505420.png" alt="Google Adsense 税务信息"></p><h2 id="被接受通知"><a href="#被接受通知" class="headerlink" title="被接受通知"></a>被接受通知</h2><p>如下图，显示“您的税务信息已被接受”，说明设置成功了。</p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1683363578326.png" alt="Google Adsense 税务信息"></p>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/66ec8687cd46/</id>
    <link href="https://blog.zisheng.pro/posts/66ec8687cd46/"/>
    <published>2023-05-06T11:47:53.000Z</published>
    <summary>本文讲解了如何填写 Google Adsense 税务信息可以免税，以及如何填写 W-8 BEN 表格。</summary>
    <title>如何填写 Google Adsense 税务信息可以免税 | W-8BEN</title>
    <updated>2026-09-18T08:15:28.636Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="Google" scheme="https://blog.zisheng.pro/categories/Google/"/>
    <category term="Google AdSense" scheme="https://blog.zisheng.pro/categories/Google/Google-AdSense/"/>
    <category term="站点运营" scheme="https://blog.zisheng.pro/categories/%E7%AB%99%E7%82%B9%E8%BF%90%E8%90%A5/"/>
    <category term="广告" scheme="https://blog.zisheng.pro/categories/%E7%AB%99%E7%82%B9%E8%BF%90%E8%90%A5/%E5%B9%BF%E5%91%8A/"/>
    <category term="Google AdSense" scheme="https://blog.zisheng.pro/tags/Google-AdSense/"/>
    <category term="内容变现" scheme="https://blog.zisheng.pro/tags/%E5%86%85%E5%AE%B9%E5%8F%98%E7%8E%B0/"/>
    <category term="博客变现" scheme="https://blog.zisheng.pro/tags/%E5%8D%9A%E5%AE%A2%E5%8F%98%E7%8E%B0/"/>
    <category term="账号注册" scheme="https://blog.zisheng.pro/tags/%E8%B4%A6%E5%8F%B7%E6%B3%A8%E5%86%8C/"/>
    <category term="中国" scheme="https://blog.zisheng.pro/tags/%E4%B8%AD%E5%9B%BD/"/>
    <category term="大陆" scheme="https://blog.zisheng.pro/tags/%E5%A4%A7%E9%99%86/"/>
    <category term="Google" scheme="https://blog.zisheng.pro/tags/Google/"/>
    <content>
      <![CDATA[<p>Google Adsense 是一个由 Google 公司设置的广告计划，会员可以利用 Youtube 流量和 Blogspot 功能置入广告服务，以赚取佣金。会员可分得搜索广告收益的 51%、内容广告收益的 68%。本文将介绍如何注册 Google Adsense 账户。</p><h2 id="前提"><a href="#前提" class="headerlink" title="前提"></a>前提</h2><ul><li>一个可以访问 Google 的代理，你可以在 <a href="https://blog.zisheng.pro/vpn/">科学上网工具</a> 中选择一款</li><li>一个 Google 账号，可以参考 <a href="https://blog.zisheng.pro/382c92b68823/">2023 大陆人如何成功创建 Google 账户</a> 进行注册。</li></ul><h2 id="扬帆启航"><a href="#扬帆启航" class="headerlink" title="扬帆启航"></a>扬帆启航</h2><p>当你决定做一件事，就已经成功了一半，现在就让我们从点击 <a href="https://adsense.google.com/adsense/signup/create?referer=https://adsense.google.com/start/&sac=true&pli=1&authuser=0">Google AdSense 注册</a> 开始内容变现之路吧。</p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1682570658242.png" alt="Google AdSense 注册"></p><ul><li>您的网站：如果没有可以先不填写</li><li>让 AdSense 发挥更大作用：可选可不选</li><li>您的收款地址所在的国家&#x2F;地区：中国大陆可以设置免税，就选中国大陆</li><li>我们的条款及条件：勾选</li></ul><p>点击确认之后，就会进入到控制台：</p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1682571240158.png" alt="Google AdSense 注册"></p><h2 id="填写客户信息"><a href="#填写客户信息" class="headerlink" title="填写客户信息"></a>填写客户信息</h2><div class="note primary modern"><p>注意：这里的信息涉及以后得收款，一定要要仔细看下面的步骤，否则可能影响收款。</p></div><p>点击上一步的“请输入信息”卡片，进入到填写客户信息页面：</p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1682571783990.png" alt="Google AdSense 注册"></p><ul><li>账号类型：个人</li><li>姓名和地址：这里就填写真实的就可以，要注意的是一点要 <strong>在地址第 1 行或者地址第 2 行 中备注上电话</strong>，因为账号收入达到 5 美金之后 Google 就会给你寄一个用于验证身份的 PIN 码，由于是通过国际平信的方式发出，所以不会有电话，而你备注在地址里，邮递员就可以看到了。</li></ul><p>点击确认之后返回首页看到成功的提示就可以了。</p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1682572039101.png" alt="Google AdSense 注册"></p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>内容变现是一个需要漫长积累的事情，你可能会在这个过程中遇到很多困难，但是只要你坚持下去，就一定会成功的。</p>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/5c6d54a112a6/</id>
    <link href="https://blog.zisheng.pro/posts/5c6d54a112a6/"/>
    <published>2023-04-27T04:36:47.000Z</published>
    <summary>Google Adsense 是一个由 Google 公司设置的广告计划，会员可以利用 Youtube 流量和 Blogspot 功能置入广告服务，以赚取佣金。会员可分得搜索广告收益的51%、内容广告收益的68%。本文将介绍如何注册 Google Adsense 账户。</summary>
    <title>2023 Google Adsense 中国大陆账户注册教程</title>
    <updated>2026-09-18T08:15:28.629Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="微信" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/%E5%BE%AE%E4%BF%A1/"/>
    <category term="微信" scheme="https://blog.zisheng.pro/tags/%E5%BE%AE%E4%BF%A1/"/>
    <category term="oAuth" scheme="https://blog.zisheng.pro/tags/oAuth/"/>
    <category term="网页授权" scheme="https://blog.zisheng.pro/tags/%E7%BD%91%E9%A1%B5%E6%8E%88%E6%9D%83/"/>
    <category term="微信网页授权" scheme="https://blog.zisheng.pro/tags/%E5%BE%AE%E4%BF%A1%E7%BD%91%E9%A1%B5%E6%8E%88%E6%9D%83/"/>
    <category term="微信公众号" scheme="https://blog.zisheng.pro/tags/%E5%BE%AE%E4%BF%A1%E5%85%AC%E4%BC%97%E5%8F%B7/"/>
    <content>
      <![CDATA[<p>如果用户在微信客户端中访问第三方网页，公众号可以通过微信网页授权机制，来获取用户基本信息，进而实现业务逻辑。</p><h2 id="网页授权回调域名"><a href="#网页授权回调域名" class="headerlink" title="网页授权回调域名"></a>网页授权回调域名</h2><p>1、在微信公众号请求用户网页授权之前，开发者需要先到公众平台官网中的 <code>开发</code> → <code>接口权限</code> → <code>网页服务</code> → <code>网页授权</code> - <code>网页授权获取用户基本信息</code> 的配置选项中，修改授权回调域名。请注意，这里填写的是域名（是一个字符串），而不是 URL，因此请勿加 <code>http://</code> 等协议头。</p><div class="note info modern"><p>注意：通过 <code>设置</code> → <code>公众号设置</code> → <code>功能设置</code> → <code>网页授权域名</code></p></div><p>2、授权回调域名配置规范为全域名，比如需要网页授权的域名为：<code>www.qq.com</code>，配置以后此域名下面的页面 <code>http://www.qq.com/music.html</code>、<code>http://www.qq.com/login.html</code> 都可以进行 OAuth2.0 鉴权。但 <code>http://pay.qq.com</code>、<code>http://music.qq.com</code>、<code>http://qq.com</code> 无法进行 OAuth2.0 鉴权</p><p>3、如果公众号登录授权给了第三方开发者来进行管理，则不必做任何设置，由第三方代替公众号实现网页授权即可</p><h2 id="业务域名"><a href="#业务域名" class="headerlink" title="业务域名"></a>业务域名</h2><p>设置业务域名后，在微信内访问该域名下页面时，不会被重新排版。用户在该域名上进行输入时，不出现下图所示的安全提示。<br>注意事项：</p><p>1、可填写三个域名或路径（例：<code>wx.qq.com</code> 或 <code>wx.qq.com/mp</code>），需使用字母、数字及 <code>-</code> 的组合，不支持 IP 地址、端口号及短链域名。</p><p>2、填写的域名须通过ICP备案的验证。</p><p>3、将文件 <code>MP_verify_naGVGuiJ6KOsOwCA.txt</code>（点击下载）上传至填写域名或路径指向的 web 服务器（或虚拟主机）的目录（若填写域名，将文件放置在域名根目录下，例如 <code>wx.qq.com/MP_verify_naGVGuiJ6KOsOwCA.txt</code>；若填写路径，将文件放置在路径目录下，例如 <code>wx.qq.com/mp/MP_verify_naGVGuiJ6KOsOwCA.txt</code>），并确保可以访问。</p><p>4、一个自然月内最多可修改并保存三次，本月剩余保存次数：3</p>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/f6ae9b237db3/</id>
    <link href="https://blog.zisheng.pro/posts/f6ae9b237db3/"/>
    <published>2023-04-27T03:02:56.000Z</published>
    <summary>本文讲述了如果用户在微信客户端中访问第三方网页，公众号可以通过微信网页授权机制，来获取用户基本信息，进而实现业务逻辑。</summary>
    <title>微信 oAuth 网页授权流程</title>
    <updated>2026-09-18T08:15:28.640Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="React Native" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/React-Native/"/>
    <category term="React Native" scheme="https://blog.zisheng.pro/tags/React-Native/"/>
    <category term="Android SDK" scheme="https://blog.zisheng.pro/tags/Android-SDK/"/>
    <content>
      <![CDATA[<p>在 React Native 开发中，如果一个原生 SDK 没有适配 React Native 的插件，原则上我们不推荐你使用。但是万不得已的情况下，我们有办法吗？答案是肯定的。步骤大致分为集成 SDK、编写桥接代码。知难行易，本文就是顺着这个思路来解决让前端工程师脑壳疼的集成第三方 SDK 并编写桥接代码的问题。</p><h2 id="预备知识"><a href="#预备知识" class="headerlink" title="预备知识"></a>预备知识</h2><p>在我们开发安卓项目的时候，不会所有的功能都自己去造轮子，经常要使用到各种的其他包，其中有谷歌给我们提供的各种 <code>support</code> 包，也有各种第三方的功能库，有时候我们自己也会将一些功能封装成包。这些包存在和导入的形式也多种多样，有远程仓库的，有直接拷贝到本地的，<code>jar</code> 包、<code>aar</code> 包、<code>so</code> 包等。所幸我们都可以在主工程和各个 <code>Module</code> 的 <code>build.gradle</code> 里进行统一管理。— <a href="http://t.cn/Ai9T02Jq">Android 依赖导入全攻略</a></p><h3 id="依赖引入方式"><a href="#依赖引入方式" class="headerlink" title="依赖引入方式"></a>依赖引入方式</h3><p><code>Android Gradle plugin 3.0</code> 几个引入依赖的方法：</p><p><strong>implementation</strong></p><p>对于使用了该命令编译的依赖，对该项目有依赖的项目将无法访问到使用该命令编译的依赖中的任何程序，也就是将该依赖隐藏在内部，而不对外部公开。<code>react-native link</code> 命令即使用该方式</p><p>使用 <code>implementation</code> 会使编译速度有所增快：比如我在一个 <code>library</code> 中使用 <code>implementation</code> 依赖了 <code>gson</code> 库，然后我的主项目依赖了 <code>library</code>，那么，我的主项目就无法访问 <code>gson</code> 库中的方法。这样的好处是编译速度会加快，我换了一个版本的 <code>Gson</code> 库，但只要 <code>library</code> 的代码不改动，就不会重新编译主项目的代码。</p><p><strong>api</strong></p><p>等同于 compile 指令</p><p><strong>compileOnly</strong></p><p>等同于 <code>provided</code>，只在编译时有效，不会参与打包，不会包含到 <code>apk</code> 文件中。可以用来解决重复导入库的冲突。</p><h2 id="远程仓库依赖"><a href="#远程仓库依赖" class="headerlink" title="远程仓库依赖"></a>远程仓库依赖</h2><blockquote><p>这里我们以 LeanCloud Android SDK 的引入来演示</p></blockquote><p>引入远程仓库依赖是很方便的，但在之前我们需要在项目根目录的 <code>build.gradle</code> 声明远程仓库的地址。</p><figure class="highlight diff"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line">buildscript &#123;</span><br><span class="line">    repositories &#123;</span><br><span class="line">        jcenter()</span><br><span class="line"><span class="addition">+        //这里是 LeanCloud 的包仓库</span></span><br><span class="line"><span class="addition">+        maven &#123;</span></span><br><span class="line"><span class="addition">+            url &quot;http://mvn.leancloud.cn/nexus/content/repositories/public&quot;</span></span><br><span class="line"><span class="addition">+        &#125;</span></span><br><span class="line"></span><br><span class="line">    &#125;</span><br><span class="line">    dependencies &#123;</span><br><span class="line">        classpath &#x27;com.android.tools.build:gradle:1.0.0&#x27;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">allprojects &#123;</span><br><span class="line">    repositories &#123;</span><br><span class="line">        jcenter()</span><br><span class="line"><span class="addition">+        //这里是 LeanCloud 的包仓库</span></span><br><span class="line"><span class="addition">+        maven &#123;</span></span><br><span class="line"><span class="addition">+            url &quot;http://mvn.leancloud.cn/nexus/content/repositories/public&quot;</span></span><br><span class="line"><span class="addition">+        &#125;</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>然后打开 <code>app</code> 目录下的 <code>build.gradle</code> 进行如下配置：</p><figure class="highlight diff"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line">android &#123;</span><br><span class="line"><span class="addition">+    //为了解决部分第三方库重复打包了META-INF的问题</span></span><br><span class="line"><span class="addition">+    packagingOptions&#123;</span></span><br><span class="line"><span class="addition">+        exclude &#x27;META-INF/LICENSE.txt&#x27;</span></span><br><span class="line"><span class="addition">+        exclude &#x27;META-INF/NOTICE.txt&#x27;</span></span><br><span class="line"><span class="addition">+    &#125;</span></span><br><span class="line">    lintOptions &#123;</span><br><span class="line">        abortOnError false</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">dependencies &#123;</span><br><span class="line">    compile (&#x27;com.android.support:support-v4:21.0.3&#x27;)</span><br><span class="line"></span><br><span class="line"><span class="addition">+    // LeanCloud 基础包</span></span><br><span class="line"><span class="addition">+    compile (&#x27;cn.leancloud.android:avoscloud-sdk:4.7.10&#x27;)</span></span><br><span class="line"><span class="addition">+</span></span><br><span class="line"><span class="addition">+    // 推送与即时通讯需要的包</span></span><br><span class="line"><span class="addition">+    compile (&#x27;cn.leancloud.android:avoscloud-push:4.7.10@aar&#x27;)&#123;transitive = true&#125;</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="本地依赖"><a href="#本地依赖" class="headerlink" title="本地依赖"></a>本地依赖</h2><blockquote><p>使用 Android Studio 的同学请参考: <a href="http://t.cn/Ai9TFsHH">Android Studio 引入 jar 包和 so 文件（armeabi 和 armeabi-v7a）</a></p></blockquote><h3 id="jar-包"><a href="#jar-包" class="headerlink" title="jar 包"></a>jar 包</h3><p>1、将 <code>jar</code> 文件复制、粘贴到 <code>app/libs</code> 目录中，React Native 默认没有该文件夹，清新建一个</p><p>2、打开 <code>app/build.gradle</code>，进行如下配置以列出包含 <code>jar</code> 包的文件夹路径。</p><blockquote><p>注意：React Native 默认已经进行了这个配置</p></blockquote><figure class="highlight diff"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">dependencies &#123;</span><br><span class="line">    implementation fileTree(dir: &quot;libs&quot;, include: [&quot;*.jar&quot;])</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>和远程仓库依赖引入方式不同，如果本地同时存在两个不同的 <code>jar</code> 包，或者本地已有 <code>jar</code> 包，再去远程依赖不同版本的 <code>jar</code> 包，就会报错。</p><p><img src="https://i.loli.net/2019/06/05/5cf73b7ea3e2b53276.jpg" alt="紫升"></p><p>解决方式：将其中的一个采用 <code>compileOnly</code> 替换 <code>implementation</code>。顾名思义，<code>compileOnly</code> 只在编译时起作用，不会包含到 <code>APK</code> 里面，在运行时也就避免找到重复的类了。</p><h3 id="aar-包"><a href="#aar-包" class="headerlink" title="aar 包"></a>aar 包</h3><p><code>arr</code> 全称是 <code>Andorid Archive</code>,是一个 Android 库项目的二进制归档文件，使用 Android Studio ，非常简单可以生成一个 AAR 文件。</p><p>和 <code>jar</code> 包不同，<code>aar</code> 包存放的路径声明和依赖引入是分开的：</p><p>1、将 <code>aar</code> 包复制到 <code>lib</code> 目录下</p><p>2、在项目根目录 <code>build.gradle</code>中声明 <code>aar</code> 文件存放路径</p><figure class="highlight diff"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">buildscript &#123;</span><br><span class="line">  repositories &#123;</span><br><span class="line"><span class="addition">+    flatDir &#123; // 引用本项目的libs下的aar</span></span><br><span class="line"><span class="addition">+      dir &quot;$rootDir/libs&quot;</span></span><br><span class="line"><span class="addition">+    &#125;</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line">allprojects &#123;</span><br><span class="line">  repositories &#123;</span><br><span class="line"><span class="addition">+    flatDir &#123; // 引用本项目的libs下的aar</span></span><br><span class="line"><span class="addition">+      dir &quot;$rootDir/libs&quot;</span></span><br><span class="line"><span class="addition">+    &#125;</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>3、在 <code>app/build.gradle</code> 中注入依赖</p><blockquote><p>注意：远程 <code>aar</code> 引入形式是：<code>implementation(&#39;com.sishu.android:watermelondb:0.7.0@aar&#39;)</code></p></blockquote><figure class="highlight diff"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">dependencies &#123;</span><br><span class="line"><span class="addition">+    implementation(name: &#x27;aar名字&#x27;, ext: &#x27;aar&#x27;)</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="so-文件"><a href="#so-文件" class="headerlink" title="so 文件"></a>so 文件</h3><p>直接在 <code>src-&gt;main</code> 下新建一个文件夹 <code>jniLib</code> ，然后再把 <code>so</code> 文件所在的那个文件夹 <code>armeabi</code> 复制过去。</p><blockquote><p>注：<code>jniLib</code> 是 so 文件默认的放置目录</p></blockquote><h2 id="坑"><a href="#坑" class="headerlink" title="坑"></a>坑</h2><h3 id="都是-Proguard-惹的祸"><a href="#都是-Proguard-惹的祸" class="headerlink" title="都是 Proguard 惹的祸"></a>都是 Proguard 惹的祸</h3><p>有时候明明导入了 <code>jar</code> 包，却仍然找不到 <code>jar</code> 包中的方法呢？八成是因为你开启了混淆，最安全的就是空间换安全。但是一个较真的程序员不能满足于此，我们还是要搞清楚 Proguard 惹了什么祸的。这里分享给大家一招：</p><p>打开 Android Studio 像原生开发工程师一样使用 Logcat 查看应用日志，比如你找到是 <code>com.huawei.**</code> 这个库找不到，那么进行如下配置：</p><figure class="highlight diff"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="addition">+ -dontwarn com.huawei.**</span></span><br><span class="line"><span class="addition">+ -keep class com.huawei.**&#123;*;&#125;</span></span><br></pre></td></tr></table></figure><ul><li><code>-dontwarn</code> 表示让 ProGuard 不要警告找不到 <code>com.huawei.**</code> 这个包里面的类的相关引用</li><li><code>-keep class</code> 表示保持 <code>com.huawei.**</code> 这个包里面的所有类和所有方法不被混淆。再次编译打包，发现 apk 大小要明显大于之前的包。运行 app，问题解决！</li></ul><h3 id="aar-包中的资源文件重复了"><a href="#aar-包中的资源文件重复了" class="headerlink" title="aar 包中的资源文件重复了"></a>aar 包中的资源文件重复了</h3><p>资源文件重复了，主工程的资源文件会直接覆盖 <code>aar</code> 包中的文件，并且不会有任何报错或者提示，最终 <code>aar</code> 包中也会直接用主工程的资源文件，所以需要注意命名方式。暂时没有更好的解决方法。</p><h3 id="AndroidManifest-合并错误"><a href="#AndroidManifest-合并错误" class="headerlink" title="AndroidManifest 合并错误"></a>AndroidManifest 合并错误</h3><p>同样也是发生在 <code>aar</code> 包上， Android Studio 项目每个 module 中都可以有一个 <code>AndroidManifest.xml</code> 文件，但最终的 APK 文件只能包含一个 <code>AndroidManifest.xml</code> 文件。在构建应用时，Gradle 构建会将所有清单文件合并到一个封装到 APK 的清单文件中。aar 包的清单文件和我们的 app 清单文件属性冲突时：用 <code>tools:replace=&quot;属性名&quot;</code> 解决。</p><h3 id="annotationProcessor-与-compileOnly-的区别"><a href="#annotationProcessor-与-compileOnly-的区别" class="headerlink" title="annotationProcessor 与 compileOnly 的区别"></a>annotationProcessor 与 compileOnly 的区别</h3><p>上文说了 <code>annotationProcessor</code> 与 <code>compileOnly</code> 都是只编译并不打入 apk 中，他俩到底有什么区别呢？扮演的角色不一样，<code>annotationProcessor</code> 作用是编译时生成代码，编译完真的就不需要了，<code>compileOnly</code> 是有重复的库，为的是剃除只保留一个库，最终还是需要的。</p><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ul><li><a href="http://t.cn/Ai9HmlWb">Android Studio 引入 jar 包和 so 库</a></li><li><a href="http://t.cn/Ai9T02Jq">Android 依赖导入全攻略</a></li><li><a href="http://t.cn/Ai98bNbj">React-native 使用原生(ios, android)第三方 sdk</a></li></ul>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/ec072d4935bc/</id>
    <link href="https://blog.zisheng.pro/posts/ec072d4935bc/"/>
    <published>2023-04-21T10:40:00.000Z</published>
    <summary>在 React Native 开发中，如果一个原生 SDK 没有适配 React Native 的插件，原则上我们不推荐你使用。但是万不得已的情况下，我们有办法吗？答案是肯定的。步骤大致分为集成 SDK、编写桥接代码。知难行易，本文就是顺着这个思路来解决让前端工程师脑壳疼的集成第三方 SDK 并编写桥接代码的问题。</summary>
    <title>React Native 引入第三方 Android SDK</title>
    <updated>2026-09-18T08:15:28.632Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="React" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/React/"/>
    <category term="面试题" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/%E9%9D%A2%E8%AF%95%E9%A2%98/"/>
    <category term="前端" scheme="https://blog.zisheng.pro/tags/%E5%89%8D%E7%AB%AF/"/>
    <category term="React" scheme="https://blog.zisheng.pro/tags/React/"/>
    <category term="VDOM" scheme="https://blog.zisheng.pro/tags/VDOM/"/>
    <category term="Virtual DOM" scheme="https://blog.zisheng.pro/tags/Virtual-DOM/"/>
    <category term="面试题" scheme="https://blog.zisheng.pro/tags/%E9%9D%A2%E8%AF%95%E9%A2%98/"/>
    <category term="虚拟 DOM" scheme="https://blog.zisheng.pro/tags/%E8%99%9A%E6%8B%9F-DOM/"/>
    <content>
      <![CDATA[<p>在 Web 前端开发中，React 是一种非常流行的 JavaScript 库，它的 Virtual DOM（虚拟 DOM）技术被广泛地应用于构建高性能、可维护的 Web 应用程序。那么，React VDOM 技术到底是什么？它又是如何工作的呢？</p><h2 id="VDOM-是什么？"><a href="#VDOM-是什么？" class="headerlink" title="VDOM 是什么？"></a>VDOM 是什么？</h2><p>VDOM（Virtual DOM）是 React 中的一个重要概念，它是一个虚拟的、内存中的 DOM 树。与真实的 DOM 树不同，VDOM 只存在于 JavaScript 内存中，它是 React 组件的一部分，并且可以随时更新和修改。</p><h2 id="React-VDOM-技术的优势"><a href="#React-VDOM-技术的优势" class="headerlink" title="React VDOM 技术的优势"></a>React VDOM 技术的优势</h2><p>React VDOM 技术主要有以下几个优势：</p><ol><li>提高 Web 应用程序的性能：由于真实的 DOM 树非常庞大，而且每次更新都需要重新计算布局和重新渲染，因此更新频繁的 Web 应用程序很容易出现性能问题。而 VDOM 只是一个轻量级的 JavaScript 对象，它可以快速地进行比较和更新，从而大大提高了 Web 应用程序的性能。</li><li>提高 Web 应用程序的可维护性：由于 VDOM 只存在于 JavaScript 内存中，因此开发人员可以更方便地对其进行操作和管理，从而提高了 Web 应用程序的可维护性。</li><li>提高 Web 应用程序的开发效率：由于 VDOM 可以快速进行比较和更新，因此开发人员可以更快地进行开发和测试，从而提高了 Web 应用程序的开发效率。</li></ol><h2 id="React-VDOM-技术的工作原理"><a href="#React-VDOM-技术的工作原理" class="headerlink" title="React VDOM 技术的工作原理"></a>React VDOM 技术的工作原理</h2><p>React VDOM 技术的工作原理主要包括以下几个步骤：</p><ol><li>首先，React 会将组件的 JSX 代码转换为 VDOM 对象，并将其存储在内存中。</li><li>当组件的状态发生变化时，React 会重新生成一个新的 VDOM 对象，并将其与之前的 VDOM 对象进行比较。</li><li>在比较过程中，React 会遍历新旧 VDOM 对象，找出它们之间的差异，并将这些差异记录在一个“差异对象”中。</li><li>最后，React 会根据“差异对象”中的记录，更新真实的 DOM 树，从而实现组件的重新渲染。</li></ol><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>React VDOM 技术是 React 中的一个重要概念，它可以提高 Web 应用程序的性能、可维护性和开发效率。虽然 VDOM 技术看起来比较复杂，但是它背后的原理非常简单，只需要理解其基本的工作原理，就可以快速上手使用。</p>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/7c184cbc698e/</id>
    <link href="https://blog.zisheng.pro/posts/7c184cbc698e/"/>
    <published>2023-04-20T09:55:04.000Z</published>
    <summary>在 Web 前端开发中，React 是一种非常流行的 JavaScript 库，它的 Virtual DOM（虚拟 DOM）技术被广泛地应用于构建高性能、可维护的 Web 应用程序。那么，React VDOM 技术到底是什么？它又是如何工作的呢？</summary>
    <title>React VDOM 原理</title>
    <updated>2026-09-18T08:15:28.632Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="Deno" scheme="https://blog.zisheng.pro/categories/Deno/"/>
    <category term="GitHub Action" scheme="https://blog.zisheng.pro/tags/GitHub-Action/"/>
    <category term="CI/CD" scheme="https://blog.zisheng.pro/tags/CI-CD/"/>
    <category term="Travis CI" scheme="https://blog.zisheng.pro/tags/Travis-CI/"/>
    <category term="Circle CI" scheme="https://blog.zisheng.pro/tags/Circle-CI/"/>
    <content>
      <![CDATA[<p>GitHub Action 是 GitHub 官方的 CI&#x2F;CD 工具，相较于 Travis CI 和 Circle CI，更轻量和易于扩展，<a href="https://github.com/marketplace?type=actions">marketplace</a> 中有大量社区贡献的插件。各大开源项目都纷纷转向使用 GitHub Action 作为持续集成的工具，比如本文的主角 Deno。</p><p>GitHub 的文档中有很多概念写的十分晦涩，有些翻译很僵硬影响理解。截止发稿时，Deno 的 <a href="https://github.com/youngjuning/deno/blob/master/.github/workflows/ci.yml">ci.yml</a> 文件有 323 行，是一个很好的学习范本。想要对 GitHub Action 有个了解或对 Deno 的持续集成部分感兴趣的同学都可以一起来探究下。</p><h2 id="Action-结构"><a href="#Action-结构" class="headerlink" title="Action 结构"></a>Action 结构</h2><blockquote><p>注意：GitHub Action 的 ci 脚本需要放在 <code>.github/workflows</code> 文件夹下</p></blockquote><figure class="highlight yml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">name:</span> <span class="string">ci</span></span><br><span class="line"></span><br><span class="line"><span class="attr">on:</span> [<span class="string">push</span>, <span class="string">pull_request</span>]</span><br><span class="line"></span><br><span class="line"><span class="attr">jobs:</span></span><br></pre></td></tr></table></figure><ul><li><code>name</code>：工作流程的名称。GitHub 在仓库的操作页面上显示工作流程的名称。如果省略 name，GitHub 将其设置为相对于仓库根目录的工作流程文件路径。</li><li><code>on</code>：必填。是<a href="https://docs.github.com/cn/articles/events-that-trigger-workflows">触发工作流程的事件</a>。Deno 中 的 <code>[push,pull_request]</code> 代表有主分支有提交代码或 pull_request 时触发 CI 流程。</li><li><code>jobs</code>：工作流程运行包括一项或多项作业。作业默认是并行运行。Deno 中只有一个 job —— <code>build</code></li></ul><h2 id="build-job"><a href="#build-job" class="headerlink" title="build job"></a>build job</h2><figure class="highlight yml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">jobs:</span></span><br><span class="line">  <span class="attr">build:</span></span><br><span class="line">    <span class="attr">name:</span> <span class="string">$&#123;&#123;</span> <span class="string">matrix.kind</span> <span class="string">&#125;&#125;</span> <span class="string">$&#123;&#123;</span> <span class="string">matrix.os</span> <span class="string">&#125;&#125;</span></span><br><span class="line">    <span class="attr">if:</span> <span class="string">|</span></span><br><span class="line"><span class="string">      github.event_name == &#x27;push&#x27; ||</span></span><br><span class="line"><span class="string">      !startsWith(github.event.pull_request.head.label, &#x27;denoland:&#x27;)</span></span><br><span class="line"><span class="string"></span>    <span class="attr">runs-on:</span> <span class="string">$&#123;&#123;</span> <span class="string">matrix.os</span> <span class="string">&#125;&#125;</span></span><br><span class="line">    <span class="attr">timeout-minutes:</span> <span class="number">60</span></span><br><span class="line">    <span class="attr">strategy:</span></span><br><span class="line">      <span class="comment"># ...</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">steps:</span></span><br><span class="line">      <span class="comment"># ...</span></span><br></pre></td></tr></table></figure><ul><li>&#96;name：作业显示在 GitHub 上的名称。<ul><li><code>matrix</code>：用于访问为当前作业配置的矩阵参数。例如，Deno 使用 kind 和 os 版本配置矩阵构建，matrix 上下文对象将包含当前作业的 kind 和 os 版本。</li></ul></li><li><code>if</code>：您可以使用 if 条件阻止作业在条件得到满足之前运行。Deno 中判断 <code>github.event_name</code> 等于 <code>&quot;push&quot;</code> 或 <code>label</code> 以 <code>&quot;denoland:&quot;</code> 开头的 <code>pull_request</code> 事件</li><li><code>runs-on</code>：<strong>必填</strong>。 任务运行的机器。机器可以是 GitHub 托管的运行器或自托管的运行器。定义操作系统矩阵时，必须将 <code>runs-on</code> 的值设置为您定义的 <code>matrix.os</code> 上下文属性。Deno 是跨平台的，因此创建了矩阵以在多个运行器操作系统上运行构建工作流程。</li><li><code>timeout-minutes</code>: 在 GitHub 自动取消运行之前可让作业运行的最大分钟数。 默认值：360。Deno 中设置为 60 来尽早暴露问题。</li><li><code>strategy</code>：策略，用于创建作业的构建矩阵。您可以定义要在其中运行每项作业的不同变种。这可能不太好理解，在 Deno 中，其实就是为了同时构建出多个平台的产物。后面的章节我们单独解析 Deno 中具体的应用。</li><li><code>env</code>：环境变量的 <code>map</code> 可用于作业中的所有步骤。 您也可以设置整个工作流程或单个步骤的环境变量。后面章节我们单独解析 Deno 中具体的应用。</li><li><code>steps</code>：作业包含一系列任务，称为 steps。步骤可以运行命令、运行设置任务，或者运行您的仓库、公共仓库中的操作或 Docker 注册表中发布的操作。后面章节我们单独解析 Deno 中具体的应用。</li></ul><h2 id="strategy-策略"><a href="#strategy-策略" class="headerlink" title="strategy 策略"></a>strategy 策略</h2><figure class="highlight yml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">strategy:</span></span><br><span class="line">  <span class="attr">matrix:</span></span><br><span class="line">    <span class="attr">include:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">os:</span> <span class="string">macos-10.15</span></span><br><span class="line">        <span class="attr">kind:</span> <span class="string">test_release</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">os:</span> <span class="string">windows-2019</span></span><br><span class="line">        <span class="attr">kind:</span> <span class="string">test_release</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">os:</span> <span class="string">$&#123;&#123;</span> <span class="string">github.repository</span> <span class="string">==</span> <span class="string">&#x27;denoland/deno&#x27;</span> <span class="string">&amp;&amp;</span> <span class="string">&#x27;ubuntu-latest&#x27;</span> <span class="string">||</span> <span class="string">&#x27;ubuntu-18.04&#x27;</span> <span class="string">&#125;&#125;</span></span><br><span class="line">        <span class="attr">kind:</span> <span class="string">test_release</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">os:</span> <span class="string">$&#123;&#123;</span> <span class="string">github.repository</span> <span class="string">==</span> <span class="string">&#x27;denoland/deno&#x27;</span> <span class="string">&amp;&amp;</span> <span class="string">&#x27;ubuntu-latest&#x27;</span> <span class="string">||</span> <span class="string">&#x27;ubuntu-18.04&#x27;</span> <span class="string">&#125;&#125;</span></span><br><span class="line">        <span class="attr">kind:</span> <span class="string">test_debug</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">os:</span> <span class="string">$&#123;&#123;</span> <span class="string">github.repository</span> <span class="string">==</span> <span class="string">&#x27;denoland/deno&#x27;</span> <span class="string">&amp;&amp;</span> <span class="string">&#x27;ubuntu-latest&#x27;</span> <span class="string">||</span> <span class="string">&#x27;ubuntu-18.04&#x27;</span> <span class="string">&#125;&#125;</span></span><br><span class="line">        <span class="attr">kind:</span> <span class="string">bench</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">os:</span> <span class="string">$&#123;&#123;</span> <span class="string">github.repository</span> <span class="string">==</span> <span class="string">&#x27;denoland/deno&#x27;</span> <span class="string">&amp;&amp;</span> <span class="string">&#x27;ubuntu-latest&#x27;</span> <span class="string">||</span> <span class="string">&#x27;ubuntu-18.04&#x27;</span> <span class="string">&#125;&#125;</span></span><br><span class="line">        <span class="attr">kind:</span> <span class="string">lint</span></span><br><span class="line"></span><br><span class="line">  <span class="comment"># Always run master branch builds to completion. This allows the cache to</span></span><br><span class="line">  <span class="comment"># stay mostly up-to-date in situations where a single job fails due to</span></span><br><span class="line">  <span class="comment"># e.g. a flaky test.</span></span><br><span class="line">  <span class="comment"># Don&#x27;t fast-fail on tag build because publishing binaries shouldn&#x27;t be</span></span><br><span class="line">  <span class="comment"># prevented if &#x27;cargo publish&#x27; fails (which can be a false negative).</span></span><br><span class="line">  <span class="attr">fail-fast:</span> <span class="string">$&#123;&#123;</span> <span class="string">github.event_name</span> <span class="string">==</span> <span class="string">&#x27;pull_request&#x27;</span> <span class="string">||</span> <span class="string">(github.ref</span> <span class="type">!=</span></span><br><span class="line">    <span class="string">&#x27;refs/heads/master&#x27;</span> <span class="string">&amp;&amp;</span> <span class="type">!startsWith(github.ref,</span> <span class="string">&#x27;refs/tags/&#x27;</span><span class="string">))</span> <span class="string">&#125;&#125;</span></span><br></pre></td></tr></table></figure><ul><li><code>strategy</code>：策略会为您的工作创建构建矩阵。您可以定义不同的变体来运行每个作业。这可能不太好理解，在 Deno 中，其实就是为了同时构建出多个平台的产物。<ul><li><code>matrix</code>：您可以定义不同作业配置的矩阵。Deno 中定义了 6 个矩阵。它们分别用来执行测试、调试、压测、lint 等工作<ul><li><code>include</code>：使用 <code>matrix</code> 只能定义使用某个 os，没办法添加其他配置。用 <code>include</code> 可以解决这个问题</li></ul></li><li><code>fail-fast</code>：设置为 <code>true</code> 时，如果任何 <code>matrix</code> 作业失败，GitHub 将取消所有进行中的作业。 默认值：<code>true</code>。ry 在这个 <a href="https://github.com/denoland/deno/commit/46d5843f753548415c87f3c8a868bba49c203b92#diff-b803fcb7f17ed9235f1e5cb1fcd2f5d3b2838429d4368ae4c57ce4436577f03f">commit</a> 中删除了 <code>cargo publish</code>，从注释中看来这里的 <code>fail-fast</code> 配置已经是冗余的了，我顺手就是一个 fork、commit、<a href="https://github.com/denoland/deno/pull/9449">pr</a> 三连。</li></ul></li></ul><p><strong>插曲</strong></p><p><code>$&#123;&#123; github.repository == &#39;denoland/deno&#39; &amp;&amp; &#39;ubuntu-latest&#39; || &#39;ubuntu-18.04&#39; &#125;&#125;</code> 这种写法我看到时很疑惑，直接取 ‘ubuntu-latest’ 不行吗？如果不行是为什么呢？抱着求真的态度，我翻阅到是在这个 <a href="https://github.com/denoland/deno/commit/1a27c19c583fa6bd1eaaec93b513bfbac37fc53c#diff-b803fcb7f17ed9235f1e5cb1fcd2f5d3b2838429d4368ae4c57ce4436577f03f">commit</a> 加入的这个判断，在一番询问后得到了下面的答案：</p><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/bf73fe62dcbc48f69272f97a1a139444~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><p>在 justjavac 大佬那里也得到了印证：</p><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/04d3d4f533e74d0795d70e5cffd69699~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><p>然后我还有个疑问是为什么不使用三元表达式，是 YAML 语法不支持吗？</p><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b76f02c91de841d587084fbc73a2cca3~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><h2 id="env-环境变量"><a href="#env-环境变量" class="headerlink" title="env 环境变量"></a>env 环境变量</h2><figure class="highlight yml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">env:</span></span><br><span class="line">  <span class="attr">CARGO_INCREMENTAL:</span> <span class="number">0</span></span><br><span class="line">  <span class="attr">RUST_BACKTRACE:</span> <span class="string">full</span></span><br><span class="line">  <span class="attr">CARGO_TERM_COLOR:</span> <span class="string">always</span></span><br></pre></td></tr></table></figure><ul><li><code>env</code>：环境变量的 <code>map</code> 可用于作业中的所有步骤。您也可以设置整个工作流程或单个步骤的环境变量。代码中的三个变量是整个流程的环境变量。<ul><li><code>CARGO_INCREMENTAL</code>、<code>CARGO_TERM_COLOR</code>、<code>RUST_BACKTRACE</code> 是配置给 Cargo 用的，具体的参考<a href="https://doc.rust-lang.org/cargo/reference/environment-variables.html#environment-variables-cargo-reads">environment-variables-cargo-reads</a>，这里不展开解读了。</li></ul></li></ul><h2 id="steps-步骤"><a href="#steps-步骤" class="headerlink" title="steps 步骤"></a>steps 步骤</h2><p>Deno 的 CI 中截止写下这段话时有 35 个步骤，相当考验我继续解读的勇气。。。</p><blockquote><p>作业包含一系列任务，称为 <code>steps</code>。步骤可以运行命令、运行设置任务，或者运行您的仓库、公共仓库中的操作或 Docker 注册表中发布的操作。并非所有步骤都会运行操作，但所有操作都会作为步骤运行。每个步骤在运行器环境中以其自己的进程运行，且可以访问工作区和文件系统。因为步骤以自己的进程运行，所以步骤之间不会保留环境变量的更改。 GitHub 提供内置的步骤来设置和完成作业。—— 文档</p></blockquote><h3 id="1、Configure-git"><a href="#1、Configure-git" class="headerlink" title="1、Configure git"></a>1、Configure git</h3><figure class="highlight yml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Configure</span> <span class="string">git</span></span><br><span class="line">  <span class="attr">run:</span> <span class="string">git</span> <span class="string">config</span> <span class="string">--global</span> <span class="string">core.symlinks</span> <span class="literal">true</span></span><br></pre></td></tr></table></figure><ul><li><code>git config --global core.symlinks true</code>：克隆一个包含子模块和符号链接的存储库。这里是为了兼容 windows，详细的解释请查看 <a href="https://github.community/t/git-bash-symbolic-links-on-windows/522">git bash symbolic links on windows</a></li></ul><h3 id="2、Clone-repository"><a href="#2、Clone-repository" class="headerlink" title="2、Clone repository"></a>2、Clone repository</h3><figure class="highlight yml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Clone</span> <span class="string">repository</span></span><br><span class="line">  <span class="attr">uses:</span> <span class="string">actions/checkout@v2</span></span><br><span class="line">  <span class="attr">with:</span></span><br><span class="line">    <span class="comment"># 使用 `depth&gt; 1`，因为有时我们需要重新构建 master 分支，如果 checkout 太浅，其他 commits push 之后将不可能重新构建。</span></span><br><span class="line">    <span class="attr">fetch-depth:</span> <span class="number">5</span></span><br><span class="line">    <span class="attr">submodules:</span> <span class="literal">true</span></span><br></pre></td></tr></table></figure><ul><li><code>actions/checkout@v2</code>：checkout action 很常用<ul><li><code>fetch-depth: 5</code>：默认情况下，仅提取一次提交。这里设置的 5，就会提取最近的 5 次提交。</li><li><code>submodules: true</code>：构建需要把子模块也获取到，Deno 中包含了子模块。</li></ul></li></ul><h3 id="3、Create-source-tarballs-release-linux"><a href="#3、Create-source-tarballs-release-linux" class="headerlink" title="3、Create source tarballs (release, linux)"></a>3、Create source tarballs (release, linux)</h3><blockquote><p>创建源文件压缩包 (release, linux)</p></blockquote><figure class="highlight yml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Create</span> <span class="string">source</span> <span class="string">tarballs</span> <span class="string">(release,</span> <span class="string">linux)</span></span><br><span class="line">  <span class="attr">if:</span> <span class="string">|</span></span><br><span class="line"><span class="string">    startsWith(matrix.os, &#x27;ubuntu&#x27;) &amp;&amp;</span></span><br><span class="line"><span class="string">    matrix.kind == &#x27;test_release&#x27; &amp;&amp;</span></span><br><span class="line"><span class="string">    github.repository == &#x27;denoland/deno&#x27; &amp;&amp;</span></span><br><span class="line"><span class="string">    startsWith(github.ref, &#x27;refs/tags/&#x27;)</span></span><br><span class="line"><span class="string"></span>  <span class="attr">run:</span> <span class="string">|</span></span><br><span class="line"><span class="string">    mkdir -p target/release</span></span><br><span class="line"><span class="string">    tar --exclude=.cargo_home --exclude=&quot;.git*&quot; --exclude=target --exclude=third_party/prebuilt -czvf target/release/deno_src.tar.gz -C .. deno</span></span><br></pre></td></tr></table></figure><ul><li>执行条件：构建矩阵 os 是 <code>&#39;ubuntu&#39;</code>、kind 是 <code>&#39;test_release&#39;</code>、仓库是 <code>&#39;denoland/deno&#39;</code>、本地执行 <code>git push --tags</code> 或 <code>git push origin v0.0.1</code></li><li><code>run</code>：其实发 release 的时候会自动打包源文件，这里之所以多打一个，是因为要忽略很多文件夹，打出纯 deno 的源文件</li></ul><h3 id="4、Install-rust"><a href="#4、Install-rust" class="headerlink" title="4、Install rust"></a>4、Install rust</h3><blockquote><p>安装 rust</p></blockquote><figure class="highlight yml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Install</span> <span class="string">rust</span></span><br><span class="line">  <span class="attr">uses:</span> <span class="string">hecrj/setup-rust-action@v1</span></span><br><span class="line">  <span class="attr">with:</span></span><br><span class="line">    <span class="attr">rust-version:</span> <span class="number">1.49</span><span class="number">.0</span></span><br></pre></td></tr></table></figure><ul><li>Deno 源码是 RUST 编写的，所以是用 <code>hecrj/setup-rust-action@v1</code>，每个语言都有自己的 <code>setup</code>，商店里能搜到 <a href="https://github.com/marketplace?type=actions&query=setup">358</a> 条记录。</li></ul><h3 id="5、Install-clippy-and-rustfmt"><a href="#5、Install-clippy-and-rustfmt" class="headerlink" title="5、Install clippy and rustfmt"></a>5、Install clippy and rustfmt</h3><figure class="highlight yml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Install</span> <span class="string">clippy</span> <span class="string">and</span> <span class="string">rustfmt</span></span><br><span class="line">  <span class="attr">if:</span> <span class="string">matrix.kind</span> <span class="string">==</span> <span class="string">&#x27;lint&#x27;</span></span><br><span class="line">  <span class="attr">run:</span> <span class="string">|</span></span><br><span class="line"><span class="string">    rustup component add clippy</span></span><br><span class="line"><span class="string">    rustup component add rustfmt</span></span><br></pre></td></tr></table></figure><h3 id="6、Install-Deno（非-Windows）"><a href="#6、Install-Deno（非-Windows）" class="headerlink" title="6、Install Deno（非 Windows）"></a>6、Install Deno（非 Windows）</h3><figure class="highlight yml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Install</span> <span class="string">Deno</span></span><br><span class="line">  <span class="attr">if:</span> <span class="string">|</span></span><br><span class="line"><span class="string">    !startsWith(matrix.os, &#x27;windows&#x27;)</span></span><br><span class="line"><span class="string"></span>  <span class="attr">run:</span> <span class="string">|-</span></span><br><span class="line"><span class="string">    curl -fsSL https://deno.land/x/install/install.sh | sh -s v1.5.1</span></span><br><span class="line"><span class="string">    echo &quot;$HOME/.deno/bin&quot; &gt;&gt; $GITHUB_PATH</span></span><br></pre></td></tr></table></figure><ul><li>执行条件：构建矩阵 os 不是 windows</li><li><code>run</code><ul><li><code>|-</code> 和 <code>|</code>是 yaml 的语法。这样，您就可以有效地声明多行 yaml 字符串。语法详情查看<a href="https://yaml-multiline.info/">这里</a></li><li><code>curl -fsSL https://deno.land/x/install/install.sh | sh -s v1.5.1</code> 安装 deno（这里应该可以 pr）</li><li><code>echo &quot;$HOME/.deno/bin&quot; &gt;&gt; $GITHUB_PATH</code>：设置环境变量，设置的指向文件系统上某个位置的任何新环境变量都应该有 <code>_PATH</code> 后缀。</li></ul></li></ul><h3 id="7、Install-Deno-Windows"><a href="#7、Install-Deno-Windows" class="headerlink" title="7、Install Deno (Windows)"></a>7、Install Deno (Windows)</h3><figure class="highlight yml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Install</span> <span class="string">Deno</span> <span class="string">(Windows)</span></span><br><span class="line">  <span class="attr">if:</span> <span class="string">startsWith(matrix.os,</span> <span class="string">&#x27;windows&#x27;</span><span class="string">)</span></span><br><span class="line">  <span class="attr">run:</span> <span class="string">|-</span></span><br><span class="line"><span class="string">    curl -fsSL https://deno.land/x/install/install.sh | sh -s v1.5.1</span></span><br><span class="line"><span class="string">    echo &quot;$HOME/.deno/bin&quot; &gt;&gt; $env:GITHUB_PATH</span></span><br></pre></td></tr></table></figure><ul><li>执行条件：构建矩阵 os 是 windows</li><li><code>run</code>：和上文一样</li></ul><h3 id="8、Install-Python"><a href="#8、Install-Python" class="headerlink" title="8、Install Python"></a>8、Install Python</h3><blockquote><p>安装 python</p></blockquote><figure class="highlight yml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Install</span> <span class="string">Python</span></span><br><span class="line">  <span class="attr">uses:</span> <span class="string">actions/setup-python@v1</span></span><br><span class="line">  <span class="attr">with:</span></span><br><span class="line">    <span class="attr">python-version:</span> <span class="string">&#x27;3.8&#x27;</span></span><br><span class="line">    <span class="attr">architecture:</span> <span class="string">x64</span></span><br></pre></td></tr></table></figure><h3 id="9、Install-Node"><a href="#9、Install-Node" class="headerlink" title="9、Install Node"></a>9、Install Node</h3><blockquote><p>安装 Node</p></blockquote><figure class="highlight yml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Install</span> <span class="string">Node</span></span><br><span class="line">  <span class="attr">uses:</span> <span class="string">actions/setup-node@v2</span></span><br><span class="line">  <span class="attr">with:</span></span><br><span class="line">    <span class="attr">node-version:</span> <span class="string">&#x27;14&#x27;</span></span><br><span class="line">    <span class="attr">check-latest:</span> <span class="literal">true</span></span><br></pre></td></tr></table></figure><h3 id="10、Remove-unused-versions-of-Python"><a href="#10、Remove-unused-versions-of-Python" class="headerlink" title="10、Remove unused versions of Python"></a>10、Remove unused versions of Python</h3><blockquote><p>删除没有用到的 Python 版本</p></blockquote><figure class="highlight yml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Remove</span> <span class="string">unused</span> <span class="string">versions</span> <span class="string">of</span> <span class="string">Python</span></span><br><span class="line">  <span class="attr">if:</span> <span class="string">startsWith(matrix.os,</span> <span class="string">&#x27;windows&#x27;</span><span class="string">)</span></span><br><span class="line">  <span class="attr">run:</span> <span class="string">|-</span></span><br><span class="line"><span class="string">    $env:PATH -split &quot;;&quot; |</span></span><br><span class="line"><span class="string">      Where-Object &#123; Test-Path &quot;$_\python.exe&quot; &#125; |</span></span><br><span class="line"><span class="string">      Select-Object -Skip 1 |</span></span><br><span class="line"><span class="string">      ForEach-Object &#123; Move-Item &quot;$_&quot; &quot;$_.disabled&quot; &#125;</span></span><br></pre></td></tr></table></figure><h3 id="11、Setup-gcloud-unix"><a href="#11、Setup-gcloud-unix" class="headerlink" title="11、Setup gcloud (unix)"></a>11、Setup gcloud (unix)</h3><blockquote><p>安装 Google Cloud SDK（unix）</p></blockquote><figure class="highlight yml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Setup</span> <span class="string">gcloud</span> <span class="string">(unix)</span></span><br><span class="line">  <span class="attr">if:</span> <span class="string">|</span></span><br><span class="line"><span class="string">    runner.os != &#x27;Windows&#x27; &amp;&amp;</span></span><br><span class="line"><span class="string">    matrix.kind == &#x27;test_release&#x27; &amp;&amp;</span></span><br><span class="line"><span class="string">    github.repository == &#x27;denoland/deno&#x27; &amp;&amp;</span></span><br><span class="line"><span class="string">    (github.ref == &#x27;refs/heads/master&#x27; ||</span></span><br><span class="line"><span class="string">    startsWith(github.ref, &#x27;refs/tags/&#x27;))</span></span><br><span class="line"><span class="string"></span>  <span class="attr">uses:</span> <span class="string">google-github-actions/setup-gcloud@master</span></span><br><span class="line">  <span class="attr">with:</span></span><br><span class="line">    <span class="attr">project_id:</span> <span class="string">denoland</span></span><br><span class="line">    <span class="attr">service_account_key:</span> <span class="string">$&#123;&#123;</span> <span class="string">secrets.GCP_SA_KEY</span> <span class="string">&#125;&#125;</span></span><br><span class="line">    <span class="attr">export_default_credentials:</span> <span class="literal">true</span></span><br></pre></td></tr></table></figure><ul><li>执行条件：运行系统不是 ‘Windows’、构建矩阵 kind 是 <code>&#39;test_release&#39;</code>、仓库是 <code>&#39;denoland/deno&#39;</code>、分支是 master 或是 tag 提交</li><li><code>google-github-actions/setup-gcloud</code>：与 Google Cloud Platform 交互的 GitHub Actions 的集合。</li></ul><h3 id="12、Setup-gcloud-windows"><a href="#12、Setup-gcloud-windows" class="headerlink" title="12、Setup gcloud (windows)"></a>12、Setup gcloud (windows)</h3><blockquote><p>同 11 作用一样，不过是为 windows 设置 gcloud</p></blockquote><figure class="highlight yml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Setup</span> <span class="string">gcloud</span> <span class="string">(windows)</span></span><br><span class="line">  <span class="attr">if:</span> <span class="string">|</span></span><br><span class="line"><span class="string">    runner.os == &#x27;Windows&#x27; &amp;&amp;</span></span><br><span class="line"><span class="string">    matrix.kind == &#x27;test_release&#x27; &amp;&amp;</span></span><br><span class="line"><span class="string">    github.repository == &#x27;denoland/deno&#x27; &amp;&amp;</span></span><br><span class="line"><span class="string">    (github.ref == &#x27;refs/heads/master&#x27; ||</span></span><br><span class="line"><span class="string">    startsWith(github.ref, &#x27;refs/tags/&#x27;))</span></span><br><span class="line"><span class="string"></span>  <span class="attr">uses:</span> <span class="string">google-github-actions/setup-gcloud@master</span></span><br><span class="line">  <span class="attr">env:</span></span><br><span class="line">    <span class="attr">CLOUDSDK_PYTHON:</span> <span class="string">$&#123;&#123;env.pythonLocation&#125;&#125;\python.exe</span></span><br><span class="line">  <span class="attr">with:</span></span><br><span class="line">    <span class="attr">project_id:</span> <span class="string">denoland</span></span><br><span class="line">    <span class="attr">service_account_key:</span> <span class="string">$&#123;&#123;</span> <span class="string">secrets.GCP_SA_KEY</span> <span class="string">&#125;&#125;</span></span><br><span class="line">    <span class="attr">export_default_credentials:</span> <span class="literal">true</span></span><br></pre></td></tr></table></figure><h3 id="13、Configure-canary-build"><a href="#13、Configure-canary-build" class="headerlink" title="13、Configure canary build"></a>13、Configure canary build</h3><blockquote><p>配置 canary 渠道的 build（Deno 发布氛围 canary 和 release 渠道）</p></blockquote><figure class="highlight yml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Configure</span> <span class="string">canary</span> <span class="string">build</span></span><br><span class="line">  <span class="attr">if:</span> <span class="string">|</span></span><br><span class="line"><span class="string">    matrix.kind == &#x27;test_release&#x27; &amp;&amp;</span></span><br><span class="line"><span class="string">    github.repository == &#x27;denoland/deno&#x27; &amp;&amp;</span></span><br><span class="line"><span class="string">    github.ref == &#x27;refs/heads/master&#x27;</span></span><br><span class="line"><span class="string"></span>  <span class="attr">shell:</span> <span class="string">bash</span></span><br><span class="line">  <span class="attr">run:</span> <span class="string">|</span></span><br><span class="line">    <span class="string">echo</span> <span class="string">&quot;DENO_CANARY=true&quot;</span> <span class="string">&gt;&gt;</span> <span class="string">$GITHUB_ENV</span></span><br></pre></td></tr></table></figure><ul><li>执行条件：构建矩阵 <code>kind</code> 为 <code>&#39;test_release&#39;</code>、github repo 是 <code>&#39;denoland/deno&#39;</code>、master 分支</li><li><code>run</code> 将 <code>&quot;DENO_CANARY=true&quot;</code> 设置进环境变量。</li></ul><blockquote><p>您也可以使用 <code>GITHUB_ENV</code> environment file 设置工作流程中的以下步骤可以使用的环境变量。 —— 文档</p></blockquote><h3 id="14、Log-versions"><a href="#14、Log-versions" class="headerlink" title="14、Log versions"></a>14、Log versions</h3><blockquote><p>检查前面安装的程序的版本</p></blockquote><figure class="highlight yml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Log</span> <span class="string">versions</span></span><br><span class="line">  <span class="attr">run:</span> <span class="string">|</span></span><br><span class="line"><span class="string">    node -v</span></span><br><span class="line"><span class="string">    python --version</span></span><br><span class="line"><span class="string">    rustc --version</span></span><br><span class="line"><span class="string">    cargo --version</span></span><br><span class="line"><span class="string">    deno --version</span></span><br></pre></td></tr></table></figure><h3 id="15、lint-js"><a href="#15、lint-js" class="headerlink" title="15、lint.js"></a>15、lint.js</h3><blockquote><p>执行 lint</p></blockquote><figure class="highlight yml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="bullet">-</span> <span class="attr">name:</span> <span class="string">lint.js</span></span><br><span class="line">  <span class="attr">if:</span> <span class="string">matrix.kind</span> <span class="string">==</span> <span class="string">&#x27;lint&#x27;</span></span><br><span class="line">  <span class="attr">run:</span> <span class="string">deno</span> <span class="string">run</span> <span class="string">--unstable</span> <span class="string">--allow-write</span> <span class="string">--allow-read</span> <span class="string">--allow-run</span> <span class="string">./tools/lint.js</span></span><br></pre></td></tr></table></figure><ul><li>执行条件：构建矩阵 <code>kind</code> 类型 为 <code>&#39;lint&#39;</code></li></ul><h3 id="16、test-format-js"><a href="#16、test-format-js" class="headerlink" title="16、test_format.js"></a>16、test_format.js</h3><blockquote><p>测试格式化程序</p></blockquote><figure class="highlight yml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="bullet">-</span> <span class="attr">name:</span> <span class="string">test_format.js</span></span><br><span class="line">  <span class="attr">if:</span> <span class="string">matrix.kind</span> <span class="string">==</span> <span class="string">&#x27;lint&#x27;</span></span><br><span class="line">  <span class="attr">run:</span> <span class="string">deno</span> <span class="string">run</span> <span class="string">--unstable</span> <span class="string">--allow-write</span> <span class="string">--allow-read</span> <span class="string">--allow-run</span> <span class="string">./tools/format.js</span> <span class="string">--check</span></span><br></pre></td></tr></table></figure><ul><li>执行条件：构建矩阵 <code>kind</code> 类型 为 <code>&#39;lint&#39;</code></li></ul><h3 id="17、Build-release"><a href="#17、Build-release" class="headerlink" title="17、Build release"></a>17、Build release</h3><figure class="highlight yml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Build</span> <span class="string">release</span></span><br><span class="line">  <span class="attr">if:</span> <span class="string">|</span></span><br><span class="line"><span class="string">    matrix.kind == &#x27;test_release&#x27; ||</span></span><br><span class="line"><span class="string">    matrix.kind == &#x27;bench&#x27;</span></span><br><span class="line"><span class="string"></span>  <span class="attr">run:</span> <span class="string">cargo</span> <span class="string">build</span> <span class="string">--release</span> <span class="string">--locked</span> <span class="string">--all-targets</span> <span class="string">-vv</span></span><br></pre></td></tr></table></figure><ul><li>执行条件：构建矩阵 <code>kind</code> 类型 为 <code>&#39;test_release&#39;</code> 或 <code>&#39;bench&#39;</code></li></ul><h3 id="18、Build-debug"><a href="#18、Build-debug" class="headerlink" title="18、Build debug"></a>18、Build debug</h3><figure class="highlight yml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Build</span> <span class="string">debug</span></span><br><span class="line">  <span class="attr">if:</span> <span class="string">matrix.kind</span> <span class="string">==</span> <span class="string">&#x27;test_debug&#x27;</span></span><br><span class="line">  <span class="attr">run:</span> <span class="string">cargo</span> <span class="string">build</span> <span class="string">--locked</span> <span class="string">--all-targets</span></span><br></pre></td></tr></table></figure><ul><li>执行条件：构建矩阵 <code>kind</code> 类型 为 <code>&#39;test_debug&#39;</code></li></ul><h3 id="19、Pre-release-linux"><a href="#19、Pre-release-linux" class="headerlink" title="19、Pre-release (linux)"></a>19、Pre-release (linux)</h3><blockquote><p>发布之前的准备工作（linux）</p></blockquote><figure class="highlight yml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Pre-release</span> <span class="string">(linux)</span></span><br><span class="line">  <span class="attr">if:</span> <span class="string">|</span></span><br><span class="line"><span class="string">    startsWith(matrix.os, &#x27;ubuntu&#x27;) &amp;&amp;</span></span><br><span class="line"><span class="string">    matrix.kind == &#x27;test_release&#x27;</span></span><br><span class="line"><span class="string"></span>  <span class="attr">run:</span> <span class="string">|</span></span><br><span class="line"><span class="string">    cd target/release</span></span><br><span class="line"><span class="string">    zip -r deno-x86_64-unknown-linux-gnu.zip deno</span></span><br><span class="line"><span class="string">    zip -r denort-x86_64-unknown-linux-gnu.zip denort</span></span><br><span class="line"><span class="string">    ./deno types &gt; lib.deno.d.ts</span></span><br></pre></td></tr></table></figure><ul><li>执行条件：构建矩阵 os 为 <code>&#39;ubuntu&#39;</code>、构建矩阵 <code>kind</code> 为 <code>&#39;test_release&#39;</code></li><li><code>run</code>：做的事情其实就是打压缩包，这些压缩包最终会发布在 GitHub Release 中供安装脚本使用</li></ul><h3 id="20、Pre-release-mac"><a href="#20、Pre-release-mac" class="headerlink" title="20、Pre-release (mac)"></a>20、Pre-release (mac)</h3><blockquote><p>和 19 作用一样，这里没有 <code>./deno types &gt; lib.deno.d.ts</code>，私以为漏了。</p></blockquote><figure class="highlight yml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Pre-release</span> <span class="string">(mac)</span></span><br><span class="line">  <span class="attr">if:</span> <span class="string">|</span></span><br><span class="line"><span class="string">    startsWith(matrix.os, &#x27;macOS&#x27;) &amp;&amp;</span></span><br><span class="line"><span class="string">    matrix.kind == &#x27;test_release&#x27;</span></span><br><span class="line"><span class="string"></span>  <span class="attr">run:</span> <span class="string">|</span></span><br><span class="line"><span class="string">    cd target/release</span></span><br><span class="line"><span class="string">    zip -r deno-x86_64-apple-darwin.zip deno</span></span><br><span class="line"><span class="string">    zip -r denort-x86_64-apple-darwin.zip denort</span></span><br></pre></td></tr></table></figure><h3 id="21、Pre-release-windows"><a href="#21、Pre-release-windows" class="headerlink" title="21、Pre-release (windows)"></a>21、Pre-release (windows)</h3><blockquote><p>和 19、20 一样，just for windows，可以学习一下 Windows 下的压缩命令怎么写！</p></blockquote><figure class="highlight yml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Pre-release</span> <span class="string">(windows)</span></span><br><span class="line">  <span class="attr">if:</span> <span class="string">|</span></span><br><span class="line"><span class="string">    startsWith(matrix.os, &#x27;windows&#x27;) &amp;&amp;</span></span><br><span class="line"><span class="string">    matrix.kind == &#x27;test_release&#x27;</span></span><br><span class="line"><span class="string"></span>  <span class="attr">run:</span> <span class="string">|</span></span><br><span class="line"><span class="string">    Compress-Archive -CompressionLevel Optimal -Force -Path target/release/deno.exe -DestinationPath target/release/deno-x86_64-pc-windows-msvc.zip</span></span><br><span class="line"><span class="string">    Compress-Archive -CompressionLevel Optimal -Force -Path target/release/denort.exe -DestinationPath target/release/denort-x86_64-pc-windows-msvc.zip</span></span><br></pre></td></tr></table></figure><h3 id="22、Upload-canary-to-dl-deno-land-unix"><a href="#22、Upload-canary-to-dl-deno-land-unix" class="headerlink" title="22、Upload canary to dl.deno.land (unix)"></a>22、Upload canary to dl.deno.land (unix)</h3><blockquote><p>上传 canary 包到 dl.deno.land（unix）(我们只要知道托管在 Google 云就好了)</p></blockquote><figure class="highlight yml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Upload</span> <span class="string">canary</span> <span class="string">to</span> <span class="string">dl.deno.land</span> <span class="string">(unix)</span></span><br><span class="line">  <span class="attr">if:</span> <span class="string">|</span></span><br><span class="line"><span class="string">    runner.os != &#x27;Windows&#x27; &amp;&amp;</span></span><br><span class="line"><span class="string">    matrix.kind == &#x27;test_release&#x27; &amp;&amp;</span></span><br><span class="line"><span class="string">    github.repository == &#x27;denoland/deno&#x27; &amp;&amp;</span></span><br><span class="line"><span class="string">    github.ref == &#x27;refs/heads/master&#x27;</span></span><br><span class="line"><span class="string"></span>  <span class="attr">run:</span> <span class="string">|</span></span><br><span class="line"><span class="string">    gsutil cp ./target/release/*.zip gs://dl.deno.land/canary/$(git rev-parse HEAD)/</span></span><br><span class="line"><span class="string">    echo $(git rev-parse HEAD) &gt; canary-latest.txt</span></span><br><span class="line"><span class="string">    gsutil cp canary-latest.txt gs://dl.deno.land/canary-latest.txt</span></span><br></pre></td></tr></table></figure><ul><li>执行条件：操作系统不是 Windows、构建矩阵 kind 是 <code>&#39;test_release&#39;</code>、github repo 是 <code>&#39;denoland/deno&#39;</code>、master 分支</li><li><code>run</code><ul><li><code>gsutil cp ./target/release/*.zip gs://dl.deno.land/canary/$(git rev-parse HEAD)/</code>：上传压缩包到服务器<ul><li><code>$(git rev-parse HEAD)</code>：获取最新的 git commit hash，你可以再任一 git 库中执行 <code>echo $(git rev-parse HEAD)</code> 验证</li></ul></li><li><code>echo $(git rev-parse HEAD) &gt; canary-latest.txt</code>：将 commit hash 值作为版本写入 canary-latest.txt</li><li><code>gsutil cp canary-latest.txt gs://dl.deno.land/canary-latest.txt</code>：上传 <code>canary-latest.txt</code> 到服务器</li></ul></li></ul><h3 id="23、Upload-canary-to-dl-deno-land-windows"><a href="#23、Upload-canary-to-dl-deno-land-windows" class="headerlink" title="23、Upload canary to dl.deno.land (windows)"></a>23、Upload canary to dl.deno.land (windows)</h3><blockquote><p>和 22 作用一样，just for windows</p></blockquote><figure class="highlight yml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Upload</span> <span class="string">canary</span> <span class="string">to</span> <span class="string">dl.deno.land</span> <span class="string">(windows)</span></span><br><span class="line">  <span class="attr">if:</span> <span class="string">|</span></span><br><span class="line"><span class="string">    runner.os == &#x27;Windows&#x27; &amp;&amp;</span></span><br><span class="line"><span class="string">    matrix.kind == &#x27;test_release&#x27; &amp;&amp;</span></span><br><span class="line"><span class="string">    github.repository == &#x27;denoland/deno&#x27; &amp;&amp;</span></span><br><span class="line"><span class="string">    github.ref == &#x27;refs/heads/master&#x27;</span></span><br><span class="line"><span class="string"></span>  <span class="attr">env:</span></span><br><span class="line">    <span class="attr">CLOUDSDK_PYTHON:</span> <span class="string">$&#123;&#123;env.pythonLocation&#125;&#125;\python.exe</span></span><br><span class="line">  <span class="attr">shell:</span> <span class="string">bash</span></span><br><span class="line">  <span class="attr">run:</span> <span class="string">|</span></span><br><span class="line"><span class="string">    gsutil cp ./target/release/*.zip gs://dl.deno.land/canary/$(git rev-parse HEAD)/</span></span><br><span class="line"><span class="string">    echo $(git rev-parse HEAD) &gt; canary-latest.txt</span></span><br><span class="line"><span class="string">    gsutil cp canary-latest.txt gs://dl.deno.land/canary-latest.txt</span></span><br></pre></td></tr></table></figure><ul><li><code>CLOUDSDK_PYTHON: $&#123;&#123;env.pythonLocation&#125;&#125;\python.exe</code>：从这里可以看到 Python 环境是给 google cloud sdk 用的</li></ul><h3 id="24、Test-release"><a href="#24、Test-release" class="headerlink" title="24、Test release"></a>24、Test release</h3><blockquote><p>测试发布</p></blockquote><figure class="highlight yml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Test</span> <span class="string">release</span></span><br><span class="line">  <span class="attr">if:</span> <span class="string">matrix.kind</span> <span class="string">==</span> <span class="string">&#x27;test_release&#x27;</span></span><br><span class="line">  <span class="attr">run:</span> <span class="string">cargo</span> <span class="string">test</span> <span class="string">--release</span> <span class="string">--locked</span> <span class="string">--all-targets</span></span><br></pre></td></tr></table></figure><h3 id="25、Test-debug"><a href="#25、Test-debug" class="headerlink" title="25、Test debug"></a>25、Test debug</h3><blockquote><p>测试 debug</p></blockquote><figure class="highlight yml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Test</span> <span class="string">debug</span></span><br><span class="line">  <span class="attr">if:</span> <span class="string">matrix.kind</span> <span class="string">==</span> <span class="string">&#x27;test_debug&#x27;</span></span><br><span class="line">  <span class="attr">run:</span> <span class="string">|</span></span><br><span class="line"><span class="string">    cargo test --locked --doc</span></span><br><span class="line"><span class="string">    cargo test --locked --all-targets</span></span><br></pre></td></tr></table></figure><h3 id="26、Configure-hosts-file-for-WPT-unix"><a href="#26、Configure-hosts-file-for-WPT-unix" class="headerlink" title="26、Configure hosts file for WPT (unix)"></a>26、Configure hosts file for WPT (unix)</h3><h3 id="26、Configure-hosts-file-for-WPT-unix-1"><a href="#26、Configure-hosts-file-for-WPT-unix-1" class="headerlink" title="26、Configure hosts file for WPT (unix)"></a>26、Configure hosts file for WPT (unix)</h3><blockquote><p>WPT 是 Web Platform Test 的意思，对应仓库 <a href="https://github.com/denoland/wpt">denoland&#x2F;wpt</a></p></blockquote><figure class="highlight yml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Configure</span> <span class="string">hosts</span> <span class="string">file</span> <span class="string">for</span> <span class="string">WPT</span> <span class="string">(unix)</span></span><br><span class="line"> <span class="attr">if:</span> <span class="string">runner.os</span> <span class="type">!=</span> <span class="string">&#x27;Windows&#x27;</span></span><br><span class="line"> <span class="attr">run:</span> <span class="string">./wpt</span> <span class="string">make-hosts-file</span> <span class="string">|</span> <span class="string">sudo</span> <span class="string">tee</span> <span class="string">-a</span> <span class="string">/etc/hosts</span></span><br><span class="line"> <span class="attr">working-directory:</span> <span class="string">test_util/wpt/</span></span><br></pre></td></tr></table></figure><h3 id="27、Configure-hosts-file-for-WPT-windows"><a href="#27、Configure-hosts-file-for-WPT-windows" class="headerlink" title="27、Configure hosts file for WPT (windows)"></a>27、Configure hosts file for WPT (windows)</h3><blockquote><p>和 26 作用一样，just for windows</p></blockquote><figure class="highlight yml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Configure</span> <span class="string">hosts</span> <span class="string">file</span> <span class="string">for</span> <span class="string">WPT</span> <span class="string">(windows)</span></span><br><span class="line">  <span class="attr">if:</span> <span class="string">runner.os</span> <span class="string">==</span> <span class="string">&#x27;Windows&#x27;</span></span><br><span class="line">  <span class="attr">working-directory:</span> <span class="string">test_util/wpt/</span></span><br><span class="line">  <span class="attr">run:</span> <span class="string">python</span> <span class="string">wpt</span> <span class="string">make-hosts-file</span> <span class="string">|</span> <span class="string">Out-File</span> <span class="string">$env:SystemRoot\System32\drivers\etc\hosts</span> <span class="string">-Encoding</span> <span class="string">ascii</span> <span class="string">-Append</span></span><br></pre></td></tr></table></figure><h3 id="28、Run-web-platform-tests-release"><a href="#28、Run-web-platform-tests-release" class="headerlink" title="28、Run web platform tests (release)"></a>28、Run web platform tests (release)</h3><blockquote><p>在 web 平台下运行测试（release）</p></blockquote><figure class="highlight yml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Run</span> <span class="string">web</span> <span class="string">platform</span> <span class="string">tests</span> <span class="string">(release)</span></span><br><span class="line">  <span class="attr">if:</span> <span class="string">matrix.kind</span> <span class="string">==</span> <span class="string">&#x27;test_release&#x27;</span></span><br><span class="line">  <span class="attr">run:</span> <span class="string">|</span></span><br><span class="line"><span class="string">    deno run --unstable --allow-write --allow-read --allow-net --allow-env --allow-run ./tools/wpt.ts setup</span></span><br><span class="line"><span class="string">    deno run --unstable --allow-write --allow-read --allow-net --allow-env --allow-run ./tools/wpt.ts run --quiet --release</span></span><br></pre></td></tr></table></figure><h3 id="29、Run-web-platform-tests-debug"><a href="#29、Run-web-platform-tests-debug" class="headerlink" title="29、Run web platform tests (debug)"></a>29、Run web platform tests (debug)</h3><blockquote><p>在 web 平台下运行测试（debug）</p></blockquote><figure class="highlight yml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Run</span> <span class="string">web</span> <span class="string">platform</span> <span class="string">tests</span> <span class="string">(debug)</span></span><br><span class="line"> <span class="attr">if:</span> <span class="string">matrix.kind</span> <span class="string">==</span> <span class="string">&#x27;test_debug&#x27;</span></span><br><span class="line"> <span class="attr">run:</span> <span class="string">|</span></span><br><span class="line"><span class="string">   deno run --unstable --allow-write --allow-read --allow-net --allow-env --allow-run ./tools/wpt.ts setup</span></span><br><span class="line"><span class="string">   deno run --unstable --allow-write --allow-read --allow-net --allow-env --allow-run ./tools/wpt.ts run --quiet</span></span><br></pre></td></tr></table></figure><h3 id="30、Run-Benchmarks"><a href="#30、Run-Benchmarks" class="headerlink" title="30、Run Benchmarks"></a>30、Run Benchmarks</h3><blockquote><p>压力测试</p></blockquote><figure class="highlight yml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Run</span> <span class="string">Benchmarks</span></span><br><span class="line">  <span class="attr">if:</span> <span class="string">matrix.kind</span> <span class="string">==</span> <span class="string">&#x27;bench&#x27;</span></span><br><span class="line">  <span class="attr">run:</span> <span class="string">cargo</span> <span class="string">bench</span></span><br></pre></td></tr></table></figure><ul><li>执行条件：构建矩阵 kind 为 <code>&#39;bench&#39;</code></li></ul><h3 id="31、Post-Benchmarks"><a href="#31、Post-Benchmarks" class="headerlink" title="31、Post Benchmarks"></a>31、Post Benchmarks</h3><blockquote><p>看起来是把压测数据存到 <code>denoland/benchmark_data</code></p></blockquote><figure class="highlight yml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Post</span> <span class="string">Benchmarks</span></span><br><span class="line">  <span class="attr">if:</span> <span class="string">|</span></span><br><span class="line"><span class="string">    matrix.kind == &#x27;bench&#x27; &amp;&amp;</span></span><br><span class="line"><span class="string">    github.repository == &#x27;denoland/deno&#x27; &amp;&amp;</span></span><br><span class="line"><span class="string">    github.ref == &#x27;refs/heads/master&#x27;</span></span><br><span class="line"><span class="string"></span>  <span class="attr">env:</span></span><br><span class="line">    <span class="attr">DENOBOT_PAT:</span> <span class="string">$&#123;&#123;</span> <span class="string">secrets.DENOBOT_PAT</span> <span class="string">&#125;&#125;</span></span><br><span class="line">  <span class="attr">run:</span> <span class="string">|</span></span><br><span class="line"><span class="string">    git clone --depth 1 -b gh-pages https://$&#123;DENOBOT_PAT&#125;@github.com/denoland/benchmark_data.git gh-pages</span></span><br><span class="line"><span class="string">    deno run --unstable -A ./tools/build_benchmark_jsons.js --release</span></span><br><span class="line"><span class="string">    cd gh-pages</span></span><br><span class="line"><span class="string">    git config user.email &quot;propelml@gmail.com&quot;</span></span><br><span class="line"><span class="string">    git config user.name &quot;denobot&quot;</span></span><br><span class="line"><span class="string">    git add .</span></span><br><span class="line"><span class="string">    git commit --message &quot;Update benchmarks&quot;</span></span><br><span class="line"><span class="string">    git push origin gh-pages</span></span><br></pre></td></tr></table></figure><ul><li>执行条件：<code>denoland/deno</code> 库的 master 分支且构建矩阵的 kind 是 <code>&#39;bench&#39;</code></li></ul><h3 id="32、Worker-info"><a href="#32、Worker-info" class="headerlink" title="32、Worker info"></a>32、Worker info</h3><figure class="highlight yml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Worker</span> <span class="string">info</span></span><br><span class="line">  <span class="attr">if:</span> <span class="string">matrix.kind</span> <span class="string">==</span> <span class="string">&#x27;bench&#x27;</span></span><br><span class="line">  <span class="attr">run:</span> <span class="string">|</span></span><br><span class="line"><span class="string">    cat /proc/cpuinfo</span></span><br><span class="line"><span class="string">    cat /proc/meminfo</span></span><br></pre></td></tr></table></figure><ul><li>执行条件：构建矩阵 kind 是 <code>bench</code></li><li>run<ul><li><code>cat /proc/cpuinfo</code>：查看 cpu 信息</li><li><code>cat /proc/meminfo</code>：查看内存信息</li></ul></li></ul><h3 id="33、Upload-release-to-dl-deno-land-unix"><a href="#33、Upload-release-to-dl-deno-land-unix" class="headerlink" title="33、Upload release to dl.deno.land (unix)"></a>33、Upload release to dl.deno.land (unix)</h3><blockquote><p>上传 unix release 到 dl.deno.land</p></blockquote><figure class="highlight yml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Upload</span> <span class="string">release</span> <span class="string">to</span> <span class="string">dl.deno.land</span> <span class="string">(unix)</span></span><br><span class="line">  <span class="attr">if:</span> <span class="string">|</span></span><br><span class="line"><span class="string">    runner.os != &#x27;Windows&#x27; &amp;&amp;</span></span><br><span class="line"><span class="string">    matrix.kind == &#x27;test_release&#x27; &amp;&amp;</span></span><br><span class="line"><span class="string">    github.repository == &#x27;denoland/deno&#x27; &amp;&amp;</span></span><br><span class="line"><span class="string">    startsWith(github.ref, &#x27;refs/tags/&#x27;)</span></span><br><span class="line"><span class="string"></span>  <span class="attr">run:</span> <span class="string">|</span></span><br><span class="line"><span class="string">    gsutil cp ./target/release/*.zip gs://dl.deno.land/release/$&#123;GITHUB_REF#refs/*/&#125;/</span></span><br><span class="line"><span class="string">    echo $&#123;GITHUB_REF#refs/*/&#125; &gt; release-latest.txt</span></span><br><span class="line"><span class="string">    gsutil cp release-latest.txt gs://dl.deno.land/release-latest.txt</span></span><br></pre></td></tr></table></figure><h3 id="34、Upload-release-to-dl-deno-land-windows"><a href="#34、Upload-release-to-dl-deno-land-windows" class="headerlink" title="34、Upload release to dl.deno.land (windows)"></a>34、Upload release to dl.deno.land (windows)</h3><blockquote><p>上传 windows release 到 dl.deno.land</p></blockquote><figure class="highlight yml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Upload</span> <span class="string">release</span> <span class="string">to</span> <span class="string">dl.deno.land</span> <span class="string">(windows)</span></span><br><span class="line">  <span class="attr">if:</span> <span class="string">|</span></span><br><span class="line"><span class="string">    runner.os == &#x27;Windows&#x27; &amp;&amp;</span></span><br><span class="line"><span class="string">    matrix.kind == &#x27;test_release&#x27; &amp;&amp;</span></span><br><span class="line"><span class="string">    github.repository == &#x27;denoland/deno&#x27; &amp;&amp;</span></span><br><span class="line"><span class="string">    startsWith(github.ref, &#x27;refs/tags/&#x27;)</span></span><br><span class="line"><span class="string"></span>  <span class="attr">env:</span></span><br><span class="line">    <span class="attr">CLOUDSDK_PYTHON:</span> <span class="string">$&#123;&#123;env.pythonLocation&#125;&#125;\python.exe</span></span><br><span class="line">  <span class="attr">shell:</span> <span class="string">bash</span></span><br><span class="line">  <span class="attr">run:</span> <span class="string">|</span></span><br><span class="line"><span class="string">    gsutil cp ./target/release/*.zip gs://dl.deno.land/release/$&#123;GITHUB_REF#refs/*/&#125;/</span></span><br><span class="line"><span class="string">    echo $&#123;GITHUB_REF#refs/*/&#125; &gt; release-latest.txt</span></span><br><span class="line"><span class="string">    gsutil cp release-latest.txt gs://dl.deno.land/release-latest.txt</span></span><br></pre></td></tr></table></figure><h3 id="35、Upload-release-to-GitHub"><a href="#35、Upload-release-to-GitHub" class="headerlink" title="35、Upload release to GitHub"></a>35、Upload release to GitHub</h3><blockquote><p>上传 release 到 GitHub</p></blockquote><figure class="highlight yml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Upload</span> <span class="string">release</span> <span class="string">to</span> <span class="string">GitHub</span></span><br><span class="line">  <span class="attr">uses:</span> <span class="string">softprops/action-gh-release@v1</span></span><br><span class="line">  <span class="attr">if:</span> <span class="string">|</span></span><br><span class="line"><span class="string">    matrix.kind == &#x27;test_release&#x27; &amp;&amp;</span></span><br><span class="line"><span class="string">    github.repository == &#x27;denoland/deno&#x27; &amp;&amp;</span></span><br><span class="line"><span class="string">    startsWith(github.ref, &#x27;refs/tags/&#x27;)</span></span><br><span class="line"><span class="string"></span>  <span class="attr">env:</span></span><br><span class="line">    <span class="attr">GITHUB_TOKEN:</span> <span class="string">$&#123;&#123;</span> <span class="string">secrets.GITHUB_TOKEN</span> <span class="string">&#125;&#125;</span></span><br><span class="line">  <span class="attr">with:</span></span><br><span class="line">    <span class="attr">files:</span> <span class="string">|</span></span><br><span class="line"><span class="string">      target/release/deno-x86_64-pc-windows-msvc.zip</span></span><br><span class="line"><span class="string">      target/release/deno-x86_64-unknown-linux-gnu.zip</span></span><br><span class="line"><span class="string">      target/release/deno-x86_64-apple-darwin.zip</span></span><br><span class="line"><span class="string">      target/release/deno_src.tar.gz</span></span><br><span class="line"><span class="string">      target/release/lib.deno.d.ts</span></span><br><span class="line"><span class="string"></span>    <span class="attr">draft:</span> <span class="literal">true</span></span><br></pre></td></tr></table></figure><ul><li><code>softprops/action-gh-release@v1</code>：发布 github release 用的 action，这个确实很棒，可以执行直接上传 release asset 等高级操作。我在 <a href="https://github.com/youngjuning/tuya-panel-demo/blob/master/.github/workflows/release.yml">tuya-panel-demo</a> 中使用了该插件。</li></ul>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/8fcf3519517c/</id>
    <link href="https://blog.zisheng.pro/posts/8fcf3519517c/"/>
    <published>2023-04-19T10:50:31.000Z</published>
    <summary>GitHub Action 是 GitHub 官方的 CI/CD 工具，相较于 Travis CI 和 Circle CI，更轻量和易于扩展，marketplace 中有大量社区贡献的插件。各大开源项目都纷纷转向使用 GitHub Action 作为持续集成的工具，比如本文的主角 Deno。</summary>
    <title>叮，一份 Deno GitHub Action 源码解析请查收</title>
    <updated>2026-09-18T08:15:28.636Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="React Native" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/React-Native/"/>
    <category term="React Native" scheme="https://blog.zisheng.pro/tags/React-Native/"/>
    <category term="调试" scheme="https://blog.zisheng.pro/tags/%E8%B0%83%E8%AF%95/"/>
    <category term="调试工具" scheme="https://blog.zisheng.pro/tags/%E8%B0%83%E8%AF%95%E5%B7%A5%E5%85%B7/"/>
    <category term="调试技巧" scheme="https://blog.zisheng.pro/tags/%E8%B0%83%E8%AF%95%E6%8A%80%E5%B7%A7/"/>
    <category term="Reactotron" scheme="https://blog.zisheng.pro/tags/Reactotron/"/>
    <category term="Flipper" scheme="https://blog.zisheng.pro/tags/Flipper/"/>
    <category term="React Native Debugger" scheme="https://blog.zisheng.pro/tags/React-Native-Debugger/"/>
    <category term="React Native Debug" scheme="https://blog.zisheng.pro/tags/React-Native-Debug/"/>
    <category term="React Native Debugging" scheme="https://blog.zisheng.pro/tags/React-Native-Debugging/"/>
    <content>
      <![CDATA[<p>人非圣贤孰能无过。</p><p>这句话还（更确切地说：尤其是…）适用于软件开发者。</p><p>这就是为什么在每一个开发者的职业生涯中最重要的是知道如何处理这些失误。作为一个 React Native 开发者，我经常想要写出没有 BUG 的代码，但是当出现问题时，我需要确保我能够追踪并解决问题。</p><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b361ca265e1d478faf33e97edd22b1e2~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><p>在这篇文章中，我想讨论一些日常用于移动应用程序开发的工具和技术。我希望你可以发现一些对你的项目有帮助的东西。</p><h2 id="静态检测，类型检查和格式化"><a href="#静态检测，类型检查和格式化" class="headerlink" title="静态检测，类型检查和格式化"></a>静态检测，类型检查和格式化</h2><h2 id="静态检测"><a href="#静态检测" class="headerlink" title="静态检测"></a>静态检测</h2><p>Linting 是执行程序的过程，用于分析潜在的语法程序错误。JavaScript 中最著名的 Linting 插件有：</p><ul><li>ESLint - JavaScript 类型检查和格式化工具</li><li>Google’s Closure Compiler – 一个 JavaScript 优化器，可以更快速，更简洁地重写代码并检查本机 JavaScript 函数的使用。</li><li>JSLint – JavaScript 语法检查器和验证器</li></ul><p>现在 ESLint 处于一统江湖的地位，也是 TypeScript 官方的 Linting 插件。</p><h2 id="类型检查"><a href="#类型检查" class="headerlink" title="类型检查"></a>类型检查</h2><p>我使用 TypeScript（TS）或者 Flow 来做 类型检查。两者最主要的区别是前者是编程语言，而 Flow 是类型检查器。在我看来，TS 是更优的选择，因为大量类似于自动导入、自我证明和使用下一代 JavaScript 的能力。</p><blockquote><p>原文中说 TS 是编程语言，我不完全赞同。毕竟官方的定位也只是 JavaScript 的超集。</p></blockquote><h2 id="格式化代码"><a href="#格式化代码" class="headerlink" title="格式化代码"></a>格式化代码</h2><p>程序员大部分时间都在阅读代码，因此重点不是更快地阅读代码，而是快速理解代码。</p><p>为了做到快读理解代码，我们需要最有效的视觉表示。这就是为什么我们需要很好地格式化代码。</p><p>比较一下下面的代码片段：</p><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b3f4cfbcf58a4726af1eb510386dc12a~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ec3a96ee27354026a8013168bbb9b053~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><p>我建议大家使用 Prettier 格式化代码 - 它很容易集成和配置。你也可以通过 <code>eslint-config-prettier</code> 和 <code>eslint-plugin-prettier</code> 这两个插件和 ESLint 配合使用。</p><h2 id="如何调试-React-Native-应用"><a href="#如何调试-React-Native-应用" class="headerlink" title="如何调试 React Native 应用"></a>如何调试 React Native 应用</h2><p>React Native 具有开发人员友好的环境，会告诉我们我们在警告或错误方面做错了什么：</p><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5b33a0defc7b47c8b7de95526dd76514~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><h2 id="内置调试模式"><a href="#内置调试模式" class="headerlink" title="内置调试模式"></a>内置调试模式</h2><p>首先，您可以使用浏览器的内置调试模式（例如 Chrome 或 Safari）调试应用程序。</p><p>在 Chrome 中使用，你需要安装 <code>react-devtools</code>：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm install -g react-devtools</span><br></pre></td></tr></table></figure><p>在开发模式下，您可以打开开发人员菜单并从那里开始调试您的应用程序。只需激活 <code>debug</code> 选项：</p><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/682b8dec24284e48a01f6647eb1b333b~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><p>它将打开带有 <code>http://localhost8081/debugger-ui</code> 路径的 Chrome 浏览器标签。</p><p>在您的 Chrome 浏览器中，您应该看到以下屏幕：</p><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a119459876cc44d7a6a7388114b92aa3~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><p>您也可以在 Safari 中调试应用的 iOS 版本，而不必启用 <code>Debug JS Remotely</code>。</p><p>怎么做？在 Safari 浏览器中，只需打开：</p><p><strong>Preferences → Advanced → Select “Show Develop menu in menu bar</strong></p><p>并选择 App 的 JSContext:</p><p><strong>Develop → Simulator → JSContext</strong></p><p>Safari 的 Web 检查器应打开，并应显示一个 <code>Console</code> 和一个 <code>Debugger</code>。每次重新加载应用程序时（使用实时 <code>live reload</code>、<code>fast refresh</code> 或通过手动重新加载），一个新的 JSContext 将被创建。只需选择 <code>Automatically Show Web Inspectors for JSContexts</code>，就可以避免手动选择最新的 JSContext。</p><h2 id="Reactotron"><a href="#Reactotron" class="headerlink" title="Reactotron"></a>Reactotron</h2><p>如果您习惯将 Redux 用于 React Native 或 ReactJS 的状态管理，<a href="https://infinite.red/reactotron">Reactotron</a> 是调试状态的绝佳工具：</p><ul><li>查看应用状态</li><li>显示 API 请求和响应</li><li>执行快速的性能压测</li><li>订阅应用的部分状态</li><li>显示类似于 <code>console.log</code> 的消息</li><li>使用 <code>source-mapped</code> 堆栈跟踪（包括 saga 堆栈跟踪）跟踪全局错误！</li><li>dispatch actions like a government-run mind control experiment</li><li>使用 Redux 或 mobx-state-tree 热交换您应用的状态</li><li>追踪你的 saga</li><li>在 React Native 中显示图像浮层</li><li>在 React Native 中跟踪您的异步存储</li></ul><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/2e43cd0a09634d70891b33ae4b563a4a~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><h2 id="React-Native-Debugger"><a href="#React-Native-Debugger" class="headerlink" title="React Native Debugger"></a>React Native Debugger</h2><p><a href="https://github.com/jhen0409/react-native-debugger">react-native-debugger</a> 这是一个桌面应用程序，具有许多可调试应用程序的功能。至于应该提到的最重要的优点：</p><ul><li>它基于官方的 Remote Debugger 并提供更多功能。</li><li>它包括来自 react-devtools-core 的 React Inspector。</li><li>它包括 Redux DevTools，并使用 redux-devtools-extension 制作了相同的 API。</li></ul><blockquote><p>注意: 如果你使用了 0.62 版本以上的 React Native，请使用 React Native Debugger v0.11</p></blockquote><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/be725c70791d4971b5a4eaefb43ad740~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><h2 id="Native-Logs"><a href="#Native-Logs" class="headerlink" title="Native Logs"></a>Native Logs</h2><p>根据<a href="https://reactnative.dev/docs/debugging#accessing-console-logs">官方</a>的 React Native 文档，您可以轻松记录两种平台的报告：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">$ npx react-native log-ios</span><br><span class="line">$ npx react-native log-android</span><br></pre></td></tr></table></figure><p>如果您需要进一步研究，可以使用特定于平台的 IDE（例如 <a href="https://developer.apple.com/xcode/">XCode</a> 或 <a href="https://developer.android.com/studio">Android Studio</a>）来分析应用程序的本机代码并解决问题。</p><h2 id="Flipper"><a href="#Flipper" class="headerlink" title="Flipper"></a>Flipper</h2><p><a href="https://fbflipper.com/">Flipper</a> 是用于调试 iOS、Android 和 React Native 应用程序的下一代平台。这是一个桌面应用程序，可让您灵活地检查、可视化和控制应用程序开发调试。这里很酷的功能是您可以通过扩展来更新 Flipper 的功能，例如：</p><ul><li><a href="https://github.com/jk-gan/redux-flipper">https://github.com/jk-gan/redux-flipper</a></li><li><a href="https://github.com/blankapp/flipper-plugin-reduxinspector">https://github.com/blankapp/flipper-plugin-reduxinspector</a></li></ul><p>如果您想集成 Redux 日志支持。</p><p>Flipper 支持 React Native Debugger 所有的功能，但是它也增加了很多。查看<a href="https://www.flippercloud.io/docs">官方文档</a>，以确保并可能创建自己的插件并支持社区。</p><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/14f985e6f5da4c238c751d873772cc7c~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><blockquote><p>本文翻译自 <a href="https://www.ideamotive.co/blog/how-to-debug-your-react-native-apps-like-a-pro">How To Debug React Native Apps Like A Pro? (Tools And Best Practices)</a></p></blockquote>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/1219c44abdaa/</id>
    <link href="https://blog.zisheng.pro/posts/1219c44abdaa/"/>
    <published>2023-04-19T10:43:42.000Z</published>
    <summary>在每一个开发者的职业生涯中最重要的是知道如何处理这些失误。作为一个 React Native 开发者，我经常想要写出没有 BUG 的代码，但是当出现问题时，我需要确保我能够追踪并解决问题。</summary>
    <title>React Native 调试最佳实践</title>
    <updated>2026-09-18T08:15:28.632Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="Rust" scheme="https://blog.zisheng.pro/categories/Rust/"/>
    <category term="Deno" scheme="https://blog.zisheng.pro/categories/Deno/"/>
    <category term="Node.js" scheme="https://blog.zisheng.pro/tags/Node-js/"/>
    <category term="Rust" scheme="https://blog.zisheng.pro/tags/Rust/"/>
    <category term="Deno" scheme="https://blog.zisheng.pro/tags/Deno/"/>
    <category term="WebAssembly" scheme="https://blog.zisheng.pro/tags/WebAssembly/"/>
    <category term="Web" scheme="https://blog.zisheng.pro/tags/Web/"/>
    <content>
      <![CDATA[<h2 id="为什么选择-Rust"><a href="#为什么选择-Rust" class="headerlink" title="为什么选择 Rust?"></a>为什么选择 Rust?</h2><p>Rust 是一门可以帮助你开发出高效率、高可靠性软件的编程语言。相较于 C&#x2F;C++，Rust 力图同时提供强大的工程能力及良好的开发体验，在给予开发者控制底层能力（比如内存操作）的同时，避免传统语言带来的诸多麻烦。</p><h3 id="高性能"><a href="#高性能" class="headerlink" title="高性能"></a>高性能</h3><p>Rust 速度惊人且内存利用率极高。由于没有运行时和垃圾回收，它能够胜任对性能要求特别高的服务，可以在嵌入式设备上运行，还能轻松和其他语言集成。</p><h3 id="可靠性"><a href="#可靠性" class="headerlink" title="可靠性"></a>可靠性</h3><p>Rust 丰富的类型系统和所有权模型保证了内存安全和线程安全，让您在编译期就能够消除各种各样的错误。</p><h3 id="生产力"><a href="#生产力" class="headerlink" title="生产力"></a>生产力</h3><p>Rust 拥有出色的文档、友好的编译器和清晰的错误提示信息，还集成了一流的工具——包管理器和构建工具， 能地自动补全和类型检验的多编辑器支持，以及自动格式化代码等等。</p><h2 id="Rust-落地情况"><a href="#Rust-落地情况" class="headerlink" title="Rust 落地情况"></a>Rust 落地情况</h2><p>全世界已有数百家公司在生产环境中使用 Rust，以达到快速、跨平台、低资源占用的目的。很多著名且受欢迎的软件，例如 Firefox、 Dropbox 和 Cloudflare 都在使用 Rust。从初创公司到大型企业，从嵌入式设备到可扩展的 Web 服务，Rust 都完全合适。</p><h3 id="Dropbox"><a href="#Dropbox" class="headerlink" title="Dropbox"></a>Dropbox</h3><p>Dropbox 核心文件存储系统的多个模块是用 Rust 写的，因为它作为大型项目的一部分，需要追求更高的数据中心效率。目前，它应用于所有的 Dropbox 存储系统，为超过 5 亿用户服务。</p><h3 id="Yelp"><a href="#Yelp" class="headerlink" title="Yelp"></a>Yelp</h3><p>Yelp 使用 Rust 开发了一个实时 A&#x2F;B 测试的框架。它广泛应用于所有的 Yelp 网页和应用，选择 Rust 是因为它和 C 语言一样快（运行代价小），同时比 C 语言更安全（易于维护）。</p><h2 id="Deno-与-Rust"><a href="#Deno-与-Rust" class="headerlink" title="Deno 与 Rust"></a>Deno 与 Rust</h2><p>Deno 早期是 Golang + TypeScript 的，后来因为 Golang 和 V8 的 GC 问题，Ryan 下了大工夫把 golang 统统换成没有 GC 的 Rust了。知乎看到的一个<a href="https://www.zhihu.com/question/291707891/answer/481908886">回答</a>觉得很有道理：</p><ol><li>Go 自己有 GC，V8 自己也有 GC，双 GC 和复杂的运行时会造成多少性能损失。</li><li>Rust 具有高性能、内存安全等特性，非常适合写这种对性能要求高的程序。</li></ol><blockquote><p>GC（Garbage Collector，垃圾回收机制），Rust 和其他没有垃圾回收机制，而是使用一种叫做所有权的机制代替。由于是入门篇，此处不展开。</p></blockquote><h2 id="安装-Rust"><a href="#安装-Rust" class="headerlink" title="安装 Rust"></a>安装 Rust</h2><p>和其他不太一样的是，Rust 官方提供了 rustup 命令行工具来下载、安装、管理不同的 Rust 发行版本以及其附带的工具链。</p><blockquote><p>注意：Rust 的稳定性保证了所有发行版本都是向后兼容的。</p></blockquote><p><strong>安装 rustup：</strong></p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">curl --proto <span class="string">&#x27;=https&#x27;</span> --tlsv1.2 -sSf https://sh.rustup.rs | sh</span><br></pre></td></tr></table></figure><p>输出：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br></pre></td><td class="code"><pre><span class="line">$ curl --proto <span class="string">&#x27;=https&#x27;</span> --tlsv1.2 -sSf https://sh.rustup.rs | sh</span><br><span class="line">info: downloading installer</span><br><span class="line"></span><br><span class="line">Welcome to Rust!</span><br><span class="line"></span><br><span class="line">This will download and install the official compiler <span class="keyword">for</span> the Rust</span><br><span class="line">programming language, and its package manager, Cargo.</span><br><span class="line"></span><br><span class="line">Rustup metadata and toolchains will be installed into the Rustup</span><br><span class="line">home directory, located at:</span><br><span class="line"></span><br><span class="line">  /Users/luozhu/.rustup</span><br><span class="line"></span><br><span class="line">This can be modified with the RUSTUP_HOME environment variable.</span><br><span class="line"></span><br><span class="line">The Cargo home directory located at:</span><br><span class="line"></span><br><span class="line">  /Users/luozhu/.cargo</span><br><span class="line"></span><br><span class="line">This can be modified with the CARGO_HOME environment variable.</span><br><span class="line"></span><br><span class="line">The cargo, rustc, rustup and other commands will be added to</span><br><span class="line">Cargo<span class="string">&#x27;s bin directory, located at:</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">  /Users/luozhu/.cargo/bin</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">This path will then be added to your PATH environment variable by</span></span><br><span class="line"><span class="string">modifying the profile files located at:</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">  /Users/luozhu/.profile</span></span><br><span class="line"><span class="string">  /Users/luozhu/.zshenv</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">You can uninstall at any time with rustup self uninstall and</span></span><br><span class="line"><span class="string">these changes will be reverted.</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">Current installation options:</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">   default host triple: x86_64-apple-darwin</span></span><br><span class="line"><span class="string">     default toolchain: stable (default)</span></span><br><span class="line"><span class="string">               profile: default</span></span><br><span class="line"><span class="string">  modify PATH variable: yes</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">1) Proceed with installation (default)</span></span><br><span class="line"><span class="string">2) Customize installation</span></span><br><span class="line"><span class="string">3) Cancel installation</span></span><br><span class="line"><span class="string">&gt;1</span></span><br></pre></td></tr></table></figure><p>以上信息可以总结为：</p><ol><li>Rustup 家目录：<code>~/.rustup</code>，可以用 <code>RUSTUP_HOME</code> 环境变量修改</li><li>Cargo 家目录：<code>~/.cargo</code>，可以用 <code>CARGO_HOME</code> 环境变量修改</li><li>cargo、rustc、rustup 和其他命令都安装在 <code>~/.cargo/bin</code></li><li>PATH 环境变量是放在 <code>~/.zshenv</code> 中的（这个方式第一次见）</li></ol><p>这里让我们选择安装方式，由于是入门，我们直接选择默认安装，输入如下：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br></pre></td><td class="code"><pre><span class="line">info: profile <span class="built_in">set</span> to <span class="string">&#x27;default&#x27;</span></span><br><span class="line">info: default host triple is x86_64-apple-darwin</span><br><span class="line">info: syncing channel updates <span class="keyword">for</span> <span class="string">&#x27;stable-x86_64-apple-darwin&#x27;</span></span><br><span class="line">info: latest update on 2021-02-11, rust version 1.50.0 (cb75ad5db 2021-02-10)</span><br><span class="line">info: downloading component <span class="string">&#x27;cargo&#x27;</span></span><br><span class="line">  4.3 MiB /   4.3 MiB (100 %)   3.1 MiB/s <span class="keyword">in</span>  1s ETA:  0s</span><br><span class="line">info: downloading component <span class="string">&#x27;clippy&#x27;</span></span><br><span class="line">info: downloading component <span class="string">&#x27;rust-docs&#x27;</span></span><br><span class="line"> 14.7 MiB /  14.7 MiB (100 %)   2.2 MiB/s <span class="keyword">in</span>  6s ETA:  0s</span><br><span class="line">info: downloading component <span class="string">&#x27;rust-std&#x27;</span></span><br><span class="line"> 23.3 MiB /  23.3 MiB (100 %)   2.2 MiB/s <span class="keyword">in</span> 11s ETA:  0s</span><br><span class="line">info: downloading component <span class="string">&#x27;rustc&#x27;</span></span><br><span class="line"> 61.0 MiB /  61.0 MiB (100 %)   2.3 MiB/s <span class="keyword">in</span> 26s ETA:  0s</span><br><span class="line">info: downloading component <span class="string">&#x27;rustfmt&#x27;</span></span><br><span class="line">info: installing component <span class="string">&#x27;cargo&#x27;</span></span><br><span class="line">info: using up to 500.0 MiB of RAM to unpack components</span><br><span class="line">info: installing component <span class="string">&#x27;clippy&#x27;</span></span><br><span class="line">info: installing component <span class="string">&#x27;rust-docs&#x27;</span></span><br><span class="line"> 14.7 MiB /  14.7 MiB (100 %)   5.8 MiB/s <span class="keyword">in</span>  1s ETA:  0s</span><br><span class="line">info: installing component <span class="string">&#x27;rust-std&#x27;</span></span><br><span class="line"> 23.3 MiB /  23.3 MiB (100 %)  13.0 MiB/s <span class="keyword">in</span>  3s ETA:  0s</span><br><span class="line">info: installing component <span class="string">&#x27;rustc&#x27;</span></span><br><span class="line"> 61.0 MiB /  61.0 MiB (100 %)  13.3 MiB/s <span class="keyword">in</span>  4s ETA:  0s</span><br><span class="line">info: installing component <span class="string">&#x27;rustfmt&#x27;</span></span><br><span class="line">info: default toolchain <span class="built_in">set</span> to <span class="string">&#x27;stable-x86_64-apple-darwin&#x27;</span></span><br><span class="line"></span><br><span class="line">  stable-x86_64-apple-darwin installed - rustc 1.50.0 (cb75ad5db 2021-02-10)</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">Rust is installed now. Great!</span><br><span class="line"></span><br><span class="line">To get started you need Cargo<span class="string">&#x27;s bin directory ($HOME/.cargo/bin) in your PATH</span></span><br><span class="line"><span class="string">environment variable. Next time you log in this will be done</span></span><br><span class="line"><span class="string">automatically.</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">To configure your current shell, run:</span></span><br><span class="line"><span class="string">source $HOME/.cargo/env</span></span><br></pre></td></tr></table></figure><p>以上这个过程是安装 rust 及其工具链并自动设置环境的过程，你可以根据提示输入 <code>source $HOME/.cargo/env</code> 让配置立即生效，也可以重新打开一个命令行让配置生效。</p><p><strong>验证是否安装成功：</strong></p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">rustc --version</span><br></pre></td></tr></table></figure><p>顺利的话，你会看到 <code>rustc x.y.z (版本的哈希码 yyyy-mm-dd)</code> 格式的输出。</p><p><strong>更新：</strong></p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">rustup update</span><br></pre></td></tr></table></figure><p><strong>卸载：</strong></p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">rustup self uninstall</span><br></pre></td></tr></table></figure><h2 id="本地文档"><a href="#本地文档" class="headerlink" title="本地文档"></a>本地文档</h2><p>安装工具在执行的过程中会在本地生成一份离线文档，你可以通过命令 <code>rustup doc</code> 在浏览器中打开它。但是我觉得一般用不到，我们在 vscode 中安装 Rust 插件有文档提示的功能。</p><h2 id="Hello-World"><a href="#Hello-World" class="headerlink" title="Hello,World!"></a>Hello,World!</h2><h3 id="vscode"><a href="#vscode" class="headerlink" title="vscode"></a>vscode</h3><p>安装 Rust 插件：</p><blockquote><p>rust-analyzer 也是一个很好的替代品，不过目前还不完善！</p></blockquote><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1719649428934.png" alt="紫升"></p><p>安装成功后将 <code>&quot;rust-client.rustupPath&quot;: &quot;$HOME/.cargo/bin/rustup&quot;</code> 添加到 <code>setting.json</code> 来修复 <a href="https://github.com/rust-lang/vscode-rust/issues/622">couldn’t start client Rust Language Server</a></p><p>为了能调试软件，再安装插件 CodeLLDB：</p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1719649496835.png" alt="紫升"></p><p>crates 是辅助开发者在使用 <code>Cargo.toml</code>时管理依赖的插件：</p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1719732946888.png" alt="紫升"></p><p>由于 Rust 的依赖管理工具 Cargo 使用 TOML 作为配置文件，所以需要下载 <code>Even Better TOML</code> 插件支持语法。</p><p>最后设置下保存时自动格式化：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// settings.json</span></span><br><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;[rust]&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</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><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="第一行代码"><a href="#第一行代码" class="headerlink" title="第一行代码"></a>第一行代码</h3><blockquote><p>注意：Rust 文件总是以 <code>rs</code> 结尾。如果在名字中使用了多个单词，那么你可以使用下划线来隔开它们。比如你最好使用 <code>hello_world.rs</code> 作为文件名而不是 helloworld.rs</p></blockquote><p>新建一个 <code>main.rs</code> 并写入以下代码：</p><figure class="highlight rs"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">fn</span> <span class="title function_">main</span>() &#123;</span><br><span class="line">  <span class="built_in">println!</span>(<span class="string">&quot;Hello,World!&quot;</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li>使用关键字 <code>fn</code> 定义了一个函数。（Go（<code>func</code>）、JS（<code>function</code>）、数学（<code>f</code>）），看起来 Rust 很省布料。</li><li>main 函数比较特殊：当你运行一个可执行 Rust 程序的时候，所有的代码都会从这个入口函数开始运行（Go 也是一样）</li><li>按照惯例，Rust 推荐把左花括号与函数声明置于同一行并以空格分隔。</li><li>我们调用了一个被叫作 <code>println!</code> 的宏：假如我们调用的是一个普通函数，那么这里会以去掉 <code>!</code> 符号的 <code>println</code> 来进行标记。</li><li>大部分 Rust 代码都会以分号来结尾。</li></ul><blockquote><p>所谓宏（Macro），是一种批量处理的称谓。计算机科学里的宏是一种抽象(Abstraction)，它根据一系列预定义的规则替换一定的文本模式。解释器或编译器在遇到宏时会自动进行这一模式替换。—— 可以简单理解为全局定义的函数、变量等</p></blockquote><p>然后在命令行输入 <code>rustc main.rs</code> 编译代码，编译成功后会生成可执行文件 <code>main</code>:</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">.</span><br><span class="line">├── main</span><br><span class="line">└── main.rs</span><br></pre></td></tr></table></figure><p>执行 <code>./main</code> 便会输出 <code>Hello,World!</code></p><blockquote><p>windows 下会生成 <code>main.exe</code></p></blockquote><blockquote><p>和 Ruby、Python 或 JavaScript 之类的动态语言不同，Rust、Go、Deno 都属于编译语言（JIT技术不在讨论范围），这意味这当你编译完 Rust 程序之后，便可以将可执行文件交付于其他人，并运行在没有安装 Rust 的环境中。如果你使用 Node 开发的应用交付给别人，别人必须要拥有 Node 开发环境。</p></blockquote><h2 id="Hello-Cargo"><a href="#Hello-Cargo" class="headerlink" title="Hello,Cargo!"></a>Hello,Cargo!</h2><p>和 Go 一样（go build），使用系统的编译工具（rustc）编译简单的程序足够了，但是它们无法满足规模大、协同开发人员多的项目。Go 有 gox、goxc、xgo，Rust 有 Cargo 来帮助我们管理简化项目依赖、代码构建这样的事情。</p><p>Cargo 是 Rust 工具链中内置的构建系统及包管理器。由于它可以处理众多诸如构建代码、下载编译依赖库等琐碎但重要的任务，绝大多数 Rust 用会选择它来管理自己的 Rust 项目（比如 Deno）。</p><p>前面我们的 helloworld 示例在 vscode 中，Rust 插件会报以下错误，可见 Cargo 是 Rust 的标配：</p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1719733630580.png" alt="紫升"></p><h2 id="使用-Cargo-创建项目"><a href="#使用-Cargo-创建项目" class="headerlink" title="使用 Cargo 创建项目"></a>使用 Cargo 创建项目</h2><p>使用 <code>cargo new hello_cargo</code> 新建项目：</p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1719733650171.png" alt="紫升"></p><p>Cargo 为我们生成了两个文件与一个目录：一个名为 <code>Cargo.toml</code> 的文件，以及一个名为 <code>main.rs</code> 的源代码文件<br>，该源代码被放置在 src 目录下。于此同时，Cargo 还会初始化一个新的 Git 仓库并升成默认的 <code>.gitignore</code> 文件。</p><h3 id="Cargo-toml"><a href="#Cargo-toml" class="headerlink" title="Cargo.toml"></a>Cargo.toml</h3><figure class="highlight toml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">[package]</span></span><br><span class="line"><span class="attr">name</span> = <span class="string">&quot;hello_cargo&quot;</span></span><br><span class="line"><span class="attr">version</span> = <span class="string">&quot;0.1.0&quot;</span></span><br><span class="line"><span class="attr">authors</span> = [<span class="string">&quot;youngjuning &lt;youngjuning@163.com&gt;&quot;</span>]</span><br><span class="line"><span class="attr">edition</span> = <span class="string">&quot;2018&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html</span></span><br><span class="line"></span><br><span class="line"><span class="section">[dependencies]</span></span><br></pre></td></tr></table></figure><ul><li><code>[package]</code>：是一个区域标签，它表明接下来的语句会被用于配置当前的程序包。<code>author</code> 这里的邮箱和名字是从 <code>~/.gitconfig</code> 中读取的</li><li><code>[dependencies]</code>：同样是一个区域标签，它表明随后的区域会被用来声明项目的依赖。</li></ul><blockquote><p>TOML 是一种旨在成为一个小规模、易于使用的语义化的配置文件格式，“TOML”这个名字是“Tom’s Obvious, Minimal Language（汤姆的浅显的、极简的语言）”的首字母略写词。“Tom”指它的作者 Tom Preston-Werner。</p></blockquote><p>按照惯例，Cargo 会默认把所有的源代码文件保存到 <code>src</code> 目录下，而项目根目录只被用来存放诸如 <code>README</code> 文档、许可声明、配置文件等与源代码无关的文件。使用 Cargo 可以帮助你合理并一致地组织自己的项目文件，从而使一切井井有条。</p><h3 id="使用-Cargo-构建和运行项目"><a href="#使用-Cargo-构建和运行项目" class="headerlink" title="使用 Cargo 构建和运行项目"></a>使用 Cargo 构建和运行项目</h3><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cargo build</span><br></pre></td></tr></table></figure><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1719733752129.png" alt="紫升"></p><p>与之前不同，这个命令会将可执行程序生成在路径 <code>target/debug/hello_cargo</code> 下：</p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1719734023741.png" alt="紫升"></p><p>首次使用命令 cargo build 构建的时候，会生成 <code>Cargo.lock</code>（类似于 yarn.lock），不要手动修改该文件，默认 <code>.gitignore</code> 中只忽略了 <code>target</code> 目录，我们从 <code>https://github.com/github/gitignore/blob/master/Rust.gitignore</code> 中复制最佳实践过来。</p><p>使用 <code>cargo run</code> 命令可以简单地以此完成编译和运行工作：</p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1719734443602.png" alt="紫升"></p><blockquote><p>我们还可以使用 <code>cargo check</code> 快速检查项目是否可以通过编译</p></blockquote><h3 id="以-Release-模式构建"><a href="#以-Release-模式构建" class="headerlink" title="以 Release 模式构建"></a>以 Release 模式构建</h3><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cargo build --release</span><br></pre></td></tr></table></figure><p>生成的可执行文件会被放置在 <code>target/release</code> 目录下，而不是之前的 <code>target/debug</code> 目录下。</p><p>假如你想要对代码的运行效率进行基准测试，请通过 <code>cargo run --release</code> 命令进行构建，并使用 <code>target/release</code> 目录下的可执行程序完成基准测试。</p><h3 id="crate"><a href="#crate" class="headerlink" title="crate"></a>crate</h3><p>在 Rust 中，我们把代码的集合称作包（crate）。crate 是 Rust 中最小的编译单元，package 是单个或多个 crate 的集合，crate 和 package 都可以被叫作包，因为单个 crate 也是一个 package，但 package 通常倾向于多个 crate 的组合。<a href="https://crates.io/">crates.io</a> 是 Rust 社区的 crate 代理。</p>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/395237906371/</id>
    <link href="https://blog.zisheng.pro/posts/395237906371/"/>
    <published>2023-04-19T10:32:26.000Z</published>
    <summary>Rust 是一门可以帮助你开发出高效率、高可靠性软件的编程语言。相较于 C/C++，Rust 力图同时提供强大的工程能力及良好的开发体验，在给予开发者控制底层能力（比如内存操作）的同时，避免传统语言带来的诸多麻烦。</summary>
    <title>Rust 极速入门教程</title>
    <updated>2026-09-18T08:15:28.632Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="Rust" scheme="https://blog.zisheng.pro/categories/Rust/"/>
    <category term="紫升的博客" scheme="https://blog.zisheng.pro/tags/%E7%B4%AB%E5%8D%87%E7%9A%84%E5%8D%9A%E5%AE%A2/"/>
    <category term="紫升" scheme="https://blog.zisheng.pro/tags/%E7%B4%AB%E5%8D%87/"/>
    <category term="Rust 语法" scheme="https://blog.zisheng.pro/tags/Rust-%E8%AF%AD%E6%B3%95/"/>
    <content>
      <![CDATA[<div class="note primary modern"><p>本文部分示例代码在 <a href="https://github.com/youngjuning/learn-rust">youngjuning&#x2F;learn-rust</a></p></div><p>语言之间都有着类似的核心特性，比如变量、基本类型、函数、注释和控制流程等概念。但是每个语言有都有自己的独有概念，本文便是记录了我在学习 Rust 语法过程中遇到的 Rust 独有概念。</p><h2 id="变量与可变性"><a href="#变量与可变性" class="headerlink" title="变量与可变性"></a>变量与可变性</h2><p>Rust 中的变量默认是不可变的。Rust 语言提供这一概念是为了能够让你安全且方便地写出复杂、甚至是并行的代码。当然，Rust 也提供了让你可以使用可变变量的方法。</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// main.rs</span></span><br><span class="line"><span class="keyword">fn</span> <span class="title function_">main</span>() &#123;</span><br><span class="line">  <span class="keyword">let</span> <span class="variable">x</span> = <span class="number">5</span>;</span><br><span class="line">  <span class="built_in">println!</span>(<span class="string">&quot;The value of x is: &#123;&#125;&quot;</span>, x);</span><br><span class="line">  x = <span class="number">6</span>;</span><br><span class="line">  <span class="built_in">println!</span>(<span class="string">&quot;The value of x is: &#123;&#125;&quot;</span>, x)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>x = 6</code> 属于重复赋值，所以执行 <code>cargo run</code> 编译是不会通过的：</p><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/7c69b2d8dd78470d917c730a5fe5a0d0~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/dcfbdcb5c80944149b31b331b8165aee~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><p>我们可以通过在声明的变量名称前添加 <code>mut</code> 关键字来使其可变。除了使变量的值可变，<code>mut</code> 还会向阅读代码的人暗示其他代码可能会改变这个变量的值。</p><h2 id="常量与变量的不同"><a href="#常量与变量的不同" class="headerlink" title="常量与变量的不同"></a>常量与变量的不同</h2><ol><li>我们不能使用 <code>mut</code> 关键字来修饰一个变量。常量不仅是默认不可变的，它还总是不可变的。</li><li>你需要使用 <code>const</code> 关键字而不是一个 <code>let</code> 关键字来声明一个常量。</li><li>在声明的同时，你必须显式地标注值的类型。</li><li>常量可以被声明在任何作用域中，甚至包括全局作用域。这在一个值需要被不同部分的代码共同引用时十分有用</li><li>你只能将一个常量绑定到一个常量表达式上，而无法将一个函数的返回值，或其他需要在运行时计算的值绑定到常量上。</li></ol><blockquote><p>我们约定俗称地使用以下划线分割的全大写字母来命名一个常量，并在数值中插入下划线来提高可读性：<code>const MAX_POINTS: u32 = 100_000;</code></p></blockquote><h2 id="隐藏"><a href="#隐藏" class="headerlink" title="隐藏"></a>隐藏</h2><p>在 Rust 中，新声明的变量可以覆盖掉旧的同名变量，我们把这一现象描述为：第一个变量被第二个变量隐藏（shadow）了。这意味着我们随后使用这个名称时，它指向的将会是第二个变量。我们可以重复使用 <code>let</code> 关键字并分配以相同的名称来不断地隐藏变量：</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">fn</span> <span class="title function_">main</span>() &#123;</span><br><span class="line">    <span class="keyword">let</span> <span class="variable">x</span> = <span class="number">5</span>;</span><br><span class="line">    <span class="keyword">let</span> <span class="variable">x</span> = x + <span class="number">1</span>;</span><br><span class="line">    <span class="keyword">let</span> <span class="variable">x</span> = x * <span class="number">2</span>;</span><br><span class="line">    <span class="built_in">println!</span>(<span class="string">&quot;The value of x is: &#123;&#125;&quot;</span>, x)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>如下图，我们可以看到 JavaScript 中并没有该特性：</p><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/42b566023d5f486996638d08cdbf31b3~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><p>隐藏机制和变量声明为 <code>mut</code> 的不同：</p><ol><li>如果不是在使用 <code>let</code> 重新为这个变量赋值，则会导致编译错误。通过使用 <code>let</code>，我们可以对这个值执行一系列的变换操作，并允许这个变量在操作完成后保持自己的不可变性。</li><li>由于重复使用 <code>let</code> 关键字会创建出新的变量，所以我们可以在复用变量名称的同时改变它的类型。</li></ol><h2 id="数据类型"><a href="#数据类型" class="headerlink" title="数据类型"></a>数据类型</h2><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/8b6b8078eff445709283898b757e9834~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><p>RUST 的编译器可以根据我们如何绑定、使用变量的值来自动推导出变量的类型。但在无法自动推导的场景，就必须显式地添加一个类型标注。</p><h3 id="标量类型（scalar）"><a href="#标量类型（scalar）" class="headerlink" title="标量类型（scalar）"></a>标量类型（scalar）</h3><p><strong>标量</strong>类型是单个值类型的统称。Rust 中内建了 4 种基础的标量类型：整数、浮点数、布尔值及字符。这类似于 JavaScript 中的基础类型。</p><h4 id="整数类型"><a href="#整数类型" class="headerlink" title="整数类型"></a>整数类型</h4><p>整数是指那些没有小数部分的数字。</p><table><thead><tr><th>长度</th><th>有符号</th><th>无符号</th></tr></thead><tbody><tr><td>8 bit</td><td>i8（-128 到 127）</td><td>u8（0 到 255）</td></tr><tr><td>16 bit</td><td>i16（-32768 到 32767）</td><td>u16（0 到 65535）</td></tr><tr><td>32 bit</td><td>i32(默认)</td><td>u32</td></tr><tr><td>64 bit</td><td>i64</td><td>u64</td></tr><tr><td>arch</td><td>isize</td><td>usize</td></tr></tbody></table><blockquote><p>Isize 和 usize 是特殊的整数类型，它们的长度取决于程序运行的目标平台。在 64 位架构上，它们就是 64 位的，而在 32 位架构上，它们就是 32 位的。</p></blockquote><blockquote><p>可以使用 <code>_</code> 作为分隔符以方便读数，比如 <code>1_000</code></p></blockquote><p>如何选择：</p><ul><li>如果拿不定注意，Rust 对于整数字面量默认推导类型 i32 通常就是一个很好的选择：它在大部分情况下都是运算速度最快的</li><li>较为特殊的两个整数类型 <code>usize</code> 和 <code>isize</code> 则主要用作某些集合的索引</li></ul><h4 id="浮点数类型"><a href="#浮点数类型" class="headerlink" title="浮点数类型"></a>浮点数类型</h4><ul><li>单精度浮点数（<code>f32</code>）</li><li>双精度浮点数（<code>f64</code>）(默认)</li></ul><blockquote><p>由于在现代 CPU 中双精度和单精度的运行效率相差无几，却拥有更高的精读，所以在 Rust 中，默认会将浮点数字面量的类型推到为 f64。</p></blockquote><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">fn</span> <span class="title function_">main</span>() &#123;</span><br><span class="line">  <span class="keyword">let</span> <span class="variable">x</span> = <span class="number">2.0</span>; <span class="comment">// f64</span></span><br><span class="line"></span><br><span class="line">  <span class="keyword">let</span> <span class="variable">y</span>: <span class="type">f32</span> = <span class="number">3.0</span>; <span class="comment">// f32</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="布尔值"><a href="#布尔值" class="headerlink" title="布尔值"></a>布尔值</h4><p>和 Go 语言一样，Rust 的布尔类型只拥有两个可能的值：<code>true</code>和 <code>false</code>。相较于 Javascript 中<code>false</code>、<code>0</code>、 <code>NaN</code>、<code>&#39;&#39;</code>、<code>null</code>、<code>undefined</code> 6 种都可以被转换成 <code>false</code> 真的是很节省脑容量。</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">fn</span> <span class="title function_">main</span>() &#123;</span><br><span class="line">  <span class="keyword">let</span> <span class="variable">t</span> = <span class="literal">true</span>;</span><br><span class="line">  <span class="keyword">let</span> <span class="variable">f</span>: <span class="type">bool</span> = <span class="literal">false</span> <span class="comment">// 附带了显式类型标注的语句</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="字符类型"><a href="#字符类型" class="headerlink" title="字符类型"></a>字符类型</h4><p>在 Rust 中，<code>char</code> 类型被用于描述语言中最基础的单个字符。需要注意的是，<code>char</code> 类型使用单引号指定，而不同于字符串使用双引号指定。</p><h3 id="复合类型（compound）"><a href="#复合类型（compound）" class="headerlink" title="复合类型（compound）"></a>复合类型（compound）</h3><p>复合类型可以将多个不同类型的值组合为一个类型。Rust 提供了两种内置的基础复合类型：元组（tuple）和数组（array）。</p><h4 id="元组类型"><a href="#元组类型" class="headerlink" title="元组类型"></a>元组类型</h4><ul><li>元组可以将其他不同类型的多个值进行组合</li><li>元组拥有一个固定的长度，我们无法在声明结束后增加或减少其中的元素数量</li></ul><p><strong>创建元组：</strong></p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">fn</span> <span class="title function_">main</span>() &#123;</span><br><span class="line">    <span class="keyword">let</span> <span class="variable">tup</span>: (<span class="type">i32</span>, <span class="type">f64</span>, <span class="type">u8</span>) = (<span class="number">500</span>, <span class="number">6.4</span>, <span class="number">1</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li>把一些列值使用逗号分隔后放置到一对圆括号中</li><li>元组每个位置的值都有一个类型，这些类型不需要是相同的。</li></ul><p>上面这段带来执行 <code>cargo run</code> 会有编译警告：</p><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/3db475c146fa43aabef6a2be17dbe1b4~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><p>意思是，如果你是故意声明一个 <code>unused variable</code>，那就给变量名加一个下划线前缀来忽略警告</p><p><strong>取值：</strong></p><p>1、解构（destructuring）：使用模式匹配来解构元组：</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">fn</span> <span class="title function_">main</span>() &#123;</span><br><span class="line">    <span class="keyword">let</span> <span class="variable">tup</span> = (<span class="number">500</span>, <span class="number">6.4</span>, <span class="number">1</span>);</span><br><span class="line">    <span class="keyword">let</span> (_x, y, _z) = tup;</span><br><span class="line">    <span class="built_in">println!</span>(<span class="string">&quot;The value of y is: &#123;&#125;&quot;</span>, y);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>2、通过索引并使用点号(<code>.</code>)来访问元组中的值：</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">fn</span> <span class="title function_">main</span>() &#123;</span><br><span class="line">  <span class="keyword">let</span> <span class="variable">x</span>: (<span class="type">i32</span>, <span class="type">f64</span>, <span class="type">u8</span>) = (<span class="number">500</span>, <span class="number">6.4</span>, <span class="number">1</span>);</span><br><span class="line">  <span class="keyword">let</span> <span class="variable">five_hundred</span> = x.<span class="number">0</span>;</span><br><span class="line">  <span class="keyword">let</span> <span class="variable">six_point_four</span> = x.<span class="number">1</span>;</span><br><span class="line">  <span class="keyword">let</span> <span class="variable">one</span> = x.<span class="number">2</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="数组类型"><a href="#数组类型" class="headerlink" title="数组类型"></a>数组类型</h4><h5 id="数组（array）"><a href="#数组（array）" class="headerlink" title="数组（array）"></a>数组（array）</h5><p>通常当你不想在栈上而不是堆上为数据分配空间时，或者想要确保总有固定数量的元素时，数组是一个非常有用的工具。</p><ul><li>与元组不同的是数组中的每一个元素都必须是相同的类型。</li><li>Rust 中的数组拥有固定的长度，一旦声明就再也不能随意更改大小，这与其他语言不同。</li></ul><p>在 Rust 中，你可以将以逗号分隔的值放置在一对方括号内来创建一个数组：</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">fn</span> <span class="title function_">main</span>() &#123;</span><br><span class="line">  <span class="keyword">let</span> <span class="variable">a</span> = [<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>, <span class="number">4</span>, <span class="number">5</span>]</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>为了写出数组的类型，你需要使用一对方括号，并在方括号中填写数组内所有元素的类型、一个分号以及数组内元素的数量，如下所示：</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> <span class="variable">a</span>: [<span class="type">i32</span>, <span class="number">5</span>] = [<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>, <span class="number">4</span>, <span class="number">5</span>]</span><br></pre></td></tr></table></figure><p><strong>创建一个含有相同元素的数组：</strong></p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> <span class="variable">a</span> = [<span class="number">3</span>; <span class="number">5</span>]</span><br></pre></td></tr></table></figure><p>以 <code>a</code> 命名的数组将会拥有 5 个元素，而这些元素全部拥有相同的初始值 3。这一写法等价于 <code>let a = [3, 3, 3, 3, 3]</code>。</p><p><strong>访问数组的元素：</strong></p><p>数组由一整块分配在栈上的内存组成，你可以通过索引来访问一个数组中的所有元素：</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">fn</span> <span class="title function_">main</span>() &#123;</span><br><span class="line"><span class="keyword">let</span> <span class="variable">a</span> = [<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>, <span class="number">4</span>, <span class="number">5</span>];</span><br><span class="line">  <span class="keyword">let</span> <span class="variable">first</span> = a[<span class="number">0</span>];</span><br><span class="line">  <span class="keyword">let</span> <span class="variable">second</span> = a[<span class="number">1</span>];</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>非法的数组访问</strong></p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">fn</span> <span class="title function_">main</span>() &#123;</span><br><span class="line">  <span class="keyword">let</span> <span class="variable">a</span> = [<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>, <span class="number">4</span>, <span class="number">5</span>];</span><br><span class="line">  <span class="keyword">let</span> <span class="variable">index</span> = <span class="number">10</span>;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">let</span> <span class="variable">element</span> = a[index];</span><br><span class="line">  <span class="built_in">println!</span>(<span class="string">&quot;The value of elements is: &#123;&#125;&quot;</span>, element)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在 <a href="https://play.rust-lang.org/">Rust Playground</a> 运行这段代码，编译器提示我们，这个操作将会在运行时崩溃。原因是索引越界（Java 常见错误），数组长度是 5，但是我们给的索引是 10。</p><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/af82e05277824ab285e1c35224c07996~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><blockquote><p>许多底层语言并没有类似的检查，一旦尝试使用非法索引，你就会访问到某块无效的内存空间（JavaScript 中则返回一个 <code>undefined</code>）</p></blockquote><h5 id="动态数组（vector）"><a href="#动态数组（vector）" class="headerlink" title="动态数组（vector）"></a>动态数组（vector）</h5><p>动态数组是一个类似于数组的集合结构，但它允许用户自由地调整数组长度。如果你不确定使用数组还是动态数组，那就先使用动态数组吧。动态数组属于高级语法，不在本文讨论范围，后期会出一期更深入的解析。</p><h2 id="函数（function）"><a href="#函数（function）" class="headerlink" title="函数（function）"></a>函数（function）</h2><ul><li><code>main</code> 函数是大部分程序开始的地方。</li><li>使用 <code>fn</code> 关键字来声明一个新的函数。</li><li>Rust 代码使用**蛇形命名法（snake case）**来作为规范函数和变量名称的风格。蛇形命名法只使用小写的字母进行命名，并以下划线分隔单词。</li></ul><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">fn</span> <span class="title function_">main</span> () &#123;</span><br><span class="line">    <span class="built_in">println!</span>(<span class="string">&quot;Hello world!&quot;</span>);</span><br><span class="line"></span><br><span class="line">    <span class="title function_ invoke__">another_function</span>();</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">fn</span> <span class="title function_">another_function</span>() &#123;</span><br><span class="line">    <span class="built_in">println!</span>(<span class="string">&quot;Another function&quot;</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="函数参数"><a href="#函数参数" class="headerlink" title="函数参数"></a>函数参数</h3><p>函数参数是一种特殊的变量，并被视作函数签名的一部分。当函数存在参数时，你需要在调用函数时为这些变量提供具体的值。</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">fn</span> <span class="title function_">main</span>() &#123;</span><br><span class="line">  <span class="title function_ invoke__">another_function</span>(<span class="number">5</span>);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">fn</span> <span class="title function_">another_function</span>(x: <span class="type">i32</span>) &#123;</span><br><span class="line">  <span class="built_in">println!</span>(<span class="string">&quot;The value of x is: &#123;&#125;&quot;</span>, x)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><blockquote><p>和 Go 语言一样，在 Rust 函数签名中，你必须显式地声明每个参数的类型。</p></blockquote><p>和其他编程语言一样，Rust 中也是使用 <code>,</code> 来分隔多个参数：</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">fn</span> <span class="title function_">main</span>() &#123;</span><br><span class="line">  <span class="title function_ invoke__">another_function</span>(<span class="number">5</span>, <span class="number">6</span>);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">fn</span> <span class="title function_">another_function</span>(x: <span class="type">i32</span>, y: <span class="type">i32</span>) &#123;</span><br><span class="line">  <span class="built_in">println!</span>(<span class="string">&quot;The value of x is: &#123;&#125;&quot;</span>, x);</span><br><span class="line">  <span class="built_in">println!</span>(<span class="string">&quot;The value of y is: &#123;&#125;&quot;</span>, y);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="函数体中的语句和表达式"><a href="#函数体中的语句和表达式" class="headerlink" title="函数体中的语句和表达式"></a>函数体中的语句和表达式</h3><p>由于 Rust 是一门基于表达式的语言，所以它将语句（statement）与表达式（expression）区别为两个不同的概念。语句指那些执行操作但不返回值的指令，而表达式则是指会进行计算并产生一个值作为结果的指令。这个其他语言不太一样：</p><p>在 C 语言、Ruby、JavaScript 中 <code>var x = y = 6</code> 这种赋值语句会返回所赋的值，但是 Rust 中是行不通的。</p><p>值得注意的是，下面代码中的 <code>x+1</code> 是表达式。</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">fn</span> <span class="title function_">main</span>() &#123;</span><br><span class="line">  <span class="keyword">let</span> <span class="variable">x</span> = <span class="number">5</span>;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">let</span> <span class="variable">y</span> = &#123;</span><br><span class="line">    <span class="keyword">let</span> <span class="variable">x</span> = <span class="number">3</span>;</span><br><span class="line"></span><br><span class="line">    x + <span class="number">1</span></span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="built_in">println!</span>(<span class="string">&quot;The value of y is: &#123;&#125;&quot;</span>, y);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>但是如果我们在表达式末尾加上了分号，这一段代码就变成了语句而不会返回任何值。</p><h3 id="函数的返回值"><a href="#函数的返回值" class="headerlink" title="函数的返回值"></a>函数的返回值</h3><ul><li>在 Rust 中，函数的返回值等同于函数体最后一个表达式的值。</li><li>和其他编程语言不同的是，Rust 中函数的 <code>return</code> 语句不是必需的，只是一个用来提前返回的关键字。而且大多数函数都隐式地返回了最后的表达式。</li><li>需要在瘦箭头（<code>-&gt;</code>）的后面声明它的类型。</li></ul><p>函数体最后一行必须是表达式，不要加分号，下面是反例：</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">fn</span> <span class="title function_">main</span>() &#123;</span><br><span class="line">  <span class="keyword">let</span> <span class="variable">x</span> = <span class="title function_ invoke__">plus_one</span>(<span class="number">5</span>);</span><br><span class="line"></span><br><span class="line">  <span class="built_in">println!</span>(<span class="string">&quot;The value of x is: &#123;&#125;&quot;</span>, x);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">fn</span> <span class="title function_">plus_one</span>(x: <span class="type">i32</span>) <span class="punctuation">-&gt;</span> <span class="type">i32</span> &#123;</span><br><span class="line">  x + <span class="number">1</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>尝试编译这段代码会产生如下错误信息：</p><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/68a309eddc6244e4a5b45bcd39553156~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><p>可以看出，由于 <code>x +1;</code> 是表达式没有返回值，函数体隐式地返回了空元祖（<code>()</code>），进而导致编译时 <code>mismatched types</code> 错误。编译器给的建议是删除 <code>x +1;</code> 语句的分号。</p><h2 id="控制流"><a href="#控制流" class="headerlink" title="控制流"></a>控制流</h2><h3 id="if-表达式"><a href="#if-表达式" class="headerlink" title="if 表达式"></a>if 表达式</h3><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">fn</span> <span class="title function_">main</span>() &#123;</span><br><span class="line">    <span class="keyword">let</span> <span class="variable">number</span> = <span class="number">3</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> number &lt; <span class="number">5</span> &#123;</span><br><span class="line">        <span class="built_in">println!</span>(<span class="string">&quot;condition was true&quot;</span>);</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        <span class="built_in">println!</span>(<span class="string">&quot;condition was false&quot;</span>)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li>和 Go 一样，判断条件不需要圆括号包裹。</li><li>代码中的条件表达式必须产生一个 <code>bool</code> 类型的值，否则就会触发编译错误。</li><li>与 Ruby 或 JavaScript 等语言不同，Rust 没有隐式转换</li></ul><h4 id="在-let-语句中使用-if"><a href="#在-let-语句中使用-if" class="headerlink" title="在 let 语句中使用 if"></a>在 <code>let</code> 语句中使用 <code>if</code></h4><p>由于<code> if</code> 是一个表达式，所以我们可以再 <code>let</code> 语句的右侧使用它来生成一个值。</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">fn</span> <span class="title function_">main</span>() &#123;</span><br><span class="line">  <span class="keyword">let</span> <span class="variable">condition</span> = <span class="literal">true</span>;</span><br><span class="line">  <span class="keyword">let</span> <span class="variable">number</span> = <span class="keyword">if</span> condition &#123;</span><br><span class="line">    <span class="number">5</span></span><br><span class="line">  &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    <span class="number">6</span></span><br><span class="line">  &#125;;</span><br><span class="line"></span><br><span class="line">  <span class="built_in">println!</span>(<span class="string">&quot;The value of number is: &#123;&#125;&quot;</span>, number);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li>代码块输出的值就是其中最后一个表达式的值，另外，数字本身也可以作为一个表达式使用。</li><li>整个 <code>if</code> 表达式的值取决于究竟是哪一个代码块得到了执行。这意味着，所有 <code>if</code> 分支可能返回的值都必须是一种类型的。</li></ul><h2 id="使用循环重复执行代码"><a href="#使用循环重复执行代码" class="headerlink" title="使用循环重复执行代码"></a>使用循环重复执行代码</h2><p>Rust 提供了 3 种循环：<code>loop</code>、<code>while</code>和<code>for</code></p><h3 id="loop-循环"><a href="#loop-循环" class="headerlink" title="loop 循环"></a>loop 循环</h3><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">fn</span> <span class="title function_">main</span>() &#123;</span><br><span class="line">  <span class="keyword">loop</span> &#123;</span><br><span class="line">    <span class="built_in">println!</span>(<span class="string">&quot;again!&quot;</span>)</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>运行这段程序时，除非我们手动强制退出程序，否则 <code>again!</code> 会被反复地输出到屏幕上。</p><p><strong>从 loop 循环中返回值</strong></p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">fn</span> <span class="title function_">main</span>() &#123;</span><br><span class="line">  <span class="keyword">let</span> <span class="keyword">mut </span><span class="variable">counter</span> = <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">let</span> <span class="variable">result</span> = <span class="keyword">loop</span> &#123;</span><br><span class="line">    counter += <span class="number">1</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> counter == <span class="number">10</span> &#123;</span><br><span class="line">      <span class="keyword">break</span> counter * <span class="number">2</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="built_in">println!</span>(<span class="string">&quot;The result is &#123;&#125;&quot;</span>, result);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>上面的代码中我们将需要返回的值添加到 break 表达式后面，也就是我们用来终止循环的表达式后面。</p><h3 id="while-条件循环"><a href="#while-条件循环" class="headerlink" title="while 条件循环"></a>while 条件循环</h3><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">fn</span> <span class="title function_">main</span>() &#123;</span><br><span class="line">  <span class="keyword">let</span> <span class="keyword">mut </span><span class="variable">number</span> = <span class="number">3</span>;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">while</span> number !=<span class="number">0</span> &#123;</span><br><span class="line">    <span class="built_in">println!</span>(<span class="string">&quot;&#123;&#125;!&quot;</span>, number);</span><br><span class="line"></span><br><span class="line">    number = number - <span class="number">1</span>;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="built_in">println!</span>(<span class="string">&quot;LOFTOFF!!!&quot;</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>while</code> 循环的模式是会在每次执行循环体之前都判断一次条件，假如条件为真则执行代码片段，加入条件为假或在执行过程中碰到 <code>break</code> 就退出当前循环。这种模式可以通过 <code>loop</code>、<code>if</code>、<code>else</code> 及 <code>break</code> 关键字的组合使用来实现。</p><h3 id="使用-for-循环遍历集合"><a href="#使用-for-循环遍历集合" class="headerlink" title="使用 for 循环遍历集合"></a>使用 for 循环遍历集合</h3><p>我们可以使用 <code>for</code> 循环来遍历集合中的每一个元素。</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">fn</span> <span class="title function_">main</span>() &#123;</span><br><span class="line">  <span class="keyword">let</span> <span class="variable">a</span> = [<span class="number">10</span>, <span class="number">20</span>, <span class="number">30</span>, <span class="number">40</span>, <span class="number">50</span>];</span><br><span class="line"></span><br><span class="line">  <span class="keyword">for</span> <span class="variable">element</span> <span class="keyword">in</span> a.<span class="title function_ invoke__">iter</span>() &#123;</span><br><span class="line">    <span class="built_in">println!</span>(<span class="string">&quot;The value is: &#123;&#125;&quot;</span>, element)</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>for</code> 循环的安全性和简捷性使它成为了 Rust 中最为常用的循环结构。大部分的 Rust 开发者也会选择使用 for 循环。</p><p>下面的例子中，我们配合标准库中的 Range 来实现打印 <code>1</code> 到<code>4</code>:</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">fn</span> <span class="title function_">main</span>() &#123;</span><br><span class="line">  <span class="keyword">for</span> <span class="variable">number</span> <span class="keyword">in</span> (<span class="number">1</span>..<span class="number">4</span>).<span class="title function_ invoke__">rev</span>() &#123;</span><br><span class="line">    <span class="built_in">println!</span>(<span class="string">&quot;&#123;&#125;!&quot;</span>, number);</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="built_in">println!</span>(<span class="string">&quot;LIFTOFF!!!&quot;</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/38a92886ba96/</id>
    <link href="https://blog.zisheng.pro/posts/38a92886ba96/"/>
    <published>2023-04-19T10:19:08.000Z</published>
    <summary>语言之间都有着类似的核心特性，比如变量、基本类型、函数、注释和控制流程等概念。但是每个语言有都有自己的独有概念，本文便是记录了我在学习 Rust 语法过程中遇到的 Rust 独有概念。</summary>
    <title>Rust 语法学习笔记</title>
    <updated>2026-09-18T08:15:28.632Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="Rust" scheme="https://blog.zisheng.pro/categories/Rust/"/>
    <category term="紫升的博客" scheme="https://blog.zisheng.pro/tags/%E7%B4%AB%E5%8D%87%E7%9A%84%E5%8D%9A%E5%AE%A2/"/>
    <category term="紫升" scheme="https://blog.zisheng.pro/tags/%E7%B4%AB%E5%8D%87/"/>
    <category term="Rust" scheme="https://blog.zisheng.pro/tags/Rust/"/>
    <content>
      <![CDATA[<div class="note primary modern"><p>猜数游戏代码在 <a href="https://github.com/youngjuning/learn-rust/tree/master/guessing_game">guessing_game</a></p></div><p>为了方便大家学习，需要运行（<code>cargo run</code>）看效果的地方，我都截了GIF图。喜欢的话可以给紫升个👍🏻鼓励一下。</p><h2 id="环境"><a href="#环境" class="headerlink" title="环境"></a>环境</h2><blockquote><ul><li><p>rustc：<code>1.50.0 (cb75ad5db 2021-02-10)</code></p></li><li><p>cargo：<code>1.50.0 (f04e7fab7 2021-02-04)</code></p></li><li><p><code>rand</code> crate：<code>0.8.3</code></p></li><li><p>vscode plugins</p><ul><li>Better TOML</li><li>CodeLLDB</li><li>crates</li><li>rust-analyzer</li></ul></li><li><p>oh-my-zsh theme：ys</p></li><li><p>截图软件：<a href="https://getkap.co/">Kap</a></p></li></ul></blockquote><h2 id="处理用户输入"><a href="#处理用户输入" class="headerlink" title="处理用户输入"></a>处理用户输入</h2><p>猜数游戏第一部分会请求用户进行输入，上代码：</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">use</span> std::io;</span><br><span class="line"></span><br><span class="line"><span class="keyword">fn</span> <span class="title function_">main</span>() &#123;</span><br><span class="line">    <span class="built_in">println!</span>(<span class="string">&quot;Guess the number!&quot;</span>);</span><br><span class="line"></span><br><span class="line">    <span class="built_in">println!</span>(<span class="string">&quot;Please input your guess.&quot;</span>);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">let</span> <span class="keyword">mut </span><span class="variable">guess</span> = <span class="type">String</span>::<span class="title function_ invoke__">new</span>();</span><br><span class="line"></span><br><span class="line">    io::<span class="title function_ invoke__">stdin</span>()</span><br><span class="line">        .<span class="title function_ invoke__">read_line</span>(&amp;<span class="keyword">mut</span> guess)</span><br><span class="line">        .<span class="title function_ invoke__">expect</span>(<span class="string">&quot;Failded to read line&quot;</span>);</span><br><span class="line"></span><br><span class="line">    <span class="built_in">println!</span>(<span class="string">&quot;You guessed: &#123;&#125;&quot;</span>, guess);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="声明导入"><a href="#声明导入" class="headerlink" title="声明导入"></a>声明导入</h3><ul><li><code>use std::io;</code> 语句：把标准库（<a href="https://github.com/rust-lang/rust/tree/master/library/std/src/io">std</a>）中的 <code>io</code> 模块引入当前的作用域中。</li><li>Rust 默认会将预导入（prelude）模块内的条目自动引入每一段程序的作用域中，它包含了一小部分相当常用的类型。如果你需要的模块不在预导入模块内，那么我们就必须使用 <code>use</code> 语句来显式地进行导入声明。</li><li><code>std::id</code> 库包含了许多有用的功能，我们可以使用它来获得用户的输入数据。</li></ul><h3 id="使用变量来存储值"><a href="#使用变量来存储值" class="headerlink" title="使用变量来存储值"></a>使用变量来存储值</h3><ul><li><code>let mut guess</code>：创建出一个名为 <code>guess</code> 的可变变量，变量默认是不可变的，<code>mut</code> 关键字表示可变（mutable）变量，关于变量与可变性的概念在<a href="https://juejin.cn/post/6929080722565267469">两个番茄钟的Rust 语法学习笔记</a>中有介绍。</li><li><code>String::new</code>：返回一个新的 <code>String</code> 实例<ul><li><code>::</code> 语法表明 <code>new</code> 是 <code>String</code> 类型的一个关联函数（associated function）——其他语言中被称为静态方法（static method）</li><li><code>String</code>是标准库中的一个字符串类型，它在内部使用了 UTF-8 格式的编码并可以按照需求扩展自己的大小。</li></ul></li><li><code>io::stdin</code>：返回<code>std::io::Stdin</code> 的实例，它被用作句柄来处理终端中的标准输入。</li><li><code>.read_line</code> 方法用来获得用户输入。<ul><li><code>read_line</code> 的参数 <code>&amp;mut guess</code> 表示引用一个可变变量。</li><li><code>&amp;</code> 表示当前的参数是一个引用，这在 Go 中也是存在的。因为这些系统级编程语言大多都提供了操作内存的权限，而 JavaScript 是不给用户这些功能的。</li></ul></li></ul><blockquote><p>句柄：在上世纪80年代的操作系统（如 Mac OS 句柄 和Windows）的内存管理中，句柄被广泛应用。Unix系统的文件描述符基本上也属于句柄。和其它桌面环境一样，Windows API大量使用句柄来标识系统中的对象)，并创建操作系统与用户空间之间的通信渠道。例如，桌面上的一个窗体由一个<code>HWND</code>类型的句柄来标识。</p></blockquote><h3 id="处理可能失败的情况"><a href="#处理可能失败的情况" class="headerlink" title="处理可能失败的情况"></a>处理可能失败的情况</h3><p>我们先来看下 Go 里边的错误处理方式：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">  result,err := getResult();</span><br><span class="line">  <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">    <span class="built_in">panic</span>(err)</span><br><span class="line">  &#125;</span><br><span class="line">  fmt.Printf(<span class="string">&quot;The result is %s&quot;</span>, result);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">getResult</span><span class="params">()</span></span> <span class="type">string</span>,(<span class="type">string</span>, err <span class="type">error</span>) &#123;</span><br><span class="line">  <span class="comment">// some codes</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>一般，一个函数可能报错的话都会在返回 result 的同时返回一个 <code>error</code> 。这并不是 Go 专属的，Rust 语言中也是如此，只不过实现方式不一样。</p><p>前面代码中，<code>read_line</code> 会将用户输入的内容存储到我们传入的字符串中，但与此同时，它还会返回一个 <code>io::Result</code> 值。在 Rust 标准库中，你可以找到许多以 <code>Result</code> 命名的类型，它们通常是各个子模块中 <code>Result</code> 泛型的特定版本，比如这里的<code>io::Result</code>。</p><blockquote><p><code>Result</code> 是一个枚举类型。枚举类型由一系列固定的值组合而成，这些值被称作枚举的变体。（众所周知，Javascript 没有枚举类型）</p></blockquote><p>对于 <code>Result</code> 而言，它拥有 <code>Ok</code> 和 <code>Err</code> 两个变体。其中的 <code>Ok</code> 变体表明当前的操作执行成功，并附带代码产生的结果值。相应地，<code>Err</code> 变体则表明当前的操作执行失败，并附带引发失败的具体原因。</p><p><code>expect</code> 是 <code>Result</code> 类型的值的一系列方法中的一个。假如 <code>io::Result</code> 实例的值是 <code>Err</code>，那么 <code>expect</code> 方法就会中断当前的程序，并将传入的字符串参数显示出来。</p><blockquote><p><code>read_line</code> 方法有可能因为底层操作系统的错误而返回一个 <code>Err</code> 结果。</p><p>相应地，假如 <code>io::Result</code> 实例的值是 <code>Ok</code>，那么 <code>expect</code> 就会提取出 <code>Ok</code> 中附带的值，并将它作为结果返回给用户。在我们的例子中，这个值就是用户输入内容的字节数。</p></blockquote><p>即便我们没有在语句末尾调用 <code>expect</code>，这段程序也能够编译通过，但你会在编译过程中看到如下所示的警告信息。这一点和 Go 也基本一致。</p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/41b7da00e7764953a833d3aeb65b2142~tplv-k3u1fbpfcp-zoom-1.image" style="zoom:50%;" /><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/fd34722caa964f5f801a77768de68fea~tplv-k3u1fbpfcp-zoom-1.image" style="zoom:50%;" /><p>Rust 编译器提醒我们 <code>read_line</code> 方法返回的 <code>Result</code> 值还没有被处理，这通常以为着我们的程序没有对潜在的错误进行处理。</p><p>消除警告最正确的方法当然是编写对应的错误处理代码，为了简单起见，我们在这里选择使用 <code>expect</code> 方法，它会让程序在出现错误时直接终止运行并退出。</p><h3 id="通过-println-中的占位符输出对应的值"><a href="#通过-println-中的占位符输出对应的值" class="headerlink" title="通过 println! 中的占位符输出对应的值"></a>通过 <code>println!</code> 中的占位符输出对应的值</h3><p><code>println!(&quot;You guessed: &#123;&#125;&quot;, guess);</code> 可以将我们存储的用户输入打印出来。这段宏调用的第一个参数是用于格式化的字符串，而字符串中的那些花括号<code>&#123;&#125;</code>则是一个占位符。</p><blockquote><p>举一反三：<code>&#123;&#125;</code> 作为占位符不是 Rust 特有的，我所知的 Shell 中也有类似的应用，比如 <code>ls | xargs -I &#123;&#125; tar zcvfm &#123;&#125;.tar.bz &#123;&#125;</code> 这段脚本的意思就是将 <code>ls</code> 输出的值逐个通过管道传入后面的命令，<code>&#123;&#125;</code> 便是接收管道传的值的占位符。顺便一提，这段脚本我在 <a href="https://github.com/youngjuning/tuya-panel-demo/blob/3b66bbaa45560fa247a76adc2ab06f5ca477fcc5/.github/workflows/release.yml#L20">tuya-panel-demo</a> 有使用。这里挖个坑，后续会出一个 Shell 脚本的系列教程。</p></blockquote><h3 id="尝试运行代码"><a href="#尝试运行代码" class="headerlink" title="尝试运行代码"></a>尝试运行代码</h3><p>现在，让我们借助 <code>cargo run</code> 命令来尝试运行一下这段代码：</p><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/74adfa100e0147b083ae72c782928916~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><h2 id="生成一个保密数字"><a href="#生成一个保密数字" class="headerlink" title="生成一个保密数字"></a>生成一个保密数字</h2><p>解决了接收用户输入的数字之后，我们需要生成一个保密数字来供玩家进行猜测。为了保证一定的可玩性，并使每局游戏都有不同的体验，这个生成的保密数字将会是随机的。</p><h3 id="引入-rand-包"><a href="#引入-rand-包" class="headerlink" title="引入 rand 包"></a>引入 rand 包</h3><p>Rust 团队并没有把类似的随机数字生成功能内置到标准库中，而是选择将它作为 rand 包（rand crate）提供给用户。</p><blockquote><p>注意：Rust 中包（crate）代表了一系列源代码文件的集合。我们当前构建的项目是一个用于生成可执行程序的二进制包（binary crate），而我们引入<code>rand</code>包则是一个用于复用功能的库包（libray crate，代码包）。</p></blockquote><p>要使用第三方 <code>crate</code> ，我们需要在 <code>Cargo.toml</code> 文件中加入依赖：</p><figure class="highlight toml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">...</span><br><span class="line"><span class="section">[dependencies]</span></span><br><span class="line"></span><br><span class="line"><span class="attr">rand</span> = <span class="string">&quot;0.3.14&quot;</span></span><br></pre></td></tr></table></figure><p>在 <code>Cargo.toml</code> 文件中，从一个标题到下一个标题之间的所有内容都属于同一区域。这里的<code>[denpendencies]</code>区域被用来声明项目中需要用到的全部依赖包及其版本号。</p><blockquote><p>书中的版本号比较老，vscode crates 插件提示了版本❌</p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d11abe6e85e6441ca5469fe4cf70dcc3~tplv-k3u1fbpfcp-zoom-1.image" style="zoom:50%;" /><p>我们点击任意版本即可选择该版本作为我们依赖的版本，点击圆括号内的 docs 便可跳转到库文档。</p></blockquote><p>我们先不修改任何代码，直接重新构建这个项目：</p><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5fe9b017f6e8431bb31df50335e7cd67~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><p>现在，我们的程序有了一个外部依赖，Cargo 可以从注册表（registry）中获取所有可用库的最新版本信息，而这些信息通常是从<a href="https://cartes.io/">cartes.io</a> 上拷贝过来的。</p><blockquote><p>cates.io 在 Rust 生态中是人们用于分享各种各样开源 Rust 项目的网站。</p></blockquote><p>现在，如果你没有做出任何改变，立即重新运行 <code>cargo build</code>，那么只会有 Finished 提示。Cargo 会自动分析当前已经下载或编译过的内容，并跳过无需重复的步骤。</p><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/910a4092bb524a788621f193cde3618f~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><h3 id="生成一个随机数"><a href="#生成一个随机数" class="headerlink" title="生成一个随机数"></a>生成一个随机数</h3><figure class="highlight diff"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="addition">+ use rand::Rng;</span></span><br><span class="line">use std::io;</span><br><span class="line"></span><br><span class="line">fn main() &#123;</span><br><span class="line">    println!(&quot;Guess the number!&quot;);</span><br><span class="line"></span><br><span class="line"><span class="addition">+   let secret_number = rand::thread_rng().gen_range(1..101);</span></span><br><span class="line"></span><br><span class="line"><span class="addition">+   println!(&quot;The secret number is: &#123;&#125;&quot;, secret_number);</span></span><br><span class="line"></span><br><span class="line">    println!(&quot;Please input your guess.&quot;);</span><br><span class="line"></span><br><span class="line">    let mut guess = String::new();</span><br><span class="line"></span><br><span class="line">    io::stdin()</span><br><span class="line">        .read_line(&amp;mut guess)</span><br><span class="line">        .expect(&quot;Failded to read line&quot;);</span><br><span class="line"></span><br><span class="line">    println!(&quot;You guessed: &#123;&#125;&quot;, guess);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li>我们额外增加了一行 <code>use</code> 语句：<code>use rand::Rng</code>。这里的 Rng 是一个 trait（特征），它定义了随机数生成器需要实现的方法集合。为了使用这些方法，我们需要显式地将它引入当前的作用域中。</li><li><code>rand::thread_rng()</code> 会返回一个特定的随机数生成器。随后我们调用这个生成器的<code>gen_range</code>方法。</li><li><code>gen_range</code> 方法是在刚刚引入作用域的 <code>Rng</code> trait 中定义的，它接收一个 Range 类型（<code>1..100</code>）的值作为参数，并生成一个范围在两者之前的随机数。</li></ul><blockquote><p>提醒：书中的 <code>gen_range</code> 接收的是两个参数，读到时不要奇怪，版本不一样而已。</p></blockquote><blockquote><p> 值得指出的是，<code>gen_range</code>生成的随机数空间包含下限但不包含上限。——做人呢，也是这样，要有底线，但不要给自己设上限。</p></blockquote><p>使用第三方包，不可避免要频繁查看文档。你可以去官方文档看，这没什么问题。但是你也可以借助<code>cargo doc --open</code> 命令在本地构建一份有关所有依赖的文档，并自动地在浏览器中将文档打来来供你查阅：</p><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5bc83c1f8feb432884ceaef1794af0a0~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><p>上面代码中，我们将保密数字打印出来，只是为了调试，之后会删掉这段代码。</p><h2 id="比较猜测数字与保密数字"><a href="#比较猜测数字与保密数字" class="headerlink" title="比较猜测数字与保密数字"></a>比较猜测数字与保密数字</h2><p>现在，我们有了一个随机生成的保密数字，还有一个用户输入的猜测数字。接下来我们将比较这两个数字。</p><figure class="highlight diff"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line">use rand::Rng;</span><br><span class="line"><span class="addition">+ use std::cmp::Ordering;</span></span><br><span class="line">use std::io;</span><br><span class="line"></span><br><span class="line">fn main() &#123;</span><br><span class="line">    println!(&quot;Guess the number!&quot;);</span><br><span class="line"></span><br><span class="line">    let secret_number = rand::thread_rng().gen_range(1..101);</span><br><span class="line"></span><br><span class="line">    println!(&quot;Please input your guess.&quot;);</span><br><span class="line"></span><br><span class="line">    let mut guess = String::new();</span><br><span class="line"></span><br><span class="line">    io::stdin()</span><br><span class="line">        .read_line(&amp;mut guess)</span><br><span class="line">        .expect(&quot;Failded to read line&quot;);</span><br><span class="line"></span><br><span class="line">    println!(&quot;You guessed: &#123;&#125;&quot;, guess);</span><br><span class="line"></span><br><span class="line"><span class="addition">+   match guess.cmp(&amp;secret_number) &#123;</span></span><br><span class="line"><span class="addition">+       Ordering::Less =&gt; println!(&quot;Too small!&quot;),</span></span><br><span class="line"><span class="addition">+       Ordering::Greater =&gt; println!(&quot;Too big!&quot;),</span></span><br><span class="line"><span class="addition">+       Ordering::Equal =&gt; println!(&quot;You win!&quot;),</span></span><br><span class="line"><span class="addition">+   &#125;</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><ul><li>我们从标准库中引入了 <code>std::cmp::Ordering</code> 类型。与 Result 相同，Ordering 也是一个枚举类型，它拥有<code>Less</code>、<code>Greater</code>、<code>Equal</code> 这3个变体。（cmp 是 compare 的缩写）</li><li><code>match</code> 表达式由由数个分支（arm）组成，每个分支都包含一个用于匹配的模式（pattern），以及匹配成功后要执行的相应代码。</li></ul><blockquote><p>Rust 中的<code>match</code>结构及模式是一类非常强大的工具，它们提供了依据不同条件执行不同代码的能力（类似其他语言的<code>switch</code>），并能够确保你不会遗漏任何分支条件。</p></blockquote><p>上面这段代码，目前是无法通过编译的：</p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/bee46fa3af2c4fd08bbef9df941af3bc~tplv-k3u1fbpfcp-zoom-1.image" style="zoom:50%;" /><p>错误原因是因为我们保存的 <code>guess</code> 变量类型是 String，保密数字是 integer 类型，不同的类型无法匹配（静态语言不存在隐式类型转换）。</p><p>为了正常进行比较操作，我们需要将程序中读取的输入从 String 类型转换为数值类型：</p><figure class="highlight diff"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line">use rand::Rng;</span><br><span class="line">use std::cmp::Ordering;</span><br><span class="line">use std::io;</span><br><span class="line"></span><br><span class="line">fn main() &#123;</span><br><span class="line">    println!(&quot;Guess the number!&quot;);</span><br><span class="line"></span><br><span class="line">    let secret_number = rand::thread_rng().gen_range(1..101);</span><br><span class="line"></span><br><span class="line">    println!(&quot;Please input your guess.&quot;);</span><br><span class="line"></span><br><span class="line">    let mut guess = String::new();</span><br><span class="line"></span><br><span class="line">    io::stdin()</span><br><span class="line">        .read_line(&amp;mut guess)</span><br><span class="line">        .expect(&quot;Failded to read line&quot;);</span><br><span class="line"></span><br><span class="line"><span class="addition">+   let guess: i32 = guess.trim().parse().expect(&quot;Please type a number&quot;);</span></span><br><span class="line"></span><br><span class="line">    println!(&quot;You guessed: &#123;&#125;&quot;, guess);</span><br><span class="line"></span><br><span class="line">    match guess.cmp(&amp;secret_number) &#123;</span><br><span class="line">        Ordering::Less =&gt; println!(&quot;Too small!&quot;),</span><br><span class="line">        Ordering::Greater =&gt; println!(&quot;Too big!&quot;),</span><br><span class="line">        Ordering::Equal =&gt; println!(&quot;You win!&quot;),</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li><p>我们这里创建一个新的不可变变量<code>guess</code>，虽然和前面的同名，但这是有效的，Rust 允许使用同名的新变量 guess 来隐藏（shadow）旧变量的值。这一特性通常被用在需要转换值类型的场景中，它在本例中允许我们重用 guess 这个变量名，而无须创造出 guess_str 之类不同的名字。</p></li><li><p><code>guess.trim()</code> 是为了删除首尾所有空白字符。</p></li><li><p>字符串的 <code>parse</code> 方法会尝试将当前的字符串解析为某种数值。由于这个方法可以处理不同的数值类型，所以需要我们通过语句<code>let guess: i32</code> 来显式地声明我们需要的数值类型。</p></li></ul><p>现在我们重新 Run 一下我们的程序：</p><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/7a2e0066cbbb4948aacf17ad5ea8b24b~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><p>这个游戏已经大体成型了，但玩家只能做出一次猜测，这显然是不够的，接下来，我们会加入一个循环来完善这个游戏。</p><h2 id="使用循环来实现多次猜测"><a href="#使用循环来实现多次猜测" class="headerlink" title="使用循环来实现多次猜测"></a>使用循环来实现多次猜测</h2><figure class="highlight diff"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br></pre></td><td class="code"><pre><span class="line">use rand::Rng;</span><br><span class="line">use std::cmp::Ordering;</span><br><span class="line">use std::io;</span><br><span class="line"></span><br><span class="line">fn main() &#123;</span><br><span class="line">    println!(&quot;Guess the number!&quot;);</span><br><span class="line"></span><br><span class="line">    // `..` 语法是标准库中提供的 Range</span><br><span class="line">    let secret_number = rand::thread_rng().gen_range(1..101);</span><br><span class="line"><span class="addition">+   loop &#123;</span></span><br><span class="line">        println!(&quot;Please input your guess.&quot;);</span><br><span class="line"></span><br><span class="line">        let mut guess = String::new();</span><br><span class="line"></span><br><span class="line">        io::stdin()</span><br><span class="line">            .read_line(&amp;mut guess)</span><br><span class="line">            .expect(&quot;Failded to read line&quot;);</span><br><span class="line"></span><br><span class="line">        let guess: i32 = guess.trim().parse().expect(&quot;Please type a number&quot;);</span><br><span class="line"></span><br><span class="line">        println!(&quot;You guessed: &#123;&#125;&quot;, guess);</span><br><span class="line"></span><br><span class="line">        match guess.cmp(&amp;secret_number) &#123;</span><br><span class="line">            Ordering::Less =&gt; println!(&quot;Too small!&quot;),</span><br><span class="line">            Ordering::Greater =&gt; println!(&quot;Too big!&quot;),</span><br><span class="line"><span class="addition">+           Ordering::Equal =&gt; &#123;</span></span><br><span class="line"><span class="addition">+               println!(&quot;You win!&quot;);</span></span><br><span class="line"><span class="addition">+               break;</span></span><br><span class="line"><span class="addition">+           &#125;</span></span><br><span class="line">        &#125;</span><br><span class="line"><span class="addition">+   &#125;</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li>我们将提示用户做出猜测决定之后的所有内容都移动到了 <code>loop</code> 中。在 Rust 中，<code>loop</code> 关键字会创建一个无限循环。</li><li>我们还给程序增加了一条<code>break</code>语句，使得玩家在猜对数字后能够正常退出游戏。</li></ul><p>运行程序，玩家猜对数字，输出**You Win!**后，会退出程序：</p><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d6b7b5953aec4e36a9fe7b44805100c0~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><h2 id="处理非法输入"><a href="#处理非法输入" class="headerlink" title="处理非法输入"></a>处理非法输入</h2><p>在转换 <code>guess</code> 字符串为 <code>number</code> 类型是，我们使用 <code>expect(&quot;Please type a number&quot;)</code>来处理可能出现的错误，我们尝试输入一个非 <code>number</code> 类型的值运行一下：</p><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b33a4d37eeb648f9837aef91fea55b05~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><p>用户如果想要重新开始游戏，需要重启程序，这很明显，用户体验挺差的。为了改善游戏的可玩性，我们可以在用户输入了一个非数字数据时简单地忽略这次猜测行为，并使用户可以继续进行猜测，从而避免程序发生崩溃。</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">use</span> rand::Rng;</span><br><span class="line"><span class="keyword">use</span> std::cmp::Ordering;</span><br><span class="line"><span class="keyword">use</span> std::io;</span><br><span class="line"></span><br><span class="line"><span class="keyword">fn</span> <span class="title function_">main</span>() &#123;</span><br><span class="line">    <span class="built_in">println!</span>(<span class="string">&quot;Guess the number!&quot;</span>);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// `..` 语法是标准库中提供的 Range</span></span><br><span class="line">    <span class="keyword">let</span> <span class="variable">secret_number</span> = rand::<span class="title function_ invoke__">thread_rng</span>().<span class="title function_ invoke__">gen_range</span>(<span class="number">1</span>..<span class="number">101</span>);</span><br><span class="line">    <span class="keyword">loop</span> &#123;</span><br><span class="line">        <span class="built_in">println!</span>(<span class="string">&quot;Please input your guess.&quot;</span>);</span><br><span class="line"></span><br><span class="line">        <span class="keyword">let</span> <span class="keyword">mut </span><span class="variable">guess</span> = <span class="type">String</span>::<span class="title function_ invoke__">new</span>();</span><br><span class="line"></span><br><span class="line">        io::<span class="title function_ invoke__">stdin</span>()</span><br><span class="line">            .<span class="title function_ invoke__">read_line</span>(&amp;<span class="keyword">mut</span> guess)</span><br><span class="line">            .<span class="title function_ invoke__">expect</span>(<span class="string">&quot;Failded to read line&quot;</span>);</span><br><span class="line"></span><br><span class="line">        <span class="keyword">let</span> <span class="variable">guess</span>: <span class="type">i32</span> = <span class="keyword">match</span> guess.<span class="title function_ invoke__">trim</span>().<span class="title function_ invoke__">parse</span>() &#123;</span><br><span class="line">            <span class="title function_ invoke__">Ok</span>(num) =&gt; num,</span><br><span class="line">            <span class="title function_ invoke__">Err</span>(_) =&gt; &#123;</span><br><span class="line">              <span class="built_in">println!</span>(<span class="string">&quot;Please type a number!&quot;</span>);</span><br><span class="line">              <span class="keyword">continue</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="built_in">println!</span>(<span class="string">&quot;You guessed: &#123;&#125;&quot;</span>, guess);</span><br><span class="line"></span><br><span class="line">        <span class="keyword">match</span> guess.<span class="title function_ invoke__">cmp</span>(&amp;secret_number) &#123;</span><br><span class="line">            Ordering::Less =&gt; <span class="built_in">println!</span>(<span class="string">&quot;Too small!&quot;</span>),</span><br><span class="line">            Ordering::Greater =&gt; <span class="built_in">println!</span>(<span class="string">&quot;Too big!&quot;</span>),</span><br><span class="line">            Ordering::Equal =&gt; &#123;</span><br><span class="line">                <span class="built_in">println!</span>(<span class="string">&quot;You win!&quot;</span>);</span><br><span class="line">                <span class="keyword">break</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><ul><li>我们使用了 <code>match</code> 表达式来替换之前的 <code>expect</code> 方法，这是我们处理错误行为的一种管用手段。</li><li><code>parse</code>会返回一个 Result 类型，而<code>Result</code>类型则包含了<code>Ok</code>与<code>Err</code>两个变体。</li></ul><p>万事具备，让我们运行这个项目试试看：</p><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/dd1f88a003e34ac0b23e1cd4e923cbbc~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/32241477f8c0/</id>
    <link href="https://blog.zisheng.pro/posts/32241477f8c0/"/>
    <published>2023-04-19T10:10:36.000Z</published>
    <summary>本文记录了使用 Rust 实现猜数游戏</summary>
    <title>Rust 编写猜数游戏</title>
    <updated>2026-09-18T08:15:28.632Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="Rust" scheme="https://blog.zisheng.pro/categories/Rust/"/>
    <category term="Rust 变量隐藏" scheme="https://blog.zisheng.pro/tags/Rust-%E5%8F%98%E9%87%8F%E9%9A%90%E8%97%8F/"/>
    <content>
      <![CDATA[<h2 id="什么是变量隐藏"><a href="#什么是变量隐藏" class="headerlink" title="什么是变量隐藏"></a>什么是变量隐藏</h2><p>在 Rust 的世界中，变量可以被一个同名的变量覆盖掉的现象就叫做隐藏。这意味着我们随后使用这个名称时，他指向的将会是第二个变量。我们可以重复使用<code>let</code>关键字并配以相同的名称来不断地隐藏变量：</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">fn</span> <span class="title function_">main</span>() &#123;</span><br><span class="line">  <span class="keyword">let</span> <span class="variable">x</span> = <span class="number">5</span>;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">let</span> <span class="variable">x</span> = x + <span class="number">1</span>;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">let</span> <span class="variable">x</span> = x + <span class="number">2</span>;</span><br><span class="line"></span><br><span class="line">  <span class="built_in">println!</span>(<span class="string">&quot;The value of x is: &#123;&#125;&quot;</span>, x);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>按照世界上没有完全一样的两个东西的原则（上一秒的你也不等于下一秒的你），这前后两个变量并不会完全一样。我并没有看过 Rust 源码，个人理解是这两个变量指向的内存空间是不一样的。所谓隐藏不过是编译器在编译的时候去做了区分。子非编译器，安知compiler之乐乎？</p><p>隐藏机制与<code>mut</code>（mutable）的不同：</p><ol><li>如果不是在使用 <code>let</code> 关键字的情况下重新为这个变量赋值，则会导致编译错误。通过使用<code>let</code>，我们可以对这个值执行一些列的变化操作，并允许这个变量在操作完成后保持自己的不可变性。</li><li>由于重复使用 <code>let</code> 关键字会创建出新的变量，所以我们可以在复用变量名称的同时改变它的类型。</li></ol><blockquote><p>隐藏特性通常被用在需要转换值类型的场景中，如果只是改变值本身 <code>mut</code> 关键字就够用。</p></blockquote><h2 id="为什么-Rust-需要变量隐藏？"><a href="#为什么-Rust-需要变量隐藏？" class="headerlink" title="为什么 Rust 需要变量隐藏？"></a>为什么 Rust 需要变量隐藏？</h2><blockquote><p>这是 reddit rust 话题下的一个问题，我这里翻译一下。</p></blockquote><h3 id="问题"><a href="#问题" class="headerlink" title="问题"></a>问题</h3><p>我最近发现 Rust 不像其他类C语言，它允许在一个块作用域中定义多个拥有相同名称的变量：</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> <span class="variable">name</span> = <span class="number">10</span>i;</span><br><span class="line"><span class="keyword">let</span> <span class="variable">name</span> = <span class="number">3.14f64</span>;</span><br><span class="line"><span class="keyword">let</span> <span class="variable">name</span> = <span class="string">&quot;string&quot;</span>;</span><br><span class="line"><span class="keyword">let</span> <span class="variable">name</span> = name; <span class="comment">// &quot;string&quot; again, this definition shadows all the others</span></span><br></pre></td></tr></table></figure><p>至少从我的 C++ 背景来说，第一次看到这个特性着实有点吓人。</p><p>这个特性来自哪里？它提供了什么好处？</p><h3 id="回答"><a href="#回答" class="headerlink" title="回答"></a>回答</h3><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6956030e8f734c3a81e5598aa9123dd7~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><p>变量隐藏与仿射类型（affine types）具有很多协同作用。例如：</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> <span class="variable">foo</span> = foo.<span class="title function_ invoke__">unwrap</span>();</span><br></pre></td></tr></table></figure><p>你在其中重新绑定 <code>foo</code> 的同时引用 <code>unwrap()</code> 的结果，因此旧 <code>foo</code> 由于该原因而变得不可访问。——变量指向了新的内存空间。</p><hr><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e9b00e7afe1946ed84b10d7085f0a320~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><p>好处是使用一个单词来表示共同的变量，例如 <code>c</code> 用于绘制上下文。变量可以被改变类型，<a href="https://github.com/PistonDevelopers/graphics">Rust-Graphics</a> 大量地依赖该特性。每一个方法返回一个新的类型，并且当上下文提供足够的信息去绘制时，你只能调用<code>.draw(gl)</code>。</p><hr><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/699da31543d44ae99d95e0d371e9fce8~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><p>我经常使用的单个字母是：</p><ul><li><code>a</code>、<code>b</code> 用作二进制操作的参数</li><li><code>c</code>、<code>d</code>、<code>g</code> 用作图形绘制</li><li><code>i</code>、<code>j</code>、<code>k</code> 用作索引</li><li><code>n</code>、<code>m</code> 用作长度</li><li><code>x</code>、<code>y</code>、<code>z</code>、<code>t</code> 作为空间坐标系</li></ul><p>有时我也使用<code>p</code>、<code>q</code>或者<code>v</code>、<code>u</code> 作复杂的数学运算。</p><hr><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/666f1cfe0c0e4a7f97779b7cc95dedf7~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><p>我认为变量隐藏的最佳论据是阻止对象可变性的能力。</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> <span class="keyword">mut </span><span class="variable">vec</span> = <span class="type">Vec</span>::<span class="title function_ invoke__">new</span>();</span><br><span class="line">vec.<span class="title function_ invoke__">push</span>(<span class="number">1</span>i);</span><br><span class="line">vec.<span class="title function_ invoke__">push</span>(<span class="number">2</span>i);</span><br><span class="line"><span class="keyword">let</span> <span class="variable">vec</span> = vec;</span><br></pre></td></tr></table></figure><p>但是，对于中间变量，我也很喜欢。有时，通过<code>let</code>绑定将长方法链分成两行只是更漂亮。您只需两次使用变量名来代替使用 <code>let temp</code>,这样，临时变量不会使函数名称空间的其余部分杂乱无章。</p><hr><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f2a0e9fa316c4ad8a95d655d76b9295b~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><p>我不确定，但我认为此功能可将可变性降至最低。您可以使用新值重新声明它，而不用突变一个变量。</p><p>无论如何，它并没有看起来那么糟糕。类型系统禁止使用<code>int</code>代替浮点数或字符串。</p><p>确实，此功能可能会导致错误，但也可以防止它们。没有隐藏，您常常必须为变量（相同类型）使用不同的名称，这使得使用错误的变量成为可能。</p><hr><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/551b748ead0b4ae89a1797d0032b79f5~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><p>我昨天也注意到了这一点。我宁愿没有它，因为它引入了难以发现的错误。</p><p>我自己的C ++代码中有与隐藏相关的错误（就在上周）。此外，由于隐藏局部变量，Gecko中也存在bug，因此显然这很危险。</p><p>我知道Rust是一种旨在防止错误的语言（即使它降低了便利性），所以我想知道为什么允许这样做。值得冒着出BUG的风险吗？</p><p>也有人评论说lint有一个选项可以警告可变阴影。会有这样的选择吗？但愿如此。</p><p>顺便提一句。该线程还有一些用例，人们如何使用隐藏，因此对此线程感兴趣的人应该通读它。</p><ul><li><a href="https://mail.mozilla.org/pipermail/rust-dev/2013-May/004306.html">[rust-dev] Variables with the same name in the same scope</a></li><li><a href="https://mail.mozilla.org/pipermail/rust-dev/2013-May/004298.html">[rust-dev] Variables with the same name in the same scope</a></li></ul><hr><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/07fc8946bff243ddaf9abac9db16b02d~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><p>我经常使用隐藏来防止再次引用另一个变量。这是我从OCaml那里习得的一种习惯，在该习惯下，隐藏是允许的并且是惯用的。</p><p>比如，在 <code>trans</code> 中我经常隐藏来终止块变量，因为再次使用它们是一个错误。这样，隐藏可以成为防止错误的强大工具。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>这个问答中，整体的意见比较一致，就是隐藏特性乍看起来很不爽，但是用多了就会惊讶于它的安全性、便捷性。凡事有捧就有踩，还是有老哥说出来隐藏用多了，很难调试，由于老哥过于啰嗦，懒得翻译原文如下：</p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c15812c59a3248c99c77eeed7838fabe~tplv-k3u1fbpfcp-zoom-1.image" style="zoom:50%;" /><p>个人觉得这有点杠了，所有设计一旦被滥用就会变得不对劲。总体来说隐藏带来的好处是大于益处的。</p>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/2e8fd8fce8fc/</id>
    <link href="https://blog.zisheng.pro/posts/2e8fd8fce8fc/"/>
    <published>2023-04-19T10:08:20.000Z</published>
    <summary>在 Rust 的世界中，变量可以被一个同名的变量覆盖掉的现象就叫做隐藏。这意味着我们随后使用这个名称时，他指向的将会是第二个变量。</summary>
    <title>为什么 Rust 需要局部变量隐藏</title>
    <updated>2026-09-18T08:15:28.636Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="React Native" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/React-Native/"/>
    <category term="跨端开发" scheme="https://blog.zisheng.pro/tags/%E8%B7%A8%E7%AB%AF%E5%BC%80%E5%8F%91/"/>
    <category term="Flutter" scheme="https://blog.zisheng.pro/tags/Flutter/"/>
    <category term="React Native" scheme="https://blog.zisheng.pro/tags/React-Native/"/>
    <category term="RN" scheme="https://blog.zisheng.pro/tags/RN/"/>
    <category term="Weex" scheme="https://blog.zisheng.pro/tags/Weex/"/>
    <content>
      <![CDATA[<h2 id="缘起"><a href="#缘起" class="headerlink" title="缘起"></a>缘起</h2><p>那是 18 年的夏天，公司 6 月底去成都团建回郑州后，小弟、肖老师和当时绰号小萌的我（以下简称三剑客）开始了使用 React Native 开发 App 的工作。当时经验尚浅的我也只是有小程序和公众号的开发经验，唯一的亮点就是封装了 <a href="https://github.com/youngjuning/wx-promise-pro">wx-promise-pro</a> 并独立完成了一个小程序的全栈开发，而另外两位同事一个后端大佬经验丰富，一个 ios、vue、android、C#、Java 等等技能栈点满。</p><p>怎么说呢，当时也就中等焦虑吧。也就赶鸭子上架各种不适，也就睡不好吃不好担心末位淘汰，也就担心没工作被我爸唠叨。</p><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c327b38e8ca3410ea7f74d67d8080ccb~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><p>人们常说压不倒你的，最终都能成就你。我就是顶着巨大心里压力一头扎进了跨端开发这个领域，一晃便是两年多过去了。接着掘金的活动，和大家聊一聊 RN 的一些事。</p><h2 id="聊聊-React-Native-入门"><a href="#聊聊-React-Native-入门" class="headerlink" title="聊聊 React Native 入门"></a>聊聊 React Native 入门</h2><p>找到紫升询问 React Native 最多的问题便是 React Native 环境的搭建，我都会耐心地帮助大家排查（有时也会让仔细看官网）。这个时候我一般都会和他们聊一下我们三剑客初次搭建环境最快的也是用了一整个工作日，最慢的因为电脑环境混乱多花了一个周末排查问题的故事。</p><p>万事开头难这句话用在 RN 和 Flutter 上尤其合适，跨端对于原生转和前端上手都是不太友好的，所以不要再问前端转合不合适，原生转不比你舒服到哪里去。</p><p>这里提醒大家关于环境的几点：</p><ul><li>Java jdk 一定要安装 <strong>1.8</strong> 系列</li><li>尽量使用安卓真机测试</li><li>如果用 Android Studio 打开项目，不要跟着提示随便升级 gradle</li><li>RN 0.60 之后 ios 使用 cocoapod 管理项目依赖，这玩意如果网速不好，会卡住。建议别折腾，买个 ** 也饿不死人的</li></ul><h2 id="聊聊跨端开发"><a href="#聊聊跨端开发" class="headerlink" title="聊聊跨端开发"></a>聊聊跨端开发</h2><p>跨端开发也叫 Hybrid Development，React Native、Flutter、Weex 都属于这个范畴。任何事物的出现必然是有其客观需求存在，跨端开发相较于传统的原生开发有其迭代周期快、可以在发布应用后热更新代码、甚至代码直接通过服务动态下发的优点，相较于 Codava、H5 打包这种解决了 60 帧问题，性能更好更接近原生。</p><p>目前前两者在各大厂 App 中都有业务落地，众所周知的闲鱼使用 Flutter 写了不少业务，前文提到的我们三剑客是完全用 React Native 搭建的项目，而现在我所在的涂鸦智能的 App 是使用原生搭建项目，嵌入我们的核心面板业务。我们不可能把所有的设备控制面板都写在 App 中，ios、android 的平台也不能动态下发代码，而 React Native 可以实现开发面板 -&gt; 打包 -&gt; 注册到云端 -&gt; 客户端根据从服务端动态加载相应的业务代码这个流程。顺便提一下隔壁小组也有在做基于 Flutter 的一码多端方案，加入涂鸦，无论你是 RN 开发者还是 Flutter 开发者都能发挥所长。</p><h2 id="从事-RN-开发需要会什么？"><a href="#从事-RN-开发需要会什么？" class="headerlink" title="从事 RN 开发需要会什么？"></a>从事 RN 开发需要会什么？</h2><p>以我的经验来看，只要你有编程经验，快速从事 RN 业务开发难度并不大。写页面用到的无非是 React 语法、React Native 内置组件的使用和第三放组件、API 的使用。</p><p>但是如果是要掌控整个 App 的架构和开发，那要求会多一些。但也无非是 React 开发同样需要掌握的全局状态管理、路由管理、组件封装等能力。</p><p>你可能会问不会原生开发可以从事 RN 开发吗？答案是肯定的，跨端方案的出现对于前端开发者的一大利好便是可以使用前端技术开发移动应用。React Native 不管是官方还是社区都有大量的封装好的 SDK 可供使用，比如 <a href="https://github.com/react-native-svg/react-native-svg">react-native-svg</a>、<a href="https://github.com/react-native-camera/react-native-camera">react-native-camera</a>、<a href="https://github.com/react-native-video/react-native-video">react-native-video</a>、<a href="https://github.com/zmxv/react-native-sound">react-native-sound</a>、<a href="https://github.com/zoontek/react-native-permissions">react-native-permissions</a> 等等这些优秀的三方库。当然你可能还有疑问，如果社区没有现成的，或者需要自定义原生的功能才能完成业务怎么办？这个就没那么简单了，但是别担心，现代工业社会是讲究协作和精细化分工的，在涂鸦，我们有专业的原生开发团队为我们提供底层 SDK 业务编写，但是我还建议大家 ios、android 的桥接代码还是要会写、原生的配置要懂，有助于提升个人能力和稀缺性。相信我这并不复杂，你遇到的问题搜一下也能解决。紫升的经验也仅限于掌握原生配置、对原生 SDK 进行封装暴露接口给 JS 层调用，ios 课程买了有一年了，也没看一点（实在惭愧）。对原生 SDK 桥接感兴趣的可以看下<a href="https://reactnative.cn/docs/native-modules-intro">原生模块简介</a>和<a href="https://github.com/brodybits/create-react-native-module">create-react-native-module</a>。</p><h2 id="聊聊我为社区做的一些事"><a href="#聊聊我为社区做的一些事" class="headerlink" title="聊聊我为社区做的一些事"></a>聊聊我为社区做的一些事</h2><blockquote><p>你可能会在各种 react native 仓库的 issue 中看到我的身影，这里也推荐大家遇到什么问题多去 github 看看 issues，一个成熟的仓库基本都能在 issue 中找到解决办法。</p></blockquote><ul><li><a href="https://github.com/youngjuning/umi-plugin-react-native">umi-plugin-react-native</a>：Show React Native Demo with dumi powered by react-native-web</li><li><a href="https://github.com/youngjuning/react-native-quill-editor">react-native-quill-editor</a>：React Native Quill Rich Text Editor Wrapper</li><li><a href="https://github.com/sishuguojixuefu/react-native-form">react-native-form</a>：私塾React Native动态表单项目，这个项目后来只在内部更新了，离职一年多了，大概率被同事重构了已经，我一直说要用 hooks 实现以下，也没得机会。</li><li><a href="https://github.com/sishuguojixuefu/react-native-screenshot">react-native-screenshot</a>：简单易用的 React Native 截屏、监听系统截屏事件组件（ios、android）</li><li><a href="https://github.com/sishuguojixuefu/react-native-office-viewer">react-native-office-viewer</a>：Office Preview Plugin for React Native（support png,pdf,doc,xls,txt…）</li><li><a href="https://github.com/sishuguojixuefu/react-native-template-sishu">react-native-template-sishu</a>：👾 Clean and minimalist React Native template for a quick start with TypeScript.</li><li><a href="https://github.com/sishuguojixuefu/react-native-qiniu">react-native-qiniu</a>：Qiniu React Native SDK ,A pure javascript implementation</li><li><a href="https://github.com/sishuguojixuefu/react-native-float-ball">react-native-float-ball</a>：🏀 floatBall with PanResponder 🏈</li><li><a href="https://juejin.cn/post/6844903843155689486">优雅地使用TypeScript开发React Native应用</a></li><li><a href="https://juejin.cn/post/6844904069937496071">react-navigation 5.x 最佳实践</a></li><li><a href="https://juejin.cn/post/6844904071137247240">React Native 项目架构面面观</a></li><li><a href="https://juejin.cn/post/6844904073309716494">React Native 集成 CodePush 指南</a></li><li><a href="https://juejin.cn/post/6844904083468337160">【译】在 React Native TypeScript 项目中使用 defaultProps</a></li><li><a href="https://juejin.cn/post/6859039959299391496">【译】在 React Native TypeScript 项目中使用 defaultProps</a></li><li><a href="https://juejin.cn/post/6867945949788897288">手摸手带你封装 React Native 富文本编辑器</a></li><li><a href="https://juejin.cn/post/6928412126587387917">React Native 调试最佳实践|牛气冲天新年征文</a></li></ul><h2 id="聊聊岗位待遇"><a href="#聊聊岗位待遇" class="headerlink" title="聊聊岗位待遇"></a>聊聊岗位待遇</h2><p>有一说一，React Native 包括 Flutter 在内，岗位比 Web 前端开发是要少很多的，但是这并不意味着不值得投资。相较于 Web 开发，跨端开发需要掌握更多的技能，入门更难一些。但是一般掌握了它，对应的薪资待遇也是很诱人的。</p>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/2affeeb3c632/</id>
    <link href="https://blog.zisheng.pro/posts/2affeeb3c632/"/>
    <published>2023-04-19T10:01:42.000Z</published>
    <summary>那是 18 年的夏天，公司 6 月底去成都团建回郑州后，小弟、肖老师和当时绰号小萌的我（以下简称三剑客）开始了使用 React Native 开发 App 的工作。当时经验尚浅的我也只是有小程序和公众号的开发经验，唯一的亮点就是封装了 wx-promise-pro 并独立完成了一个小程序的全栈开发，而另外两位同事一个后端大佬经验丰富，一个 ios、vue、android、C#、Java 等等技能栈点满。</summary>
    <title>当我们在聊 React Native 时，我们在聊什么</title>
    <updated>2026-09-18T08:15:28.640Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="Rust" scheme="https://blog.zisheng.pro/categories/Rust/"/>
    <category term="Rust" scheme="https://blog.zisheng.pro/tags/Rust/"/>
    <category term="Ownership" scheme="https://blog.zisheng.pro/tags/Ownership/"/>
    <category term="Borrowing" scheme="https://blog.zisheng.pro/tags/Borrowing/"/>
    <category term="Lifetimes" scheme="https://blog.zisheng.pro/tags/Lifetimes/"/>
    <category term="所有权" scheme="https://blog.zisheng.pro/tags/%E6%89%80%E6%9C%89%E6%9D%83/"/>
    <category term="借用" scheme="https://blog.zisheng.pro/tags/%E5%80%9F%E7%94%A8/"/>
    <category term="生命周期" scheme="https://blog.zisheng.pro/tags/%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F/"/>
    <content>
      <![CDATA[<blockquote><ul><li>原文地址：<a href="https://medium.com/@bugaevc/understanding-rust-ownership-borrowing-lifetimes-ff9ee9f79a9c">Understanding Rust: ownership, borrowing, lifetimes</a></li><li>原文作者：<a href="hhttps://medium.com/@bugaevc">bugaevc</a></li><li>译文出自：<a href="https://github.com/xitu/gold-miner">掘金翻译计划</a></li><li>本文永久链接：<a href="https://github.com/xitu/gold-miner/blob/master/article/2021/understanding-rust-ownership-borrowing-lifetimes.md">https://github.com/xitu/gold-miner/blob/master/article/2021/understanding-rust-ownership-borrowing-lifetimes.md</a></li><li>译者：<a href="https://blog.zisheng.pro/">紫升</a></li><li>校对者：<a href="https://github.com/PassionPenguin">霜羽 Hoarfroster</a>、<a href="https://github.com/zenblo">Zz招锦</a></li></ul></blockquote><p>我对这些概念的理解是，你一旦掌握了它，所有这些语法都会看起来自然且优雅。</p><p>我不会从零开始展开教学，也不会机械地重复官方文档的内容（虽然说有时会 🙈）—— 如果你还不了解这些概念，那么你现在应该读一下<a href="https://kaisery.github.io/trpl-zh-cn/ch04-00-understanding-ownership.html">对应章节</a>的内容，因为本文是对书上内容的补充，而不是要替代它。</p><p>另外，我也建议你读一下<a href="http://blog.skylight.io/rust-means-never-having-to-close-a-socket/">这篇</a>出色的文章。它实际上是在讲述相近的话题，但关注点不一样，也值得一读。</p><p>让我们先来谈谈资源是什么。资源是宝贵的、“沉重的”、可以获取和释放（或销毁）的东西，比如一个套接字，一个打开的文件，一个信号量，一个锁，一个堆内存区域。按照传统，所有这些事情都是通过调用一个函数来创建的，该函数返回对资源本身的某种引用（一个内存指针或一个文件描述符），当程序认为自己已完成对资源的处理时，需要程序员 👨🏻‍💻 显式关闭该文件。</p><p>这种方法存在着问题。人非圣贤，孰能无过。通常我们很容易忘记释放某些资源，从而导致发生所谓的<strong>内存泄漏</strong>。更糟糕的是，人们可能会尝试访问已经释放的资源（即在释放之后使用）。如果运气好，他们会收到一条报错消息，这可能会帮助他们识别和修复错误，也可能不会。反之，它们所具有的引用（尽管就逻辑而言是无效的）可能仍是引用某个“内存位置”，而该“内存位置”已经被其他资源占用。例如说已存储其他内容的内存，其它打开的文件所使用的文件描述符等。试图通过无效的引用访问旧资源可能会破坏其他资源或使得程序完全崩溃。</p><p>我们讨论这些问题并不是杞人忧天，因为它们无时无刻伴随着我们。比如，在 <a href="http://googlechromereleases.blogspot.ru/search/label/Stable%20updates">Google Chrome 发布博客</a>中就存在着大量因为使用了被释放的资源引发的漏洞和崩溃的修复记录 —— 这也极大的浪费了人力物力，去识别和修复它们。</p><p>并不是说开发人员是愚蠢和健忘的，因为逻辑流程本身就容易出错：它需要你显示释放资源，但是并不强制你做这些。此外，我们通常不会注意到资源被忘记释放，因为这个问题很少会有着什么明显的影响。</p><p>有时要实现简单的目地就需要发明复杂的解决方案，而这些解决方案会带来更复杂的逻辑。我们很难避免在庞大的代码库中迷失，并且 Bug 总是在这里或那里突然冒出来，我们最终也见怪不怪了。其中大多数的问题都很容易被发现，但是与资源相关的错误却很难被发现。因此，一旦如果资源被野指针利用，便会非常危险。</p><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d66aa3a26a7842d7b2102f3a3ef2b6db~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><p>当然，像 Rust 这样的新语言无法为你解决 Bug，但是，它可以成功地影响你的思维方式，将一些架构带入你的思想，从而使这类错误的发生几率大大降低。</p><p>Rust 为你提供了一种安全清晰的方法来管理资源。而且，它不允许你以其他任何方式对其进行管理。这是非常严格的，但这不正是我们的目的吗？</p><p>这些限制之所以很棒，有几个原因:</p><ul><li>它们能让你以正确的方式思考。在有了一些 Rust 开发经验后，即使在其他语言的语法中没有内置这些概念时，你也经常会发现自己尝试应用相似的概念。</li><li>它们能让你编写的代码更安全。除了几个很稀有的<a href="https://doc.rust-lang.org/nomicon/meet-safe-and-unsafe.html">极端案例</a>，Rust 基本上可以保证你所有的代码都不会涉及我们正在谈论的错误。</li><li>虽然如果有垃圾收集机制，Rust 就会像高级语言一样令人愉悦（我可没说 JavaScript 是令人愉悦的！），但是 Rust 与其他低级编译语言一样快且接近底层。</li></ul><p>考虑到这一点，让我们来看一下 Rust 的一些优点。</p><h2 id="所有权"><a href="#所有权" class="headerlink" title="所有权"></a>所有权</h2><p>在 Rust 中，关于资源属于哪块代码有很明确的规则。在最简单的情况下，是代码块创建了代表资源的对象。在代码块的末尾，对象被销毁且资源被释放。这里重要的区别是对象不是某种容易忘记的“弱引用”。在内部，该对象只是用于完全相同引用的包装器，而从外部看，它似乎是它表示的资源。当到达拥有资源的代码块的末尾时，资源将会自动且可预测地释放。</p><p>当编译到拥有该内存的代码的尾部，程序会自动且安全地释放资源。妈妈再也不用担心忘记释放资源了！因为该行为是全自动且可预测的，它完全会按照你的预期来完成。</p><p>这时你可能会问，为什么我要描述这些琐碎而明显的事情，而不是仅仅告诉你聪明人称之为 <a href="https://zh.wikipedia.org/wiki/RAII">RAII</a> 的概念？ 好吧，让我们继续聊一下。</p><p>这个概念适用于临时对象。比如以下操作：<code>将一些文本写入文件</code> -&gt; <code>专用代码块（例如，一个函数）将打开一个文件</code>（结果是得到一个文件对象（包装文件描述符））-&gt; <code>然后对其进行一些处理</code> -&gt; <code>然后在该块的末尾将得到文件对象</code> -&gt; <code>最后删除并且文件描述符关闭</code>。</p><p>但是在很多场景中这个概念并不管用。你可能希望将资源传递给其他人，在几个“用户”之间甚至在线程之间共享它。</p><p>让我们来看看这些。首先，你可能希望将资源传递给其他人（转移所有权），被转移的人便会拥有资源，可以对资源进行任何操作，甚至更重要的是负责释放资源。Rust 很好的支持了这一点，实际上，当你将资源提供给其他人时，默认便会发生这种情况。</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">fn</span> <span class="title function_">print_sum</span>(v: <span class="type">Vec</span>&lt;<span class="type">i32</span>&gt;) &#123;</span><br><span class="line">    <span class="built_in">println!</span>(<span class="string">&quot;&#123;&#125;&quot;</span>, v[<span class="number">0</span>] + v[<span class="number">1</span>]);</span><br><span class="line">    <span class="comment">// v 被移除随后被释放</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">fn</span> <span class="title function_">main</span>() &#123;</span><br><span class="line">    <span class="keyword">let</span> <span class="keyword">mut </span><span class="variable">v</span> = <span class="type">Vec</span>::<span class="title function_ invoke__">new</span>(); <span class="comment">// 资源在这里被创建</span></span><br><span class="line">    <span class="keyword">for</span> <span class="variable">i</span> <span class="keyword">in</span> <span class="number">1</span>..<span class="number">1000</span> &#123;</span><br><span class="line">        v.<span class="title function_ invoke__">push</span>(i);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 在这里， 可变变量 v 被使用</span></span><br><span class="line">    <span class="comment">// 不少于 4000 字节的内存</span></span><br><span class="line">    <span class="comment">// -------------------</span></span><br><span class="line">    <span class="comment">// 转移所有权给 print_sum 函数</span></span><br><span class="line">    <span class="title function_ invoke__">print_sum</span>(v);</span><br><span class="line">    <span class="comment">// 我们不拥有并且不能以任何方式控制变量 v</span></span><br><span class="line">    <span class="comment">// 在这里尝试访问 v 将引发编译时错误</span></span><br><span class="line">    <span class="built_in">println!</span>(<span class="string">&quot;We&#x27;re done&quot;</span>);</span><br><span class="line">    <span class="comment">// 这里并不会发生任何释放动作</span></span><br><span class="line">    <span class="comment">// 因为 print_sum 此时负责可变变量 v 的一切</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>所有权转移的过程也称为<strong>移动</strong>，因为资源是从旧位置（例如，局部变量）被移动到了新位置（例如，一个函数参数）的。从性能角度来看，这只是“弱引用”被移动，因此这个过程很快。但是对于代码来说，好像我们实际上将整个资源都移到了新地方。</p><p>移动和复制是有区别的。广义来说，它们都意味着复制数据（如果 Rust 允许复制资源的话，这种情况下将是“弱引用”），但移动后，原始变量的内容将被视为不再有效或不再重要。Rust 实际上会将该变量视为“ <a href="https://doc.rust-lang.org/nomicon/checked-uninit.html">逻辑上未初始化</a>”，也就是说，充满了一些垃圾，例如刚刚创建的那些变量。这类变量是被禁止使用的（除非你使用新值重新初始化它），此时也不会发生资源的重新分配：现在拥有资源的人有责任在完成后进行清理。</p><p>移动不仅限于传递参数。你可以移动给一个变量。你还可以移至返回值。为此，你可以从返回值、变量、函数参数移动。基本上到处都是隐式和显示的分配。</p><p>尽管移动语法是处理资源的完全合理的方式，我将在稍后演示对于普通的旧原始数字类型变量来说，这将是一场灾难（设想无法复制一个 int 类型变量的值给另一个变量）。幸运的是，Rust 有 <a href="https://doc.rust-lang.org/std/marker/trait.Copy.html">Copy 特征</a>。实现它的类型（所有原始类型都使用）在分配时使用复制语法，所有其他类型都使用移动语法。这非常容实现，如果你希望自己的类型是可以被复制的，则只需要可选地实现 <code>Copy</code> 特征。</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">fn</span> <span class="title function_">print_sum</span>(a: <span class="type">i32</span>, b: <span class="type">i32</span>) &#123;</span><br><span class="line">    <span class="built_in">println!</span>(<span class="string">&quot;&#123;&#125;&quot;</span>, a + b);</span><br><span class="line">    <span class="comment">// 被复制的 a 和 b 变量在这里被移除和释放</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">fn</span> <span class="title function_">main</span>() &#123;</span><br><span class="line">    <span class="keyword">let</span> <span class="variable">a</span> = <span class="number">35</span>;</span><br><span class="line">    <span class="keyword">let</span> <span class="variable">b</span> = <span class="number">42</span>;</span><br><span class="line">    <span class="comment">// 复制和传递值</span></span><br><span class="line">    <span class="comment">// 被复制的值传递的所有权传递给 print_sum：</span></span><br><span class="line">    <span class="title function_ invoke__">print_sum</span>(a, b);</span><br><span class="line">    <span class="comment">// 我们仍然保留对原始a和b变量的完全控制权</span></span><br><span class="line">    <span class="built_in">println!</span>(<span class="string">&quot;We still have &#123;&#125; and &#123;&#125;&quot;</span>, a, b);</span><br><span class="line">    <span class="comment">// 原始的 a 和 b 被移除并随后被释放</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>现在，我们来探讨下为什么移动语法会有用呢？如果没有他们，一切都显得那么完美。好吧，也不完全是。有时候，这是最合乎逻辑的事情。比如 <a href="https://doc.rust-lang.org/std/string/struct.String.html#method.with_capacity">with_capacity</a> 函数会分配一个字符串缓冲区，然后将其返回给调用方。所有权被转移了，并且该函数不再关心缓冲区的生死。而调用者可以完全控制缓冲区，包括负责缓冲区的释放。</p><p>在 C 语言中是一样的。诸如 <code>strdup</code> 之类的功能将分配内存，将其内存管理交给你，并期望你进行管理并最终对其进行分配。区别在于它只是一个指针，它们所能做的就是在完成后要求或提醒你使用 <code>free()</code>。上面所说的移动特性几乎无法做到，而在 Rust 中，这是该语言不可分割的一部分。</p><p>另一个示例是迭代器适配器，比如 <a href="https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.count">count</a> 这种无论如何之后都没有访问迭代器的意义。</p><p>相反的问题是，在什么情况下，我们需要对同一资源有多个引用。最明显的用例是进行多线程处理的场景。否则，如果所有操作都按顺序执行，则移动语法可能总是起作用的。尽管如此，一直来回移动东西还是很不方便的。</p><p>有时，尽管代码严格按顺序运行，但仍然感觉好像同时发生了几件事。想象一下在 vector（可变数组）上进行迭代。循环完成后，迭代器可以将你对相关 vector 的所有权转移给你，但你将无法在循环内获得对 vector 的任何访问权限。也就是说，除非你每次迭代都在你的代码和迭代器之间拥有所有权，否则那将是一团糟。似乎也无法在不破坏堆栈的情况下遍历一棵树，然后重新构造并备给以后做其他事情时用。</p><p>同时，我们将无法执行多线程，这就很不方便甚至让人厌烦。值得庆幸的是，还有一个很酷的 Rust 概念可以为我们提供帮助。那就是借用！</p><h2 id="借用"><a href="#借用" class="headerlink" title="借用"></a>借用</h2><blockquote><p>当一个函数使用引用而不是值本身作为参数时，我们便不需要为了归还所有权而特意去返回值，毕竟在这种情况下，我们根本没有取得所有权。这种通过引用传递参数给函数的方法也被成为借用。——《Rust 权威指南》</p></blockquote><p>我们有多种角度解读借用：</p><ul><li>它使我们在拥有资源的多个引用的同时仍坚持“单一所有者，单一责任”的概念。</li><li>引用类似于 C 语言中的指针。</li><li>引用也是一个对象。可变引用被移动，不可变引用被复制。删除引用后，借用将终止（取决于生命周期规则，请参见下一节）。</li><li>在最简单的情况下，引用的行为就像在没有明确地进行所有权操作的情况下来回移动所有权。</li></ul><p>下面这段代码就是最后一条的意思：</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 没有借用发生</span></span><br><span class="line"><span class="keyword">fn</span> <span class="title function_">print_sum1</span>(v: <span class="type">Vec</span>&lt;<span class="type">i32</span>&gt;) <span class="punctuation">-&gt;</span> <span class="type">Vec</span>&lt;<span class="type">i32</span>&gt; &#123;</span><br><span class="line">    <span class="built_in">println!</span>(<span class="string">&quot;&#123;&#125;&quot;</span>, v[<span class="number">0</span>] + v[<span class="number">1</span>]);</span><br><span class="line">    <span class="comment">// 返回 v 把所有权返回</span></span><br><span class="line">    <span class="comment">// 顺便一提，由于 Rust 是基于表达式的，所有这里不需要使用 return 关键字便可返回值</span></span><br><span class="line">    v</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">fn</span> <span class="title function_">print_sum2</span>(vr: &amp;<span class="type">Vec</span>&lt;<span class="type">i32</span>&gt;) &#123;</span><br><span class="line">    <span class="built_in">println!</span>(<span class="string">&quot;&#123;&#125;&quot;</span>, (*vr)[<span class="number">0</span>] + (*vr)[<span class="number">1</span>]);</span><br><span class="line">    <span class="comment">// vr 是一个引用，在这里被移除，因为借用结束了</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">fn</span> <span class="title function_">print_sum3</span>(v: &amp;<span class="type">Vec</span>&lt;<span class="type">i32</span>&gt;) &#123;</span><br><span class="line">    <span class="built_in">println!</span>(<span class="string">&quot;&#123;&#125;&quot;</span>, v[<span class="number">0</span>] + v[<span class="number">1</span>]);</span><br><span class="line">    <span class="comment">// 同 print_sum2</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">fn</span> <span class="title function_">main</span>() &#123;</span><br><span class="line">    <span class="keyword">let</span> <span class="keyword">mut </span><span class="variable">v</span> = <span class="type">Vec</span>::<span class="title function_ invoke__">new</span>(); <span class="comment">// 创建可变数组</span></span><br><span class="line">    <span class="keyword">for</span> <span class="variable">i</span> <span class="keyword">in</span> <span class="number">1</span>..<span class="number">1000</span> &#123;</span><br><span class="line">        v.<span class="title function_ invoke__">push</span>(i);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 此时， v 被使用</span></span><br><span class="line">    <span class="comment">// 不超过 4000 字节的内存</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 传递 v 的所有权给 print_sum 并在执行结束后反会 v</span></span><br><span class="line">    v = <span class="title function_ invoke__">print_sum1</span>(v);</span><br><span class="line">    <span class="comment">// 现在，我们重新取得了 v 的所有权</span></span><br><span class="line">    <span class="built_in">println!</span>(<span class="string">&quot;(1) We still have v: &#123;&#125;, &#123;&#125;, ...&quot;</span>, v[<span class="number">0</span>], v[<span class="number">1</span>]);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 取 v 的引用传递给 print_sum2（借用它）</span></span><br><span class="line">    <span class="title function_ invoke__">print_sum2</span>(&amp;v);</span><br><span class="line">    <span class="comment">// v 现在仍然可以被使用</span></span><br><span class="line">    <span class="built_in">println!</span>(<span class="string">&quot;(2) We still have v: &#123;&#125;, &#123;&#125;, ...&quot;</span>, v[<span class="number">0</span>], v[<span class="number">1</span>]);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 此时仍可以</span></span><br><span class="line">    <span class="title function_ invoke__">print_sum3</span>(&amp;v);</span><br><span class="line">    <span class="built_in">println!</span>(<span class="string">&quot;(3) We still have v: &#123;&#125;, &#123;&#125;, ...&quot;</span>, v[<span class="number">0</span>], v[<span class="number">1</span>]);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// v 被移除并在此处被释放</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>让我们看看这里发生了什么。第一个函数中，我们可以始终转移所有权，但是我们已经确信有时这并不是我们想要的。</p><p>第二个函数中，我们对 vector 进行引用，然后将其传递给函数。和 C 语言很像，我们通过解引用来获取对象。由于没有复杂的生命周期，因此一旦删除引用，借用便会终止。虽然它看起来像第一个示例，但是有一个重要的区别。<code>main</code> 函数拥有 vector 的所有权，在借用 vector 时只能对它做些限制。在这个示例中，<code>main</code> 函数在借用 vector 时甚至没有机会观察向量，因此这没什么大不了的。</p><p>第三个函数结合了第一个函数不需要解引用和第二个函数不弄乱所有权的优点。这之所以可行是因为 Rust 的<a href="http://stackoverflow.com/questions/28519997/what-are-rusts-exact-auto-dereferencing-rules">自动解除引用规则</a>。这些有点复杂，但是在大多数情况下，它们可以使你几乎就像使用引用指向的对象一样编写代码，这和 C++ 的引用很相似。</p><p>这里是另一个示例：</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 通过不可变引用获取 v</span></span><br><span class="line"><span class="keyword">fn</span> <span class="title function_">count_occurences</span>(v: &amp;<span class="type">Vec</span>&lt;<span class="type">i32</span>&gt;, val: <span class="type">i32</span>) <span class="punctuation">-&gt;</span> <span class="type">usize</span> &#123;</span><br><span class="line">    v.<span class="title function_ invoke__">into_iter</span>().<span class="title function_ invoke__">filter</span>(|&amp;&amp;x| x == val).<span class="title function_ invoke__">count</span>()</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">fn</span> <span class="title function_">main</span>() &#123;</span><br><span class="line">    <span class="keyword">let</span> <span class="variable">v</span> = <span class="built_in">vec!</span>[<span class="number">2</span>, <span class="number">9</span>, <span class="number">3</span>, <span class="number">1</span>, <span class="number">3</span>, <span class="number">2</span>, <span class="number">5</span>, <span class="number">5</span>, <span class="number">2</span>];</span><br><span class="line">    <span class="comment">// 为迭代借用 v</span></span><br><span class="line">    <span class="keyword">for</span> &amp;item <span class="keyword">in</span> &amp;v &#123;</span><br><span class="line">        <span class="comment">// the first borrow is still active</span></span><br><span class="line">        <span class="comment">// 第一个借用仍生效</span></span><br><span class="line">        <span class="comment">// 我们在这里第二次借用</span></span><br><span class="line">        <span class="keyword">let</span> <span class="variable">res</span> = <span class="title function_ invoke__">count_occurences</span>(&amp;v, item);</span><br><span class="line">        <span class="built_in">println!</span>(<span class="string">&quot;&#123;&#125; is repeated &#123;&#125; times&quot;</span>, item, res);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>你无需关心 <code>count_occurrences</code> 函数内部发生的事情，只需要知道它借用了 vector 即可（再次提醒，没有移动它）。循环也借用了 vector，因此我们有两个借用处于同时活动状态。循环结束后，<code>main</code> 函数将删除 vector。</p><p>哈哈，我会有点不地道了。我前面提到多线程是需要引用的主要原因，但是我展示的所有示例都是单线程的。如果你真的有兴趣，可以在 Rust 中获得有关多线程的一些<a href="https://doc.rust-lang.org/book/concurrency.html">详细信息</a>。</p><p>获取和删除引用似乎很有效，好像涉及到垃圾回收一样。但实际并不是这样的。这一切都在编译时完成。为此，Rust 需要另一个神奇的概念。让我们看下以下示例代码：</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">fn</span> <span class="title function_">middle_name</span>(full_name: &amp;<span class="type">str</span>) <span class="punctuation">-&gt;</span> &amp;<span class="type">str</span> &#123;</span><br><span class="line">    full_name.<span class="title function_ invoke__">split_whitespace</span>().<span class="title function_ invoke__">nth</span>(<span class="number">1</span>).<span class="title function_ invoke__">unwrap</span>()</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">fn</span> <span class="title function_">main</span>() &#123;</span><br><span class="line">    <span class="keyword">let</span> <span class="variable">name</span> = <span class="type">String</span>::<span class="title function_ invoke__">from</span>(<span class="string">&quot;Harry James Potter&quot;</span>);</span><br><span class="line">    <span class="keyword">let</span> <span class="variable">res</span> = <span class="title function_ invoke__">middle_name</span>(&amp;name);</span><br><span class="line">    <span class="built_in">assert_eq!</span>(res, <span class="string">&quot;James&quot;</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这是可以被成功编译的，但下面的代码是无法被编译的:</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">fn</span> <span class="title function_">middle_name</span>(full_name: &amp;<span class="type">str</span>) <span class="punctuation">-&gt;</span> &amp;<span class="type">str</span> &#123;</span><br><span class="line">    full_name.<span class="title function_ invoke__">split_whitespace</span>().<span class="title function_ invoke__">nth</span>(<span class="number">1</span>).<span class="title function_ invoke__">unwrap</span>()</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">fn</span> <span class="title function_">main</span>() &#123;</span><br><span class="line">    <span class="keyword">let</span> <span class="variable">res</span>;</span><br><span class="line">    &#123;</span><br><span class="line">        <span class="keyword">let</span> <span class="variable">name</span> = <span class="type">String</span>::<span class="title function_ invoke__">from</span>(<span class="string">&quot;Harry James Potter&quot;</span>);</span><br><span class="line">        res = <span class="title function_ invoke__">middle_name</span>(&amp;name);</span><br><span class="line">        <span class="comment">// `name` 在这里被移除并随后被释放</span></span><br><span class="line">    &#125;</span><br><span class="line">    <span class="built_in">assert_eq!</span>(res, <span class="string">&quot;James&quot;</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>首先，让我们解释下 <a href="http://doc.rust-lang.org/book/strings.html"><code>string</code> 类型</a>。<code>String</code> 拥有字符串缓冲区，一个 <code>&amp;str</code>（字符串切片）是 <code>String</code> 类型的一段或其他内存的一段（在这里并不重要）。</p><p>为了解释地更加明显，我用 C 语言编写类似的内容：</p><blockquote><p>顺便一提：在 C 语言中，你不能获取字符串的中间部分，因为标记字符串的结尾将需要更改字符串，因此我们仅限于在此处查找姓氏。</p></blockquote><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;stdlib.h&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;stdio.h&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;string.h&gt;</span></span></span><br><span class="line"></span><br><span class="line"><span class="type">const</span> <span class="type">char</span> *<span class="title function_">last_name</span><span class="params">(<span class="type">const</span> <span class="type">char</span> *full_name)</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="built_in">strrchr</span>(full_name, <span class="string">&#x27; &#x27;</span>) + <span class="number">1</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="type">int</span> <span class="title function_">main</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="type">const</span> <span class="type">char</span> *buffer = <span class="built_in">strcpy</span>(<span class="built_in">malloc</span>(<span class="number">80</span>), <span class="string">&quot;Harry Potter&quot;</span>);</span><br><span class="line">    <span class="type">const</span> <span class="type">char</span> *res = last_name(buffer);</span><br><span class="line">    <span class="built_in">free</span>(buffer);</span><br><span class="line">    <span class="built_in">printf</span>(<span class="string">&quot;%s\n&quot;</span>, res);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>你现在明白了吗？在使用结果之前，将删除缓冲区并重新分配缓冲区。这是一个在释放后使用资源的特殊例子。 如果 <code>printf</code> 的实现不会立即将内存用于其他用途，则此 C 代码可以编译并运行良好。不过，在一个不那么特殊的示例中，它仍然是崩溃、错误和安全漏洞的来源。正是在介绍所有权之前我们所说的。</p><p>你甚至无法在 Rust 中进行编译（我的意思是上面的 Rust 代码）。这种静态分析机制已内置在语言中，并且在整个生命周期可用。</p><h2 id="生命周期"><a href="#生命周期" class="headerlink" title="生命周期"></a>生命周期</h2><p>资源在 Rust 中是有生命周期的。他们从被创造的那一刻起一直存在到被移除的那一刻。生命周期通常被认为是作用域或代码块，但这实际上并不是一个准确的表述，因为资源可以在代码块之间移动，正如我们已经看到的那样。我们无法引用尚未创建或已删除的对象，我们很快就会看到这个要求是如何被强制执行。否则，这一切看起来都与所有权概念并没有什么不同。</p><p>这是比较难理解的一部分。引用以及其他对象也具有生存期，并且这些生存期可能与它们的借用的生存期不同（所谓的关联生命周期）。</p><p>让我们来改写下代码。借用的持续时间可能长于它所控制的引用的时间。这通常是因为可以使用另一个引用，该引用取决于借用是否处于活动状态——可以借用相同的对象或只借用其一部分，例如上例中的字符串切片。</p><p>实际上，每个引用都会记住它所代表的借用期限，也就是说，每一个引用都有一个生命周期。像所有与“借用检查”相关的事情一样，这是在编译时完成的，并且不占用任何运行时开销。与其他事物不同，你有时必须明确指定生命周期详细信息。</p><p>综上所述，让我们用代码深入探讨下：</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">fn</span> <span class="title function_">middle_name</span>&lt;<span class="symbol">&#x27;a</span>&gt;(full_name: &amp;<span class="symbol">&#x27;a</span> <span class="type">str</span>) <span class="punctuation">-&gt;</span> &amp;<span class="symbol">&#x27;a</span> <span class="type">str</span> &#123;</span><br><span class="line">    full_name.<span class="title function_ invoke__">split_whitespace</span>().<span class="title function_ invoke__">nth</span>(<span class="number">1</span>).<span class="title function_ invoke__">unwrap</span>()</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">fn</span> <span class="title function_">main</span>() &#123;</span><br><span class="line">    <span class="keyword">let</span> <span class="variable">name</span> = <span class="type">String</span>::<span class="title function_ invoke__">from</span>(<span class="string">&quot;Harry James Potter&quot;</span>);</span><br><span class="line">    <span class="keyword">let</span> <span class="variable">res</span> = <span class="title function_ invoke__">middle_name</span>(&amp;name);</span><br><span class="line">    <span class="built_in">assert_eq!</span>(res, <span class="string">&quot;James&quot;</span>);</span><br><span class="line"></span><br><span class="line">    <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="comment">    let res;</span></span><br><span class="line"><span class="comment">    &#123;</span></span><br><span class="line"><span class="comment">        let name = String::from(&quot;Harry James Potter&quot;);</span></span><br><span class="line"><span class="comment">        res = middle_name(&amp;name);</span></span><br><span class="line"><span class="comment">    &#125;</span></span><br><span class="line"><span class="comment">    assert_eq!(res, &quot;James&quot;);</span></span><br><span class="line"><span class="comment">    */</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在前面的示例中，我们不必明确地指出生命周期，因为生命周期的细致程度足以让 Rust 编译器自动找出来（请参阅[lifetime elision](<a href="https://doc.rust-lang.org/book/lifetimes.html">https://doc.rust-lang.org/book/lifetimes.html</a> ＃lifetime-elision)）。无论如何，我们已经在这里演示了它们的工作原理。</p><p><code>&lt;&gt;</code> 表示该函数在整个生命周期内都是通用的，我们称其为 <code>a</code>。也就是说，对于具有关联生命周期的任何引用传入，它将返回具有相同关联生命周期的另一个引用。友情提示，关联的生命周期是指借用的生命周期，而不是引用的生命周期。</p><p>在实践中，它的含义可能不是显而易见的，所以让我们从相反的角度来看它。返回的引用被存储在 <code>res</code> 变量中，该变量在 <code>main()</code> 的整个范围内都有效。那是引用的生命周期，因此借用（相关的生命周期）至少存在了很长的时间。这意味着函数传入参数的关联生命周期必须相同，因此我们可以得出结论，必须为整个函数借用 <code>name</code> 变量。</p><p>在释放后使用的示例中（此处已注释），<code>res</code> 的生命周期仍然是整个函数，而 <code>name</code> 的生存周期没有足够长的时间，以至于借用不能在整个函数中有效。如果你尝试编译此代码，毫无疑问会触发编译错误。</p><p>Rust 编译器尝试使借用的生命周期尽可能短，理想情况下，一旦引用被移除就结束了（这是我在<strong>借用</strong>部分开始时所说的“最简单的情况”）。“借用应有尽可能长的生命周期” 的约束却是以另一种相反的方式运作的，比如从 <code>result</code> 到原始借用的生命周期会延伸地很长。只要满足所有约束条件，此过程就会停止，如果无法实现，则会出错。</p><p>你无法欺骗 Rust 让函数的返回的借用的值与生命周期完全无关，因为那样的话，在函数中你将得到相同的 <code>does not live long enough</code> 报错信息，因为不相关的生命周期可能比传入的生命周期长很多。</p><p>让我们来看下这个示例：</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">fn</span> <span class="title function_">search</span>&lt;<span class="symbol">&#x27;a</span>, <span class="symbol">&#x27;b</span>&gt;(needle: &amp;<span class="symbol">&#x27;a</span> <span class="type">str</span>, haystack: &amp;<span class="symbol">&#x27;b</span> <span class="type">str</span>) <span class="punctuation">-&gt;</span> <span class="type">Option</span>&lt;&amp;<span class="symbol">&#x27;b</span> <span class="type">str</span>&gt; &#123;</span><br><span class="line">    <span class="comment">// 想象这里有一些聪明的算法</span></span><br><span class="line">    <span class="comment">// 返回了一个原始字符串的切片</span></span><br><span class="line">    <span class="keyword">let</span> <span class="variable">len</span> = needle.<span class="title function_ invoke__">len</span>();</span><br><span class="line">    <span class="keyword">if</span> haystack.<span class="title function_ invoke__">chars</span>().<span class="title function_ invoke__">nth</span>(<span class="number">0</span>) == needle.<span class="title function_ invoke__">chars</span>().<span class="title function_ invoke__">nth</span>(<span class="number">0</span>) &#123;</span><br><span class="line">        <span class="title function_ invoke__">Some</span>(&amp;haystack[..len])</span><br><span class="line">    &#125; <span class="keyword">else</span> <span class="keyword">if</span> haystack.<span class="title function_ invoke__">chars</span>().<span class="title function_ invoke__">nth</span>(<span class="number">1</span>) == needle.<span class="title function_ invoke__">chars</span>().<span class="title function_ invoke__">nth</span>(<span class="number">0</span>) &#123;</span><br><span class="line">        <span class="title function_ invoke__">Some</span>(&amp;haystack[<span class="number">1</span>..len+<span class="number">1</span>])</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        <span class="literal">None</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">fn</span> <span class="title function_">main</span>() &#123;</span><br><span class="line">    <span class="keyword">let</span> <span class="variable">haystack</span> = <span class="string">&quot;hello little girl&quot;</span>;</span><br><span class="line">    <span class="keyword">let</span> <span class="variable">res</span>;</span><br><span class="line">    &#123;</span><br><span class="line">        <span class="keyword">let</span> <span class="variable">needle</span> = <span class="type">String</span>::<span class="title function_ invoke__">from</span>(<span class="string">&quot;ello&quot;</span>);</span><br><span class="line">        res = <span class="title function_ invoke__">search</span>(&amp;needle, haystack);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">match</span> res &#123;</span><br><span class="line">        <span class="title function_ invoke__">Some</span>(x) =&gt; <span class="built_in">println!</span>(<span class="string">&quot;found &#123;&#125;&quot;</span>, x),</span><br><span class="line">        <span class="literal">None</span> =&gt; <span class="built_in">println!</span>(<span class="string">&quot;nothing found&quot;</span>)</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 输出 &quot;found ello&quot;</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>search</code> 函数接受两个引用，这些引用具有完全不相关的生命周期。尽管 <code>haystack</code> 受到限制，但关于 <code>needle</code> 的唯一要求是在函数本身执行时借用必须有效。完成后，借用立即结束，我们可以安全地重新分配关联的内存，同时仍然保持函数结果不变。</p><p><code>haystack</code>是用字符串字面量初始化的。这些是 <code>&amp;’static str</code> 类型的字符串切片（一个始终有效的借用）。因此我们可以在需要时将 <code>res</code> 变量保持在有效范围内。这是借用期限尽可能短规则的例外。你可以将其视为对“借用字符串”的另一个限制：字符串字面量借用必须持续整个程序的整个执行时间。</p><p>最后，我们返回的不是引用本身，而是一个内部的复合对象。这是完全支持的并且不会影响我们的一生逻辑。</p><p>因此，在此示例中，该函数接受两个参数，并且在两个生存期内都是通用的。让我们看看如果我们将生命周期设置为相同，会发生什么情况：</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">fn</span> <span class="title function_">the_longest</span>&lt;<span class="symbol">&#x27;a</span>&gt;(s1: &amp;<span class="symbol">&#x27;a</span> <span class="type">str</span>, s2: &amp;<span class="symbol">&#x27;a</span> <span class="type">str</span>) <span class="punctuation">-&gt;</span> &amp;<span class="symbol">&#x27;a</span> <span class="type">str</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> s1.<span class="title function_ invoke__">len</span>() &gt; s2.<span class="title function_ invoke__">len</span>() &#123; s1 &#125; <span class="keyword">else</span> &#123; s2 &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">fn</span> <span class="title function_">main</span>() &#123;</span><br><span class="line">    <span class="keyword">let</span> <span class="variable">s1</span> = <span class="type">String</span>::<span class="title function_ invoke__">from</span>(<span class="string">&quot;Python&quot;</span>);</span><br><span class="line">    <span class="comment">// 明确借用以确保借入的持续时间长于s2</span></span><br><span class="line">    <span class="keyword">let</span> <span class="variable">s1_b</span> = &amp;s1;</span><br><span class="line">    &#123;</span><br><span class="line">        <span class="keyword">let</span> <span class="variable">s2</span> = <span class="type">String</span>::<span class="title function_ invoke__">from</span>(<span class="string">&quot;C&quot;</span>);</span><br><span class="line">        <span class="keyword">let</span> <span class="variable">res</span> = <span class="title function_ invoke__">the_longest</span>(s1_b, &amp;s2);</span><br><span class="line">        <span class="built_in">println!</span>(<span class="string">&quot;&#123;&#125; is the longest if you judge by name&quot;</span>, res);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>我在内部代码块之外进行了明确的借用，因此借用会在 <code>main()</code> 的其余部分都有效。这明显和 <code>&amp;s2</code> 的生命周期不一样。如果仅接受两个具有相同生命周期的参数，那么这里为什么可以调用该函数？</p><p>事实证明，相关的生命周期会受到 <a href="https://en.wikipedia.org/wiki/Type_conversion">类型强制</a> 的约束。与大多数语言（至少是我所熟知的那些语言）不同，Rust 中的原始（整数）值不会强制转换，为此你必须始终明确地强制转换它们。你可以在一些不太明显的地方找到强制转换，例如这些关联的生命周期和 <a href="http://doc.rust-lang.org/book/trait-objects.html#dynamic-dispatch">dynamic dispatch with type erasure</a>。</p><p>我们用 C++ 代码进行比较：</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">struct</span> <span class="title class_">A</span> &#123;</span><br><span class="line">    <span class="type">int</span> x;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">B</span>: A &#123;</span><br><span class="line">    <span class="type">int</span> y;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">C</span>: B &#123;</span><br><span class="line">    <span class="type">int</span> z;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="function">B <span class="title">func</span><span class="params">(B arg)</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    <span class="keyword">return</span> arg;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">int</span> <span class="title">main</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    A a;</span><br><span class="line">    B b;</span><br><span class="line">    <span class="comment">/*</span></span><br><span class="line"><span class="comment">     * 这很好用：B值是有效的A值</span></span><br><span class="line"><span class="comment">     * 换句话说，只要期望A值，就可以使用B值</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    a = b;</span><br><span class="line">    <span class="comment">/*</span></span><br><span class="line"><span class="comment">     * 另一方面，这将是一个错误</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// b = a;</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 这能很好地工作</span></span><br><span class="line">    C arg;</span><br><span class="line">    A res = <span class="built_in">func</span>(arg);</span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>派生类型强制为其基本类型。 当我们传递 <code>C</code> 的实例时，它强制转换为 <code>B</code>，然后返回，强制转换为 <code>A</code>，然后存储在 <code>res</code> 变量中。</p><p>同样，在 Rust 中，更长的借用可以被强制缩短。它不会影响借用本身，而只会在需要较短借用的地方起作用。因此，你可以为函数传递寿命比预期更长的借用（它将被强制执行），并且可以强制将返回的借用的生命周期缩短。</p><p>再考虑一下这个示例：</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">fn</span> <span class="title function_">middle_name</span>&lt;<span class="symbol">&#x27;a</span>&gt;(full_name: &amp;<span class="symbol">&#x27;a</span> <span class="type">str</span>) <span class="punctuation">-&gt;</span> &amp;<span class="symbol">&#x27;a</span> <span class="type">str</span> &#123;</span><br><span class="line">    full_name.<span class="title function_ invoke__">split_whitespace</span>().<span class="title function_ invoke__">nth</span>(<span class="number">1</span>).<span class="title function_ invoke__">unwrap</span>()</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">fn</span> <span class="title function_">main</span>() &#123;</span><br><span class="line">    <span class="keyword">let</span> <span class="variable">name</span> = <span class="type">String</span>::<span class="title function_ invoke__">from</span>(<span class="string">&quot;Harry James Potter&quot;</span>);</span><br><span class="line">    <span class="keyword">let</span> <span class="variable">res</span> = <span class="title function_ invoke__">middle_name</span>(&amp;name);</span><br><span class="line">    <span class="built_in">assert_eq!</span>(res, <span class="string">&quot;James&quot;</span>);</span><br><span class="line"></span><br><span class="line">    <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="comment">    let res;</span></span><br><span class="line"><span class="comment">    &#123;</span></span><br><span class="line"><span class="comment">        let name = String::from(&quot;Harry James Potter&quot;);</span></span><br><span class="line"><span class="comment">        res = middle_name(&amp;name);</span></span><br><span class="line"><span class="comment">    &#125;</span></span><br><span class="line"><span class="comment">    assert_eq!(res, &quot;James&quot;);</span></span><br><span class="line"><span class="comment">    */</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>人们通常会想知道这样的函数声明是否意味着参数的关联生命周期必须（至少）与返回值一样长，反之亦然。</p><p>答案现在应该很明显。对函数来说，两个生命周期完全相同。但是由于可以强制，你可以将其借用更长的时间，甚至可以在获得结果之后缩短结果的关联生命周期。因此正确的答案是参数必须至少与返回值一样长。</p><p>而且，如果你创建一个通过引用接受多个参数的函数，并声明它们必须具有相等的关联生命周期（如在我们之前的示例中一样），则该函数的实际参数将被强制为其中最短的生命周期。这只是意味着结果不能超过任何借用的参数。</p><p>这与我们之前讨论的反向约束规则可以很好地配合。被调用者并不关心这些-它只是获得并返回相同生命周期的借用。</p><p>另一方面，调用者确保参数的关联生命周期永远不会比结果的生命周期短，可以通过扩展它们来实现。</p><h2 id="小技巧"><a href="#小技巧" class="headerlink" title="小技巧"></a>小技巧</h2><ul><li>你不能移走借用的值，因为在借用结束后该值必须保持有效。即使你在下一行中移回某些内容，也无法将其移出。但是 <code>[mem::replace](https://doc.rust-lang.org/std/mem/fn.replace.html)</code> 特征可以让你同时做这两件事。</li><li>如果你想拥有一个像 C++ 中的 <code>unique_ptr</code> 一样的指针，可以使用 <code>[Box](https://doc.rust-lang.org/std/boxed/index.html)</code> 类型。</li><li>如果你想进行一些基本的引用计数-例如 C ++ 中的 <code>shared_ptr</code> 和 <code>weak_ptr</code>，可以使用 <a href="https://doc.rust-lang.org/std/rc/index.html">这些标准模块</a></li><li>如果你确实需要摆脱 Rust 所施加的限制，则可以随时求助于 <a href="https://doc.rust-lang.org/nomicon/meet-safe-and-unsafe.html">unsafe code</a></li></ul><blockquote><p>如果发现译文存在错误或其他需要改进的地方，欢迎到 <a href="https://github.com/xitu/gold-miner">掘金翻译计划</a> 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 <strong>本文永久链接</strong> 即为本文在 GitHub 上的 MarkDown 链接。</p></blockquote><hr><blockquote><p><a href="https://github.com/xitu/gold-miner">掘金翻译计划</a> 是一个翻译优质互联网技术文章的社区，文章来源为 <a href="https://juejin.im/">掘金</a> 上的英文分享文章。内容覆盖 <a href="https://github.com/xitu/gold-miner#android">Android</a>、<a href="https://github.com/xitu/gold-miner#ios">iOS</a>、<a href="https://github.com/xitu/gold-miner#%E5%89%8D%E7%AB%AF">前端</a>、<a href="https://github.com/xitu/gold-miner#%E5%90%8E%E7%AB%AF">后端</a>、<a href="https://github.com/xitu/gold-miner#%E5%8C%BA%E5%9D%97%E9%93%BE">区块链</a>、<a href="https://github.com/xitu/gold-miner#%E4%BA%A7%E5%93%81">产品</a>、<a href="https://github.com/xitu/gold-miner#%E8%AE%BE%E8%AE%A1">设计</a>、<a href="https://github.com/xitu/gold-miner#%E4%BA%BA%E5%B7%A5%E6%99%BA%E8%83%BD">人工智能</a>等领域，想要查看更多优质译文请持续关注 <a href="https://github.com/xitu/gold-miner">掘金翻译计划</a>、<a href="http://weibo.com/juejinfanyi">官方微博</a>、<a href="https://zhuanlan.zhihu.com/juejinfanyi">知乎专栏</a>。</p></blockquote>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/c6ffca3ef902/</id>
    <link href="https://blog.zisheng.pro/posts/c6ffca3ef902/"/>
    <published>2023-04-19T09:59:17.000Z</published>
    <summary>本文由浅入深，深入浅出地讲解了 Rust 中的所有权、借用、生命周期，希望能帮助大家更好地理解 Rust 中的所有权、借用、生命周期。</summary>
    <title>深入理解 Rust 所有权、借用、生命周期</title>
    <updated>2026-09-18T08:15:28.640Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="React Native" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/React-Native/"/>
    <category term="React Native" scheme="https://blog.zisheng.pro/tags/React-Native/"/>
    <category term="Jest" scheme="https://blog.zisheng.pro/tags/Jest/"/>
    <category term="Enzyme" scheme="https://blog.zisheng.pro/tags/Enzyme/"/>
    <category term="单元测试" scheme="https://blog.zisheng.pro/tags/%E5%8D%95%E5%85%83%E6%B5%8B%E8%AF%95/"/>
    <category term="测试覆盖率" scheme="https://blog.zisheng.pro/tags/%E6%B5%8B%E8%AF%95%E8%A6%86%E7%9B%96%E7%8E%87/"/>
    <category term="TDD" scheme="https://blog.zisheng.pro/tags/TDD/"/>
    <content>
      <![CDATA[<h2 id="单元测试是什么-🧐"><a href="#单元测试是什么-🧐" class="headerlink" title="单元测试是什么 🧐"></a>单元测试是什么 🧐</h2><p>单元测试是用来对一个模块、一个函数或者一个类来进行正确性检验的测试工作。程序单元是应用的最小可测试部件，在 React 编程中，最小单元通常是组件、函数。如果你熟悉“测试驱动开发”（TDD：Test-Driven Development），单元测试也不会陌生，狭义来说就是单测驱动开发。</p><p>通常来说，程序员每修改一次程序就会进行最少一次单元测试，在编写程序的过程中前后很可能要进行多次单元测试，以证实程序达到软件规格书要求的工作目标，没有程序错误。在 TDD 中，甚至是先根据设计编写单元测试，然后根据单元测试写代码。</p><p>万丈高楼平地起，单元测试和文档一样，是保障程序最小单元质量的重要一环。试想一下，一块砖可能不需要使用说明书就可以量产使用，但是一块砖不经质检测验就投入使用带来的后果可能是恐怖的。从这个角度来看，单测可能是比文档更重要的存在。当然我们也不提倡为了单测而单测，单测是为了防范于未然。</p><h2 id="其他测试"><a href="#其他测试" class="headerlink" title="其他测试"></a>其他测试</h2><p>前端测试常见的测试类型有单元测试(Unit testing)、集成测试(Integration testing)、端到端(E2E testing)测试，一般我们投入的测试资源排序如下：</p><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/411210ad2c1b46a69444efca8b5f9c35~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><p>集成测试是在单元测试的基础上，集成多个模块进行测试，确保模块之间互动行为正确无误的工作。有时，单一的模块完全通过单元测试，单独使用也没有问题，但是当与其他模块配合使用时，可能就出现问题了，下图是未通过集成测试的例子：</p><p><img src="https://user-images.githubusercontent.com/88981/52933895-c0d47600-338f-11e9-9034-11e1ad0c42f1.gif" alt="紫升"></p><p>端到端测试是站在用户角度出发（一端）到真实运行环境（另一端）进行测试。一般我们会使用 Cypress、puppeteer 这些工具进行自动化测试以替代人肉测试。下图是未通过端到端测试的例子：</p><p><img src="https://imgur.com/download/Po1unSh" alt="紫升"></p><h2 id="测试覆盖率"><a href="#测试覆盖率" class="headerlink" title="测试覆盖率"></a>测试覆盖率</h2><p>我们在测试的时候，会经常关心我们的代码是否都测试到了，以及哪些代码没有测试到。jest 内置了 Istanbul 测试覆盖率工具，我们可以通过四个维度的覆盖率来了解代码测试覆盖率情况：</p><ul><li>Statements（stmts）：表达式覆盖率，是不是每个表达式都执行了？</li><li>Branches（Branch）：分支覆盖率，是不是每个 if 代码块都执行了？</li><li>Functions（Funcs）：函数覆盖率，是不是每个函数都调用了？</li><li>Lines（Lines）：行覆盖率，是不是每一行都执行了？</li></ul><p>下图是执行 <code>jest --coverage</code> 之后生成的命令行输出：</p><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/304867f9cd894882b110a3f36eb491f4~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><p>下图是生成的精美的测试覆盖率报告：</p><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0f9eaa5afd7c48a8a0c6e354075790db~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><p>点击 App.js 可以查看单个文件的测试覆盖率情况：</p><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9c6240f0c86b497999fe854b20533b1a~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><p>点开每个也没你，你会看到页面是五颜六色的，别担心，这些颜色都是有明确的意义：</p><ul><li>粉紅色的代码: 尚未被执行的 statement 或 function</li><li>黄色的代码: 沒被覆盖到的 branch</li><li>I: 代表 if-else 的 if 没有被执行<blockquote><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/530b087960784ddd821a4fe86d06525e~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p></blockquote></li><li>E: 代表 if-else 的 if 没有被执行<blockquote><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/2215ef958ea5409aad116f07f42543d8~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p></blockquote></li><li>Nx: 代表代码块被执行到的次数，可以作为代码性能的参考依据</li></ul><h2 id="安装依赖"><a href="#安装依赖" class="headerlink" title="安装依赖"></a>安装依赖</h2><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">yarn add jest -D</span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">babel</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">yarn add babel-jest -D</span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">enzyme</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">yarn add enzyme jest-enzyme enzyme-adapter-react-16 enzyme-to-json -D</span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">react-native-mock-render</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">yarn add react-native-mock-render -D</span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">types</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">yarn add @types/enzyme @types/jest @types/react @types/react-native -D</span></span><br></pre></td></tr></table></figure><p>工具介绍：</p><ul><li>jest: Jest 是一个令人愉快的 JavaScript 测试框架，专注于简洁明快。</li><li>enzyme: Enzyme 是用于 React 的 JavaScript 测试实用程序，可以更轻松地测试 React 组件的输出。您还可以根据给定的输出进行操作，遍历并以某种方式模拟运行时。</li><li>jest-enzyme: 针对 enzyme 的 Jest 断言</li><li>enzyme-adapter-react-16: React Native 测试所需的桥接器</li><li>enzyme-to-json: 将 Enzyme wrappers 转换成符合 Jest 快照测试的 JSON 格式。</li><li>react-native-mock-render: A fully mocked and test-friendly version of react native</li></ul><h2 id="配置"><a href="#配置" class="headerlink" title="配置"></a>配置</h2><h3 id="jest-config-js"><a href="#jest-config-js" class="headerlink" title="jest.config.js"></a>jest.config.js</h3><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="variable language_">module</span>.<span class="property">exports</span> = &#123;</span><br><span class="line">  <span class="attr">preset</span>: <span class="string">&#x27;react-native&#x27;</span>,</span><br><span class="line">  <span class="attr">verbose</span>: <span class="literal">true</span>,</span><br><span class="line">  <span class="attr">collectCoverage</span>: <span class="literal">true</span>,</span><br><span class="line">  <span class="attr">moduleNameMapper</span>: &#123;</span><br><span class="line">    <span class="comment">// for https://github.com/facebook/jest/issues/919</span></span><br><span class="line">    <span class="string">&#x27;^image![a-zA-Z0-9$_-]+$&#x27;</span>: <span class="string">&#x27;GlobalImageStub&#x27;</span>,</span><br><span class="line">    <span class="string">&#x27;^[@./a-zA-Z0-9$_-]+\\.(png|gif)$&#x27;</span>: <span class="string">&#x27;RelativeImageStub&#x27;</span>,</span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="attr">setupFilesAfterEnv</span>: [<span class="string">&#x27;&lt;rootDir&gt;/jest.setup.js&#x27;</span>],</span><br><span class="line">  <span class="attr">snapshotSerializers</span>: [<span class="string">&#x27;enzyme-to-json/serializer&#x27;</span>],</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><ul><li>collectCoverage: 生成测试覆盖率报告</li><li>setupFilesAfterEnv: 使用 Jest 运行安装文件以配置 Enzyme 和适配器（如下文<code>jest.setup.js</code>中所示），之前是<code>setupTestFrameworkScriptFile</code>，也可以使用<code>setupFiles</code></li><li>snapshotSerializers：推荐使用序列化程序使用<code>enzyme-to-json</code>，它的安装和使用非常简单，并允许您编写简洁的快照测试。</li></ul><blockquote><p>注意：Jest 在 <a href="https://stackoverflow.com/questions/55752673/option-setuptestframeworkscriptfile-was-replaced-by-configuration-setupfilesa">24.1.0</a> 之后只能使用 <code>setupFilesAfterEnv</code></p></blockquote><h3 id="jest-setup-js"><a href="#jest-setup-js" class="headerlink" title="jest.setup.js"></a>jest.setup.js</h3><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="string">&#x27;react-native&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> <span class="string">&#x27;react-native-mock-render/mock&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> <span class="string">&#x27;react-native/Libraries/Animated/src/bezier&#x27;</span>; <span class="comment">// for https://github.com/facebook/jest/issues/4710</span></span><br><span class="line"><span class="keyword">import</span> <span class="title class_">Enzyme</span> <span class="keyword">from</span> <span class="string">&#x27;enzyme&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> <span class="title class_">Adapter</span> <span class="keyword">from</span> <span class="string">&#x27;enzyme-adapter-react-16&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="title class_">Enzyme</span>.<span class="title function_">configure</span>(&#123; <span class="attr">adapter</span>: <span class="keyword">new</span> <span class="title class_">Adapter</span>() &#125;);</span><br></pre></td></tr></table></figure><h2 id="enzyme-入门"><a href="#enzyme-入门" class="headerlink" title="enzyme 入门"></a>enzyme 入门</h2><p>enzyme 是 Airbnb 开源的 react 测试类库，提供了一套简洁强大的 API，并通过 jquery 风格的方式进行 dom 处理，开发体验十分友好. 它提供三种测试方法.</p><h3 id="shallow"><a href="#shallow" class="headerlink" title="shallow"></a>shallow</h3><p>shallow 返回组件的浅渲染，对官方 shallow rendering 进行封装。浅渲染 作用就是：它仅仅会渲染至虚拟 dom，不会返回真实的 dom 节点，这个对测试性能有极大的提升。shallow 只渲染当前组件，只能能对当前组件做断言</p><h3 id="mount"><a href="#mount" class="headerlink" title="mount"></a>mount</h3><p>mount 方法用于将 React 组件加载为真实 DOM 节点。mount 会渲染当前组件以及所有子组件。多数情况下，shallow 方法就能满足我们的需求了。ref 测试则旨在 mount 模式下生效。</p><h3 id="render"><a href="#render" class="headerlink" title="render"></a>render</h3><p>render 采用的是第三方库 Cheerio 的渲染，渲染结果是普通的 html 结构，对于 snapshot 使用 render 比较合适。</p><h2 id="组件测试"><a href="#组件测试" class="headerlink" title="组件测试"></a>组件测试</h2><h3 id="组件快照测试"><a href="#组件快照测试" class="headerlink" title="组件快照测试"></a>组件快照测试</h3><p>当我们要确保 UI 不会意外更改时，快照测试都是非常有用的工具。通过 <code>toMatchSnapshot</code> 即可完成。</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="title function_">describe</span>(<span class="string">&#x27;Button Component&#x27;</span>, <span class="function">() =&gt;</span> &#123;</span><br><span class="line">  <span class="title function_">it</span>(<span class="string">&#x27;basic render&#x27;</span>, <span class="function">() =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">const</span> component = renderer.<span class="title function_">create</span>(<span class="language-xml"><span class="tag">&lt;<span class="name">Button</span> /&gt;</span></span>).<span class="title function_">toJSON</span>();</span><br><span class="line">    <span class="title function_">expect</span>(component).<span class="title function_">toMatchSnapshot</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><h4 id="componentDidMount"><a href="#componentDidMount" class="headerlink" title="componentDidMount"></a>componentDidMount</h4><p>通过调用 <code>shallow</code> 和 <code>mount</code> 方法，可以触发 componentDidMount 生命周期：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; shallow &#125; <span class="keyword">from</span> <span class="string">&#x27;enzyme&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">setup</span>(<span class="params">props = &#123;&#125;</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> wrapper = <span class="title function_">shallow</span>(<span class="language-xml"><span class="tag">&lt;<span class="name">CarouselComponent</span> /&gt;</span></span>);</span><br><span class="line">  <span class="keyword">const</span> instance = wrapper.<span class="title function_">instance</span>();</span><br><span class="line">  <span class="keyword">return</span> &#123; wrapper, instance &#125;;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="title function_">describe</span>(<span class="string">&#x27;Carousel Component&#x27;</span>, <span class="function">() =&gt;</span> &#123;</span><br><span class="line">  <span class="title function_">it</span>(<span class="string">&#x27;renders correctly&#x27;</span>, <span class="function">() =&gt;</span> &#123;</span><br><span class="line">    <span class="title function_">setup</span>();</span><br><span class="line">  &#125;);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p>也可以通过 <code>wrapper.setState</code> 方法进行触发：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; shallow &#125; <span class="keyword">from</span> <span class="string">&#x27;enzyme&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">setup</span>(<span class="params">props = &#123;&#125;</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> wrapper = <span class="title function_">shallow</span>(<span class="language-xml"><span class="tag">&lt;<span class="name">Component</span> &#123;<span class="attr">...props</span>&#125; /&gt;</span></span>);</span><br><span class="line">  <span class="keyword">const</span> instance = wrapper.<span class="title function_">instance</span>();</span><br><span class="line">  <span class="keyword">return</span> &#123; wrapper, instance &#125;;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="title function_">describe</span>(<span class="string">&#x27;Component&#x27;</span>, <span class="function">() =&gt;</span> &#123;</span><br><span class="line">  <span class="title function_">it</span>(<span class="string">&#x27;renders correctly&#x27;</span>, <span class="function">() =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">const</span> &#123; wrapper &#125; = <span class="title function_">setup</span>();</span><br><span class="line">    wrapper.<span class="title function_">setState</span>(&#123;</span><br><span class="line">      <span class="attr">enable</span>: <span class="literal">true</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><h4 id="componentWillUnMont"><a href="#componentWillUnMont" class="headerlink" title="componentWillUnMont"></a>componentWillUnMont</h4><p>通过调用 <code>wrapper.unmount()</code> 可以触发 componentWillUnMont 生命周期：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; shallow &#125; <span class="keyword">from</span> <span class="string">&#x27;enzyme&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">setup</span>(<span class="params">props = &#123;&#125;</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> wrapper = <span class="title function_">shallow</span>(<span class="language-xml"><span class="tag">&lt;<span class="name">Component</span> /&gt;</span></span>);</span><br><span class="line">  <span class="keyword">const</span> instance = wrapper.<span class="title function_">instance</span>();</span><br><span class="line">  <span class="keyword">return</span> &#123; wrapper, instance &#125;;</span><br><span class="line">&#125;</span><br><span class="line"><span class="title function_">describe</span>(<span class="string">&#x27;Carousel Component&#x27;</span>, <span class="function">() =&gt;</span> &#123;</span><br><span class="line">  <span class="title function_">it</span>(<span class="string">&#x27;renders correctly&#x27;</span>, <span class="function">() =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">const</span> &#123; wrapper &#125; = <span class="title function_">setup</span>();</span><br><span class="line">    <span class="title function_">expect</span>(wrapper).<span class="title function_">toMatchSnapshot</span>();</span><br><span class="line">    wrapper.<span class="title function_">unmount</span>();</span><br><span class="line">    <span class="title function_">expect</span>(wrapper).<span class="title function_">toMatchSnapshot</span>();</span><br><span class="line">  &#125;);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><h4 id="componentWillReceiveProps"><a href="#componentWillReceiveProps" class="headerlink" title="componentWillReceiveProps"></a>componentWillReceiveProps</h4><p>可以通过 <code>wrapper.setProps</code> 方法触发：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; shallow &#125; <span class="keyword">from</span> <span class="string">&#x27;enzyme&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">setup</span>(<span class="params">props = &#123;&#125;</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> wrapper = <span class="title function_">shallow</span>(<span class="language-xml"><span class="tag">&lt;<span class="name">Component</span> &#123;<span class="attr">...props</span>&#125; /&gt;</span></span>);</span><br><span class="line">  <span class="keyword">const</span> instance = wrapper.<span class="title function_">instance</span>();</span><br><span class="line">  <span class="keyword">return</span> &#123; wrapper, instance &#125;;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="title function_">it</span>(<span class="string">&#x27;componentWillReceiveProps&#x27;</span>, <span class="function">() =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">const</span> &#123; wrapper, instance &#125; = <span class="title function_">setup</span>(&#123;</span><br><span class="line">    <span class="attr">autoplay</span>: <span class="literal">true</span>,</span><br><span class="line">  &#125;);</span><br><span class="line">  wrapper.<span class="title function_">setProps</span>(&#123; <span class="attr">autoplay</span>: <span class="literal">false</span> &#125;);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><h3 id="定时器模拟（Timer-Mocks）"><a href="#定时器模拟（Timer-Mocks）" class="headerlink" title="定时器模拟（Timer Mocks）"></a>定时器模拟（Timer Mocks）</h3><p>原生定时器功能(即 setTimeout，setInterval，clearTimeout，clearInterval)对于测试环境来说不太理想，因为它们依赖于实时时间。<br>Jest 可以将定时器换成允许我们自己控制时间的功能。</p><p>这里我们通过调用 <code>jest.useFakeTimers()</code> 来启用假定时器。然后在需要的时候执行 <code>jest.runOnlyPendingTimers()</code> 来触发定时器：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; shallow &#125; <span class="keyword">from</span> <span class="string">&#x27;enzyme&#x27;</span>;</span><br><span class="line"></span><br><span class="line">jest.<span class="title function_">useFakeTimers</span>();</span><br><span class="line"></span><br><span class="line"><span class="title function_">it</span>(<span class="string">&#x27;autoplay methods with count(2) and os(ios)&#x27;</span>, <span class="function">() =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">const</span> &#123; wrapper, instance &#125; = <span class="title function_">setup</span>(&#123;</span><br><span class="line">    <span class="attr">autoplay</span>: <span class="literal">true</span>,</span><br><span class="line">    <span class="attr">loop</span>: <span class="literal">false</span>,</span><br><span class="line">  &#125;);</span><br><span class="line">  wrapper.<span class="title function_">setState</span>(&#123; <span class="attr">isScrolling</span>: <span class="literal">true</span> &#125;, <span class="function">() =&gt;</span> &#123;</span><br><span class="line">    jest.<span class="title function_">runOnlyPendingTimers</span>();</span><br><span class="line">  &#125;);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><h2 id="FAQ"><a href="#FAQ" class="headerlink" title="FAQ"></a>FAQ</h2><h3 id="如何忽略某一块代码"><a href="#如何忽略某一块代码" class="headerlink" title="如何忽略某一块代码"></a>如何忽略某一块代码</h3><p>添加以下格式的注释到要忽略的代码块前即可：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/* istanbul ignore next */</span></span><br></pre></td></tr></table></figure><h3 id="使用-mount-时，忽略-React-Native-的警告"><a href="#使用-mount-时，忽略-React-Native-的警告" class="headerlink" title="使用 mount 时，忽略 React Native 的警告"></a>使用 mount 时，忽略 React Native 的警告</h3><ul><li>参考自：<a href="https://github.com/enzymejs/enzyme/issues/831#issuecomment-352934963">Remove warnings when rendering react-native components</a></li></ul><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="title function_">describe</span>(<span class="string">&#x27;mounting&#x27;</span>, <span class="function">() =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">const</span> origConsole = <span class="variable language_">console</span>.<span class="property">error</span>;</span><br><span class="line">    <span class="title function_">beforeEach</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">      <span class="variable language_">console</span>.<span class="property">error</span> = <span class="function">() =&gt;</span> &#123;&#125;;</span><br><span class="line">    &#125;);</span><br><span class="line">    <span class="title function_">afterEach</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">      <span class="variable language_">console</span>.<span class="property">error</span> = origConsole;</span><br><span class="line">    &#125;);</span><br><span class="line">    it ......</span><br><span class="line">       mount....</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><h3 id="常见-issues"><a href="#常见-issues" class="headerlink" title="常见 issues"></a>常见 issues</h3><ul><li><p>enzyme</p><ul><li><a href="https://github.com/enzymejs/enzyme/issues/1436">Create Adapter for React Native &amp; React 16</a></li><li><a href="https://github.com/enzymejs/enzyme/issues/991">Can’t simulate press event in react-native</a></li><li><a href="https://github.com/enzymejs/enzyme/issues/1636">Shallow with New React Context API. Consumer not getting context</a></li><li><a href="https://stackoverflow.com/questions/40776121/enzyme-is-not-finding-component-by-props">Enzyme is not finding component by props</a></li></ul></li><li><p>jest</p><ul><li><a href="https://github.com/facebook/jest/issues/4710">TypeError: Cannot read property ‘Object.<anonymous>‘ of null</a></li><li><a href="https://stackoverflow.com/questions/46252396/jest-how-to-test-if-a-component-does-not-exist">Jest - how to test if a component does not exist?</a></li><li><a href="https://github.com/enzymejs/enzyme/issues/316">Refs not working in component being shallow rendered</a></li><li><a href="https://github.com/facebook/jest/issues/6434">ReferenceError: You are trying to <code>import</code> a file after the Jest environment has been torn down.</a></li></ul></li><li><p>react-native</p><ul><li><a href="https://github.com/facebook/jest/issues/919">requiring image in react-native</a></li><li><a href="https://github.com/facebook/jest/issues/3822">Cannot find module ‘setupDevtools’ from ‘setup.js’</a></li><li><a href="https://github.com/react-navigation/react-navigation/issues/7950#issuecomment-615220412">Unable to resolve module <code>./views/assets/back-icon.png</code></a></li></ul></li></ul>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/5e817da7587c/</id>
    <link href="https://blog.zisheng.pro/posts/5e817da7587c/"/>
    <published>2023-04-19T09:48:29.000Z</published>
    <summary>通常来说，程序员每修改一次程序就会进行最少一次单元测试，在编写程序的过程中前后很可能要进行多次单元测试，以证实程序达到软件规格书要求的工作目标，没有程序错误。在 TDD 中，甚至是先根据设计编写单元测试，然后根据单元测试写代码。</summary>
    <title>使用 Jest 和 Enzyme 进行 React Native 单元测试</title>
    <updated>2026-09-18T08:15:28.636Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="Go" scheme="https://blog.zisheng.pro/categories/Go/"/>
    <category term="Go" scheme="https://blog.zisheng.pro/tags/Go/"/>
    <category term="Cobra" scheme="https://blog.zisheng.pro/tags/Cobra/"/>
    <category term="CLI" scheme="https://blog.zisheng.pro/tags/CLI/"/>
    <content>
      <![CDATA[<h2 id="项目简介"><a href="#项目简介" class="headerlink" title="项目简介"></a>项目简介</h2><p><a href="https://github.com/tuya/tuya-panel-cli">tuya-panel-cli</a> 是一款基于 Go 语言的服务于涂鸦智能面板开发者的开发工具。</p><p>主要功能是基于 <a href="https://github.com/tuya/tuya-panel-demo">tuya-panel-demo</a> 仓库初始化一个面板项目、打包开发完成的项目以及工具后台自动检查更新。</p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/7eeb936770f44767ba14ce60a170737b~tplv-k3u1fbpfcp-zoom-1.image" style="zoom: 33%;" /><p>项目基于流行的 Go CLI 框架 Cobra 项目开发，基本按预期实现了需求和设计目标，为之后进一步完善开发者生态找到了抓手，为形成开发闭环添加了重要一环（皮一下）。</p><p>我在该项目中负责需求承接、技术选型（后边会介绍）、方案设计、方案落地以及项目交付整个流程的工作。当然这并没什么可吹嘘的，如您所见也并不是什么复杂的项目。我之所以很想复盘，是因为这个项目是由我从 0 到 1 再到 1.1 完全把控的一个项目。</p><h2 id="项目背景"><a href="#项目背景" class="headerlink" title="项目背景"></a>项目背景</h2><p>涂鸦智能是一家全球化物联网 AI + IOT 领军企业，紫升所在的 <a href="https://www.tuya.com/cn/product/product-development/IoTOS-Development">IoTOS</a> 部门主要是赋能开发者使用涂鸦 IoTOS, 自主选择不同芯片进行智能产品开发。</p><p>随着公司内部和公司外部开发者不断增多，维护两套开发工具已不再适应趋势。为了更好地服务广大开发者。我们需要搭建统一的开发者平台。开发者工具（CLI）无疑是很好的开端。</p><h2 id="技术选型"><a href="#技术选型" class="headerlink" title="技术选型"></a>技术选型</h2><p>在真正开始开发前，紫升分别对 Node、Deno 和 Go 的方案进行了调研。之所以没有直接使用 Node 进行开发，一是因为 Boss 要求源代码不开放，二是因为我们的开发者很多是搞嵌入式的。为了最大程度方便开发者（无需安装运行环境）我们们需要一个可以将 CLI 打包成二进制的方案。</p><h3 id="Node"><a href="#Node" class="headerlink" title="Node"></a><strong>Node</strong></h3><p>众所周知，Node 开发的 CLI 是需要通过 NPM 安装的，开发者需要在本地事先安装 Node 环境。这一点作为前端开发者可能习以为常，但是对于非前端开发人员或非程序员，运行你的脚本需要额外配置环境就会显得很突兀。</p><p>那 Node 有没有二进制打包方案呢？</p><p>答案是肯定的，紫升在调研中发现了<a href="https://github.com/oclif/oclif">oclif</a> 和 <a href="https://github.com/vercel/pkg">pkg</a>，两者都可以将开发好的程序进行跨平台打包。区别在于 pkg 只是一个简单地将程序打包成二进制可执行文件的工具，而 oclif 是一个完善的支持插件系统的 CLI 框架。</p><p>目前为止，oclif 看起来就是我们想要的，但是很遗憾，它由于将 Node 环境打包进安装程序造成安装包过大以及直接暴露源代码落选了（pkg 则是由于太过简陋的功能直接 pass）。</p><h3 id="Deno"><a href="#Deno" class="headerlink" title="Deno"></a>Deno</h3><p>紫升是一个 Deno 爱好者，<a href="https://juejin.cn/post/6854573220432248839">Deno从入门到跑路 | 🏆 技术专题第一期征文</a> 证明了我写过 Hello World。由于语法和 API 上都很接近 Node，所以成为了第二个考虑的方案。</p><p>Deno 在 1.7.0 加入了对 Windows 平台的编译支持后，完整支持了跨平台编译。<a href="https://github.com/youngjuning/seve">seve</a> 是我写的一个简单的 CLI Demo。</p><p>没有选择 Deno 的原因生态不够完善需要自己造的轮子有点多。比如没有成熟的跨平台编译、Flag 解析、问卷插件、彩色文字、loading、进度条插件。至于类似于 Node 的 Commander 和 cac 的 CLI 框架就更没有了。seve 就是我挖的一个大坑，本意是打造一款 Deno 版的 Commander，在接触 cac 之后改了主意。</p><h3 id="Go"><a href="#Go" class="headerlink" title="Go"></a>Go</h3><p>紫升也是一个 Go 的爱好者，同样的 <a href="https://juejin.cn/post/6860522117423857678">极速入门Go并爬取掘金专栏 | 🏆 技术专题第二期</a> 证明我写过 Hello World。之所以最后才考虑 Go，是因为作为前端开发者，Node 和 Deno 给我的心理压力很小或者几乎没有，而 Go 对我来说是既熟悉又陌生。如果不是前两者都无法符合要求，恐怕我是不会选择 Go 的。</p><p><strong>跨平台编译</strong></p><p>Go 本身就支持编译命令，下面便是使用 go 自带 <code>build</code> 命令编写的打包脚本：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">#</span><span class="language-bash">!/bin/bash</span></span><br><span class="line">name=&quot;tuya-panel-cli&quot;</span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">Windows</span></span><br><span class="line">CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o $&#123;name&#125;.exe main.go</span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">Linux</span></span><br><span class="line">CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o $&#123;name&#125; main.go</span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">MacOS</span></span><br><span class="line">CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o $&#123;name&#125; main.go</span><br></pre></td></tr></table></figure><p>你可能会觉得以上脚本很繁琐，Go 也有第三方的编译插件可以使用，比如 <a href="https://github.com/mitchellh/gox">gox</a> 和 <a href="https://github.com/karalabe/xgo">xgo</a>。</p><p><strong>CLI 框架</strong></p><p><a href="https://github.com/urfave/cli">urfave</a> 比较简单易上手，如果是小型的或者单命令的程序推荐选择，我选择的是 <a href="https://github.com/spf13/cobra">cobra</a>。调研 Cobra 的过程中，顺手整理翻译了<a href="https://juejin.cn/post/6924541628031959047">Cobra 中文文档</a>，有需要的掘友可以直接白嫖。我们看下 Cobra 的特性：</p><ul><li><p>简单的基于子命令的 CLIs：<code>app server</code>、<code>app fetch</code> 等；</p></li><li><p>完全兼容 <a href="https://zh.wikipedia.org/wiki/%E5%8F%AF%E7%A7%BB%E6%A4%8D%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E6%8E%A5%E5%8F%A3">POSIX（可移植操作系统接口）</a> 的标志（包括短版和长版）</p></li><li><p>嵌套子命令</p></li><li><p>全局、局部和级联的标志</p></li><li><p>使用 <code>cobra init appname</code> 和 <code>cobra add cmdname</code> 轻松生成应用程序和命令</p></li><li><p>智能提示（<code>app srver</code> …did you mean <code>app server</code>）</p></li><li><p>自动生成命令和标志的帮助</p></li><li><p>自动识别 <code>-h</code>、<code>--help</code> 等帮助标识</p></li><li><p>自动为你的应用程序生成的 bash 自动完成</p></li><li><p>自动为你的应用程序生成 man 手册</p></li><li><p>命令别名，以便你可以更改内容而不会破坏它们</p></li><li><p>定义自己的帮助，用法等的灵活性。</p></li><li><p>可选与 <a href="https://github.com/spf13/viper">viper</a> 紧密集成，可用于 <a href="https://12factor.net/zh_cn/">12factor</a> 应用程序</p></li></ul><blockquote><p>从特性便可以看出来 Cobra 既可以为我们提供丰富的特性，又可以让我们的方案落地。</p></blockquote><h2 id="实践过程"><a href="#实践过程" class="headerlink" title="实践过程"></a>实践过程</h2><p>实践过程中遇到的问题，以及解决方式？<br>最后达成了什么效果，或者什么样的优化和提升？</p><p>技术选型之后就是落地实践，项目框架搭建掘友可直接阅读前文提到的中文文档，这里不再赘述。下面我将按照 tuya-panel-cli 实现的命令分别讲述实践过程。</p><h3 id="init-初始化模板"><a href="#init-初始化模板" class="headerlink" title="init 初始化模板"></a>init 初始化模板</h3><blockquote><p>需求：<a href="https://github.com/tuya/tuya-panel-demo">tuya-panel-demo</a> 项目包含了所有的模板，我们需要实现提示用户选择对应模板并初始化到当前目录</p></blockquote><p>1、发布模板：<a href="https://github.com/tuya/tuya-panel-demo/blob/master/.github/workflows/release.yml">GitHub CI</a> 监听到有新的 git tag 被 push 到仓库，则将所有模板打包并发布一个草稿版的 release，管理员手动编写发行说明后正式发布 release。这个过程和 Deno 本身的版本管理是一致的。关于这部分内容可以阅读<a href="https://juejin.cn/post/6926899307735957511">叮，一份 Deno GitHub Action 源码解析请查收</a>。</p><p>2、选择模板：第一个思路是维护一个 <code>txt</code> 文件，通过 http 获取内容，这有点蠢，每次有新的模板或删除模板都需要手动维护；第二个思路是通过爬虫爬取 <code>https://github.com/tuya/tuya-panel-demo/releases/latest</code> 并取出模板列表。我用的爬虫插件是 <a href="github.com/gocolly/colly">colly</a>，选择提示插件是 <a href="github.com/manifoldco/promptui">promptui</a>。</p><p>3、下载模板：我是使用 <code>net/http</code> 手撸了一个文件下载函数，然后使用 <a href="github.com/mholt/archiver">archiver</a> 对压缩包解压到缓存目录，最后使用 <a href="github.com/otiai10/copy">copy</a> 复制到当前目录。</p><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ee160384538a4fba8882ccf08bed0cb0~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><h3 id="upgrade-升级命令"><a href="#upgrade-升级命令" class="headerlink" title="upgrade 升级命令"></a>upgrade 升级命令</h3><p><a href="https://juejin.cn/post/6924465443704930318">向 Deno 学习优秀的脚本管理</a> 讲述了如何管理我们的程序，<a href="https://juejin.cn/post/6925201316264443918">基于 Go 实现 Deno upgrade</a> 讲述了如何站在巨人的肩膀上实现了自己的 upgrade 命令。这里简要概述一下。</p><p>1、借鉴 Deno 的脚本管理方式，我们通过 github release 管理版本（Go 语言的插件管理也是这个套路）</p><p>2、阅读 Deno upgrade 命令源码并用 Go 实现（由此接触并喜欢上了 Rust）</p><p>3、在模仿的基础上实现了后台检查更新的功能</p><p>4、封装了 <a href="https://github.com/youngjuning/go-release">go-release</a> 插件并开源。</p><h3 id="package-打包"><a href="#package-打包" class="headerlink" title="package 打包"></a>package 打包</h3><p>这个命令是从内部项目迁移出来的，学到一个知识是文件顶部加 <code>// +build windows</code> 代表 windows 平台编译使用，<code>// +build linux darwin</code> 代表 linux 和 mac 下编译使用。</p><h3 id="homebrew"><a href="#homebrew" class="headerlink" title="homebrew"></a>homebrew</h3><p>为了进一步方便 Homebrew 用户，紫升将 tuya-panel-cli 发布到了 <a href="https://github.com/youngjuning/homebrew-tap">youngjuning&#x2F;homebrew-tap</a>，具体的内容请参考当时我写的文章<a href="https://juejin.cn/post/6922347045692899336">不会吧？不会吧！还有人不会发 Homebrew 包？</a></p><h2 id="总结思考"><a href="#总结思考" class="headerlink" title="总结思考"></a>总结思考</h2><ul><li>技术选型要实际写 demo，看上去合适的不一定真的合适</li><li>技术选型要考虑时间、生态，作为职业程序员不应该畏惧舒适区以外的世界</li><li>项目能够如期交付，一方面是 cobra 这个框架的强大和易用，二是由于先前储备的 Go 基础知识。不过这也没什么值得夸耀的，用到的语法都足够简单。</li><li>项目发布后，进行了文档的补充和提示的优化。暂未发现使用上的问题。</li><li>暂未和 viper 联动，后续有需求可以加上。</li><li>未来需要系统的学习 Go 语言，以便更好地优化项目。</li></ul><blockquote><p>最最重要的一点感悟是，一点要及时记录自己的学习和工作情况，就像记账一样。</p></blockquote>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/e238033719e7/</id>
    <link href="https://blog.zisheng.pro/posts/e238033719e7/"/>
    <published>2023-04-19T09:46:01.000Z</published>
    <summary>项目基于流行的 Go CLI 框架 Cobra 项目开发，基本按预期实现了需求和设计目标，为之后进一步完善开发者生态找到了抓手，为形成开发闭环添加了重要一环（皮一下）。</summary>
    <title>从零使用 Go 语言构建 CLI 项目</title>
    <updated>2026-09-18T08:15:28.636Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="紫升翻译计划" scheme="https://blog.zisheng.pro/categories/%E7%B4%AB%E5%8D%87%E7%BF%BB%E8%AF%91%E8%AE%A1%E5%88%92/"/>
    <category term="掘金翻译计划" scheme="https://blog.zisheng.pro/categories/%E6%8E%98%E9%87%91%E7%BF%BB%E8%AF%91%E8%AE%A1%E5%88%92/"/>
    <category term="Python" scheme="https://blog.zisheng.pro/tags/Python/"/>
    <category term="JSON" scheme="https://blog.zisheng.pro/tags/JSON/"/>
    <category term="CPython" scheme="https://blog.zisheng.pro/tags/CPython/"/>
    <category term="simplejson" scheme="https://blog.zisheng.pro/tags/simplejson/"/>
    <category term="ujson" scheme="https://blog.zisheng.pro/tags/ujson/"/>
    <category term="pysimdjson" scheme="https://blog.zisheng.pro/tags/pysimdjson/"/>
    <category term="python-rapidjson" scheme="https://blog.zisheng.pro/tags/python-rapidjson/"/>
    <category term="orjson" scheme="https://blog.zisheng.pro/tags/orjson/"/>
    <content>
      <![CDATA[<blockquote><ul><li>原文地址：<a href="https://levelup.gitconnected.com/json-encoding-decoding-with-python-62a2cae63a6a">JSON encoding&#x2F;decoding with Python</a></li><li>原文作者：<a href="https://medium.com/@martinthoma">Martin Thoma</a></li><li>译文出自：<a href="https://github.com/xitu/gold-miner">掘金翻译计划</a></li><li>本文永久链接：<a href="https://github.com/xitu/gold-miner/blob/master/article/2021/json-encoding-decoding-with-python.md">https://github.com/xitu/gold-miner/blob/master/article/2021/json-encoding-decoding-with-python.md</a></li><li>译者：<a href="https://github.com/youngjuning">紫升</a>、<a href="https://github.com/a651092254">雪刺</a></li><li>校对者：<a href="https://github.com/husiyu">Zoe</a></li></ul></blockquote><p>REST API 是 在全世界使用标准化的消息格式。JSON 是互联网上数据交换的基石，作为 JavaScript 的一个子集，它从一开始就获得了巨大的推广。它特别清晰易读的语法也有利于推广。</p><p>据我所知各种语言都有 JSON 库用于序列化和反序列化。实际上在 Python 中就有很多种 JSON 库。在下文中，我将为大家比较它们的异同。</p><h2 id="引用库"><a href="#引用库" class="headerlink" title="引用库"></a>引用库</h2><p><strong>CPython</strong> 本身具有一个 <a href="https://docs.python.org/3/library/json.html">json</a> 模块。它最初是由 Bob Ippolito 作为 simplejson 开发的，并被合并到 Python 2.4 中（<a href="https://docs.python.org/3/whatsnew/2.6.html#the-json-module-javascript-object-notation">源代码</a>）。CPython 遵循 Python 软件基金会（Python Software Foundation）许可协议。</p><p><strong>simplejson</strong> 仍然作为一个单独的库存在，你可以通过 pip 安装它。它是带有可选 C 扩展的纯 Python 库。 Simplejson 遵循 MIT 和 Academic Free License（AFL）许可协议。</p><p><a href="https://pypi.org/project/ujson/">ujson</a> 是对 C 语言库 Ultra JSON 的绑定。<a href="https://github.com/ultrajson/ultrajson">Ultra JSON</a> 由 ESN（<a href="https://techcrunch.com/2012/09/26/electronic-arts-buys-online-gaming-development-studio-esn/">一家电子艺术工作室公司</a>）开发，并获得了 <a href="https://tldrlegal.com/license/bsd-3-clause-license-(revised)">3条款BSD许可</a>。 Ultra JSON 在 Github 上拥有 3k 星，305 个 fork，50 个贡献者，最近一次提交的日期只有 12 天，而最后一次提交是在 5 天之前发布的。我听说它处于“维护模式”（<a href="https://github.com/ultrajson/ultrajson/issues/428#issuecomment-699456053">来源</a>），表明没有新的进展。</p><p><strong>pysimdjson</strong> 是对 C ++ 库 <a href="https://github.com/simdjson/simdjson">simdjson</a> 的绑定。 SIMDjson 从加拿大获得资助。simdjson 在 Github 上有 12.2k 颗星，611 个分支，63 个贡献者，最后一次提交是 11 小时前，而最后一个 issue 是 2 小时前创建的。</p><p><strong>python-rapidjson</strong> 是对 C ++ 库 <a href="https://github.com/Tencent/rapidjson">RapidJSON</a> 的绑定。 RapidJSON 由 <a href="https://en.wikipedia.org/wiki/Tencent">腾讯</a> 开发。 RapidJSON 在 GitHub 上有 9.8k 个星，2.7k 个 fork，150 个贡献者，最近一次提交大约在 2 个月前，而最后一个 issue 是 17 天前创建的。</p><p><a href="https://pypi.org/project/orjson/">orjson</a> 是一个 Python 软件包，依靠 Rust 来完成繁重的工作。</p><h2 id="成熟度和操作安全性"><a href="#成熟度和操作安全性" class="headerlink" title="成熟度和操作安全性"></a>成熟度和操作安全性</h2><p>上面所有提到的库都可以毫无问题地用作 benchmark 示例，切换 JSON 模块也不是什么大问题，但我仍然想确定相关模块是否支持。</p><p>CPython，simplejson，ujson 和 orjson 都认为他们自己已经可以投产了。</p><p>python-rapidjson 将自身标记为 alpha，但是一位维护人员说这是一个错误，并将很快得到修复（<a href="https://github.com/python-rapidjson/python-rapidjson/issues/140#issuecomment-699475354">资源</a>）。</p><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/7c990d034af342a8ba240e887ae86270~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><h2 id="问题"><a href="#问题" class="headerlink" title="问题"></a>问题</h2><p>判断一个库的问题是否能够被顺利解决，一个直接的方式是直接去它的仓库创建 issue，并观察后续的跟进反馈：</p><ul><li><a href="https://github.com/simplejson/simplejson/issues/267">SimpleJSON</a>：第二天我得到了答复，回答很明确，易于理解，友善。 <a href="https://github.com/xitu/gold-miner/blob/master/article/2021/undefined">Bob Ippolito</a> 回答了我。他是最初开发这个库的人，并且在 JSON 模块的 Python 文档中也提到了他！</li><li><a href="https://github.com/ultrajson/ultrajson/issues/428">uJSON</a>：30分钟内，我得到了一个清晰，友好，易于遵循的答案。 @hugovank</li><li><a href="https://github.com/ijl/orjson/issues/127">ORJSON</a>：10天没有反应，然后关闭，没有任何评论。</li><li><a href="https://github.com/TkTech/pysimdjson/issues/54">[PySIMDJSON]</a>：15天后无人答复。</li><li><a href="https://github.com/python-rapidjson/python-rapidjson/issues/140">Python-RapidJSON</a>：在30分钟内，我得到了一个清晰，友好，易于遵循的答案。十天后合并了一个<a href="https://github.com/python-rapidjson/python-rapidjson/pull/143">简单的PR</a>。</li></ul><p>通过以上操作我得出一个答案，它们基本上没有相互关系。</p><h2 id="基准测试（Benchmark"><a href="#基准测试（Benchmark" class="headerlink" title="基准测试（Benchmark)"></a>基准测试（Benchmark)</h2><p>为了正确地对不同的库进行基准测试，我考虑了以下情况：</p><ul><li><strong>API</strong>：交换信息的 Web 服务。它可能包含 Unicode 并具有嵌套结构。 Twitter API 的 JSON 文件听起来不错，可以对此进行测试。</li><li><strong>API JSON错误</strong>：我很好奇如果 JSON API 格式有错误，性能会如何变化。因此，我在中间删除了一个大括号。</li><li><strong>GeoJSON</strong>：我首先通过一个开源街道地图导出器 <a href="https://overpass-turbo.eu/">Overpass Turbo</a> 得到了 <a href="https://en.wikipedia.org/wiki/GeoJSON">GeoJSON</a> 格式的 JSON 文件。你将获得疯狂多的 JSON 文件，这些文件大多具有坐标，而且还很嵌套。</li><li><strong>机器学习</strong>：只是大量的浮点数列表。这些可能是神经网络层的权重。</li><li><strong>JSON行</strong>：结构化日志在行业中大量使用。如果分析这些日志，可能需要遍历千兆字节的数据。它们都是带有日期时间对象、消息、记录器、日志状态等信息的简单字集。</li></ul><h4 id="反序列化速度"><a href="#反序列化速度" class="headerlink" title="反序列化速度"></a>反序列化速度</h4><p>我将我的硬盘驱动器的读取速度设置了一个较低的上限，在以下3个图表中将以它作为基准。</p><p><img src="https://i.loli.net/2021/03/26/ypSPNaJseM24WZL.png" alt="紫升"></p><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e8f138e2304c45c886374ac04f8fff52~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a681aacfe9e34a6a9fbe808f1b9a8c94~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/fa658530b4224ef5909984ad9a818632~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ec272cb068f54f67b2233426d83e29fd~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><p>由此得出的结论是：</p><ul><li>Rapidjson 速度很慢，但是对于像 twitter.json 这样的小型 JSON，你不会注意到有什么不同。可以通过结构化日志看到这一点。</li><li>simdjson，orjson 和 ujson 都快得惊人。</li><li>对于大多数库而言，读取包含结构错误的 JSON 文件的速度相同。一个值得注意的例外是 Rapidjson。我猜一旦发现错误，它将中止读取文件。</li></ul><h4 id="序列化速度"><a href="#序列化速度" class="headerlink" title="序列化速度"></a>序列化速度</h4><p>在这里，我预先创建了 JSON 字符串,并以写入磁盘的时间作为基线测量了所需的时间。</p><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a6c6caa82b8b454caf79534eacc0f4f3~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5e3665d926b5418eaa07f45893f5bb74~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/8873d330493747f7906649e5af6c6c6f~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/fcc5c29d33cd43939ec6d8ae860f26ee~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><p>我由此得出的结论是：</p><ul><li>orjson 非常快，超级接近我的硬盘驱动器写入速度。 ujson 也非常接近。</li><li>Rapidjson 也很快，但与 orjson 或 ujson 不在同一级别。</li><li>simdjson 很慢。</li></ul><h2 id="专业的-JSON-工作流"><a href="#专业的-JSON-工作流" class="headerlink" title="专业的 JSON 工作流"></a>专业的 JSON 工作流</h2><p>最后总结一下，我想指出一些我之前看到并记录下来的问题：</p><ul><li><p>调用变量 <code>foo_json</code>：JSON 是一种字符串格式。如果不是字符串，则不是 JSON。<br>如果使用 <code>bar = json.loads(foo)</code> 反序列化 JSON，则 bar 不是 JSON。</p><p>你可以将 bar 序列化为与 JSON <code>foo</code> 等效的 JSON，但 bar 不是 JSON,这是一个 Python 对象，很像一个字典对象，就将它当作 <code>foo_json</code>。</p></li><li><p>属性会在各处进行检查：如果你收到 JSON 数据，很轻松就可以转换为 Python 对象（例如字典）并使用它。这对于概念验证代码或很小的 JSON 字符串来说是很好的选择。如果你不将其转换为 <a href="https://docs.python.org/3/library/dataclasses.html">dataclass</a> 之类的，它将一团糟。</p></li></ul><p><a href="https://github.com/samuelcolvin/pydantic">pydantic</a> 是一个超级有用的验证库。你可以使用自己喜欢的 JSON 库将 JSON 字符串解析为带有 字典&#x2F;列表&#x2F;字符串&#x2F;数字&#x2F;布尔值的 Python 基本表示形式，然后再使用 Pydantic 对其进行解析。这样做的好处是你知道以后要处理的内容。不再只是将 <code>Dict[str, Any]</code> 用作 <a href="https://medium.com/analytics-vidhya/type-annotations-in-python-3-8-3b401384403d">type annotation</a>,不再用没用的的编辑器自动完成，不再检查属性是否在整个代码中都存在。</p><p>要引入除默认 <code>json</code> 以外的其他 json 包，我建议使用此模式</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> ujson <span class="keyword">as</span> json</span><br></pre></td></tr></table></figure><p>对于 Flask，你可以使用其他 <a href="https://flask.palletsprojects.com/en/1.1.x/api/#flask.json.JSONEncoder">编码器</a>&#x2F;<a href="https://flask.palletsprojects.com/en/1.1.x/api/#flask.json.JSONDecoder">解码器</a>，如下所示：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> simplejson <span class="keyword">import</span> JSONEncoder, JSONDecoder</span><br><span class="line"></span><br><span class="line">app.json_encoder = JSONEncoder</span><br><span class="line">app.json_decoder = JSONDecoder</span><br></pre></td></tr></table></figure><h2 id="还可以看看"><a href="#还可以看看" class="headerlink" title="还可以看看"></a>还可以看看</h2><ul><li><a href="https://github.com/xitu/gold-miner/blob/master/article/2021/undefined">Daniel Lemire</a>: <a href="https://www.youtube.com/watch?v=wlvKAT7SZIQ">Parsing JSON Really Quickly: Lessons Learned</a></li><li><a href="https://github.com/xitu/gold-miner/blob/master/article/2021/undefined">Ng Wai Foong</a>: <a href="https://levelup.gitconnected.com/introduction-to-orjson-3d06dde79208">Introduction to orjson</a></li><li><a href="https://github.com/xitu/gold-miner/blob/master/article/2021/undefined">Nicolas Seriot</a>: <a href="http://seriot.ch/parsing_json.php">Parsing JSON is a Minefield</a></li></ul><blockquote><p>如果发现译文存在错误或其他需要改进的地方，欢迎到 <a href="https://github.com/xitu/gold-miner">掘金翻译计划</a> 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 <strong>本文永久链接</strong> 即为本文在 GitHub 上的 MarkDown 链接。</p></blockquote><hr><blockquote><p><a href="https://github.com/xitu/gold-miner">掘金翻译计划</a> 是一个翻译优质互联网技术文章的社区，文章来源为 <a href="https://juejin.im/">掘金</a> 上的英文分享文章。内容覆盖 <a href="https://github.com/xitu/gold-miner#android">Android</a>、<a href="https://github.com/xitu/gold-miner#ios">iOS</a>、<a href="https://github.com/xitu/gold-miner#%E5%89%8D%E7%AB%AF">前端</a>、<a href="https://github.com/xitu/gold-miner#%E5%90%8E%E7%AB%AF">后端</a>、<a href="https://github.com/xitu/gold-miner#%E5%8C%BA%E5%9D%97%E9%93%BE">区块链</a>、<a href="https://github.com/xitu/gold-miner#%E4%BA%A7%E5%93%81">产品</a>、<a href="https://github.com/xitu/gold-miner#%E8%AE%BE%E8%AE%A1">设计</a>、<a href="https://github.com/xitu/gold-miner#%E4%BA%BA%E5%B7%A5%E6%99%BA%E8%83%BD">人工智能</a>等领域，想要查看更多优质译文请持续关注 <a href="https://github.com/xitu/gold-miner">掘金翻译计划</a>、<a href="http://weibo.com/juejinfanyi">官方微博</a>、<a href="https://zhuanlan.zhihu.com/juejinfanyi">知乎专栏</a>。</p></blockquote>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/802686c84f7e/</id>
    <link href="https://blog.zisheng.pro/posts/802686c84f7e/"/>
    <published>2023-04-19T09:38:34.000Z</published>
    <summary>REST API 是 在全世界使用标准化的消息格式。JSON 是互联网上数据交换的基石，作为 JavaScript 的一个子集，它从一开始就获得了巨大的推广。它特别清晰易读的语法也有利于推广。</summary>
    <title>Python 中的 JSON 库：按速度、成熟度和操作安全性进行比较</title>
    <updated>2026-09-18T08:15:28.632Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="漫谈" scheme="https://blog.zisheng.pro/categories/%E6%BC%AB%E8%B0%88/"/>
    <category term="紫升" scheme="https://blog.zisheng.pro/tags/%E7%B4%AB%E5%8D%87/"/>
    <category term="跨端" scheme="https://blog.zisheng.pro/tags/%E8%B7%A8%E7%AB%AF/"/>
    <category term="掘金" scheme="https://blog.zisheng.pro/tags/%E6%8E%98%E9%87%91/"/>
    <category term="宗心" scheme="https://blog.zisheng.pro/tags/%E5%AE%97%E5%BF%83/"/>
    <content>
      <![CDATA[<h2 id="感谢掘金-Zoe"><a href="#感谢掘金-Zoe" class="headerlink" title="感谢掘金&amp;Zoe"></a>感谢掘金&amp;Zoe</h2><p>首先感谢掘金这个帮助开发者成长的社区，从 2020 年开始写博客至今，不仅沉淀（水）了近 80 篇博客，还完成了职业生涯的小突破。最重要的在优弧大大发掘我之后，借由掘金这个平台认识了很多好朋友和大佬。其中大圣和言哥对我的影响最多，在我多次迷茫或者瞎搞的时候给出建议或者直接敲打我。</p><p>话说回此次由 Zoe 同学主办的 <a href="https://juejin.cn/post/6933147477399109640">3 月闯关活动开启，等你来战！</a> 活动，一开始我是拒绝的，毕竟我没有换工作的打算。Zoe 同学告诉我不求职，总是有升职加薪的追求的吧，顺手就把 <a href="https://juejin.cn/post/6933147477399109640#heading-7">闯关路线二：升职加薪</a> 推荐给我，甚至拿出我欠优弧文章的事情来压我。嗯！我就是在 Zoe 同学不断约稿和不断催稿的情况下完成了闯关活动，直到活动快结束的时候我才知道还有导师职业规划指导的环节(p≧w≦q)。向我们勤劳又负责的 Zoe 同学致敬，您辛苦啦！以前是我错了，现在我想做个好人。</p><h2 id="活动历程"><a href="#活动历程" class="headerlink" title="活动历程"></a>活动历程</h2><p><strong>技术点评</strong></p><ul><li><a href="https://juejin.cn/post/6937983077209604127">使用 Jest 和 Enzyme 进行 React Native 单元测试|技术点评</a></li><li><a href="https://juejin.cn/post/6937124684030967845">「译」2021 Flutter 和 React Native 如何选|技术点评</a></li><li><a href="https://juejin.cn/post/6936391941806358535">当我们在聊 RN 时，我们在聊什么 | 技术点评</a></li></ul><p>其中第三篇获得了 <a href="https://juejin.cn/post/6939854159831105549#heading-8">优秀文章奖</a>，奖品是掘金周边大礼包！</p><p><strong>项目复盘</strong></p><ul><li><a href="https://juejin.cn/post/6940296964089053221">从零使用 Go 语言构建 CLI 项目 | 项目复盘</a></li></ul><p>由于在临安买房的事耽搁，这个阶段只输出了一篇文章，很荣幸再次获得 <a href="https://juejin.cn/post/6942744127461130277#heading-2">优秀项目复盘文章奖</a>，奖品还是掘金周边大礼包！我对象说我都可以卖掘金周边了，看我这三个缸子，一个泡茶，一个泡咖啡，一个白开水，还有两个在家躺尸：</p><img src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f0033f65290149cf947e41a6735fd914~tplv-k3u1fbpfcp-watermark.image" alt="WechatIMG101.jpeg" style="zoom:33%;" /><p><strong>成长计划</strong></p><ul><li><a href="https://juejin.cn/pin/6942466001271259172">在跨端领域继续深耕，一年内达到公司技术专家级别</a></li><li><a href="https://juejin.cn/pin/6942686885214945288">目前处于广泛涉猎技术，构建个人知识和技能体系的阶段。垂直领域的精深和知识体系的架…</a></li><li><a href="https://juejin.cn/pin/6943014460898410504">不断调整优化自己的目标，并严格要求自己：#网页链接#。虚心听取别人的建议和批评。</a></li><li><a href="https://juejin.cn/pin/6943271666751373326">肖老师，经验丰富，一个字稳</a></li><li><a href="https://juejin.cn/pin/6943864731392278542">不管什么事，我都有整理和记录的习惯。所以我经手的东西都有详细文档。离职的时候，领…</a></li></ul><p>肖老师是谁？肖老师是我老东家一起奋斗过的兄弟，一起写过代码，一起泡过澡，现在是晞格玛云技术总监，有合适的小伙伴我可以帮忙投递简历。</p><h2 id="感谢宗心大佬"><a href="#感谢宗心大佬" class="headerlink" title="感谢宗心大佬"></a>感谢宗心大佬</h2><p>当我知道还有导师指导职业规划这个事之后，我就拜托 Zoe 同学帮忙联系宗心大佬选我，一是很早就关注了闲鱼在 Flutter 方面的实践，二是宗心大佬带领的团队正是我所从事的跨端方向（具体聊了之后发现跨端只是闲鱼的一部分，后面会展开说）。感谢宗心大佬给了我这次机会，下面是宗心大佬的简介：</p><p><img src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/4ce993a4e5634984bc746356585ea242~tplv-k3u1fbpfcp-watermark.image" alt="紫升"></p><h2 id="跨端问与答"><a href="#跨端问与答" class="headerlink" title="跨端问与答"></a>跨端问与答</h2><p>问题是我提前准备的，都是现阶段令我焦虑和困惑的点。宗心大佬人真的很随和很 nice，也极其耐心地针对我的问题给出了答疑，真的就是拨云见日，令我茅塞顿开（此处非玩笑话）。可惜的是我这个憨憨虽然录了屏，但是没有声音，记忆它不完整，也不能完全复述大佬的意思！希望大家见谅，放上视频合照以纪念此次指导：</p><table><thead><tr><th>大佬侧颜</th><th>大佬正脸</th></tr></thead><tbody><tr><td><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6bce94fe51be45b68415f42ea977ee78~tplv-k3u1fbpfcp-watermark.image" style="zoom:33%;" /></td><td><img src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/7243bd2796204aafb62a75d59d62ee31~tplv-k3u1fbpfcp-watermark.image" style="zoom:33%;" /></td></tr></tbody></table><h3 id="Flutter-真的会干掉-React-Native-吗？"><a href="#Flutter-真的会干掉-React-Native-吗？" class="headerlink" title="Flutter 真的会干掉 React Native 吗？"></a>Flutter 真的会干掉 React Native 吗？</h3><h4 id="问题背景"><a href="#问题背景" class="headerlink" title="问题背景"></a>问题背景</h4><p>前一段 Flutter 2.0 出来的时候，一片唱衰 React Native，Flutter 完爆 RN 的声音让我很焦虑。在我的认知里，这两者并不存在你死我活的问题呀，而且早起两个同队都给出声明说不存在直接竞争关系，那么作为国内最早且使用 Flutter 最多的团队，宗心大佬是怎么思考这两者的呢？</p><h4 id="大佬解答"><a href="#大佬解答" class="headerlink" title="大佬解答"></a>大佬解答</h4><p>Flutter 和 React Native 所适用的业务场景是不一样的，Flutter 更多地可以替代原生开发作为 App 开发的新模式，闲鱼目前的基础链路就在用 Flutter 做归一的架构。而 React Native 可以做动态化的事情，比如手淘的活动引导页这类需要高度动态化的场景。</p><p>React Native 更多的是让前端可以写客户端，对前端更友好。而 Flutter 偏向于原生，对原生同学比较友好。</p><h4 id="我的感悟"><a href="#我的感悟" class="headerlink" title="我的感悟"></a>我的感悟</h4><p>技术存在即合理，站在宗心大佬的高度，一切技术的选型和架构一定是跟随业务场景走的。脱离业务去谈技术和架构，就是流氓行为，不负责任的，纯属制造焦虑的。</p><h3 id="国内有没有可能出现比肩-Flutter-和-React-Native-的框架？"><a href="#国内有没有可能出现比肩-Flutter-和-React-Native-的框架？" class="headerlink" title="国内有没有可能出现比肩 Flutter 和 React Native 的框架？"></a>国内有没有可能出现比肩 Flutter 和 React Native 的框架？</h3><h4 id="问题背景-1"><a href="#问题背景-1" class="headerlink" title="问题背景"></a>问题背景</h4><p>作为国内一线大厂，阿里是否有野心做出比肩甚至超越 Flutter 和 React Native 的框架呢?</p><h4 id="大佬解答-1"><a href="#大佬解答-1" class="headerlink" title="大佬解答"></a>大佬解答</h4><p>大佬对 Flutter 和 React Native 的理解很透彻，把问题一分为二。首先是 React Native，列举了 Weex 这个框架，RN的架构比较好实现。而 Flutter 的实现就是一个庞大的工程，需要对操作系统和浏览器引擎有深厚研究的团队才有希望做出。大佬列举了 UC、鸿蒙这类公司。</p><h4 id="我的感悟-1"><a href="#我的感悟-1" class="headerlink" title="我的感悟"></a>我的感悟</h4><p>看似是单一的框架，但背后是整个生态。难实现的不是框架，而是大环境和生态的建设。相信国家的精英们迟早会有属于我们自己的操作系统，自己的生态，自己的跨端框架。</p><h3 id="闲鱼是否放弃-Flutter？"><a href="#闲鱼是否放弃-Flutter？" class="headerlink" title="闲鱼是否放弃 Flutter？"></a>闲鱼是否放弃 Flutter？</h3><h4 id="问题背景-2"><a href="#问题背景-2" class="headerlink" title="问题背景"></a>问题背景</h4><p>因为上份工作是有机会在阿里园区呆过，听闻 Weex 已经被放弃了，所以问了一下宗心大佬进展。由于不是一个团队，大佬不方便代表 Weex 团队，但是对大家比较关心的闲鱼是否放弃 Flutter 做出了解答。</p><h4 id="大佬解答-2"><a href="#大佬解答-2" class="headerlink" title="大佬解答"></a>大佬解答</h4><p>前面我们提到，宗心大佬是闲鱼基础链路负责人，大佬也解答了 Flutter 的应用场景是可以替代原生开发的一些事情。那么闲鱼是否会放弃 Flutter 呢？答案是否定的，宗心大佬描述了闲鱼基础链路架构的方向：Flutter 作为基础链路的核心，辅以原生、H5 等容器，最终做到基础链路的归一。也就是说闲鱼并不是要放弃 Flutter，而是基于 Flutter 在做更大更深的事情。</p><p>目前跨端处于比较混乱的局面，RN 容器、H5 容器、原生壳、各厂封闭生态的小程序以及 Flutter 共存于一个 App 的情况屡见不鲜。这带来的不只是管理成本，还有性能问题。此时急需一个能够统一基础链路的技术，闲鱼基于多年的经验选择了 Flutter。</p><p>宗心大佬也透露了内部十分活跃的 Flutter 开源团队建设，相信不久就会为 Flutter 生态输入优质的开源项目。希望彼时在座的各位有机会能参与进去。</p><h4 id="我的感悟-2"><a href="#我的感悟-2" class="headerlink" title="我的感悟"></a>我的感悟</h4><p>其实，在涂鸦也有相应的小组在做同样的事情，在我们的沟通里叫一码多端。也是基于 Flutter 实现一套代码多端运行。这在跨端领域可以说是炙手可热的话题了。</p><h3 id="在跨端领域达到专家水平需要具备哪些指标？"><a href="#在跨端领域达到专家水平需要具备哪些指标？" class="headerlink" title="在跨端领域达到专家水平需要具备哪些指标？"></a>在跨端领域达到专家水平需要具备哪些指标？</h3><h4 id="问题背景-3"><a href="#问题背景-3" class="headerlink" title="问题背景"></a>问题背景</h4><p>嗯，技术专家是紫升目前努力的方向，也制定了一系列的目标和计划。但是我并不知道这些目标和计划是否正确！</p><h4 id="大佬解答-3"><a href="#大佬解答-3" class="headerlink" title="大佬解答"></a>大佬解答</h4><p>大佬将专家分为技术向专家和业务向专家。技术向专家比较少见，需要对某一个领域或某一个问题有很专精的研究，比如 Flutter 渲染引擎的优化、音视频方向的编码研究等等。而更常见的是业务向专家，这里的业务并不是具体的 ERP 或者电商。而是要有产品意识，需要在缺少产品的紧急情况下能顶上去的能力。</p><p>到 P7 有的是解决了行业解决不了的问题；有的是首创了一个架构并可以长期沿用；有的是在原有架构上做了创新性工作；有的是在某一个技术领域达到专家级别；有的是对某一类业务问题的技术实现非常精通。有的是凭借自身对技术整体把控。</p><p>大佬也表示了对方法论的肯定，建议我不要太过于执着于技术追求。以自己的经验告戒我还年轻，早些读一读经管类、管理类、心理类方面的书籍开阔思路，为以后的发展做准备。</p><p>宗心大佬最后还是劝诫我不要只在意技术方面的进步，能走的更远需要的不只是你的技术能力，更多的是综合能力，甚至是平衡生活和家庭的能力。</p><h4 id="我的感悟-3"><a href="#我的感悟-3" class="headerlink" title="我的感悟"></a>我的感悟</h4><p>每个人选择和适合的是不一样的，这受限于个人学识、经历、经验、精力以及家庭因素。我们无法通过简单地模仿他人达到目的。我们要做的是学习大佬的习惯、学习方法、对于技术框架的架构以及技术的前瞻性。</p><h3 id="您对于全栈开发的理解是什么样呢？"><a href="#您对于全栈开发的理解是什么样呢？" class="headerlink" title="您对于全栈开发的理解是什么样呢？"></a>您对于全栈开发的理解是什么样呢？</h3><h4 id="问题背景-4"><a href="#问题背景-4" class="headerlink" title="问题背景"></a>问题背景</h4><p>去年年底由于焦虑和迷茫，疯狂入手各种后端语言，其中包括 Deno、Go、甚至 Rust。大圣老师曾告诫我多看看前端框架源码，我却在广度上疯狂拓展。直到把自己搞的很疲惫，达到极限才开始反思我所认为的全栈是否是一个错误的概念。</p><h4 id="大佬解答-4"><a href="#大佬解答-4" class="headerlink" title="大佬解答"></a>大佬解答</h4><p>每个人的职业发展道路是不一样的，每个人的立身之本也不一样。紫升你思考一下你作为一个前端真的需要精通后端吗？你需要的不是精通那么多后端细枝末节，而是培养自己解决问题的通用能力。你精通了前端的加载优化、分包、异步编程、性能优化、引擎运行原理与调优等等这些深度问题。同样的经验放在后端是一样通用的。</p><p>大佬还以自己作为客户端起家最后能带领服务端的经验教导我，紫升你有一天能带服务端的时候，不是说你就要后端代码写的很溜。但是你要有大局观，遇到问题知道问题是什么还不够，还需要知道解决的方向以及安排可以解决这个问题的人去做。</p><p>我问大佬这些能力我不实际去学习如何获取？大佬说 Hello World 对你来说不难吧？整体的对一个技术的了解不难吧。在你不需要实际在工作中用到时这些就够了。你要做的首先还是把你安身立命的东西做到足够好，前后端只是职责划分，工程化和底层的本质是一样的。</p><p>现在云原生技术发展的这么迅速，作为前端，你也可以更多地关注云开发。这些也能让你自己做出一个项目出来。没必要说精通后端自己接口这样子。</p><h4 id="我的感悟-4"><a href="#我的感悟-4" class="headerlink" title="我的感悟"></a>我的感悟</h4><p>宗心大佬一番话点醒了梦中人，我的计划中放入过多的后端技能学习。这样并没有让我缓解焦虑，反倒是越来越焦虑。虽然还是很不舍，但我不得不逼自己暂时放弃 Deno、Rust 的学习。之所以要坚持 Go 的学习是因为我需要一个编译语言来解 JS 的毒，而 Go 足够简单不会对我造成太大的负担。</p><h3 id="我是在大前端方向突破还是专精跨端领域呢？"><a href="#我是在大前端方向突破还是专精跨端领域呢？" class="headerlink" title="我是在大前端方向突破还是专精跨端领域呢？"></a>我是在大前端方向突破还是专精跨端领域呢？</h3><h4 id="问题背景-5"><a href="#问题背景-5" class="headerlink" title="问题背景"></a>问题背景</h4><p>我从 18 年开始做 React Native 开发，积淀了很多业务开发知识和架构工程化方面的能力。一直在我心里过不去的坎是原生开发能力的欠缺。我很迷惑我到底是往大前端方向努力，还是闷头在跨端领域深挖。我指的不是 RN 和 Flutter，我指的是要不要把原生开发学一学。</p><h4 id="大佬解答-5"><a href="#大佬解答-5" class="headerlink" title="大佬解答"></a>大佬解答</h4><p>我建议你去学一下 iOS 和 安卓开发，起 Demo 玩一玩知道怎么运行的，如何开发的。大佬 12 年进阿里实习的时候也是不懂客户端开发的，当年资料更少。但是也就用一两周上手开始做项目。</p><p>但是宗心大佬还是告诫我一定要知道自己安身立命的东西是什么，不要忘记初心。不管是就业形势还是职业发展我更希望你能做一个懂客户端的大前端，而不是放弃前端往客户端突破。</p><h4 id="我的感悟-5"><a href="#我的感悟-5" class="headerlink" title="我的感悟"></a>我的感悟</h4><p>这和上一个全栈的问题类似，也是导致我越来越焦虑的问题之一。听了大佬的教导，我更加坚定了接下来的道路。原生开发我是要懂，而不是要什么都会。跨端和客户端还是不一样的。我的优势是在前端的积累，为什么要跟客户端转前端的同学抢饭碗呢？</p><h2 id="感悟"><a href="#感悟" class="headerlink" title="感悟"></a>感悟</h2><p>聊完之后我最大的感受是掘金的活动质量真的太高了，之前对宗心大佬不够了解，这一番聊下来，简直就是偶像级别的。以上所有的问题在没有聊之前几乎都没有自己的答案的，全部是迷茫的点。大佬不仅耐心的解惑，还以自身的实际经历为我的职业发展给出了清晰和明确的建议。再次感谢掘金和宗心大佬。</p>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/c3f1c12d5d33/</id>
    <link href="https://blog.zisheng.pro/posts/c3f1c12d5d33/"/>
    <published>2023-04-19T09:34:00.000Z</published>
    <summary>首先感谢掘金这个帮助开发者成长的社区，从 2020 年开始写博客至今，不仅沉淀（水）了近 80 篇博客，还完成了职业生涯的小突破。最重要的在优弧大大发掘我之后，借由掘金这个平台认识了很多好朋友和大佬。其中大圣和言哥对我的影响最多，在我多次迷茫或者瞎搞的时候给出建议或者直接敲打我。</summary>
    <title>闲鱼宗心大佬聊跨端职业发展</title>
    <updated>2026-09-18T08:15:28.640Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="紫升翻译计划" scheme="https://blog.zisheng.pro/categories/%E7%B4%AB%E5%8D%87%E7%BF%BB%E8%AF%91%E8%AE%A1%E5%88%92/"/>
    <category term="掘金翻译计划" scheme="https://blog.zisheng.pro/categories/%E6%8E%98%E9%87%91%E7%BF%BB%E8%AF%91%E8%AE%A1%E5%88%92/"/>
    <category term="紫升翻译计划" scheme="https://blog.zisheng.pro/tags/%E7%B4%AB%E5%8D%87%E7%BF%BB%E8%AF%91%E8%AE%A1%E5%88%92/"/>
    <category term="掘金翻译计划" scheme="https://blog.zisheng.pro/tags/%E6%8E%98%E9%87%91%E7%BF%BB%E8%AF%91%E8%AE%A1%E5%88%92/"/>
    <category term="微软" scheme="https://blog.zisheng.pro/tags/%E5%BE%AE%E8%BD%AF/"/>
    <category term="谷歌" scheme="https://blog.zisheng.pro/tags/%E8%B0%B7%E6%AD%8C/"/>
    <content>
      <![CDATA[<blockquote><ul><li>原文地址：<a href="https://www.theverge.com/2021/3/12/22327306/google-microsoft-attack-open-web-online-news-australia-laws">Google slams Microsoft for trying ‘to break the way the open web works’</a></li><li>原文作者：Tom Warren</li><li>译文出自：<a href="https://github.com/xitu/gold-miner">掘金翻译计划</a></li><li>本文永久链接：<a href="https://github.com/xitu/gold-miner/blob/master/article/2021/google-microsoft-attack-open-web-online-news-australia-laws.md">https://github.com/xitu/gold-miner/blob/master/article/2021/google-microsoft-attack-open-web-online-news-australia-laws.md</a></li><li>译者：<a href="https://github.com/youngjuning">紫升</a></li><li>校对者：<a href="https://github.com/zenblo">zenblo</a></li></ul></blockquote><p>谷歌和微软工程师可能会在 Chromium 浏览器项目开发上进行合作，但这并没有消除双方之间的企业政治斗争。今天谷歌对微软发起了严厉的攻击，指责它试图 “打破开放 web 的运作方式，以此来削弱竞争对手”。</p><p>谷歌对微软破坏其支持新闻业和出版商努力的攻击感到不满。今年一月，谷歌威胁要将其搜索引擎从澳大利亚移走以回应一项法律，该法律一旦实行将迫使谷歌为新闻出版商的内容付费。就在谷歌屈服并与新闻集团和其他出版商达成协议，确保其服务继续在澳大利亚提供之后几天，澳大利亚 <a href="https://www.theverge.com/2021/2/24/22283777/australia-new-media-bargaining-code-facebook-google-paying-news">2 月通过了这项法律</a>。另一方面，Facebook 确实取消了用户和出版商在澳内分享新闻内容的功能，这 <a href="https://www.theverge.com/2021/2/23/22296778/facebook-ends-australian-news-ban-bargaining-code-google">赢得了澳大利亚政府的一些让步</a>。</p><p>在这中间，<a href="https://news.microsoft.com/en-au/2021/02/03/microsoft-supports-australian-government-proposal-addressing-news-media-and-digital-platforms/">微软非常公开</a> 地表示支持澳大利亚的新法律，它甚至 <a href="https://www.reuters.com/article/us-australia-media-facebook-microsoft/microsoft-teams-up-with-eu-publishers-amid-facebooks-australian-spat-idUSKBN2AM1MB">与欧洲出版商联手</a> 呼吁网络平台达成协议，为新闻机构的内容付费。谷歌对微软参与其中并不满意，这是自 <a href="https://www.theverge.com/2013/11/22/5135626/scroogled-microsoft-ads-attack-google">Scroogled 时代</a> 以来，我们看到的第一次大规模的公开争辩。</p><p>谷歌全球事务主管肯特-沃克（Kent Walker）<a href="https://blog.google/products/news/google-commitment-supporting-journalism/">在一篇博客文章中</a> 说：”他们现在提出了自私的要求，甚至不惜打破开放 web 的工作方式，以削弱竞争对手的实力”。”这次最新的攻击标志着微软惯用手法的回归。沃克在文中放上了维基百科上的”恐惧、不确定性和怀疑”（FUD）条目的链接，并指责微软混淆视听，以转移对最近安全问题的注意力。</p><p>沃克说：“微软这次的攻击并非巧合，而是有备而来。这发生在 SolarWinds 攻击之后，在他们允许他们的数万名客户被黑客通过微软的主要漏洞攻击的时刻。” 沃克接着说：“微软被警告他们系统中存在的漏洞，我们知道这些漏洞正在被黑客利用，微软也在做止损工作。与此同时，他们的客户正在被称为 “电子邮件大劫案” 的事件中中收拾残局。所以对他们转移视线的老把戏我们不必大惊小怪”</p><p><img src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/00f4fd6fed7545018f48651d4f2bf549~tplv-k3u1fbpfcp-watermark.image" alt="image.png"></p><p>众议院司法委员会</p><p>谷歌的这一不同寻常的攻击也正值众议院司法委员会 <a href="https://judiciary.house.gov/">（House Judiciary Committee）</a> 研究自由和多元化新闻竞争的反垄断和商业方面的问题。谷歌认为它并没有从谷歌新闻中赚钱，但微软认为这要复杂得多，涉及到谷歌搜索广告、广告技术业务、广告交换、广告技术工具以及谷歌的整体消费者数据集。</p><p>微软总裁布拉德-史密斯在 <a href="https://docs.house.gov/meetings/JU/JU05/20210312/111315/HHRG-117-JU05-Wstate-SmithB-20210312.pdf">这篇文章</a> 说：“新闻机构有广告库存要卖，但他们不能再直接卖给那些想投放广告的人。相反，出于客观原因，他们必须使用谷歌的工具，在谷歌的广告交易所上运营，为谷歌的运营贡献数据，并向谷歌支付资金。所有这一切都影响了新闻机构甚至从自己网站上的广告中获得经济利益的能力。”</p><p>在出版商是否应该对由搜索巨头和 Facebook 主导的数字广告行业拥有更多控制权的核心论点上，谷歌和微软显然存在分歧。微软希望国会推进《新闻竞争与保护法》来让新闻机构能够与在线内容发行商进行集体谈判。</p><p>而谷歌则认为，其试图与新闻行业合作的 <a href="https://newsinitiative.withgoogle.com/">谷歌新闻计划</a> 足以帮助新闻机构。在微软和谷歌唇枪舌战的同时，<a href="https://judiciary.house.gov/calendar/eventsingle.aspx?EventID=4440">众议院司法委员会今天召开会议</a>，就数字时代新闻业的未来听取不那么尖锐的争论。</p><blockquote><p>如果发现译文存在错误或其他需要改进的地方，欢迎到 <a href="https://github.com/xitu/gold-miner">掘金翻译计划</a> 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 <strong>本文永久链接</strong> 即为本文在 GitHub 上的 MarkDown 链接。</p></blockquote><hr><blockquote><p><a href="https://github.com/xitu/gold-miner">掘金翻译计划</a> 是一个翻译优质互联网技术文章的社区，文章来源为 <a href="https://juejin.im/">掘金</a> 上的英文分享文章。内容覆盖 <a href="https://github.com/xitu/gold-miner#android">Android</a>、<a href="https://github.com/xitu/gold-miner#ios">iOS</a>、<a href="https://github.com/xitu/gold-miner#%E5%89%8D%E7%AB%AF">前端</a>、<a href="https://github.com/xitu/gold-miner#%E5%90%8E%E7%AB%AF">后端</a>、<a href="https://github.com/xitu/gold-miner#%E5%8C%BA%E5%9D%97%E9%93%BE">区块链</a>、<a href="https://github.com/xitu/gold-miner#%E4%BA%A7%E5%93%81">产品</a>、<a href="https://github.com/xitu/gold-miner#%E8%AE%BE%E8%AE%A1">设计</a>、<a href="https://github.com/xitu/gold-miner#%E4%BA%BA%E5%B7%A5%E6%99%BA%E8%83%BD">人工智能</a>等领域，想要查看更多优质译文请持续关注 <a href="https://github.com/xitu/gold-miner">掘金翻译计划</a>、<a href="http://weibo.com/juejinfanyi">官方微博</a>、<a href="https://zhuanlan.zhihu.com/juejinfanyi">知乎专栏</a>。</p></blockquote>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/70420a4b1330/</id>
    <link href="https://blog.zisheng.pro/posts/70420a4b1330/"/>
    <published>2023-04-19T09:31:35.000Z</published>
    <summary>谷歌和微软工程师可能会在 Chromium 浏览器项目开发上进行合作，但这并没有消除双方之间的企业政治斗争。今天谷歌对微软发起了严厉的攻击，指责它试图 “打破开放 web 的运作方式，以此来削弱竞争对手&quot;。</summary>
    <title>谷歌抨击微软“试图破坏开放 web 的运作方式”</title>
    <updated>2026-09-18T08:15:28.640Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="紫升翻译计划" scheme="https://blog.zisheng.pro/categories/%E7%B4%AB%E5%8D%87%E7%BF%BB%E8%AF%91%E8%AE%A1%E5%88%92/"/>
    <category term="掘金翻译计划" scheme="https://blog.zisheng.pro/categories/%E6%8E%98%E9%87%91%E7%BF%BB%E8%AF%91%E8%AE%A1%E5%88%92/"/>
    <category term="架构图" scheme="https://blog.zisheng.pro/tags/%E6%9E%B6%E6%9E%84%E5%9B%BE/"/>
    <category term="draw.io" scheme="https://blog.zisheng.pro/tags/draw-io/"/>
    <content>
      <![CDATA[<blockquote><ul><li>原文地址：<a href="https://betterprogramming.pub/solutions-architect-tips-the-5-types-of-architecture-diagrams-eb0c11996f9e">Solutions Architect Tips — The 5 Types of Architecture Diagrams</a></li><li>原文作者：<a href="https://medium.com/@allenheltondev">Allen Helton</a></li><li>译文出自：<a href="https://github.com/xitu/gold-miner">掘金翻译计划</a></li><li>本文永久链接：<a href="https://github.com/xitu/gold-miner/blob/master/article/2021/solutions-architect-tips-the-5-types-of-architecture-diagrams.md">https://github.com/xitu/gold-miner/blob/master/article/2021/solutions-architect-tips-the-5-types-of-architecture-diagrams.md</a></li><li>译者：<a href="https://github.com/youngjuning">紫升</a></li><li>校对者：<a href="https://github.com/PassionPenguin">PassionPenguin</a>、<a href="https://github.com/zenblo">Zz招锦</a></li></ul></blockquote><p>你是否曾经在会议中，有人试图解释一个软件系统如何工作？</p><p>我与一位解决方案架构师新手进行了一次对话。他试图描述他们提出的一个大约有八个不同组件的系统，而这些组件都以多种方式相互作用。</p><p>他们用手势和大量的 “这块和这块之间通过…… ” 来解释解决方案。</p><p>我能听懂他们嘴里说的每个字，但是把这些字连起来我什么都没弄明白。</p><p>在解释复杂的架构概念时，口头表述显得苍白无力。我一边跟着思路走，一边试图建立一个心理模型。我需要一个画面感。</p><p>我需要<strong>一张图表</strong>。</p><p>但不是说一张图就可以走遍天下。架构图并不是一个“一刀切”的解决方案。</p><p>我们最近讨论过，<a href="https://betterprogramming.pub/how-to-switch-from-software-developer-to-solutions-architect-5e0c12bdc4b1">作为一个解决方案架构师</a> 的一个重要能力是将你的想法有效地传达给技术和非技术受众。</p><p>你的图表必须考虑到这一点。如果你想把你的想法传达给不同的人群，你必须制作多个版本的图。</p><p>今天，我们将讨论你应该根据五个不同的受众制作的五种不同类型的图表。</p><p>我们将讨论一个业务是假想的但 API 是真实的例子：<a href="https://github.com/allenheltondev/gopher-holes-unlimited">无限地鼠洞</a>，我们在系统中添加一个新的地鼠进行追踪。</p><h2 id="1-流程图"><a href="#1-流程图" class="headerlink" title="1. 流程图"></a>1. 流程图</h2><p>你可以制作的最通用、一般来说影响最广的图是流程图。它是一个中高层的图，可以展示工作流的所有部分。</p><p>该图说明了业务流程中的流动部分。</p><p><img src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6cc634aefcc5437a86311d1a5ffca3f2~tplv-k3u1fbpfcp-watermark.image" alt="0_ZUyEi9v9becFhGfp.png"></p><h3 id="受众"><a href="#受众" class="headerlink" title="受众"></a>受众</h3><p>这种类型的图的受众通常是技术人员。它可以用来向架构委员会介绍一个想法，或者向开发人员描述一个业务流程如何工作。</p><h3 id="注意事项"><a href="#注意事项" class="headerlink" title="注意事项"></a>注意事项</h3><p><strong>架构流程图</strong>的主要内容是包含所有流动的部分。在 <a href="https://betterprogramming.pub/serverless-you-keep-using-that-word-i-do-not-think-it-means-what-you-think-it-means-c7d5516a5ecc">无服务器 AWS 环境</a> 的案例中，我们标注了每个托管服务以及服务之间的通信。</p><p>虽然没有描述这些部件之间如何交互的细节，但是图中确实显示了这些连接。它显示了数据如何在系统中流动。</p><h2 id="2-服务图"><a href="#2-服务图" class="headerlink" title="2. 服务图"></a>2. 服务图</h2><p>服务图从高层次上说明了连通性。它并不显示工作流或服务如何工作的任何细节，而是显示了发挥作用的主要部分。这是一个旨在显示应用程序中使用的内部与外部服务的图。</p><p><img src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/44d93cf3319b4515873197760ac20127~tplv-k3u1fbpfcp-watermark.image" alt="0_hgdP8Dhm9taCxsec.png"></p><h3 id="受众-1"><a href="#受众-1" class="headerlink" title="受众"></a>受众</h3><p>IT 和网络工程师往往对这种类型的图最感兴趣。他们关心你与外部服务的任何连接。另外，他们需要知道是否有任何内部连接需要被监控。</p><p>我经常用这种图来向高管们描述系统的工作原理。他们想知道主要应用程序之间的连接，没有什么比服务图更好地表示这些连接了。</p><h3 id="注意事项-1"><a href="#注意事项-1" class="headerlink" title="注意事项"></a>注意事项</h3><p>在构建<strong>架构服务图</strong>时，最好列出构成你的应用或生态系统的所有微服务。标明哪些服务之间相互通信，并确保区分你公司拥有的服务和外部的服务。</p><p>关于服务如何工作的细节对于这个高级图来说是不必要的。这是所有关于使应用程序运行的服务。</p><h2 id="3-角色图"><a href="#3-角色图" class="headerlink" title="3. 角色图"></a>3. 角色图</h2><p>重要的是要表明你的架构需要解决的业务问题。角色图描述了一个按时间顺序排列的视图和特定工作流中的角色。这是证明你在构建解决方案时已经考虑了业务因素的最佳工具。</p><p><img src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/8a04a6009c3d465ca053d2bcc70ede78~tplv-k3u1fbpfcp-watermark.image" alt="0_YS49oSULHmzUvx2L.png"></p><h3 id="受众-2"><a href="#受众-2" class="headerlink" title="受众"></a>受众</h3><p>面向企业的个人和产品所有者是这类图的目标受众。他们关注的是角色以及他们如何与系统互动。向他们展示一张<strong>谁做了什么</strong>和<strong>在什么时间做</strong>的图，它将完美地描述你的系统正在做什么。</p><h3 id="注意事项-2"><a href="#注意事项-2" class="headerlink" title="注意事项"></a>注意事项</h3><p><strong>架构角色图</strong>对 <a href="https://www.bpmn.org/">BPMN 模型</a> 进行了一些尝试。利用泳道来显示工作流中的不同角色。这种类型的图往往是偏底层的，因为它比其它图包含更多的细节。一定要标明角色、工作流，以及关于业务流程如何从一个步骤到另一个步骤的所有假设。</p><p>这些图还可以帮助那些刚接触一个领域的开发人员，并为他们将要构建的东西提供深刻的上下文。</p><h2 id="4-基础设施图"><a href="#4-基础设施图" class="headerlink" title="4. 基础设施图"></a>4. 基础设施图</h2><p>基础设施图是一个“所见即所得”的模型。它代表了已经实现的一切。它是一个偏底层的图，旨在包含服务、应用、生态系统中存在的一切。</p><p>这个图的目的是显示已经建立的东西和系统当前的工作方式。可以把它看作是你所构建的应用程序的<strong>蓝图</strong>。</p><p><img src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/df11db95ab9948ffa796ca1ae0e52a4e~tplv-k3u1fbpfcp-watermark.image" alt="0_fHIrAhbsy8vjXtrC.png"></p><h3 id="受众-3"><a href="#受众-3" class="headerlink" title="受众"></a>受众</h3><p>基础设施图的受众不同。它可以用来向开发人员展示他们在特定微服务中必须使用的东西。它也可以用来向客户展示你的公司为完成一项任务而使用的所有资源。</p><p>技术人员将是你的基础设施图的主要受众。由于你提供的是一个清单，而不是传达想法或业务流程，所以这个图的预期用途仅限于信息。这是为那些喜欢 “细枝末节” 细节的人准备的。</p><h3 id="注意事项-3"><a href="#注意事项-3" class="headerlink" title="注意事项"></a>注意事项</h3><p>在构建<strong>基础设施架构图</strong>时，不要漏掉任何一块。这种类型的图的目标是显示你的应用程序中的所有内容以及它们如何连接。你不需要在<strong>如何</strong>上做得太过详细，而是专注于让你的应用的所有部分都包含在图中。</p><h2 id="5-开发者图"><a href="#5-开发者图" class="headerlink" title="5. 开发者图"></a>5. 开发者图</h2><p>当你需要开始着手处理问题时，开发者图将是你最好的选择。它包括了开发人员为了构建解决方案所需要的一切。</p><p>我们的目标是回答任何可能通过查看<strong>流程图</strong>出现的问题，并在设计中包含它们。这是一堆图中最底层的图，目的是为了在你不在场的情况下传达想法。</p><p>有人应该能够读懂这张图，并确切地知道该怎么做。</p><p><img src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/cb9ebee9c83342449840372366222293~tplv-k3u1fbpfcp-watermark.image" alt="0_Ph0P0a9JRdHqDDlN.png"></p><h3 id="受众-4"><a href="#受众-4" class="headerlink" title="受众"></a>受众</h3><p>实现落地方案的开发人员是该类型图的受众。图中所包含的详细程度对于你团队以外的人来说是不必要的。有时，对于不需要细节的受众来说，太多细节可能是件坏事。</p><p>向开发团队以外的人提供实施细节就是一个过于详细的例子。它导致分散注意力，并掩盖了你试图传达的其他信息。</p><h3 id="注意事项-4"><a href="#注意事项-4" class="headerlink" title="注意事项"></a>注意事项</h3><p>开发者架构图本质上是补充了细节的<strong>流程图</strong>。用你能想到的任何具体的实现细节来标注每一块，并且一定要标注重要的转换。</p><p>这种类型的图并不能取代用户故事，但它确实有助于增强用户故事，增加整个开发团队的理解。当你可以使用它们的时候，因为当实现完成后，你将有一个有用的在未来可以参考的材料。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>架构图有很多类型。每一种都有独特的目的，很多都服务于不同的受众。作为一个解决方案架构师，你必须能够在推销你的想法时，向正确的人提供正确类型的图表。</p><p>通常情况下，一个版本的图是不够的。当我开始一个新的设计时，我总是从<strong>流程图</strong>开始。我把所有的想法都写下来，然后把它推荐给其他系统架构师。一旦我们就解决方案达成一致，我就会拿着这个图，把它变成一个<strong>角色图</strong>，然后拿给业务人员。</p><p>当我得到业务人员的签收后，我就可以自由地做出<strong>开发人员图</strong>和<strong>服务图</strong>。服务图是给高管的，以确保他们对我们正在做的事情有一个高层次的看法。开发者图是给将要落地解决方案的工程师的。</p><p>一旦解决方案构建完成，我们就可以更新<strong>基础设施图</strong>，以继续新的工作。</p><p>一张图片胜过千言万语，但当涉及到架构图时，它们可能胜过五千张。能够让人们快速、轻松地理解你的想法，是成为一个优秀的解决方案架构师的关键。</p><p>有了为不同的受众构建不同类型的图表的能力，你就能为自己的成功做好准备。</p><blockquote><p><strong>P.S. 我总是使用 <a href="https://draw.io/">draw.io</a> 来构建我的图表。这是一个免费的工具，为我们提供制作漂亮的各种各样的图表、模型所需的所有东西。</strong></p></blockquote><blockquote><p>如果发现译文存在错误或其他需要改进的地方，欢迎到 <a href="https://github.com/xitu/gold-miner">掘金翻译计划</a> 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 <strong>本文永久链接</strong> 即为本文在 GitHub 上的 MarkDown 链接。</p></blockquote><hr><blockquote><p><a href="https://github.com/xitu/gold-miner">掘金翻译计划</a> 是一个翻译优质互联网技术文章的社区，文章来源为 <a href="https://juejin.im/">掘金</a> 上的英文分享文章。内容覆盖 <a href="https://github.com/xitu/gold-miner#android">Android</a>、<a href="https://github.com/xitu/gold-miner#ios">iOS</a>、<a href="https://github.com/xitu/gold-miner#%E5%89%8D%E7%AB%AF">前端</a>、<a href="https://github.com/xitu/gold-miner#%E5%90%8E%E7%AB%AF">后端</a>、<a href="https://github.com/xitu/gold-miner#%E5%8C%BA%E5%9D%97%E9%93%BE">区块链</a>、<a href="https://github.com/xitu/gold-miner#%E4%BA%A7%E5%93%81">产品</a>、<a href="https://github.com/xitu/gold-miner#%E8%AE%BE%E8%AE%A1">设计</a>、<a href="https://github.com/xitu/gold-miner#%E4%BA%BA%E5%B7%A5%E6%99%BA%E8%83%BD">人工智能</a>等领域，想要查看更多优质译文请持续关注 <a href="https://github.com/xitu/gold-miner">掘金翻译计划</a>、<a href="http://weibo.com/juejinfanyi">官方微博</a>、<a href="https://zhuanlan.zhihu.com/juejinfanyi">知乎专栏</a>。</p></blockquote>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/36caef603791/</id>
    <link href="https://blog.zisheng.pro/posts/36caef603791/"/>
    <published>2023-04-19T09:29:27.000Z</published>
    <summary>本文将讨论你应该根据五个不同的受众制作的五种不同类型的图表。</summary>
    <title>解决方案架构师技巧：架构图的 5 种类型</title>
    <updated>2026-09-18T08:15:28.640Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="运维" scheme="https://blog.zisheng.pro/categories/%E8%BF%90%E7%BB%B4/"/>
    <category term="Docker" scheme="https://blog.zisheng.pro/tags/Docker/"/>
    <category term="宝塔" scheme="https://blog.zisheng.pro/tags/%E5%AE%9D%E5%A1%94/"/>
    <category term="Linux" scheme="https://blog.zisheng.pro/tags/Linux/"/>
    <category term="LAMP" scheme="https://blog.zisheng.pro/tags/LAMP/"/>
    <category term="LNMP" scheme="https://blog.zisheng.pro/tags/LNMP/"/>
    <content>
      <![CDATA[<h2 id="宝塔面板是什么？"><a href="#宝塔面板是什么？" class="headerlink" title="宝塔面板是什么？"></a>宝塔面板是什么？</h2><p>宝塔 Linux 面板是提升运维效率的服务器管理软件，支持一键 LAMP&#x2F;LNMP&#x2F;集群&#x2F;监控&#x2F;网站&#x2F;FTP&#x2F;数据库&#x2F;JAVA 等 100 多项服务器管理功能。有 30 个人的专业团队研发及维护，经过 200 多个版本的迭代，功能全，少出错且足够安全，已获得全球百万用户认可安装。运维要高效，装宝塔。</p><p><img src="https://www.bt.cn/Public/images/linux_pc_free.png" alt="紫升"></p><h2 id="为什么使用-Docker-安装？"><a href="#为什么使用-Docker-安装？" class="headerlink" title="为什么使用 Docker 安装？"></a>为什么使用 Docker 安装？</h2><ol><li>环境隔离，一个主机可以部署多套服务</li><li>宝塔对主机控制度较高，依赖关系复杂，而 Docker 可以保证宝塔服务不影响到宿主机其他服务。</li><li>由于和宿主机环境隔离，不容易被木马工具。</li><li>宝塔面板的兼容性顺序为 <code>Centos7.x &gt; Debian10 &gt; Ubuntu 20.04 &gt; Centos8.x &gt; Ubuntu 18.04 &gt; 其它系统</code>，而用户的主机五花八门，为了获取最佳的兼容性，使用 Docker 部署是不二选择。</li><li>Docker 管理成本低，方便迁移。</li><li>可随时管理容器，无需重置服务器。</li></ol><h2 id="Docker-安装"><a href="#Docker-安装" class="headerlink" title="Docker 安装"></a>Docker 安装</h2><p>推荐使用脚本安装：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">curl -fsSL https://get.docker.com -o get-docker.sh</span><br><span class="line">sudo sh get-docker.sh --mirror Aliyun</span><br></pre></td></tr></table></figure><p>安装成功后，会自动启动 Docker 服务。用户可以使用 <code>systemctl is-enabled docker</code> 来确认 Docker 服务是否是开机自启动。如果不是请使用 <code>systemctl enable docker</code> 设置自启动。</p><p><strong>代理加速</strong></p><p>执行 <code>nano /etc/docker/daemon.json</code> 中写入如下内容：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;registry-mirrors&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">    <span class="string">&quot;https://registry.docker-cn.com&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="string">&quot;https://mirror.baidubce.com&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="string">&quot;https://hub-mirror.c.163.com&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><h2 id="开端口教程，不开不能用"><a href="#开端口教程，不开不能用" class="headerlink" title="开端口教程，不开不能用"></a>开端口教程，不开不能用</h2><blockquote><p>宝塔需要的端口：8888、888、20、21、80、443</p></blockquote><ul><li>腾讯云：<a href="https://www.bt.cn/bbs/thread-1229-1-1.html">https://www.bt.cn/bbs/thread-1229-1-1.html</a></li><li>阿里云：<a href="https://www.bt.cn/bbs/thread-2897-1-1.html">https://www.bt.cn/bbs/thread-2897-1-1.html</a></li><li>华为云：<a href="https://www.bt.cn/bbs/thread-3923-1-1.html">https://www.bt.cn/bbs/thread-3923-1-1.html</a></li><li>京东云：<a href="https://docs.jdcloud.com/cn/virtual-machines/configurate-inbound-rules">https://docs.jdcloud.com/cn/virtual-machines/configurate-inbound-rules</a></li></ul><h2 id="安装宝塔镜像"><a href="#安装宝塔镜像" class="headerlink" title="安装宝塔镜像"></a>安装宝塔镜像</h2><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">curl -fsSL https://blog.zisheng.pro/shell/baota/install.sh | sh</span><br></pre></td></tr></table></figure><ul><li><code>/www/wwwroot</code> 挂载在宿主机的 <code>~/wwwroot</code> 目录</li><li><code>/www</code> 挂载在 <code>baota_www</code> volume，即 <code>/var/lib/docker/volumes/baota_www/</code></li></ul><blockquote><p>上面的脚本是为了我为了方便管理，等价于 <code>docker run -tid --name baota --net=host --privileged=true --shm-size=1g --restart always -v baota_www:/www -v ~/wwwroot:/www/wwwroot pch18/baota</code></p></blockquote><h3 id="登录方式"><a href="#登录方式" class="headerlink" title="登录方式"></a>登录方式</h3><ul><li>登陆地址：<code>http://&#123;&#123;面板ip地址&#125;&#125;:8888</code></li><li>初始账号：<code>username</code></li><li>初始密码：<code>password</code></li></ul><h3 id="宝塔面板命令行"><a href="#宝塔面板命令行" class="headerlink" title="宝塔面板命令行"></a>宝塔面板命令行</h3><p><strong>进入容器：</strong></p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker <span class="built_in">exec</span> -it baota bash</span><br></pre></td></tr></table></figure><p><strong>执行 <code>bt</code> 进行交互式操作：</strong></p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">[root@bebc0d8e35b1 wwwroot]<span class="comment"># bt</span></span><br><span class="line">===============宝塔面板命令行==================</span><br><span class="line">(1) 重启面板服务           (8) 改面板端口</span><br><span class="line">(2) 停止面板服务           (9) 清除面板缓存</span><br><span class="line">(3) 启动面板服务           (10) 清除登录限制</span><br><span class="line">(4) 重载面板服务           (11) 取消入口限制</span><br><span class="line">(5) 修改面板密码           (12) 取消域名绑定限制</span><br><span class="line">(6) 修改面板用户名         (13) 取消IP访问限制</span><br><span class="line">(7) 强制修改MySQL密码      (14) 查看面板默认信息</span><br><span class="line">(22) 显示面板错误日志      (15) 清理系统垃圾</span><br><span class="line">(23) 关闭BasicAuth认证     (16) 修复面板(检查错误并更新面板文件到最新版)</span><br><span class="line">(24) 关闭谷歌认证          (17) 设置日志切割是否压缩</span><br><span class="line">(25) 设置是否保存文件历史副本  (18) 设置是否自动备份面板</span><br><span class="line">(0) 取消</span><br><span class="line">===============================================</span><br></pre></td></tr></table></figure><h2 id="进阶配置"><a href="#进阶配置" class="headerlink" title="进阶配置"></a>进阶配置</h2><ol><li>登录面板后，请立即修改面板用户和面板密码，并建议修改面板端口和安全入口。</li><li>更新的话，可以使用面板提供的功能进行修复。</li></ol><h2 id="备份宝塔面板"><a href="#备份宝塔面板" class="headerlink" title="备份宝塔面板"></a>备份宝塔面板</h2><h3 id="备份容器"><a href="#备份容器" class="headerlink" title="备份容器"></a>备份容器</h3><p>如果我们迁移的话，不需要再重复上面的安装配置步骤，直接使用我提供的脚本：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">curl -fsSL https://blog.zisheng.pro/shell/baota/backup.sh | sh</span><br></pre></td></tr></table></figure><p>压缩包 <code>baota.tar</code> 将备份到当前目录，上传到新的服务器之后载入容器：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker load -i baota.tar</span><br></pre></td></tr></table></figure><p>恢复容器：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">curl -fsSL https://blog.zisheng.pro/shell/baota/restore.sh | sh</span><br></pre></td></tr></table></figure><h2 id="已知问题"><a href="#已知问题" class="headerlink" title="已知问题"></a>已知问题</h2><ol><li>部分机器初始化环境可能某些端口被占用，比如 80 端口被占用，使用 <code>netstat -lnp|grep 80</code> 查看进程 PID，然后使用 <code>kill -9 XXXX</code> 结束该进程 (xxxx 为进程的相关 pid)。</li><li>如果事先安装了 nginx，请先卸载，完全卸载请执行 <code>curl -fsSL https://blog.zisheng.pro/shell/nginx/uninstall.sh | sh</code></li></ol>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/b68410bd395b/</id>
    <link href="https://blog.zisheng.pro/posts/b68410bd395b/"/>
    <published>2023-04-18T09:19:26.000Z</published>
    <summary>宝塔 Linux 面板是提升运维效率的服务器管理软件，支持一键 LAMP/LNMP/集群/监控/网站/FTP/数据库/JAVA 等 100 多项服务器管理功能。</summary>
    <title>Docker 安装配置宝塔面板</title>
    <updated>2026-09-18T08:15:28.629Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="笔记" scheme="https://blog.zisheng.pro/categories/%E7%AC%94%E8%AE%B0/"/>
    <category term="React" scheme="https://blog.zisheng.pro/tags/React/"/>
    <category term="Vue.js" scheme="https://blog.zisheng.pro/tags/Vue-js/"/>
    <category term="Angular.js" scheme="https://blog.zisheng.pro/tags/Angular-js/"/>
    <category term="前端框架" scheme="https://blog.zisheng.pro/tags/%E5%89%8D%E7%AB%AF%E6%A1%86%E6%9E%B6/"/>
    <category term="UI" scheme="https://blog.zisheng.pro/tags/UI/"/>
    <category term="逻辑" scheme="https://blog.zisheng.pro/tags/%E9%80%BB%E8%BE%91/"/>
    <content>
      <![CDATA[<p>前端框架旨在帮助开发人员更有效地组织和管理UI和逻辑。以下是一些常见方法，用于描述和组织UI和逻辑</p><h2 id="组件化"><a href="#组件化" class="headerlink" title="组件化"></a>组件化</h2><p>前端框架通常支持将UI划分为可重用组件。这有助于减少代码复制和提高代码的可维护性。每个组件都具有自己的状态和属性，以及与之相关联的事件处理程序。</p><p>比如 React 哲学就是在构建用户界面时，首先会把它分解成一个个组件，然后，你需要把这些组件连接在一起，使数据流经它们。</p><h2 id="模块化"><a href="#模块化" class="headerlink" title="模块化"></a>模块化</h2><p>前端框架还经常使用模块化来组织 JavaScript 逻辑。这样做可以将应用程序拆分为小块，每个块具有自己的职责和功能。这有助于改善代码的可读性和可维护性，并使团队成员能够更轻松地协作。</p><p>比如 React 通过 Hooks 来实现模块化，Hooks 是一些可以让你在函数组件里“钩入” React state 及生命周期等特性的函数。</p><h2 id="数据绑定"><a href="#数据绑定" class="headerlink" title="数据绑定"></a>数据绑定</h2><p>前端框架通常包含数据绑定机制，允许开发人员将UI元素与模型数据绑定。这种技术使得当一个对象发生变化时，与之相关联的UI元素会自动更新，从而简化了UI的维护。</p><h2 id="路由"><a href="#路由" class="headerlink" title="路由"></a>路由</h2><p>前端框架通常支持路由机制，允许页面根据URL路径进行重新加载或刷新。这对于单页应用程序尤其有用，因为它可以在不刷新整个页面的情况下更新UI。</p><h2 id="状态管理"><a href="#状态管理" class="headerlink" title="状态管理"></a>状态管理</h2><p>一些前端框架提供了状态管理工具，例如 Redux 和 Vuex。这使得开发人员能够轻松地管理应用程序的状态，并在不同组件之间共享数据。</p><h2 id="模板语言"><a href="#模板语言" class="headerlink" title="模板语言"></a>模板语言</h2><p>前端框架通常使用模板语言来生成 UI 元素。这些语言允许开发人员将 JavaScript 逻辑嵌入到 HTML 中，并根据数据生成动态内容。比如 vue template 语法就是这样的。</p><h2 id="样式管理"><a href="#样式管理" class="headerlink" title="样式管理"></a>样式管理</h2><p>前端框架通常提供样式管理工具，例如 CSS 预处理器（如 Sass 或 Less）和 CSS 模块。这些工具使得样式更易于管理和维护，并提高了代码的可重用性。</p><h2 id="构建工具"><a href="#构建工具" class="headerlink" title="构建工具"></a>构建工具</h2><p>前端框架经常使用构建工具（例如 Webpack 或 Parcel）来自动化构建过程。这些工具可以执行任务，例如打包、压缩和转换代码，在开发和生产环境中进行分离。</p><h2 id="三大前端框架"><a href="#三大前端框架" class="headerlink" title="三大前端框架"></a>三大前端框架</h2><ul><li>Angular：由 Google 开发的 JavaScript 框架，提供完整的 MVC（模型-视图-控制器）解决方案，并包含许多内置的功能，例如表单验证、路由和 HTTP 请求管理。</li><li>React：由 Facebook 开发的 JavaScript 库，专注于 UI 层面的构建，提供虚拟 DOM（Document Object Model）和组件化架构，使得构建大规模应用程序更加容易。</li><li>Vue.js：一个轻量级的 JavaScript 框架，通过使用模板语言和指令来实现响应式数据绑定和组件化架构。</li></ul><h2 id="结论"><a href="#结论" class="headerlink" title="结论"></a>结论</h2><p>尽管前端框架有很多不同的实现方式，但它们的目标都是相似的：通过提供结构、约定和工具来帮助开发人员更容易地构建复杂的Web 应用程序。如果使用得当，前端框架可以大大简化开发流程，并提高代码的可维护性和可读性。</p>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/58a20039730b/</id>
    <link href="https://blog.zisheng.pro/posts/58a20039730b/"/>
    <published>2023-04-12T11:45:23.000Z</published>
    <summary>前端框架旨在帮助开发人员更有效地组织和管理 UI 和逻辑。本文讲解了一些常见的用于描述和组织 UI 和逻辑的方法</summary>
    <title>前端框架如何描述和组织 UI 与逻辑</title>
    <updated>2026-09-18T08:15:28.636Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="Google" scheme="https://blog.zisheng.pro/categories/Google/"/>
    <category term="Google AdSense" scheme="https://blog.zisheng.pro/categories/Google/Google-AdSense/"/>
    <category term="站点运营" scheme="https://blog.zisheng.pro/categories/%E7%AB%99%E7%82%B9%E8%BF%90%E8%90%A5/"/>
    <category term="广告" scheme="https://blog.zisheng.pro/categories/%E7%AB%99%E7%82%B9%E8%BF%90%E8%90%A5/%E5%B9%BF%E5%91%8A/"/>
    <category term="Google AdSense" scheme="https://blog.zisheng.pro/tags/Google-AdSense/"/>
    <category term="Google" scheme="https://blog.zisheng.pro/tags/Google/"/>
    <category term="Google Adsense 付款方式" scheme="https://blog.zisheng.pro/tags/Google-Adsense-%E4%BB%98%E6%AC%BE%E6%96%B9%E5%BC%8F/"/>
    <category term="Google Adsense 收款方式" scheme="https://blog.zisheng.pro/tags/Google-Adsense-%E6%94%B6%E6%AC%BE%E6%96%B9%E5%BC%8F/"/>
    <category term="Google Adsense 招商银行" scheme="https://blog.zisheng.pro/tags/Google-Adsense-%E6%8B%9B%E5%95%86%E9%93%B6%E8%A1%8C/"/>
    <category term="Google Adsense 电汇" scheme="https://blog.zisheng.pro/tags/Google-Adsense-%E7%94%B5%E6%B1%87/"/>
    <content>
      <![CDATA[<blockquote><p>当账户余额达到 100 美金时，Adsense 会显示收款账户设置选项，本文讲解了如何设置有效的收款方式。</p></blockquote><p>Google Adsense 并没有人工客服服务，由于 Google 在中国大陆的退出和服务受限，对于不熟悉相关操作流程而且无法直接联系客服获取帮助的人来说可能会存在一些困难。针对 Google AdSense 的付款方式，之前西联汇款和支票兑现方式确实很常见，但是如今也可以通过银行转账、Paypal 等方式来收取付款。我建议需要使用 Google 服务的人可以多查找可靠的网上教学资源，同时多了解相关政策和操作流程，以更好地利用这些工具。</p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1681051625654.png" alt="Google Adsense 添加付款方式"></p><p>自 2020 年起 Google 暂停西联汇款，采用电汇方式付款，其实经过改革，Google AdSense提现变得更加便捷，加上现在银行都开发了APP，收款+结汇都可以在线完成，下文以招商银行为例介绍收款的方法。</p><p>补充：中国各大银行都支持跨境收款，但是每家银行政策不一样，例如中国银行不支持公对私，广发银行需要提供合同协议等，经过验证，目前招商银行收款非常稳定，暂时没有要求需要提供相关文件。添加付款方式后 Google AdSense 账户达到 $100，每月 21 日（中国时间 22 日）付款到指定银行账户。</p><h2 id="添加付款方式"><a href="#添加付款方式" class="headerlink" title="添加付款方式"></a>添加付款方式</h2><div class="note danger modern"><p>由于国内信用卡的限制，无法接收境外汇款，所以必须使用储蓄卡付款。</p></div><p>进入 Google AdSense 管理后台点击<code>付款</code> -&gt; <code>付款信息</code> -&gt; <code>管理付款方式</code>，添加付款方式信息。银行卡必须为一类账户，二类及以下账户没有权限收款外币，所以到银行开户需要注意账户类型。</p><ul><li>付款人ID（可选）</li><li>银行账户上的姓名：LUO ZHU（姓名拼音大写）</li><li>银行名称：<code>China Merchants Bank, H.O. Shenzhen, China</code>（不需要改）</li><li>SWIFT BIC：<code>CMBCCNBS</code> (不需要改)</li><li>账号：招商银行卡号</li><li>重新输入账号：确认招商银行卡号</li></ul><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1681053357059.png" alt="总行接受境外汇款的路径"></p><p>以上信息是通过向招商银行 App 客服机器人问出来的：</p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1681053178244.png" alt="总行接受境外汇款的路径"></p><ul><li>收款行(Beneficiary＇s Bank): <code>China Merchants Bank, H.O. Shenzhen, China</code></li><li>Swift Code(BIC): <code>CMBCCNBS</code>（11位的可填写：<code>CMBCCNBSXXX</code>）</li><li>收款人(一卡通或存折或对公账号)账号(Beneficiary＇s a&#x2F;c no.):  <code>************</code></li><li>收款人名称(Beneficiary): ***(注:境内个人以我行开户证件姓名的汉语拼音为准，境外个人以我行开户证件姓名为准；对公客户英文名称)</li><li>地址(address): 深圳市深南大道7088号招商银行大厦（China Merchants Bank, H.O.,CMB Tower,7088 Shennan Boulevard, Shenzhen, China）</li></ul><h2 id="Google-AdSense-付款"><a href="#Google-AdSense-付款" class="headerlink" title="Google AdSense 付款"></a>Google AdSense 付款</h2><p>账号金额达到 $100，每月 22 日打款到指定招商银行账户，大约 2 个工作日会收到银行短信提示入账一笔美金。</p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1681053414520.png" alt="Google AdSense 付款"></p><h2 id="结汇"><a href="#结汇" class="headerlink" title="结汇"></a>结汇</h2><p>下载招商银行一卡通APP，进入”外汇结汇“委托结汇，卖出账户现有的美元现汇即可兑现为人民币，每个人年度结汇总额度（$50000）如果超出可到银行柜台申请额度。</p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1681053513525.png" alt="招商银行外汇结汇"></p><p>需要注意的是结汇业务办理时间是 8:30-22:00，如果超出时间，需要在下一个工作日办理。</p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1681053627730.png" alt="招商银行委托结汇"></p><p>如果有额度且在业务办理时间内，结汇委托页面如下：</p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1681053688032.png" alt="招商银行委托结汇"></p><p>最后祝大家 Google AdSense 付款顺利！</p>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/5de90b19f4d9/</id>
    <link href="https://blog.zisheng.pro/posts/5de90b19f4d9/"/>
    <published>2023-04-09T14:42:37.000Z</published>
    <summary>自2020年起，Google AdSense 已暂停使用西联汇款付款方式，而采用更为便捷的电汇方式进行付款。由于技术的不断发展，现在银行也都开发了APP，用户可以在线完成收款和结汇的操作。以下是以招商银行为例介绍 Google AdSense 收款方法</summary>
    <title>2023 Google Adsense 付款方式设置教程</title>
    <updated>2026-09-18T08:15:28.629Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="Google" scheme="https://blog.zisheng.pro/categories/Google/"/>
    <category term="Google AdSense" scheme="https://blog.zisheng.pro/categories/Google/Google-AdSense/"/>
    <category term="站点运营" scheme="https://blog.zisheng.pro/categories/%E7%AB%99%E7%82%B9%E8%BF%90%E8%90%A5/"/>
    <category term="广告" scheme="https://blog.zisheng.pro/categories/%E7%AB%99%E7%82%B9%E8%BF%90%E8%90%A5/%E5%B9%BF%E5%91%8A/"/>
    <category term="Google AdSense" scheme="https://blog.zisheng.pro/tags/Google-AdSense/"/>
    <category term="Google Adsense PIN 码" scheme="https://blog.zisheng.pro/tags/Google-Adsense-PIN-%E7%A0%81/"/>
    <category term="中国大陆" scheme="https://blog.zisheng.pro/tags/%E4%B8%AD%E5%9B%BD%E5%A4%A7%E9%99%86/"/>
    <category term="谷歌地址验证" scheme="https://blog.zisheng.pro/tags/%E8%B0%B7%E6%AD%8C%E5%9C%B0%E5%9D%80%E9%AA%8C%E8%AF%81/"/>
    <category term="谷歌广告" scheme="https://blog.zisheng.pro/tags/%E8%B0%B7%E6%AD%8C%E5%B9%BF%E5%91%8A/"/>
    <category term="谷歌广告收入" scheme="https://blog.zisheng.pro/tags/%E8%B0%B7%E6%AD%8C%E5%B9%BF%E5%91%8A%E6%94%B6%E5%85%A5/"/>
    <category term="谷歌身份验证" scheme="https://blog.zisheng.pro/tags/%E8%B0%B7%E6%AD%8C%E8%BA%AB%E4%BB%BD%E9%AA%8C%E8%AF%81/"/>
    <category term="谷歌地址验证最低限额" scheme="https://blog.zisheng.pro/tags/%E8%B0%B7%E6%AD%8C%E5%9C%B0%E5%9D%80%E9%AA%8C%E8%AF%81%E6%9C%80%E4%BD%8E%E9%99%90%E9%A2%9D/"/>
    <content>
      <![CDATA[<p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1681044391871.jpeg" alt="Google Adsense PIN 码"></p><p>为确保你的帐号安全无虞，在向你发放任何付款之前，谷歌会要求你先验证地址。验证地址的 PIN 码会以国际平信的方式邮寄给你。那么 2023 年中国大陆还能收到 PIN 码吗？答案是肯定的，只不过有一些需要注意的细节。</p><h2 id="地址验证的工作原理"><a href="#地址验证的工作原理" class="headerlink" title="地址验证的工作原理"></a>地址验证的工作原理</h2><p>1、当你的收入达到<a href="https://support.google.com/adsense/answer/1709871#v">进行验证所需的最低限额时</a>（目前中国账号是 10 美金），我们会将个人识别码（PIN 码）邮寄到您的付款地址。</p><p>PIN 码通常会在 2 至 4 周内寄达，但也可能需要更长时间，具体取决于您所在的地理位置。</p><div class="note warning modern"><p>如果我们要求您<a href="https://support.google.com/adsense/answer/7568896">验证自己的身份</a>，那么在您成功完成身份验证之前，谷歌不会给您寄送 PIN 码。</p></div><p>2、收到 PIN 码后，请将其输入 AdSense 帐号以验证您的地址。</p><p>自 PIN 码生成之日算起，您可以在 4 个月内将该码输入帐号。如果您在 4 个月后还未输入该码，我们会停止在您的页面上展示广告。</p><div class="note warning modern"><p>PIN 码是指在您的帐号余额达到进行验证所需的最低限额时生成的 6 位数字。PIN 码与发布商 ID 不同。请仅在 PIN 码验证卡片中输入正确的 PIN 码。请勿输入随机数字。如果 PIN 码输错三次，您的帐号将停止展示广告。</p></div><p>但是不用担心，如果你错过了 PIN 码，你可以申请线上验证 PIN 码。</p><h2 id="我的经验"><a href="#我的经验" class="headerlink" title="我的经验"></a>我的经验</h2><p>1、由于 Google Adsense 是以国际平信的形式发的 PIN 码，而国际平信的性质决定了是没有电话信息的，解决办法是在地址里面加上电话号码，这样就可以通过电话联系到你了。</p><p>2、如果过了两周多没有收到 PIN 码，可以自己到就近的中国邮政网点找一找，他们一般有专门负责国际平邮的分拣员。</p><p>由于我没想着能收到钱，所以之前填写地址的时候没有备注上电话，而且地址还有错别字，Google Adsense 又是在额度达到 10 美金后自动寄出的 PIN 码。所以我就等了两周多，还是没有收到，就去了中国邮政网点，并加了他们负责国际平信的快递员微信。第二天快递师傅就发微信告诉我这个到了好几天了。</p><p>而且由于我第二次改正了地址并在地址中备注了电话，我第二次申请的 PIN 码很快就也寄到了：</p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1681050930527.png" alt="Google Adsense PIN 码"></p>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/5328a4a14a49/</id>
    <link href="https://blog.zisheng.pro/posts/5328a4a14a49/"/>
    <published>2023-04-09T12:32:06.000Z</published>
    <summary>为确保你的帐号安全无虞，在向你发放任何付款之前，谷歌会要求你先验证地址。验证地址的 PIN 码会以国际平信的方式邮寄给你。那么 2023 年中国大陆还能收到 PIN 码吗？答案是肯定的，只不过有一些需要注意的细节。</summary>
    <title>2023 中国大陆可以收到 Google Adsense PIN 码吗?</title>
    <updated>2026-09-18T08:15:28.629Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="VS Code" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/VS-Code/"/>
    <category term="Git" scheme="https://blog.zisheng.pro/tags/Git/"/>
    <category term="VS Code" scheme="https://blog.zisheng.pro/tags/VS-Code/"/>
    <category term="时间线" scheme="https://blog.zisheng.pro/tags/%E6%97%B6%E9%97%B4%E7%BA%BF/"/>
    <category term="找回文件版本" scheme="https://blog.zisheng.pro/tags/%E6%89%BE%E5%9B%9E%E6%96%87%E4%BB%B6%E7%89%88%E6%9C%AC/"/>
    <category term="撤销修改" scheme="https://blog.zisheng.pro/tags/%E6%92%A4%E9%94%80%E4%BF%AE%E6%94%B9/"/>
    <category term="撤销保存" scheme="https://blog.zisheng.pro/tags/%E6%92%A4%E9%94%80%E4%BF%9D%E5%AD%98/"/>
    <category term="撤销提交" scheme="https://blog.zisheng.pro/tags/%E6%92%A4%E9%94%80%E6%8F%90%E4%BA%A4/"/>
    <category term="Local History" scheme="https://blog.zisheng.pro/tags/Local-History/"/>
    <content>
      <![CDATA[<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>你是否遇到过这样的情况：你修改了本地代码文件，但需要恢复到之前保存的未提交到 Git 存储库中的版本？</p><p>如果你是 Visual Studio Code 的用户，那么你很幸运，因为我将向你展示如何做到这一点…</p><div class="img-wrap"><div class="img-bg"><img class="img" src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1680956041180.png" style="width:100px;height:100px;"/></div></div><h2 id="步骤"><a href="#步骤" class="headerlink" title="步骤"></a>步骤</h2><p>在探索 VS Code 中众多的菜单选项和面板时，我最近偶然发现了一个鲜为人知的功能，叫做“时间线”（timeline），它可以显示给定文件的运行历史。</p><p>你可以在 VS Code 中打开一个给定的文件，在“资源管理器”菜单底部注意到一个名为“时间线”的面板，它显示给定文件的运行版本历史。它将你的本地文件保存历史（未提交更改）和 Git 存储库提交整合在一个地方。这是一个非常方便但鲜为人知的 VS Code 功能。</p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1680957163086.png" alt="vscode 资源管理器时间线面板"></p><p>点击时间轴面板中的条目，将显示所做修改的不同对比。</p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1680957154278.png" alt="vscode 资源管理器时间线面板"></p><p>需要注意的是，本地文件的保存历史只有在你用 VS Code 编辑文件时才会被跟踪。因此，如果你在 VS Code 之外编辑文件，你将不会看到任何文件的版本历史。</p><h2 id="Local-History"><a href="#Local-History" class="headerlink" title="Local History"></a>Local History</h2><p>VS Code 内置的时间线功能有一个局限：它只能显示你当前打开的文件的历史，如果你的文件丢失（比如由于 git pull 导致的丢失）那就没有办法找回了。</p><p>为了解决这个问题，我发现了一个名为 Local History 的 VS Code 扩展，它可以让你在 VS Code 中查看本地文件的历史版本。</p><h2 id="结语"><a href="#结语" class="headerlink" title="结语"></a>结语</h2><p>Visual Studio Code 有时会让你觉得像糖果店里的孩子……有这么多的闪光灯和旋钮要转，有时会让人有点不知所措。我经常觉得我几乎没有触及它广泛的功能集的表面。说到这里，我希望你能从这篇文章中了解到一些关于 Visual Studio Code 的新知识。</p><p>如果你有任何其他 VS Code 的技巧想分享。欢迎在下面的评论中发表，以帮助其他开发者。</p><p>祝编码愉快!</p>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/2a5412bc286d/</id>
    <link href="https://blog.zisheng.pro/posts/2a5412bc286d/"/>
    <published>2023-04-08T12:08:19.000Z</published>
    <summary>你是否遇到过这样的情况：你修改了本地代码文件，但需要恢复到之前保存的未提交到 Git 存储库中的版本？</summary>
    <title>如何在 VS Code 中找回以前的文件版本</title>
    <updated>2026-09-18T08:15:28.636Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="React Native" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/React-Native/"/>
    <category term="编程" scheme="https://blog.zisheng.pro/categories/%E7%BC%96%E7%A8%8B/"/>
    <category term="React Native" scheme="https://blog.zisheng.pro/tags/React-Native/"/>
    <category term="npm" scheme="https://blog.zisheng.pro/tags/npm/"/>
    <category term="rollup" scheme="https://blog.zisheng.pro/tags/rollup/"/>
    <category term="打包" scheme="https://blog.zisheng.pro/tags/%E6%89%93%E5%8C%85/"/>
    <category term="发布" scheme="https://blog.zisheng.pro/tags/%E5%8F%91%E5%B8%83/"/>
    <category term="React Native 插件" scheme="https://blog.zisheng.pro/tags/React-Native-%E6%8F%92%E4%BB%B6/"/>
    <content>
      <![CDATA[<h2 id="安装-rollup"><a href="#安装-rollup" class="headerlink" title="安装 rollup"></a>安装 rollup</h2><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ yarn add -D rollup</span><br></pre></td></tr></table></figure><h2 id="package-json"><a href="#package-json" class="headerlink" title="package.json"></a>package.json</h2><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><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;react-native-refined-components&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;0.1.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;refined react-native components&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;main&quot;</span><span class="punctuation">:</span> <span class="string">&quot;dist/cjs/index.js&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;module&quot;</span><span class="punctuation">:</span> <span class="string">&quot;dist/es/index.js&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;browser&quot;</span><span class="punctuation">:</span> <span class="string">&quot;dist/umd/index.js&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;types&quot;</span><span class="punctuation">:</span> <span class="string">&quot;dist/es/index.d.ts&quot;</span><span class="punctuation">,</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;build&quot;</span><span class="punctuation">:</span> <span class="string">&quot;rimraf dist/* &amp;&amp; rollup -c&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;dev&quot;</span><span class="punctuation">:</span> <span class="string">&quot;rollup -c -w&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="rollup配置文件"><a href="#rollup配置文件" class="headerlink" title="rollup配置文件"></a>rollup配置文件</h2><p>在根目录新建 <code>rollup.config.js</code>:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> pkg <span class="keyword">from</span> <span class="string">&#x27;./package.json&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> [</span><br><span class="line">  <span class="comment">// browser-friendly UMD build</span></span><br><span class="line">  &#123;</span><br><span class="line">    <span class="attr">input</span>: <span class="string">&quot;./src/index.ts&quot;</span>,</span><br><span class="line">    <span class="comment">// 输出</span></span><br><span class="line">    <span class="attr">output</span>: &#123;</span><br><span class="line">      <span class="attr">file</span>: pkg.<span class="property">browser</span>, <span class="comment">// 文件</span></span><br><span class="line">      <span class="attr">format</span>: <span class="string">&#x27;umd&#x27;</span>, <span class="comment">// 格式</span></span><br><span class="line">      <span class="attr">name</span>: <span class="string">&#x27;refined-components&#x27;</span>, <span class="comment">// 生成包名称，代表你的 iife/umd 包</span></span><br><span class="line">    &#125;,</span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="comment">// CommonJS (for Node) and ES module (for bundlers) build.</span></span><br><span class="line">  &#123;</span><br><span class="line"><span class="attr">input</span>: <span class="string">&quot;./src/index.ts&quot;</span>,</span><br><span class="line"><span class="attr">output</span>: [</span><br><span class="line">&#123; <span class="attr">file</span>: pkg.<span class="property">main</span>, <span class="attr">format</span>: <span class="string">&#x27;cjs&#x27;</span> &#125;,</span><br><span class="line">&#123; <span class="attr">file</span>: pkg.<span class="property">module</span>, <span class="attr">format</span>: <span class="string">&#x27;es&#x27;</span> &#125;</span><br><span class="line">    ],</span><br><span class="line">&#125;</span><br><span class="line">]</span><br></pre></td></tr></table></figure><h2 id="转换-json-文件为-ES6-modules"><a href="#转换-json-文件为-ES6-modules" class="headerlink" title="转换 .json 文件为 ES6 modules"></a>转换 <code>.json</code> 文件为 ES6 modules</h2><h3 id="安装-rollup-plugin-json"><a href="#安装-rollup-plugin-json" class="headerlink" title="安装 @rollup/plugin-json:"></a>安装 <code>@rollup/plugin-json</code>:</h3><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ yarn add -D @rollup/plugin-json</span><br></pre></td></tr></table></figure><h3 id="配置-rollup-config-js"><a href="#配置-rollup-config-js" class="headerlink" title="配置 rollup.config.js"></a>配置 <code>rollup.config.js</code></h3><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// rollup.config.js</span></span><br><span class="line"><span class="keyword">import</span> json <span class="keyword">from</span> <span class="string">&#x27;@rollup/plugin-json&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> pkg <span class="keyword">from</span> <span class="string">&#x27;./package.json&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> [</span><br><span class="line">  <span class="comment">// browser-friendly UMD build</span></span><br><span class="line">  &#123;</span><br><span class="line">    <span class="attr">input</span>: <span class="string">&quot;./src/index.ts&quot;</span>,</span><br><span class="line">    <span class="comment">// 输出</span></span><br><span class="line">    <span class="attr">output</span>: &#123;</span><br><span class="line">      <span class="attr">file</span>: pkg.<span class="property">browser</span>, <span class="comment">// 文件</span></span><br><span class="line">      <span class="attr">format</span>: <span class="string">&#x27;umd&#x27;</span>, <span class="comment">// 格式</span></span><br><span class="line">      <span class="attr">name</span>: <span class="string">&#x27;refined-components&#x27;</span>, <span class="comment">// 生成包名称，代表你的 iife/umd 包</span></span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="attr">plugins</span>: [</span><br><span class="line">      <span class="title function_">json</span>(),</span><br><span class="line">    ]</span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="comment">// CommonJS (for Node) and ES module (for bundlers) build.</span></span><br><span class="line">  &#123;</span><br><span class="line"><span class="attr">input</span>: <span class="string">&quot;./src/index.ts&quot;</span>,</span><br><span class="line"><span class="attr">output</span>: [</span><br><span class="line">&#123; <span class="attr">file</span>: pkg.<span class="property">main</span>, <span class="attr">format</span>: <span class="string">&#x27;cjs&#x27;</span> &#125;,</span><br><span class="line">&#123; <span class="attr">file</span>: pkg.<span class="property">module</span>, <span class="attr">format</span>: <span class="string">&#x27;es&#x27;</span> &#125;</span><br><span class="line">    ],</span><br><span class="line">    <span class="attr">plugins</span>: [<span class="title function_">json</span>()]</span><br><span class="line">&#125;</span><br><span class="line">]</span><br></pre></td></tr></table></figure><h2 id="加载并解析-CommonJS-模块"><a href="#加载并解析-CommonJS-模块" class="headerlink" title="加载并解析 CommonJS 模块"></a>加载并解析 CommonJS 模块</h2><p>我们写组件库或工具库时不可避免会用到外部库，这些外部库可能是符合 CommonJS 规范的。而 Rollup 力图实现 ES 模块的规范， 因此，加载 CommonJS 模块和使用 Node 模块位置解析逻辑都被实现为可选插件，默认情况下不在 Rollup 内核中。我们需要安装并配置 <a href="https://github.com/rollup/plugins/tree/master/packages/commonjs">CommonJS</a> 和 <a href="https://github.com/rollup/plugins/tree/master/packages/node-resolve">node-resolve</a> 插件。</p><h3 id="安装"><a href="#安装" class="headerlink" title="安装"></a>安装</h3><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ yarn add -D @rollup/plugin-node-resolve @rollup/plugin-commonjs</span><br></pre></td></tr></table></figure><h3 id="配置"><a href="#配置" class="headerlink" title="配置"></a>配置</h3><p>一般我们打的 cjs 和 esm 格式文件需要把第三放包打进来</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// rollup.config.js</span></span><br><span class="line"><span class="keyword">import</span> commonjs <span class="keyword">from</span> <span class="string">&#x27;@rollup/plugin-commonjs&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; nodeResolve &#125; <span class="keyword">from</span> <span class="string">&#x27;@rollup/plugin-node-resolve&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> pkg <span class="keyword">from</span> <span class="string">&#x27;./package.json&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> [</span><br><span class="line">  <span class="comment">// browser-friendly UMD build</span></span><br><span class="line">  &#123;</span><br><span class="line">    <span class="attr">input</span>: <span class="string">&quot;./src/index.ts&quot;</span>,</span><br><span class="line">    <span class="comment">// 输出</span></span><br><span class="line">    <span class="attr">output</span>: &#123;</span><br><span class="line">      <span class="attr">file</span>: <span class="string">&quot;./dist/umd/index.js&quot;</span>, <span class="comment">// 文件</span></span><br><span class="line">      <span class="attr">format</span>: <span class="string">&#x27;umd&#x27;</span>, <span class="comment">// 格式</span></span><br><span class="line">      <span class="attr">name</span>: <span class="string">&#x27;refined-components&#x27;</span>, <span class="comment">// 生成包名称，代表你的 iife/umd 包</span></span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="attr">plugins</span>: [</span><br><span class="line">      <span class="title function_">json</span>(),</span><br><span class="line">      <span class="title function_">commonjs</span>(), <span class="comment">// 加载 commonjs 模块</span></span><br><span class="line">      <span class="title function_">nodeResolve</span>() <span class="comment">// 将 commonjs 转换为 ES 模块</span></span><br><span class="line">    ]</span><br><span class="line">  &#125;</span><br><span class="line">]</span><br></pre></td></tr></table></figure><blockquote><p>注意：CommonJS 和 ES module 格式下我们一般不希望把第三方库打包到输出产物中，所以并不需要配置这两个插件。</p></blockquote><h2 id="忽略-warning-treating-module-as-external-dependency"><a href="#忽略-warning-treating-module-as-external-dependency" class="headerlink" title="忽略 warning-treating-module-as-external-dependency"></a>忽略 <code>warning-treating-module-as-external-dependency</code></h2><p>Rollup 默认只会解析相对路径的模块，像是 <code>import _ from &#39;lodash&#39;</code> 不会被打包进 bundle，并且打包时会有警告。如果你想忽略这些警告，你需要在 <code>external</code> 中指明这些外部模块。那么有没有更优雅的方式呢？答案是肯定的，我们只需要安装并配置 <code>rollup-plugin-node-externals</code> 插件即可。</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ yarn add -D rollup-plugin-node-externals</span><br></pre></td></tr></table></figure><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// rollup.config.js</span></span><br><span class="line"><span class="keyword">import</span> externals <span class="keyword">from</span> <span class="string">&#x27;rollup-plugin-node-externals&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> [</span><br><span class="line">  <span class="comment">// CommonJS (for Node) and ES module (for bundlers) build.</span></span><br><span class="line">  &#123;</span><br><span class="line"><span class="attr">output</span>: [</span><br><span class="line">&#123;</span><br><span class="line">        <span class="attr">file</span>: <span class="string">&#x27;./dist/cjs/index.js&#x27;</span>,</span><br><span class="line">        <span class="attr">format</span>: <span class="string">&#x27;cjs&#x27;</span>,</span><br><span class="line">        <span class="attr">exports</span>: <span class="string">&#x27;auto&#x27;</span>,</span><br><span class="line">      &#125;,</span><br><span class="line">&#123;</span><br><span class="line">        <span class="attr">file</span>: <span class="string">&#x27;./dist/es/index.js&#x27;</span>,</span><br><span class="line">        <span class="attr">format</span>: <span class="string">&#x27;es&#x27;</span></span><br><span class="line">      &#125;</span><br><span class="line">    ],</span><br><span class="line">    <span class="attr">plugins</span>: [</span><br><span class="line">      <span class="title function_">externals</span>(&#123;<span class="attr">deps</span>: <span class="literal">true</span>&#125;),</span><br><span class="line">    ]</span><br><span class="line">&#125;</span><br><span class="line">]</span><br></pre></td></tr></table></figure><h2 id="打包-ts-文件"><a href="#打包-ts-文件" class="headerlink" title="打包 ts 文件"></a>打包 ts 文件</h2><h3 id="安装依赖"><a href="#安装依赖" class="headerlink" title="安装依赖"></a>安装依赖</h3><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ yarn add -D rollup-plugin-typescript2 typescript</span><br></pre></td></tr></table></figure><h3 id="配置-1"><a href="#配置-1" class="headerlink" title="配置"></a>配置</h3><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> typescript <span class="keyword">from</span> <span class="string">&#x27;rollup-plugin-typescript2&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> [</span><br><span class="line">  <span class="comment">// browser-friendly UMD build</span></span><br><span class="line">  &#123;</span><br><span class="line">    <span class="attr">input</span>: <span class="string">&quot;./src/index.ts&quot;</span>,</span><br><span class="line">    <span class="comment">// 输出</span></span><br><span class="line">    <span class="attr">output</span>: &#123;</span><br><span class="line">      <span class="attr">file</span>: <span class="string">&#x27;./dist/umd/index.js&#x27;</span>, <span class="comment">// 文件</span></span><br><span class="line">      <span class="attr">format</span>: <span class="string">&#x27;umd&#x27;</span>, <span class="comment">// 格式</span></span><br><span class="line">      <span class="attr">name</span>: <span class="string">&#x27;refined-components&#x27;</span>, <span class="comment">// 生成包名称，代表你的 iife/umd 包</span></span><br><span class="line">      <span class="attr">globals</span>: &#123;</span><br><span class="line">        <span class="string">&#x27;react&#x27;</span>: <span class="string">&#x27;React&#x27;</span>,</span><br><span class="line">        <span class="string">&#x27;react-native&#x27;</span>: <span class="string">&#x27;reactNative&#x27;</span></span><br><span class="line">      &#125;,</span><br><span class="line">      <span class="attr">sourcemap</span>: <span class="literal">true</span></span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="attr">plugins</span>: [</span><br><span class="line">      <span class="comment">// 如果用了 rollup-plugin-node-resolve， 则必须将它放在 typescript 插件前面</span></span><br><span class="line">      <span class="title function_">typescript</span>(&#123;</span><br><span class="line">        <span class="attr">tsconfigOverride</span>: &#123;</span><br><span class="line">          <span class="attr">compilerOptions</span>: &#123; <span class="attr">declaration</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><br><span class="line">    <span class="attr">external</span>: [<span class="string">&quot;react&quot;</span>,<span class="string">&quot;react-native&quot;</span>],</span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="comment">// CommonJS (for Node) and ES module (for bundlers) build.</span></span><br><span class="line">  &#123;</span><br><span class="line"><span class="attr">input</span>: <span class="string">&#x27;./src/index.ts&#x27;</span>,</span><br><span class="line"><span class="attr">output</span>: [</span><br><span class="line">&#123; <span class="attr">dir</span>: <span class="string">&#x27;./dist/cjs/index.js&#x27;</span>, <span class="attr">format</span>: <span class="string">&#x27;cjs&#x27;</span>, <span class="attr">exports</span>: <span class="string">&#x27;auto&#x27;</span> &#125;,</span><br><span class="line">&#123; <span class="attr">dir</span>: <span class="string">&#x27;./dist/es/index.js&#x27;</span>, <span class="attr">format</span>: <span class="string">&#x27;es&#x27;</span> &#125;</span><br><span class="line">    ],</span><br><span class="line">    <span class="attr">plugins</span>: [</span><br><span class="line">      <span class="title function_">typescript</span>(),</span><br><span class="line">    ],</span><br><span class="line">    <span class="attr">external</span>: [<span class="string">&quot;react&quot;</span>,<span class="string">&quot;react-native&quot;</span>],</span><br><span class="line">&#125;</span><br><span class="line">]</span><br></pre></td></tr></table></figure><h3 id="tsconfig-json"><a href="#tsconfig-json" class="headerlink" title="tsconfig.json"></a>tsconfig.json</h3><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;compilerOptions&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;target&quot;</span><span class="punctuation">:</span> <span class="string">&quot;es5&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;module&quot;</span><span class="punctuation">:</span> <span class="string">&quot;esnext&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;declaration&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;jsx&quot;</span><span class="punctuation">:</span> <span class="string">&quot;react&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;strict&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;noImplicitAny&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">false</span></span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;skipLibCheck&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;moduleResolution&quot;</span><span class="punctuation">:</span> <span class="string">&quot;node&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;allowSyntheticDefaultImports&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;esModuleInterop&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;resolveJsonModule&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="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;exclude&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span><span class="string">&quot;dist&quot;</span><span class="punctuation">,</span><span class="string">&quot;rollup.config.js&quot;</span><span class="punctuation">]</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><h2 id="rollup-plugin-multi-input-使用"><a href="#rollup-plugin-multi-input-使用" class="headerlink" title="rollup-plugin-multi-input 使用"></a>rollup-plugin-multi-input 使用</h2><p>组件库比较大时，我们可能需要让我们的库支持 tree-shaking。那么你就不能将所有的文件都打入到一个文件中。<code>rollup-plugin-multi-input</code> 便是一个将打包产物输出到各自的文件中的插件。就像：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">src</span><br><span class="line">  - A.ts</span><br><span class="line">  - B.ts</span><br><span class="line">  - index.ts</span><br><span class="line">-&gt;</span><br><span class="line">dist</span><br><span class="line">  - A.js</span><br><span class="line">  - B.js</span><br><span class="line">  - index.js</span><br></pre></td></tr></table></figure><h3 id="安装依赖-1"><a href="#安装依赖-1" class="headerlink" title="安装依赖"></a>安装依赖</h3><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ yarn add -D rollup-plugin-multi-input</span><br></pre></td></tr></table></figure><h3 id="配置-2"><a href="#配置-2" class="headerlink" title="配置"></a>配置</h3><blockquote><p>注意：因为产物是多文件，所以需要用 <code>dir</code> 属性指定文件夹。</p></blockquote><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> multiInput <span class="keyword">from</span> <span class="string">&#x27;rollup-plugin-multi-input&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> [</span><br><span class="line">  ...</span><br><span class="line">  <span class="comment">// CommonJS (for Node) and ES module (for bundlers) build.</span></span><br><span class="line">  &#123;</span><br><span class="line"><span class="attr">input</span>: [<span class="string">&#x27;src/**/*.ts&#x27;</span>,<span class="string">&#x27;src/**/*.tsx&#x27;</span>],</span><br><span class="line"><span class="attr">output</span>: [</span><br><span class="line">&#123; <span class="attr">dir</span>: <span class="string">&#x27;./src&#x27;</span>, <span class="attr">format</span>: <span class="string">&#x27;cjs&#x27;</span>, <span class="attr">exports</span>: <span class="string">&#x27;auto&#x27;</span> &#125;,</span><br><span class="line">&#123; <span class="attr">dir</span>: <span class="string">&#x27;./src&#x27;</span>, <span class="attr">format</span>: <span class="string">&#x27;es&#x27;</span> &#125;</span><br><span class="line">    ],</span><br><span class="line">    <span class="attr">plugins</span>: [</span><br><span class="line">      <span class="title function_">multiInput</span>(),</span><br><span class="line">    ],</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><ul><li>rollup-plugin-progress: 打包进度条</li><li>rollup-plugin-terser: 压缩文件</li></ul>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/74c90b834cdf/</id>
    <link href="https://blog.zisheng.pro/posts/74c90b834cdf/"/>
    <published>2023-04-08T11:28:25.000Z</published>
    <summary>本文接绍了如何使用 rollup 打包 React Native 插件并发布的流程和使用到的 rollup 插件</summary>
    <title>使用 rollup 打包 React Native 插件并发布</title>
    <updated>2026-09-18T08:15:28.636Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="紫升翻译计划" scheme="https://blog.zisheng.pro/categories/%E7%B4%AB%E5%8D%87%E7%BF%BB%E8%AF%91%E8%AE%A1%E5%88%92/"/>
    <category term="掘金翻译计划" scheme="https://blog.zisheng.pro/categories/%E6%8E%98%E9%87%91%E7%BF%BB%E8%AF%91%E8%AE%A1%E5%88%92/"/>
    <category term="Node.js" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/Node-js/"/>
    <category term="Node.js" scheme="https://blog.zisheng.pro/tags/Node-js/"/>
    <category term="Hapi.js" scheme="https://blog.zisheng.pro/tags/Hapi-js/"/>
    <category term="Express.js" scheme="https://blog.zisheng.pro/tags/Express-js/"/>
    <category term="Nest.js" scheme="https://blog.zisheng.pro/tags/Nest-js/"/>
    <category term="Koa.js" scheme="https://blog.zisheng.pro/tags/Koa-js/"/>
    <category term="Socket.io" scheme="https://blog.zisheng.pro/tags/Socket-io/"/>
    <category term="Meteor.js" scheme="https://blog.zisheng.pro/tags/Meteor-js/"/>
    <category term="Adonis.js" scheme="https://blog.zisheng.pro/tags/Adonis-js/"/>
    <category term="Sails.js" scheme="https://blog.zisheng.pro/tags/Sails-js/"/>
    <content>
      <![CDATA[<blockquote><ul><li>原文地址：<a href="https://javascript.plainenglish.io/top-node-js-frameworks-to-use-in-2021-4951ee5940b8">Top Node.js Frameworks to use in 2021</a></li><li>原文作者：<a href="https://medium.com/@ronakataglowid">Ronak Patel</a></li><li>译文出自：<a href="https://github.com/xitu/gold-miner">掘金翻译计划</a></li><li>本文永久链接：<a href="https://github.com/xitu/gold-miner/blob/master/article/2021/top-node-js-frameworks-to-use-in-2021.md">https://github.com/xitu/gold-miner/blob/master/article/2021/top-node-js-frameworks-to-use-in-2021.md</a></li><li>译者：<a href="https://github.com/youngjuning">紫升</a></li><li>校对者：<a href="https://github.com/PassionPenguin">PassionPenguin</a>、<a href="https://github.com/xilihuasi">xilihuasi</a></li></ul></blockquote><p>Node.js 是最敏捷的服务端 web 应用平台，因为它为应用开发公司提供了构建可扩展的单一编程语言 web 平台的便利。它是最热门的开源的 JavaScript 运行时框架之一，具有跨平台属性，让我们可以在浏览器以外的环境运行代码。</p><h2 id="Node-js-的特别之处是什么？"><a href="#Node-js-的特别之处是什么？" class="headerlink" title="Node.js 的特别之处是什么？"></a>Node.js 的特别之处是什么？</h2><ol><li>它有一个干净简洁的代码库。</li><li>它最适合敏捷开发和原型设计。</li><li>它有一个庞大的生态系统以提供开源库。</li><li>它可以用于更快地开发企业级可扩展的应用程序。</li><li>它基于最常用的编程语言 —— JavaScript。</li></ol><h2 id="市场对-Node-js-的反应如何？"><a href="#市场对-Node-js-的反应如何？" class="headerlink" title="市场对 Node.js 的反应如何？"></a>市场对 Node.js 的反应如何？</h2><p><img src="https://cdn-images-1.medium.com/max/2366/1*4kohl3kB8tfsqcJY-f3xlA.png" alt="NPM 趋势"></p><p>图片来源：<a href="https://www.npmtrends.com/nodejs">npmtrends</a></p><p><strong>下面是一些关于 Node.js 的更多信息：</strong></p><ul><li>Amazon、Netflix、LinkedIn、eBay、PayPal 和 Reddit 使用 Node.js 作为他们的后端框架。</li><li>43% 的 Node.js 开发者使用这个框架创建企业级应用</li><li>Paypal 注意到改用 Node.js 后，平均响应时间显著缩短了 35%。</li><li>Node.js 可以将任何现有应用的性能提高 50%。</li><li>Node.js 降低了 58% 的应用开发成本。</li><li>Statista 提到，截至 2020 年初，Node 是开发者中使用最多的框架，占 51.4%。</li></ul><h2 id="使用-Node-js-框架的收益"><a href="#使用-Node-js-框架的收益" class="headerlink" title="使用 Node.js 框架的收益"></a>使用 Node.js 框架的收益</h2><ul><li>易扩展性</li><li>学习曲线低</li><li>JavaScript 全栈开发</li><li>庞大而活跃的社区</li><li>使用单一的代码库，以提高整体 web 性能。</li></ul><h2 id="2021-年最值得用的-Node-js-框架"><a href="#2021-年最值得用的-Node-js-框架" class="headerlink" title="2021 年最值得用的 Node.js 框架"></a>2021 年最值得用的 Node.js 框架</h2><p>我们已经介绍了 Node.js，并详细地了解了它的功能，现在我们可以讨论 2021 年最值得使用的 Node.js 框架啦。</p><h3 id="1-Hapi-js"><a href="#1-Hapi-js" class="headerlink" title="1. Hapi.js"></a>1. Hapi.js</h3><ul><li><strong>GitHub</strong><ul><li><strong>Stars</strong>：13.1k</li><li><strong>Forks</strong>：1.3k</li><li><strong>版本</strong>：v20.1.0</li><li><strong>贡献者</strong>：208</li><li><strong>使用人数</strong>：16.5k</li></ul></li><li><strong>NPM 周下载量</strong>：105,065</li><li><strong>License</strong>：BSD-3-Clause</li></ul><p><img src="https://cdn-images-1.medium.com/max/2316/1*bnq7cugj6hQYaLYlY2a53w.png" alt="Hapi.js 框架流行度"></p><p>Hapi.js 是众多开发者信赖的最简单、安全、可靠的框架之一。你可以使用 Hapi.js 来创建可扩展和健壮的应用程序，它具有最小的开销和开箱即用的功能。它是开发 JSON API 的顶级<a href="https://aglowiditsolutions.com/blog/hidden-cost-of-app-development/">Node.js</a>框架。</p><p><strong>Hapi.js 可以被用于：</strong></p><ul><li>网站</li><li>HTTP 代理应用</li><li>应用程序接口服务</li></ul><p><strong>Hapi.js 主要特性：</strong></p><ul><li>输入验证</li><li>日志</li><li>错误处理</li><li>代码可重用性</li><li>缓存</li><li>没有外部依赖</li><li>基于配置的功能</li><li>集成框架：在 Node 框架中提供全面的认证和授权 API 支持。</li></ul><p><strong>什么时候使用 Hapi.js：</strong></p><p>Hapi.js 是开发安全、实时、可扩展和社交媒体应用的理想选择。大多数移动应用开发者都喜欢用 Hapi.js 来创建代理和 API 服务器。</p><p><strong>谁在使用 Hapi.js：</strong></p><ul><li>Commercetools</li><li>Main Stack</li><li>Beam</li><li>Taggun</li><li>Artifakt</li></ul><h3 id="2-Express-js"><a href="#2-Express-js" class="headerlink" title="2. Express.js"></a>2. Express.js</h3><ul><li><strong>GitHub</strong><ul><li><strong>Stars</strong>：52.3k</li><li><strong>Forks</strong>：8.8k</li><li><strong>版本</strong>：v4.17.1</li><li><strong>贡献者</strong>：262</li><li><strong>使用人数</strong>：9.2m</li></ul></li><li><strong>NPM 周下载量</strong>：17,193,915</li><li><strong>License</strong>：MIT</li></ul><p><img src="https://cdn-images-1.medium.com/max/2080/1*oV-DuccRP7WvxgSabkTNqQ.png" alt="Express.js 流行度"></p><p>Express.js 是一个灵活而简约的 Node.js 应用框架。这个插件并不是围绕着特定的组件构建的，因此它并不限制你使用什么技术。这就给了开发者尝试的自由。他们还可以获得闪电般的配置和纯 JavaScript 体验，这些特性使 Express.js 成为快速原型设计和敏捷开发市场的有力竞争者。</p><p><strong>Express.js 可以被用于：</strong></p><ul><li>单页应用</li><li>多页应用</li><li>混合应用</li></ul><p><strong>Express.js 主要特性：</strong></p><ul><li>更快的服务端开发</li><li>赋能开发者更快地构建 RESTful API</li><li>Express 支持 MVC 架构，但需要开发者做一些额外工作</li><li>开箱支持 NoSQL 数据库</li></ul><p><strong>什么时候使用 Express.js：</strong></p><p>Express.js 是快速创建 Web 应用程序和服务的理想选择，因为它有现成的 API 生成工具。它是基于 JavaScript 的全栈方案 MEAN 的一部分。这意味着你可以使用 Express.js 来制作任何基于浏览器的企业级应用。</p><p><strong>谁在使用 Express.js：</strong></p><ul><li>FindHotel</li><li>Omnipresent</li><li>Okay</li><li>SiHub</li><li>TheDoe</li></ul><h3 id="3-Nest-js"><a href="#3-Nest-js" class="headerlink" title="3. Nest.js"></a>3. Nest.js</h3><ul><li><strong>Github</strong><ul><li><strong>Stars</strong>：35.5k</li><li><strong>Forks</strong>：3.4k</li><li><strong>版本</strong>：7.5.0</li><li><strong>贡献者</strong>：226</li><li><strong>使用人数</strong>：60.4k</li></ul></li><li><strong>NPM 周下载量</strong>：508,214</li><li><strong>License</strong>：MIT</li></ul><p><img src="https://cdn-images-1.medium.com/max/2000/1*uTZrckIaqRi2BolefxaVTg.png" alt="Nest.js 流行度"></p><p>Nest.js 是一个服务器端应用框架，它是为了解放开发者的生产力，让他们的生活变得更轻松而打造的。开发者通常为了更好地组织和管理代码而使用这个 Node.js 框架。</p><p><strong>Nest.js 可以被用于：</strong></p><ul><li>编写更清晰和可重用的代码。</li><li>编写具有更高层次结构的代码，如拦截器、过滤器、管道等；</li><li>编写可扩展、可测试和松散型应用程序。</li></ul><p><strong>Nest.js 主要特性</strong></p><ul><li>易于扩展：可与其他库一起使用。</li><li>允许开发人员完全使用纯 JavaScript 进行编码。</li><li>结合了函数式编程、面向对象编程和响应式编程的特点。</li><li>公开框架 API，帮助开发者使用各平台上的各种第三方模块。</li><li>它有一个详细且维护良好的文档。</li></ul><p><strong>什么时候使用 Nest.js：</strong></p><p>Nest.js 主要用于编写具有可扩展、可测试和松散耦合特点的应用。它将 Node.js 的扩展潜力提高到了一个全新的水平。它提供了结构和灵活性的适当平衡，可以高效地管理你的大型项目的代码，并且仍然有结构感可循。</p><p><strong>谁在使用 Nest.js：</strong></p><ul><li>Roche</li><li>Adidas</li><li>Decathlon</li><li>Capgemini</li></ul><h3 id="4-Koa-js"><a href="#4-Koa-js" class="headerlink" title="4. Koa.js"></a>4. Koa.js</h3><ul><li><strong>GitHub</strong><ul><li><strong>Stars</strong>：30.9k</li><li><strong>Forks</strong>：3k</li><li><strong>版本</strong>：2.13.1</li><li><strong>贡献者</strong>：219</li><li><strong>使用人数</strong>：160k</li></ul></li><li><strong>NPM 周下载量</strong>：870,944</li><li><strong>License</strong>：MIT</li></ul><p><img src="https://cdn-images-1.medium.com/max/2078/1*lKpRVNszZ6gVbTF_blVJRQ.png" alt="Koa.js 框架流行度"></p><p>Koa.js 是一个开源的 Node web 框架，由 Express.js 原班人马创建。通过 Koa，他们的目标是为 Web 应用和 API 创建一个更小、更有价值、更强大的平台。它提供了多种高效的方法，以让构建服务的过程更快速。</p><p><strong>Koa.js 可以被用于：</strong></p><ul><li>前台系统</li><li>后台系统</li><li>混合系统</li></ul><p><strong>Koa.js 主要特性：</strong></p><ul><li>代表现代和未来</li><li>与所有 Node.js 框架相比，体积更小。</li><li>有一个内置的错误捕捉器，防止网站崩溃。</li><li>使用 context 对象，该对象同时拥有请求和响应对象。</li></ul><p><strong>什么时候使用 Koa.js：</strong></p><p>Koa.js 最适合用于创建服务器、路由、处理响应和处理错误。</p><p><strong>谁在使用 Koa.js：</strong></p><ul><li>Paralect</li><li>LetzChange</li><li>BrainHub</li><li>Bulb</li></ul><h3 id="5-Socket-io"><a href="#5-Socket-io" class="headerlink" title="5. Socket.io"></a>5. Socket.io</h3><ul><li><strong>GitHub</strong><ul><li><strong>Stars</strong>：52.7k</li><li><strong>Forks</strong>：9.6k</li><li><strong>版本</strong>：4.0.0</li><li><strong>贡献者</strong>：189</li><li><strong>使用人数</strong>：2.2m</li></ul></li><li><strong>NPM 周下载量</strong>：3,617,636</li><li><strong>License</strong>：MIT</li></ul><p><img src="https://cdn-images-1.medium.com/max/2046/1*5zUKf_Dt48W00aWfkAs84g.png" alt="socket.io 流行度"></p><p>Socket.io 是用来在客户端和服务器端之间创建实时双向通信的框架。要做到这一点，客户端需要在浏览器中安装 Socket.io，服务器也要集成 Socket.io 包。这使得数据可以在数百万种形式中共享。然而，最受欢迎的方法仍然是 JSON。</p><p><strong>Socket.io 由以下两个部分组成：</strong></p><ol><li>JavaScript 服务：Node.js</li><li>JavaScript 客户端库：Node.js</li></ol><blockquote><p><strong>注意：</strong> Socket.io 还兼容许多其他语言，如 Java、C+、Swift、Dart、.Net 和 Python。</p></blockquote><p><strong>Socket.io 可以被用于：</strong></p><ul><li>各种命名空间</li><li>广播</li><li>事件处理</li><li>错误处理</li><li>日志和调试</li><li>聊天应用</li><li>内部</li></ul><p><strong>Socket.io 主要特性：</strong></p><ul><li>将信息编码为命名的 JSON 或二进制事件。</li><li>在应用程序中添加“实时”能力。</li><li>支持自动重新连接</li><li>出色的速度和可靠性</li><li>即时通讯和聊天</li></ul><p><strong>什么时候使用 Socket.io：</strong></p><p>Socket.io 是最好的基于事件的实时双向通信工具之一。任何想要在应用中添加实时分析功能的人都应该使用它。Socket.io 对于实时游戏应用也很有用。在实时游戏中使用基本的 HTTP 或 HTTPS 协议是不可行的，因为这些文件很大，建立通信需要时间。在这里，我们使用体积更小的 socket 包，几乎是实时地完成工作，以获得更流畅和更好的体验。</p><p><strong>谁在使用 Socket.io：</strong></p><ul><li>Alibaba Travels</li><li>Patreon</li><li>Trello</li><li>Justmop</li><li>Plaid</li></ul><h3 id="6-Meteor-js"><a href="#6-Meteor-js" class="headerlink" title="6. Meteor.js"></a>6. Meteor.js</h3><ul><li><strong>Github</strong><br>— <strong>Stars</strong>：42.3k<ul><li><strong>Forks</strong>：5.2k</li><li><strong>版本</strong>：4.0.0</li><li><strong>贡献者</strong>：452</li></ul></li><li>NPM：不可用</li><li><strong>License</strong>：MIT</li></ul><p>Meteor.js 是一个开源的全栈 JavaScript 平台，JavaScript 根据意图不同运行在不同的地方。JavaScript 运行在 Web 浏览器内部；然后 JavaScript 运行在 Node.js 容器内的 Meteor 服务器上，支持 HTML 片段、静态资源和 CSS 规则。</p><p><strong>Meteor.js 可以被用于：</strong></p><ul><li>移动应用程序全流程</li><li>web 应用程序全流程</li></ul><p><strong>Meteor.js 主要特性：</strong></p><ul><li>纯 JavaScript</li><li>干净、稳健的数据同步</li><li>互操作性</li><li>智能套件</li><li>代码热更新</li></ul><p><strong>什么时候使用 Meteor.js：</strong></p><p>Meteor.js 具有快速原型设计的能力，并能生成跨平台（Android、iOS、Web）的代码。它也是最直接的学习框架之一，因为它不遵循任何严格的结构规则。因此，Meteor.js 应该被任何希望以最少的学习曲线为多个平台创建应用程序的初级或中级开发人员使用。</p><p><strong>谁在使用 Meteor.js：</strong></p><ul><li>Accenture</li><li>NetApp</li><li>Rocket Chat</li><li>Esri</li><li>NordStorm</li></ul><h3 id="7-Adonis-js"><a href="#7-Adonis-js" class="headerlink" title="7. Adonis.js"></a>7. Adonis.js</h3><ul><li><strong>Github</strong><ul><li><strong>Stars</strong>：9.7k</li><li><strong>Forks</strong>：498</li><li><strong>版本</strong>：5.0.13</li><li><strong>贡献者</strong>：47</li><li><strong>使用人数</strong>：754</li></ul></li><li><strong>周下载数</strong>：3808</li><li><strong>License</strong>：MIT</li></ul><p><img src="https://cdn-images-1.medium.com/max/2052/1*QVu2Z2wbudWv06tBwkJ8mQ.png" alt="adoni.js 流行度"></p><p>Adonis.js 是一个 <a href="https://aglowiditsolutions.com/blog/hidden-cost-of-app-development/">Node.js</a> 的 MVC 框架，可以运行在所有的操作系统上。它为编写服务器端 Web 应用程序提供了一个稳定的生态系统，以让开发者专注于业务需求，如最终确定选择或排除哪个包。对于想要换个口味，正在尝试 <a href="https://aglowiditsolutions.com/blog/hidden-cost-of-app-development/">Node.js</a> 框架的 Laravel 开发者来说，它是理想的选择。Adonis.js为 <a href="https://aglowiditsolutions.com/blog/hidden-cost-of-app-development/">Node.js</a> 提供了与Laravel自然具有的相同的功能和能力。</p><p><strong>Adonis.js 可以被用于：</strong></p><ul><li>构建 web 应用</li><li>应用程序接口服务</li></ul><p><strong>Adonis.js 主要特性：</strong></p><ul><li>强大的 ORM，帮助进行安全的 SQL 查询。</li><li>API 和基于会话的认证系统</li><li>验证和给每一个用户的输入做卫生处理。</li><li>高度强调安全问题</li><li>可扩展的应用分层</li></ul><p><strong>什么时候使用 Adonis.js：</strong></p><p>如果你是一个正在寻找 MVC 工具的 <a href="https://aglowiditsolutions.com/blog/hidden-cost-of-app-development/">Node.js</a> 开发者，Adonis.js 是你的首选 Node.js 框架。然而，如果你是一个 Laravel 开发者或任何其他移动应用框架开发者，你仍然可以给 Adonis.js 一个机会，甚至从 PHP 迁移到 <a href="https://aglowiditsolutions.com/blog/hidden-cost-of-app-development/">Node.js</a> 也可以尝试一下 Adonis.js。</p><p><strong>谁在使用 Adonis.js：</strong></p><ul><li>Dotgroup</li><li>DORMshed</li><li>Nina</li><li>Zelo</li><li>FindUp</li></ul><h3 id="8-Sails-js"><a href="#8-Sails-js" class="headerlink" title="8. Sails.js"></a>8. Sails.js</h3><ul><li><strong>Github</strong><ul><li><strong>Stars</strong>：21.9k</li><li><strong>Forks</strong>：1.9k</li><li><strong>版本</strong>：1.4.0</li><li><strong>贡献者</strong>：229</li><li><strong>使用人数</strong>：23.3k</li></ul></li><li><strong>周下载数</strong>：20,457</li><li><strong>License</strong>：MIT</li></ul><p><img src="https://cdn-images-1.medium.com/max/2000/1*nYu5aJOd4f933c6WZkX8eA.png" alt="sails.js 框架流行度"></p><p>Sails.js 是又一个实时 <a href="https://aglowiditsolutions.com/blog/hidden-cost-of-app-development/">Node.js</a> MVC 框架。它基于 Express 构建，其 MVC 架构与 Ruby on Rails 相似。它与 Ruby on Rails 的不同之处在于，它提供了对更现代的、以数据为中心的 API 和 Web 应用开发风格的支持。</p><p><strong>Sails.js 可以被用于：</strong></p><ul><li>构建企业级 <a href="https://aglowiditsolutions.com/blog/hidden-cost-of-app-development/">Node.js</a> 应用</li><li>构建前端应用</li><li>构建处理 HTTP 请求的后端应用</li></ul><p><strong>Sails.js 主要特性：</strong></p><ul><li>支持自动生成 REST APIs</li><li>具有简单的 WebSocket 集成</li><li>兼容任何流行的前端框架：Angular、Android、React、iOS、Windows 或任何自定义硬件。</li><li>它还具有实时功能支持</li></ul><p><strong>什么时候使用 Sails.js：</strong></p><p>任何想要一个模拟 MVC 模式的 <a href="https://aglowiditsolutions.com/blog/hidden-cost-of-app-development/">Node.js</a> 框架（如 Laravel 和 Ruby on Rails）、想要实现现代应用架构，并构建以数据为中心的 API 和实时应用的开发者都应该在他们的下一个项目中使用 Sails.js。</p><p><strong>谁在使用 Sails.js：</strong></p><ul><li>Tutor Platform</li><li>Redox Engine</li><li>Brainhub</li><li>Created Informed</li><li>People Grove</li></ul><h2 id="总结一下"><a href="#总结一下" class="headerlink" title="总结一下"></a>总结一下</h2><p>市场上有很多新的 <a href="https://aglowiditsolutions.com/blog/hidden-cost-of-app-development/">Node.js</a> 框架。不同的 <a href="https://aglowiditsolutions.com/blog/hidden-cost-of-app-development/">Node.js</a> 框架会在不同阶段帮助你开发项目，并带来很多价值和功能。合理利用这些框架，妈妈再也不用担心你的应用开发啦。</p><blockquote><p>如果发现译文存在错误或其他需要改进的地方，欢迎到 <a href="https://github.com/xitu/gold-miner">掘金翻译计划</a> 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 <strong>本文永久链接</strong> 即为本文在 GitHub 上的 MarkDown 链接。</p></blockquote><hr><blockquote><p><a href="https://github.com/xitu/gold-miner">掘金翻译计划</a> 是一个翻译优质互联网技术文章的社区，文章来源为 <a href="https://juejin.im/">掘金</a> 上的英文分享文章。内容覆盖 <a href="https://github.com/xitu/gold-miner#android">Android</a>、<a href="https://github.com/xitu/gold-miner#ios">iOS</a>、<a href="https://github.com/xitu/gold-miner#%E5%89%8D%E7%AB%AF">前端</a>、<a href="https://github.com/xitu/gold-miner#%E5%90%8E%E7%AB%AF">后端</a>、<a href="https://github.com/xitu/gold-miner#%E5%8C%BA%E5%9D%97%E9%93%BE">区块链</a>、<a href="https://github.com/xitu/gold-miner#%E4%BA%A7%E5%93%81">产品</a>、<a href="https://github.com/xitu/gold-miner#%E8%AE%BE%E8%AE%A1">设计</a>、<a href="https://github.com/xitu/gold-miner#%E4%BA%BA%E5%B7%A5%E6%99%BA%E8%83%BD">人工智能</a>等领域，想要查看更多优质译文请持续关注 <a href="https://github.com/xitu/gold-miner">掘金翻译计划</a>、<a href="http://weibo.com/juejinfanyi">官方微博</a>、<a href="https://zhuanlan.zhihu.com/juejinfanyi">知乎专栏</a>。</p></blockquote>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/76292c0fc52d/</id>
    <link href="https://blog.zisheng.pro/posts/76292c0fc52d/"/>
    <published>2023-04-08T11:22:08.000Z</published>
    <summary>Node.js 是最敏捷的服务端 web 应用平台，因为它为应用开发公司提供了构建可扩展的单一编程语言 web 平台的便利。它是最热门的开源的 JavaScript 运行时框架之一，具有跨平台属性，让我们可以在浏览器以外的环境运行代码。</summary>
    <title>2023 年最值得使用的 Node.js 框架</title>
    <updated>2026-09-18T08:15:28.629Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="React Native" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/React-Native/"/>
    <category term="编程" scheme="https://blog.zisheng.pro/categories/%E7%BC%96%E7%A8%8B/"/>
    <category term="React Native" scheme="https://blog.zisheng.pro/tags/React-Native/"/>
    <category term="React Native 组件库" scheme="https://blog.zisheng.pro/tags/React-Native-%E7%BB%84%E4%BB%B6%E5%BA%93/"/>
    <category term="React Native Animation Libraries" scheme="https://blog.zisheng.pro/tags/React-Native-Animation-Libraries/"/>
    <content>
      <![CDATA[<p>动画在手机上有很大的影响，它可以创造更好的用户体验，这些动画主要用于与用户的行为互动，使用户更多的参与你的应用程序。</p><p>在技术层面，React Native 为我们提供了强大的声明式 API 去创建动画，通常我们称之为 <code>Animated API</code>。然而，有时你可能想使用一些第三方库来为你处理动画，而不用直接处理 <code>Animated API</code>，本文我们便是探索和讨论 5 个你值得是使用的 React Native 插件。</p><h2 id="重用-React-组件"><a href="#重用-React-组件" class="headerlink" title="重用 React 组件"></a>重用 React 组件</h2><p>使用 <a href="https://bit.dev/">Bit</a> 可以跨平台分享和重用 React 组件。作为一个团队在共享组件上进行协作，可以更敏捷地共同构建应用程序。让 Bit 做繁重的工作，这样你就可以轻松地发布、安装和更新你的个人组件而不需要任何开销。</p><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e1c5f1759c924881ba62904a329645ef~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><h2 id="react-native-animatable"><a href="#react-native-animatable" class="headerlink" title="react-native-animatable"></a>react-native-animatable</h2><p>我是 <a href="https://github.com/oblador/react-native-animatable">react-native-animatable</a> 的忠实粉丝。它为你提供了声明性的包装器，你可以用它来为 React Native 中的元素制作动画，这个库的好处是它的 API 很容易使用，你不需要做任何 linking 操作。所以让我们举个例子，看看这个库是如何工作的😃。</p><p>首先，安装该库！</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">yarn add react-native-animatable</span><br></pre></td></tr></table></figure><p>然后，让我们制作一个 slideInDown 动画效果！</p><p><img src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/abc35f9d7b304742b867c58f3d07f7e5~tplv-k3u1fbpfcp-watermark.image" alt="1_amBoBML8nY2DxpuNC5MbNQ.gif"></p><p>我们的组件看起来如下：</p><figure class="highlight jsx"><figcaption><span>| pure</span></figcaption><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">React</span>, &#123;<span class="title class_">Component</span>&#125; <span class="keyword">from</span> <span class="string">&#x27;react&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123;<span class="title class_">Text</span>, <span class="title class_">View</span>, <span class="title class_">Dimensions</span>, <span class="title class_">SafeAreaView</span>, <span class="title class_">StyleSheet</span>&#125; <span class="keyword">from</span> <span class="string">&#x27;react-native&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> * <span class="keyword">as</span> <span class="title class_">Animatable</span> <span class="keyword">from</span> <span class="string">&#x27;react-native-animatable&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">AnimatableScreen</span> <span class="keyword">extends</span> <span class="title class_ inherited__">Component</span> &#123;</span><br><span class="line">  <span class="title function_">render</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">SafeAreaView</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;&#123;alignItems:</span> &#x27;<span class="attr">center</span>&#x27;&#125;&#125;&gt;</span></span></span><br><span class="line"><span class="language-xml">          <span class="tag">&lt;<span class="name">Animatable.View</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">            <span class="attr">style</span>=<span class="string">&#123;styles.card&#125;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">            <span class="attr">animation</span>=<span class="string">&quot;slideInDown&quot;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">            <span class="attr">iterationCount</span>=<span class="string">&#123;5&#125;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">            <span class="attr">direction</span>=<span class="string">&quot;alternate&quot;</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.whiteText&#125;</span>&gt;</span>slideInDown Animation<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">Animatable.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">SafeAreaView</span>&gt;</span></span></span><br><span class="line">    );</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> 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">width</span>: <span class="title class_">Dimensions</span>.<span class="title function_">get</span>(<span class="string">&#x27;screen&#x27;</span>).<span class="property">width</span>,</span><br><span class="line">    <span class="attr">height</span>: <span class="title class_">Dimensions</span>.<span class="title function_">get</span>(<span class="string">&#x27;screen&#x27;</span>).<span class="property">height</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">card</span>: &#123;</span><br><span class="line">    <span class="attr">width</span>: <span class="title class_">Dimensions</span>.<span class="title function_">get</span>(<span class="string">&#x27;screen&#x27;</span>).<span class="property">width</span> * <span class="number">0.6</span>,</span><br><span class="line">    <span class="attr">height</span>: <span class="title class_">Dimensions</span>.<span class="title function_">get</span>(<span class="string">&#x27;screen&#x27;</span>).<span class="property">height</span> * <span class="number">0.35</span>,</span><br><span class="line">    <span class="attr">backgroundColor</span>: <span class="string">&#x27;#206225&#x27;</span>,</span><br><span class="line">    <span class="attr">alignItems</span>: <span class="string">&#x27;center&#x27;</span>,</span><br><span class="line">    <span class="attr">justifyContent</span>: <span class="string">&#x27;center&#x27;</span>,</span><br><span class="line">    <span class="attr">borderRadius</span>: <span class="number">8</span>,</span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="attr">whiteText</span>: &#123;</span><br><span class="line">    <span class="attr">color</span>: <span class="string">&#x27;#ffffff&#x27;</span>,</span><br><span class="line">    <span class="attr">fontSize</span>: <span class="number">18</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="title class_">AnimatableScreen</span>;</span><br></pre></td></tr></table></figure><p>我们首先通过 <code>react-native-animatable</code> 导入了 Animatable 对象，然后我们将 Animatable 元素与我们想要动画化的元素结合起来，你可以使用 Image 和 Text 元素，另外你也可以使用 <code>createAnimatableComponent</code> 制作其他元素的动画，你可以查看 <a href="https://github.com/oblador/react-native-animatable">文档</a> 了解更多细节。</p><figure class="highlight jsx"><figcaption><span>| pure</span></figcaption><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="title class_">MyCustomComponent</span> = <span class="title class_">Animatable</span>.<span class="title function_">createAnimatableComponent</span>(<span class="title class_">MyCustomComponent</span>);</span><br></pre></td></tr></table></figure><p><strong>Props：</strong></p><ul><li><strong>animation</strong>：它接受一个字符串，用来指定我们想要的动画类型，例如：<code>SlideInDown</code>、<code>SlideInLeft</code> 请查看 <a href="https://github.com/oblador/react-native-animatable">docs</a> 来探索你可以使用的动画类型。</li><li><strong>iteractionCount</strong>：它指定了动画应该运行和迭代的次数。我们可以使用 <code>infinite</code> 来使动画永远运行。</li><li><strong>direction</strong>：指定动画的方向，可以使用 <code>normal</code>,<code>alternate-reverse</code>,<code>reverse</code>。</li></ul><p>我们也可以使用其他属性，比如 <code>transition</code> 允许我们定义我们想要动画化的样式属性，如下图所示：</p><figure class="highlight jsx"><figcaption><span>| pure</span></figcaption><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">React</span>, &#123;<span class="title class_">Component</span>&#125; <span class="keyword">from</span> <span class="string">&#x27;react&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123;<span class="title class_">Text</span>, <span class="title class_">Dimensions</span>, <span class="title class_">SafeAreaView</span>, <span class="title class_">StyleSheet</span>&#125; <span class="keyword">from</span> <span class="string">&#x27;react-native&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> * <span class="keyword">as</span> <span class="title class_">Animatable</span> <span class="keyword">from</span> <span class="string">&#x27;react-native-animatable&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">AnimatableScreen</span> <span class="keyword">extends</span> <span class="title class_ inherited__">Component</span> &#123;</span><br><span class="line">  state = &#123;</span><br><span class="line">    <span class="attr">opacity</span>: <span class="number">0</span>,</span><br><span class="line">  &#125;;</span><br><span class="line"></span><br><span class="line">  <span class="title function_">componentDidMount</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="variable language_">this</span>.<span class="title function_">setState</span>(&#123;<span class="attr">opacity</span>: <span class="number">1</span>&#125;);</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="title function_">render</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">SafeAreaView</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">Animatable.View</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">styles.card</span>,</span></span></span><br><span class="line"><span class="tag"><span class="language-xml">            &#123;</span></span></span><br><span class="line"><span class="tag"><span class="language-xml">              <span class="attr">opacity:</span> <span class="attr">this.state.opacity</span>,</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">          ]&#125;</span></span></span><br><span class="line"><span class="tag"><span class="language-xml">          <span class="attr">iterationCount</span>=<span class="string">&quot;infinite&quot;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">          <span class="attr">direction</span>=<span class="string">&quot;reverse&quot;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">          <span class="attr">duration</span>=<span class="string">&#123;3000&#125;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">          <span class="attr">transition</span>=<span class="string">&quot;opacity&quot;</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.whiteText&#125;</span>&gt;</span>Transition Animation<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">Animatable.View</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;/<span class="name">SafeAreaView</span>&gt;</span></span></span><br><span class="line">    );</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> 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">alignItems</span>: <span class="string">&#x27;center&#x27;</span>,</span><br><span class="line">    <span class="attr">width</span>: <span class="title class_">Dimensions</span>.<span class="title function_">get</span>(<span class="string">&#x27;screen&#x27;</span>).<span class="property">width</span>,</span><br><span class="line">    <span class="attr">height</span>: <span class="title class_">Dimensions</span>.<span class="title function_">get</span>(<span class="string">&#x27;screen&#x27;</span>).<span class="property">height</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">card</span>: &#123;</span><br><span class="line">    <span class="attr">width</span>: <span class="title class_">Dimensions</span>.<span class="title function_">get</span>(<span class="string">&#x27;screen&#x27;</span>).<span class="property">width</span> * <span class="number">0.6</span>,</span><br><span class="line">    <span class="attr">height</span>: <span class="title class_">Dimensions</span>.<span class="title function_">get</span>(<span class="string">&#x27;screen&#x27;</span>).<span class="property">height</span> * <span class="number">0.35</span>,</span><br><span class="line">    <span class="attr">backgroundColor</span>: <span class="string">&#x27;#206225&#x27;</span>,</span><br><span class="line">    <span class="attr">alignItems</span>: <span class="string">&#x27;center&#x27;</span>,</span><br><span class="line">    <span class="attr">justifyContent</span>: <span class="string">&#x27;center&#x27;</span>,</span><br><span class="line">    <span class="attr">borderRadius</span>: <span class="number">8</span>,</span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="attr">whiteText</span>: &#123;</span><br><span class="line">    <span class="attr">color</span>: <span class="string">&#x27;#ffffff&#x27;</span>,</span><br><span class="line">    <span class="attr">fontSize</span>: <span class="number">18</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="title class_">AnimatableScreen</span>;</span><br></pre></td></tr></table></figure><p><a href="https://github.com/oblador/react-native-animatable">react-native-animatable</a> 给我们提供了更多的选项，使我们在动画中拥有更多的控制权，关于这个库还有更多的东西要讲，一篇文章无法涵盖，我真的推荐你去看看文档。</p><h2 id="Lottie"><a href="#Lottie" class="headerlink" title="Lottie"></a>Lottie</h2><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/fc00afe5e9ef421aa00ca61a5e7b5e8c~tplv-k3u1fbpfcp-watermark.image" alt="1_audbt0qXd6H5RQl7ebkgeQ.gif"></p><p>这是迄今为止我最喜欢的库，Lottie 是一个由 Airbnb 创建的动画库，它将 After Effect 动画解析成 JSON 文件，你可以将其导出并在你的应用程序中使用，你可以在 ios、Android、Windows、Web 和 React Native 上使用 Lottie。</p><p>在 React Native 中 使用 <a href="https://airbnb.io/lottie/#/">Lottie</a> 你需要安装适配插件 <a href="http://lottie-react-native/">lottie-react-native</a>，它是一个简单的接收 JSON 文件的封装元素：</p><figure class="highlight jsx"><figcaption><span>| pure</span></figcaption><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">React</span>, &#123;<span class="title class_">Component</span>&#125; <span class="keyword">from</span> <span class="string">&#x27;react&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123;<span class="title class_">StyleSheet</span>, <span class="title class_">SafeAreaView</span>&#125; <span class="keyword">from</span> <span class="string">&#x27;react-native&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> <span class="title class_">LottieView</span> <span class="keyword">from</span> <span class="string">&#x27;lottie-react-native&#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">class</span> <span class="title class_">index</span> <span class="keyword">extends</span> <span class="title class_ inherited__">Component</span> &#123;</span><br><span class="line">  <span class="title function_">render</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">SafeAreaView</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">LottieView</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">          <span class="attr">source</span>=<span class="string">&#123;require(</span>&#x27;<span class="attr">.</span>/<span class="attr">animation.json</span>&#x27;)&#125;</span></span></span><br><span class="line"><span class="tag"><span class="language-xml">          <span class="attr">loop</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">          <span class="attr">autoPlay</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;width:</span> <span class="attr">100</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">      <span class="tag">&lt;/<span class="name">SafeAreaView</span>&gt;</span></span></span><br><span class="line">    );</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> 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">alignItems</span>: <span class="string">&#x27;center&#x27;</span>,</span><br><span class="line">  &#125;,</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p>你可以查看 <a href="https://lottiefiles.com/%EF%BC%8C%E8%8E%B7%E5%BE%97%E5%85%8D%E8%B4%B9%E7%9A%84">https://lottiefiles.com/，获得免费的</a> After Effect 动画的 JSON 文件与 Lottie 一起使用，享受不一样的动画体验😎。</p><h2 id="react-native-reanimated"><a href="#react-native-reanimated" class="headerlink" title="react-native-reanimated"></a>react-native-reanimated</h2><p><a href="https://github.com/kmagiera/react-native-reanimated">react-native-reanimated</a> 的作者从零重写了 React Native 的 Animated API，正如作者在 <a href="https://github.com/kmagiera/react-native-reanimated">官方文档</a> 中描述的，react-native-reanimated 为以它基础的 Animated 库的 API 提供了更全面、更底层的抽象，因此在编写动画时可以有更大的灵活性，尤其是在涉及到基于手势的交互时。</p><p>react-native-reanimated 提供了一个新的 Animated API 来让你替代 <a href="https://reactnative.cn/docs/animations">React Native Animated API</a>，你可以用它来代替 <a href="https://reactnative.cn/docs/animations">React Native Animated API</a> 在 React native 中创建动画。以下是为什么你可能必须使用 <a href="https://github.com/kmagiera/react-native-reanimated">react-native-reanimated</a> 而不是 <a href="https://reactnative.cn/docs/animations">React Native Animated API</a>：</p><ul><li>react-native-reanimated 通过提供多个声明性 API，让你对动画值有更大的控制权，以保持对值变化的跟踪。</li><li>不再需要使用 <code>useNativeDriver</code>，因为 react-native-reanimated 直接在 UI 线程执行动画。</li></ul><p>了解更多关系该库的信息，我建议你阅读他们托管在 <a href="https://github.com/kmagiera/react-native-reanimated">GitHub 的文档</a>，我也建议你关注 <a href="https://medium.com/@wcandillon">William Candillon</a> 和它的 <a href="https://www.youtube.com/channel/UC806fwFWpiLQV5y-qifzHnA">youtube channel</a>，他在 React Native 中做了一些很棒的动画，而且他一直在使用 react-native-reanimated。</p><h2 id="React-Native-Animations-Library-rnal"><a href="#React-Native-Animations-Library-rnal" class="headerlink" title="React Native Animations Library (rnal)"></a>React Native Animations Library (rnal)</h2><p><img src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/1772cb10093d489b9357f6c5c08f431e~tplv-k3u1fbpfcp-watermark.image" alt="1_BLFTzAhn1uV5OLtHFUQJQg.gif"></p><p><code>rnal</code> , 是由 <a href="https://saidhayani.medium.com/">SaidHayani@</a> 创建的 React Native 动画库，它的目的是使 React Native 中使用动画变得足够简单，通过提供简单的封装来创建过渡效果，如 <code>Fade</code>，<code>Scale</code>，或 <code>rotation</code> 效果，并可选择创建自定义动画，要创建 <code>Fade</code> 效果，你可以只做以下事情：</p><p><img src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0259be58f06642e592bd39c55f263bc2~tplv-k3u1fbpfcp-watermark.image" alt="1_zuqjwJjTqPUMXAoOYGfYIw.gif"></p><p>你可以查看 <a href="https://github.com/hayanisaid/rnal">docs</a> 探索更多你可以使用的选项.</p><h2 id="react-native-motion"><a href="#react-native-motion" class="headerlink" title="react-native-motion"></a>react-native-motion</h2><p><a href="https://github.com/xotahal/react-native-motion">react-native-motion</a> 是一个在 React Native 中制作动画的库，使用起来非常简单，下面是一个使用 <a href="https://github.com/xotahal/react-native-motion">react-native-motion</a> 制作简单 Shake 动画的例子：</p><figure class="highlight jsx"><figcaption><span>| pure</span></figcaption><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">React</span>, &#123;<span class="title class_">Component</span>&#125; <span class="keyword">from</span> <span class="string">&#x27;react&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123;<span class="title class_">Text</span>, <span class="title class_">StyleSheet</span>, <span class="title class_">TouchableOpacity</span>, <span class="title class_">SafeAreaView</span>&#125; <span class="keyword">from</span> <span class="string">&#x27;react-native&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123;<span class="title class_">Shake</span>&#125; <span class="keyword">from</span> <span class="string">&#x27;react-native-motion&#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">class</span> <span class="title class_">index</span> <span class="keyword">extends</span> <span class="title class_ inherited__">Component</span> &#123;</span><br><span class="line">  state = &#123;</span><br><span class="line">    <span class="attr">value</span>: <span class="number">0</span>,</span><br><span class="line">  &#125;;</span><br><span class="line">  _startAnimation = <span class="function">() =&gt;</span> &#123;</span><br><span class="line">    <span class="variable language_">this</span>.<span class="title function_">setState</span>(&#123;<span class="attr">value</span>: <span class="variable language_">this</span>.<span class="property">state</span>.<span class="property">value</span> + <span class="number">1</span>&#125;);</span><br><span class="line">  &#125;;</span><br><span class="line">  <span class="title function_">render</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">SafeAreaView</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">TouchableOpacity</span> <span class="attr">style</span>=<span class="string">&#123;styles.btn&#125;</span> <span class="attr">onPress</span>=<span class="string">&#123;this._startAnimation&#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.textBtn&#125;</span>&gt;</span>Start animation 🚀<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">        <span class="tag">&lt;<span class="name">Shake</span> <span class="attr">style</span>=<span class="string">&#123;[styles.card]&#125;</span> <span class="attr">value</span>=<span class="string">&#123;this.state.value&#125;</span> <span class="attr">type</span>=<span class="string">&quot;timing&quot;</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.whiteText&#125;</span>&gt;</span>&#123;this.state.value&#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">Shake</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;/<span class="name">SafeAreaView</span>&gt;</span></span></span><br><span class="line">    );</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> 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">alignItems</span>: <span class="string">&#x27;center&#x27;</span>,</span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="attr">btn</span>: &#123;</span><br><span class="line">    <span class="attr">alignItems</span>: <span class="string">&#x27;center&#x27;</span>,</span><br><span class="line">    <span class="attr">backgroundColor</span>: <span class="string">&#x27;#d3d3d3&#x27;</span>,</span><br><span class="line">    <span class="attr">padding</span>: <span class="number">10</span>,</span><br><span class="line">    <span class="attr">width</span>: <span class="string">&#x27;80%&#x27;</span>,</span><br><span class="line">    <span class="attr">borderRadius</span>: <span class="number">10</span>,</span><br><span class="line">    <span class="attr">marginTop</span>: <span class="number">20</span>,</span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="attr">textBtn</span>: &#123;</span><br><span class="line">    <span class="attr">fontWeight</span>: <span class="string">&#x27;bold&#x27;</span>,</span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="attr">card</span>: &#123;</span><br><span class="line">    <span class="attr">backgroundColor</span>: <span class="string">&#x27;#007fff&#x27;</span>,</span><br><span class="line">    <span class="attr">width</span>: <span class="string">&#x27;60%&#x27;</span>,</span><br><span class="line">    <span class="attr">height</span>: <span class="number">300</span>,</span><br><span class="line">    <span class="attr">marginTop</span>: <span class="number">100</span>,</span><br><span class="line">    <span class="attr">justifyContent</span>: <span class="string">&#x27;center&#x27;</span>,</span><br><span class="line">    <span class="attr">alignItems</span>: <span class="string">&#x27;center&#x27;</span>,</span><br><span class="line">    <span class="attr">borderRadius</span>: <span class="number">10</span>,</span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="attr">whiteText</span>: &#123;</span><br><span class="line">    <span class="attr">color</span>: <span class="string">&#x27;#ffffff&#x27;</span>,</span><br><span class="line">  &#125;,</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/15b7fc60d48d46ed94457eda63d5aa5d~tplv-k3u1fbpfcp-watermark.image" alt="1_nhKMy9zlUPvCYWWbiGNFaQ.gif"></p><p>react-native-motion 为我们提供了一个简单的 API 来制作共享的过渡效果，该库的作者写了 <a href="https://medium.com/react-native-motion/transition-challenge-9bc9fdef56c7">一篇关于共享过渡效果的文章</a>。值得注意的是。</p><blockquote><p>译者 Tips：该库已有 3 年未更新，且没有 TypeScript 类型声明，生产环境慎用！</p></blockquote><blockquote><p>原文链接：<a href="https://blog.bitsrc.io/top-5-animation-libraries-in-react-native-d00ec8ddfc8d">Top 5 Animation Libraries in React Native</a><br>原文作者：<a href="https://saidhayani.medium.com/">SaidHayani@</a><br>译文出自：<a href="https://blog.zisheng.pro/categories/%E6%B4%9B%E7%AB%B9%E7%BF%BB%E8%AF%91%E8%AE%A1%E5%88%92/">紫升翻译计划</a></p></blockquote>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/dca3341914f8/</id>
    <link href="https://blog.zisheng.pro/posts/dca3341914f8/"/>
    <published>2023-04-06T15:53:15.000Z</published>
    <summary>在技术层面，React Native 为我们提供了强大的声明式 API 去创建动画，通常我们称之为 `Animated API`。然而，有时你可能想使用一些第三方库来为你处理动画，而不用直接处理 `Animated API`，本文我们便是探索和讨论 5 个你值得是使用的 React Native 插件。</summary>
    <title>2023 React Native Top5 动画库</title>
    <updated>2026-09-18T08:15:28.629Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="前端工程化" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/"/>
    <category term="源码解析" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90/"/>
    <category term="Rollup 源码解析" scheme="https://blog.zisheng.pro/tags/Rollup-%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90/"/>
    <category term="Rollup 插件机制" scheme="https://blog.zisheng.pro/tags/Rollup-%E6%8F%92%E4%BB%B6%E6%9C%BA%E5%88%B6/"/>
    <category term="tapable" scheme="https://blog.zisheng.pro/tags/tapable/"/>
    <content>
      <![CDATA[<h2 id="rollup-c-简要流程"><a href="#rollup-c-简要流程" class="headerlink" title="rollup -c 简要流程"></a><code>rollup -c</code> 简要流程</h2><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/3bd4a66beffc4b6182498a4bda07ce8f~tplv-k3u1fbpfcp-zoom-1.image" alt="rollup -c 简要流程"></p><h2 id="插件系统相关模块"><a href="#插件系统相关模块" class="headerlink" title="插件系统相关模块"></a>插件系统相关模块</h2><ul><li>Graph: 全局唯一的图，包含入口以及各种依赖的相互关系，操作方法，缓存等。是 rollup 的核心</li><li>PluginDriver: 插件驱动器，调用插件和提供插件环境上下文等</li></ul><h2 id="插件机制分析"><a href="#插件机制分析" class="headerlink" title="插件机制分析"></a>插件机制分析</h2><h3 id="概述"><a href="#概述" class="headerlink" title="概述"></a>概述</h3><p>一个 Rollup 插件是由一个或多个属性、构建钩子函数、输出钩子函数组成的对象，插件还需要符合一些官方的约定。一个插件应该作为一个包来发布，这个包导出一个可以用插件特定的选项来调用的函数，并且该函数返回一个对象。</p><p>插件允许你自定义 Rollup 的行为，比如，打包之前转换代码或者在你的 <code>node_modules</code> 文件夹中查找第三方包。</p><p>官方插件维护在 <a href="https://github.com/rollup/plugins">rollup&#x2F;plugins</a> 仓库，社区精选插件维护在 <a href="https://github.com/rollup/awesome">rollup&#x2F;awesome</a>。如果你想给某个插件提建议，请提交一个 pr。</p><h3 id="一个简单的例子"><a href="#一个简单的例子" class="headerlink" title="一个简单的例子"></a>一个简单的例子</h3><p>下面的插件可以在不访问文件系统的前提下拦截任何 <code>virtual-module</code> 的导入。例如，如果你想在浏览器中使用 Rollup，这是必要的。它甚至可以用来替换入口点，如例子中所示。</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// index.js</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_">myExample</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> &#123;</span><br><span class="line">    <span class="attr">name</span>: <span class="string">&#x27;my-example&#x27;</span>, <span class="comment">// 名字会在 warnings 和 errors 中显示</span></span><br><span class="line">    <span class="title function_">resolveId</span>(<span class="params">source</span>) &#123;</span><br><span class="line">      <span class="keyword">if</span> (source === <span class="string">&#x27;virtual-module&#x27;</span>) &#123;</span><br><span class="line">        <span class="keyword">return</span> source; <span class="comment">// 这表明 rollup 不应该询问其他插件或检查文件系统来寻找这个 ID。</span></span><br><span class="line">      &#125;</span><br><span class="line">      <span class="keyword">return</span> <span class="literal">null</span>; <span class="comment">// 其他的 ID 应该按照通常的方式处理</span></span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="title function_">load</span>(<span class="params">id</span>) &#123;</span><br><span class="line">      <span class="keyword">if</span> (id === <span class="string">&#x27;virtual-module&#x27;</span>) &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="string">&#x27;export default &quot;This is virtual!&quot;&#x27;</span>; <span class="comment">// &quot;virtual-module&quot; 的源码</span></span><br><span class="line">      &#125;</span><br><span class="line">      <span class="keyword">return</span> <span class="literal">null</span>; <span class="comment">// 其他的 ID 应该按照通常的方式处理</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">// rollup.config.js</span></span><br><span class="line"><span class="keyword">import</span> myExample <span class="keyword">from</span> <span class="string">&#x27;./index&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> &#123;</span><br><span class="line">  <span class="attr">input</span>: <span class="string">&#x27;virtual-module&#x27;</span>, <span class="comment">// 被我们的插件解析</span></span><br><span class="line">  <span class="attr">plugins</span>: [<span class="title function_">myExample</span>()],</span><br><span class="line">  <span class="attr">output</span>: [</span><br><span class="line">    &#123;</span><br><span class="line">      <span class="attr">file</span>: <span class="string">&#x27;bundle.js&#x27;</span>,</span><br><span class="line">      <span class="attr">format</span>: <span class="string">&#x27;es&#x27;</span>,</span><br><span class="line">    &#125;,</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><ul><li>插件应该有一个清晰的名字，并且必须带上 <code>rollup-plugin-</code> 前缀。</li><li>在 <code>package.json</code> 中包含 <code>rollup-plugin</code> 关键字。</li><li>插件应该被测试，我们推荐 mocha 或者 ava 这类开箱支持 promises 的库。</li><li>尽可能使用异步方法。</li><li>使用英文编写插件文档</li><li>如果合适的话，确保你的插件输出正确的 sourcemap</li><li>如果你的插件使用 ‘virtual modules’（比如帮助函数），给模块名加上 <code>\0</code> 前缀。这可以阻止其他插件执行它。</li></ul><h3 id="钩子函数"><a href="#钩子函数" class="headerlink" title="钩子函数"></a>钩子函数</h3><p>rollup 插件的核心是钩子函数，rollup 钩子函数分为两类：</p><h4 id="构建钩子函数"><a href="#构建钩子函数" class="headerlink" title="构建钩子函数"></a>构建钩子函数</h4><p>为了与构建过程交互，你的插件对象需要包含一些构建钩子函数。构建钩子是构建的各个阶段调用的函数。构建钩子函数可以影响构建执行方式、提供构建的信息或者在构建完成后修改构建。rollup 中有不同的构建钩子函数：</p><ul><li><code>async</code>：这类 hook 也可以返回一个解析为相同类型值的 promise;否则，hook 将被标记为 <code>sync</code>。</li><li><code>first</code>：如果有多个插件实现了这个 hook，hook 将依次运行，直到钩子返回一个非 <code>null</code> 或非 <code>undefined</code> 的值。</li><li><code>sequential</code>：如果有多个插件实现了这个 hook，所有的插件都将按照指定的插件顺序运行。如果一个 hook 是异步的，这种类型的后续 hook 将一直等待，直到当前 hook 被解析。</li><li><code>parallel</code>：如果有多个插件实现了这个 hook，所有的插件都将按照指定的插件顺序运行。如果一个 hook 是异步的，这种类型的后续 hook 将并行运行，而不等待当前钩子。</li></ul><p>构建钩子函数在构建阶段执行，它们被 <a href="https://github.com/rollup/rollup/blob/07b3a02069594147665daa95d3fa3e041a82b2d0/cli/run/build.ts#L34"><code>rollup.rollup(inputOptions)</code></a> 触发。它们主要关注在 Rollup 处理输入文件之前定位、提供和转换输入文件。构建阶段的第一个钩子是 <code>options</code>，最后一个钩子总是 <code>buildEnd</code>，除非有一个构建错误，在这种情况下 <code>closeBundle</code> 将在这之后被调用。</p><p>此外，在观察模式下，<code>watchChange</code> 钩子可以在任何时候被触发，以通知新的运行将在当前运行产生其输出后被触发。另外，当 watcher 关闭时，closeWatcher 钩子函数将被触发。</p><h4 id="输出生成钩子函数"><a href="#输出生成钩子函数" class="headerlink" title="输出生成钩子函数"></a>输出生成钩子函数</h4><p>输出生成钩子函数可以提供关于生成的包的信息并在构建完成后立马执行。它们和构建钩子函数拥有一样的工作原理和相同的类型，但是不同的是它们分别被 ·<a href="https://github.com/rollup/rollup/blob/07b3a02069594147665daa95d3fa3e041a82b2d0/cli/run/build.ts#L44"><code>bundle.generate(output)</code></a> 或 <a href="https://github.com/rollup/rollup/blob/07b3a02069594147665daa95d3fa3e041a82b2d0/cli/run/build.ts#L64"><code>bundle.write(outputOptions)</code></a> 调用。只使用输出生成钩子的插件也可以通过输出选项传入，因为只对某些输出运行。</p><p>输出生成阶段的第一个钩子函数是 <a href="https://github.com/rollup/rollup/blob/07b3a02069594147665daa95d3fa3e041a82b2d0/src/Bundle.ts#L50">outputOptions</a>，如果输出通过 <a href="https://github.com/rollup/rollup/blob/master/cli/run/build.ts#L44">bundle.generate(…)</a> 成功生成则第一个钩子函数是 <a href="https://github.com/rollup/rollup/blob/master/src/Bundle.ts#L73">generateBundle</a>，如果输出通过 <a href="https://github.com/rollup/rollup/blob/07b3a02069594147665daa95d3fa3e041a82b2d0/src/watch/watch.ts#L200"><code>bundle.write(...)</code></a> 生成则最后一个钩子函数是 <a href="https://github.com/rollup/rollup/blob/master/src/rollup/rollup.ts#L176"><code>writeBundle</code></a>，另外如果输出生成阶段发生了错误的话，最后一个钩子函数则是 <a href="https://github.com/rollup/rollup/blob/master/src/Bundle.ts#L70">renderError</a>。</p><p>另外，<a href="https://github.com/rollup/rollup/blob/master/src/rollup/rollup.ts#L59">closeBundle</a> 可以作为最后一个钩子被调用，但用户有责任手动调用 <code>bundle.close()</code> 来触发它。CLI 将始终确保这种情况发生。</p><h4 id="钩子函数加载实现"><a href="#钩子函数加载实现" class="headerlink" title="钩子函数加载实现"></a>钩子函数加载实现</h4><p><a href="https://github.com/rollup/rollup/blob/07b3a02069594147665daa95d3fa3e041a82b2d0/src/utils/PluginDriver.ts#L124"><code>PluginDriver</code></a> 中有 9 个 hook 加载函数。主要是因为每种类别的 hook 都有同步和异步的版本。</p><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/2bba7776f98d4cbc9d184101d1f1b752~tplv-k3u1fbpfcp-zoom-1.image" alt="rollup 钩子函数加载实现"></p><p><strong>1.hookFirst：</strong></p><blockquote><p>加载 <code>first</code> 类型的钩子函数，场景有 <code>resolveId</code>、<code>resolveAssetUrl</code> 等</p></blockquote><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> hookFirst&lt;H <span class="keyword">extends</span> keyof <span class="title class_">PluginHooks</span>, R = <span class="title class_">ReturnType</span>&lt;<span class="title class_">PluginHooks</span>[H]&gt;&gt;(</span><br><span class="line">  <span class="attr">hookName</span>: H,</span><br><span class="line">  <span class="attr">args</span>: <span class="title class_">Args</span>&lt;<span class="title class_">PluginHooks</span>[H]&gt;,</span><br><span class="line">  replaceContext?: <span class="title class_">ReplaceContext</span> | <span class="literal">null</span>,</span><br><span class="line">  skip?: number | <span class="literal">null</span></span><br><span class="line">): <span class="title class_">EnsurePromise</span>&lt;R&gt; &#123;</span><br><span class="line">  <span class="comment">// 初始化 promise</span></span><br><span class="line">  <span class="keyword">let</span> <span class="attr">promise</span>: <span class="title class_">Promise</span>&lt;any&gt; = <span class="title class_">Promise</span>.<span class="title function_">resolve</span>();</span><br><span class="line">  <span class="comment">// this.plugins 在实例化 Graph 的时候，进行了初始化</span></span><br><span class="line">  <span class="keyword">for</span> (<span class="keyword">let</span> i = <span class="number">0</span>; i &lt; <span class="variable language_">this</span>.<span class="property">plugins</span>.<span class="property">length</span>; i++) &#123;</span><br><span class="line">    <span class="keyword">if</span> (skip === i) <span class="keyword">continue</span>;</span><br><span class="line">    <span class="comment">// 覆盖之前的 promise，换言之就是串行执行钩子函数</span></span><br><span class="line">    promise = promise.<span class="title function_">then</span>(<span class="function">(<span class="params">result: any</span>) =&gt;</span> &#123;</span><br><span class="line">      <span class="comment">// 返回非 null 或 undefined 的时候，停止运行，返回结果</span></span><br><span class="line">      <span class="keyword">if</span> (result != <span class="literal">null</span>) <span class="keyword">return</span> result;</span><br><span class="line">      <span class="comment">// 执行钩子函数</span></span><br><span class="line">      <span class="keyword">return</span> <span class="variable language_">this</span>.<span class="title function_">runHook</span>(hookName, args <span class="keyword">as</span> any[], i, <span class="literal">false</span>, replaceContext);</span><br><span class="line">    &#125;);</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="comment">// 返回 hook 过的 promise</span></span><br><span class="line">  <span class="keyword">return</span> promise;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>2.hookFirstSync</strong>：</p><blockquote><p>hookFirst 的同步版本，使用场景有 <code>resolveFileUrl</code>、<code>resolveImportMeta</code> 等</p></blockquote><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> hookFirstSync&lt;H <span class="keyword">extends</span> keyof <span class="title class_">PluginHooks</span>, R = <span class="title class_">ReturnType</span>&lt;<span class="title class_">PluginHooks</span>[H]&gt;&gt;(</span><br><span class="line">  <span class="attr">hookName</span>: H,</span><br><span class="line">  <span class="attr">args</span>: <span class="title class_">Args</span>&lt;<span class="title class_">PluginHooks</span>[H]&gt;,</span><br><span class="line">  replaceContext?: <span class="title class_">ReplaceContext</span></span><br><span class="line">): R &#123;</span><br><span class="line">  <span class="keyword">for</span> (<span class="keyword">let</span> i = <span class="number">0</span>; i &lt; <span class="variable language_">this</span>.<span class="property">plugins</span>.<span class="property">length</span>; i++) &#123;</span><br><span class="line">    <span class="comment">// runHook 的同步版本</span></span><br><span class="line">    <span class="keyword">const</span> result = <span class="variable language_">this</span>.<span class="title function_">runHookSync</span>(hookName, args, i, replaceContext);</span><br><span class="line">    <span class="comment">// 返回非 null 或 undefined 的时候，停止运行，返回结果</span></span><br><span class="line">    <span class="keyword">if</span> (result != <span class="literal">null</span>) <span class="keyword">return</span> result <span class="keyword">as</span> any;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="comment">// 否则返回 null</span></span><br><span class="line">  <span class="keyword">return</span> <span class="literal">null</span> <span class="keyword">as</span> any;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>3.hookSeq</strong></p><blockquote><p>加载 <code>sequential</code> 类型的钩子函数，和 hookFirst 的区别就是不能中断，使用场景有 <code>onwrite</code>、<code>generateBundle</code> 等</p></blockquote><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">async</span> <span class="keyword">function</span> hookSeq&lt;H <span class="keyword">extends</span> keyof <span class="title class_">PluginHooks</span>&gt;(</span><br><span class="line">  <span class="attr">hookName</span>: H,</span><br><span class="line">  <span class="attr">args</span>: <span class="title class_">Args</span>&lt;<span class="title class_">PluginHooks</span>[H]&gt;,</span><br><span class="line">  replaceContext?: <span class="title class_">ReplaceContext</span>,</span><br><span class="line">  <span class="comment">// hookFirst 通过 skip 参数决定是否跳过某个钩子函数</span></span><br><span class="line">): <span class="title class_">Promise</span>&lt;<span class="built_in">void</span>&gt; &#123;</span><br><span class="line">  <span class="keyword">let</span> <span class="attr">promise</span>: <span class="title class_">Promise</span>&lt;<span class="built_in">void</span>&gt; = <span class="title class_">Promise</span>.<span class="title function_">resolve</span>();</span><br><span class="line">  <span class="keyword">for</span> (<span class="keyword">let</span> i = <span class="number">0</span>; i &lt; <span class="variable language_">this</span>.<span class="property">plugins</span>.<span class="property">length</span>; i++)</span><br><span class="line">    promise = promise.<span class="title function_">then</span>(<span class="function">() =&gt;</span></span><br><span class="line">      <span class="variable language_">this</span>.<span class="property">runHook</span>&lt;<span class="built_in">void</span>&gt;(hookName, args <span class="keyword">as</span> <span class="built_in">any</span>[], i, <span class="literal">false</span>, replaceContext),</span><br><span class="line">    );</span><br><span class="line">  <span class="keyword">return</span> promise;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>4.hookSeqSync</strong></p><blockquote><p>hookSeq 同步版本，不需要构造 promise，而是直接使用 <code>runHookSync</code> 执行钩子函数。使用场景有 <code>closeWatcher</code>、<code>watchChange</code> 等。</p></blockquote><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">hookSeqSync&lt;H <span class="keyword">extends</span> <span class="title class_">SyncPluginHooks</span> &amp; <span class="title class_">SequentialPluginHooks</span>&gt;(</span><br><span class="line">  <span class="attr">hookName</span>: H,</span><br><span class="line">  <span class="attr">args</span>: <span class="title class_">Parameters</span>&lt;<span class="title class_">PluginHooks</span>[H]&gt;,</span><br><span class="line">  replaceContext?: <span class="title class_">ReplaceContext</span></span><br><span class="line">): <span class="keyword">void</span> &#123;</span><br><span class="line">  <span class="keyword">for</span> (<span class="keyword">const</span> plugin <span class="keyword">of</span> <span class="variable language_">this</span>.<span class="property">plugins</span>) &#123;</span><br><span class="line">    <span class="variable language_">this</span>.<span class="title function_">runHookSync</span>(hookName, args, plugin, replaceContext);</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>5.hookReduceArg0</strong></p><blockquote><p>对 arg 第一项进行 reduce 操作。使用场景: <code>options</code>、<code>renderChunk</code> 等</p></blockquote><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> hookReduceArg0&lt;H <span class="keyword">extends</span> keyof <span class="title class_">PluginHooks</span>, V, R = <span class="title class_">ReturnType</span>&lt;<span class="title class_">PluginHooks</span>[H]&gt;&gt;(</span><br><span class="line">    <span class="attr">hookName</span>: H,</span><br><span class="line">    [arg0, ...args]: any[], <span class="comment">// 取出传入的数组的第一个参数，将剩余的置于一个数组中</span></span><br><span class="line">    <span class="attr">reduce</span>: <span class="title class_">Reduce</span>&lt;V, R&gt;,</span><br><span class="line">    replaceContext?: <span class="title class_">ReplaceContext</span> <span class="comment">// 替换当前 plugin 调用时候的上下文环境</span></span><br><span class="line">) &#123;</span><br><span class="line">  <span class="keyword">let</span> promise = <span class="title class_">Promise</span>.<span class="title function_">resolve</span>(arg0); <span class="comment">// 默认返回 source.code</span></span><br><span class="line">  <span class="keyword">for</span> (<span class="keyword">let</span> i = <span class="number">0</span>; i &lt; <span class="variable language_">this</span>.<span class="property">plugins</span>.<span class="property">length</span>; i++) &#123;</span><br><span class="line">    <span class="comment">// 第一个 promise 的时候只会接收到上面传递的arg0</span></span><br><span class="line">    <span class="comment">// 之后每一次 promise 接受的都是上一个插件处理过后的 source.code 值</span></span><br><span class="line">    promise = promise.<span class="title function_">then</span>(<span class="function"><span class="params">arg0</span> =&gt;</span> &#123;</span><br><span class="line">      <span class="keyword">const</span> hookPromise = <span class="variable language_">this</span>.<span class="title function_">runHook</span>(hookName, [arg0, ...args], i, <span class="literal">false</span>, replaceContext);</span><br><span class="line">      <span class="comment">// 如果没有返回 promise，那么直接返回 arg0</span></span><br><span class="line">      <span class="keyword">if</span> (!hookPromise) <span class="keyword">return</span> arg0;</span><br><span class="line">      <span class="comment">// result 代表插件执行完成的返回值</span></span><br><span class="line">      <span class="keyword">return</span> hookPromise.<span class="title function_">then</span>(<span class="function">(<span class="params">result: any</span>) =&gt;</span></span><br><span class="line">        reduce.<span class="title function_">call</span>(<span class="variable language_">this</span>.<span class="property">pluginContexts</span>[i], arg0, result, <span class="variable language_">this</span>.<span class="property">plugins</span>[i])</span><br><span class="line">      );</span><br><span class="line">    &#125;);</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">return</span> promise;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>6.hookReduceArg0Sync</strong></p><p><code>hookReduceArg0</code> 同步版本，使用场景 <code>transform</code>、<code>generateBundle</code> 等</p><p><strong>7.hookParallel</strong></p><blockquote><p>并行执行 hook，不会等待当前 hook 完成。使用场景 <code>buildEnd</code>、<code>buildStart</code>、<code>moduleParsed</code> 等。</p></blockquote><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">hookParallel&lt;H <span class="keyword">extends</span> <span class="title class_">AsyncPluginHooks</span> &amp; <span class="title class_">ParallelPluginHooks</span>&gt;(</span><br><span class="line">  <span class="attr">hookName</span>: H,</span><br><span class="line">  <span class="attr">args</span>: <span class="title class_">Parameters</span>&lt;<span class="title class_">PluginHooks</span>[H]&gt;,</span><br><span class="line">  replaceContext?: <span class="title class_">ReplaceContext</span></span><br><span class="line">): <span class="title class_">Promise</span>&lt;<span class="keyword">void</span>&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> <span class="attr">promises</span>: <span class="title class_">Promise</span>&lt;<span class="keyword">void</span>&gt;[] = [];</span><br><span class="line">  <span class="keyword">for</span> (<span class="keyword">const</span> plugin <span class="keyword">of</span> <span class="variable language_">this</span>.<span class="property">plugins</span>) &#123;</span><br><span class="line">    <span class="keyword">const</span> hookPromise = <span class="variable language_">this</span>.<span class="title function_">runHook</span>(hookName, args, plugin, <span class="literal">false</span>, replaceContext);</span><br><span class="line">    <span class="keyword">if</span> (!hookPromise) <span class="keyword">continue</span>;</span><br><span class="line">    promises.<span class="title function_">push</span>(hookPromise);</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">return</span> <span class="title class_">Promise</span>.<span class="title function_">all</span>(promises).<span class="title function_">then</span>(<span class="function">() =&gt;</span> &#123;&#125;);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="runHook"><a href="#runHook" class="headerlink" title="runHook"></a>runHook</h4><p>上面的钩子函数加载函数，内部都调用了执行钩子函数的方法 <code>runHook</code> 或 <code>runHookSync</code>，我们以 <code>runHook</code> 为例分析一下源码：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> runHook&lt;T&gt;(</span><br><span class="line">  <span class="attr">hookName</span>: <span class="built_in">string</span>,</span><br><span class="line">  <span class="attr">args</span>: <span class="built_in">any</span>[],</span><br><span class="line">  <span class="attr">pluginIndex</span>: <span class="built_in">number</span>,</span><br><span class="line">  <span class="attr">permitValues</span>: <span class="built_in">boolean</span>,</span><br><span class="line">  hookContext?: <span class="title class_">ReplaceContext</span> | <span class="literal">null</span>,</span><br><span class="line">): <span class="title class_">Promise</span>&lt;T&gt; &#123;</span><br><span class="line">  <span class="variable language_">this</span>.<span class="property">previousHooks</span>.<span class="title function_">add</span>(hookName);</span><br><span class="line">  <span class="comment">// 找到当前 plugin</span></span><br><span class="line">  <span class="keyword">const</span> plugin = <span class="variable language_">this</span>.<span class="property">plugins</span>[pluginIndex];</span><br><span class="line">  <span class="comment">// 找到当前执行的在 plugin 中定义的 hooks 钩子函数</span></span><br><span class="line">  <span class="keyword">const</span> hook = (plugin <span class="keyword">as</span> <span class="built_in">any</span>)[hookName];</span><br><span class="line">  <span class="keyword">if</span> (!hook) <span class="keyword">return</span> <span class="literal">undefined</span> <span class="keyword">as</span> <span class="built_in">any</span>;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// pluginContexts 在初始化 plugin 驱动器类的时候定义，是个数组，数组保存对应着每个插件的上下文环境</span></span><br><span class="line">  <span class="keyword">let</span> context = <span class="variable language_">this</span>.<span class="property">pluginContexts</span>[pluginIndex];</span><br><span class="line">  <span class="comment">// 用于区分对待不同钩子函数的插件上下文</span></span><br><span class="line">  <span class="keyword">if</span> (hookContext) &#123;</span><br><span class="line">    context = <span class="title function_">hookContext</span>(context, plugin);</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">return</span> <span class="title class_">Promise</span>.<span class="title function_">resolve</span>()</span><br><span class="line">    .<span class="title function_">then</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">      <span class="comment">// 许可值允许返回值，而不是一个函数钩子，使用 hookReduceValue 或 hookReduceValueSync 加载。</span></span><br><span class="line">      <span class="keyword">if</span> (<span class="keyword">typeof</span> hook !== <span class="string">&#x27;function&#x27;</span>) &#123;</span><br><span class="line">        <span class="keyword">if</span> (permitValues) <span class="keyword">return</span> hook;</span><br><span class="line">        <span class="keyword">return</span> <span class="title function_">error</span>(&#123;</span><br><span class="line">          <span class="attr">code</span>: <span class="string">&#x27;INVALID_PLUGIN_HOOK&#x27;</span>,</span><br><span class="line">          <span class="attr">message</span>: <span class="string">`Error running plugin hook <span class="subst">$&#123;hookName&#125;</span> for <span class="subst">$&#123;plugin.name&#125;</span>, expected a function hook.`</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="keyword">return</span> hook.<span class="title function_">apply</span>(context, args);</span><br><span class="line">    &#125;)</span><br><span class="line">    .<span class="title function_">catch</span>(<span class="function"><span class="params">err</span> =&gt;</span> <span class="title function_">throwPluginError</span>(err, plugin.<span class="property">name</span>, &#123; <span class="attr">hook</span>: hookName &#125;));</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="核心依赖"><a href="#核心依赖" class="headerlink" title="核心依赖"></a>核心依赖</h2><ul><li><a href="https://www.npmjs.com/package/yargs-parser">yargs-parser</a>：yargs 使用的强大的选项解析插件</li><li><a href="https://www.npmjs.com/package/source-map-support">source-map-support</a>：这个模块通过 V8 堆栈追踪 API 支持 堆栈 sourcemap 支持</li></ul><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>Rollup 的插件和其他大型框架大同小异，都是提供统一的接口并贯彻了约定优于配置的思想。9 种 hook 加载函数使 rollup 的插件开发非常灵活，同时也带来了学习成本。</p><p>和 webpack 相比，rollup 的插件系统自称一派且没有区分 plugin 和 loader。</p><p>Rollup 插件机制的核心是构建阶段和输出生成阶段的各种钩子函数。内部通过基于 Promise 实现异步 hook 的调度。</p><p>rollup 的源码全都糅杂在一个库中，阅读起来着实头大，模块、工具函数管理的看起来很随意。而且我们无法直接移植它的任何工具到我们的项目中，相比起来，webpack 的插件系统封装成了一个插件 <a href="https://github.com/webpack/tapable">tapable</a> 就很利于我们学习和使用。</p><blockquote><p>本文首发于「<a href="https://blog.zisheng.pro/">紫升的博客</a>」，同步于公众号「<a href="https://cdn.jsdelivr.net/gh/youngjuning/images/20210418112129.jpeg">紫升早茶馆</a>」和「<a href="https://juejin.cn/user/325111174662855">掘金专栏</a>」。</p></blockquote>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/1470e2efc9e9/</id>
    <link href="https://blog.zisheng.pro/posts/1470e2efc9e9/"/>
    <published>2023-04-06T15:41:26.000Z</published>
    <summary>一个 Rollup 插件是由一个或多个属性、构建钩子函数、输出钩子函数组成的对象，插件还需要符合一些官方的约定。一个插件应该作为一个包来发布，这个包导出一个可以用插件特定的选项来调用的函数，并且该函数返回一个对象。</summary>
    <title>2023 Rollup 插件机制源码解析</title>
    <updated>2026-09-18T08:15:28.632Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="副业" scheme="https://blog.zisheng.pro/categories/%E5%89%AF%E4%B8%9A/"/>
    <category term="Google AdSense" scheme="https://blog.zisheng.pro/tags/Google-AdSense/"/>
    <category term="Google Adsense 付款周期" scheme="https://blog.zisheng.pro/tags/Google-Adsense-%E4%BB%98%E6%AC%BE%E5%91%A8%E6%9C%9F/"/>
    <category term="Google Adsense 付款时间" scheme="https://blog.zisheng.pro/tags/Google-Adsense-%E4%BB%98%E6%AC%BE%E6%97%B6%E9%97%B4/"/>
    <content>
      <![CDATA[<div class="note info modern"><p>AdSense 的付款周期为每月一次。</p></div><p>Google Adsense 服务团队会将你一整个月的估算收入累积起来，然后在下个月月初确定你的最终收入，将其计入帐号余额并显示在付款页中。</p><p>如果你的余额超出付款最低限额，且你没有设置暂停付款状态，Google Adsense 会在<strong>该月的 21 日至 26 日</strong>为你发放付款。</p><p>请注意，你收到付款的确切时间取决于你所在的时区、21 日是否为周末或节假日以及你选择的付款方式。</p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1680831939815.png" alt="2023 Google Adsense 具体付款时间"></p><h2 id="结算（3-日）"><a href="#结算（3-日）" class="headerlink" title="结算（3 日）"></a>结算（3 日）</h2><p>当月 3 日 Google Adsense 会确定你上个月的估算收入并将其公布到你的付款页上。你会在付款页上看到一个专列项，其中显示了你上月交易的总收入。</p><p>你的付款页还会显示与调整项或费用有关的各种扣减金额。</p><p>请注意，Google Adsense 在确定收入的过程中扣减了你因无效活动而产生的收入，但是你无法看到这些扣减金额的专列项。详细了解<a href="https://support.google.com/adsense/answer/2808531">Google Adsense 无效活动扣减金额</a>。</p><h2 id="付款（20-日）"><a href="#付款（20-日）" class="headerlink" title="付款（20 日）"></a>付款（20 日）</h2><p>如果你需要对付款信息进行更改（包括解除暂停付款状态），请务必在每月的 20 日或之前完成。每月 20 日之后进行的更改要到下一个月的付款周期才会生效。此外，你每月 20 日的总余额必须达到付款最低限额。</p><p>如果你的余额未达到付款最低限额或者你的帐号处于暂停付款状态，那么你在该月不会收到任何付款，且你的余额将会结转到下个月的收入中。</p><h2 id="付款中（21-日-26-日）"><a href="#付款中（21-日-26-日）" class="headerlink" title="付款中（21 日 - 26 日）"></a>付款中（21 日 - 26 日）</h2><p>每月 21 日至 26 日，付款页上会显示一个“正在付款”专列项，表示 Google Adsense 已处理收入并将其支付给你的银行机构。当这条专列项更新为“付款已发出”时，即表示 Google Adsense 银行已同意支付这笔款项，并已发放进行处理。</p><div class="note warning modern"><p>如果 21 日是周末或节假日，那么付款将在该月 21 日之后第一个工作日发放。</p></div><h2 id="5-个工作日"><a href="#5-个工作日" class="headerlink" title="5 个工作日"></a>5 个工作日</h2><p>如下图的邮件，发放付款后，你最晚会在 5 个工作日内收到付款。如果月底之前你仍未收到付款，请与你的银行机构联系。</p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1682230887140.png"></p><div class="note warning modern"><p>电汇付款可能需要 15 个工作日的时间才能汇入你的账户。电汇到达你银行账户所需的具体时间取决于你账户所在的银行机构。</p></div>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/c9874d785dd6/</id>
    <link href="https://blog.zisheng.pro/posts/c9874d785dd6/"/>
    <published>2023-04-06T13:38:36.000Z</published>
    <summary>Google Adsense 服务团队会将您一整个月的估算收入累积起来，然后在下个月月初确定您的最终收入，将其计入帐号余额并显示在付款页中。如果您的余额超出付款最低限额，且您没有设置暂停付款状态，我们会在该月的 21 日至 26 日为您发放付款。</summary>
    <title>2023 Google Adsense 付款周期和时间</title>
    <updated>2026-09-18T08:15:28.629Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="问题排查" scheme="https://blog.zisheng.pro/categories/%E9%97%AE%E9%A2%98%E6%8E%92%E6%9F%A5/"/>
    <category term="GitHub" scheme="https://blog.zisheng.pro/categories/%E9%97%AE%E9%A2%98%E6%8E%92%E6%9F%A5/GitHub/"/>
    <category term="GitHub" scheme="https://blog.zisheng.pro/tags/GitHub/"/>
    <category term="GitHub Actions" scheme="https://blog.zisheng.pro/tags/GitHub-Actions/"/>
    <category term="GitHub Actions Error" scheme="https://blog.zisheng.pro/tags/GitHub-Actions-Error/"/>
    <category term="ci/cd" scheme="https://blog.zisheng.pro/tags/ci-cd/"/>
    <content>
      <![CDATA[<p>当我使用 GitHub Actions 时，有一个漫长的等待，它显示 “Waiting for a runner to pick up this job”，但是这个配置已经成功运行了很多次，我不知道为什么会出现这个问题。</p><p>runner 是由 <code>runs-on</code> 指定的，所以我尝试将 <code>runs-on</code> 的版本从 18 升到了 22，问题解决了。但是我想如果我不想总是升级 runner 的版本，我应该怎么做？我们可以像给 docker 指定 os 一样指定：</p><figure class="highlight yml"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">runs-on:</span> <span class="string">ubuntu-latest</span></span><br></pre></td></tr></table></figure>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/0c97438eff72/</id>
    <link href="https://blog.zisheng.pro/posts/0c97438eff72/"/>
    <published>2023-04-05T15:36:05.000Z</published>
    <summary>当我使用 GitHub Actions 时，有一个漫长的等待，它显示 &quot;Waiting for a runner to pick up this job&quot;，但是这个配置已经成功运行了很多次，我不知道为什么会出现这个问题。</summary>
    <title>「已解决」Error &quot;Waiting for a runner to pick up this job&quot; using GitHub Actions</title>
    <updated>2026-09-18T08:15:28.632Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="站点运营" scheme="https://blog.zisheng.pro/categories/%E7%AB%99%E7%82%B9%E8%BF%90%E8%90%A5/"/>
    <category term="SEO" scheme="https://blog.zisheng.pro/categories/%E7%AB%99%E7%82%B9%E8%BF%90%E8%90%A5/SEO/"/>
    <category term="SEO" scheme="https://blog.zisheng.pro/tags/SEO/"/>
    <category term="优化" scheme="https://blog.zisheng.pro/tags/%E4%BC%98%E5%8C%96/"/>
    <category term="文章" scheme="https://blog.zisheng.pro/tags/%E6%96%87%E7%AB%A0/"/>
    <category term="网站" scheme="https://blog.zisheng.pro/tags/%E7%BD%91%E7%AB%99/"/>
    <category term="优化实战" scheme="https://blog.zisheng.pro/tags/%E4%BC%98%E5%8C%96%E5%AE%9E%E6%88%98/"/>
    <category term="hexo seo" scheme="https://blog.zisheng.pro/tags/hexo-seo/"/>
    <content>
      <![CDATA[<h2 id="SEO-是什么"><a href="#SEO-是什么" class="headerlink" title="SEO 是什么"></a>SEO 是什么</h2><p>SEO 全名为 Search Engine Optimization，是搜索引擎优化的缩写。 SEO 原理是一种利用搜索引擎的规则提高网站在有关搜索引擎内自然排名的方式。 它是一种可衡量、可重复的过程，可用于向搜索引擎发送信号，表明你的页面值得出现在搜索结果中。</p><h2 id="SEO-内容优化技巧"><a href="#SEO-内容优化技巧" class="headerlink" title="SEO 内容优化技巧"></a>SEO 内容优化技巧</h2><h3 id="使用合适的标题"><a href="#使用合适的标题" class="headerlink" title="使用合适的标题"></a>使用合适的标题</h3><p>在页面中使用 H1 标题标记主要内容。此外，确保使用 H2、H3 等副标题来组织和说明页面上的其他内容。</p><h3 id="增加描述性-meta-标签"><a href="#增加描述性-meta-标签" class="headerlink" title="增加描述性 meta 标签"></a>增加描述性 meta 标签</h3><p>在 head 部分中添加 meta 标签并填写描述信息，向搜索引擎解释该网页的主要内容和关键字。</p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">head</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">title</span>&gt;</span>页面标题<span class="tag">&lt;/<span class="name">title</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">meta</span> <span class="attr">name</span>=<span class="string">&quot;description&quot;</span> <span class="attr">content</span>=<span class="string">&quot;页面描述&quot;</span>&gt;</span></span><br><span class="line">  <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">&quot;关键词&quot;</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">head</span>&gt;</span></span><br></pre></td></tr></table></figure><p>只要正确添加了 Hexo Front-Matter，Hexo 就会自动为你生成 meta 标签。</p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1680594944707.png" alt="seo meta 标签"></p><h3 id="图像优化"><a href="#图像优化" class="headerlink" title="图像优化"></a>图像优化</h3><p>对于每个图像，在 alt 属性中使用适当的文本描述。这有助于提高搜索引擎对页面的理解。同时，压缩图像以减少页面加载时间，并将其大小放在无需滚动即可看到的位置。如下图，可以看到我之前的文章中，有些图片是没有 alt 属性的，这是不合理的。</p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1680594655436.png" alt="seo 图像优化"></p><h3 id="内部链接"><a href="#内部链接" class="headerlink" title="内部链接"></a>内部链接</h3><p>添加内部链接，使用户能够轻松浏览相关页面。此外，添加具有含义的锚文本，以便搜索引擎更好地理解您的网站结构。</p><h3 id="状态码和-URL-结构"><a href="#状态码和-URL-结构" class="headerlink" title="状态码和 URL 结构"></a>状态码和 URL 结构</h3><p>确保所有页面都有正确的状态码（如 404 或 301）和规范的 URL 结构。这将有助于保持您的页面在搜索引擎中的良好排名。</p><p>可以看到由于我之前博客迁移过一次，造成有很多 404 的页面。</p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1680596391394.png" alt="Google Search 404"></p><p>Google Search Console 没有批量删除失效链接的功能，如果嫌麻烦，可以给博客配置一个精美的 404 页面。</p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1680596445237.png" alt="Google Search 删除 404 链接"></p><p>我有强迫症，正在一点一点地删除失效链接。我的建议就是文章发布之后，不要轻易改动文章的 URL、标题、分类、标签等，这样会造成很多的 404 链接。</p><h3 id="良好的内容和排版"><a href="#良好的内容和排版" class="headerlink" title="良好的内容和排版"></a>良好的内容和排版</h3><p>最后，一个优化 SEO 的重要因素是内容和排版。确保您的页面具有有价值的、原创的、易于阅读的内容，并使用适当的文本格式、段落和标题来使其易于阅读和理解。</p><h3 id="爬虫友好"><a href="#爬虫友好" class="headerlink" title="爬虫友好"></a>爬虫友好</h3><ol><li>提交网站地图：hexo 的话使用 hexo-generator-search 插件</li><li><code>robots.txt</code>：告诉爬虫哪些页面可以抓取，哪些页面不可以抓取</li><li>减少重复网页，使用 <code>canonical</code> 标签。</li><li>使用 <code>rel=&quot;nofollow&quot;</code> 标签来告诉爬虫不要抓取外链，hexo 的话使用 hexo-filter-nofollow。</li></ol><blockquote><p>注意：请不要给友情链接添加 <code>rel=&quot;nofollow&quot;</code> 标签，这样会影响到你的友链网站的 SEO。</p></blockquote><ul><li><a href="https://ahrefs.com/blog/zh/seo-chrome-extensions/">14个最适合 SEO 的免费 Chrome 扩展程序（亲测好用）</a></li></ul><h3 id="关键词挖掘"><a href="#关键词挖掘" class="headerlink" title="关键词挖掘"></a>关键词挖掘</h3><p>使用关键词优化工具，比如 <a href="https://ads.google.com/home/tools/keyword-planner/">Google Keyword Planner</a>，来找到最适合您的网站的关键词。</p><p>还有一些功能更强大的关键词优化工具：</p><ul><li><a href="https://keywordplanner.net/">Keyword Planner</a></li><li><a href="https://keywordtool.io/">Keyword Tool</a></li><li><a href="https://www.wordstream.com/keywords">WordStream</a></li><li><a href="https://www.cifnews.com/article/126696">好用的Google关键词分析工具</a></li></ul><p>除了关键词挖掘工具，你还可以使用 <a href="https://trends.google.com/trends/?geo=US">Google Trends</a> 来找到热门的关键词，并针对性地去创作文章。</p><p>另外还可以直接找排名靠前的文章，看看他们的关键词是怎么做的，直接复制过来就行了。</p><h2 id="免费自然流量"><a href="#免费自然流量" class="headerlink" title="免费自然流量"></a>免费自然流量</h2><p>写博客本来就不盈利，为什么要花钱去买流量呢？下面是一些免费的自然流量工具，可以让你的博客获得更多的免费流量。</p><ul><li><a href="https://www.traffic-engine.com/">Traffic Engine</a>：自动的流量交换系统，可以让你的网站获得来自世界各地的免费流量。</li><li><a href="https://somiibo.com/">Somiibo</a>：一个免费的社交媒体自动化工具，当然也可以优化网站流量。</li><li><a href="https://www.sparktraffic.com/">Spark Traffic</a>：流量机器人免费试用，在短短 1 小时内获得 2000 个免费页面浏览量</li></ul><h2 id="交换友链"><a href="#交换友链" class="headerlink" title="交换友链"></a>交换友链</h2><p>交换友链是一种很好的获取流量的方式，但是要注意的是，不要给不相关的网站交换友链，这样会影响到你的 SEO。我的建议是可以将博客同步到不同的平台，比如 CSDN、掘金、简书等，然后在这些平台上附上原文链接。</p>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/480812bf02a4/</id>
    <link href="https://blog.zisheng.pro/posts/480812bf02a4/"/>
    <published>2023-04-04T07:44:38.000Z</published>
    <summary>本文主要记录了一些用 Hexo 写技术比克的 SEO 优化方法，包括文章标题、文章内容、文章标签、文章分类等。</summary>
    <title>Hexo 博客 SEO 优化实战</title>
    <updated>2026-09-18T08:15:28.629Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="互联网" scheme="https://blog.zisheng.pro/categories/%E4%BA%92%E8%81%94%E7%BD%91/"/>
    <category term="机器学习" scheme="https://blog.zisheng.pro/tags/%E6%9C%BA%E5%99%A8%E5%AD%A6%E4%B9%A0/"/>
    <category term="推特" scheme="https://blog.zisheng.pro/tags/%E6%8E%A8%E7%89%B9/"/>
    <category term="推荐算法" scheme="https://blog.zisheng.pro/tags/%E6%8E%A8%E8%8D%90%E7%AE%97%E6%B3%95/"/>
    <category term="马斯克" scheme="https://blog.zisheng.pro/tags/%E9%A9%AC%E6%96%AF%E5%85%8B/"/>
    <category term="开源" scheme="https://blog.zisheng.pro/tags/%E5%BC%80%E6%BA%90/"/>
    <category term="推荐系统" scheme="https://blog.zisheng.pro/tags/%E6%8E%A8%E8%8D%90%E7%B3%BB%E7%BB%9F/"/>
    <content>
      <![CDATA[<p>3 月 31 日，正如马斯克一直在推特上宣传的那样，他将推特的推荐算法开源。这是一个非常有趣的项目，因为它是一个非常复杂的系统，涉及到数百万用户，每天产生数十亿条推文。这篇文章将介绍推特的推荐算法是如何工作的。</p><p>Twitter 旨在为您提供当今世界上发生的最好的事情。这需要一种推荐算法，将每天发布的大约 5 亿条推文提炼成少数几条热门推文，最终显示在您设备的 For You 时间轴上。该博客介绍了该算法如何为您的时间线选择推文。</p><p>我们的推荐系统由许多相互关联的服务和工作组成，我们将在本文中详细介绍。虽然该应用程序的许多区域都推荐推文——搜索、探索、广告——但这篇文章将重点关注主页时间线的“为你推荐”提要。</p><h2 id="我们如何选择推文？"><a href="#我们如何选择推文？" class="headerlink" title="我们如何选择推文？"></a>我们如何选择推文？</h2><p>Twitter 推荐的基础是一组核心模型和功能，它们从推文、用户和参与数据中提取潜在信息。这些模型旨在回答有关 Twitter 网络的重要问题，例如“您将来与其他用户互动的可能性有多大？” 或者，“Twitter 上有哪些社区，其中有哪些热门推文？” 准确回答这些问题使 Twitter 能够提供更相关的推荐。</p><p>推荐管道由使用这些功能的三个主要阶段组成：</p><p>在称为候选采购的过程中从不同的推荐来源获取最佳推文。<br>使用机器学习模型对每条推文进行排名。<br>应用启发式和过滤器，例如过滤掉来自您已阻止的用户的推文、NSFW 内容和您已经看过的推文。<br>负责构建和提供 For You 时间线的服务称为 Home Mixer。Home Mixer 建立在 Product Mixer 之上，Product Mixer 是我们的自定义 Scala 框架，有助于构建内容提要。该服务充当连接不同候选源、评分函数、启发式方法和过滤器的软件主干。</p><p>下图说明了用于构建时间线的主要组件：</p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1680331902056.png" alt="推特推荐算法"></p><p>让我们探索这个系统的关键部分，大致按照它们在单个时间线请求期间被调用的顺序，从从Candidate Sources检索候选人开始。</p><h2 id="候选来源"><a href="#候选来源" class="headerlink" title="候选来源"></a>候选来源</h2><p>Twitter 有几个候选源，我们用它们来为用户检索最近和相关的推文。对于每个请求，我们尝试通过这些来源从数亿推文中提取最佳的 1500 条推文。我们从您关注的人（网络内）和您不关注的人（网络外）中寻找候选人。如今，For You 时间轴平均包含 50% 的网络内推文和 50% 的网络外推文，尽管这可能因用户而异。</p><h3 id="网内资源"><a href="#网内资源" class="headerlink" title="网内资源"></a>网内资源</h3><p>网络内来源是最大的候选来源，旨在提供您关注的用户的最相关、最新的推文。它使用逻辑回归模型根据推文的相关性对您关注的推文进行有效排名。然后将热门推文发送到下一阶段。</p><p>对网络内推文进行排名的最重要组成部分是 <a href="https://www.ueo-workshop.com/wp-content/uploads/2014/04/sig-alternate.pdf">Real Graph</a>。Real Graph 是一种预测两个用户之间参与可能性的模型。您和推文作者之间的 Real Graph 得分越高，我们将包括的他们的推文就越多。</p><p>网络内源一直是 Twitter 最近工作的主题。我们最近停止使用 Fanout Service，这是一项已有 12 年历史的服务，以前用于从每个用户的推文缓存中提供网络内推文。我们还在重新设计几年前最后一次更新和训练的逻辑回归排名模型！</p><h3 id="网络外资源"><a href="#网络外资源" class="headerlink" title="网络外资源"></a>网络外资源</h3><p>在用户网络之外查找相关推文是一个更棘手的问题：如果您不关注作者，我们如何判断某个推文是否与您相关？Twitter 采用两种方法来解决这个问题。</p><h4 id="社交图谱"><a href="#社交图谱" class="headerlink" title="社交图谱"></a>社交图谱</h4><p>我们的第一种方法是通过分析您关注的人或具有相似兴趣的人的参与度来估计您会发现什么是相关的。</p><p>我们遍历参与图并回答以下问题：</p><p>我关注的人最近与哪些推文进行了互动？<br>谁喜欢和我相似的推文，他们最近还喜欢什么？<br>我们根据这些问题的答案生成候选推文，并使用逻辑回归模型对生成的推文进行排名。这种类型的图遍历对于我们的网络外推荐至关重要；我们开发了GraphJet，这是一种图形处理引擎，可维护用户和推文之间的实时交互图，以执行这些遍历。虽然这种用于搜索 Twitter 参与度和关注网络的启发式方法已被证明是有用的（这些目前服务于大约 15% 的家庭时间线推文），但嵌入空间方法已成为网络外推文的更大来源。</p><h4 id="嵌入空间"><a href="#嵌入空间" class="headerlink" title="嵌入空间"></a>嵌入空间</h4><p>嵌入空间方法旨在回答一个关于内容相似性的更普遍的问题：哪些推文和用户与我的兴趣相似？</p><p>嵌入通过生成用户兴趣和推文内容的数字表示来工作。然后，我们可以计算该嵌入空间中任意两个用户、推文或用户-推文对之间的相似度。如果我们生成准确的嵌入，我们可以使用这种相似性作为相关性的替代。</p><p>Twitter 最有用的嵌入空间之一是<a href="https://dl.acm.org/doi/10.1145/3394486.3403370">SimClusters</a>。<a href="https://github.com/twitter/sbf">SimClusters 使用自定义矩阵分解算法</a>发现由一群有影响力的用户锚定的社区。有 145,000 个社区，每三周更新一次。用户和推文在社区空间中表示，并且可以属于多个社区。社区的规模从个别朋友组的几千用户到新闻或流行文化的数亿用户不等。这些是一些最大的社区：</p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1680332081420.png" alt="Twitter 推荐算法"></p><p>我们可以通过查看推文在每个社区中的当前流行度来将推文嵌入到这些社区中。喜欢推文的社区用户越多，推文与该社区的关联度就越高。</p><h2 id="排行"><a href="#排行" class="headerlink" title="排行"></a>排行</h2><p>For You 时间线的目标是为您提供相关的推文。在这一点上，我们有大约 1500 名可能相关的候选人。评分直接预测每个候选推文的相关性，并且是在你的时间线上对推文进行排名的主要信号。在此阶段，所有候选人都受到平等对待，而不管其来自哪个候选人来源。</p><p>排名是通过一个约 48M 参数的神经网络实现的，该神经网络在推文交互上持续训练以优化积极参与（例如喜欢、转推和回复）。这种排名机制考虑了数千个特征并输出十个标签来为每条推文打分，其中每个标签代表参与的概率。我们根据这些分数对推文进行排名。</p><h2 id="启发式、过滤器和产品功能"><a href="#启发式、过滤器和产品功能" class="headerlink" title="启发式、过滤器和产品功能"></a>启发式、过滤器和产品功能</h2><p>在排名阶段之后，我们应用启发式和过滤器来实现各种产品功能。这些功能协同工作以创建平衡且多样化的提要。一些例子包括：</p><ul><li>可见性过滤：根据内容和您的偏好过滤掉推文。例如，从您屏蔽或静音的帐户中删除推文。</li><li>作者多样性：避免来自同一作者的太多连续推文。</li><li>内容平衡：确保我们提供网络内和网络外推文的公平平衡。</li><li>基于反馈的疲劳：如果观众提供了负面反馈，则降低某些推文的分数。</li><li>社会证明：排除与推文没有二级关联的网络外推文作为质量保障。换句话说，确保你关注的人与推文互动或关注推文的作者。</li><li>对话：通过将回复与原始推文串连在一起，为回复提供更多上下文。</li><li>已编辑的推文：确定设备上当前的推文是否过时，并发送指令以将其替换为已编辑的版本。</li></ul><h2 id="混合和服务"><a href="#混合和服务" class="headerlink" title="混合和服务"></a>混合和服务</h2><p>此时，Home Mixer 已准备好发送到您的设备的一组推文。作为流程的最后一步，系统将推文与其他非推文内容（如广告、关注推荐和入职提示）混合在一起，这些内容将返回到您的设备上进行显示。</p><p>上面的管道每天运行大约 50 亿次，平均完成时间不到 1.5 秒。单个管道执行需要 220 秒的 CPU 时间，几乎是您在应用程序上看到的延迟的 150 倍。</p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1680332154794.png" alt="Twitter 推荐算法"></p><p>我们开源努力的目标是向您（我们的用户）提供有关我们系统工作方式的完全透明信息。我们已经发布了为我们的建议提供支持的代码，您可以<a href="https://github.com/twitter/the-algorithm">在此处</a> （和此处）查看这些代码以更详细地了解我们的算法，并且我们还在开发多项功能，以便在我们的应用程序中为您提供更大的透明度。我们计划的一些新开发项目包括：</p><ul><li>为创作者提供更好的 Twitter 分析平台，提供更多关于影响力和参与度的信息</li><li>提高应用到你的推文或帐户的任何安全标签的透明度</li><li>更好地了解推文出现在你的时间线上的原因</li></ul>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/48e2be28f51c/</id>
    <link href="https://blog.zisheng.pro/posts/48e2be28f51c/"/>
    <published>2023-04-01T06:50:59.000Z</published>
    <summary>3 月 31 日，正如马斯克一直在推特上宣传的那样，他将推特的推荐算法开源。这是一个非常有趣的项目，因为它是一个非常复杂的系统，涉及到数百万用户，每天产生数十亿条推文。这篇文章将介绍推特的推荐算法是如何工作的。</summary>
    <title>马斯克开源的推特推荐算法是什么？</title>
    <updated>2026-09-18T08:15:28.640Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="问题排查" scheme="https://blog.zisheng.pro/categories/%E9%97%AE%E9%A2%98%E6%8E%92%E6%9F%A5/"/>
    <category term="Ant Design" scheme="https://blog.zisheng.pro/categories/%E9%97%AE%E9%A2%98%E6%8E%92%E6%9F%A5/Ant-Design/"/>
    <category term="React" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/React/"/>
    <category term="React" scheme="https://blog.zisheng.pro/tags/React/"/>
    <category term="Ant Design" scheme="https://blog.zisheng.pro/tags/Ant-Design/"/>
    <category term="table" scheme="https://blog.zisheng.pro/tags/table/"/>
    <category term="fixed" scheme="https://blog.zisheng.pro/tags/fixed/"/>
    <category term="issues" scheme="https://blog.zisheng.pro/tags/issues/"/>
    <category term="columns" scheme="https://blog.zisheng.pro/tags/columns/"/>
    <content>
      <![CDATA[<p>一般 antd Table columns 比较多的时候，我们会使用 fixed 属性来固定列，但是在使用的时候发现，fixed 属性不生效，这是为什么呢？</p><p>如下，我们使用了 fixed 属性，但是并没有生效。</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> dataSource = [</span><br><span class="line">  &#123;</span><br><span class="line">    <span class="attr">key</span>: <span class="string">&#x27;1&#x27;</span>,</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">32</span>,</span><br><span class="line">    <span class="attr">address</span>: <span class="string">&#x27;西湖区湖底公园1号&#x27;</span>,</span><br><span class="line">  &#125;,</span><br><span class="line">  &#123;</span><br><span class="line">    <span class="attr">key</span>: <span class="string">&#x27;2&#x27;</span>,</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">42</span>,</span><br><span class="line">    <span class="attr">address</span>: <span class="string">&#x27;西湖区湖底公园1号&#x27;</span>,</span><br><span class="line">  &#125;,</span><br><span class="line">];</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> columns = [</span><br><span class="line">  &#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">dataIndex</span>: <span class="string">&#x27;name&#x27;</span>,</span><br><span class="line">    <span class="attr">fixed</span>: <span class="string">&#x27;left&#x27;</span>,</span><br><span class="line">    <span class="attr">key</span>: <span class="string">&#x27;name&#x27;</span>,</span><br><span class="line">  &#125;,</span><br><span class="line">  &#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">dataIndex</span>: <span class="string">&#x27;address&#x27;</span>,</span><br><span class="line">    <span class="attr">ellipsis</span>: <span class="literal">true</span>,</span><br><span class="line">    <span class="attr">key</span>: <span class="string">&#x27;address&#x27;</span>,</span><br><span class="line">  &#125;,</span><br><span class="line">  &#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">dataIndex</span>: <span class="string">&#x27;address&#x27;</span>,</span><br><span class="line">    <span class="attr">width</span>: <span class="string">&#x27;100&#x27;</span>,</span><br><span class="line">    <span class="attr">key</span>: <span class="string">&#x27;address&#x27;</span>,</span><br><span class="line">  &#125;,</span><br><span class="line">  &#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">dataIndex</span>: <span class="string">&#x27;address&#x27;</span>,</span><br><span class="line">    <span class="attr">width</span>: <span class="string">&#x27;100&#x27;</span>,</span><br><span class="line">    <span class="attr">key</span>: <span class="string">&#x27;address&#x27;</span>,</span><br><span class="line">  &#125;,</span><br><span class="line">  &#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">dataIndex</span>: <span class="string">&#x27;address&#x27;</span>,</span><br><span class="line">    <span class="attr">width</span>: <span class="string">&#x27;100&#x27;</span>,</span><br><span class="line">    <span class="attr">key</span>: <span class="string">&#x27;address&#x27;</span>,</span><br><span class="line">  &#125;,</span><br><span class="line">  &#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">dataIndex</span>: <span class="string">&#x27;address&#x27;</span>,</span><br><span class="line">    <span class="attr">width</span>: <span class="string">&#x27;100&#x27;</span>,</span><br><span class="line">    <span class="attr">key</span>: <span class="string">&#x27;address&#x27;</span>,</span><br><span class="line">  &#125;,</span><br><span class="line">  &#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">dataIndex</span>: <span class="string">&#x27;age&#x27;</span>,</span><br><span class="line">    <span class="attr">key</span>: <span class="string">&#x27;age&#x27;</span>,</span><br><span class="line">    <span class="attr">fixed</span>: <span class="string">&#x27;right&#x27;</span></span><br><span class="line">  &#125;,</span><br><span class="line">];</span><br><span class="line"></span><br><span class="line"><span class="language-xml"><span class="tag">&lt;<span class="name">Table</span> <span class="attr">dataSource</span>=<span class="string">&#123;dataSource&#125;</span> <span class="attr">columns</span>=<span class="string">&#123;columns&#125;</span> /&gt;</span></span>;</span><br></pre></td></tr></table></figure><p>代码里有两处使用了 fixed 属性，一处是在第一列，一处是在最后一列，但是并没有生效。原因是有配置项设置了 <code>ellipsis</code> 和 <code>width</code>，这两个属性删除后，就可以表现正常了。</p>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/e9a681ea0422/</id>
    <link href="https://blog.zisheng.pro/posts/e9a681ea0422/"/>
    <published>2023-03-31T03:24:13.000Z</published>
    <summary>一般 antd Table columns 比较多的时候，我们会使用 fixed 属性来固定列，但是在使用的时候发现，fixed 属性不生效，这是为什么呢？</summary>
    <title>「已解决」Antd Table columns fixed 不生效</title>
    <updated>2026-09-18T08:15:28.636Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="副业" scheme="https://blog.zisheng.pro/categories/%E5%89%AF%E4%B8%9A/"/>
    <category term="CPA" scheme="https://blog.zisheng.pro/tags/CPA/"/>
    <category term="每次行动成本" scheme="https://blog.zisheng.pro/tags/%E6%AF%8F%E6%AC%A1%E8%A1%8C%E5%8A%A8%E6%88%90%E6%9C%AC/"/>
    <category term="广告计费" scheme="https://blog.zisheng.pro/tags/%E5%B9%BF%E5%91%8A%E8%AE%A1%E8%B4%B9/"/>
    <category term="高效性" scheme="https://blog.zisheng.pro/tags/%E9%AB%98%E6%95%88%E6%80%A7/"/>
    <category term="量身定制" scheme="https://blog.zisheng.pro/tags/%E9%87%8F%E8%BA%AB%E5%AE%9A%E5%88%B6/"/>
    <category term="精确度高" scheme="https://blog.zisheng.pro/tags/%E7%B2%BE%E7%A1%AE%E5%BA%A6%E9%AB%98/"/>
    <category term="投放门槛高" scheme="https://blog.zisheng.pro/tags/%E6%8A%95%E6%94%BE%E9%97%A8%E6%A7%9B%E9%AB%98/"/>
    <category term="流量难控制" scheme="https://blog.zisheng.pro/tags/%E6%B5%81%E9%87%8F%E9%9A%BE%E6%8E%A7%E5%88%B6/"/>
    <category term="Cost Per Action" scheme="https://blog.zisheng.pro/tags/Cost-Per-Action/"/>
    <content>
      <![CDATA[<h2 id="什么是-CPA？"><a href="#什么是-CPA？" class="headerlink" title="什么是 CPA？"></a>什么是 CPA？</h2><p>CPA（每次行动成本，Cost Per Action）是一种广告计费方式，其中广告主只需为广告获得的实际行动（例如点击、下载、注册等）付费。与传统的广告计费方式（例如CPC和CPM）相比，CPA通常更符合广告主的目标，因为他们只需要为真正的业绩付费。</p><h2 id="CPA-优点"><a href="#CPA-优点" class="headerlink" title="CPA 优点"></a>CPA 优点</h2><h3 id="高效性"><a href="#高效性" class="headerlink" title="高效性"></a>高效性</h3><p>CPA 广告模式更加高效。相比于 CPC 广告模式依赖广告点击，CPA 广告模式更注重广告销售或者申请转化。当广告转化可预测时，CPA 广告模式将比 CPC 广告更具有吸引力。</p><h3 id="量身定制"><a href="#量身定制" class="headerlink" title="量身定制"></a>量身定制</h3><p>CPA 广告是基于受众的特征进行选择的。它可以精确地针对用户兴趣和行为来决定哪些用户更可能产生招募或购买转化，并将广告放在适当的平台上。这时候，品牌也就能够通过小投入获得较大收益</p><h3 id="精确度高"><a href="#精确度高" class="headerlink" title="精确度高"></a>精确度高</h3><p>CPA 广告不同于其他广告运营模式，由于其明确的转化目标，才能够真正达到有效广告计费的目的，广告方会根据其所期望的用户行为类型来进行广告曝光，从而达到更好的广告效果。</p><h2 id="CPA-缺点"><a href="#CPA-缺点" class="headerlink" title="CPA 缺点"></a>CPA 缺点</h2><h3 id="投放门槛高"><a href="#投放门槛高" class="headerlink" title="投放门槛高"></a>投放门槛高</h3><p>与传统的 CPC 和 CPM 广告计费方式相比，CPA 对广告主的要求较高。首先要精准地选择用户受众，其次需要建立一个可追踪转化的推广渠道。</p><h3 id="流量难控制"><a href="#流量难控制" class="headerlink" title="流量难控制"></a>流量难控制</h3><p>当你的投放目标是阅读文章、下载 APP 等有一些的行为去实现，这种情况下很难通过流量的监测来判断真正的转化行为是否发生，在这方面的效果无法够足范围内的期待。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>虽然 CPA 不适用于所有营销活动，但它已经成为了许多品牌市场营销策略中非常重要的一个组成部分。根据实际情况权衡利弊，合理整合资源，并在 CPA 广告模式下得到回报。</p>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/25919c20aa71/</id>
    <link href="https://blog.zisheng.pro/posts/25919c20aa71/"/>
    <published>2023-03-30T13:11:30.000Z</published>
    <summary>本文描述了什么是 CPA 广告计费方式，及其优点和缺点。CPA 广告的高效性在于它更加关注广告销售或申请转化，而且能够量身定制受众，适当地放置相应平台。然而，CPA广告也有投放门槛高、流量难控等问题。</summary>
    <title>什么是 CPA</title>
    <updated>2026-09-18T08:15:28.636Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="跨境电商" scheme="https://blog.zisheng.pro/categories/%E8%B7%A8%E5%A2%83%E7%94%B5%E5%95%86/"/>
    <category term="独立站" scheme="https://blog.zisheng.pro/tags/%E7%8B%AC%E7%AB%8B%E7%AB%99/"/>
    <category term="跨境电商" scheme="https://blog.zisheng.pro/tags/%E8%B7%A8%E5%A2%83%E7%94%B5%E5%95%86/"/>
    <category term="免费" scheme="https://blog.zisheng.pro/tags/%E5%85%8D%E8%B4%B9/"/>
    <category term="Hexo" scheme="https://blog.zisheng.pro/tags/Hexo/"/>
    <category term="静态站点" scheme="https://blog.zisheng.pro/tags/%E9%9D%99%E6%80%81%E7%AB%99%E7%82%B9/"/>
    <category term="GitHub" scheme="https://blog.zisheng.pro/tags/GitHub/"/>
    <content>
      <![CDATA[<h2 id="环境准备"><a href="#环境准备" class="headerlink" title="环境准备"></a>环境准备</h2><ol><li>NodeJS: <a href="https://nodejs.org/zh-cn/download">https://nodejs.org/zh-cn/download</a></li><li>Git: <a href="https://git-scm.com/downloads">https://git-scm.com/downloads</a></li><li>GitHub: <a href="https://github.com/">https://github.com</a></li></ol><h2 id="Hexo-搭建"><a href="#Hexo-搭建" class="headerlink" title="Hexo 搭建"></a>Hexo 搭建</h2><h3 id="创建-Hexo-站点"><a href="#创建-Hexo-站点" class="headerlink" title="创建 Hexo 站点"></a>创建 Hexo 站点</h3><blockquote><p>请替换 hyonline 为你自己的</p></blockquote><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">npm install hexo-cli -g</span><br><span class="line">hexo init hyonline</span><br><span class="line"><span class="built_in">cd</span> blog</span><br><span class="line">yarn install</span><br><span class="line">yarn server</span><br></pre></td></tr></table></figure><p>按照提示打开 <code>http://localhost:4000</code>，如果看到 Hexo 的默认页面，说明安装成功。</p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1680158474181.png" alt="Hexo 本地预览"></p><h3 id="推送到-GitHub"><a href="#推送到-GitHub" class="headerlink" title="推送到 GitHub"></a>推送到 GitHub</h3><p>1、创建 GitHub 仓库</p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1680158386923.png" alt="创建 GitHub 仓库"></p><blockquote><p>仓库格式是 username.github.io</p></blockquote><p>2、推送到 GitHub</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">git init</span><br><span class="line">git add .</span><br><span class="line">git commit -m <span class="string">&quot;First Commit&quot;</span></span><br><span class="line">git remote add origin https://github.com/hyonline-store/hyonline-store.github.io.git</span><br><span class="line">git branch -M main</span><br><span class="line">git push -u origin main</span><br></pre></td></tr></table></figure><h2 id="自动部署"><a href="#自动部署" class="headerlink" title="自动部署"></a>自动部署</h2><p>1、设置 Workflow permissions 为 Read and write permissions</p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1680162995972.png" alt="Workflow permissions"></p><p>2、添加 <code>.github/workflows</code> 目录，创建 <code>gh-pages.yml</code> 文件，内容如下：</p><figure class="highlight yml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">name:</span> <span class="string">github</span> <span class="string">pages</span></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><br><span class="line">      <span class="bullet">-</span> <span class="string">main</span> <span class="comment"># default branch</span></span><br><span class="line"><span class="attr">jobs:</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-22.04</span></span><br><span class="line">    <span class="attr">steps:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">uses:</span> <span class="string">actions/checkout@v2</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">uses:</span> <span class="string">c-hive/gha-yarn-cache@v2</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">run:</span> <span class="string">yarn</span> <span class="string">install</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">run:</span> <span class="string">yarn</span> <span class="string">build</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Deploy</span></span><br><span class="line">        <span class="attr">uses:</span> <span class="string">peaceiris/actions-gh-pages@v3</span></span><br><span class="line">        <span class="attr">with:</span></span><br><span class="line">          <span class="attr">github_token:</span> <span class="string">$&#123;&#123;</span> <span class="string">secrets.GITHUB_TOKEN</span> <span class="string">&#125;&#125;</span></span><br><span class="line">          <span class="attr">publish_dir:</span> <span class="string">./public</span></span><br></pre></td></tr></table></figure><p>3、 部署成功后，设置 Pages</p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1680163086920.png" alt="设置 GitHub Pages"></p><p>等待 GitHub Action 执行完成，就可以通过 <code>https://hyonline-store.github.io</code> 访问了。</p><h2 id="域名解析"><a href="#域名解析" class="headerlink" title="域名解析"></a>域名解析</h2><p>1、添加 A 记录（Godaddy 为例）</p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1680169974154.png" alt="添加 A 记录"></p><p>2、添加 CNAME 记录（Godaddy 为例）</p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1680170058837.png" alt="添加 CNAME 记录"></p><p>3、GitHub Pages 解析</p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1680170403897.png" alt="GitHub Pages 解析"></p><p>4、Hexo CNAME 设置</p><figure class="highlight diff"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line">name: github pages</span><br><span class="line">on:</span><br><span class="line">  push:</span><br><span class="line">    branches:</span><br><span class="line">      - main # default branch</span><br><span class="line">jobs:</span><br><span class="line">  deploy:</span><br><span class="line">    runs-on: ubuntu-22.04</span><br><span class="line">    steps:</span><br><span class="line">      - uses: actions/checkout@v2</span><br><span class="line">      - uses: c-hive/gha-yarn-cache@v2</span><br><span class="line">      - run: yarn install</span><br><span class="line">      - run: yarn build</span><br><span class="line">      - name: Deploy</span><br><span class="line">        uses: peaceiris/actions-gh-pages@v3</span><br><span class="line">        with:</span><br><span class="line">          github_token: $&#123;&#123; secrets.GITHUB_TOKEN &#125;&#125;</span><br><span class="line">          publish_dir: ./public</span><br><span class="line"><span class="addition">+         cname: you.hyonline.store</span></span><br></pre></td></tr></table></figure>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/632d3a121d65/</id>
    <link href="https://blog.zisheng.pro/posts/632d3a121d65/"/>
    <published>2023-03-29T11:05:04.000Z</published>
    <summary>
      <![CDATA[<h2 id="环境准备"><a href="#环境准备" class="headerlink" title="环境准备"></a>环境准备</h2><ol>
<li>NodeJS: <a href="https://nodejs.org/zh-cn/download">http]]>
    </summary>
    <title>Hexo × Github Pages 免费搭建博客</title>
    <updated>2026-09-18T08:15:28.629Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="Go" scheme="https://blog.zisheng.pro/categories/Go/"/>
    <category term="Go" scheme="https://blog.zisheng.pro/tags/Go/"/>
    <category term="Cobra" scheme="https://blog.zisheng.pro/tags/Cobra/"/>
    <category term="CLI" scheme="https://blog.zisheng.pro/tags/CLI/"/>
    <category term="中文文档" scheme="https://blog.zisheng.pro/tags/%E4%B8%AD%E6%96%87%E6%96%87%E6%A1%A3/"/>
    <content>
      <![CDATA[<h2 id="关于"><a href="#关于" class="headerlink" title="关于"></a>关于</h2><p>Cobra 是 Go 的 CLI 框架。它包含一个用于创建功能强大的现代 CLI 应用程序的库，以及一个用于快速生成基于 Cobra 的应用程序和命令文件的工具。</p><p>Cobra 由 Go 项目成员和 hugo 作者 <a href="https://github.com/spf13">spf13</a> 创建，已经被许多流行的 Go 项目采用，比如 <a href="https://github.com/cli/cli">GitHub CLI</a> 和 <a href="https://github.com/docker/cli">Docker CLI</a>。</p><h2 id="特性"><a href="#特性" class="headerlink" title="特性"></a>特性</h2><ul><li>简单的基于子命令的 CLIs：<code>app server</code>、<code>app fetch</code> 等；</li><li>完全兼容 <a href="https://zh.wikipedia.org/wiki/%E5%8F%AF%E7%A7%BB%E6%A4%8D%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E6%8E%A5%E5%8F%A3">POSIX（可移植操作系统接口）</a> 的标志（包括短版和长版）</li><li>嵌套子命令</li><li>全局、局部和级联的标志</li><li>使用 <code>cobra init appname</code> 和 <code>cobra add cmdname</code> 轻松生成应用程序和命令</li><li>智能提示（<code>app srver</code> …did you mean <code>app server</code>）</li><li>自动生成命令和标志的帮助</li><li>自动识别 <code>-h</code>、<code>--help</code> 等帮助标识</li><li>自动为你的应用程序生成的 bash 自动完成</li><li>自动为你的应用程序生成 man 手册</li><li>命令别名，以便你可以更改内容而不会破坏它们</li><li>定义自己的帮助，用法等的灵活性。</li><li>可选与 <a href="https://github.com/spf13/viper">viper</a> 紧密集成，可用于 <a href="https://12factor.net/zh_cn/">12factor</a> 应用程序</li></ul><h2 id="安装"><a href="#安装" class="headerlink" title="安装"></a>安装</h2><p>Cobra 非常易用，首先使用 <code>go get</code> 命令安装最新版本。此命令将安装 <code>cobra</code> generator 的可执行文件及其依赖项：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ go get -u github.com/spf13/cobra/cobra</span><br></pre></td></tr></table></figure><h2 id="概念"><a href="#概念" class="headerlink" title="概念"></a>概念</h2><p>Cobra 构建在命令（commands）、参数（arguments）和 标志（flags）上。</p><p><strong>Commands</strong> 代表动作，<strong>Args</strong> 是事物，<strong>Flags</strong> 是这些动作的修饰符。</p><p>最好的应用程序在使用时会像句子一样读起来。用户将知道如何使用该应用程序，因为他们将自然地了解如何使用它。</p><p>遵循的模式是 <code>APPNAME VERB NOUN --ADJECTIVE</code>。 或 <code>APPNAME COMMAND ARG --FLAG</code></p><p>一些真实的例子可以更好地说明这一点。</p><p>在以下示例中，<code>server</code> 是命令，<code>port</code> 是标志：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">hugo server --port=1313</span><br></pre></td></tr></table></figure><p>在此命令中，我们告诉 Git 克隆 url 的内容：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">git <span class="built_in">clone</span> URL --bare</span><br></pre></td></tr></table></figure><h3 id="命令（Command）"><a href="#命令（Command）" class="headerlink" title="命令（Command）"></a>命令（Command）</h3><p>命令是应用程序的核心。应用程序提供的每一个交互都包含在 Command 中。一个命令可以有子命令和可选的运行一个动作。</p><p>在上面的示例中，<code>server</code> 是命令。</p><p><a href="https://godoc.org/github.com/spf13/cobra#Command">cobra.Command API</a></p><h3 id="标志（Flags）"><a href="#标志（Flags）" class="headerlink" title="标志（Flags）"></a>标志（Flags）</h3><p>一个标志是一种修饰命令行为的方式。Cobra 支持完全符合 <a href="https://zh.wikipedia.org/wiki/%E5%8F%AF%E7%A7%BB%E6%A4%8D%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E6%8E%A5%E5%8F%A3">POSIX（可移植操作系统接口）</a> 的标志和 Go <a href="https://golang.org/pkg/flag/">flag</a> 包。</p><p>Cobra 命令可以定义一直保留到子命令的标志和仅可用于该命令的标志。</p><p>在上面的例子中，<code>port</code> 是标志。</p><p>标志的功能是 <a href="https://github.com/spf13/pflag">pflag</a> 库提供的，该库是一个标准库的 fork，在维护相同接口的基础上兼容了 <a href="https://zh.wikipedia.org/wiki/%E5%8F%AF%E7%A7%BB%E6%A4%8D%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E6%8E%A5%E5%8F%A3">POSIX（可移植操作系统接口）</a>。</p><h2 id="入门"><a href="#入门" class="headerlink" title="入门"></a>入门</h2><p>欢迎大家提供自己的项目组织结构，但是通常基于 Cobra 的应用程序将遵循以下组织结构</p><Tree title="appName">  <ul>    <li>      cmd      <small>放置命令的文件夹</small>      <ul>        <li>add.go</li>        <li>your.go</li>        <li>commands.go</li>        <li>here.go</li>      </ul>    </li>    <li>      main.go      <small>应用程序入口</small>    </li>  </ul></Tree><p>在 Cobra 应用程序中，通常 <code>main.go</code> 文件非常。它有一个目的：初始化 Cobra。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">  <span class="string">&quot;&#123;pathToYourApp&#125;/cmd&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">  cmd.Execute()</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="使用-Cobra-生成器"><a href="#使用-Cobra-生成器" class="headerlink" title="使用 Cobra 生成器"></a>使用 Cobra 生成器</h3><p>Cobra 提供了 CLI 来创建您的应用程序和添加任意你想要的命令。这是将 Cobra 集成到您的应用程序中的最简单方法。</p><p><a href="https://github.com/spf13/cobra/blob/master/cobra/README.md">这里</a> 你可以查看更多关于生成器的资料。</p><h3 id="使用-Cobra-库"><a href="#使用-Cobra-库" class="headerlink" title="使用 Cobra 库"></a>使用 Cobra 库</h3><p>要手动接入 Cobra，您需要创建一个 <code>main.go</code> 文件和 <code>rootCmd</code> 文件。您可以选择提供合适的其他命令。</p><h4 id="创建-rootCmd"><a href="#创建-rootCmd" class="headerlink" title="创建 rootCmd"></a>创建 rootCmd</h4><p>Cobra 不需要任何特殊的构造函数。只需创建您的命令。</p><p>理想情况下，将其放置在 <code>/cmd/root.go</code> 中：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// rootCmd 代表没有调用子命令时的基础命令</span></span><br><span class="line"><span class="keyword">var</span> rootCmd = &amp;cobra.Command&#123;</span><br><span class="line">Use:   <span class="string">&quot;hugo&quot;</span>,</span><br><span class="line">Short: <span class="string">&quot;Hugo is a very fast static site generator&quot;</span>,</span><br><span class="line">  Long: <span class="string">`A Fast and Flexible Static Site Generator built with</span></span><br><span class="line"><span class="string">                love by spf13 and friends in Go.</span></span><br><span class="line"><span class="string">                Complete documentation is available at http://hugo.spf13.com`</span>,</span><br><span class="line">  <span class="comment">// 如果有相关的 action 要执行，请取消下面这行代码的注释</span></span><br><span class="line">  <span class="comment">// Run: func(cmd *cobra.Command, args []string) &#123; &#125;,</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// Execute 将所有子命令添加到root命令并适当设置标志。</span></span><br><span class="line"><span class="comment">// 这由 main.main() 调用。它只需要对 rootCmd 调用一次。</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">Execute</span><span class="params">()</span></span> &#123;</span><br><span class="line"><span class="keyword">if</span> err := rootCmd.Execute(); err != <span class="literal">nil</span> &#123;</span><br><span class="line">fmt.Println(err)</span><br><span class="line">os.Exit(<span class="number">1</span>)</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>您还将在 <code>init()</code> 函数中定义标志并处理配置。例子如下：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// cmd/root.go</span></span><br><span class="line"><span class="keyword">package</span> cmd</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line"><span class="string">&quot;fmt&quot;</span></span><br><span class="line"><span class="string">&quot;os&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="string">&quot;github.com/spf13/cobra&quot;</span></span><br><span class="line"></span><br><span class="line">homedir <span class="string">&quot;github.com/mitchellh/go-homedir&quot;</span></span><br><span class="line"><span class="string">&quot;github.com/spf13/viper&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> cfgFile <span class="type">string</span></span><br><span class="line"><span class="keyword">var</span> projectBase <span class="type">string</span></span><br><span class="line"><span class="keyword">var</span> userLicense <span class="type">string</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// rootCmd 代表没有调用子命令时的基础命令</span></span><br><span class="line"><span class="keyword">var</span> rootCmd = &amp;cobra.Command&#123;</span><br><span class="line">Use:   <span class="string">&quot;hugo&quot;</span>,</span><br><span class="line">Short: <span class="string">&quot;Hugo is a very fast static site generator&quot;</span>,</span><br><span class="line">  Long: <span class="string">`A Fast and Flexible Static Site Generator built with</span></span><br><span class="line"><span class="string">                love by spf13 and friends in Go.</span></span><br><span class="line"><span class="string">                Complete documentation is available at http://hugo.spf13.com`</span>,</span><br><span class="line">  <span class="comment">// 如果有相关的 action 要执行，请取消下面这行代码的注释</span></span><br><span class="line">  <span class="comment">// Run: func(cmd *cobra.Command, args []string) &#123; &#125;,</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// Execute 将所有子命令添加到root命令并适当设置标志。会被 main.main() 调用一次。</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">Execute</span><span class="params">()</span></span> &#123;</span><br><span class="line"><span class="keyword">if</span> err := rootCmd.Execute(); err != <span class="literal">nil</span> &#123;</span><br><span class="line">fmt.Println(err)</span><br><span class="line">os.Exit(<span class="number">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="function"><span class="keyword">func</span> <span class="title">init</span><span class="params">()</span></span> &#123;</span><br><span class="line">cobra.OnInitialize(initConfig)</span><br><span class="line">rootCmd.PersistentFlags().StringVar(&amp;cfgFile, <span class="string">&quot;config&quot;</span>, <span class="string">&quot;&quot;</span>, <span class="string">&quot;config file (default is $HOME/.cobra.yaml)&quot;</span>)</span><br><span class="line">rootCmd.PersistentFlags().StringVarP(&amp;projectBase, <span class="string">&quot;projectbase&quot;</span>, <span class="string">&quot;b&quot;</span>, <span class="string">&quot;&quot;</span>, <span class="string">&quot;base project directory eg. github.com/spf13/&quot;</span>)</span><br><span class="line">rootCmd.PersistentFlags().StringP(<span class="string">&quot;author&quot;</span>, <span class="string">&quot;a&quot;</span>, <span class="string">&quot;YOUR NAME&quot;</span>, <span class="string">&quot;Author name for copyright attribution&quot;</span>)</span><br><span class="line">rootCmd.PersistentFlags().StringVarP(&amp;userLicense, <span class="string">&quot;license&quot;</span>, <span class="string">&quot;l&quot;</span>, <span class="string">&quot;&quot;</span>, <span class="string">&quot;Name of license for the project (can provide `licensetext` in config)&quot;</span>)</span><br><span class="line">rootCmd.PersistentFlags().Bool(<span class="string">&quot;viper&quot;</span>, <span class="literal">true</span>, <span class="string">&quot;Use Viper for configuration&quot;</span>)</span><br><span class="line">viper.BindPFlag(<span class="string">&quot;author&quot;</span>, rootCmd.PersistentFlags().Lookup(<span class="string">&quot;author&quot;</span>))</span><br><span class="line">viper.BindPFlag(<span class="string">&quot;projectbase&quot;</span>, rootCmd.PersistentFlags().Lookup(<span class="string">&quot;projectbase&quot;</span>))</span><br><span class="line">viper.BindPFlag(<span class="string">&quot;useViper&quot;</span>, rootCmd.PersistentFlags().Lookup(<span class="string">&quot;viper&quot;</span>))</span><br><span class="line">viper.SetDefault(<span class="string">&quot;author&quot;</span>, <span class="string">&quot;NAME HERE &lt;EMAIL ADDRESS&gt;&quot;</span>)</span><br><span class="line">viper.SetDefault(<span class="string">&quot;license&quot;</span>, <span class="string">&quot;apache&quot;</span>)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">initConfig</span><span class="params">()</span></span> &#123;</span><br><span class="line"><span class="comment">// Don&#x27;t forget to read config either from cfgFile or from home directory!</span></span><br><span class="line"><span class="keyword">if</span> cfgFile != <span class="string">&quot;&quot;</span> &#123;</span><br><span class="line"><span class="comment">// Use config file from the flag.</span></span><br><span class="line">viper.SetConfigFile(cfgFile)</span><br><span class="line">&#125; <span class="keyword">else</span> &#123;</span><br><span class="line"><span class="comment">// Find home directory.</span></span><br><span class="line">home, err := homedir.Dir()</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">fmt.Println(err)</span><br><span class="line">os.Exit(<span class="number">1</span>)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// Search config in home directory with name &quot;.cobra&quot; (without extension).</span></span><br><span class="line">viper.AddConfigPath(home)</span><br><span class="line">viper.SetConfigName(<span class="string">&quot;.cobra&quot;</span>)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> err := viper.ReadInConfig(); err != <span class="literal">nil</span> &#123;</span><br><span class="line">fmt.Println(<span class="string">&quot;Can&#x27;t read config:&quot;</span>, err)</span><br><span class="line">os.Exit(<span class="number">1</span>)</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="创建-main-go"><a href="#创建-main-go" class="headerlink" title="创建 main.go"></a>创建 main.go</h3><p>有了根命令，你需要一个 main 函数去执行它。为了清晰起见，<code>Execute</code> 应该在根目录上运行，尽管可以在任何命令上调用它。</p><p>在 Cobra 应用中，<code>main.go</code> 是非常简单的。它只有一个作用——初始化 Cobra。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// main.go</span></span><br><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">  <span class="string">&quot;&#123;pathToYourApp&#125;/cmd&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">  cmd.Execute()</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="创建额外的命令"><a href="#创建额外的命令" class="headerlink" title="创建额外的命令"></a>创建额外的命令</h3><p>可以定义其他命令，并且通常在 <code>cmd/</code> 目录中为每个命令提供自己的文件。</p><p>如果要创建 <code>version</code> 命令，则可以创建 <code>cmd/version.go</code> 并使用以下命令进行填充：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// cmd/version.go</span></span><br><span class="line"><span class="keyword">package</span> cmd</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">  <span class="string">&quot;fmt&quot;</span></span><br><span class="line"></span><br><span class="line">  <span class="string">&quot;github.com/spf13/cobra&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">init</span><span class="params">()</span></span> &#123;</span><br><span class="line">  rootCmd.AddCommand(versionCmd)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> versionCmd = &amp;cobra.Command&#123;</span><br><span class="line">  Use:   <span class="string">&quot;version&quot;</span>,</span><br><span class="line">  Short: <span class="string">&quot;Print the version number of Hugo&quot;</span>,</span><br><span class="line">  Long:  <span class="string">`All software has versions. This is Hugo&#x27;s`</span>,</span><br><span class="line">  Run: <span class="function"><span class="keyword">func</span><span class="params">(cmd *cobra.Command, args []<span class="type">string</span>)</span></span> &#123;</span><br><span class="line">    fmt.Println(<span class="string">&quot;Hugo Static Site Generator v0.9 -- HEAD&quot;</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>标志提供修饰符以控制命令的操作方式。</p><p>由于标志是在不同位置定义和使用的，我们需要在外部定义一个具有正确作用域的变量，以分配要使用的标志。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> verbose <span class="type">bool</span></span><br><span class="line"><span class="keyword">var</span> source <span class="type">string</span></span><br></pre></td></tr></table></figure><p>这里有两种不同分配标志的方法。</p><h4 id="持久标志"><a href="#持久标志" class="headerlink" title="持久标志"></a>持久标志</h4><p>标志可以是 “persistent” 的，这意味着该标志将可用于分配给它的命令以及该命令下的每个命令。对于全局标志，将标志分配为根上的持久标志。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">rootCmd.PersistentFlags().BoolVarP(&amp;verbose, <span class="string">&quot;verbose&quot;</span>, <span class="string">&quot;v&quot;</span>, <span class="literal">false</span>, <span class="string">&quot;verbose output&quot;</span>)</span><br></pre></td></tr></table></figure><h4 id="本地标志"><a href="#本地标志" class="headerlink" title="本地标志"></a>本地标志</h4><p>也可以在本地分配一个标志，该标志仅适用于该特定命令。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">rootCmd.Flags().StringVarP(&amp;source, <span class="string">&quot;source&quot;</span>, <span class="string">&quot;s&quot;</span>, <span class="string">&quot;&quot;</span>, <span class="string">&quot;Source directory to read from&quot;</span>)</span><br></pre></td></tr></table></figure><h4 id="父命令上的本地标志"><a href="#父命令上的本地标志" class="headerlink" title="父命令上的本地标志"></a>父命令上的本地标志</h4><p>默认情况下，Cobra 仅解析目标命令上的本地标志，而忽略父命令上的任何本地标志。通过启用 <code>Command.TraverseChildren</code>，Cobra 将在执行目标命令之前解析每个命令上的本地标志</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">command := cobra.Command&#123;</span><br><span class="line">  Use: <span class="string">&quot;print [OPTIONS] [COMMANDS]&quot;</span>,</span><br><span class="line">  TraverseChildren: <span class="literal">true</span>,</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="用配置绑定标志"><a href="#用配置绑定标志" class="headerlink" title="用配置绑定标志"></a>用配置绑定标志</h4><p>您还可以将标志与 <a href="https://github.com/spf13/viper">viper</a> 绑定：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> author <span class="type">string</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">init</span><span class="params">()</span></span> &#123;</span><br><span class="line">  rootCmd.PersistentFlags().StringVar(&amp;author, <span class="string">&quot;author&quot;</span>, <span class="string">&quot;YOUR NAME&quot;</span>, <span class="string">&quot;Author name for copyright attribution&quot;</span>)</span><br><span class="line">  viper.BindPFlag(<span class="string">&quot;author&quot;</span>, rootCmd.PersistentFlags().Lookup(<span class="string">&quot;author&quot;</span>))</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在此示例中，持久标记 <code>author</code> 与 viper 绑定。请注意，当用户未提供 <code>--author</code> 标志时，变量 <code>author</code> 不会设置为 <code>config</code> 中的值。</p><p>更多信息请查看 <a href="https://github.com/spf13/viper">viper</a>。</p><h4 id="必需标志"><a href="#必需标志" class="headerlink" title="必需标志"></a>必需标志</h4><p>标志默认是可选的。如果你想在缺少标志时命令报错，请设置该标志为必需：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> region <span class="type">string</span></span><br><span class="line"></span><br><span class="line">rootCmd.Flags().StringVarP(&amp;region, <span class="string">&quot;region&quot;</span>, <span class="string">&quot;r&quot;</span>, <span class="string">&quot;&quot;</span>, <span class="string">&quot;AWS region (required)&quot;</span>)</span><br><span class="line">rootCmd.MarkFlagRequired(<span class="string">&quot;region&quot;</span>)</span><br></pre></td></tr></table></figure><h3 id="位置和自定义参数"><a href="#位置和自定义参数" class="headerlink" title="位置和自定义参数"></a>位置和自定义参数</h3><p>可以使用 Command 的 Args 字段指定位置参数的验证。</p><p>下面的验证符是内置的：</p><ul><li><code>NoArgs</code> - 如果有任何位置参数，该命令将报告错误。</li><li><code>ArbitraryArgs</code> - 命令将接受任意参数</li><li><code>OnlyValidArgs</code> - 如果 Command 的 <code>ValidArgs</code> 字段中不存在该位置参数，则该命令将报告错误。</li><li><code>MinimumNArgs(int)</code> - 如果不存在至少 N 个位置参数，则该命令将报告错误。</li><li><code>MaximumNArgs(int)</code> - 如果存在超过 N 个位置参数，则该命令将报告错误。</li><li><code>ExactArgs(int)</code> - 如果不存在 N 个位置参数，则该命令将报告错误。</li><li><code>ExactValidArgs(int)</code> - 如果没有确切的 N 个位置参数，或者如果 Command 的 ValidArgs 字段中不存在该位置参数，则该命令将报告并出错。</li><li><code>RangeArgs(min, max)</code> - 如果 args 的数目不在期望的 args 的最小和最大数目之间，则该命令将报告并出错。</li></ul><p>内置验证符使用实例：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> cmd = &amp;cobra.Command&#123;</span><br><span class="line">Use:   <span class="string">&quot;hello&quot;</span>,</span><br><span class="line">Short: <span class="string">&quot;hello&quot;</span>,</span><br><span class="line">Args:  cobra.MinimumNArgs(<span class="number">2</span>),</span><br><span class="line">Run: <span class="function"><span class="keyword">func</span><span class="params">(cmd *cobra.Command, args []<span class="type">string</span>)</span></span> &#123;</span><br><span class="line">fmt.Println(<span class="string">&quot;Hello, World!&quot;</span>)</span><br><span class="line">&#125;,</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><blockquote><p>如果只传递一个位置参数会报 <code>Error: requires at least 2 arg(s), only received 1</code> 的警告。</p></blockquote><p>设置自定义验证器的示例：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> cmd = &amp;cobra.Command&#123;</span><br><span class="line">  Short: <span class="string">&quot;hello&quot;</span>,</span><br><span class="line">  Args: <span class="function"><span class="keyword">func</span><span class="params">(cmd *cobra.Command, args []<span class="type">string</span>)</span></span> <span class="type">error</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> <span class="built_in">len</span>(args) &lt; <span class="number">1</span> &#123;</span><br><span class="line">      <span class="keyword">return</span> errors.New(<span class="string">&quot;requires at least one arg&quot;</span>)</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">if</span> myapp.IsValidColor(args[<span class="number">0</span>]) &#123;</span><br><span class="line">      <span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> fmt.Errorf(<span class="string">&quot;invalid color specified: %s&quot;</span>, args[<span class="number">0</span>])</span><br><span class="line">  &#125;,</span><br><span class="line">  Run: <span class="function"><span class="keyword">func</span><span class="params">(cmd *cobra.Command, args []<span class="type">string</span>)</span></span> &#123;</span><br><span class="line">    fmt.Println(<span class="string">&quot;Hello, World!&quot;</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>在下面的例子中，我们定义了三个命令。两个在顶层，一个（<code>cmdTimes</code>）是子命令。在这种情况下，根目录不可执行，这意味着需要一个子命令。通过不为 <code>rootCmd</code> 提供 <code>Run</code> 来实现。</p><p>我们只为一个命令定义了一个标志。</p><p>关于标志的文档在 [pflag]<a href="https://github.com/spf13/pflag)%E3%80%82">https://github.com/spf13/pflag)。</a></p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line"><span class="string">&quot;fmt&quot;</span></span><br><span class="line"><span class="string">&quot;strings&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="string">&quot;github.com/spf13/cobra&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line"><span class="keyword">var</span> echoTimes <span class="type">int</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> cmdPrint = &amp;cobra.Command&#123;</span><br><span class="line">Use:   <span class="string">&quot;Print [string to print]&quot;</span>,</span><br><span class="line">Short: <span class="string">&quot;Print anything to the screen&quot;</span>,</span><br><span class="line">Long: <span class="string">`print is for printing anything back to the screen.</span></span><br><span class="line"><span class="string">For many years people have printed back to the screen.`</span>,</span><br><span class="line">Args: cobra.MinimumNArgs(<span class="number">1</span>),</span><br><span class="line">Run: <span class="function"><span class="keyword">func</span><span class="params">(cmd *cobra.Command, args []<span class="type">string</span>)</span></span> &#123;</span><br><span class="line">fmt.Println(<span class="string">&quot;Print: &quot;</span> + strings.Join(args, <span class="string">&quot; &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">var</span> cmdEcho = &amp;cobra.Command&#123;</span><br><span class="line">Use:   <span class="string">&quot;echo [string to echo]&quot;</span>,</span><br><span class="line">Short: <span class="string">&quot;Echo anything to the screen&quot;</span>,</span><br><span class="line">Long: <span class="string">`echo is for echoing anything back.</span></span><br><span class="line"><span class="string">Echo works a lot like print, except it has a child command.`</span>,</span><br><span class="line">Args: cobra.MinimumNArgs(<span class="number">1</span>),</span><br><span class="line">Run: <span class="function"><span class="keyword">func</span><span class="params">(cmd *cobra.Command, args []<span class="type">string</span>)</span></span> &#123;</span><br><span class="line">fmt.Println(<span class="string">&quot;Print: &quot;</span> + strings.Join(args, <span class="string">&quot; &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">var</span> cmdTimes = &amp;cobra.Command&#123;</span><br><span class="line">Use:   <span class="string">&quot;times [# times] [string to echo]&quot;</span>,</span><br><span class="line">Short: <span class="string">&quot;Echo anyting to the screen more times&quot;</span>,</span><br><span class="line">Long: <span class="string">`echo things multiple times back to the user y providing</span></span><br><span class="line"><span class="string">a count and a string.`</span>,</span><br><span class="line">Args: cobra.MinimumNArgs(<span class="number">1</span>),</span><br><span class="line">Run: <span class="function"><span class="keyword">func</span><span class="params">(cmd *cobra.Command, args []<span class="type">string</span>)</span></span> &#123;</span><br><span class="line"><span class="keyword">for</span> i := <span class="number">0</span>; i &lt; echoTimes; i++ &#123;</span><br><span class="line">fmt.Println(<span class="string">&quot;Echo: &quot;</span> + strings.Join(args, <span class="string">&quot; &quot;</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">cmdTimes.Flags().IntVarP(&amp;echoTimes, <span class="string">&quot;times&quot;</span>, <span class="string">&quot;t&quot;</span>, <span class="number">1</span>, <span class="string">&quot;times to echo the input&quot;</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment">// 设置根命令</span></span><br><span class="line"><span class="keyword">var</span> rootCmd = &amp;cobra.Command&#123;Use: <span class="string">&quot;app&quot;</span>&#125;</span><br><span class="line">rootCmd.AddCommand(cmdPrint, cmdEcho)</span><br><span class="line">cmdEcho.AddCommand(cmdTimes)</span><br><span class="line"></span><br><span class="line"><span class="comment">// 初始化应用</span></span><br><span class="line">rootCmd.Execute()</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>更复杂的应用，请参考 <a href="https://github.com/gohugoio/hugo">Hugo</a> 或者 <a href="https://github.com/cli/cli">GitHub CLI</a>。</p><h3 id="帮助命令"><a href="#帮助命令" class="headerlink" title="帮助命令"></a>帮助命令</h3><p>当你添加了子命令，Cobra 会自动添加一些帮助命令。当你执行 <code>app help</code> 命令时会显示帮助信息。另外，<code>help</code> 还支持其他命令作为输入参数。举例来说，你有一个没有额外配置的 <code>create</code> 命令，<code>app help create</code> 是有效的。每一个命令还会自动获取一个 <code>--help</code> 标志。</p><h4 id="示例-1"><a href="#示例-1" class="headerlink" title="示例"></a>示例</h4><p>以下输出由 Cobra 自动生成。 除了命令和标志定义外，什么都不需要。</p><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/4ab343af6ed24362b1926bb8fd0a23b0~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><p><code>help</code> 就像其他命令一样。并没有特殊的逻辑或行为。实际上，你可以根据需要提供自己的服务。</p><h4 id="定义你自己的-help"><a href="#定义你自己的-help" class="headerlink" title="定义你自己的 help"></a>定义你自己的 help</h4><p>你可以使用下面的方法提供你自己的 Help 命令或模板。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">cmd.SetHelpCommand(cmd *Command)</span><br><span class="line">cmd.setHelpCommand(f <span class="function"><span class="keyword">func</span><span class="params">(*Command, []<span class="type">string</span>)</span></span>)</span><br><span class="line">cmd.setHelpTemplate(s <span class="type">string</span>)</span><br></pre></td></tr></table></figure><p>后两者也适用于所有子命令。</p><h3 id="使用信息"><a href="#使用信息" class="headerlink" title="使用信息"></a>使用信息</h3><p>当用户提供无效的标志或无效的命令时，Cobra 会通过向用户显示 <code>usage</code> 进行响应。</p><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0f1975f1c578418ebd37e81b682e69dd~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><h4 id="定义你自己的使用信息"><a href="#定义你自己的使用信息" class="headerlink" title="定义你自己的使用信息"></a>定义你自己的使用信息</h4><p>你可以提供你自己的 usage 函数或模板。像 <code>help</code> 一样，函数和模板可通过公共方法重写：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">cmd.SetUsageFunc(f <span class="function"><span class="keyword">func</span><span class="params">(*Command)</span></span> <span class="type">error</span>)</span><br><span class="line">cmd.SetUsageTemplate(s <span class="type">string</span>)</span><br></pre></td></tr></table></figure><p>可以参考 <a href="https://github.com/cli/cli/blob/dcf5a27f5343ea0e9b3ef71ca37a4c3948102667/pkg/cmd/root/root.go#L63">GitHub CLI</a> 的写法。</p><h3 id="版本标志"><a href="#版本标志" class="headerlink" title="版本标志"></a>版本标志</h3><p>如果给根命令设置了 <code>Version</code> 字段，Cobra 会添加一个顶级的 <code>--version</code> 标志。运行带有 <code>–version</code> 标志的应用程序，将使用版本模板将版本打印到 stdout。模板可以使用 <code>cmd.SetVersionTemplate(s string)</code> 函数自定义。</p><blockquote><p><code>SetVersionTemplate</code> 的使用可以参考 <a href="https://github.com/cli/cli/blob/dcf5a27f5343ea0e9b3ef71ca37a4c3948102667/pkg/cmd/root/root.go#L67">GitHub CLI</a></p></blockquote><h3 id="PreRun-和-PostRun-Hooks"><a href="#PreRun-和-PostRun-Hooks" class="headerlink" title="PreRun 和 PostRun Hooks"></a>PreRun 和 PostRun Hooks</h3><p>可以在执行命令之前和之后运行一个函数。<code>PersistentPreRun</code> 和 <code>PreRun</code> 函数将在 <code>Run</code> 之前执行。<code>PersistentPostRun</code> 和 <code>PostRun</code> 会在 <code>Run</code> 之后运行。如果子级未声明自己的 <code>Persistent * Run</code> 函数，则子级将继承父级的。这些函数的执行顺续如下：</p><ul><li>PersistentPreRun</li><li>PreRun</li><li>Run</li><li>PostRun</li><li>PersistentPostRun</li></ul><p>下面这个包含了两个命令的例子使用了这些特性。当子命令执行时，它会运行根命令的 <code>PersistentPreRun</code>，但是不会运行根命令的 <code>PersistentPostRun</code>：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line"><span class="string">&quot;fmt&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="string">&quot;github.com/spf13/cobra&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line"><span class="keyword">var</span> rootCmd = &amp;cobra.Command&#123;</span><br><span class="line">Use:   <span class="string">&quot;root [sub]&quot;</span>,</span><br><span class="line">Short: <span class="string">&quot;My root command&quot;</span>,</span><br><span class="line">PersistentPreRun: <span class="function"><span class="keyword">func</span><span class="params">(cmd *cobra.Command, args []<span class="type">string</span>)</span></span> &#123;</span><br><span class="line">fmt.Printf(<span class="string">&quot;Inside rootCmd PersistentPreRun with args: %v\n&quot;</span>, args)</span><br><span class="line">&#125;,</span><br><span class="line">PreRun: <span class="function"><span class="keyword">func</span><span class="params">(cmd *cobra.Command, args []<span class="type">string</span>)</span></span> &#123;</span><br><span class="line">fmt.Printf(<span class="string">&quot;Inside rootCmd PreRun with args: %v\n&quot;</span>, args)</span><br><span class="line">&#125;,</span><br><span class="line">Run: <span class="function"><span class="keyword">func</span><span class="params">(cmd *cobra.Command, args []<span class="type">string</span>)</span></span> &#123;</span><br><span class="line">fmt.Printf(<span class="string">&quot;Inside rootCmd Run with args: %v\n&quot;</span>, args)</span><br><span class="line">&#125;,</span><br><span class="line">PostRun: <span class="function"><span class="keyword">func</span><span class="params">(cmd *cobra.Command, args []<span class="type">string</span>)</span></span> &#123;</span><br><span class="line">fmt.Printf(<span class="string">&quot;Inside rootCmd PostRun with args: %v\n&quot;</span>, args)</span><br><span class="line">&#125;,</span><br><span class="line">PersistentPostRun: <span class="function"><span class="keyword">func</span><span class="params">(cmd *cobra.Command, args []<span class="type">string</span>)</span></span> &#123;</span><br><span class="line">fmt.Printf(<span class="string">&quot;Inside rootCmd PersistentPostRun with args: %v\n&quot;</span>, args)</span><br><span class="line">&#125;,</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">subCmd := &amp;cobra.Command&#123;</span><br><span class="line">Use:   <span class="string">&quot;sub [no options!]&quot;</span>,</span><br><span class="line">Short: <span class="string">&quot;My subcommand&quot;</span>,</span><br><span class="line">PreRun: <span class="function"><span class="keyword">func</span><span class="params">(cmd *cobra.Command, args []<span class="type">string</span>)</span></span> &#123;</span><br><span class="line">fmt.Printf(<span class="string">&quot;Inside subCmd PreRun with args: %v\n&quot;</span>, args)</span><br><span class="line">&#125;,</span><br><span class="line">Run: <span class="function"><span class="keyword">func</span><span class="params">(cmd *cobra.Command, args []<span class="type">string</span>)</span></span> &#123;</span><br><span class="line">fmt.Printf(<span class="string">&quot;Inside subCmd Run with args: %v\n&quot;</span>, args)</span><br><span class="line">&#125;,</span><br><span class="line">PostRun: <span class="function"><span class="keyword">func</span><span class="params">(cmd *cobra.Command, args []<span class="type">string</span>)</span></span> &#123;</span><br><span class="line">fmt.Printf(<span class="string">&quot;Inside subCmd PostRun with args: %v\n&quot;</span>, args)</span><br><span class="line">&#125;,</span><br><span class="line">PersistentPostRun: <span class="function"><span class="keyword">func</span><span class="params">(cmd *cobra.Command, args []<span class="type">string</span>)</span></span> &#123;</span><br><span class="line">fmt.Printf(<span class="string">&quot;Inside subCmd PersistentPostRun with args: %v\n&quot;</span>, args)</span><br><span class="line">&#125;,</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">rootCmd.AddCommand(subCmd)</span><br><span class="line"></span><br><span class="line">rootCmd.SetArgs([]<span class="type">string</span>&#123;<span class="string">&quot;&quot;</span>&#125;)</span><br><span class="line">rootCmd.Execute()</span><br><span class="line">fmt.Println()</span><br><span class="line">rootCmd.SetArgs([]<span class="type">string</span>&#123;<span class="string">&quot;sub&quot;</span>, <span class="string">&quot;arg1&quot;</span>, <span class="string">&quot;arg2&quot;</span>&#125;)</span><br><span class="line">rootCmd.Execute()</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>输出：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">Inside rootCmd PersistentPreRun with args: []</span><br><span class="line">Inside rootCmd PreRun with args: []</span><br><span class="line">Inside rootCmd Run with args: []</span><br><span class="line">Inside rootCmd PostRun with args: []</span><br><span class="line">Inside rootCmd PersistentPostRun with args: []</span><br><span class="line"></span><br><span class="line">Inside rootCmd PersistentPreRun with args: [arg1 arg2]</span><br><span class="line">Inside subCmd PreRun with args: [arg1 arg2]</span><br><span class="line">Inside subCmd Run with args: [arg1 arg2]</span><br><span class="line">Inside subCmd PostRun with args: [arg1 arg2]</span><br><span class="line">Inside subCmd PersistentPostRun with args: [arg1 arg2]</span><br></pre></td></tr></table></figure><h3 id="“unknown-command”-时的提示"><a href="#“unknown-command”-时的提示" class="headerlink" title="“unknown command” 时的提示"></a>“unknown command” 时的提示</h3><p>当 <code>&quot;unknown command&quot;</code> 错误发生时，Cobra 会自动打印提示。这和 git 命令的行为一致。比如</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">$ hugo srever</span><br><span class="line">Error: unknown <span class="built_in">command</span> <span class="string">&quot;srever&quot;</span> <span class="keyword">for</span> <span class="string">&quot;hugo&quot;</span></span><br><span class="line"></span><br><span class="line">Did you mean this?</span><br><span class="line">        server</span><br><span class="line"></span><br><span class="line">Run <span class="string">&#x27;hugo --help&#x27;</span> <span class="keyword">for</span> usage.</span><br></pre></td></tr></table></figure><p>系统会根据注册的每个子命令自动生成建议，并使用<a href="https://zh.wikipedia.org/wiki/%E8%90%8A%E6%96%87%E6%96%AF%E5%9D%A6%E8%B7%9D%E9%9B%A2">萊文斯坦距離</a>的实现。每个匹配最小距离 2（忽略大小写）的注册命令都将显示为建议。</p><p>如果需要禁用建议或在命令中调整字符串距离，请使用：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cmd.DisableSuggestions = <span class="literal">true</span></span><br></pre></td></tr></table></figure><p>或</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cmd.SuggestionsMinimumDistance = <span class="number">1</span></span><br></pre></td></tr></table></figure><p>您还可以使用 <code>SuggestFor</code> 属性显式为给定命令设置建议的名称。这样就可以针对不是距离很近的字符串提出建议，但是对于您的命令集和不希望使用别名的命令来说，它们都是有意义的。比如：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">$ kubectl remove</span><br><span class="line">Error: unknown <span class="built_in">command</span> <span class="string">&quot;remove&quot;</span> <span class="keyword">for</span> <span class="string">&quot;kubectl&quot;</span></span><br><span class="line"></span><br><span class="line">Did you mean this?</span><br><span class="line">        delete</span><br><span class="line"></span><br><span class="line">Run <span class="string">&#x27;kubectl help&#x27;</span> <span class="keyword">for</span> usage.</span><br></pre></td></tr></table></figure><h3 id="为你的命令生成文档"><a href="#为你的命令生成文档" class="headerlink" title="为你的命令生成文档"></a>为你的命令生成文档</h3><p>Cobra 可以基于子命令、标志等生成文档。可用格式如下：</p><ul><li><a href="https://blog.zisheng.pro/cobra/md_docs">Markdown</a></li><li><a href="https://blog.zisheng.pro/cobra/rest_docs">ReStructured Text</a></li><li><a href="https://blog.zisheng.pro/cobra/man_docs">Man Page</a></li></ul><h3 id="为你的命令生成-Bash-Completions"><a href="#为你的命令生成-Bash-Completions" class="headerlink" title="为你的命令生成 Bash Completions"></a>为你的命令生成 Bash Completions</h3><p>Cobra 可以生成 bash-completion 文件。如果你给你的命令添加了更多的信息，这些自动提示的分析会非常强大和灵活。更是信息请阅读<a href="https://blog.zisheng.pro/cobra/bash_completions">Bash Completions</a></p>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/65fc6b431371/</id>
    <link href="https://blog.zisheng.pro/posts/65fc6b431371/"/>
    <published>2023-03-28T14:18:09.000Z</published>
    <summary>Cobra 是 Go 的 CLI 框架。它包含一个用于创建功能强大的现代 CLI 应用程序的库，以及一个用于快速生成基于 Cobra 的应用程序和命令文件的工具。</summary>
    <title>Cobra 中文文档</title>
    <updated>2026-09-18T08:15:28.629Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="TypeScript" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/TypeScript/"/>
    <category term="TypeScript" scheme="https://blog.zisheng.pro/tags/TypeScript/"/>
    <category term="types" scheme="https://blog.zisheng.pro/tags/types/"/>
    <category term="DefinitelyTyped" scheme="https://blog.zisheng.pro/tags/DefinitelyTyped/"/>
    <category term="dts-gen" scheme="https://blog.zisheng.pro/tags/dts-gen/"/>
    <category term="dtslint" scheme="https://blog.zisheng.pro/tags/dtslint/"/>
    <category term="tuya-panel-kit" scheme="https://blog.zisheng.pro/tags/tuya-panel-kit/"/>
    <category term="微软" scheme="https://blog.zisheng.pro/tags/%E5%BE%AE%E8%BD%AF/"/>
    <content>
      <![CDATA[<p>在前端社区中，TypeScript 差不多是老生常谈的主题了。这不仅反映了 TypeScript 的流行度，也反映了它的学习上手成本。今天我们不来探讨 TypeScript 本身。而是记录一下我艰难地发布一个 <a href="https://www.npmjs.com/package/@types/tuya-panel-kit">@types</a> 包的历程。</p><h2 id="a-year-ago"><a href="#a-year-ago" class="headerlink" title="a year ago"></a>a year ago</h2><img src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b4aeb4e02f1640069b7e763ea0b5be40~tplv-k3u1fbpfcp-watermark.image" height="400px"/><p>上图是我在掘金的第一篇文章 <a href="https://juejin.cn/post/6844903843155689486">优雅地使用 TypeScript 开发 React Native 应用</a> 中的一条素质问答。问题就是有些库不是 TS 写的，也没提供类型声明该怎么办。从图中可见我当时的解决方法都是不可复用且不利他的。但这就是我这一年来处理该问题的常规手段。</p><p><img src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/04f411da146740aab8f620337a592850~tplv-k3u1fbpfcp-watermark.image" alt="紫升"></p><h2 id="DefinitelyTyped"><a href="#DefinitelyTyped" class="headerlink" title="DefinitelyTyped"></a>DefinitelyTyped</h2><p>像是 Node 有 NPM，Java 有 Maven，TypeScript 也有它的另一半，那就是号称 GitHub review 数量之最的 <a href="https://github.com/DefinitelyTyped/DefinitelyTyped">DefinitelyTyped</a> 项目。</p><p><img src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d503ee17f0ab45068a0d50a4d6f6034a~tplv-k3u1fbpfcp-watermark.image" alt="紫升"></p><p>在 TypeScript 大规模应用之前，社区已经有超过 90% 的顶级 JavaScript 库，或基于 Flow 编写的库（React系）。如果没有 DefinitelyTyped 项目，这些库想要提供类型支持，无疑只有完全重构代码。这既不现实也没必要。</p><blockquote><p>鉴于 DefinitelyTyped 的作用，我们说 DefinitelyTyped 让 TypeScript 再次伟大也不为过。</p></blockquote><p>DefinitelyTyped 目前是由微软官方维护的开源项目，参与的方式和 Homebrew 差不多，都是基于 GitHub 的工作流:</p><ol><li>fork <a href="https://github.com/DefinitelyTyped/DefinitelyTyped">DefinitelyTyped</a> 到自己的账号下</li><li>添加自己的包并编写类型声明</li><li>提交 PR</li><li>官方 review 并合并发布到 NPM</li></ol><h2 id="艰辛的贡献历程"><a href="#艰辛的贡献历程" class="headerlink" title="艰辛的贡献历程"></a>艰辛的贡献历程</h2><p>1、检查是否已存在同名的包：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm info @types/tuya-panel-kit</span><br></pre></td></tr></table></figure><p>2、安装 dts-gen：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm install -g dts-gen</span><br></pre></td></tr></table></figure><p>3、生成新包</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">dts-gen --dt --name tuya-panel-kit --template module</span><br></pre></td></tr></table></figure><p>4、格式化代码</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm run prettier -- --write types/tuya-panel-kit/**/*.ts</span><br></pre></td></tr></table></figure><p>这一步务必要执行，因为 DefinitelyTyped 十分严格，格式不对过不了 CI。过不了 CI，就只配和机器人对话：</p><p><img src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/140adf442a17460aa845aef8e4b8ac18~tplv-k3u1fbpfcp-watermark.image" alt="紫升"></p><p>5、dtslint</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">yarn lint tuya-panel-kit</span><br></pre></td></tr></table></figure><p>这一步是最让人头大的一步，Definitely 的规则可谓严苛，真就对的起它的 SLOGAN：</p><blockquote><p>The repository for high quality TypeScript type definitions</p></blockquote><p>我梳理了一下成功路上的绊脚石：</p><p>1、 <a href="https://github.com/DefinitelyTyped/DefinitelyTyped/issues/24788">Cannot find module ‘csstype’ when npm run lint package-name</a></p><p>这是一个流程 BUG，如果你的包依赖了 react，你需要执行 <code>cd types/react &amp;&amp; npm install</code> 和 <code>cd ~/.dts/typescript-installs/3.2/ &amp;&amp; npm install</code></p><p>2、如果你的包依赖了别的外部库，需要添加到 <a href="https://github.com/microsoft/DefinitelyTyped-tools/pull/165/files">microsoft&#x2F;DefinitelyTyped-tools</a> 项目中，否则 CI 不给过。</p><p>3、你的类型声明可能有很多不符合 dtslint 的标准，我看到有的包是在 <code>tslint.json</code> 中配置禁用掉部分规则，但是我做了尝试后被人工拒绝了。</p><p><img src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/fafa598fac2e4015ab496cc15fd94496~tplv-k3u1fbpfcp-watermark.image" alt="紫升"></p><p>然后我尝试在顶部加入 <code>// tslint:disable:max-line-length</code> 禁用规则，在说明原因后通过了 Review。就在发稿时，最新 PR 却因为一个禁用规则，被要求修改：</p><p><img src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/21a0b43a6f464c47a1b94a5fc7ed619f~tplv-k3u1fbpfcp-watermark.image" alt="紫升"></p><h2 id="规范的重要性"><a href="#规范的重要性" class="headerlink" title="规范的重要性"></a>规范的重要性</h2><p>刚开始时，这种严苛漫长的 review 流程着实让我头大。但在提过 4 个 PR 都被合并后，我发现 review 的人关心的是你为什么要这么写，是不是有什么不得已的苦衷，是否符合高质量的要求。</p><p>在参与 DefinitelyTyped 的协作中，我越来越发现规范的重要。如此体量的项目，如果没有严格有效的规范约束，势必会被开发者抛弃。那我们来看看 DefinitelyTyped 中是如何约束的：</p><ol><li><a href="https://github.com/microsoft/dtslint">dtslint</a> ：微软专门写的用来检验类型声明文件的工具。正是因为它，我做了大量优化工作。</li><li>机器人🤖（dt-mergebot、dt-review-bot、typescript-bot-watchdog）：在你的代码通过所有规范之前，都是这些机器人在和你交互。大家感兴趣的话，之后我会单独出一篇解析的文章</li><li>尽职尽责的维护：虽然有时 review 速度明显很慢（可能因为国外疫情）。但是这些维护者真的是尽职尽责的 review 你的代码。机器再厉害也只是一个减少工作量的工具。我们应该致敬的还是这些为社区默默奉献的人。</li></ol>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/5d12a72b0712/</id>
    <link href="https://blog.zisheng.pro/posts/5d12a72b0712/"/>
    <published>2023-03-26T15:17:29.000Z</published>
    <summary>在前端社区中，TypeScript 差不多是老生常谈的主题了。这不仅反映了 TypeScript 的流行度，也反映了它的学习上手成本。今天我们不来探讨 TypeScript 本身。而是记录一下我艰难地发布一个 @types包的历程。</summary>
    <title>向微软官方贡献 @types 包后引发的思考</title>
    <updated>2026-09-18T08:15:28.636Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="问题排查" scheme="https://blog.zisheng.pro/categories/%E9%97%AE%E9%A2%98%E6%8E%92%E6%9F%A5/"/>
    <category term="JavaScript" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/JavaScript/"/>
    <category term="JavaScript" scheme="https://blog.zisheng.pro/tags/JavaScript/"/>
    <category term="moment-timezone" scheme="https://blog.zisheng.pro/tags/moment-timezone/"/>
    <category term="时区" scheme="https://blog.zisheng.pro/tags/%E6%97%B6%E5%8C%BA/"/>
    <category term="TypeError" scheme="https://blog.zisheng.pro/tags/TypeError/"/>
    <content>
      <![CDATA[<p>下面这行代码报错时，问题通常不在 <code>timeZone</code> 的值，而在 <code>tz</code> 这个方法根本没有挂到当前的 <code>moment</code> 实例上：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="title function_">moment</span>().<span class="title function_">tz</span>(vm.<span class="property">timeZone</span>).<span class="title function_">format</span>(<span class="string">&#x27;YYYY-MM-DD&#x27;</span>)</span><br></pre></td></tr></table></figure><h2 id="一句话总结"><a href="#一句话总结" class="headerlink" title="一句话总结"></a>一句话总结</h2><p><code>tz()</code> 来自 <code>moment-timezone</code>，不是 <code>moment</code> 本体 API。直接从 <code>moment-timezone</code> 导入，最能避免 bundler、默认导入和重复依赖导致的实例不一致。</p><h2 id="推荐写法"><a href="#推荐写法" class="headerlink" title="推荐写法"></a>推荐写法</h2><p>先安装依赖：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">pnpm add moment-timezone</span><br></pre></td></tr></table></figure><p>然后直接导入：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> moment <span class="keyword">from</span> <span class="string">&#x27;moment-timezone&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> date = <span class="title function_">moment</span>().<span class="title function_">tz</span>(<span class="string">&#x27;Asia/Shanghai&#x27;</span>).<span class="title function_">format</span>(<span class="string">&#x27;YYYY-MM-DD&#x27;</span>)</span><br></pre></td></tr></table></figure><p>CommonJS 项目则可以使用：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> moment = <span class="built_in">require</span>(<span class="string">&#x27;moment-timezone&#x27;</span>)</span><br></pre></td></tr></table></figure><h2 id="为什么-import-moment-timezone-有时不够"><a href="#为什么-import-moment-timezone-有时不够" class="headerlink" title="为什么 import &#39;moment-timezone&#39; 有时不够"></a>为什么 <code>import &#39;moment-timezone&#39;</code> 有时不够</h2><p>旧项目常见下面的副作用导入：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> moment <span class="keyword">from</span> <span class="string">&#x27;moment&#x27;</span></span><br><span class="line"><span class="keyword">import</span> <span class="string">&#x27;moment-timezone&#x27;</span></span><br></pre></td></tr></table></figure><p>它依赖 <code>moment-timezone</code> 修改同一份 <code>moment</code> 实例。在 monorepo、alias 配置或 lockfile 出现多个 <code>moment</code> 副本时，副作用可能修改的是 A 实例，而业务代码拿到的是 B 实例，于是仍然报 <code>tz is not a function</code>。直接从 <code>moment-timezone</code> 导入可避免这层不确定性。</p><h2 id="排查清单"><a href="#排查清单" class="headerlink" title="排查清单"></a>排查清单</h2><ol><li>运行 <code>pnpm why moment</code> 或对应包管理器命令，确认没有意外安装多份 <code>moment</code>。</li><li>检查 <code>moment-timezone</code> 是否在生产依赖中，而不是只在 <code>devDependencies</code>。</li><li>检查 bundler alias 是否把 <code>moment</code> 指到不同路径。</li><li>确认时区名是 IANA 格式，例如 <code>Asia/Shanghai</code>，而不是 <code>GMT+8</code> 这类展示字符串。</li></ol><h2 id="需要新代码时的选择"><a href="#需要新代码时的选择" class="headerlink" title="需要新代码时的选择"></a>需要新代码时的选择</h2><p>Moment 已进入维护模式。新项目如果只需时区转换，可以评估 <code>date-fns-tz</code>、Luxon 或原生 <code>Intl.DateTimeFormat</code>；但在已有 Moment 代码库里，局部替换的成本未必值得。先把导入链路修正确，再决定是否迁移。</p><h2 id="我的判断框架"><a href="#我的判断框架" class="headerlink" title="我的判断框架"></a>我的判断框架</h2><p>遗留项目：继续使用 <code>moment-timezone</code> 并统一入口导入，风险最低。</p><p>新项目：优先选择现代日期库或 <code>Intl</code>，避免新增 Moment 依赖。</p>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/3ee8e89b57ce/</id>
    <link href="https://blog.zisheng.pro/posts/3ee8e89b57ce/"/>
    <published>2023-03-21T04:41:19.000Z</published>
    <summary>moment().tz is not a function 通常不是时区格式问题，而是 moment-timezone 没有被正确加载。本文给出可复现的导入方式、排查步骤和替代方案。</summary>
    <title>「已解决」TypeError: moment().tz is not a function</title>
    <updated>2026-09-18T08:15:28.636Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="紫升翻译计划" scheme="https://blog.zisheng.pro/categories/%E7%B4%AB%E5%8D%87%E7%BF%BB%E8%AF%91%E8%AE%A1%E5%88%92/"/>
    <category term="Go" scheme="https://blog.zisheng.pro/categories/Go/"/>
    <category term="Go" scheme="https://blog.zisheng.pro/tags/Go/"/>
    <category term="Go 教程" scheme="https://blog.zisheng.pro/tags/Go-%E6%95%99%E7%A8%8B/"/>
    <category term="紫升翻译计划" scheme="https://blog.zisheng.pro/tags/%E7%B4%AB%E5%8D%87%E7%BF%BB%E8%AF%91%E8%AE%A1%E5%88%92/"/>
    <category term="Go 字符串" scheme="https://blog.zisheng.pro/tags/Go-%E5%AD%97%E7%AC%A6%E4%B8%B2/"/>
    <category term="Go String" scheme="https://blog.zisheng.pro/tags/Go-String/"/>
    <content>
      <![CDATA[<p>在 Go 中，String 值得特别一提，因为与其他语言相比，它们在实现上有所不同。</p><h2 id="String-是什么？"><a href="#String-是什么？" class="headerlink" title="String 是什么？"></a>String 是什么？</h2><p><strong>在 Go 中，一个字符串是字节的一个切片。字符串可以通过将一组字符放在双引号内来创建</strong></p><p>让我们看看一个简单的例子，创建一个 <code>string</code> 并打印出来。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;fmt&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    name := <span class="string">&quot;Hello World&quot;</span></span><br><span class="line">    fmt.Println(name)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/o9OVDgEMU0">Run in playground</a></p><p>上述程序将打印 <code>Hello World</code>。</p><p>Go 中的字符串是 <a href="https://naveenr.net/unicode-character-set-and-utf-8-utf-16-utf-32-encoding/">符合 Unicode 标准</a> 并且是 <a href="https://naveenr.net/unicode-character-set-and-utf-8-utf-16-utf-32-encoding/">UTF-8 编码</a> 的。</p><h2 id="访问一个字符串的单个字节"><a href="#访问一个字符串的单个字节" class="headerlink" title="访问一个字符串的单个字节"></a>访问一个字符串的单个字节</h2><p>由于字符串是字节的一个切片，所以可以访问字符串的每个字节。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;fmt&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">printBytes</span><span class="params">(s <span class="type">string</span>)</span></span> &#123;</span><br><span class="line">    fmt.Printf(<span class="string">&quot;Bytes: &quot;</span>)</span><br><span class="line">    <span class="keyword">for</span> i := <span class="number">0</span>; i &lt; <span class="built_in">len</span>(s); i++ &#123;</span><br><span class="line">      fmt.Printf(<span class="string">&quot;%x &quot;</span>, s[i])</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">  name := <span class="string">&quot;Hello World&quot;</span></span><br><span class="line">    fmt.Printf(<span class="string">&quot;String: %s\n&quot;</span>, name) <span class="comment">// 输入的字符串被打印出来</span></span><br><span class="line">    printBytes(name)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/B3KgBBQhiN9">Run in playground</a></p><p><code>%s</code> 是用于打印字符串的格式化标识符。<code>len(s)</code> 返回字符串中的字节数，我们使用 <code>for</code> 循环以十六进制符号打印这些字节。<code>%x</code> 是十六进制的格式指定符。上述程序的输出结果是：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">String: Hello World</span><br><span class="line">Bytes: 48 65 6c 6c 6f 20 57 6f 72 6c 64</span><br></pre></td></tr></table></figure><p>这是 <code>Hello World</code> 的 <a href="https://mothereff.in/utf-8#Hello%20World">Unicode UT8 编码</a> 值. 为了更好地理解字符串，需要对 Unicode 和 UTF-8 有一个基本的了解。 我推荐阅读 <a href="https://naveenr.net/unicode-character-set-and-utf-8-utf-16-utf-32-encoding/">https://naveenr.net/unicode-character-set-and-utf-8-utf-16-utf-32-encoding/</a> 了解更多 Unicode 和 UTF-8 的知识。</p><h2 id="访问字符串的单个字符"><a href="#访问字符串的单个字符" class="headerlink" title="访问字符串的单个字符"></a>访问字符串的单个字符</h2><p>让我们对上述程序稍作修改，以打印字符串的字符。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;fmt&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">printBytes</span><span class="params">(s <span class="type">string</span>)</span></span> &#123;</span><br><span class="line">    fmt.Printf(<span class="string">&quot;Bytes: &quot;</span>)</span><br><span class="line">    <span class="keyword">for</span> i := <span class="number">0</span>; i &lt; <span class="built_in">len</span>(s); i++ &#123;</span><br><span class="line">        fmt.Printf(<span class="string">&quot;%x &quot;</span>, s[i])</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">printChars</span><span class="params">(s <span class="type">string</span>)</span></span> &#123;</span><br><span class="line">    fmt.Printf(<span class="string">&quot;Characters: &quot;</span>)</span><br><span class="line">    <span class="keyword">for</span> i := <span class="number">0</span>; i &lt; <span class="built_in">len</span>(s); i++ &#123;</span><br><span class="line">        fmt.Printf(<span class="string">&quot;%c &quot;</span>, s[i])</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    name := <span class="string">&quot;Hello World&quot;</span></span><br><span class="line">    fmt.Printf(<span class="string">&quot;String: %s\n&quot;</span>, name)</span><br><span class="line">    printChars(name)</span><br><span class="line">    fmt.Printf(<span class="string">&quot;\n&quot;</span>)</span><br><span class="line">    printBytes(name)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/ZkXmyVNsqv7">Run in playground</a></p><p><code>%c</code> 格式化标识符用于打印 <code>printChars</code> 方法中字符串参数中的字符。该程序打印的是：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">String: Hello World</span><br><span class="line">Characters: H e l l o   W o r l d</span><br><span class="line">Bytes: 48 65 6c 6c 6f 20 57 6f 72 6c 64</span><br></pre></td></tr></table></figure><p>虽然上面的程序看起来是访问字符串的单个字符的合法方式，但这有一个严重的错误。让我们来看看这个错误是什么。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;fmt&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">printBytes</span><span class="params">(s <span class="type">string</span>)</span></span> &#123;</span><br><span class="line">    fmt.Printf(<span class="string">&quot;Bytes: &quot;</span>)</span><br><span class="line">    <span class="keyword">for</span> i := <span class="number">0</span>; i &lt; <span class="built_in">len</span>(s); i++ &#123;</span><br><span class="line">        fmt.Printf(<span class="string">&quot;%x &quot;</span>, s[i])</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">printChars</span><span class="params">(s <span class="type">string</span>)</span></span> &#123;</span><br><span class="line">    fmt.Printf(<span class="string">&quot;Characters: &quot;</span>)</span><br><span class="line">    <span class="keyword">for</span> i := <span class="number">0</span>; i &lt; <span class="built_in">len</span>(s); i++ &#123;</span><br><span class="line">        fmt.Printf(<span class="string">&quot;%c &quot;</span>, s[i])</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    name := <span class="string">&quot;Hello World&quot;</span></span><br><span class="line">    fmt.Printf(<span class="string">&quot;String: %s\n&quot;</span>, name)</span><br><span class="line">    printChars(name)</span><br><span class="line">    fmt.Printf(<span class="string">&quot;\n&quot;</span>)</span><br><span class="line">    printBytes(name)</span><br><span class="line">    fmt.Printf(<span class="string">&quot;\n\n&quot;</span>)</span><br><span class="line">    name = <span class="string">&quot;Señor&quot;</span></span><br><span class="line">    fmt.Printf(<span class="string">&quot;String: %s\n&quot;</span>, name)</span><br><span class="line">    printChars(name) <span class="comment">//</span></span><br><span class="line">    fmt.Printf(<span class="string">&quot;\n&quot;</span>)</span><br><span class="line">    printBytes(name)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/2hyVf8l9fiO">Run in playground</a></p><p>上述程序的输出是</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">String: Hello World</span><br><span class="line">Characters: H e l l o   W o r l d</span><br><span class="line">Bytes: 48 65 6c 6c 6f 20 57 6f 72 6c 64</span><br><span class="line"></span><br><span class="line">String: Señor</span><br><span class="line">Characters: S e Ã ± o r</span><br><span class="line">Bytes: 53 65 c3 b1 6f 72</span><br></pre></td></tr></table></figure><p>我们试图打印 <strong>Señor</strong> 的字符，但它输出 <strong>S e Ã ± o r</strong>，这是错误的。为什么这个程序对 <code>Señor</code> 会出错，而对 <code>Hello World </code> 却能完全正常工作。原因是 <code>ñ</code> 的 Unicode 码位是 <code>U+00F1</code>，其 <a href="https://mothereff.in/utf-8#%C3%B1">UTF-8编码</a> 占用了 2 个字节 <code>c3</code> 和 <code>b1</code>。我们试图打印字符，假设每个代码点是一个字节，这是错误的。**在 UTF-8 编码中，一个代码点可以占用 1个以上的字节。**那么我们如何解决这个问题？这就需要 <strong>rune</strong> 拯救我们的地方了。</p><h2 id="Rune"><a href="#Rune" class="headerlink" title="Rune"></a>Rune</h2><p>Rune 是 Go 中的一个内置类型，它是 <code>int32</code> 的别名。Rune 在 Go 中代表一个 Unicode 代码点。不管这个代码点占用多少字节，它都可以用 Rune 来表示。让我们修改上面的程序，用 Rune 来打印字符。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;fmt&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">printBytes</span><span class="params">(s <span class="type">string</span>)</span></span> &#123;</span><br><span class="line">    fmt.Printf(<span class="string">&quot;Bytes: &quot;</span>)</span><br><span class="line">    <span class="keyword">for</span> i := <span class="number">0</span>; i &lt; <span class="built_in">len</span>(s); i++ &#123;</span><br><span class="line">        fmt.Printf(<span class="string">&quot;%x &quot;</span>, s[i])</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">printChars</span><span class="params">(s <span class="type">string</span>)</span></span> &#123;</span><br><span class="line">    fmt.Printf(<span class="string">&quot;Characters: &quot;</span>)</span><br><span class="line">    runes := []<span class="type">rune</span>(s) <span class="comment">// 字符串被转换为 runes 的切片</span></span><br><span class="line">    <span class="comment">// 然后我们对其进行循环，并显示这些字符。</span></span><br><span class="line">    <span class="keyword">for</span> i := <span class="number">0</span>; i &lt; <span class="built_in">len</span>(runes); i++ &#123;</span><br><span class="line">        fmt.Printf(<span class="string">&quot;%c &quot;</span>, runes[i])</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    name := <span class="string">&quot;Hello World&quot;</span></span><br><span class="line">    fmt.Printf(<span class="string">&quot;String: %s\n&quot;</span>, name)</span><br><span class="line">    printChars(name)</span><br><span class="line">    fmt.Printf(<span class="string">&quot;\n&quot;</span>)</span><br><span class="line">    printBytes(name)</span><br><span class="line">    fmt.Printf(<span class="string">&quot;\n\n&quot;</span>)</span><br><span class="line">    name = <span class="string">&quot;Señor&quot;</span></span><br><span class="line">    fmt.Printf(<span class="string">&quot;String: %s\n&quot;</span>, name)</span><br><span class="line">    printChars(name)</span><br><span class="line">    fmt.Printf(<span class="string">&quot;\n&quot;</span>)</span><br><span class="line">    printBytes(name)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/n8rsfagm2SJ">Run in playground</a></p><p>上述程序打印出：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">String: Hello World</span><br><span class="line">Characters: H e l l o   W o r l d</span><br><span class="line">Bytes: 48 65 6c 6c 6f 20 57 6f 72 6c 64</span><br><span class="line"></span><br><span class="line">String: Señor</span><br><span class="line">Characters: S e ñ o r</span><br><span class="line">Bytes: 53 65 c3 b1 6f 72</span><br></pre></td></tr></table></figure><p>上述输出是完美的。只是我们想要的😀。</p><h2 id="使用-for-range-循环访问单个-Rune"><a href="#使用-for-range-循环访问单个-Rune" class="headerlink" title="使用 for range 循环访问单个 Rune"></a>使用 <code>for range</code> 循环访问单个 Rune</h2><p>上面的程序是一个完美的方式来迭代一个字符串的各个 Rune。但是 Go 为我们提供了一种更简单的方法，即使用 <code>for range</code> 循环来实现这一目的。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;fmt&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">charsAndBytePosition</span><span class="params">(s <span class="type">string</span>)</span></span> &#123;</span><br><span class="line">    <span class="comment">// 使用 for range 循环迭代 string</span></span><br><span class="line">    <span class="keyword">for</span> index, <span class="type">rune</span> := <span class="keyword">range</span> s &#123;</span><br><span class="line">        fmt.Printf(<span class="string">&quot;%c starts at byte %d\n&quot;</span>, <span class="type">rune</span>, index)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    name := <span class="string">&quot;Señor&quot;</span></span><br><span class="line">    charsAndBytePosition(name)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/0ldNBeffjYI">Run in playground</a></p><p>循环返回 Rune 开始的字节的位置，同时返回 Rune 的位置。这个程序输出：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">S starts at byte 0</span><br><span class="line">e starts at byte 1</span><br><span class="line">ñ starts at byte 2</span><br><span class="line">o starts at byte 4</span><br><span class="line">r starts at byte 5</span><br></pre></td></tr></table></figure><p>从上面的输出可以看出，<code>ñ</code> 占用了 2 个字节，因为下一个字符 <code>o</code> 是从第 4 字节开始的，而不是第 3 字节😀。</p><h2 id="从一个字节片中创建一个字符串"><a href="#从一个字节片中创建一个字符串" class="headerlink" title="从一个字节片中创建一个字符串"></a>从一个字节片中创建一个字符串</h2><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;fmt&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    byteSlice := []<span class="type">byte</span>&#123;<span class="number">0x43</span>, <span class="number">0x61</span>, <span class="number">0x66</span>, <span class="number">0xC3</span>, <span class="number">0xA9</span>&#125;</span><br><span class="line">    str := <span class="type">string</span>(byteSlice)</span><br><span class="line">    fmt.Println(str)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/Vr9pf8X8xO">Run in playground</a></p><p><code>byteSlice</code> 包含字符串 <code>Café</code>的 <a href="https://mothereff.in/utf-8#Caf%C3%A9">UTF-8编码</a> 十六进制字节。该程序打印出</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Café</span><br></pre></td></tr></table></figure><p>如果我们有相当于十六进制的十进制值，怎么办？上面的程序能工作吗？让我们来看看。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;fmt&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    byteSlice := []<span class="type">byte</span>&#123;<span class="number">67</span>, <span class="number">97</span>, <span class="number">102</span>, <span class="number">195</span>, <span class="number">169</span>&#125; <span class="comment">// 十进制相当于 &#123;&#x27;\x43&#x27;, &#x27;\x61&#x27;, &#x27;\x66&#x27;, &#x27;\xC3&#x27;, &#x27;\xA9&#x27;&#125;</span></span><br><span class="line">    str := <span class="type">string</span>(byteSlice)</span><br><span class="line">    fmt.Println(str)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/jgsRowW6XN">Run in playground</a></p><p>小数点值也可以，上述程序也会打印出 <code>Café</code>。</p><blockquote><p>原文地址 <a href="https://golangbot.com/strings/">Golang tutorial series Strings</a><br>原文作者：<a href="https://golangbot.com/about/">Naveen Ramanathan</a><br>译文出自：<a href="https://blog.zisheng.pro/categories/%E6%B4%9B%E7%AB%B9%E7%BF%BB%E8%AF%91%E8%AE%A1%E5%88%92/">紫升翻译计划</a></p></blockquote>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/e17097ce4465/</id>
    <link href="https://blog.zisheng.pro/posts/e17097ce4465/"/>
    <published>2023-03-19T07:14:00.000Z</published>
    <summary>在 Go 中，一个字符串是字节的一个切片。字符串可以通过将一组字符放在双引号内来创建</summary>
    <title>Go 语言系列教程の字符串</title>
    <updated>2026-09-18T08:15:28.629Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="紫升翻译计划" scheme="https://blog.zisheng.pro/categories/%E7%B4%AB%E5%8D%87%E7%BF%BB%E8%AF%91%E8%AE%A1%E5%88%92/"/>
    <category term="Go" scheme="https://blog.zisheng.pro/categories/Go/"/>
    <category term="Go" scheme="https://blog.zisheng.pro/tags/Go/"/>
    <category term="Go 教程" scheme="https://blog.zisheng.pro/tags/Go-%E6%95%99%E7%A8%8B/"/>
    <category term="紫升翻译计划" scheme="https://blog.zisheng.pro/tags/%E7%B4%AB%E5%8D%87%E7%BF%BB%E8%AF%91%E8%AE%A1%E5%88%92/"/>
    <category term="Go Map" scheme="https://blog.zisheng.pro/tags/Go-Map/"/>
    <content>
      <![CDATA[<h2 id="map-是什么？"><a href="#map-是什么？" class="headerlink" title="map 是什么？"></a>map 是什么？</h2><p>map 是 Go 中的一个内置类型，用于存储键值对。让我们以一个有几个员工的创业公司为例。为了简单起见，我们假设所有这些员工的名字都是唯一的。我们正在寻找一个数据结构来存储每个员工的工资。对于这个用例来说，一个 map 将是一个完美的选择。员工的名字可以是键，工资可以是值。map 类似于其他语言中的字典，如 Python。</p><h2 id="如何创建-map"><a href="#如何创建-map" class="headerlink" title="如何创建 map"></a>如何创建 map</h2><p>通过向 <code>make</code> 函数传递键和值的类型，可以创建一个 map。下面是创建一个新 map 的语法。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">make</span>(<span class="keyword">map</span>[<span class="keyword">type</span> of key]<span class="keyword">type</span> of value)</span><br></pre></td></tr></table></figure><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">employeeSalary := <span class="built_in">make</span>(<span class="keyword">map</span>[<span class="type">string</span>]<span class="type">int</span>)</span><br></pre></td></tr></table></figure><p>上面这行代码创建了一个名为 <code>employeeSalary</code> 的地图，它有 <code>string</code> 键和 <code>int</code> 值。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;fmt&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    employeeSalary := <span class="built_in">make</span>(<span class="keyword">map</span>[<span class="type">string</span>]<span class="type">int</span>)</span><br><span class="line">    fmt.Println(employeeSalary)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/EoaQ_Xwz66Z">Run in Playground</a></p><p>上面的程序创建了一个名为 <code>employeeSalary</code> 的 map，有 <code>string</code> 键和 <code>int</code> 值。上面的程序将打印。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">map</span>[]</span><br></pre></td></tr></table></figure><p>由于我们没有向 map 添加任何元素，所以它是空的。</p><h2 id="向-map-添加元素"><a href="#向-map-添加元素" class="headerlink" title="向 map 添加元素"></a>向 map 添加元素</h2><p>向 map 添加新元素的语法与数组的语法相同。下面的程序在 <code>employeeSalary</code> map 中增加了一些新雇员。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;fmt&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    employeeSalary := <span class="built_in">make</span>(<span class="keyword">map</span>[<span class="type">string</span>]<span class="type">int</span>)</span><br><span class="line">    employeeSalary[<span class="string">&quot;steve&quot;</span>] = <span class="number">12000</span></span><br><span class="line">    employeeSalary[<span class="string">&quot;jamie&quot;</span>] = <span class="number">15000</span></span><br><span class="line">    employeeSalary[<span class="string">&quot;mike&quot;</span>] = <span class="number">9000</span></span><br><span class="line">    fmt.Println(<span class="string">&quot;employeeSalary map contents:&quot;</span>, employeeSalary)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/-IUSnvdgF2I">Run in playground</a></p><p>我们增加了三个雇员 <code>steve</code>、<code>jamie</code> 和 <code>mike</code> 以及他们相应的工资。</p><p>上述程序打印出来。</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">employeeSalary map contents: map[steve:12000 jamie:15000 mike:9000]</span><br></pre></td></tr></table></figure><p>也可以在声明本身中初始化一个 map。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;fmt&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    employeeSalary := <span class="keyword">map</span>[<span class="type">string</span>]<span class="type">int</span> &#123;</span><br><span class="line">        <span class="string">&quot;steve&quot;</span>: <span class="number">12000</span>,</span><br><span class="line">        <span class="string">&quot;jamie&quot;</span>: <span class="number">15000</span>,</span><br><span class="line">    &#125;</span><br><span class="line">    employeeSalary[<span class="string">&quot;mike&quot;</span>] = <span class="number">9000</span></span><br><span class="line">    fmt.Println(<span class="string">&quot;employeeSalary map contents:&quot;</span>, employeeSalary)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/oR_j4jkJflf">Run in playground</a></p><p>上面的程序声明了 <code>employeeSalary</code>，并在声明本身中添加了两个元素。后来又增加了一个键为 <code>mike</code> 的元素。该程序打印出</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">employeeSalary map contents: map[jamie:15000 mike:9000 steve:12000]</span><br></pre></td></tr></table></figure><p>不一定只有字符串类型才是键。所有可比较的类型，如布尔、整数、浮点、复数、字符串也可以是键。甚至用户定义的类型，如结构体也可以是键。如果你想了解更多关于可比较类型的信息，请访问 <a href="http://golang.org/ref/spec#Comparison_operators%E3%80%82">http://golang.org/ref/spec#Comparison_operators。</a></p><h2 id="map-的零值"><a href="#map-的零值" class="headerlink" title="map 的零值"></a>map 的零值</h2><p>map 的零值是 <code>nil</code>。如果你试图向一个 <code>nil</code> map 添加元素，将会发生运行时 <a href="https://golangbot.com/panic-and-recover/">panic</a>。因此，在添加元素之前，map 必须被初始化。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    <span class="keyword">var</span> employeeSalary <span class="keyword">map</span>[<span class="type">string</span>]<span class="type">int</span></span><br><span class="line">    employeeSalary[<span class="string">&quot;steve&quot;</span>] = <span class="number">12000</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/DH8gJVjn6M2">Run in playground</a></p><p>在上面的程序中，<code>employeeSalary</code> 是 <code>nil</code>，我们试图在 map 中添加一个新的键。该程序将出现错误</p><p><code>panic: assignment to entry in nil map</code></p><h2 id="从-map-中检索一个键的值"><a href="#从-map-中检索一个键的值" class="headerlink" title="从 map 中检索一个键的值"></a>从 map 中检索一个键的值</h2><p>现在我们已经向 map 添加了一些元素，让我们来学习如何检索它们。<code>map[key]</code> 是检索 map 元素的语法。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;fmt&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    employeeSalary := <span class="keyword">map</span>[<span class="type">string</span>]<span class="type">int</span>&#123;</span><br><span class="line">        <span class="string">&quot;steve&quot;</span>: <span class="number">12000</span>,</span><br><span class="line">        <span class="string">&quot;jamie&quot;</span>: <span class="number">15000</span>,</span><br><span class="line">        <span class="string">&quot;mike&quot;</span>: <span class="number">9000</span>,</span><br><span class="line">    &#125;</span><br><span class="line">    employee := <span class="string">&quot;jamie&quot;</span></span><br><span class="line">    salary := employeeSalary[employee]</span><br><span class="line">    fmt.Println(<span class="string">&quot;Salary of&quot;</span>, employee, <span class="string">&quot;is&quot;</span>, salary)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/qthGPQ6pj0Z">Run in playground</a></p><p>上面的程序是非常直接的。雇员 <code>jamie</code> 的工资被检索并打印出来。该程序打印了</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Salary of jamie is 15000</span><br></pre></td></tr></table></figure><p>如果一个元素不存在，会发生什么？map 将返回该元素的类型的零值。在 <code>employeeSalary</code> 地图的例子中，如果我们试图访问一个不存在的元素，将返回 <code>int</code> 的零值，即 <code>0</code>。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;fmt&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    employeeSalary := <span class="keyword">map</span>[<span class="type">string</span>]<span class="type">int</span>&#123;</span><br><span class="line">        <span class="string">&quot;steve&quot;</span>: <span class="number">12000</span>,</span><br><span class="line">        <span class="string">&quot;jamie&quot;</span>: <span class="number">15000</span>,</span><br><span class="line">    &#125;</span><br><span class="line">    fmt.Println(<span class="string">&quot;Salary of joe is&quot;</span>, employeeSalary[<span class="string">&quot;joe&quot;</span>])</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/iVal_ll7iN7">Run in playground</a></p><p>上述程序的输出是</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Salary of joe is 0</span><br></pre></td></tr></table></figure><p>上面的程序返回 joe 的工资为 <code>0</code>。当我们试图检索 map 中不存在的键的值时，将不会出现运行时错误。</p><h2 id="Checking-if-a-key-exists"><a href="#Checking-if-a-key-exists" class="headerlink" title="Checking if a key exists"></a>Checking if a key exists</h2><p>在上一节中我们了解到，当一个键不存在时，将返回该类型的零值。当我们想知道键是否真的存在于 map 中时，这并没有帮助。</p><p>例如，我们想知道一个键是否存在于 <code>employeeSalary</code> map 中。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">value, ok := <span class="keyword">map</span>[key]</span><br></pre></td></tr></table></figure><p>以上是查找一个特定的键是否存在于 map 中的语法。如果 <code>ok</code> 为真，那么该键就存在，其值也存在于变量 <code>value</code> 中，否则该键就不存在。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;fmt&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    employeeSalary := <span class="keyword">map</span>[<span class="type">string</span>]<span class="type">int</span>&#123;</span><br><span class="line">        <span class="string">&quot;steve&quot;</span>: <span class="number">12000</span>,</span><br><span class="line">        <span class="string">&quot;jamie&quot;</span>: <span class="number">15000</span>,</span><br><span class="line">    &#125;</span><br><span class="line">    newEmp := <span class="string">&quot;joe&quot;</span></span><br><span class="line">    value, ok := employeeSalary[newEmp]</span><br><span class="line">    <span class="keyword">if</span> ok == <span class="literal">true</span> &#123;</span><br><span class="line">        fmt.Println(<span class="string">&quot;Salary of&quot;</span>, newEmp, <span class="string">&quot;is&quot;</span>, value)</span><br><span class="line">        <span class="keyword">return</span></span><br><span class="line">    &#125;</span><br><span class="line">    fmt.Println(newEmp, <span class="string">&quot;not found&quot;</span>)</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/Y4n1p4yfdVi">Run in playground</a></p><p>在上述程序中，<code>ok</code> 将是假的，因为 <code>joe</code> 不存在。因此，该程序将打印。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">joe not found</span><br></pre></td></tr></table></figure><h2 id="遍历一个-map-中的所有元素"><a href="#遍历一个-map-中的所有元素" class="headerlink" title="遍历一个 map 中的所有元素"></a>遍历一个 map 中的所有元素</h2><p><code>for</code> 循环的 <code>range</code> 形式用于遍历一个 map 的所有元素。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;fmt&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    employeeSalary := <span class="keyword">map</span>[<span class="type">string</span>]<span class="type">int</span>&#123;</span><br><span class="line">        <span class="string">&quot;steve&quot;</span>: <span class="number">12000</span>,</span><br><span class="line">        <span class="string">&quot;jamie&quot;</span>: <span class="number">15000</span>,</span><br><span class="line">        <span class="string">&quot;mike&quot;</span>:  <span class="number">9000</span>,</span><br><span class="line">    &#125;</span><br><span class="line">    fmt.Println(<span class="string">&quot;Contents of the map&quot;</span>)</span><br><span class="line">    <span class="keyword">for</span> key, value := <span class="keyword">range</span> employeeSalary &#123;</span><br><span class="line">        fmt.Printf(<span class="string">&quot;employeeSalary[%s] = %dn&quot;</span>, key, value)</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><a href="https://play.golang.org/p/rz8U_g2slb0">Run in playground</a></p><p>上述程序的输出。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">Contents of the map</span><br><span class="line">employeeSalary[mike] = 9000</span><br><span class="line">employeeSalary[steve] = 12000</span><br><span class="line">employeeSalary[jamie] = 15000</span><br></pre></td></tr></table></figure><blockquote><p>一个重要的事实是，当使用 <code>for range</code> 时，从 map 中检索数值的顺序不保证在程序的每次执行中都是一样的。它也和元素被添加到 map 中的顺序不一样</p></blockquote><h2 id="从-map-中删除元素"><a href="#从-map-中删除元素" class="headerlink" title="从 map 中删除元素"></a>从 map 中删除元素</h2><p><a href="https://golang.org/pkg/builtin/#delete">delete(map, key)</a> 是从 <code>map</code> 中删除 <code>key</code> 的语法。删除函数不返回任何值。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;fmt&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    employeeSalary := <span class="keyword">map</span>[<span class="type">string</span>]<span class="type">int</span>&#123;</span><br><span class="line">        <span class="string">&quot;steve&quot;</span>: <span class="number">12000</span>,</span><br><span class="line">        <span class="string">&quot;jamie&quot;</span>: <span class="number">15000</span>,</span><br><span class="line">        <span class="string">&quot;mike&quot;</span>: <span class="number">9000</span>,</span><br><span class="line">    &#125;</span><br><span class="line">    fmt.Println(<span class="string">&quot;map before deletion&quot;</span>, employeeSalary)</span><br><span class="line">    <span class="built_in">delete</span>(employeeSalary, <span class="string">&quot;steve&quot;</span>)</span><br><span class="line">    fmt.Println(<span class="string">&quot;map after deletion&quot;</span>, employeeSalary)</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/u0WCB-Ta_dB">Run in playground</a></p><p>上面的程序删除了键 <code>steve</code>，并且打印了</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">map before deletion map[steve:12000 jamie:15000 mike:9000]</span><br><span class="line">map after deletion map[mike:9000 jamie:15000]</span><br></pre></td></tr></table></figure><p>如果我们试图删除一个不存在于 map 中的键，将不会出现运行时错误。</p><h2 id="结构体-map"><a href="#结构体-map" class="headerlink" title="结构体 map"></a>结构体 map</h2><p>到目前为止，我们只在地图中存储了雇员的工资。如果我们也能在地图中存储每个雇员的国家，那不是很好吗？这可以通过使用一个结构体 map 来实现。雇员可以被表示为一个包含工资和国家字段的结构，它们将以字符串键和结构值存储在 map 中。让我们写一个程序来了解如何做到这一点。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;fmt&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> employee <span class="keyword">struct</span> &#123;</span><br><span class="line">    salary  <span class="type">int</span></span><br><span class="line">    country <span class="type">string</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    emp1 := employee&#123;</span><br><span class="line">        salary:  <span class="number">12000</span>,</span><br><span class="line">        country: <span class="string">&quot;USA&quot;</span>,</span><br><span class="line">    &#125;</span><br><span class="line">    emp2 := employee&#123;</span><br><span class="line">        salary:  <span class="number">14000</span>,</span><br><span class="line">        country: <span class="string">&quot;Canada&quot;</span>,</span><br><span class="line">    &#125;</span><br><span class="line">    emp3 := employee&#123;</span><br><span class="line">        salary:  <span class="number">13000</span>,</span><br><span class="line">        country: <span class="string">&quot;India&quot;</span>,</span><br><span class="line">    &#125;</span><br><span class="line">    employeeInfo := <span class="keyword">map</span>[<span class="type">string</span>]employee&#123;</span><br><span class="line">        <span class="string">&quot;Steve&quot;</span>: emp1,</span><br><span class="line">        <span class="string">&quot;Jamie&quot;</span>: emp2,</span><br><span class="line">        <span class="string">&quot;Mike&quot;</span>:  emp3,</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> name, info := <span class="keyword">range</span> employeeInfo &#123;</span><br><span class="line">        fmt.Printf(<span class="string">&quot;Employee: %s Salary:$%d  Country: %s\n&quot;</span>, name, info.salary, info.country)</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><a href="https://play.golang.org/p/wbGhkyZld1a">Run in playground</a></p><p>在上述程序中，<code>employee</code> 结构包含 <code>salary</code> 和 <code>country</code> 字段。我们创建了三个雇员<code>emp1</code>, <code>emp2</code> 和 <code>emp3</code>。</p><p>我们用我们创建的三个雇员初始化一个键类型为 <code>string</code>、值类型为 <code>employee</code> 的地图。</p><p>这个程序将打印。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">Employee: Mike Salary:$13000  Country: India</span><br><span class="line">Employee: Steve Salary:$12000  Country: USA</span><br><span class="line">Employee: Jamie Salary:$14000  Country: Canada</span><br></pre></td></tr></table></figure><h2 id="map-的长度"><a href="#map-的长度" class="headerlink" title="map 的长度"></a>map 的长度</h2><p>map 的长度可以用 <a href="https://golang.org/pkg/builtin/#len">len</a>函数来确定。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;fmt&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    employeeSalary := <span class="keyword">map</span>[<span class="type">string</span>]<span class="type">int</span>&#123;</span><br><span class="line">        <span class="string">&quot;steve&quot;</span>: <span class="number">12000</span>,</span><br><span class="line">        <span class="string">&quot;jamie&quot;</span>: <span class="number">15000</span>,</span><br><span class="line">    &#125;</span><br><span class="line">    fmt.Println(<span class="string">&quot;length is&quot;</span>, <span class="built_in">len</span>(employeeSalary))</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/vDxsqn6g-0p">Run in playground</a></p><p>上面程序中的 <code>len(employeeSalary)</code> 返回 map 长度。上面的程序打印出来。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">length is 2</span><br></pre></td></tr></table></figure><h2 id="map-是引用类型"><a href="#map-是引用类型" class="headerlink" title="map 是引用类型"></a>map 是引用类型</h2><p>与切片类似，map 是引用类型。当一个 map 被分配给一个新的变量时，它们都指向同一个内部数据结构。因此，在一个中的变化将反映在另一个中。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;fmt&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    employeeSalary := <span class="keyword">map</span>[<span class="type">string</span>]<span class="type">int</span>&#123;</span><br><span class="line">        <span class="string">&quot;steve&quot;</span>: <span class="number">12000</span>,</span><br><span class="line">        <span class="string">&quot;jamie&quot;</span>: <span class="number">15000</span>,</span><br><span class="line">        <span class="string">&quot;mike&quot;</span>: <span class="number">9000</span>,</span><br><span class="line">    &#125;</span><br><span class="line">    fmt.Println(<span class="string">&quot;Original employee salary&quot;</span>, employeeSalary)</span><br><span class="line">    modified := employeeSalary</span><br><span class="line">    modified[<span class="string">&quot;mike&quot;</span>] = <span class="number">18000</span></span><br><span class="line">    fmt.Println(<span class="string">&quot;Employee salary changed&quot;</span>, employeeSalary)</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/hWouI1KvEb_i">Run in playground</a></p><p>在上述程序中，<code>employeeSalary</code> 被分配到 <code>modified</code>。在下一行中，<code>mike</code> 的工资在 <code>modified</code> map 中被改为<code>18000</code>。Mike 的工资现在在 <code>employeeSalary</code> 中也是 <code>18000</code>。该程序输出。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">Original employee salary map[jamie:15000 mike:9000 steve:12000]</span><br><span class="line">Employee salary changed map[jamie:15000 mike:18000 steve:12000]</span><br></pre></td></tr></table></figure><p>当 map 作为参数传递给函数时也是类似的情况。当在函数中对 map 做任何改变时，它对调用者也是可见的。</p><h2 id="map-相等性"><a href="#map-相等性" class="headerlink" title="map 相等性"></a>map 相等性</h2><p>map 不能使用 <code>==</code> 操作符进行比较。<code>==</code> 只能用于检查一个 map 是否为<code>nil</code>。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    map1 := <span class="keyword">map</span>[<span class="type">string</span>]<span class="type">int</span>&#123;</span><br><span class="line">        <span class="string">&quot;one&quot;</span>: <span class="number">1</span>,</span><br><span class="line">        <span class="string">&quot;two&quot;</span>: <span class="number">2</span>,</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    map2 := map1</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> map1 == map2 &#123;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/MALqDyWkcT">Run in playground</a></p><p>上述程序将无法编译，错误为</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">invalid operation: map1 == map2 (map can only be compared to nil)</span><br></pre></td></tr></table></figure><p>检查两个 map 是否相等的一种方法是逐一比较每个 map 的各个元素。另一种方法是使用 <a href="https://golangbot.com/reflection/">反射</a>。我鼓励你为此写一个程序，并使其发挥作用。</p><p>我已经把我们讨论过的所有概念编成了一个程序。你可以从 <a href="https://github.com/golangbot/maps">github</a> 下载它。</p><p>至此，本教程结束。希望你喜欢它。请留下您的评论。</p><blockquote><p>原文地址 <a href="https://golangbot.com/maps/">Golang Maps Tutorial</a><br>原文作者：<a href="https://golangbot.com/about/">Naveen Ramanathan</a><br>译文出自：<a href="https://blog.zisheng.pro/categories/%E6%B4%9B%E7%AB%B9%E7%BF%BB%E8%AF%91%E8%AE%A1%E5%88%92/">紫升翻译计划</a></p></blockquote>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/b6214c116672/</id>
    <link href="https://blog.zisheng.pro/posts/b6214c116672/"/>
    <published>2023-03-19T07:13:00.000Z</published>
    <summary>map 是 Go 中的一个内置类型，用于存储键值对。</summary>
    <title>Go 语言系列教程のMap</title>
    <updated>2026-09-18T08:15:28.629Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="紫升翻译计划" scheme="https://blog.zisheng.pro/categories/%E7%B4%AB%E5%8D%87%E7%BF%BB%E8%AF%91%E8%AE%A1%E5%88%92/"/>
    <category term="Go" scheme="https://blog.zisheng.pro/categories/Go/"/>
    <category term="Go" scheme="https://blog.zisheng.pro/tags/Go/"/>
    <category term="Go 教程" scheme="https://blog.zisheng.pro/tags/Go-%E6%95%99%E7%A8%8B/"/>
    <category term="紫升翻译计划" scheme="https://blog.zisheng.pro/tags/%E7%B4%AB%E5%8D%87%E7%BF%BB%E8%AF%91%E8%AE%A1%E5%88%92/"/>
    <category term="Go 参数可变的函数" scheme="https://blog.zisheng.pro/tags/Go-%E5%8F%82%E6%95%B0%E5%8F%AF%E5%8F%98%E7%9A%84%E5%87%BD%E6%95%B0/"/>
    <content>
      <![CDATA[<h2 id="什么是参数可变的函数？"><a href="#什么是参数可变的函数？" class="headerlink" title="什么是参数可变的函数？"></a>什么是参数可变的函数？</h2><p>一般来说，函数只接受固定数量的参数。参数可变的函数是一个接受可变参数的函数。如果一个函数定义的最后一个参数前面有省略号 <strong>…</strong>，那么该函数可以接受该参数的任何数量的参数。</p><p><strong>只有一个函数的最后一个参数可以是可变的。我们将在本教程的下一节中了解为什么是这样的情况。</strong></p><h2 id="语法"><a href="#语法" class="headerlink" title="语法"></a>语法</h2><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">hello</span><span class="params">(a <span class="type">int</span>, b ...<span class="type">int</span>)</span></span> &#123;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在上面的函数中，参数 <code>b</code> 是可变的，因为它的前缀是省略号，它可以接受任何数量的参数。这个函数可以通过使用语法来调用。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">hello(<span class="number">1</span>, <span class="number">2</span>) <span class="comment">// 将一个参数 `2` 传递给 b</span></span><br><span class="line">hello(<span class="number">5</span>, <span class="number">6</span>, <span class="number">7</span>, <span class="number">8</span>, <span class="number">9</span>) <span class="comment">// 将参数 6、7、8 和 9 传递给 b</span></span><br></pre></td></tr></table></figure><p>在上面的代码中，我们调用 <code>hello</code>，参数 <code>b</code> 是一个参数 <code>2</code>。1 中，我们调用 <code>hello</code>，参数 <code>b</code> 是一个参数 <code>2</code>，我们在下一行向 <code>b</code> 传递四个参数 <code>6, 7, 8, 9</code>。</p><p>也可以向一个变量函数传递零参数。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">hello(<span class="number">1</span>)</span><br></pre></td></tr></table></figure><p>在上面的代码中，我们调用 <code>hello</code> 时，<code>b</code> 的参数为零。这是很好的。</p><p>现在我想你已经明白了为什么变量参数应该只在最后一个。</p><p>让我们试着让 <code>hello</code> 函数的第一个参数变成变量。</p><p>语法将是这样的</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">hello</span><span class="params">(b ...<span class="type">int</span>, a <span class="type">int</span>)</span></span> &#123;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在上面的函数中，不可能向参数 <code>a</code> 传递参数，因为无论我们传递什么参数，都会被分配给第一个参数 <code>b</code>，因为它是变元的。因此，变量参数只能出现在函数定义的最后。上述函数将无法编译，错误为 <code>syntax error: cannot use ... with non-final parameter b</code>。</p><h2 id="例子和了解变量函数的工作原理"><a href="#例子和了解变量函数的工作原理" class="headerlink" title="例子和了解变量函数的工作原理"></a>例子和了解变量函数的工作原理</h2><p>让我们来创建我们自己的变量函数。我们将编写一个简单的程序来查找输入的整数列表中是否存在一个整数。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;fmt&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">find</span><span class="params">(num <span class="type">int</span>, nums ...<span class="type">int</span>)</span></span> &#123;</span><br><span class="line">    fmt.Printf(<span class="string">&quot;type of nums is %T\n&quot;</span>, nums)</span><br><span class="line">    found := <span class="literal">false</span></span><br><span class="line">    <span class="keyword">for</span> i, v := <span class="keyword">range</span> nums &#123;</span><br><span class="line">        <span class="keyword">if</span> v == num &#123;</span><br><span class="line">            fmt.Println(num, <span class="string">&quot;found at index&quot;</span>, i, <span class="string">&quot;in&quot;</span>, nums)</span><br><span class="line">            found = <span class="literal">true</span></span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">if</span> !found &#123;</span><br><span class="line">        fmt.Println(num, <span class="string">&quot;not found in &quot;</span>, nums)</span><br><span class="line">    &#125;</span><br><span class="line">    fmt.Printf(<span class="string">&quot;\n&quot;</span>)</span><br><span class="line">&#125;</span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    find(<span class="number">89</span>, <span class="number">89</span>, <span class="number">90</span>, <span class="number">95</span>)</span><br><span class="line">    find(<span class="number">45</span>, <span class="number">56</span>, <span class="number">67</span>, <span class="number">45</span>, <span class="number">90</span>, <span class="number">109</span>)</span><br><span class="line">    find(<span class="number">78</span>, <span class="number">38</span>, <span class="number">56</span>, <span class="number">98</span>)</span><br><span class="line">    find(<span class="number">87</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/7occymiS6s">Run in playground</a></p><p>在上面的程序中，<code>func find(num int, nums ...int)</code> 接受参数 <code>nums</code> 的数量不定。在函数 <em>find</em> 中，<code>nums</code> 的类型是 <code>[]int</code>，即一个整数切片。</p><p>变量函数的工作方式是将变量参数的数量转换为变量参数的类型的切片。例如，在上面程序的中，<code>find</code> 函数的变量参数数是 89、90、95。查找函数期望一个变量 <code>int</code> 参数。因此，这三个参数将被编译器转换为 <code>int</code> 类型的切片 <code>[]int{89, 90, 95}</code>，然后它将被传递给 <code>find</code> 函数。</p><p><code>for</code> 的循环是在 <code>f</code> 的基础上进行的。<code>for</code> 循环在 <code>nums</code> 切片上运行，如果 <code>num</code> 存在于切片中，则打印出它的位置。如果没有，则打印出没有找到这个数字。</p><p>上述程序的输出。</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">type</span> of nums is []int</span><br><span class="line">89 found at index 0 <span class="keyword">in</span> [89 90 95]</span><br><span class="line"></span><br><span class="line"><span class="built_in">type</span> of nums is []int</span><br><span class="line">45 found at index 2 <span class="keyword">in</span> [56 67 45 90 109]</span><br><span class="line"></span><br><span class="line"><span class="built_in">type</span> of nums is []int</span><br><span class="line">78 not found <span class="keyword">in</span>  [38 56 98]</span><br><span class="line"></span><br><span class="line"><span class="built_in">type</span> of nums is []int</span><br><span class="line">87 not found <span class="keyword">in</span>  []</span><br></pre></td></tr></table></figure><p>在上述程序中，<code>find</code> 函数的调用只有一个参数。我们没有向变量 <code>nums ...int</code> 参数传递任何参数。正如前面所讨论的，这是完全合法的，在这种情况下，<code>nums</code> 将是一个长度和容量为 0 的 <code>nil</code> 切片。</p><h2 id="切片参数-vs-可变参数"><a href="#切片参数-vs-可变参数" class="headerlink" title="切片参数 vs 可变参数"></a>切片参数 vs 可变参数</h2><p>我们现在肯定应该有一个问题在你的脑海中徘徊。在上一节中，我们了解到函数的变量参数实际上是转换了一个切片。那么，既然我们可以用切片实现同样的功能，为什么还要用变量函数呢？我在下面用切片重写了上面的程序。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;fmt&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">find</span><span class="params">(num <span class="type">int</span>, nums []<span class="type">int</span>)</span></span> &#123;</span><br><span class="line">    fmt.Printf(<span class="string">&quot;type of nums is %T\n&quot;</span>, nums)</span><br><span class="line">    found := <span class="literal">false</span></span><br><span class="line">    <span class="keyword">for</span> i, v := <span class="keyword">range</span> nums &#123;</span><br><span class="line">        <span class="keyword">if</span> v == num &#123;</span><br><span class="line">            fmt.Println(num, <span class="string">&quot;found at index&quot;</span>, i, <span class="string">&quot;in&quot;</span>, nums)</span><br><span class="line">            found = <span class="literal">true</span></span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">if</span> !found &#123;</span><br><span class="line">        fmt.Println(num, <span class="string">&quot;not found in &quot;</span>, nums)</span><br><span class="line">    &#125;</span><br><span class="line">    fmt.Printf(<span class="string">&quot;\n&quot;</span>)</span><br><span class="line">&#125;</span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    find(<span class="number">89</span>, []<span class="type">int</span>&#123;<span class="number">89</span>, <span class="number">90</span>, <span class="number">95</span>&#125;)</span><br><span class="line">    find(<span class="number">45</span>, []<span class="type">int</span>&#123;<span class="number">56</span>, <span class="number">67</span>, <span class="number">45</span>, <span class="number">90</span>, <span class="number">109</span>&#125;)</span><br><span class="line">    find(<span class="number">78</span>, []<span class="type">int</span>&#123;<span class="number">38</span>, <span class="number">56</span>, <span class="number">98</span>&#125;)</span><br><span class="line">    find(<span class="number">87</span>, []<span class="type">int</span>&#123;&#125;)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/rG-XRL3yycJ">Run in playground</a></p><p>以下是使用可变参数而不是切片的优点。</p><ol><li>不需要在每次函数调用时创建一个片断。如果你看一下上面的程序，我们在每次函数调用时创建了新的分片。当使用变量函数时，可以避免这种额外的片断创建。</li><li>在上述程序中，我们创建了一个空的切片，以满足 <code>find</code> 函数的签名。在参数可变函数的情况下，这完全没有必要。当使用变量函数时，这一行可以直接用 <code>find(87)</code>。</li><li>我个人认为，有变量函数的程序比有切片的程序更具可读性 :)</li></ol><h2 id="Append-是一个变量函数"><a href="#Append-是一个变量函数" class="headerlink" title="Append 是一个变量函数"></a>Append 是一个变量函数</h2><p>你有没有想过，标准库中的 append 函数是如何将数值追加到一个切片中的，它可以接受任何数量的参数。这是因为它是一个变量函数。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">append</span><span class="params">(slice []Type, elems ...Type)</span></span> []Type</span><br></pre></td></tr></table></figure><p>以上是 <code>append</code> 函数的定义。在这个定义中，<code>elems</code> 是一个变量参数。因此 append 可以接受不同数量的参数。</p><h2 id="将一个切片传递给一个参数可变的函数"><a href="#将一个切片传递给一个参数可变的函数" class="headerlink" title="将一个切片传递给一个参数可变的函数"></a>将一个切片传递给一个参数可变的函数</h2><p>让我们把一个切片传递给一个变量函数，并从下面的例子中找出会发生什么。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;fmt&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">find</span><span class="params">(num <span class="type">int</span>, nums ...<span class="type">int</span>)</span></span> &#123;</span><br><span class="line">    fmt.Printf(<span class="string">&quot;type of nums is %T\n&quot;</span>, nums)</span><br><span class="line">    found := <span class="literal">false</span></span><br><span class="line">    <span class="keyword">for</span> i, v := <span class="keyword">range</span> nums &#123;</span><br><span class="line">        <span class="keyword">if</span> v == num &#123;</span><br><span class="line">            fmt.Println(num, <span class="string">&quot;found at index&quot;</span>, i, <span class="string">&quot;in&quot;</span>, nums)</span><br><span class="line">            found = <span class="literal">true</span></span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">if</span> !found &#123;</span><br><span class="line">        fmt.Println(num, <span class="string">&quot;not found in &quot;</span>, nums)</span><br><span class="line">    &#125;</span><br><span class="line">    fmt.Printf(<span class="string">&quot;\n&quot;</span>)</span><br><span class="line">&#125;</span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    nums := []<span class="type">int</span>&#123;<span class="number">89</span>, <span class="number">90</span>, <span class="number">95</span>&#125;</span><br><span class="line">    find(<span class="number">89</span>, nums)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/A-DNilpH2L">Run in playground</a></p><p>我们将一个切片传递给一个期望有可变数量参数的函数。</p><p>这是不可行的。上述程序将失败，编译错误为 <code>./prog.go:23:10: cannot use nums (type []int) as type int in argument to find</code>。</p><p>为什么会出现这种情况呢？嗯，这是很直接的问题。下面提供了 <code>find</code> 函数的签名。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">find</span><span class="params">(num <span class="type">int</span>, nums ...<span class="type">int</span>)</span></span></span><br></pre></td></tr></table></figure><p>根据变量函数的定义，<code>nums ...int</code>意味着它将接受可变数量的<code>int</code>类型的参数。</p><p>在上面程序中，<code>nums</code> 是 <code>[]int</code> 切片，被传递给 <code>find</code> 函数，该函数期待一个变量 <code>int</code> 参数。正如我们已经讨论过的，这些变量参数将被转换为 <code>int</code> 类型的切片，因为 <code>find</code> 期待变量 <code>int</code> 参数。在这种情况下，<code>nums</code> 已经是一个 <code>[]int</code> 分片，编译器试图创建一个新的 <code>[]int</code>，也就是说，编译器试图做到:</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">find(<span class="number">89</span>, []<span class="type">int</span>&#123;nums&#125;)</span><br></pre></td></tr></table></figure><p>这将会失败，因为 <code>nums</code> 是一个 <code>[]int</code> 而不是一个 <code>int</code>。</p><p>那么有没有一种方法可以将一个切片传递给一个变量函数呢？答案是肯定的。</p><p>有一个语法糖可以用来传递一个分片到变量函数中。你必须用省略号 <code>...</code> 作为切片的后缀，如果这样做了，切片就会直接传递给函数，而不需要创建新的切片。</p><p>在上面的程序中，如果你把 <code>find(89, nums)</code> 替换为 <code>find(89, nums...)</code>，程序将被编译并打印出以下输出。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">type of nums is []int</span><br><span class="line">89 found at index 0 in [89 90 95]</span><br></pre></td></tr></table></figure><p>以下是完整的方案供您参考。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;fmt&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">find</span><span class="params">(num <span class="type">int</span>, nums ...<span class="type">int</span>)</span></span> &#123;</span><br><span class="line">    fmt.Printf(<span class="string">&quot;type of nums is %T\n&quot;</span>, nums)</span><br><span class="line">    found := <span class="literal">false</span></span><br><span class="line">    <span class="keyword">for</span> i, v := <span class="keyword">range</span> nums &#123;</span><br><span class="line">        <span class="keyword">if</span> v == num &#123;</span><br><span class="line">            fmt.Println(num, <span class="string">&quot;found at index&quot;</span>, i, <span class="string">&quot;in&quot;</span>, nums)</span><br><span class="line">            found = <span class="literal">true</span></span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">if</span> !found &#123;</span><br><span class="line">        fmt.Println(num, <span class="string">&quot;not found in &quot;</span>, nums)</span><br><span class="line">    &#125;</span><br><span class="line">    fmt.Printf(<span class="string">&quot;\n&quot;</span>)</span><br><span class="line">&#125;</span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    nums := []<span class="type">int</span>&#123;<span class="number">89</span>, <span class="number">90</span>, <span class="number">95</span>&#125;</span><br><span class="line">    find(<span class="number">89</span>, nums...)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/IvzwhzhFsT">Run in playground</a></p><h3 id="明白了吗？"><a href="#明白了吗？" class="headerlink" title="明白了吗？"></a>明白了吗？</h3><p>只要确保你知道当你在一个参数可变的函数内修改一个切片时你在做什么。</p><p>让我们看一个简单的例子。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;fmt&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">change</span><span class="params">(s ...<span class="type">string</span>)</span></span> &#123;</span><br><span class="line">    s[<span class="number">0</span>] = <span class="string">&quot;Go&quot;</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    welcome := []<span class="type">string</span>&#123;<span class="string">&quot;hello&quot;</span>, <span class="string">&quot;world&quot;</span>&#125;</span><br><span class="line">    change(welcome...)</span><br><span class="line">    fmt.Println(welcome)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/R0GsuW7rdd">Run in playground</a></p><p>你认为上述程序的输出会是什么？如果你认为会是 <code>[Go world]</code>，恭喜你！你已经理解了参数可变的函数和切片。如果你弄错了，也没什么大不了的，让我解释一下我们是如何得到这个输出的。</p><p>在上面程序中，我们使用了语法糖 <code>...</code>，并将切片作为变量参数传递给<code>change</code> 函数。</p><p>正如我们已经讨论过的，如果使用 <code>...</code>，<code>welcome</code> 切片本身将作为一个参数被传递，而不会创建一个新的切片。因此 <code>welcome</code> 将作为参数传递给 <code>change</code> 函数。</p><p>在 <code>change</code> 函数中，分片的第一个元素被改变为 <code>Go</code>。因此，这个程序输出</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">[Go world]</span><br></pre></td></tr></table></figure><p>这里还有一个了解参数可变的函数的程序。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;fmt&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">change</span><span class="params">(s ...<span class="type">string</span>)</span></span> &#123;</span><br><span class="line">    s[<span class="number">0</span>] = <span class="string">&quot;Go&quot;</span></span><br><span class="line">    s = <span class="built_in">append</span>(s, <span class="string">&quot;playground&quot;</span>)</span><br><span class="line">    fmt.Println(s)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    welcome := []<span class="type">string</span>&#123;<span class="string">&quot;hello&quot;</span>, <span class="string">&quot;world&quot;</span>&#125;</span><br><span class="line">    change(welcome...)</span><br><span class="line">    fmt.Println(welcome)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/WdbFIkdLoe">Run in playground</a></p><p>我想把它作为一个练习，让你弄清楚上述程序是如何工作的</p><p>参数可变的函数就介绍到这里。谢谢你的阅读。请留下您宝贵的反馈和意见。祝你有个愉快的一天。</p><blockquote><p>原文地址 <a href="https://golangbot.com/variadic-functions/">Variadic Functions</a><br>原文作者：<a href="https://golangbot.com/about/">Naveen Ramanathan</a><br>译文出自：<a href="https://blog.zisheng.pro/categories/%E6%B4%9B%E7%AB%B9%E7%BF%BB%E8%AF%91%E8%AE%A1%E5%88%92/">紫升翻译计划</a></p></blockquote>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/f4c08ab26cae/</id>
    <link href="https://blog.zisheng.pro/posts/f4c08ab26cae/"/>
    <published>2023-03-19T07:12:00.000Z</published>
    <summary>一般来说，函数只接受固定数量的参数。参数可变的函数是一个接受可变参数的函数。如果一个函数定义的最后一个参数前面有省略号 ...，那么该函数可以接受该参数的任何数量的参数。</summary>
    <title>Go 语言系列教程の参数可变的函数</title>
    <updated>2026-09-18T08:15:28.629Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="紫升翻译计划" scheme="https://blog.zisheng.pro/categories/%E7%B4%AB%E5%8D%87%E7%BF%BB%E8%AF%91%E8%AE%A1%E5%88%92/"/>
    <category term="Go" scheme="https://blog.zisheng.pro/categories/Go/"/>
    <category term="Go" scheme="https://blog.zisheng.pro/tags/Go/"/>
    <category term="Go 教程" scheme="https://blog.zisheng.pro/tags/Go-%E6%95%99%E7%A8%8B/"/>
    <category term="紫升翻译计划" scheme="https://blog.zisheng.pro/tags/%E7%B4%AB%E5%8D%87%E7%BF%BB%E8%AF%91%E8%AE%A1%E5%88%92/"/>
    <category term="Go 切片" scheme="https://blog.zisheng.pro/tags/Go-%E5%88%87%E7%89%87/"/>
    <content>
      <![CDATA[<p>切片是数组上面的一个方便、灵活和强大的包装器。切片并不拥有自己的任何数据。它们只是对现有数组的引用。</p><h2 id="创建切片"><a href="#创建切片" class="headerlink" title="创建切片"></a>创建切片</h2><p>一个具有 T 类型元素的切片用 <code>[]T</code> 表示。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;fmt&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    a := [<span class="number">5</span>]<span class="type">int</span>&#123;<span class="number">76</span>, <span class="number">77</span>, <span class="number">78</span>, <span class="number">79</span>, <span class="number">80</span>&#125;</span><br><span class="line">    <span class="keyword">var</span> b []<span class="type">int</span> = a[<span class="number">1</span>:<span class="number">4</span>] <span class="comment">// 创建一个 a[1] 到 a[3] 的切片</span></span><br><span class="line">    fmt.Println(b)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/Za6w5eubBB">Run in playground</a></p><p>语法<code>a[start:end]</code>从数组 <code>a</code> 中创建一个切片，从索引 <code>start</code> 开始到索引 <code>end</code>-1<code>。所以在上述程序中，</code>a[1:4]<code>创建了一个从索引 1 到 3 的数组</code>a<code>的切片。因此切片</code>b<code>的值是</code>[77 78 79]&#96;。</p><p>让我们来看看创建切片的另一种方法。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;fmt&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    c := []<span class="type">int</span>&#123;<span class="number">6</span>, <span class="number">7</span>, <span class="number">8</span>&#125; <span class="comment">// 创建一个数组并返回切片的索引</span></span><br><span class="line">    fmt.Println(c)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/_Z97MgXavA">Run in playground</a></p><p>在上述程序中，<code>c := []int{6, 7, 8}</code> 创建了一个包含 3 个整数的数组，并返回一个存储在 c 中的切片引用。</p><h3 id="修改一个切片"><a href="#修改一个切片" class="headerlink" title="修改一个切片"></a>修改一个切片</h3><p>一个切片并不拥有自己的任何数据。它只是底层数组的一个代表。对切片所做的任何修改都会反映在底层数组中。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;fmt&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    darr := [...]<span class="type">int</span>&#123;<span class="number">57</span>, <span class="number">89</span>, <span class="number">90</span>, <span class="number">82</span>, <span class="number">100</span>, <span class="number">78</span>, <span class="number">67</span>, <span class="number">69</span>, <span class="number">59</span>&#125;</span><br><span class="line">    dslice := darr[<span class="number">2</span>:<span class="number">5</span>]</span><br><span class="line">    fmt.Println(<span class="string">&quot;array before&quot;</span>,darr)</span><br><span class="line">    <span class="keyword">for</span> i := <span class="keyword">range</span> dslice &#123;</span><br><span class="line">        dslice[i]++</span><br><span class="line">    &#125;</span><br><span class="line">    fmt.Println(<span class="string">&quot;array after&quot;</span>,darr)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/6FinudNf1k">Run in playground</a></p><p>在上述程序中，我们从数组的索引 2、3、4 创建 <code>dslice</code>。for 循环将这些索引的值增加 1。当我们在 for 循环后打印数组时，我们可以看到切片的变化已经反映在数组中。该程序的输出是</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">array before [57 89 90 82 100 78 67 69 59]</span><br><span class="line">array after [57 89 91 83 101 78 67 69 59]</span><br></pre></td></tr></table></figure><p>当一些切片共享同一个底层数组时，每个切片的变化都会反映在数组中。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;fmt&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    numa := [<span class="number">3</span>]<span class="type">int</span>&#123;<span class="number">78</span>, <span class="number">79</span> ,<span class="number">80</span>&#125;</span><br><span class="line">    nums1 := numa[:] <span class="comment">// 创建一个包含数组中所有元素的片断</span></span><br><span class="line">    nums2 := numa[:]</span><br><span class="line">    fmt.Println(<span class="string">&quot;array before change 1&quot;</span>,numa)</span><br><span class="line">    nums1[<span class="number">0</span>] = <span class="number">100</span></span><br><span class="line">    fmt.Println(<span class="string">&quot;array after modification to slice nums1&quot;</span>, numa)</span><br><span class="line">    nums2[<span class="number">1</span>] = <span class="number">101</span></span><br><span class="line">    fmt.Println(<span class="string">&quot;array after modification to slice nums2&quot;</span>, numa)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/mdNi4cs854">Run in playground</a></p><p>在 <code>numa[:]</code> 中，缺少开始和结束值。start 和 end 的默认值分别为 <code>0</code> 和 <code>len(numa)</code>。两个切片 <code>nums1</code> 和 <code>nums2</code> 共享同一个数组。该程序的输出是</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">array before change 1 [78 79 80]</span><br><span class="line">array after modification to slice nums1 [100 79 80]</span><br><span class="line">array after modification to slice nums2 [100 101 80]</span><br></pre></td></tr></table></figure><p>从输出结果来看，很明显，当切片共享同一个数组时。对切片所做的修改会反映在数组中。</p><h3 id="切片的长度和容量"><a href="#切片的长度和容量" class="headerlink" title="切片的长度和容量"></a>切片的长度和容量</h3><p>切片的长度是切片中元素的数量。<strong>切片的容量是指从创建分片的索引开始的底层数组中的元素数</strong>。</p><p>让我们写一些代码来更好地理解这一点。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;fmt&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    fruitarray := [...]<span class="type">string</span>&#123;<span class="string">&quot;apple&quot;</span>, <span class="string">&quot;orange&quot;</span>, <span class="string">&quot;grape&quot;</span>, <span class="string">&quot;mango&quot;</span>, <span class="string">&quot;water melon&quot;</span>, <span class="string">&quot;pine apple&quot;</span>, <span class="string">&quot;chikoo&quot;</span>&#125;</span><br><span class="line">    fruitslice := fruitarray[<span class="number">1</span>:<span class="number">3</span>]</span><br><span class="line">    fmt.Printf(<span class="string">&quot;length of slice %d capacity %d&quot;</span>, <span class="built_in">len</span>(fruitslice), <span class="built_in">cap</span>(fruitslice)) <span class="comment">// 切的长度为 2，容量为 6</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/a1WOcdv827">Run in playground</a></p><p>在上面的程序中，<code>fruitslice</code> 是由 <code>fruitarray</code> 的索引 1 和 2 创建的。因此，<code>fruitslice</code> 的长度是 2。</p><p><code>fruitarray</code> 的长度是 7。<code>fruiteslice</code> 是从 <code>fruitarray</code> 的索引 1 创建的。因此，<code>fruitslice</code> 的容量是 <code>fruitarray&#39; 中从索引 1 开始的元素数量，即从</code>orange<code> 开始，这个值是</code>6<code>。因此，</code>fruitslice&#96; 的容量是 6。<a href="https://play.golang.org/p/a1WOcdv827">程序</a> 打印出 <strong>length of slice 2 capacity 6</strong>。</p><p>一个切片可以被重新切割到它的容量。任何超过这个容量的部分都会导致程序出现运行时错误。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;fmt&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    fruitarray := [...]<span class="type">string</span>&#123;<span class="string">&quot;apple&quot;</span>, <span class="string">&quot;orange&quot;</span>, <span class="string">&quot;grape&quot;</span>, <span class="string">&quot;mango&quot;</span>, <span class="string">&quot;water melon&quot;</span>, <span class="string">&quot;pine apple&quot;</span>, <span class="string">&quot;chikoo&quot;</span>&#125;</span><br><span class="line">    fruitslice := fruitarray[<span class="number">1</span>:<span class="number">3</span>]</span><br><span class="line">    fmt.Printf(<span class="string">&quot;length of slice %d capacity %d\n&quot;</span>, <span class="built_in">len</span>(fruitslice), <span class="built_in">cap</span>(fruitslice)) <span class="comment">// 长度为 2，容量为 6</span></span><br><span class="line">    fruitslice = fruitslice[:<span class="built_in">cap</span>(fruitslice)] <span class="comment">// 重新分割切片，直到其容量。</span></span><br><span class="line">    fmt.Println(<span class="string">&quot;After re-slicing length is&quot;</span>,<span class="built_in">len</span>(fruitslice), <span class="string">&quot;and capacity is&quot;</span>,<span class="built_in">cap</span>(fruitslice))</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/GcNzOOGicu">Run in playground</a></p><p>在上述程序中，<code>fruitslice</code> 被重新f分割成到其容量。上述程序的输出。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">length of slice 2 capacity 6</span><br><span class="line">After re-slicing length is 6 and capacity is 6</span><br></pre></td></tr></table></figure><h3 id="使用-make-创建切片"><a href="#使用-make-创建切片" class="headerlink" title="使用 make 创建切片"></a>使用 make 创建切片</h3><p><em>func make([]T, len, cap) []T</em> 可以通过传递类型、长度和容量来创建一个切片。容量参数是可选的，默认为长度。make 函数创建一个数组，并返回对它的切片的引用。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;fmt&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    i := <span class="built_in">make</span>([]<span class="type">int</span>, <span class="number">5</span>, <span class="number">5</span>)</span><br><span class="line">    fmt.Println(i)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/M4OqxzerxN">Run in playground</a></p><p>当使用 make 创建一个切片时，这些值默认为零。上述程序将输出 <code>[0 0 0 0]</code>。</p><blockquote><p>原文地址 <a href="https://golangbot.com/arrays-and-slices/">Arrays and Slices</a><br>原文作者：<a href="https://golangbot.com/about/">Naveen Ramanathan</a><br>译文出自：<a href="https://blog.zisheng.pro/categories/%E6%B4%9B%E7%AB%B9%E7%BF%BB%E8%AF%91%E8%AE%A1%E5%88%92/">紫升翻译计划</a></p></blockquote>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/26829e2dd29f/</id>
    <link href="https://blog.zisheng.pro/posts/26829e2dd29f/"/>
    <published>2023-03-19T07:11:30.000Z</published>
    <summary>切片是数组上面的一个方便、灵活和强大的包装器。切片并不拥有自己的任何数据。它们只是对现有数组的引用。</summary>
    <title>Go 语言系列教程の切片</title>
    <updated>2026-09-18T08:15:28.629Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="紫升翻译计划" scheme="https://blog.zisheng.pro/categories/%E7%B4%AB%E5%8D%87%E7%BF%BB%E8%AF%91%E8%AE%A1%E5%88%92/"/>
    <category term="Go" scheme="https://blog.zisheng.pro/categories/Go/"/>
    <category term="Go" scheme="https://blog.zisheng.pro/tags/Go/"/>
    <category term="Go 教程" scheme="https://blog.zisheng.pro/tags/Go-%E6%95%99%E7%A8%8B/"/>
    <category term="紫升翻译计划" scheme="https://blog.zisheng.pro/tags/%E7%B4%AB%E5%8D%87%E7%BF%BB%E8%AF%91%E8%AE%A1%E5%88%92/"/>
    <category term="Go 数组" scheme="https://blog.zisheng.pro/tags/Go-%E6%95%B0%E7%BB%84/"/>
    <content>
      <![CDATA[<p>数组是属于同一类型的元素的集合。 例如，整数 5、8、9、79、76 的集合构成一个数组。 Go 中不允许混合不同类型的值，例如同时包含字符串和整数的数组。</p><h2 id="数组声明"><a href="#数组声明" class="headerlink" title="数组声明"></a>数组声明</h2><p>数组属于类型 <code>[n]T</code>。 <code>n</code> 表示数组中元素的数量，<code>T</code> 表示每个元素的类型。 元素的数量 <code>n</code> 也是类型的一部分（我们将在稍后更详细地讨论这一点。）</p><p>声明数组有不同的方法。 让我们一一看看它们。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;fmt&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    <span class="keyword">var</span> a [<span class="number">3</span>]<span class="type">int</span> <span class="comment">// 长度为 3 的数组</span></span><br><span class="line">    fmt.Println(a)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/Zvgh82u0ej">Run in playground</a></p><p><code>var a[3]int</code> 声明了一个长度为 3 的整数数组。数组中的所有元素都会自动分配数组类型的零值。 在这种情况下，<code>a</code> 是一个整数数组，因此 <code>a</code> 的所有元素都分配给 <code>0</code>，即 int 的零值。 运行上面的程序会打印</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">[0 0 0]</span><br></pre></td></tr></table></figure><p>数组的索引从 <code>0</code> 开始，到 <code>length - 1</code> 结束。 让我们为上面的数组分配一些值。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;fmt&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    <span class="keyword">var</span> a [<span class="number">3</span>]<span class="type">int</span> <span class="comment">// 长度为 3 的 int 数组</span></span><br><span class="line">    a[<span class="number">0</span>] = <span class="number">12</span> <span class="comment">// 数组索引从 0 开始</span></span><br><span class="line">    a[<span class="number">1</span>] = <span class="number">78</span></span><br><span class="line">    a[<span class="number">2</span>] = <span class="number">50</span></span><br><span class="line">    fmt.Println(a)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/WF0Uj8sv39">Run in playground</a></p><p>a[0] 为数组的第一个元素赋值。 该程序将打印</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">[12 78 50]</span><br></pre></td></tr></table></figure><p>让我们使用 <strong>短声明</strong> 来创建相同的数组。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;fmt&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    a := [<span class="number">3</span>]<span class="type">int</span>&#123;<span class="number">12</span>, <span class="number">78</span>, <span class="number">50</span>&#125; <span class="comment">// 创建数组的短声明</span></span><br><span class="line">    fmt.Println(a)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/NKOV04zgI6">Run in playground</a></p><p>上面的程序将打印相同的输出</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">[12 78 50]</span><br></pre></td></tr></table></figure><p>使用短声明时，不必为数组中的所有元素分配一个值。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;fmt&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    a := [<span class="number">3</span>]<span class="type">int</span>&#123;<span class="number">12</span>&#125;</span><br><span class="line">    fmt.Println(a)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/AdPH0kXRly">Run in playground</a></p><p>在上面的程序中，<code>a := [3]int{12}</code> 声明了一个长度为 3 的数组，但只提供了一个值 <code>12</code>。 其余 2 个元素自动分配为 <code>0</code>。 该程序将打印</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">[12 0 0]</span><br></pre></td></tr></table></figure><p>你甚至可以忽略声明中数组的长度，并用 <code>...</code> 替换它，让编译器自动计算长度。 代码实现如下。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;fmt&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    a := [...]<span class="type">int</span>&#123;<span class="number">12</span>, <span class="number">78</span>, <span class="number">50</span>&#125; <span class="comment">// ... 让编译器决定数组长度</span></span><br><span class="line">    fmt.Println(a)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/_fVmr6KGDh">Run in playground</a></p><p><strong>数组的大小是类型的一部分。</strong> 因此 <code>[5]int</code> 和 <code>[25]int</code> 是不同的类型。 因此，无法调整数组大小。 不要担心这个限制，因为可以使用 <code>切片</code> 来突破这个限制。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    a := [<span class="number">3</span>]<span class="type">int</span>&#123;<span class="number">5</span>, <span class="number">78</span>, <span class="number">8</span>&#125;</span><br><span class="line">    <span class="keyword">var</span> b [<span class="number">5</span>]<span class="type">int</span></span><br><span class="line">    b = a <span class="comment">// 不可能成立，因为 `[3]int` 和 `[5]int` 是不同的类型</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/kBdot3pXSB">Run in playground</a></p><p>在上面的程序中，我们试图将类型为 <code>[3]int</code> 的变量分配给类型为 <code>[5]int</code> 的变量，这是不允许的，因此编译器将打印以下错误</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">./prog.go:6:7: cannot use a (type [3]int) as type [5]int in assignment</span><br></pre></td></tr></table></figure><h2 id="数组是值类型"><a href="#数组是值类型" class="headerlink" title="数组是值类型"></a>数组是值类型</h2><p>Go 中的数组是值类型而不是引用类型。 这意味着当它们被分配给一个新变量时，原始数组的副本被分配给新变量。 如果对新变量进行了更改，它将不会反映在原始数组中。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> <span class="string">&quot;fmt&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    a := [...]<span class="type">string</span>&#123;<span class="string">&quot;USA&quot;</span>, <span class="string">&quot;China&quot;</span>, <span class="string">&quot;India&quot;</span>, <span class="string">&quot;Germany&quot;</span>, <span class="string">&quot;France&quot;</span>&#125;</span><br><span class="line">    b := a <span class="comment">// a 的副本被分配给 b</span></span><br><span class="line">    b[<span class="number">0</span>] = <span class="string">&quot;Singapore&quot;</span> <span class="comment">// b 的第一个元素被改变为 Singapore，这不会影响到 a</span></span><br><span class="line">    fmt.Println(<span class="string">&quot;a is &quot;</span>, a)</span><br><span class="line">    fmt.Println(<span class="string">&quot;b is &quot;</span>, b)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/-ncGk1mqPd">Run in playground</a></p><p>程序会打印出：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">a is [USA China India Germany France]</span><br><span class="line">b is [Singapore China India Germany France]</span><br></pre></td></tr></table></figure><p>同样，当数组作为参数传递给函数时，它们是按值传递的，原始数组不变。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> <span class="string">&quot;fmt&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">changeLocal</span><span class="params">(num [5]<span class="type">int</span>)</span></span> &#123;</span><br><span class="line">    num[<span class="number">0</span>] = <span class="number">55</span></span><br><span class="line">    fmt.Println(<span class="string">&quot;inside function &quot;</span>, num)</span><br><span class="line"></span><br><span class="line">&#125;</span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    num := [...]<span class="type">int</span>&#123;<span class="number">5</span>, <span class="number">6</span>, <span class="number">7</span>, <span class="number">8</span>, <span class="number">8</span>&#125;</span><br><span class="line">    fmt.Println(<span class="string">&quot;before passing to function &quot;</span>, num)</span><br><span class="line">    changeLocal(num) <span class="comment">// num 是按值传递的，因此不会因为函数调用而改变</span></span><br><span class="line">    fmt.Println(<span class="string">&quot;after passing to function &quot;</span>, num)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/e3U75Q8eUZ">Run in playground</a></p><p>该程序将打印：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">before passing to function  [5 6 7 8 8]</span><br><span class="line">inside function  [55 6 7 8 8]</span><br><span class="line">after passing to function  [5 6 7 8 8]</span><br></pre></td></tr></table></figure><h2 id="数组长度"><a href="#数组长度" class="headerlink" title="数组长度"></a>数组长度</h2><p>数组的长度是通过将数组作为参数传递给 <code>len</code> 函数来得到的。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> <span class="string">&quot;fmt&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    a := [...]<span class="type">float64</span>&#123;<span class="number">67.7</span>, <span class="number">89.8</span>, <span class="number">21</span>, <span class="number">78</span>&#125;</span><br><span class="line">    fmt.Println(<span class="string">&quot;length of a is&quot;</span>,<span class="built_in">len</span>(a))</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/UrIeNlS0RN">Run in playground</a></p><p>上面的程序会打印出</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">length of a is 4</span><br></pre></td></tr></table></figure><h3 id="使用-range-迭代数组"><a href="#使用-range-迭代数组" class="headerlink" title="使用 range 迭代数组"></a>使用 range 迭代数组</h3><p><code>for</code> 循环可用于迭代数组的元素。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> <span class="string">&quot;fmt&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    a := [...]<span class="type">float64</span>&#123;<span class="number">67.7</span>, <span class="number">89.8</span>, <span class="number">21</span>, <span class="number">78</span>&#125;</span><br><span class="line">    <span class="keyword">for</span> i := <span class="number">0</span>; i &lt; <span class="built_in">len</span>(a); i++ &#123; <span class="comment">// 从 0 循环到数组的长度</span></span><br><span class="line">        fmt.Printf(<span class="string">&quot;%d th element of a is %.2f\n&quot;</span>, i, a[i])</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/80ejSTACO6">Run in playground</a></p><p>上面的程序使用一个 <code>for</code> 循环来迭代数组的元素，从索引 <code>0</code> 到 <code>length of the array - 1</code>。 这个程序可以工作并且会打印</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">0 th element of a is 67.70</span><br><span class="line">1 th element of a is 89.80</span><br><span class="line">2 th element of a is 21.00</span><br><span class="line">3 th element of a is 78.00</span><br></pre></td></tr></table></figure><p>Go 通过使用 <code>for</code> 循环的 <strong>range</strong> 形式提供了一种更好、更简洁的方法来迭代数组。 <code>range</code> 返回索引和该索引处的值。 让我们使用 <code>range</code> 重写上面的代码。 我们还将找到数组所有元素的总和。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> <span class="string">&quot;fmt&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    a := [...]<span class="type">float64</span>&#123;<span class="number">67.7</span>, <span class="number">89.8</span>, <span class="number">21</span>, <span class="number">78</span>&#125;</span><br><span class="line">    sum := <span class="type">float64</span>(<span class="number">0</span>)</span><br><span class="line">    <span class="keyword">for</span> i, v := <span class="keyword">range</span> a &#123; <span class="comment">// range 同时返回索引和值</span></span><br><span class="line">        fmt.Printf(<span class="string">&quot;%d the element of a is %.2f\n&quot;</span>, i, v)</span><br><span class="line">        sum += v</span><br><span class="line">    &#125;</span><br><span class="line">    fmt.Println(<span class="string">&quot;\nsum of all elements of a&quot;</span>,sum)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/Ji6FRon36m">Run in playground</a></p><p>我们打印值并计算数组 <code>a</code> 的所有元素的总和。 程序的<strong>输出</strong>是，</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">0 the element of a is 67.70</span><br><span class="line">1 the element of a is 89.80</span><br><span class="line">2 the element of a is 21.00</span><br><span class="line">3 the element of a is 78.00</span><br><span class="line"></span><br><span class="line">sum of all elements of a 256.5</span><br></pre></td></tr></table></figure><p>如果你只想要该值而想要忽略索引，您可以通过用 <code>_</code> 空白标识符替换索引来实现。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">for</span> _, v := <span class="keyword">range</span> a &#123; <span class="comment">// 忽略索引</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>上面的 for 循环忽略了索引。 同样，值也可以忽略。</p><h3 id="多维数组"><a href="#多维数组" class="headerlink" title="多维数组"></a>多维数组</h3><p>到目前为止我们创建的数组都是一维的。我们可以创建多维数组。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;fmt&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">printarray</span><span class="params">(a [3][2]<span class="type">string</span>)</span></span> &#123;</span><br><span class="line">    <span class="keyword">for</span> _, v1 := <span class="keyword">range</span> a &#123;</span><br><span class="line">        <span class="keyword">for</span> _, v2 := <span class="keyword">range</span> v1 &#123;</span><br><span class="line">            fmt.Printf(<span class="string">&quot;%s &quot;</span>, v2)</span><br><span class="line">        &#125;</span><br><span class="line">        fmt.Printf(<span class="string">&quot;\n&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="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    a := [<span class="number">3</span>][<span class="number">2</span>]<span class="type">string</span>&#123;</span><br><span class="line">        &#123;<span class="string">&quot;lion&quot;</span>, <span class="string">&quot;tiger&quot;</span>&#125;,</span><br><span class="line">        &#123;<span class="string">&quot;cat&quot;</span>, <span class="string">&quot;dog&quot;</span>&#125;,</span><br><span class="line">        &#123;<span class="string">&quot;pigeon&quot;</span>, <span class="string">&quot;peacock&quot;</span>&#125;, <span class="comment">// 这个逗号是必须的。 如果省略这个逗号，编译器会报错</span></span><br><span class="line">    &#125;</span><br><span class="line">    printarray(a)</span><br><span class="line">    <span class="keyword">var</span> b [<span class="number">3</span>][<span class="number">2</span>]<span class="type">string</span></span><br><span class="line">    b[<span class="number">0</span>][<span class="number">0</span>] = <span class="string">&quot;apple&quot;</span></span><br><span class="line">    b[<span class="number">0</span>][<span class="number">1</span>] = <span class="string">&quot;samsung&quot;</span></span><br><span class="line">    b[<span class="number">1</span>][<span class="number">0</span>] = <span class="string">&quot;microsoft&quot;</span></span><br><span class="line">    b[<span class="number">1</span>][<span class="number">1</span>] = <span class="string">&quot;google&quot;</span></span><br><span class="line">    b[<span class="number">2</span>][<span class="number">0</span>] = <span class="string">&quot;AT&amp;T&quot;</span></span><br><span class="line">    b[<span class="number">2</span>][<span class="number">1</span>] = <span class="string">&quot;T-Mobile&quot;</span></span><br><span class="line">    fmt.Printf(<span class="string">&quot;\n&quot;</span>)</span><br><span class="line">    printarray(b)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/InchXI4yY8">Run in playground</a></p><p>上面的程序会打印：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">lion tiger</span><br><span class="line">cat dog</span><br><span class="line">pigeon peacock</span><br><span class="line"></span><br><span class="line">apple samsung</span><br><span class="line">microsoft google</span><br><span class="line">AT&amp;T T-Mobile</span><br></pre></td></tr></table></figure><p>这就是数组。 尽管数组似乎足够灵活，但它们具有固定长度的限制。 无法增加数组的长度。而这正是 <strong>切片</strong> 擅长的。 事实上，在 Go 中，切片比传统数组更常见。</p><h2 id="结语"><a href="#结语" class="headerlink" title="结语"></a>结语</h2><p>关注公众号<code>紫升早茶馆</code>，一个持续分享编程知识的地方。</p><ul><li><code>点赞</code>等于学会，<code>在看</code>等于精通</li><li>最后祝大家 2021 学习进步，升职加薪</li></ul><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a9752661d5474d8f8f4ae2584c288ac9~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><blockquote><p>原文地址 <a href="https://golangbot.com/arrays-and-slices/">Arrays and Slices</a><br>原文作者：<a href="https://golangbot.com/about/">Naveen Ramanathan</a><br>译文出自：<a href="https://blog.zisheng.pro/categories/%E6%B4%9B%E7%AB%B9%E7%BF%BB%E8%AF%91%E8%AE%A1%E5%88%92/">紫升翻译计划</a></p></blockquote>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/e647c735eac5/</id>
    <link href="https://blog.zisheng.pro/posts/e647c735eac5/"/>
    <published>2023-03-19T07:11:00.000Z</published>
    <summary>数组是属于同一类型的元素的集合。 例如，整数 5、8、9、79、76 的集合构成一个数组。 Go 中不允许混合不同类型的值，例如同时包含字符串和整数的数组。</summary>
    <title>Go 语言系列教程の数组</title>
    <updated>2026-09-18T08:15:28.629Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="紫升翻译计划" scheme="https://blog.zisheng.pro/categories/%E7%B4%AB%E5%8D%87%E7%BF%BB%E8%AF%91%E8%AE%A1%E5%88%92/"/>
    <category term="Go" scheme="https://blog.zisheng.pro/categories/Go/"/>
    <category term="Go" scheme="https://blog.zisheng.pro/tags/Go/"/>
    <category term="Go 教程" scheme="https://blog.zisheng.pro/tags/Go-%E6%95%99%E7%A8%8B/"/>
    <category term="Golang Switch" scheme="https://blog.zisheng.pro/tags/Golang-Switch/"/>
    <content>
      <![CDATA[<h2 id="switch-语句是什么？"><a href="#switch-语句是什么？" class="headerlink" title="switch 语句是什么？"></a>switch 语句是什么？</h2><p><code>switch</code> 是一个条件语句，它对表达式进行求值并将其与可能的匹配列表进行比较，并执行相应的代码块。可以将其视为替换复杂的 <code>if</code> 语句的惯用方式。</p><h2 id="示例"><a href="#示例" class="headerlink" title="示例"></a>示例</h2><p><code>Talk is cheap,show you the example</code>。让我们从一个简单的示例开始，该示例将一个手指编号作为输入并输出该手指的名称。 例如，1 是拇指，2 是食指，依此类推。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;fmt&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    finger := <span class="number">4</span></span><br><span class="line">    fmt.Printf(<span class="string">&quot;Finger %d is &quot;</span>, finger)</span><br><span class="line">    <span class="keyword">switch</span> finger &#123;</span><br><span class="line">    <span class="keyword">case</span> <span class="number">1</span>:</span><br><span class="line">        fmt.Println(<span class="string">&quot;Thumb&quot;</span>)</span><br><span class="line">    <span class="keyword">case</span> <span class="number">2</span>:</span><br><span class="line">        fmt.Println(<span class="string">&quot;Index&quot;</span>)</span><br><span class="line">    <span class="keyword">case</span> <span class="number">3</span>:</span><br><span class="line">        fmt.Println(<span class="string">&quot;Middle&quot;</span>)</span><br><span class="line">    <span class="keyword">case</span> <span class="number">4</span>:</span><br><span class="line">        fmt.Println(<span class="string">&quot;Ring&quot;</span>)</span><br><span class="line">    <span class="keyword">case</span> <span class="number">5</span>:</span><br><span class="line">        fmt.Println(<span class="string">&quot;Pinky&quot;</span>)</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><a href="https://play.golang.org/p/94ktmJWlUom">Run in playground</a></p><p>在上面第 10 行的程序 <code>switch finger</code> 中，将 <code>finger</code> 的值与每个 <code>case</code> 语句进行比较。从上到下判断条件，并执行与表达式匹配的第一个分支。 在这种情况下，<code>finger</code> 的值为 <code>4</code>，因此输出：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Finger 4 is Ring</span><br></pre></td></tr></table></figure><h2 id="不允许重复的分支"><a href="#不允许重复的分支" class="headerlink" title="不允许重复的分支"></a>不允许重复的分支</h2><p>不允许重复使用具有相同常量值的分支。如果你尝试运行以下程序，则编译器将抛错 <code>./prog.go:19:7: duplicate case 4 in switch previous case at ./prog.go:17:7</code>。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;fmt&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    finger := <span class="number">4</span></span><br><span class="line">    fmt.Printf(<span class="string">&quot;Finger %d is &quot;</span>, finger)</span><br><span class="line">    <span class="keyword">switch</span> finger &#123;</span><br><span class="line">    <span class="keyword">case</span> <span class="number">1</span>:</span><br><span class="line">        fmt.Println(<span class="string">&quot;Thumb&quot;</span>)</span><br><span class="line">    <span class="keyword">case</span> <span class="number">2</span>:</span><br><span class="line">        fmt.Println(<span class="string">&quot;Index&quot;</span>)</span><br><span class="line">    <span class="keyword">case</span> <span class="number">3</span>:</span><br><span class="line">        fmt.Println(<span class="string">&quot;Middle&quot;</span>)</span><br><span class="line">    <span class="keyword">case</span> <span class="number">4</span>:</span><br><span class="line">        fmt.Println(<span class="string">&quot;Ring&quot;</span>)</span><br><span class="line">    <span class="keyword">case</span> <span class="number">4</span>: <span class="comment">// 重复的分支</span></span><br><span class="line">        fmt.Println(<span class="string">&quot;Another Ring&quot;</span>)</span><br><span class="line">    <span class="keyword">case</span> <span class="number">5</span>:</span><br><span class="line">        fmt.Println(<span class="string">&quot;Pinky&quot;</span>)</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><a href="https://play.golang.org/p/7qrmR0hdvHH">Run in playground</a></p><h2 id="默认分支"><a href="#默认分支" class="headerlink" title="默认分支"></a>默认分支</h2><p>我们只有五个手指。如果输入不正确的手指编号会怎样？这时我们需要一个默认分支。当其他情况都不匹配时，将执行默认分支。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;fmt&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    <span class="keyword">switch</span> finger := <span class="number">8</span>; finger &#123;</span><br><span class="line">    <span class="keyword">case</span> <span class="number">1</span>:</span><br><span class="line">        fmt.Println(<span class="string">&quot;Thumb&quot;</span>)</span><br><span class="line">    <span class="keyword">case</span> <span class="number">2</span>:</span><br><span class="line">        fmt.Println(<span class="string">&quot;Index&quot;</span>)</span><br><span class="line">    <span class="keyword">case</span> <span class="number">3</span>:</span><br><span class="line">        fmt.Println(<span class="string">&quot;Middle&quot;</span>)</span><br><span class="line">    <span class="keyword">case</span> <span class="number">4</span>:</span><br><span class="line">        fmt.Println(<span class="string">&quot;Ring&quot;</span>)</span><br><span class="line">    <span class="keyword">case</span> <span class="number">5</span>:</span><br><span class="line">        fmt.Println(<span class="string">&quot;Pinky&quot;</span>)</span><br><span class="line">    <span class="keyword">default</span>: <span class="comment">// 默认分支</span></span><br><span class="line">        fmt.Println(<span class="string">&quot;incorrect finger number&quot;</span>)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/Fq7U7SkHe1">Run in playground</a></p><p>在上面的程序中，<code>finger</code> 是 <code>8</code>，它与任何情况都不匹配，因此在默认情况下会打印 <code>incorrect finger number</code>。把 <code>default</code> 作为 <code>switch</code> 语句的最后一个分支是没有必要的。它可以存在于 <code>switch</code> 中的任何位置。</p><p>你可能还注意到 <code>finger</code> 声明中的微小变化。它在 <code>switch 语句中声明。 </code>switch<code>可以包含在计算表达式之前执行的可选语句。在第 8 行中，首先声明</code>finger<code>，然后在表达式中使用它。在这种情况下，</code>finger<code>的范围仅限于</code>switch&#96; 模块。</p><h2 id="多表达式分支"><a href="#多表达式分支" class="headerlink" title="多表达式分支"></a>多表达式分支</h2><p>通过用逗号将多个表达式分隔开，可以包含多个表达式。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;fmt&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    letter := <span class="string">&quot;i&quot;</span></span><br><span class="line">    fmt.Printf(<span class="string">&quot;Letter %s is a &quot;</span>, letter)</span><br><span class="line">    <span class="keyword">switch</span> letter &#123;</span><br><span class="line">    <span class="keyword">case</span> <span class="string">&quot;a&quot;</span>, <span class="string">&quot;e&quot;</span>, <span class="string">&quot;i&quot;</span>, <span class="string">&quot;o&quot;</span>, <span class="string">&quot;u&quot;</span>: <span class="comment">// 分支中的多个表达式</span></span><br><span class="line">        fmt.Println(<span class="string">&quot;vowel&quot;</span>)</span><br><span class="line">    <span class="keyword">default</span>:</span><br><span class="line">        fmt.Println(<span class="string">&quot;not a vowel&quot;</span>)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/AAVSQK76Me7">Run in playground</a></p><p>上面的程序判断 <code>letter</code> 是否是元音。第 11 行中的代码 <code>case &quot;a&quot;, &quot;e&quot;, &quot;i&quot;, &quot;o&quot;, &quot;u&quot;:</code> 与任何元音匹配。由于 <code>i</code> 是元音，因此该程序会打印：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Letter i is a vowel</span><br></pre></td></tr></table></figure><h2 id="无表达式分支"><a href="#无表达式分支" class="headerlink" title="无表达式分支"></a>无表达式分支</h2><p>switch 中的表达式是可选的，可以省略。如果省略该表达式，则认为该 <code>switch</code> 为 <code>switch true</code>，并且对每个 <code>case</code> 表达式进行求值，并执行相应的代码块。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;fmt&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    num := <span class="number">75</span></span><br><span class="line">    <span class="keyword">switch</span> &#123; <span class="comment">// 表达式被省略</span></span><br><span class="line">    <span class="keyword">case</span> num &gt;= <span class="number">0</span> &amp;&amp; num &lt;= <span class="number">50</span>:</span><br><span class="line">        fmt.Printf(<span class="string">&quot;%d is greater than 0 and less than 50&quot;</span>, num)</span><br><span class="line">    <span class="keyword">case</span> num &gt;= <span class="number">51</span> &amp;&amp; num &lt;= <span class="number">100</span>:</span><br><span class="line">        fmt.Printf(<span class="string">&quot;%d is greater than 51 and less than 100&quot;</span>, num)</span><br><span class="line">    <span class="keyword">case</span> num &gt;= <span class="number">101</span>:</span><br><span class="line">        fmt.Printf(<span class="string">&quot;%d is greater than 100&quot;</span>, num)</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><a href="https://play.golang.org/p/KPkwK0VdXII">Run in playground</a></p><p>在上面的程序中，<code>switch</code> 中不存在表达式，因此将其视为真，并判断每种情况。 第12行的 <code>case num&gt; = 51 &amp;&amp; num &lt;= 100:</code> 为 <code>true</code>，程序将打印：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">75 is greater than 51 and less than 100</span><br></pre></td></tr></table></figure><p>这种类型的 <code>switch</code> 可以看作是多个 <code>if else</code> 语句句的替代方法。</p><h2 id="Fallthrough"><a href="#Fallthrough" class="headerlink" title="Fallthrough"></a>Fallthrough</h2><p>在 Go 中，在执行分支后，控制权立即从 <code>switch</code> 语句中释放出来。<code>fallthrough</code> 语句用于将控制权转移到该分支执行后立即出现的分支的第一条语句。</p><p>让我们编写一个程序来理解 <code>fallthrough</code>。我们的程序将检查输入的数字是否小于 50、100 或 200。例如，如果输入 75，则程序将打印 75 小于 100 和 200。我们将使用 <code>fallthrough</code> 来实现。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;fmt&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">number</span><span class="params">()</span></span> <span class="type">int</span> &#123;</span><br><span class="line">    num := <span class="number">15</span> * <span class="number">5</span></span><br><span class="line">    <span class="keyword">return</span> num</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    <span class="keyword">switch</span> num := number(); &#123; <span class="comment">// num 不是一个常量</span></span><br><span class="line">    <span class="keyword">case</span> num &lt; <span class="number">50</span>:</span><br><span class="line">        fmt.Printf(<span class="string">&quot;%d is lesser than 50\n&quot;</span>, num)</span><br><span class="line">        <span class="keyword">fallthrough</span></span><br><span class="line">    <span class="keyword">case</span> num &lt; <span class="number">100</span>:</span><br><span class="line">        fmt.Printf(<span class="string">&quot;%d is lesser than 100\n&quot;</span>, num)</span><br><span class="line">        <span class="keyword">fallthrough</span></span><br><span class="line">    <span class="keyword">case</span> num &lt; <span class="number">200</span>:</span><br><span class="line">        fmt.Printf(<span class="string">&quot;%d is lesser than 200&quot;</span>, num)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/svGJAiswQj">Run in playground</a></p><p><code>switch</code> 和 <code>case</code> 表达式只能是常量。它们也可以在运行时进行计算。 在上面的程序中，第 14 行的 <code>num</code> 被初始化为函数 <code>number()</code> 的返回值。控制权位于 <code>switch</code> 内部，并对分支进行判断。第 18 行中的 <code>case num &lt;100:</code> 是正确的，程序将输出 <code>75 is lesser than 100</code>。下一条语句是 <code>fallthrough</code>。 当遇到 <code>fallthrough</code> 时，控制权将移至下一种情况的第一个语句，并打印 <code>75 is lesser than 200</code>。该程序的输出是：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">75 is lesser than 100</span><br><span class="line">75 is lesser than 200</span><br></pre></td></tr></table></figure><p><code>fallthrough</code> 应该是 <code>case</code> 中的最后一条语句。如果它位于中间的某个位置，则编译器将报错 <code>fallthrough statement out of place</code>。</p><h2 id="即使分支被计算为假，也会发生-Fallthrough"><a href="#即使分支被计算为假，也会发生-Fallthrough" class="headerlink" title="即使分支被计算为假，也会发生 Fallthrough"></a>即使分支被计算为假，也会发生 Fallthrough</h2><p>当使用 <code>fallthrough</code> 时要考虑一些细微之处。即使分支被计算为假，也会发生 Fallthrough。</p><p>请考虑以下程序。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;fmt&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    <span class="keyword">switch</span> num := <span class="number">25</span>; &#123;</span><br><span class="line">    <span class="keyword">case</span> num &lt; <span class="number">50</span>:</span><br><span class="line">        fmt.Printf(<span class="string">&quot;%d is lesser than 50\n&quot;</span>, num)</span><br><span class="line">        <span class="keyword">fallthrough</span></span><br><span class="line">    <span class="keyword">case</span> num &gt; <span class="number">100</span>:</span><br><span class="line">        fmt.Printf(<span class="string">&quot;%d is greater than 100\n&quot;</span>, num)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/sjynQMXtnmY">Run in playground</a></p><p>在上述程序中，<code>num</code> 为 25，小于 50，因此第 9 行的情况为 <code>true</code>。第 11 行出现 <code>fallthrough</code>。行号中的下一个case <code>case num&gt; 100:</code>。由于 <code>num &lt; 100</code>，所以 12 是假的。但是 <code>fallthrough</code> 不考虑这一点。即使分支被计算为假，也会发生 Fallthrough。</p><p>上面的程序将打印：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">25 is lesser than 50</span><br><span class="line">25 is greater than 100</span><br></pre></td></tr></table></figure><p>因此，请确保你了解使用 <code>fallthrough</code> 时的操作。</p><p>还有一件事是，不能在 switch 的最后一种分支中下使用 <code>fallthrough</code>。 如果在最后一种情况下存在 <code>fallthrough</code>，则将导致以下编译错误。</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cannot fallthrough final <span class="keyword">case</span> <span class="keyword">in</span> switch</span><br></pre></td></tr></table></figure><h2 id="switch-中的-switch"><a href="#switch-中的-switch" class="headerlink" title="switch 中的 switch"></a>switch 中的 switch</h2><p>可以使用 <code>break</code> 语句在 <code>switch</code> 完成之前提早终止它。 让我们将上面的示例修改为一个人为的示例，以了解 <code>break</code> 的工作原理。</p><p>让我们添加一个条件，如果 <code>num</code> 小于0，则 switch 应终止。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;fmt&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    <span class="keyword">switch</span> num := <span class="number">-5</span>; &#123;</span><br><span class="line">    <span class="keyword">case</span> num &lt; <span class="number">50</span>:</span><br><span class="line">        <span class="keyword">if</span> num &lt; <span class="number">0</span> &#123;</span><br><span class="line">            <span class="keyword">break</span></span><br><span class="line">        &#125;</span><br><span class="line">        fmt.Printf(<span class="string">&quot;%d is lesser than 50\n&quot;</span>, num)</span><br><span class="line">        <span class="keyword">fallthrough</span></span><br><span class="line">    <span class="keyword">case</span> num &lt; <span class="number">100</span>:</span><br><span class="line">        fmt.Printf(<span class="string">&quot;%d is lesser than 100\n&quot;</span>, num)</span><br><span class="line">        <span class="keyword">fallthrough</span></span><br><span class="line">    <span class="keyword">case</span> num &lt; <span class="number">200</span>:</span><br><span class="line">        fmt.Printf(<span class="string">&quot;%d is lesser than 200&quot;</span>, num)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/UHwBXPYLv1B">Run in playground</a></p><p>在上面的程序中，<code>num</code> 是 <code>-5</code>。当控件权到达第 10 行的 <code>if</code> 语句时，由于 <code>num &lt;0</code>，因此满足条件。<code>break</code> 语句在 <code>switch</code> 完成之前终止了，并且程序不输出任何内容。</p><h3 id="退出外部的循环"><a href="#退出外部的循环" class="headerlink" title="退出外部的循环"></a>退出外部的循环</h3><p>当 <code>switch case</code> 位于 <code>for</code> 循环内时，可能需要尽早终止 <code>for</code> 循环。 这可以通过标记 <code>for</code> 循环并使用 <code>switch</code> 语句内的该标记中断 <code>for</code> 循环来完成。让我们来看一个例子。</p><p>让我们编写一个程序来生成一个随机偶数。</p><p>我们将创建一个无限 <code>for</code> 循环，并使用 <code>switch</code> 条件来确定所生成的随机数是否为偶数。如果是偶数，将打印生成的数字，并使用其标签终止 <code>for</code> 循环。<code>rand</code> 包的 <a href="https://golang.org/pkg/math/rand/#Rand.Intn"><code>Intn</code></a> 函数用于生成非负伪随机数。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;fmt&quot;</span></span><br><span class="line">    <span class="string">&quot;math/rand&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">randloop:</span><br><span class="line">    <span class="keyword">for</span> &#123;</span><br><span class="line">        <span class="keyword">switch</span> i := rand.Intn(<span class="number">100</span>); &#123;</span><br><span class="line">        <span class="keyword">case</span> i%<span class="number">2</span> == <span class="number">0</span>:</span><br><span class="line">            fmt.Printf(<span class="string">&quot;Generated even number %d&quot;</span>, i)</span><br><span class="line">            <span class="keyword">break</span> randloop</span><br><span class="line">        &#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><a href="https://play.golang.org/p/0bLYOgs2TUk">Run in playground</a></p><p>在上面的程序中，第 9 行中的 <code>for</code> 循环标记为 <code>randloop</code>。使用第 <code>11</code> 行的 <code>Intn</code> 函数在 0 到 99 之间生成一个随机数（不包括 100）。 如果生成的数字为偶数，则使用标签在第 14 行中断循环。</p><p>该程序打印：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Generated even number 18</span><br></pre></td></tr></table></figure><p>请注意，如果使用不带标签的 <code>break</code> 语句，则将仅中断 <code>switch</code> 语句，循环将继续运行。因此，标记循环并在 <code>switch</code> 内部的 <code>break</code> 语句中使用它对于中断外部 for 循环是必要的。</p><blockquote><p>原文地址 <a href="https://golangbot.com/switch/">Switch Statement</a><br>原文作者：<a href="https://golangbot.com/about/">Naveen Ramanathan</a><br>译文出自：<a href="https://blog.zisheng.pro/categories/%E6%B4%9B%E7%AB%B9%E7%BF%BB%E8%AF%91%E8%AE%A1%E5%88%92/">紫升翻译计划</a></p></blockquote>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/b3d3163f4bf0/</id>
    <link href="https://blog.zisheng.pro/posts/b3d3163f4bf0/"/>
    <published>2023-03-19T07:10:00.000Z</published>
    <summary>switch 是一个条件语句，它对表达式进行求值并将其与可能的匹配列表进行比较，并执行相应的代码块。可以将其视为替换复杂的 if 语句的惯用方式。</summary>
    <title>Go 语言教程系列のSwitch 语句</title>
    <updated>2026-09-18T08:15:28.629Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="紫升翻译计划" scheme="https://blog.zisheng.pro/categories/%E7%B4%AB%E5%8D%87%E7%BF%BB%E8%AF%91%E8%AE%A1%E5%88%92/"/>
    <category term="Go" scheme="https://blog.zisheng.pro/categories/Go/"/>
    <category term="Go" scheme="https://blog.zisheng.pro/tags/Go/"/>
    <category term="Go 教程" scheme="https://blog.zisheng.pro/tags/Go-%E6%95%99%E7%A8%8B/"/>
    <category term="Golang Loop" scheme="https://blog.zisheng.pro/tags/Golang-Loop/"/>
    <content>
      <![CDATA[<p>循环语句用于重复执行代码块。</p><p><code>for</code> 是 Go 中唯一可用的循环。Go 没有在其他语言（例如 C）中存在的<code>while</code> 或 <code>do while</code> 循环。</p><h2 id="for-循环语法"><a href="#for-循环语法" class="headerlink" title="for 循环语法"></a>for 循环语法</h2><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">for</span> initialisation; condition; post &#123;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>初始化语句将仅执行一次。循环初始化后，将检查条件。如果条件的计算结果为 <code>true</code>，则将执行 <code>{</code> <code>}</code> 内部的循环主体，然后执行 post 语句。 每次循环成功迭代后，将执行 post 语句。执行 post 语句后，将重新检查条件。如果为 <code>true</code>，则循环将继续执行，否则 <code>for</code> 循环终止。</p><p>Go 中的 for 循环的三个部分，即初始化，条件和发布都是可选的。让我们看一个示例，以更好地理解循环。</p><h2 id="示例"><a href="#示例" class="headerlink" title="示例"></a>示例</h2><p>让我们编写一个程序，该程序使用 <code>for</code> 循环打印从 1 到 10 的所有数字。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;fmt&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    <span class="keyword">for</span> i := <span class="number">1</span>; i &lt;= <span class="number">10</span>; i++ &#123;</span><br><span class="line">        fmt.Printf(<span class="string">&quot; %d&quot;</span>,i)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/mV6Zgcx2DY">Run in playground</a></p><p>在上面的程序中，<code>i</code> 被初始化为 1。条件语句将检查 <code>i &lt;= 10</code>。如果条件为真，则输出 <code>i</code> 的值，否则循环终止。在每次迭代结束时，post 语句将 <code>i</code> 递增 1。一旦 <code>i</code> 大于 10，则循环终止。</p><p>上面的程序将打印 <code>1 2 3 4 5 6 7 8 9 10</code></p><p>在 for 循环中声明的变量仅在循环范围内可用。因此，无法在主体外部进行循环访问 <code>i</code>。</p><h2 id="break"><a href="#break" class="headerlink" title="break"></a>break</h2><p><code>break</code> 语句用于在完成正常执行之前突然终止 <code>for</code> 循环，并将控制权移至 <code>for</code> 循环之后的代码行。</p><p>让我们编写一个使用 <code>break</code> 打印从 1 到 5 的数字的程序。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;fmt&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    <span class="keyword">for</span> i := <span class="number">1</span>; i &lt;= <span class="number">10</span>; i++ &#123;</span><br><span class="line">        <span class="keyword">if</span> i &gt; <span class="number">5</span> &#123;</span><br><span class="line">            <span class="keyword">break</span> <span class="comment">// 如果 i 大于 5，循环将终止</span></span><br><span class="line">        &#125;</span><br><span class="line">        fmt.Printf(<span class="string">&quot;%d &quot;</span>, i)</span><br><span class="line">    &#125;</span><br><span class="line">    fmt.Printf(<span class="string">&quot;\nline after for loop&quot;</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/sujKy92f--">Run in playground</a></p><p>在上面的程序中，在每次迭代过程中都会检查 <code>i</code> 值。如果 <code>i</code> 大于 5，则执行 <code>break</code> 并终止循环。然后在 <code>for</code> 循环之后执行 <code>print</code> 语句。上面的程序将输出：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">1 2 3 4 5</span><br><span class="line">line after <span class="keyword">for</span> loop</span><br></pre></td></tr></table></figure><h2 id="continue"><a href="#continue" class="headerlink" title="continue"></a>continue</h2><p><code>continue</code> 语句用于跳过 <code>for</code> 循环的当前迭代。在 <code>continue</code> 语句之后的 <code>for</code> 循环中存在的所有代码将不会针对当前迭代执行。循环将继续进行下一个迭代。</p><p>让我们编写一个程序，使用 <code>continue</code> 打印从 <code>1</code> 到 <code>10</code> 的所有奇数。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;fmt&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    <span class="keyword">for</span> i := <span class="number">1</span>; i &lt;= <span class="number">10</span>; i++ &#123;</span><br><span class="line">        <span class="keyword">if</span> i%<span class="number">2</span> == <span class="number">0</span> &#123;</span><br><span class="line">            <span class="keyword">continue</span></span><br><span class="line">        &#125;</span><br><span class="line">        fmt.Printf(<span class="string">&quot;%d &quot;</span>, i)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/DRLN2ZHwVS">Run in playground</a></p><p>在上面的程序中，<code>if i％2 == 0</code> 行检查将 <code>i</code> 除以 <code>2</code> 的提示是否为 0。如果为零，则数字为偶数，并执行 <code>continue</code> 语句，并且控制移至 循环的下一次迭代。 因此，<code>continue</code> 之后的 <code>print</code> 语句将不会被调用，并且循环会进行到下一个迭代。上面程序的输出是 <code>1 3 5 7 9</code>。</p><h2 id="嵌套循环"><a href="#嵌套循环" class="headerlink" title="嵌套循环"></a>嵌套循环</h2><p>一个内部有另一个 <code>for</code> 循环的 <code>for</code> 循环称为嵌套 <code>for</code> 循环。让我们通过编写一个打印以下序列的程序来了解嵌套的 <code>for</code> 循环。</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><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><p>下面的程序使用嵌套的 <code>for</code> 循环来打印序列。第 8 行中的变量 <code>n</code> 存储序列中的行数。在我们的例子中是 5。外部 <code>for</code> 循环将 <code>i</code> 从 <code>0</code> 迭代到 <code>4</code>，内部 <code>for</code> 循环将 <code>j</code> 从 <code>0</code> 迭代到 <code>i</code> 的当前值。 内循环为每次迭代打印“ *”，而外循环在每次迭代结束时打印新行。 运行该程序，你会看到序列打印为输出。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;fmt&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    n := <span class="number">5</span></span><br><span class="line">    <span class="keyword">for</span> i := <span class="number">0</span>; i &lt; n; i++ &#123;</span><br><span class="line">        <span class="keyword">for</span> j := <span class="number">0</span>; j &lt;= i; j++ &#123;</span><br><span class="line">            fmt.Print(<span class="string">&quot;*&quot;</span>)</span><br><span class="line">        &#125;</span><br><span class="line">        fmt.Println()</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/0rq8fWjVDLb">Run in playground</a></p><h2 id="标签"><a href="#标签" class="headerlink" title="标签"></a>标签</h2><p>标签可用于从内部 <code>for</code> 循环中断外部循环。让我们通过一个简单的例子来理解我的意思。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;fmt&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    <span class="keyword">for</span> i := <span class="number">0</span>; i &lt; <span class="number">3</span>; i++ &#123;</span><br><span class="line">        <span class="keyword">for</span> j := <span class="number">1</span>; j &lt; <span class="number">4</span>; j++ &#123;</span><br><span class="line">            fmt.Printf(<span class="string">&quot;i = %d , j = %d\n&quot;</span>, i, j)</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><p><a href="https://play.golang.org/p/BnCKho2x5hM">Run in playground</a></p><p>上面的程序是不言自明的，它将打印：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">i = 0 , j = 1</span><br><span class="line">i = 0 , j = 2</span><br><span class="line">i = 0 , j = 3</span><br><span class="line">i = 1 , j = 1</span><br><span class="line">i = 1 , j = 2</span><br><span class="line">i = 1 , j = 3</span><br><span class="line">i = 2 , j = 1</span><br><span class="line">i = 2 , j = 2</span><br><span class="line">i = 2 , j = 3</span><br></pre></td></tr></table></figure><p>这没有什么特殊的！</p><p>如果我们想在 <code>i</code> 和 <code>j</code> 相等时停止打印该怎么办。为此，我们需要退出外部 <code>for</code> 循环。 当 <code>i</code> 和 <code>j</code> 相等时，在内部 <code>for</code> 循环中添加 <code>break</code> 只会退出内部 <code>for</code> 循环。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;fmt&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    <span class="keyword">for</span> i := <span class="number">0</span>; i &lt; <span class="number">3</span>; i++ &#123;</span><br><span class="line">        <span class="keyword">for</span> j := <span class="number">1</span>; j &lt; <span class="number">4</span>; j++ &#123;</span><br><span class="line">            fmt.Printf(<span class="string">&quot;i = %d , j = %d\n&quot;</span>, i, j)</span><br><span class="line">            <span class="keyword">if</span> i == j &#123;</span><br><span class="line">                <span class="keyword">break</span></span><br><span class="line">            &#125;</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><p><a href="https://play.golang.org/p/uMjbF8Ii41d">Run in playground</a></p><p>在上面的程序中，当第 10 行的 <code>i</code> 和 <code>j</code> 相等时，我在内部的 <code>for</code> 循环中添加了一个 <code>break</code>。这只会退出内部 <code>for</code> 循环，而外部循环将继续执行。该程序将打印：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">i = 0 , j = 1</span><br><span class="line">i = 0 , j = 2</span><br><span class="line">i = 0 , j = 3</span><br><span class="line">i = 1 , j = 1</span><br><span class="line">i = 2 , j = 1</span><br><span class="line">i = 2 , j = 2</span><br></pre></td></tr></table></figure><p>这不是预期的输出。当 <code>i</code> 和 <code>j</code> 相等时，即当它们等于 <code>1</code> 时，我们需要停止打印。标签可用于从外部循环中断开。让我们使用标签重写上面的程序：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;fmt&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">outer:</span><br><span class="line">    <span class="keyword">for</span> i := <span class="number">0</span>; i &lt; <span class="number">3</span>; i++ &#123;</span><br><span class="line">        <span class="keyword">for</span> j := <span class="number">1</span>; j &lt; <span class="number">4</span>; j++ &#123;</span><br><span class="line">            fmt.Printf(<span class="string">&quot;i = %d , j = %d\n&quot;</span>, i, j)</span><br><span class="line">            <span class="keyword">if</span> i == j &#123;</span><br><span class="line">                <span class="keyword">break</span> outer</span><br><span class="line">            &#125;</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><p><a href="https://play.golang.org/p/BI10Rmp_Z3y">Run in playground</a></p><p>在上面的程序中，我们在外部 <code>for</code> 循环的第 8 行中添加了标签 <code>outer</code>，在第 13 行中，通过指定标签来中断外部 <code>for</code> 循环。当 <code>i</code> 和 <code>j</code> 相等时，该程序将停止打印。该程序将输出：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">i = 0 , j = 1</span><br><span class="line">i = 0 , j = 2</span><br><span class="line">i = 0 , j = 3</span><br><span class="line">i = 1 , j = 1</span><br></pre></td></tr></table></figure><h2 id="更多示例"><a href="#更多示例" class="headerlink" title="更多示例"></a>更多示例</h2><p>让我们写更多的代码来覆盖 <code>for</code> 循环的所有变体。下面的程序打印从 0 到 10 的所有偶数。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;fmt&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    i := <span class="number">0</span></span><br><span class="line">    <span class="keyword">for</span> ;i &lt;= <span class="number">10</span>; &#123; <span class="comment">// initialisation and post 被省略</span></span><br><span class="line">        fmt.Printf(<span class="string">&quot;%d &quot;</span>, i)</span><br><span class="line">        i += <span class="number">2</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/PNXliGINku">Run in playground</a></p><p>我们已经知道 <code>for</code> 循环的所有三个部分，即 <code>initialisation</code>、<code>condition</code> 和 <code>post</code> 都是可选的。在上述程序中，省略了<code>initialisation</code> 和 <code>post</code>。在 <code>for</code> 循环外，<code>i</code> 被初始化为 <code>0</code>。只要 <code>i &lt;= 10</code>，循环就会执行。我在 <code>for</code> 循环内增加了 <code>2</code>。上面的程序输出 <code>0 2 4 6 8 10</code>。</p><p>也可以省略上述程序的 <code>for</code> 循环中的分号。可以将这种格式视为 <code>while</code> 循环的替代方法。上面的程序可以改写为：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;fmt&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    i := <span class="number">0</span></span><br><span class="line">    <span class="keyword">for</span> i &lt;= <span class="number">10</span> &#123; <span class="comment">// 分号被省略，只剩下条件语句</span></span><br><span class="line">        fmt.Printf(<span class="string">&quot;%d &quot;</span>, i)</span><br><span class="line">        i += <span class="number">2</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/UYiz-Wtnoj">Run in playground</a></p><p>可以在 <code>for</code> 循环中声明多个变量并对其进行操作。让我们编写一个使用多个变量声明打印以下序列的程序。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">10 * 1 = 10</span><br><span class="line">11 * 2 = 22</span><br><span class="line">12 * 3 = 36</span><br><span class="line">13 * 4 = 52</span><br><span class="line">14 * 5 = 70</span><br><span class="line">15 * 6 = 90</span><br><span class="line">16 * 7 = 112</span><br><span class="line">17 * 8 = 136</span><br><span class="line">18 * 9 = 162</span><br><span class="line">19 * 10 = 190</span><br></pre></td></tr></table></figure><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;fmt&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    <span class="keyword">for</span> no, i := <span class="number">10</span>, <span class="number">1</span>; i &lt;= <span class="number">10</span> &amp;&amp; no &lt;= <span class="number">19</span>; i, no = i+<span class="number">1</span>, no+<span class="number">1</span> &#123;</span><br><span class="line">        fmt.Printf(<span class="string">&quot;%d * %d = %d\n&quot;</span>, no, i, no*i)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/e7Pf0UDjj0">Run in playground</a></p><p>在上面的程序中，<code>no</code> 和 <code>i</code> 被声明并分别初始化为 10 和 1。在每次迭代结束时，它们将增加 1。布尔运算符 <code>&amp;&amp;</code> 用于确保 <code>i</code> 小于或等于 10 以及 <code>no</code> 小于或等于 19 的条件成立。</p><h2 id="无限循环"><a href="#无限循环" class="headerlink" title="无限循环"></a>无限循环</h2><p>创建无限循环的语法是：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">for</span> &#123;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>以下程序将持续打印 <code>Hello World</code>，而不会终止。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> <span class="string">&quot;fmt&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    <span class="keyword">for</span> &#123;</span><br><span class="line">        fmt.Println(<span class="string">&quot;Hello World&quot;</span>)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>如果尝试在 <a href="https://play.golang.org/p/kYQZw1AWT4">go park</a> 中运行上述程序，则会收到错误消息 <code>process took too long</code>。请尝试在你的本地系统中运行它以无限打印 <code>Hello World</code>。</p><p>还有一个范围 <strong>range</strong>可以在 <code>for</code> 循环中用于数组操作。当我们在后续教程中了解数组时，我们将进行介绍。</p><blockquote><p>原文地址 <a href="https://golangbot.com/loops/">loops</a><br>原文作者：<a href="https://golangbot.com/about/">Naveen Ramanathan</a><br>译文出自：<a href="https://blog.zisheng.pro/categories/%E6%B4%9B%E7%AB%B9%E7%BF%BB%E8%AF%91%E8%AE%A1%E5%88%92/">紫升翻译计划</a></p></blockquote>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/1e53ff51d8a4/</id>
    <link href="https://blog.zisheng.pro/posts/1e53ff51d8a4/"/>
    <published>2023-03-19T07:09:00.000Z</published>
    <summary>Go 循环语句用于重复执行代码块。</summary>
    <title>Go 语言教程系列の循环</title>
    <updated>2026-09-18T08:15:28.629Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="紫升翻译计划" scheme="https://blog.zisheng.pro/categories/%E7%B4%AB%E5%8D%87%E7%BF%BB%E8%AF%91%E8%AE%A1%E5%88%92/"/>
    <category term="Go" scheme="https://blog.zisheng.pro/categories/Go/"/>
    <category term="Go" scheme="https://blog.zisheng.pro/tags/Go/"/>
    <category term="Go 教程" scheme="https://blog.zisheng.pro/tags/Go-%E6%95%99%E7%A8%8B/"/>
    <category term="Golang if else" scheme="https://blog.zisheng.pro/tags/Golang-if-else/"/>
    <content>
      <![CDATA[<p><code>if</code> 是一个具有布尔条件的语句，如果该条件的值为 <code>true</code>，它将执行一个代码块。如果判定条件为 <code>false</code>，它将执行 <code>else</code> 代码块。在本教程中，我们将研究 <code>if</code> 语句的各种语法和使用方法。</p><h2 id="if-语句语法"><a href="#if-语句语法" class="headerlink" title="if 语句语法"></a><code>if</code> 语句语法</h2><p>下面的代码正是 <code>if</code> 语句的语法：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> condition &#123;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>如果 <code>condition</code> 被判定为 <code>true</code>，大括号 <code>{</code> 和 <code>}</code> 之间的代码将被执行。</p><p>与其他语言（例如C、JavaScript）不同，Go 语言中大括号 <code>{}</code> 是必需的，即使大括号 <code>{}</code> 之间只有一行代码。</p><h2 id="Example"><a href="#Example" class="headerlink" title="Example"></a>Example</h2><p>让我们来写一个简单的程序来判断一个数字的奇偶性。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;fmt&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    num := <span class="number">10</span></span><br><span class="line">    <span class="keyword">if</span> num%<span class="number">2</span> == <span class="number">0</span> &#123; <span class="comment">// 检查数字是否是奇数</span></span><br><span class="line">        fmt.Println(<span class="string">&quot;The number&quot;</span>, num, <span class="string">&quot;is even&quot;</span>)</span><br><span class="line">        <span class="keyword">return</span></span><br><span class="line">    &#125;</span><br><span class="line">    fmt.Println(<span class="string">&quot;The number&quot;</span>, num, <span class="string">&quot;is odd&quot;</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/RRxkgK07ul4">Run in Playground</a></p><p>在上面的程序中，第9行的条件 <code>num％2</code> 确定 <code>num</code> 除以 <code>2</code> 的余数是否为零。由于在这种情况下为 <code>0</code>，所以将打印文本 <code>The number 10 is even</code>，然后程序退出。</p><h2 id="if-else-语句"><a href="#if-else-语句" class="headerlink" title="if...else... 语句"></a><code>if...else...</code> 语句</h2><p><code>if</code> 语句具有可选的 <code>else</code> 结构体，如果 <code>if</code> 语句中的条件被判定为 <code>false</code>，则将执行该结构体。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> condition &#123;</span><br><span class="line">&#125; <span class="keyword">else</span> &#123;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在上面的代码片段中，如果条件被判定为 <code>false</code>，那么将执行 <code>else {</code>和 <code>}</code> 之间的代码。</p><p>让我们重写程序以使用 <code>if else</code> 语句判断数字的奇偶性。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;fmt&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    num := <span class="number">11</span></span><br><span class="line">    <span class="keyword">if</span> num%<span class="number">2</span> == <span class="number">0</span> &#123; <span class="comment">// 检查数字是否是奇数</span></span><br><span class="line">        fmt.Println(<span class="string">&quot;the number&quot;</span>, num, <span class="string">&quot;is even&quot;</span>)</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        fmt.Println(<span class="string">&quot;the number&quot;</span>, num, <span class="string">&quot;is odd&quot;</span>)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/bMevwhJhguO">Run in playground</a></p><p>在上面的代码中，我们没有像上一节那样返回条件是否为 <code>true</code>，而是创建了 <code>else</code> 语句，如果条件为 <code>false</code> 则执行该语句。在这种情况下，由于 <code>11</code> 为奇数，因此 <code>if</code> 条件为 <code>false</code> 并执行 <code>else</code> 语句中的代码。上面的程序将打印。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">the number 11 is odd</span><br></pre></td></tr></table></figure><h2 id="if-else-if-else-语句"><a href="#if-else-if-else-语句" class="headerlink" title="if...else if...else... 语句"></a><code>if...else if...else...</code> 语句</h2><p><code>if</code> 语句还具有可选的 <code>else if</code> 和 <code>else</code> 组件。下面提供了相同的语法</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> condition1 &#123;</span><br><span class="line">...</span><br><span class="line">&#125; <span class="keyword">else</span> <span class="keyword">if</span> condition2 &#123;</span><br><span class="line">...</span><br><span class="line">&#125; <span class="keyword">else</span> &#123;</span><br><span class="line">...</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>该语法用来从上到下来查找问题的真相。</p><p>在上面的语句中，如果 <code>condition1</code> 为 <code>true</code>，则执行 <code>if condition1 {</code> 和右括号 <code>}</code> 中的代码。</p><p>如果 <code>condition1</code> 是 <code>false</code>，而 <code>condition2</code> 是 <code>true</code>，则执行 <code>condition2 {</code> 和下一个右括号 <code>}</code> 的其他代码。</p><p>如果 <code>condition1</code> 和 <code>condition2</code> 均为假，则执行 <code>else {</code> 和 <code>}</code> 之间 <code>else</code> 语句中的代码。</p><p>可以有任意数量的 <code>else if</code> 语句。</p><p>通常，无论条件是 <code>if</code> 还是 <code>else if</code>，都将执行相应的代码块。如果没有一个条件为真，则执行 <code>else</code> 块。</p><p>让我们编写一个使用 <code>else if</code> 的程序。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;fmt&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    num := <span class="number">99</span></span><br><span class="line">    <span class="keyword">if</span> num &lt;= <span class="number">50</span> &#123;</span><br><span class="line">        fmt.Println(num, <span class="string">&quot;is less than or equal to 50&quot;</span>)</span><br><span class="line">    &#125; <span class="keyword">else</span> <span class="keyword">if</span> num &gt;= <span class="number">51</span> &amp;&amp; num &lt;= <span class="number">100</span> &#123;</span><br><span class="line">        fmt.Println(num, <span class="string">&quot;is between 51 and 100&quot;</span>)</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        fmt.Println(num, <span class="string">&quot;is greater than 100&quot;</span>)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/VNPbCiK9eXT">Run in playground</a></p><p>在上面的程序中，第 11 行的条件 <code>else if num &gt;= 51 &amp;&amp; num &lt;= 100</code> 为 <code>true</code>，因此程序将打印：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">99 is between 51 and 100</span><br></pre></td></tr></table></figure><h2 id="带有赋值语句的-if"><a href="#带有赋值语句的-if" class="headerlink" title="带有赋值语句的 if"></a>带有赋值语句的 <code>if</code></h2><p><code>if</code> 的另一种变体，包括可选的短赋值语句，该语句在判定条件之前执行。它的语法是：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> assignment-statement; condition &#123;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在上面的代码段中，在判定条件之前首先执行 <code>assignment-statement</code>。</p><p>让我们使用上面的语法重写判断数字奇偶性的程序。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;fmt&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    <span class="keyword">if</span> num := <span class="number">10</span>; num % <span class="number">2</span> == <span class="number">0</span> &#123; <span class="comment">//checks if number is even</span></span><br><span class="line">        fmt.Println(num,<span class="string">&quot;is even&quot;</span>)</span><br><span class="line">    &#125;  <span class="keyword">else</span> &#123;</span><br><span class="line">        fmt.Println(num,<span class="string">&quot;is odd&quot;</span>)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/_X9q4MWr4s">Run in playground</a></p><p>在上面的程序中，在第 8 行的 <code>if</code> 语句中初始化了 <code>num</code>。需要注意的一件事是 <code>num</code> 仅可用于 <code>if</code> 和 <code>else</code> 内部的访问。即 <code>num</code> 的有效范围仅限于 <code>if</code> 块。 如果我们尝试从 <code>if</code> 或 <code>else</code> 外部访问 <code>num</code>，则编译器会报错。当我们仅出于 <code>if else</code> 构造的目的声明变量时，这种语法通常会派上用场。在这种情况下使用此语法可确保变量的范围仅在 <code>if else</code> 语句内。</p><h2 id="陷阱"><a href="#陷阱" class="headerlink" title="陷阱"></a>陷阱</h2><p><code>else</code> 语句应在 <code>if</code> 语句的大括号 <code>}</code> 之后的同一行开始。如果没有，编译器会抛错。</p><p>让我们通过一个程序来理解这一点。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;fmt&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    num := <span class="number">10</span></span><br><span class="line">    <span class="keyword">if</span> num % <span class="number">2</span> == <span class="number">0</span> &#123; <span class="comment">//checks if number is even</span></span><br><span class="line">        fmt.Println(<span class="string">&quot;the number is even&quot;</span>)</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">else</span> &#123;</span><br><span class="line">        fmt.Println(<span class="string">&quot;the number is odd&quot;</span>)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/RYNqZZO2F9">Run in playground</a></p><p>在上面的程序中，在第 11 行的 <code>if</code> 语句的结束符 <code>}</code> 之后，<code>else</code> 语句不在同一行开始。而是从下一行开始。 Go 不允许这样做。如果你运行此程序，则编译器将输出错误，</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">./prog.go:12:5: syntax error: unexpected <span class="keyword">else</span>, expecting &#125;</span><br></pre></td></tr></table></figure><p>原因是因为 Go 自动插入分号的方式。你可以在 <a href="https://golang.org/ref/spec#Semicolons">Semicolons</a> 中阅读有关分号插入规则的信息。</p><p>在规则中，指定了在结束括号 <code>}</code> 之后插入分号（如果这是该行的最后一个标记）。因此，Go 编译器在第 11 行的 <code>if</code> 语句的右括号 <code>}</code> 之后自动插入了分号。</p><p>所以我们的程序实际上变成了：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">...</span><br><span class="line"><span class="keyword">if</span> num%<span class="number">2</span> == <span class="number">0</span> &#123;</span><br><span class="line">      fmt.Println(<span class="string">&quot;the number is even&quot;</span>)</span><br><span class="line">&#125;;  <span class="comment">// Go 编译器自动插入分号</span></span><br><span class="line"><span class="keyword">else</span> &#123;</span><br><span class="line">      fmt.Println(<span class="string">&quot;the number is odd&quot;</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>编译器将在以上代码片段的第 4 行中插入分号。</p><p>由于 <code>if {...} else {...}</code> 是 一个单独的语句，因此中间不应使用分号。因此，该程序无法编译。 在语法上需要将 <code>else</code> 放在 <code>if</code> 语句的右括号 <code>}</code> 之后的同一行中。</p><p>我已经通过在 <code>if</code> 语句的结束符号 <code>}</code> 之后移动 <code>else</code> 来重写程序，以防止自动分号插入。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;fmt&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    num := <span class="number">10</span></span><br><span class="line">    <span class="keyword">if</span> num%<span class="number">2</span> == <span class="number">0</span> &#123; <span class="comment">// 检查数字是否是奇数</span></span><br><span class="line">        fmt.Println(<span class="string">&quot;the number is even&quot;</span>)</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        fmt.Println(<span class="string">&quot;the number is odd&quot;</span>)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/hv_27vbIBC">Run in playground</a></p><p>现在编译器会很高兴，我们也很高兴。</p><h2 id="Go-惯例"><a href="#Go-惯例" class="headerlink" title="Go 惯例"></a>Go 惯例</h2><p>我们已经看到了各种 <code>if-else</code> 构造，并且实际上已经看到了编写同一程序的多种方法。例如，我们已经看到了编写程序的多种方法，该程序使用不同的 <code>if</code> 构造检查数字是偶数还是奇数。Go 中的惯用编码方式是哪一种？ 按照 Go 的哲学，最好避免不必要的分支和代码缩进。也可以理解为尽早返回。我已经从下面的上一节中提供了该程序，</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;fmt&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    <span class="keyword">if</span> num := <span class="number">10</span>; num % <span class="number">2</span> == <span class="number">0</span> &#123; <span class="comment">// 检查数字是否是奇数</span></span><br><span class="line">        fmt.Println(num,<span class="string">&quot;is even&quot;</span>)</span><br><span class="line">    &#125;  <span class="keyword">else</span> &#123;</span><br><span class="line">        fmt.Println(num,<span class="string">&quot;is odd&quot;</span>)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/_X9q4MWr4s">Run in playground</a></p><p>按照 Go 的哲学，编写上述程序的惯用方式是避免 <code>else</code> 并在条件为 <code>true</code> 时从 <code>if</code> 返回。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;fmt&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    num := <span class="number">10</span>;</span><br><span class="line">    <span class="keyword">if</span> num%<span class="number">2</span> == <span class="number">0</span> &#123; <span class="comment">// 检查数字是否是奇数</span></span><br><span class="line">        fmt.Println(num, <span class="string">&quot;is even&quot;</span>)</span><br><span class="line">        <span class="keyword">return</span></span><br><span class="line">    &#125;</span><br><span class="line">    fmt.Println(num, <span class="string">&quot;is odd&quot;</span>)</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/N8A5tPLnVYr">Run in playground</a></p><p>在上面的程序中，一旦我们发现数字是偶数，我们将立即返回。这样可以避免不必要的 <code>else</code> 代码分支。这是 Go 中完成工作的方式。每当编写 Go 程序时，请记住这一点。</p><blockquote><p>原文地址 <a href="https://golangbot.com/if-statement/">If else statement</a><br>原文作者：<a href="https://golangbot.com/about/">Naveen Ramanathan</a><br>译文出自：<a href="https://blog.zisheng.pro/categories/%E6%B4%9B%E7%AB%B9%E7%BF%BB%E8%AF%91%E8%AE%A1%E5%88%92/">紫升翻译计划</a></p></blockquote>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/4176036c2d35/</id>
    <link href="https://blog.zisheng.pro/posts/4176036c2d35/"/>
    <published>2023-03-19T07:08:00.000Z</published>
    <summary>if 是一个具有布尔条件的语句，如果该条件的值为 true，它将执行一个代码块。如果判定条件为 false，它将执行 else 代码块。在本教程中，我们将研究 if 语句的各种语法和使用方法。</summary>
    <title>Go 语言教程系列のif else 语句</title>
    <updated>2026-09-18T08:15:28.629Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="紫升翻译计划" scheme="https://blog.zisheng.pro/categories/%E7%B4%AB%E5%8D%87%E7%BF%BB%E8%AF%91%E8%AE%A1%E5%88%92/"/>
    <category term="Go" scheme="https://blog.zisheng.pro/categories/Go/"/>
    <category term="Go" scheme="https://blog.zisheng.pro/tags/Go/"/>
    <category term="Go 教程" scheme="https://blog.zisheng.pro/tags/Go-%E6%95%99%E7%A8%8B/"/>
    <category term="Golang 包" scheme="https://blog.zisheng.pro/tags/Golang-%E5%8C%85/"/>
    <content>
      <![CDATA[<h2 id="什么是包（package），为什么使用它们？"><a href="#什么是包（package），为什么使用它们？" class="headerlink" title="什么是包（package），为什么使用它们？"></a>什么是包（package），为什么使用它们？</h2><p>到目前为止，我们已经看到 Go 程序只有一个带有 <code>main</code> 函数的文件，以及几个其他函数。在实际情况下，这种将所有源代码写入单个文件的方法是不可扩展的。重用和维护以此方式编写的代码几乎是不可能。这时就需要用到包的概念。</p><p><strong>包用于组织 Go 源代码，以提高可重用性和可读性。包是位于同一目录中的 Go 源文件的集合。包提供了代码分隔，因此我们可以轻松维护 Go 项目。</strong></p><p>例如，假设我们正在 Go 中编写财务应用程序，其中一些功能是单利计算、复利计算和贷款计算。组织此应用程序的一种简单方法是根据功能。我们可以创建包 <code>simpleinterest</code>、<code>compoundinterest</code> 和 <code>loan</code>。如果 <code>loan</code> 包需要计算单利，则可以通过导入 <code>simpleinterest</code> 包来简单地进行计算。这样，代码就可以重用。</p><p>我们将通过创建一个简单的应用程序来学习包，以确定给定的本金、利率和持续时间（以年为单位）的单利。</p><h2 id="main-函数和-main-包"><a href="#main-函数和-main-包" class="headerlink" title="main 函数和 main 包"></a>main 函数和 main 包</h2><p>每个可执行的 Go 应用程序都必须包含 <code>main</code> 函数。此函数是执行的入口点。<code>main</code> 函数应保留在主包中。</p><p><code>package packagename</code> 指定一个指向 <code>packagename</code> 包的特定源文件。这在每一个 go 源文件中都应该放在第一行。</p><p>接下来让我们从为应用创建 <code>main</code> 函数和 <code>main</code> 包开始吧。</p><p>执行下面的命令在 <code>Desktop</code> 文件夹下创建一个名为 <code>lernapackage</code> 的文件。</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">mkdir</span> ~/Desktop/learnpackage/</span><br></pre></td></tr></table></figure><p>在 <code>learnpackage</code> 目录下创建一个 <code>main.go</code> 文件，然后写入一下内容：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> <span class="string">&quot;fmt&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    fmt.Println(<span class="string">&quot;Simple interest calculation&quot;</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>package main</code> 表示该文件属于 <code>main</code> 包，<code>import &quot;packagename&quot;</code> 语句被用来导入一个已经存在的包。<code>packagename.FunctionName()</code> 是调用包中函数的语法。</p><p>在第 3 行中，为了使用 <code>Println</code> 函数我们导入了 <code>fmt</code> 包。<code>fmt</code> 是一个 Go 的标准库并且是内置可用的。然后是打印 <code>Simple interest calculation</code> 的 <code>main</code> 函数。</p><p>进入 <code>learnpackage</code> 目录并使用下面的命令编译上面的项目：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">cd</span> ~/Desktop/learnpackage/</span><br></pre></td></tr></table></figure><p>然后输入下面的初始化 <code>mod</code>：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">go mod init learnpackage</span><br></pre></td></tr></table></figure><p>然后尝试输入下面的命令：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">go install</span><br></pre></td></tr></table></figure><p>如果一切顺利，我们的二进制文件将被编译并准备执行。在终端中输入命令 <code>learnpackage</code>，你将看到以下输出：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Simple interest calculation</span><br></pre></td></tr></table></figure><h2 id="Go-模块（Module）"><a href="#Go-模块（Module）" class="headerlink" title="Go 模块（Module）"></a>Go 模块（Module）</h2><p>我们将以这样的方式构造代码，使所有与单利相关的功能都在 <code>simpleinterest</code> 包中。为此，我们需要创建一个自定义包 <code>simpleinterest</code>，其中将包含用于计算单利的函数。在创建自定义包之前，我们首先需要理解 Go 模块，因为创建自定义 packages 需要用到 <strong>Go 模块</strong>。</p><p><strong>Go 模块不过是 Go 软件包的集合。<strong>现在你可能会想到一个问题：为什么我们需要 Go 模块来创建自定义包？答案是</strong>我们创建的自定义包的导入路径是从 go 模块的名称获得的</strong>。除此之外，我们的应用程序使用的所有其他第三方软件包（例如来自 github 的 源代码）将与版本一起出现在 <code>go.mod</code> 文件中。这个 <code>go.mod</code> 文件是在我们创建一个新模块时创建的。在下一部分中，你将更好地理解这一点。</p><h2 id="创建一个单利自定义包"><a href="#创建一个单利自定义包" class="headerlink" title="创建一个单利自定义包"></a>创建一个单利自定义包</h2><p><strong>属于包的源文件应放置在各自的单独文件夹中。Go 中的惯例是使用与包相同的名称来命名此文件夹。</strong></p><p>让我们在 <code>learnpackage</code> 文件夹内创建一个名为 <code>simpleinterest</code> 的文件夹。<code>mkdir simpleinterest</code> 将为我们创建此文件夹。</p><p><code>simpleinterest</code> 文件夹中的所有文件均应以 <code>package simpleinterest</code> 开头，因为它们都属于 <code>simpleinterest</code> 软件包。</p><p>在 <code>simpleinterest</code> 文件夹内创建一个文件 <code>simpleinterest.go</code>。</p><p>以下将是我们应用程序的目录结构。</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">├── learnpackage</span><br><span class="line">│   ├── go.mod</span><br><span class="line">│   ├── main.go</span><br><span class="line">│   └── simpleinterest</span><br><span class="line">│       └── simpleinterest.go</span><br></pre></td></tr></table></figure><p>添加下面的代码到 <code>simpleinterest.go</code> 文件中。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> simpleinterest</span><br><span class="line"></span><br><span class="line"><span class="comment">// 计算并返回本金 p 的单利，持续时间 t 年的利率 r</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">Calculate</span><span class="params">(p <span class="type">float64</span>, r <span class="type">float64</span>, t <span class="type">float64</span>)</span></span> <span class="type">float64</span> &#123;</span><br><span class="line">    interest := p * (r / <span class="number">100</span>) * t</span><br><span class="line">    <span class="keyword">return</span> interest</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在上面的代码中，我们创建了一个函数 <code>Calculate</code>，该函数计算并返回单利。此功能不言自明，它计算并返回单利。</p><p>请注意，函数名称 <strong>Calculate</strong> 以大写字母开头。这是必不可少的，我们后边会解释为什么需要这样做。</p><h2 id="导入自定义包"><a href="#导入自定义包" class="headerlink" title="导入自定义包"></a>导入自定义包</h2><p>要使用自定义包，我们必须首先将其导入。导入路径是模块的名称，该名称后跟的是程序包的子目录和程序包名称。在我们的例子中，模块名称为 <code>learnpackage</code>，而软件包 <code>simpleinterest</code> 位于 <code>learnpackage</code> 下的 <code>simpleinterest</code> 文件夹中。</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">├── learnpackage</span><br><span class="line">│   └── simpleinterest</span><br></pre></td></tr></table></figure><p>因此 <code>import &quot;learnpackage/simpleinterest&quot;</code> 这行代码将会引入 simpleinterest 包。</p><p>如果我们有这样的目录结构</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">learnpackage</span><br><span class="line">│   └── finance</span><br><span class="line">│       └── simpleinterest</span><br></pre></td></tr></table></figure><p>导入语句则应该是 <code>import &quot;learnpackage/finance/simpleinterest&quot;</code></p><p>在 <code>main.go</code> 文件中添加相应代码：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;fmt&quot;</span></span><br><span class="line">    <span class="string">&quot;learnpackage/simpleinterest&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    fmt.Println(<span class="string">&quot;Simple interest calculation&quot;</span>)</span><br><span class="line">    p := <span class="number">5000.0</span></span><br><span class="line">    r := <span class="number">10.0</span></span><br><span class="line">    t := <span class="number">1.0</span></span><br><span class="line">    si := simpleinterest.Calculate(p, r, t)</span><br><span class="line">    fmt.Println(<span class="string">&quot;Simple interest is&quot;</span>, si)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>上面的代码导入 <code>simpleinterest</code> 包，并使用 <code>Calculate</code> 函数计算单利。标准库中的软件包不需要模块名称前缀，因此 <code>fmt</code> 可以在没有模块前缀的情况下工作。当应用程序运行时，输出将是：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">Simple interest calculation</span><br><span class="line">Simple interest is 500</span><br></pre></td></tr></table></figure><h2 id="导出名称"><a href="#导出名称" class="headerlink" title="导出名称"></a>导出名称</h2><p>我们在单利包中将函数 <code>Calculate</code> 大写。这在 Go 中具有特殊含义。 任何以大写字母开头的变量或函数都会在 go 中导出名称。在 Go 中只能从其他程序包访问导出的函数和变量。在我们的例子中，我们想从主程序包访问 <code>Calculate</code> 函数。 因此，这是大写的。</p><p>如果在 <code>simpleinterest.go</code> 中将函数名称从 <code>Calculate</code> 更改为 <code>calculate</code>，并且如果我们尝试在 <code>main.go</code> 中使用<code>simpleinterest.calculate(p，r，t)</code> 调用函数，则编译器将报错：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"># learnpackage</span><br><span class="line">./main.go:13:8: undefined: simpleinterest.Calculate</span><br></pre></td></tr></table></figure><p>因此，如果要访问包外部的函数，应该将其大写。</p><h2 id="init-函数"><a href="#init-函数" class="headerlink" title="init 函数"></a><code>init</code> 函数</h2><p>Go 中的每个包都可以包含一个 <code>init</code> 函数。<code>init</code> 函数不能有任何返回类型，也不能有任何参数。在我们的源代码中不能显式调用 <code>init</code> 函数。程序包初始化时将自动调用它。初始化函数的语法：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">init</span><span class="params">()</span></span> &#123;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>init</code> 函数可用于执行初始化任务，也可用于在执行开始之前验证程序的正确性。包的初始化顺序如下：</p><ol><li>首先对包级的变量进行初始化</li><li>接下来调用 <code>init</code> 函数。包可以具有多个 <code>init</code> 函数（在单个文件中或分布在多个文件中），并且按将其呈现给编译器的顺序进行调用。</li></ol><p>如果一个包导入了其他包，则首先初始化导入的软件包。</p><p>一个包即使从多个包中导入，也只会被初始化一次。</p><p>让我们对应用程序进行一些修改，以了解 <code>init</code> 函数。</p><p>首先，让我们将 <code>init</code> 函数添加到 <code>simpleinterest.go</code> 文件中。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> simpleinterest</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> <span class="string">&quot;fmt&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment">/*</span></span><br><span class="line"><span class="comment"> * 添加 init 函数</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">init</span><span class="params">()</span></span> &#123;</span><br><span class="line">    fmt.Println(<span class="string">&quot;Simple interest package initialized&quot;</span>)</span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">// Calculate 计算并返回本金 p 的单利，持续时间 t 年的利率 r</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">Calculate</span><span class="params">(p <span class="type">float64</span>, r <span class="type">float64</span>, t <span class="type">float64</span>)</span></span> <span class="type">float64</span> &#123;</span><br><span class="line">    interest := p * (r / <span class="number">100</span>) * t</span><br><span class="line">    <span class="keyword">return</span> interest</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>我们添加了一个简单的 <code>init</code> 函数，该函数仅打印 已初始化的单利包</p><p>现在，让我们修改主程序包。我们知道，在计算单利时，本金、利率和持续时间应大于零。我们将使用 <code>init</code> 函数和 <code>main.go</code> 文件中的包级别变量定义此检查。</p><p>将 <code>main.go</code> 修改为以下内容：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;fmt&quot;</span></span><br><span class="line">    <span class="string">&quot;learnpackage/simpleinterest&quot;</span> <span class="comment">// 导入自定义的包</span></span><br><span class="line">    <span class="string">&quot;log&quot;</span></span><br><span class="line">)</span><br><span class="line"><span class="keyword">var</span> p, r, t = <span class="number">5000.0</span>, <span class="number">10.0</span>, <span class="number">1.0</span></span><br><span class="line"></span><br><span class="line"><span class="comment">/*</span></span><br><span class="line"><span class="comment">* init 函数检查 p, r 和 t 是否大于 0</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">init</span><span class="params">()</span></span> &#123;</span><br><span class="line">    <span class="built_in">println</span>(<span class="string">&quot;Main package initialized&quot;</span>)</span><br><span class="line">    <span class="keyword">if</span> p &lt; <span class="number">0</span> &#123;</span><br><span class="line">        log.Fatal(<span class="string">&quot;Principal is less than zero&quot;</span>)</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">if</span> r &lt; <span class="number">0</span> &#123;</span><br><span class="line">        log.Fatal(<span class="string">&quot;Rate of interest is less than zero&quot;</span>)</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">if</span> t &lt; <span class="number">0</span> &#123;</span><br><span class="line">        log.Fatal(<span class="string">&quot;Duration is less than zero&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="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    fmt.Println(<span class="string">&quot;Simple interest calculation&quot;</span>)</span><br><span class="line">    si := simpleinterest.Calculate(p, r, t)</span><br><span class="line">    fmt.Println(<span class="string">&quot;Simple interest is&quot;</span>, si)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>以下是对 <code>main.go</code> 所做的更改</p><ol><li><strong>p</strong>、<strong>r</strong> 和 ** t ** 变量从 <code>main</code> 函数级移至包级的。 2.增加了 <code>init</code> 函数。如果本金、利率或持续时间小于零，则使用 <code>init</code> 函数打印日志并终止程序执行。</li></ol><p>初始化的顺序如下：</p><ol><li>首先初始化导入的软件包。因此，首先初始化 <code>simpleinterest</code> 包，然后调用它的 <code>init</code> 方法。</li><li>接下来初始化包级别变量 <code>p</code>，<code>r</code> 和 <code>t</code>。</li><li><code>init</code> 函数在 <code>main</code> 中调用。 4.最后调用 <code>main</code> 函数。</li></ol><p>如果运行该程序，将得到以下输出：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">Simple interest package initialized</span><br><span class="line">Main package initialized</span><br><span class="line">Simple interest calculation</span><br><span class="line">Simple interest is 500</span><br></pre></td></tr></table></figure><p>如预期的那样，首先调用 <code>simpleinterest</code> 包的 <code>init</code> 函数，然后初始化包级变量 <code>p</code>，<code>r</code> 和 <code>t</code>。接下来调用 <code>main</code> 包的 <code>init</code> 函数。它检查<code>p</code>、<code>r</code> 和 <code>t</code> 是否小于零，并在条件为真时终止。现在你可以假定 <code>if p &lt;0</code> 将检查 <code>p</code> 是否小于 <code>0</code>，如果小于<code>0</code>，则程序将终止。我们为 <code>r</code> 和 <code>t</code> 写了类似的条件。在这种情况下，所有这些条件都是假的，程序将继续执行。最后，调用 <code>main</code> 函数。</p><p>让我们对该程序进行一些修改以学习 <code>init</code> 函数的用法。修改 <code>main.go</code> 这一行</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> p, r, t = <span class="number">5000.0</span>, <span class="number">10.0</span>, <span class="number">1.0</span></span><br></pre></td></tr></table></figure><p>为</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> p, r, t = <span class="number">-5000.0</span>, <span class="number">10.0</span>, <span class="number">1.0</span></span><br></pre></td></tr></table></figure><p>我们已经将 <code>p</code> 初始化为负数。</p><p>现在，如果你运行该应用程序，你将看到</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">Simple interest package initialized</span><br><span class="line">Main package initialized</span><br><span class="line">2020/02/15 21:25:12 Principal is less than zero</span><br></pre></td></tr></table></figure><p><code>p</code> 是负数。因此，当 <code>init</code> 函数运行时，程序在打印 <code>Principal is less than zero</code> 后终止。</p><h2 id="空白标识符的使用"><a href="#空白标识符的使用" class="headerlink" title="空白标识符的使用"></a>空白标识符的使用</h2><p>在 Go 中导入包却不使用是不合法的。如果这样做，编译器会抱怨。这样设计的原因是避免使未使用的包越来越多，这会大大增加编译时间。用以下内容替换 <code>main.go</code> 中的代码，</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;learnpackage/simpleinterest&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>上面的程序将会抛错：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&quot;learnpackage/simpleinterest&quot; imported but not used</span><br></pre></td></tr></table></figure><p>但是，在应用程序处于开发阶段中时，导入包并随后在代码中的某个位置（如果不是现在）使用它们是很常见的。在这种情况下，<code>_</code> 空白标识符可以为我们节省时间。</p><p>上面的程序中的错误可以通过以下代码消除：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">  <span class="string">&quot;learnpackage/simpleinterest&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> _ = simpleinterest.Calculate</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>var _ = simpleinterest.Calculate</code> 使错误静音了。如果不使用包，我们应该跟踪这些错误消音器，并在应用程序开发结束时删除它们，包括导入的包。因此，建议在 <code>import</code> 语句之后在包级别编写错误消音器。</p><p>有时，即使我们不需要使用包中的任何函数或变量，也需要导入包以确保初始化发生。例如，即使我们计划在代码中的任何地方都不使用该包，我们可能仍需要确保调用 <code>simpleinterest</code> 包的 <code>init</code> 函数。空白标识符 <code>_</code> 也可以在这种情况下使用，如下所示。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    _ <span class="string">&quot;learnpackage/simpleinterest&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>运行以上程序将输出 <code>Simple interest package initialized</code>。我们已经成功地初始化了 <code>simpleinterest</code> 包，即使在代码中的任何地方都没有使用它。</p><p>在第 13 行中，我们仅使用 <code>area</code>，而使用 <code>_</code> 标识符来丢弃 <code>perimeter</code>。</p><blockquote><p>原文地址 <a href="https://golangbot.com/go-packages/">Go Packages</a><br>原文作者：<a href="https://golangbot.com/about/">Naveen Ramanathan</a><br>译文出自：<a href="https://blog.zisheng.pro/categories/%E6%B4%9B%E7%AB%B9%E7%BF%BB%E8%AF%91%E8%AE%A1%E5%88%92/">紫升翻译计划</a></p></blockquote>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/588813e2ac19/</id>
    <link href="https://blog.zisheng.pro/posts/588813e2ac19/"/>
    <published>2023-03-19T07:07:00.000Z</published>
    <summary>包用于组织 Go 源代码，以提高可重用性和可读性。包是位于同一目录中的 Go 源文件的集合。包提供了代码分隔，因此我们可以轻松维护 Go 项目。</summary>
    <title>Go 语言教程系列の包</title>
    <updated>2026-09-18T08:15:28.629Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="紫升翻译计划" scheme="https://blog.zisheng.pro/categories/%E7%B4%AB%E5%8D%87%E7%BF%BB%E8%AF%91%E8%AE%A1%E5%88%92/"/>
    <category term="Go" scheme="https://blog.zisheng.pro/categories/Go/"/>
    <category term="Go" scheme="https://blog.zisheng.pro/tags/Go/"/>
    <category term="Go 教程" scheme="https://blog.zisheng.pro/tags/Go-%E6%95%99%E7%A8%8B/"/>
    <category term="Golang 函数" scheme="https://blog.zisheng.pro/tags/Golang-%E5%87%BD%E6%95%B0/"/>
    <content>
      <![CDATA[<h2 id="函数是什么？"><a href="#函数是什么？" class="headerlink" title="函数是什么？"></a>函数是什么？</h2><p>函数是执行特定任务的代码块。一个函数接受一个输入，对该输入执行一些计算，然后生成一个输出。</p><h2 id="函数声明"><a href="#函数声明" class="headerlink" title="函数声明"></a>函数声明</h2><p>Go 语言声明一个函数的语法是：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">functionname</span><span class="params">(parametername <span class="keyword">type</span>)</span></span> returntype &#123;</span><br><span class="line"> <span class="comment">// 函数体</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>函数声明以关键字 <code>func</code> 开头，后跟 <code>functionname</code>。参数在函数 <code>(</code> 和 <code>)</code> 之间指定，然后跟着的是 函数的 <code>returntype</code>。指定参数的语法是参数名称后跟类型。可以指定任意数量的参数，例如 <code>(parameter1 type，parameter2 type)</code>。然后在 <code>{</code> 和 <code>}</code> 之间有一个代码块，它是函数的主体。</p><p>参数和返回类型在函数中是可选的。因此，以下语法也是有效的函数声明。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">functionname</span><span class="params">()</span></span> &#123;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="示例"><a href="#示例" class="headerlink" title="示例"></a>示例</h2><p>让我们编写一个函数，该函数将单个产品的价格和产品数量作为输入参数，并通过将这两个值相乘来计算总价格并返回输出。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">calculateBill</span><span class="params">(price <span class="type">int</span>, no <span class="type">int</span>)</span></span> <span class="type">int</span> &#123;</span><br><span class="line">    <span class="keyword">var</span> totalPrice = price * no</span><br><span class="line">    <span class="keyword">return</span> totalPrice</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>上面的函数有两个输入参数 <code>int</code> 类型的 <code>price</code> 和 <code>no</code>，并返回 <code>price</code> 和 <code>no</code> 的乘积 <code> totalPrice</code>。返回值也是 <code>int</code> 类型。</p><p><strong>如果连续参数具有相同的类型，我们可以不用重复书写该类型，事实上你只需要写一次就足够了。</strong> 因此，上述功能可以重写为：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">calculateBill</span><span class="params">(price, no <span class="type">int</span>)</span></span> <span class="type">int</span> &#123;</span><br><span class="line">    <span class="keyword">var</span> totalPrice = price * no</span><br><span class="line">    <span class="keyword">return</span> totalPrice</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>现在我们已经准备好一个函数，让我们从代码中的某个地方调用它。 调用函数的语法是 <code>functionname(parameters)</code>。现在我们可以使用代码来调用上述函数。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">calculateBill(<span class="number">10</span>, <span class="number">5</span>)</span><br></pre></td></tr></table></figure><p>这是使用上述函数并打印总价格的完整程序。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;fmt&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">calculateBill</span><span class="params">(price, no <span class="type">int</span>)</span></span> <span class="type">int</span> &#123;</span><br><span class="line">    <span class="keyword">var</span> totalPrice = price * no</span><br><span class="line">    <span class="keyword">return</span> totalPrice</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    price, no := <span class="number">90</span>, <span class="number">6</span></span><br><span class="line">    totalPrice := calculateBill(price, no)</span><br><span class="line">    fmt.Println(<span class="string">&quot;Total price is&quot;</span>, totalPrice)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/FtjhPcx3ySa">Run in playground</a></p><p>上面的程序将打印出以下内容：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Total price is 540</span><br></pre></td></tr></table></figure><h2 id="多个返回值"><a href="#多个返回值" class="headerlink" title="多个返回值"></a>多个返回值</h2><p>可以从一个函数返回多个值。让我们编写一个函数 <code>rectProps</code>，该函数接受矩形的 <code>length</code> 和 <code>width</code>，并返回矩形的 <code>area</code> 和 <code>perimeter</code>。矩形的面积是长度和宽度的乘积，周长是长度和宽度的总和的两倍。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;fmt&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">rectProps</span><span class="params">(length, width <span class="type">float64</span>)</span></span>(<span class="type">float64</span>, <span class="type">float64</span>) &#123;</span><br><span class="line">    <span class="keyword">var</span> area = length * width</span><br><span class="line">    <span class="keyword">var</span> perimeter = (length + width) * <span class="number">2</span></span><br><span class="line">    <span class="keyword">return</span> area, perimeter</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    area, perimeter := rectProps(<span class="number">10.8</span>, <span class="number">5.6</span>)</span><br><span class="line">    fmt.Printf(<span class="string">&quot;Area %f Perimeter %f&quot;</span>, area, perimeter)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/qAftE_yke_">Run in playground</a></p><p>如果一个函数返回多个返回值，那么必须在 <code>(</code> 和 <code>)</code> 之间指定它们的类型。 <code>func rectProps(length, width float64)(float64,float64)</code> 具有两个<code>float64</code> 参数 <code>length</code> 和 <code>width</code>，并且还返回两个 <code>float64</code> 值。上面的程序会打印出：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Area 60.480000 Perimeter 32.800000</span><br></pre></td></tr></table></figure><h2 id="命名返回值"><a href="#命名返回值" class="headerlink" title="命名返回值"></a>命名返回值</h2><p>可以从函数返回命名值。如果已命名返回值，则可以将其视为在函数的第一行中声明为变量。</p><p>上面的 <code>rectProps</code> 函数可以使用命名的返回值重写为</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">rectProps</span><span class="params">(length, width <span class="type">float64</span>)</span></span>(area, perimeter <span class="type">float64</span>) &#123;</span><br><span class="line">    area = length * width</span><br><span class="line">    perimeter = (length + width) * <span class="number">2</span></span><br><span class="line">    <span class="keyword">return</span> <span class="comment">// 无需明确指定返回值</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>area</code> 和 <code>perimeter </code> 是上述函数中的命名返回值。请注意，函数中的<code>return</code> 语句不会显式返回任何值。由于在函数声明中将 <code>area</code> 和 <code> perimeter</code> 指定为返回值，因此当遇到 <code>return</code> 语句时，它们会自动从函数中返回。</p><h2 id="空白标识符"><a href="#空白标识符" class="headerlink" title="空白标识符"></a>空白标识符</h2><p><code>_</code> 是 Go 语言中的空白标识符。它可以代替任何类型的任何值。让我们看看这个空白标识符的用途。</p><p><code>rectProps</code> 函数返回矩形的面积和周长。如果我们只需要 <code>area</code> 而想要丢弃 <code>perimeter</code>，该怎么办。这时  <code>_</code> 就可以派上用场。</p><p>下面的程序仅使用从 <code>rectProps</code> 函数返回的 <code>area</code>。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;fmt&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">rectProps</span><span class="params">(length, width <span class="type">float64</span>)</span></span> (<span class="type">float64</span>, <span class="type">float64</span>) &#123;</span><br><span class="line">    <span class="keyword">var</span> area = length * width</span><br><span class="line">    <span class="keyword">var</span> perimeter = (length + width) * <span class="number">2</span></span><br><span class="line">    <span class="keyword">return</span> area, perimeter</span><br><span class="line">&#125;</span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    area, _ := rectProps(<span class="number">10.8</span>, <span class="number">5.6</span>) <span class="comment">// perimeter 被丢弃</span></span><br><span class="line">    fmt.Printf(<span class="string">&quot;Area %f &quot;</span>, area)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/IkugSH1jIt">Run in playground</a></p><p>在第 13 行中，我们仅使用 <code>area</code>，而使用 <code>_</code> 标识符来丢弃 <code>perimeter</code>。</p><blockquote><p>原文地址 <a href="https://golangbot.com/functions/">Functions</a><br>原文作者：<a href="https://golangbot.com/about/">Naveen Ramanathan</a><br>译文出自：<a href="https://blog.zisheng.pro/categories/%E6%B4%9B%E7%AB%B9%E7%BF%BB%E8%AF%91%E8%AE%A1%E5%88%92/">紫升翻译计划</a></p></blockquote>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/e59d3fd533b9/</id>
    <link href="https://blog.zisheng.pro/posts/e59d3fd533b9/"/>
    <published>2023-03-19T07:06:00.000Z</published>
    <summary>函数是执行特定任务的代码块。一个函数接受一个输入，对该输入执行一些计算，然后生成一个输出。</summary>
    <title>Go 语言教程系列の函数</title>
    <updated>2026-09-18T08:15:28.629Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="紫升翻译计划" scheme="https://blog.zisheng.pro/categories/%E7%B4%AB%E5%8D%87%E7%BF%BB%E8%AF%91%E8%AE%A1%E5%88%92/"/>
    <category term="Go" scheme="https://blog.zisheng.pro/categories/Go/"/>
    <category term="Go" scheme="https://blog.zisheng.pro/tags/Go/"/>
    <category term="Go 教程" scheme="https://blog.zisheng.pro/tags/Go-%E6%95%99%E7%A8%8B/"/>
    <category term="Go 常量" scheme="https://blog.zisheng.pro/tags/Go-%E5%B8%B8%E9%87%8F/"/>
    <content>
      <![CDATA[<h2 id="常量是什么？"><a href="#常量是什么？" class="headerlink" title="常量是什么？"></a>常量是什么？</h2><p>Go 中的常量一词用于表示固定值，例如：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">95</span><br><span class="line"><span class="string">&quot;I love Go&quot;</span></span><br><span class="line">67.89</span><br></pre></td></tr></table></figure><h2 id="声明一个常量"><a href="#声明一个常量" class="headerlink" title="声明一个常量"></a>声明一个常量</h2><p>关键字 <code>const</code> 用于声明常量。让我们看看如何使用 <code>const</code> 来声明一个常量。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;fmt&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    <span class="keyword">const</span> a = <span class="number">50</span></span><br><span class="line">    fmt.Println(a)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/mv3B-q3h0zh">Run in playground</a></p><p>上面的代码中，<code>a</code> 是一个常量并被赋值为 <code>50</code>。</p><h2 id="声明一组常量"><a href="#声明一组常量" class="headerlink" title="声明一组常量"></a>声明一组常量</h2><p>还有另一种语法可以使用单个语句定义一组常量。下面提供了使用此语法定义一组常量的示例。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;fmt&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    <span class="keyword">const</span> (</span><br><span class="line">        name = <span class="string">&quot;John&quot;</span></span><br><span class="line">        age = <span class="number">50</span></span><br><span class="line">        country = <span class="string">&quot;Canada&quot;</span></span><br><span class="line">    )</span><br><span class="line">    fmt.Println(name)</span><br><span class="line">    fmt.Println(age)</span><br><span class="line">    fmt.Println(country)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/KvZ6zNz4A04">Run in playground</a></p><p>在上面的程序中，我们声明了 3 个常量 <code>name</code>、<code>age</code> 和 <code>country</code>。 执行上面的程序会打印出以下内容：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">John</span><br><span class="line">50</span><br><span class="line">Canada</span><br></pre></td></tr></table></figure><p>顾名思义，常量不能再次重新分配任何其他值。在下面的程序中，我们试图将另一个值 <code>89</code> 分配给 <code>a</code>。这是不允许的，因为 <code>a</code> 是常量。 该程序将无法运行，并显示编译错误 <code>cannot assign to a</code>。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line"><span class="keyword">const</span> a = <span class="number">55</span> <span class="comment">//allowed</span></span><br><span class="line">a = <span class="number">89</span> <span class="comment">//reassignment not allowed</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/b2J8_UQobb">Run in playground</a></p><p>常量的值应在编译时确定。因此，由于函数调用在运行时发生，因此不能将函数调用返回的值分配给常量。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;math&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    <span class="keyword">var</span> a = math.Sqrt(<span class="number">4</span>)   <span class="comment">//allowed</span></span><br><span class="line">    <span class="keyword">const</span> b = math.Sqrt(<span class="number">4</span>) <span class="comment">//not allowed</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/GBlQDcbqfn-">Run in playground</a></p><p>在上面的程序中，<code>a</code> 是一个变量，因此可以将函数 <code>math.Sqrt(4)</code> 的返回值分配给它（我们将在单独的教程中更详细地讨论函数）。</p><p><code>b</code> 是一个常量，在编译时必须确定 <code>b</code> 的值。而 <code>math.Sqrt(4)</code> 函数是在运行时确定返回值，因此 <code>const b = math.Sqrt(4)</code> 编译失败并显示错误：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">./prog.go:9:8: const initializer math.Sqrt(4) is not a constant</span><br></pre></td></tr></table></figure><h2 id="字符串常量、有类型常量和无类型常量"><a href="#字符串常量、有类型常量和无类型常量" class="headerlink" title="字符串常量、有类型常量和无类型常量"></a>字符串常量、有类型常量和无类型常量</h2><p>在 Go 中，双引号之间包含的任何值都是字符串常量。 例如，<code>&quot;Hello World&quot;</code>，<code>&quot;Sam&quot;</code> 之类的字符串在 Go 中都是常量。</p><p>字符串常量属于什么类型？ 答案是它们是无类型的。</p><p>像 <code>&quot;Hello World&quot;</code> 这样的字符串常量没有任何类型。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> hello = <span class="string">&quot;Hello World&quot;</span></span><br></pre></td></tr></table></figure><p>在上面的代码行中，常量 <code>&quot;hello&quot;</code> 没有类型。</p><p>Go 是一种强类型语言。所有变量都需要显式类型。试想一下，以下程序将无类型的常量 <code>n</code> 的值分配给变量 <code>name</code> 会发生什么？</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;fmt&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    <span class="keyword">const</span> n = <span class="string">&quot;Sam&quot;</span></span><br><span class="line">    <span class="keyword">var</span> name = n</span><br><span class="line">    fmt.Printf(<span class="string">&quot;type %T value %v&quot;</span>, name, name)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/oFv_cFuEucL">Run in playground</a></p><p>答案是未类型的常量具有与之关联的默认类型，并且仅当一行代码需要它时，它们才提供默认类型。在 <code>var name = n</code> 的语句中，<code>name</code> 需要一个类型，它是从字符串常量 <code>n</code> 的默认类型（即字符串）中获取的。</p><p>有没有办法创建有类型的常量呢？答案是肯定的。下面的代码会创建一个有类型的常量。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> typedhello <span class="type">string</span> = <span class="string">&quot;Hello World&quot;</span></span><br></pre></td></tr></table></figure><p><strong>上面的代码中的 <code>typedhello</code> 是字符串类型的常量。</strong></p><p>Go 是一种强类型语言。分配期间不允许混合类型。让我们看看下面的程序来理解下这意味着什么。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    <span class="keyword">var</span> defaultName = <span class="string">&quot;Sam&quot;</span> <span class="comment">//allowed</span></span><br><span class="line">    <span class="keyword">type</span> myString <span class="type">string</span></span><br><span class="line">    <span class="keyword">var</span> customName myString = <span class="string">&quot;Sam&quot;</span> <span class="comment">//allowed</span></span><br><span class="line">    customName = defaultName <span class="comment">//not allowed</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/1Q-vudNn_9">Run in playground</a></p><p>在上面的代码中，我们首先创建一个变量 <code>defaultName</code>，并将常量 <code>Sam</code> 分配给它。<strong>常量 <code>Sam</code> 的默认类型是 <code>string</code>，因此 <code>defaultName</code> 赋值后的类型是 <code>string</code>。</strong>.</p><p>在下一行中，我们创建一个新类型 <code>myString</code>，它是字符串类型的别名。</p><p>然后，我们创建一个类型为 <code>myString</code> 的变量 <code>customName</code>，并为其分配常量 <code>&quot;Sam&quot;</code>。由于常量 <code>&quot;Sam&quot;</code> 是无类型的，因此可以将其分配给任何 <code>string</code> 变量。Go 允许这种分配，并且 <code>customName</code> 的类型为 <code>myString</code>。</p><p>现在，我们有一个字符串类型的变量 <code>defaultName</code> 和另一个 <code>myString</code> 类型的变量 <code>customName</code>。即使我们知道 <code>myString</code> 是字符串的别名，Go 的强类型策略也不允许将一种类型的变量分配给另一种类型。因此，不允许 <code>customName = defaultName</code> 这种分配，并且编译器将引发错误 <code>./prog.go:7:20: cannot use defaultName (type string) as type myString in assignment</code>。</p><h2 id="布尔常量"><a href="#布尔常量" class="headerlink" title="布尔常量"></a>布尔常量</h2><p>布尔常量与字符串常量没有什么不同。它们是两个无类型的常量 <code>true</code> 和 <code>false</code>。字符串常量的相同规则也适用于布尔值，因此在此不再赘述。以下是一个解释布尔常量的简单程序：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    <span class="keyword">const</span> trueConst = <span class="literal">true</span></span><br><span class="line">    <span class="keyword">type</span> myBool <span class="type">bool</span></span><br><span class="line">    <span class="keyword">var</span> defaultBool = trueConst <span class="comment">//allowed</span></span><br><span class="line">    <span class="keyword">var</span> customBool myBool = trueConst <span class="comment">//allowed</span></span><br><span class="line">    defaultBool = customBool <span class="comment">//not allowed</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/h9yzC6RxOR">Run in playground</a></p><p>上面的程序的意思是不言而喻的。</p><h2 id="数字常量"><a href="#数字常量" class="headerlink" title="数字常量"></a>数字常量</h2><p>数字常量包含整形、浮点型和复数常量。这些概念使得数字常量很容易混淆。让我们看一些示例以使事情变得清楚。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;fmt&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    <span class="keyword">const</span> a = <span class="number">5</span></span><br><span class="line">    <span class="keyword">var</span> intVar <span class="type">int</span> = a</span><br><span class="line">    <span class="keyword">var</span> int32Var <span class="type">int32</span> = a</span><br><span class="line">    <span class="keyword">var</span> float64Var <span class="type">float64</span> = a</span><br><span class="line">    <span class="keyword">var</span> complex64Var <span class="type">complex64</span> = a</span><br><span class="line">    fmt.Println(<span class="string">&quot;intVar&quot;</span>,intVar, <span class="string">&quot;\nint32Var&quot;</span>, int32Var, <span class="string">&quot;\nfloat64Var&quot;</span>, float64Var, <span class="string">&quot;\ncomplex64Var&quot;</span>,complex64Var)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/a8sxVNdU8M">Run in playground</a></p><p>在上面的程序中，常量 <code>a</code> 是无类型的，其值是 5。你可能想知道 <code>a</code> 的默认类型是什么，如果它确实有一个，那么我们如何将其分配给不同类型的变量。答案在于 <code>a</code> 的语法。以下程序将使事情更加清楚。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;fmt&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    <span class="keyword">var</span> i = <span class="number">5</span></span><br><span class="line">    <span class="keyword">var</span> f = <span class="number">5.6</span></span><br><span class="line">    <span class="keyword">var</span> c = <span class="number">5</span> + <span class="number">6i</span></span><br><span class="line">    fmt.Printf(<span class="string">&quot;i&#x27;s type is %T, f&#x27;s type is %T, c&#x27;s type is %T&quot;</span>, i, f, c)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/0-eVCbJ76B5">Run in playground</a></p><p>在上面的程序中，每个变量的类型由数字常量的语法确定。从语法上看，<code>5</code> 是整数，<code>5.6</code> 是浮点数，而 <code>5 + 6i</code> 是复数。当上面的程序运行时，它会打印：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">i<span class="string">&#x27;s type is int, f&#x27;</span>s <span class="built_in">type</span> is float64, c<span class="string">&#x27;s type is complex128</span></span><br></pre></td></tr></table></figure><p>有了这些知识，让我们尝试了解以下程序的运行原理。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;fmt&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    <span class="keyword">const</span> a = <span class="number">5</span></span><br><span class="line">    <span class="keyword">var</span> intVar <span class="type">int</span> = a</span><br><span class="line">    <span class="keyword">var</span> int32Var <span class="type">int32</span> = a</span><br><span class="line">    <span class="keyword">var</span> float64Var <span class="type">float64</span> = a</span><br><span class="line">    <span class="keyword">var</span> complex64Var <span class="type">complex64</span> = a</span><br><span class="line">    fmt.Println(<span class="string">&quot;intVar&quot;</span>,intVar, <span class="string">&quot;\nint32Var&quot;</span>, int32Var, <span class="string">&quot;\nfloat64Var&quot;</span>, float64Var, <span class="string">&quot;\ncomplex64Var&quot;</span>,complex64Var)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/_zu0iK-Hyj">Run in playground</a></p><p>在上面的程序中，<code>a</code> 的值是 <code>5</code>，而 <code>a</code> 的语法是通用的。它可以表示浮点数，整数或者是没有虚部的复数。 因此，可以将其分配给任何兼容类型。这些常量的默认类型可以被认为是根据上下文动态生成的。 <code>var complex64Var complex64 = a</code> 要求 <code>a</code> 是一个复数，因此它成为一个复数常数。</p><h2 id="数字表达式"><a href="#数字表达式" class="headerlink" title="数字表达式"></a>数字表达式</h2><p>数字常量可以在表达式中自由混合和匹配，并且仅当将它们分配给变量或在代码中需要类型的任何位置使用时，才需要类型。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;fmt&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    <span class="keyword">var</span> a = <span class="number">5.9</span> / <span class="number">8</span></span><br><span class="line">    fmt.Printf(<span class="string">&quot;a&#x27;s type is %T and value is %v&quot;</span>, a, a)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/Nsak9scUAWg">Run in playground</a></p><p>在上面的程序中，<code>5.9</code> 是语法上的浮点数，<code>8</code> 是语法上的整数。不过，仍允许使用 <code>5.9 / 8</code>，这是因为它们都是数字常量。除法的结果是<code>0.7375</code> 是 <code>float</code>，因此变量 <code>a</code> 是 <code>float</code> 类型。该程序的输出是：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">a<span class="string">&#x27;s type is float64 and value is 0.7375</span></span><br></pre></td></tr></table></figure><blockquote><p>原文地址 <a href="https://golangbot.com/constants/">Constants</a><br>原文作者：<a href="https://golangbot.com/about/">Naveen Ramanathan</a><br>译文出自：<a href="https://blog.zisheng.pro/categories/%E6%B4%9B%E7%AB%B9%E7%BF%BB%E8%AF%91%E8%AE%A1%E5%88%92/">紫升翻译计划</a></p></blockquote>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/84e9c8841ae1/</id>
    <link href="https://blog.zisheng.pro/posts/84e9c8841ae1/"/>
    <published>2023-03-19T07:05:00.000Z</published>
    <summary>Go 中的常量一词用于表示固定值</summary>
    <title>Go 语言教程系列の常量</title>
    <updated>2026-09-18T08:15:28.629Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="紫升翻译计划" scheme="https://blog.zisheng.pro/categories/%E7%B4%AB%E5%8D%87%E7%BF%BB%E8%AF%91%E8%AE%A1%E5%88%92/"/>
    <category term="Go" scheme="https://blog.zisheng.pro/categories/Go/"/>
    <category term="Go" scheme="https://blog.zisheng.pro/tags/Go/"/>
    <category term="Go 教程" scheme="https://blog.zisheng.pro/tags/Go-%E6%95%99%E7%A8%8B/"/>
    <category term="Go 数据类型" scheme="https://blog.zisheng.pro/tags/Go-%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B/"/>
    <content>
      <![CDATA[<p>下面是 Go 语言中可用的基础类型：</p><ul><li>bool</li><li>Numeric Types</li><li>int8, int16, int32, int64, int</li><li>uint8, uint16, uint32, uint64, uint</li><li>float32, float64</li><li>complex64, complex128</li><li>byte</li><li>rune</li><li>string</li></ul><h2 id="bool"><a href="#bool" class="headerlink" title="bool"></a>bool</h2><p><code>bool</code> 表示 <code>boolean</code> 类型，用来表达 <code>true</code> 或 <code>false</code> 的。</p><blockquote><p>生存还是毁灭，这是一个问题！</p></blockquote><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> <span class="string">&quot;fmt&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    a := <span class="literal">true</span></span><br><span class="line">    b := <span class="literal">false</span></span><br><span class="line">    fmt.Println(<span class="string">&quot;a:&quot;</span>, a, <span class="string">&quot;b:&quot;</span>, b)</span><br><span class="line">    c := a &amp;&amp; b</span><br><span class="line">    fmt.Println(<span class="string">&quot;c:&quot;</span>, c)</span><br><span class="line">    d := a || b</span><br><span class="line">    fmt.Println(<span class="string">&quot;d:&quot;</span>, d)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/v_W3HQ0MdY">Run in playground</a></p><p>上面的程序中，变量<code>a</code> 被分配了 <code>true</code>，变量<code>b</code> 被分配了 <code>false</code>。<code>c</code> 被分配了 <code>a &amp;&amp; b</code> 表达式的值。当 <code>a</code> 和 <code>b</code> 的值都为 <code>true</code> 时 <code>&amp;&amp;</code> 操作符返回 <code>true</code>。因此上面的变量 <code>c</code> 的值是 <code>false</code>。</p><p>当 <code>a</code> 或 <code>b</code> 的值为 <code>true</code> 时，<code>||</code> 操作符返回 <code>true</code>。在这个例子中，因为 <code>a</code> 的值是 <code>true</code>，所以变量 <code>d</code> 的值是 <code>true</code>。上面的程序执行后会输出以下内容：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">a: <span class="literal">true</span> b: <span class="literal">false</span></span><br><span class="line">c: <span class="literal">false</span></span><br><span class="line">d: <span class="literal">true</span></span><br></pre></td></tr></table></figure><h2 id="有符号整形"><a href="#有符号整形" class="headerlink" title="有符号整形"></a>有符号整形</h2><ul><li><strong>int8</strong>：表示 8 位有符号整数<ul><li><strong>size</strong>：8 位</li><li><strong>range</strong>：-128 到 127</li></ul></li><li><strong>int16</strong>：表示 16 位有符号整数<ul><li><strong>size</strong>：16  位</li><li><strong>range</strong>：-32768 到 32767</li></ul></li><li><strong>int32</strong>：表示 32 位有符号整数<ul><li><strong>size</strong>：32 位</li><li><strong>range</strong>：-2147483648 到 2147483647</li></ul></li><li><strong>int64</strong>：表示 64 位有符号整数<ul><li><strong>size</strong>：64 位</li><li><strong>range</strong>：-9223372036854775808 到 9223372036854775807</li></ul></li><li><strong>int</strong>：代表 32 或 64 位整数，具体取决于基础平台。除非需要使用特定大小的整数，否则通常应该使用 <code>int</code> 表示整数。<ul><li><strong>size</strong>：32 位操作系统中是 32 位，64 位操作系统中是 64 位。</li><li><strong>range</strong>：32 位操作系统中是 -2147483648 到 2147483647，64 位操作系统中是 -9223372036854775808 到 9223372036854775807</li></ul></li></ul><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> <span class="string">&quot;fmt&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    <span class="keyword">var</span> a <span class="type">int</span> = <span class="number">89</span></span><br><span class="line">    b := <span class="number">95</span></span><br><span class="line">    fmt.Println(<span class="string">&quot;value of a is&quot;</span>, a, <span class="string">&quot;and b is&quot;</span>, b)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/NyDPsjkma3">Run in playground</a></p><p>上面的程序会输出 <code>value of a is 89 and b is 95</code>。</p><p>在上面的程序中，<code>a</code> 是 <code>int</code> 类型，<code>b</code> 的类型是从分配给它的值中推断出来的（95）。如上所述，<code>int</code> 的大小在 32 位系统中为 32 位，在 64 位系统中为 64 位。让我们继续进行验证。</p><p>可以使用 <code>Printf</code> 函数中的 <code>％T</code> 格式说明符来打印变量的类型。Go 有一个 <a href="https://golang.org/pkg/unsafe/">unsafe</a> 包，该包拥有 <code>Sizeof</code> 函数，该函数以字节为单位返回传递给它的变量的大小。不安全的软件包应谨慎使用，因为使用它的代码可能存在可移植性问题，但是出于本教程的目的，我们可以使用它。</p><p>以下程序输出变量 <code>a</code> 和 <code>b</code> 的类型和大小。<code>％T</code> 是格式说明符，用于打印类型，而 <code>％d</code> 用于打印尺寸。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">  <span class="string">&quot;fmt&quot;</span></span><br><span class="line">  <span class="string">&quot;unsafe&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">  <span class="keyword">var</span> a <span class="type">int</span> = <span class="number">89</span></span><br><span class="line">  b := <span class="number">95</span></span><br><span class="line">  fmt.Println(<span class="string">&quot;value of a is&quot;</span>, a, <span class="string">&quot;and b is&quot;</span>, b)</span><br><span class="line">  fmt.Printf(<span class="string">&quot;type of a is %T, size of a is %d&quot;</span>, a, unsafe.Sizeof(a)) <span class="comment">//type and size of a</span></span><br><span class="line">  fmt.Printf(<span class="string">&quot;\ntype of b is %T, size of b is %d&quot;</span>, b, unsafe.Sizeof(b)) <span class="comment">//type and size of b</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/mFsmjVk5oc">Run in playground</a></p><p>上面的程序将输出以下内容：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">value of a is 89 and b is 95</span><br><span class="line"><span class="built_in">type</span> of a is int, size of a is 4</span><br><span class="line"><span class="built_in">type</span> of b is int, size of b is 4</span><br></pre></td></tr></table></figure><p>我们可以从上面的输出中推断出 <code>a</code> 和 <code>b</code> 的类型为 <code>int</code>，它们的大小为 32 位（4 个字节）。如果在 64 位系统上运行上述程序，输出将有所不同。 在 64 位系统中，<code>a</code> 和 <code>b</code> 占用 64 位（8 字节）。</p><h2 id="无符号整形"><a href="#无符号整形" class="headerlink" title="无符号整形"></a>无符号整形</h2><ul><li><strong>uint8</strong>：代表 8 位无符号整形<ul><li><strong>size</strong>：8 位</li><li><strong>range</strong>：0 到 255</li></ul></li><li><strong>uint16</strong>：代表 16 位无符号整形<ul><li><strong>size</strong>：16 位</li><li><strong>range</strong>：0 到 65535</li></ul></li><li><strong>uint32</strong>：代表 32 位无符号整形<ul><li><strong>size</strong>：32  位</li><li><strong>range</strong>：0 到 4294967295</li></ul></li><li><strong>uint64</strong>：代表 64 位无符号整形<ul><li><strong>size</strong>：64 位</li><li><strong>range</strong>：0 到 18446744073709551615</li></ul></li><li><strong>uint</strong>： 表示 32 或 64 位无符号整数，具体取决于基础平台。<ul><li><strong>size</strong>：在 32 位系统中为 32 位，在 64 位系统中为 64 位。</li><li><strong>range</strong>：在 32 位系统中是 0 到 4294967295，在 62 位操作系统中是 0 到 18446744073709551615</li></ul></li></ul><h2 id="浮点类型"><a href="#浮点类型" class="headerlink" title="浮点类型"></a>浮点类型</h2><ul><li><strong>float32</strong>：32 位浮点数</li><li><strong>float64</strong>：64 位浮点数</li></ul><p>下面是一个简单的程序，用于说明整数和浮点类型：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line"> <span class="string">&quot;fmt&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">  a, b := <span class="number">5.67</span>, <span class="number">8.97</span></span><br><span class="line">  fmt.Printf(<span class="string">&quot;type of a %T b %T\n&quot;</span>, a, b)</span><br><span class="line">  sum := a + b</span><br><span class="line">  diff := a - b</span><br><span class="line">  fmt.Println(<span class="string">&quot;sum&quot;</span>, sum, <span class="string">&quot;diff&quot;</span>, diff)</span><br><span class="line">  no1, no2 := <span class="number">56</span>, <span class="number">89</span></span><br><span class="line">  fmt.Println(<span class="string">&quot;sum&quot;</span>, no1+no2, <span class="string">&quot;diff&quot;</span>, no1-no2)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/upwUCprT-j">Run in playground</a></p><p><code>a</code> 和 <code>b</code> 的类型是从分配给它们的值推断出来的。在这种情况下，<code>a</code> 和 <code>b</code> 的类型为 <code>float64</code>。（<code>float64</code> 是浮点值的默认类型）。我们将 <code>a</code> 和 <code>b</code> 相加并将其分配给变量 <code>sum</code>。我们从 <code>a</code> 中减去 <code>b</code> 并将其分配给 <code>diff</code>。然后打印出 <code>sum</code> 和 <code>diff</code>。使用 <code>no1</code> 和 <code>no2</code> 进行类似的计算。上面的程序将打印：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">type</span> of a float64 b float64</span><br><span class="line"><span class="built_in">sum</span> 14.64 diff -3.3000000000000007</span><br><span class="line"><span class="built_in">sum</span> 145 diff -33</span><br></pre></td></tr></table></figure><h2 id="复数（complex）类型"><a href="#复数（complex）类型" class="headerlink" title="复数（complex）类型"></a>复数（complex）类型</h2><blockquote><p><a href="https://zh.wikipedia.org/wiki/%E5%A4%8D%E6%95%B0_(%E6%95%B0%E5%AD%A6)">复数</a>，为实数的延伸，它使任一多项式方程都有根。——维基百科</p></blockquote><ul><li><strong>complex64</strong>：具有 <code>float32</code> 实部和虚部的复数</li><li><strong>complex128</strong>：具有 <code>float64</code> 实部和虚部的复数</li></ul><p>内置函数 <code>complex</code> 用于构造具有实部和虚部的复数。<code>complex</code> 函数具有以下定义：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">complex</span><span class="params">(r, i FloatType)</span></span> ComplexType</span><br></pre></td></tr></table></figure><p>它以实部和虚部作为参数，并返回复数类型。实部和虚部都必须是同一类型。即 <code>float32</code> 或 <code>float64</code>。如果实部和虚部都为 <code>float32</code>，则此函数返回类型为 <code>complex64</code> 的复数值。如果实部和虚部都为 <code>float64</code> 类型，则此函数返回 <code>complex128</code> 类型的复数值：</p><p>复数可以使用短声明语法创建：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">c := <span class="number">6</span> + <span class="number">7i</span></span><br></pre></td></tr></table></figure><p>让我们来写一个小程序助于理解复数：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line"> <span class="string">&quot;fmt&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">  c1 := <span class="built_in">complex</span>(<span class="number">5</span>, <span class="number">7</span>)</span><br><span class="line">  c2 := <span class="number">8</span> + <span class="number">27i</span></span><br><span class="line">  cadd := c1 + c2</span><br><span class="line">  fmt.Println(<span class="string">&quot;sum:&quot;</span>, cadd)</span><br><span class="line">  cmul := c1 * c2</span><br><span class="line">  fmt.Println(<span class="string">&quot;product:&quot;</span>, cmul)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/kEz1uKCdKs">Run in playground</a></p><p>在上面的程序中，<code>c1</code> 和 <code>c2</code> 是两个复数。<code>c1</code> 的实部为 5，虚部为 7。 <code>c2</code> 具有实部 8 和虚部 27。<code>cadd</code> 被分配为 <code>c1</code> 和 <code>c2</code> 的和，而 <code>cmul</code> 被分配为 <code>c1</code> 和 <code>c2</code> 的乘积。该程序将输出：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sum</span>: (13+34i)</span><br><span class="line">product: (-149+191i)</span><br></pre></td></tr></table></figure><h2 id="其他数字类型"><a href="#其他数字类型" class="headerlink" title="其他数字类型"></a>其他数字类型</h2><ul><li><strong>byte</strong>：<code>uint8</code> 的别名</li><li><strong>rune</strong>：<code>int32</code> 的别名</li></ul><h2 id="字符串类型"><a href="#字符串类型" class="headerlink" title="字符串类型"></a>字符串类型</h2><p>在 Go 语言中，字符串是字节的集合。如果这个定义没有任何意义，那也没关系。现在，我们可以假设字符串是字符的集合。</p><p>让我们使用字符串类型来写一个程序：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line"> <span class="string">&quot;fmt&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">  first := <span class="string">&quot;Naveen&quot;</span></span><br><span class="line">  last := <span class="string">&quot;Ramanathan&quot;</span></span><br><span class="line">  name := first +<span class="string">&quot; &quot;</span>+ last</span><br><span class="line">  fmt.Println(<span class="string">&quot;My name is&quot;</span>,name)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/CI6phwSVel">Run in playground</a></p><p>在上面的程序中，首先分配了字符串 <code>Naveen</code>，最后分配了字符串 <code>Ramanathan</code>。可以使用 <code>+</code> 运算符将字符串连接在一起。为 <code>name</code> 分配了 <code>first</code> 的值，该值连接到一个空格，后跟一个 <code>last</code>。上面的程序将输出 <code>My name is Naveen Ramanathan</code>。</p><p>还有一些可以对字符串执行的操作。我们将在单独的教程中介绍这些内容。</p><h2 id="类型转换"><a href="#类型转换" class="headerlink" title="类型转换"></a>类型转换</h2><p>Go 对于类型明确非常严格。没有自动类型提升或转换。让我们看一个例子来理解这意味着什么：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line"> <span class="string">&quot;fmt&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">  i := <span class="number">55</span> <span class="comment">//int</span></span><br><span class="line">  j := <span class="number">67.8</span> <span class="comment">//float64</span></span><br><span class="line">  sum := i + j <span class="comment">//int + float64 not allowed</span></span><br><span class="line">  fmt.Println(sum)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/m-TMRpmmnm">Run in playground</a></p><p>上面的代码在 C 语言中是完全合法的。但是在使用 go 的情况下，这将行不通。 <code>i</code> 是 <code>int</code> 类型，<code>j</code> 是 <code>float64</code> 类型。我们正在尝试添加 2 个不同类型的数字，这是不允许的。运行程序时，你将得到 <code>main.go：10:invalid operation:i + j(mismatched types int and float64)</code> 的错误。</p><p>要解决该错误，<code>i</code> 和 <code>j</code> 应该具有相同的类型。让我们将 <code>j</code> 转换为 <code>int</code>。<code>T(v)</code> 是将值 <code>v</code> 转换为类型 <code>T</code> 的语法：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line"> <span class="string">&quot;fmt&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">  i := <span class="number">55</span> <span class="comment">//int</span></span><br><span class="line">  j := <span class="number">67.8</span> <span class="comment">//float64</span></span><br><span class="line">  sum := i + <span class="type">int</span>(j) <span class="comment">//j is converted to int</span></span><br><span class="line">  fmt.Println(sum)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/mweu3n3jMy">Run in playground</a></p><p>现在，当你运行上述程序时，你会看到 <code>122</code> 作为输出。</p><p>分配也是如此。需要进行显式类型转换才能将一种类型的变量分配给另一种类型。以下程序能够证实这一点：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line"> <span class="string">&quot;fmt&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">  i := <span class="number">10</span></span><br><span class="line">  <span class="keyword">var</span> j <span class="type">float64</span> = <span class="type">float64</span>(i) <span class="comment">//this statement will not work without explicit conversion</span></span><br><span class="line">  fmt.Println(<span class="string">&quot;j&quot;</span>, j)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/Y2uSYYr46c">Run in playground</a></p><p>在第 9 行中，<code>i</code> 转换为 <code>float64</code>，然后被分配给 <code>j</code>。当你尝试将 <code>i</code> 分配给 <code>j</code> 而不进行任何类型转换时，编译器将引发错误。</p><blockquote><p>原文地址 <a href="https://golangbot.com/types/">Types</a><br>原文作者：<a href="https://golangbot.com/about/">Naveen Ramanathan</a><br>译文出自：<a href="https://blog.zisheng.pro/categories/%E6%B4%9B%E7%AB%B9%E7%BF%BB%E8%AF%91%E8%AE%A1%E5%88%92/">紫升翻译计划</a></p></blockquote>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/6fecb073913f/</id>
    <link href="https://blog.zisheng.pro/posts/6fecb073913f/"/>
    <published>2023-03-19T07:04:00.000Z</published>
    <summary>本文介绍了 Go 的数据类型。</summary>
    <title>Go 语言教程系列の数据类型</title>
    <updated>2026-09-18T08:15:28.629Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="紫升翻译计划" scheme="https://blog.zisheng.pro/categories/%E7%B4%AB%E5%8D%87%E7%BF%BB%E8%AF%91%E8%AE%A1%E5%88%92/"/>
    <category term="Go" scheme="https://blog.zisheng.pro/categories/Go/"/>
    <category term="Go" scheme="https://blog.zisheng.pro/tags/Go/"/>
    <category term="Go 教程" scheme="https://blog.zisheng.pro/tags/Go-%E6%95%99%E7%A8%8B/"/>
    <category term="Golang 变量" scheme="https://blog.zisheng.pro/tags/Golang-%E5%8F%98%E9%87%8F/"/>
    <content>
      <![CDATA[<p>这是我们 Golang 教程系列中的第三篇教程，它涉及 Golang 中的变量。</p><p>你可以先阅读 <a href="https://juejin.cn/post/6942876429625327653">《Go 语言教程系列》Hello World | Go 主题月</a> 来了解关于配置 Go 和运行 hello world 程序的信息。</p><h2 id="变量是什么？"><a href="#变量是什么？" class="headerlink" title="变量是什么？"></a>变量是什么？</h2><p>变量是为内存地址指定的名称，用于存储特定类型的值。Go 中有多种语法来声明变量。让我们一一看一下。</p><h2 id="声明单个变量"><a href="#声明单个变量" class="headerlink" title="声明单个变量"></a>声明单个变量</h2><p><strong>var name type</strong> 是声明单个变量的语法。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> <span class="string">&quot;fmt&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">  <span class="keyword">var</span> age <span class="type">int</span> <span class="comment">// 变量声明</span></span><br><span class="line">  fmt.Println(<span class="string">&quot;My age is&quot;</span>, age)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/lvKWH29bwke">Run in playground</a></p><p>语句 <code>var age int</code> 声明了一个类型为 <code>int</code> 的名为 <code>age</code> 的变量。我们没有为该变量赋任何值。如果未为变量分配任何值，则 Go 会使用变量类型的“零值”自动将其初始化。在这种情况下，为年龄分配了值 <code>0</code>，即 <code>int</code>的零值。如果运行此程序，则可以看到以下输出。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">My age is <span class="number">0</span></span><br></pre></td></tr></table></figure><p>可以为变量分配给其类型的任何值。在上面的程序中，可以为 <code>age</code> 分配任何整数值。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> <span class="string">&quot;fmt&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    <span class="keyword">var</span> age <span class="type">int</span> <span class="comment">// 变量声明</span></span><br><span class="line">    fmt.Println(<span class="string">&quot;My age is&quot;</span>, age)</span><br><span class="line">    age = <span class="number">29</span> <span class="comment">// 分配</span></span><br><span class="line">    fmt.Println(<span class="string">&quot;My age is&quot;</span>, age)</span><br><span class="line">    age = <span class="number">54</span> <span class="comment">// 分配</span></span><br><span class="line">    fmt.Println(<span class="string">&quot;My new age is&quot;</span>, age)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/hSsWbF-3lQU">Run in playground</a></p><p>上面的程序将打印以下输出：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">My age is  <span class="number">0</span></span><br><span class="line">My age is <span class="number">29</span></span><br><span class="line">My <span class="built_in">new</span> age is <span class="number">54</span></span><br></pre></td></tr></table></figure><h2 id="声明一个具有初始值的变量"><a href="#声明一个具有初始值的变量" class="headerlink" title="声明一个具有初始值的变量"></a>声明一个具有初始值的变量</h2><p>声明变量时，还可以为其提供初始值。以下是声明具有初始值的变量的语法。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> name <span class="keyword">type</span> = initialvalue</span><br></pre></td></tr></table></figure><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> <span class="string">&quot;fmt&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    <span class="keyword">var</span> age <span class="type">int</span> = <span class="number">29</span> <span class="comment">// 带有初始值的变量声明</span></span><br><span class="line"></span><br><span class="line">    fmt.Println(<span class="string">&quot;My age is&quot;</span>, age)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/NAbtaE0pjZ8">Run in playground</a></p><p>在上面的程序中，<code>age</code>是类型为 <code>int</code> 的变量，其初始值为 <code>29</code>。上面的程序将输出以下内容：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">My age is 29</span><br></pre></td></tr></table></figure><p>它显示年龄已经用值 29 初始化。</p><h2 id="类型推断"><a href="#类型推断" class="headerlink" title="类型推断"></a>类型推断</h2><p>如果变量具有初始值，Go 将自动使用该初始值来推断该变量的类型。 因此，如果变量具有初始值，则可以删除变量声明中的 <code>type</code>。</p><p>如果使用以下语法声明了变量</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> name = initialvalue</span><br></pre></td></tr></table></figure><p>Go 会自动从初始值推断出该变量的类型。</p><p>在下面的示例中，我们可以看到变量 <code>age</code> 的类型 <code>int</code> 已在第 6 行中删除。由于变量的初始值为 29，因此 Go 可以推断出它的类型为 <code>int</code>。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> <span class="string">&quot;fmt&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">  <span class="keyword">var</span> age = <span class="number">29</span> <span class="comment">// 类型将被推断为 int</span></span><br><span class="line">  fmt.Println(<span class="string">&quot;My age is&quot;</span>, age)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/meyJPveRLrK">Run in playground</a></p><h2 id="多变量声明"><a href="#多变量声明" class="headerlink" title="多变量声明"></a>多变量声明</h2><p>多变量声明可以使用单行语句来声明多个变量。</p><p><code>var name1, name2 type = initialvalue1, initialvalue2</code> 是多变量声明的语法。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> <span class="string">&quot;fmt&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    <span class="keyword">var</span> width, height <span class="type">int</span> = <span class="number">100</span>, <span class="number">50</span> <span class="comment">// 声明多变量</span></span><br><span class="line"></span><br><span class="line">    fmt.Println(<span class="string">&quot;width is&quot;</span>, width, <span class="string">&quot;height is&quot;</span>, height)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/4aOQyt55ah">Run in playground</a></p><p>如果变量具有初始值，则可以删除该类型。 由于上述程序具有变量的初始值，因此可以删除 <code>int</code> 类型。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> <span class="string">&quot;fmt&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    <span class="keyword">var</span> width, height = <span class="number">100</span>, <span class="number">50</span> <span class="comment">// int 被移除了</span></span><br><span class="line"></span><br><span class="line">    fmt.Println(<span class="string">&quot;width is&quot;</span>, width, <span class="string">&quot;height is&quot;</span>, height)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/f7deGqYFNEL">Run in playground</a></p><p>上面的程序将以 <code>print width is 100 height is 50</code> 作为输出。</p><p>就像你现在可能已经猜到的那样，如果没有为 <code>width</code> 和 <code>height</code> 指定初始值，则它们的初始值将被指定为 <code>0</code>。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> <span class="string">&quot;fmt&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    <span class="keyword">var</span> width, height <span class="type">int</span></span><br><span class="line">    fmt.Println(<span class="string">&quot;width is&quot;</span>, width, <span class="string">&quot;height is&quot;</span>, height)</span><br><span class="line">    width = <span class="number">100</span></span><br><span class="line">    height = <span class="number">50</span></span><br><span class="line">    fmt.Println(<span class="string">&quot;new width is&quot;</span>, width, <span class="string">&quot;new height is&quot;</span>, height)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/-4CnqDgWOKG">Run in playground</a></p><p>上面的程序会打印如下内容。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">width is 0 height is 0</span><br><span class="line">new width is 100 new height is 50</span><br></pre></td></tr></table></figure><p>在某些情况下，我们可能希望在单个语句中声明属于不同类型的变量。这样做的语法是：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> (</span><br><span class="line">    name1 = initialvalue1</span><br><span class="line">    name2 = initialvalue2</span><br><span class="line">)</span><br></pre></td></tr></table></figure><p>以下程序使用上述语法声明不同类型的变量。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> <span class="string">&quot;fmt&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    <span class="keyword">var</span> (</span><br><span class="line">        name   = <span class="string">&quot;naveen&quot;</span></span><br><span class="line">        age    = <span class="number">29</span></span><br><span class="line">        height <span class="type">int</span></span><br><span class="line">    )</span><br><span class="line">    fmt.Println(<span class="string">&quot;my name is&quot;</span>, name, <span class="string">&quot;, age is&quot;</span>, age, <span class="string">&quot;and height is&quot;</span>, height)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/7pkp74h_9L">Run in playground</a></p><p>在这里，我们声明了一个 <code>string</code> 类型的变量 <code>name</code>，类型为 <code>age</code> 的变量和 <code>height</code> 为 int 的变量。</p><p>运行以上程序将打印：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">my name is naveen , age is 29 and height is 0</span><br></pre></td></tr></table></figure><h2 id="短声明"><a href="#短声明" class="headerlink" title="短声明"></a>短声明</h2><p>Go 还提供了另一种简洁的声明变量的方法。这被称为短声明，它使用<code>:=</code> 运算符。</p><p><code>name := initialvalue</code> 是声明变量的简写语法。</p><p>以下程序使用简写形式的语法来声明变量 <code>count</code>，并将其初始化为 <code> 10</code>。Go 会自动推断 <code>count</code> 是 <code>int</code> 类型的，因为它已经用整数值 <code>10</code>初始化了。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> <span class="string">&quot;fmt&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    count := <span class="number">10</span></span><br><span class="line">    fmt.Println(<span class="string">&quot;Count =&quot;</span>,count)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/L5_8aru7VQM">Run in playground</a></p><p>以上程序将打印：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Count = 10</span><br></pre></td></tr></table></figure><p>It is also possible to declare multiple variables in a single line using short hand syntax.</p><p>你也可以使用短声明在一行内声明多个变量：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> <span class="string">&quot;fmt&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">  name, age := <span class="string">&quot;naveen&quot;</span>, <span class="number">29</span> <span class="comment">// short hand declaration</span></span><br><span class="line"></span><br><span class="line">  fmt.Println(<span class="string">&quot;my name is&quot;</span>, name, <span class="string">&quot;age is&quot;</span>, age)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/ctqgw4w6kx">Run in playground</a></p><p>上面的程序声明了两个变量，分别是类型为 <code>string</code> 的 <code>name</code> 和类型为 <code>int</code> 的 <code>age</code>。</p><p>如果运行上述程序，则可以看到 <code>my name is naveen age is 29</code> 被打印出来。</p><p>短声明要求赋值左侧所有变量的初始值。以下程序将显示错误 <code>assignment mismatch: 2 variables but 1 values</code>。这是因为 <code>age</code> 尚未分配值。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> <span class="string">&quot;fmt&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">  name, age := <span class="string">&quot;naveen&quot;</span> <span class="comment">//error</span></span><br><span class="line"></span><br><span class="line">  fmt.Println(<span class="string">&quot;my name is&quot;</span>, name, <span class="string">&quot;age is&quot;</span>, age)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/wZd2HmDvqw">Run in playground</a></p><p>仅当 <code>:=</code> 左侧至少有一个新声明的变量时，才可以使用简写语法。注意以下程序：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> <span class="string">&quot;fmt&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    a, b := <span class="number">20</span>, <span class="number">30</span> <span class="comment">// declare variables a and b</span></span><br><span class="line">    fmt.Println(<span class="string">&quot;a is&quot;</span>, a, <span class="string">&quot;b is&quot;</span>, b)</span><br><span class="line">    b, c := <span class="number">40</span>, <span class="number">50</span> <span class="comment">// b is already declared but c is new</span></span><br><span class="line">    fmt.Println(<span class="string">&quot;b is&quot;</span>, b, <span class="string">&quot;c is&quot;</span>, c)</span><br><span class="line">    b, c = <span class="number">80</span>, <span class="number">90</span> <span class="comment">// assign new values to already declared variables b and c</span></span><br><span class="line">    fmt.Println(<span class="string">&quot;changed b is&quot;</span>, b, <span class="string">&quot;c is&quot;</span>, c)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/MSUYR8vazB">Run in playground</a></p><p>在上面的程序中，在第 8 行中，已经声明了<strong>b</strong>，但是新声明了<strong>c</strong>，因此它可以工作并输出：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">a is 20 b is 30</span><br><span class="line">b is 40 c is 50</span><br><span class="line">changed b is 80 c is 90</span><br></pre></td></tr></table></figure><p>而如果我们运行以下程序，</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> <span class="string">&quot;fmt&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    a, b := <span class="number">20</span>, <span class="number">30</span> <span class="comment">//a and b declared</span></span><br><span class="line">    fmt.Println(<span class="string">&quot;a is&quot;</span>, a, <span class="string">&quot;b is&quot;</span>, b)</span><br><span class="line">    a, b := <span class="number">40</span>, <span class="number">50</span> <span class="comment">//error, no new variables</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/EYTtRnlDu3">Run in playground</a></p><p>它将显示错误 <code>/prog.go:8:10: no new variables on left side of :=</code> ，这是因为变量 <strong>a</strong> 和 <strong>b</strong> 已经声明，并且 <code>:=</code> 左侧没有新声明的变量。</p><p>还可以为变量分配在运行时计算的值。注意以下程序，</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;fmt&quot;</span></span><br><span class="line">    <span class="string">&quot;math&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    a, b := <span class="number">145.8</span>, <span class="number">543.8</span></span><br><span class="line">    c := math.Min(a, b)</span><br><span class="line">    fmt.Println(<span class="string">&quot;Minimum value is&quot;</span>, c)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/Kk84pOyFgQB">Run in playground</a></p><p>在上述程序中，<a href="https://golang.org/pkg/math/">math</a> 是一个程序包，<a href="https://golang.org/pkg/math/#Min">Min</a> 是该程序包中的函数 。现在不用担心，我们将在后续的教程中详细讨论软件包和功能。我们需要知道的是，<code>c</code> 的值是在运行时计算出来的，它是 <code>a</code> 和 <code>b</code> 的最小值。 上面的程序将打印，</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Minimum value is  145.8</span><br></pre></td></tr></table></figure><p>由于 Go 是强类型的，因此不能将声明为属于一种类型的变量分配给另一种类型的值。以下程序将打印错误 <code>cannot use &quot;naveen&quot; (type string) as type int in assignment</code>，因为 <code>age</code> 被声明为类型 <code>int</code>，并且我们正在尝试为其分配 <code>string</code> 类型的值。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    age := <span class="number">29</span>      <span class="comment">// age is int</span></span><br><span class="line">    age = <span class="string">&quot;naveen&quot;</span> <span class="comment">// error since we are trying to assign a string to a variable of type int</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="https://play.golang.org/p/K5rz4gxjPj">Run in playground</a></p><p>谢谢阅读。 请在评论部分留下你的反馈和查询。<br>贵的反馈和意见。谢谢阅读。</p><blockquote><p>原文地址 <a href="https://golangbot.com/variables/">Variables</a><br>原文作者：<a href="https://golangbot.com/about/">Naveen Ramanathan</a><br>译文出自：<a href="https://blog.zisheng.pro/categories/%E6%B4%9B%E7%AB%B9%E7%BF%BB%E8%AF%91%E8%AE%A1%E5%88%92/">紫升翻译计划</a></p></blockquote>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/019671ed10fd/</id>
    <link href="https://blog.zisheng.pro/posts/019671ed10fd/"/>
    <published>2023-03-19T07:03:00.000Z</published>
    <summary>这是我们 Golang 教程系列中的第三篇教程，它涉及 Golang 中的变量。</summary>
    <title>Go 语言教程系列の变量</title>
    <updated>2026-09-18T08:15:28.629Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="紫升翻译计划" scheme="https://blog.zisheng.pro/categories/%E7%B4%AB%E5%8D%87%E7%BF%BB%E8%AF%91%E8%AE%A1%E5%88%92/"/>
    <category term="Go" scheme="https://blog.zisheng.pro/categories/Go/"/>
    <category term="Go" scheme="https://blog.zisheng.pro/tags/Go/"/>
    <category term="Go 教程" scheme="https://blog.zisheng.pro/tags/Go-%E6%95%99%E7%A8%8B/"/>
    <content>
      <![CDATA[<p>这是《Golang 教程系列》的第二篇。如果还没有，你可能需要先看一下我们的上一篇教程 <a href="https://juejin.cn/post/6942492190291525662">Golang 介绍和环境安装</a> 以了解一下 Golang 是什么以及如何下载 Golang。</p><p>这篇教程是基于 Go 1.16 以上的版本。</p><p>学习编程语言没有比让自己满手沾满代码的血🩸更有效的方法了。让我们一起开始第一个 Go 程序吧。（First Blood）</p><h2 id="设置开发环境"><a href="#设置开发环境" class="headerlink" title="设置开发环境"></a>设置开发环境</h2><p>让我们先创建一个编写 hello world 程序的文件夹。请打开命令行然后运行下面的命令：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">mkdir</span> ~/Desktop/learngo/</span><br></pre></td></tr></table></figure><p>上面的命令将会在用户桌面创建一个名为 <code>learngo</code> 的文件夹（译者开发环境为 Macbook Pro）。你可以在任意位置创建目录编写代码。</p><h2 id="Hello-World"><a href="#Hello-World" class="headerlink" title="Hello World"></a>Hello World</h2><p>使用你喜爱的文本编辑器在 <code>learngo</code> 文件夹中创建一个名为 <code>main.go</code> 的文件并写入以下内容：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> <span class="string">&quot;fmt&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">  fmt.Println(<span class="string">&quot;Hello World&quot;</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在 Go 中将包含 <code>main</code> 函数的文件命名为 <code>main.go</code> 是约定俗称的，但是其他名称也是可以使用。</p><h2 id="运行一个程序"><a href="#运行一个程序" class="headerlink" title="运行一个程序"></a>运行一个程序</h2><p>有几种不同的方式来运行 Go 程序。让我们一一看一下。</p><h3 id="1-go-install"><a href="#1-go-install" class="headerlink" title="1. go install"></a>1. <code>go install</code></h3><p>第一个运行 Go 程序的方法是使用 <code>go install</code> 命令。让我们来使用 <code>cd</code> 命令进入刚创建的 <code>learngo</code> 目录</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">cd</span> ~/Desktop/learngo/</span><br></pre></td></tr></table></figure><p>接着运行下面的命令。</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">go install</span><br></pre></td></tr></table></figure><p>上面的命令将编译当前程序并将其安装（拷贝）二进制可执行文件到 <code>~/go/bin</code> 目录。二进制可执行文件的名字是包含 <code>main.go</code> 文件的文件夹名。在我们示例中，它将被命名为 <code>learngo</code>。</p><p>当你尝试安装程序时，你可能会遇到以下错误。</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">go: cannot find main module; see <span class="string">&#x27;go help modules&#x27;</span></span><br></pre></td></tr></table></figure><p>上面的错误实际上意味着，<code>go install</code> 无法找到 main 模块，这是因为我们没有初始化 go modules，我们使用以下命令初始化模块：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">go mod init github.com/youngjuning/learngo</span><br></pre></td></tr></table></figure><p>上面的命令会在 <code>learngo</code> 目录下创建一个 <code>go.mod</code> 文件，该文件是程序模块定义的地方，作用类似于 Node 的 <code>package.json</code> 文件。然后我再执行 <code>go install</code> 便可以成功。</p><p>你可以在命令行输入 <code>ls -al ~/go/bin/learngo</code>，然后你会发现 <code>go install</code> 实际上是把二进制可执行文件放在了 <code>~/go/bin</code> 中。</p><p>现在让我们运行编译后的二进制可执行文件。</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">~/go/bin/learngo</span><br></pre></td></tr></table></figure><p>上面的命令将运行 <code>learngo</code> 并打印出以下内容：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Hello World</span><br></pre></td></tr></table></figure><p>恭喜你！你已经成功地运行了你的第一个 Go 程序。</p><p>如果你不想每次都输入完整的 <code>~/go/bin/learngo</code> 路径来运行程序，你可以添加 <code>~/go/bin/</code> 到你的 <code>PATH</code> 中。</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">export</span> GOPATH=~/.go</span><br><span class="line"><span class="built_in">export</span> PATH=<span class="variable">$&#123;PATH&#125;</span>:<span class="variable">$GOPATH</span>/bin</span><br></pre></td></tr></table></figure><p>现在你可以在命令行中只输入 <code>learngo</code> 来运行程序。</p><p>你可能想知道，当 <code>learngo</code> 目录包含多个 Go 文件而不只是只有 <code>main.go</code> 时会发生什么。在这种情况下，<code>go install</code> 将如何工作？ 请继续往下看，我们将在了解软件包和 Go 模块时讨论这些内容。</p><h3 id="2-go-build"><a href="#2-go-build" class="headerlink" title="2. go build"></a>2. go build</h3><p>运行程序的第二个选项是使用 <code>go build</code>。<code>go build</code> 与 <code>go install</code> 非常相似，不同之处在于它不会将编译的二进制文件安装（拷贝）到路径 <code>~/go/bin/</code>，而是在 <code>go build</code> 所在的文件夹下创建二进制文件：</p><p>在命令行输入以下命令来切换当前目录到 <code>learngo</code>：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">cd</span> ~/Desktop/learngo/</span><br></pre></td></tr></table></figure><p>然后输入下面的命令：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">go build</span><br></pre></td></tr></table></figure><p>上面的命令将会在当前目录下创建一个名为 <code>learngo</code> 的二进制可执行文件。<code>ls -al</code> 命令可以证实名为 <code>learngo</code> 的文件被创建了。</p><p>输入 <code>./learngo</code> 来运行程序，将会输入和前面一样的内容：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Hello World</span><br></pre></td></tr></table></figure><p>到此，我们用 <code>go build</code> 也成功地运行了我们的第一个 Go 程序 😁</p><h3 id="3-go-run"><a href="#3-go-run" class="headerlink" title="3. go run"></a>3. go run</h3><p>第三个运行程序的方法是使用 <code>go run</code> 命令。</p><p>在命令行输入 <code>cd ~/Desktop/learngo</code> 命令来改变当前目录为 <code>learngo</code>。</p><p>然后输入以下命令。</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">go run main.go</span><br></pre></td></tr></table></figure><p>输入以上命令后，我们也可以看到一样的输出：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Hello World</span><br></pre></td></tr></table></figure><p><code>go run</code> 命令和 <code>go build</code> 或 <code>go install</code> 命令之间的一个细微差别是，<code>go run</code> 要求使用 <code>.go</code> 文件的名称作为参数。</p><p>在引擎盖下，<code>go run</code> 的工作原理与 <code>go build</code> 非常相似。无需将程序编译并安装到当前目录，而是将文件编译到一个临时位置并从该位置运行文件。如果你想知道 <code>go run</code> 将文件编译到的位置，请使用 <code>--work</code> 参数运行 <code>go run</code>。</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">go run --work main.go</span><br></pre></td></tr></table></figure><p>在我的场景中，运行以上命令会输出下面的内容：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">WORK=/var/folders/mf/_fk8g5jn23gcw970pypqlv4m0000gn/T/go-build3519209434</span><br><span class="line">Hello World</span><br></pre></td></tr></table></figure><p><code>WORK</code> 的值表示程序将被编译到的一个临时位置。</p><p>就我的场景而言，程序被编译到 <code>/var/folders/mf/_fk8g5jn23gcw970pypqlv4m0000gn/T/go-build3519209434</code> 。这可能因你的情况而异 😁</p><h3 id="4-Go-Playground"><a href="#4-Go-Playground" class="headerlink" title="4. Go Playground"></a>4. Go Playground</h3><p>运行程序的最后一种方法是使用 go playground。尽管此方法有一些限制，但由于我们可以使用浏览器并且不需要在本地本地安装 Go：我已经为 Hello World 程序创建了一个 playground。 <a href="https://play.golang.org/p/oXGayDtoLPh">点击此处</a> 以在线运行该程序。</p><p>你还可以使用 Go Playground 与他人分享你的源代码。</p><p>既然我们知道4种不同的方式来运行程序，那么你可能会很困惑该使用哪种方法。答案是，当我想快速检查逻辑或找出标准库函数如何工作时，通常使用 <a href="https://play.golang.org/">playground</a>。在大多数其他情况下，我更喜欢 <code>go install</code>，因为它为我提供了从终端中任何目录运行程序的选项，因为它将所有程序编译到标准的 <code>~/go/bin/</code> 路径。</p><h2 id="对-Hello-World-程序的简短解析"><a href="#对-Hello-World-程序的简短解析" class="headerlink" title="对 Hello World 程序的简短解析"></a>对 Hello World 程序的简短解析</h2><p>这是我们刚刚创建的简单的 hello world 程序：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> <span class="string">&quot;fmt&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">  fmt.Println(<span class="string">&quot;Hello World&quot;</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>我们将简要讨论该程序的每一行的作用。在接下来的教程中，我们将深入研究程序的每个部分。</p><p><strong>package main</strong> - 每个 go 文件都必须以 <code>package name</code> 开始。Packages 用于提供代码分隔和可重用性。此处使用包名称 <code>main</code>。主要功能应始终保留在 main package 中。</p><p><strong>import “fmt”</strong> - <code>import</code> 语句用于导入其他软件包。在我们的例子中，<code>fmt</code> 包被导入，它将在 <code>main</code> 函数中用于将文本打印到标准输出。</p><p><strong>func main()</strong> - <code>func</code> 关键字标记函数的开始。<code>main</code> 是一个特殊函数。程序从 <code>main</code> 函数开始执行。大括号 <code>{</code> 和 <code>}</code> 表示 <code>main</code> 函数的开始和结束。</p><p><strong>fmt.Println(“Hello World”)</strong> - <code>fmt</code> 软件包的 <code>PrintIn</code> 函数用于将文本写入标准输出。<code>package.function()</code> 是在包中调用函数的语法。</p><p>本文的实例代码可以在 <a href="https://github.com/golangbot/hello">github</a> 下载。</p><blockquote><p>原文地址 <a href="https://golangbot.com/hello-world-gomod/">Hello World</a><br>原文作者：<a href="https://golangbot.com/about/">Naveen Ramanathan</a><br>译文出自：<a href="https://blog.zisheng.pro/categories/%E6%B4%9B%E7%AB%B9%E7%BF%BB%E8%AF%91%E8%AE%A1%E5%88%92/">紫升翻译计划</a></p></blockquote>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/6cbf2167b038/</id>
    <link href="https://blog.zisheng.pro/posts/6cbf2167b038/"/>
    <published>2023-03-19T07:02:00.000Z</published>
    <summary>学习编程语言没有比让自己满手沾满代码的血🩸更有效的方法了。让我们一起开始第一个 Go 程序吧。</summary>
    <title>Go 语言教程系列のHello World</title>
    <updated>2026-09-18T08:15:28.629Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="紫升翻译计划" scheme="https://blog.zisheng.pro/categories/%E7%B4%AB%E5%8D%87%E7%BF%BB%E8%AF%91%E8%AE%A1%E5%88%92/"/>
    <category term="Go" scheme="https://blog.zisheng.pro/categories/Go/"/>
    <category term="swiper" scheme="https://blog.zisheng.pro/tags/swiper/"/>
    <category term="Go" scheme="https://blog.zisheng.pro/tags/Go/"/>
    <category term="Go 教程" scheme="https://blog.zisheng.pro/tags/Go-%E6%95%99%E7%A8%8B/"/>
    <category term="Go 安装教程" scheme="https://blog.zisheng.pro/tags/Go-%E5%AE%89%E8%A3%85%E6%95%99%E7%A8%8B/"/>
    <content>
      <![CDATA[<p>这是我们 Golang 教程系列中的第一篇教程。本教程介绍了 Go，并讨论了选择 Go 而不是其他编程语言的原因。我们还将学习如何在 Mac OS，Windows 和 Linux 中安装 Go。</p><h2 id="介绍"><a href="#介绍" class="headerlink" title="介绍"></a>介绍</h2><p>Go 也被称为 Golang，是 Google 开发的一种开源、编译和静态类型的编程语言。Go 创作背后的关键人物是 Rob Pike，Ken Thompson 和Robert Griesemer。Go 于 2009 年 11 月公开发布。</p><p>Go 是一种通用的编程语言，语法简单并具有强大的标准库作为后盾。Go 大放异彩的关键领域之一是创建高可用性和可伸缩性的 Web 应用程序。Go 还可以用于创建命令行应用程序、桌面应用程序甚至移动应用程序。</p><h2 id="Go-的优势"><a href="#Go-的优势" class="headerlink" title="Go 的优势"></a>Go 的优势</h2><p>当大量其他语言（例如 python，ruby，nodejs …）可以实现相同需求时，为什么选择 Go 作为服务器端编程语言。</p><p>这里是我选择 Go 时发现的一些优点。</p><h3 id="简单的语法"><a href="#简单的语法" class="headerlink" title="简单的语法"></a>简单的语法</h3><p>语法简单明了，并且该语言不会因不必要的功能而显得臃肿。这使得编写可读性和可维护性的代码变得容易。</p><h3 id="易于编写并发程序"><a href="#易于编写并发程序" class="headerlink" title="易于编写并发程序"></a>易于编写并发程序</h3><p>并发是语言的固有部分。直接的影响是编写多线程程序变得简直小菜一碟。这是通过 Goroutines 和管道实现的，我们将在接下来的教程中对其进行讨论。</p><h3 id="编译语言"><a href="#编译语言" class="headerlink" title="编译语言"></a>编译语言</h3><p>Go是一种编译语言。源代码被编译为本地二进制文件。 解释语言（例如，nodejs 中使用的 JavaScript 和 Python）缺少此功能。</p><h3 id="快速编译"><a href="#快速编译" class="headerlink" title="快速编译"></a>快速编译</h3><p>Go编译器非常神奇，并且从一开始就被设计为快速的。</p><h3 id="静态链接"><a href="#静态链接" class="headerlink" title="静态链接"></a>静态链接</h3><p>Go 编译器支持静态链接。整个 Go 项目可以静态链接到一个大型的二进制文件中，并且可以轻松部署在云服务器中，而不必担心依赖关系。</p><h3 id="Go-工具链"><a href="#Go-工具链" class="headerlink" title="Go 工具链"></a>Go 工具链</h3><p>工具在 Go 中值得特别提及。Go 附带了功能强大的工具，可以帮助开发人员编写更好的代码。常用的工具有：</p><ul><li><code>gofmt</code>：<code>gofmt</code> 是用于自动格式化 go 源代码的。它使用制表符进行缩进，并使用空格进行对齐。</li><li><code>vet</code>：<code>vet</code> 分析 go 源代码并报告可能的可疑代码。vet 报告的所有内容都不是真正的问题，但具有捕获编译器未报告的错误的能力，例如使用 <code>Printf</code> 时格式说明符不正确。</li><li><code>golint</code>：golint 用于识别代码中的样式问题。</li></ul><h3 id="垃圾回收"><a href="#垃圾回收" class="headerlink" title="垃圾回收"></a>垃圾回收</h3><p>Go 使用垃圾回收，因此内存管理几乎会自动执行，开发人员无需担心内存管理问题。 这也有助于轻松地编写并发程序。</p><h3 id="简单语言规范"><a href="#简单语言规范" class="headerlink" title="简单语言规范"></a>简单语言规范</h3><p>语言规范非常简单。 整个规范只有一个页面，你甚至可以使用它来编写自己的编译器 :)</p><h3 id="开源"><a href="#开源" class="headerlink" title="开源"></a>开源</h3><p>最后但并非最不重要的一点是，Go 是一个开源项目。 你可以参与 Go 项目并为该项目做出贡献。</p><h2 id="使用-Go-构建的热门产品"><a href="#使用-Go-构建的热门产品" class="headerlink" title="使用 Go 构建的热门产品"></a>使用 Go 构建的热门产品</h2><p>以下是一些使用 Go 构建的流行产品。</p><ul><li>Google 使用 Go 开发了 Kubernetes using。</li><li>Docker, 世界上最著名的容器平台是使用 Go 开发的</li><li>Dropbox 已将其性能敏感组件从 Python 迁移到 Go。</li><li>Infoblox 的 下一代网络产品是使用 Go 开发的。</li></ul><h2 id="安装"><a href="#安装" class="headerlink" title="安装"></a>安装</h2><p>Go 可以安装在 Mac，Windows 和 Linux 这三个平台上。你可以从<a href="https://golang.org/dl/">https://golang.org/dl/</a> 下载对应平台的二进制文件。</p><h3 id="Mac-OS"><a href="#Mac-OS" class="headerlink" title="Mac OS"></a>Mac OS</h3><p>从 <a href="https://golang.org/dl/">https://golang.org/dl/</a> 下载 Mac OS 安装程序。双击开始安装。按照提示进行操作，这会将 Golang 安装在 <code>/usr/local/go</code> 中，并且还将文件夹 <code>/usr/local/go/bin</code> 添加到 <code>PATH</code> 环境变量中。</p><h3 id="Windows"><a href="#Windows" class="headerlink" title="Windows"></a>Windows</h3><p>从 <a href="https://golang.org/dl/">https://golang.org/dl/</a> 下载 MSI 安装程序。双击开始安装，然后按照提示进行操作。 这会将Go安装在位置 <code>c:\Go</code>，还将目录 <code>c:\Go\bin</code> 添加到你的路径环境变量中。</p><h3 id="Linux"><a href="#Linux" class="headerlink" title="Linux"></a>Linux</h3><p>从 <a href="https://golang.org/dl/">https://golang.org/dl/</a> 下载 tar 文件并将其解压缩到 <code>/usr/local</code>。</p><p>将 <code>/usr/local/go/bin</code> 添加到 <code>PATH</code> 环境变量中。这将在 Linux 中安装 Go。</p><h2 id="验证安装"><a href="#验证安装" class="headerlink" title="验证安装"></a>验证安装</h2><p>要验证 Go 安装是否成功，请在终端中键入命令 <code>go version</code>，它将输出已安装的 Go 版本。这是我终端的输出。</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">$ go version</span><br><span class="line">go version go1.16 darwin/amd64</span><br></pre></td></tr></table></figure><p>在编写本教程时，1.16 是 Go 的最新版本。这证明 Go 已成功安装。在下一个教程中，我们将在 Go 中编写第一个 Hello World 程序 :)</p><p>请提供你宝贵的反馈和意见。谢谢阅读。</p><blockquote><p>原文地址 <a href="https://golangbot.com/golang-tutorial-part-1-introduction-and-installation/">Part 1: Introduction and Installation</a><br>原文作者：<a href="https://golangbot.com/about/">Naveen Ramanathan</a><br>译文出自：<a href="https://blog.zisheng.pro/categories/%E6%B4%9B%E7%AB%B9%E7%BF%BB%E8%AF%91%E8%AE%A1%E5%88%92/">紫升翻译计划</a></p></blockquote>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/f6093e483bf0/</id>
    <link href="https://blog.zisheng.pro/posts/f6093e483bf0/"/>
    <published>2023-03-19T07:01:00.000Z</published>
    <summary>这是我们 Golang 教程系列中的第一篇教程。本教程介绍了 Go，并讨论了选择 Go 而不是其他编程语言的原因。我们还将学习如何在 Mac OS，Windows 和 Linux 中安装 Go。</summary>
    <title>Go 语言教程系列の介绍和环境安装</title>
    <updated>2026-09-18T08:15:28.629Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="紫升翻译计划" scheme="https://blog.zisheng.pro/categories/%E7%B4%AB%E5%8D%87%E7%BF%BB%E8%AF%91%E8%AE%A1%E5%88%92/"/>
    <category term="AI" scheme="https://blog.zisheng.pro/categories/AI/"/>
    <category term="AI" scheme="https://blog.zisheng.pro/tags/AI/"/>
    <category term="GPT-4" scheme="https://blog.zisheng.pro/tags/GPT-4/"/>
    <category term="ChatGpt" scheme="https://blog.zisheng.pro/tags/ChatGpt/"/>
    <content>
      <![CDATA[<p>科技世界正在庆祝最新版本的 ChatGPT-4。</p><div class="note info modern"><p>如果您还没有注册 ChatGpt，可以参考 <a href="https://blog.zisheng.pro/d744d1d3d1e8/">OpenAI 推出超神 ChatGPT 注册攻略来了</a> 快速注册一个。</p></div><p>“Chat GPT已经长大了，” 哥伦比亚大学工程教授霍德•利普森说道。</p><p>作为 AI 聊天机器人的 ChatGPT 于 2022 年 11 月诞生，现在已经走向成年，其创造者微软资助的 Open AI 已经第四次更新技术，使这个神奇的机器人变得更加智能、快速和有趣。</p><p><strong>以下是一个 ChatGP T的笑话：</strong></p><ul><li>Madonna 为什么要过马路？<blockquote><p>去马路的另一边摆 POSE（strike a pose）。</p></blockquote></li></ul><p>GPT-3 能够写诗、解决问题和总结文本，但人们对其准确性存在担忧。而 GPT-4 解决了这些问题。事实上，它在 GRE、MCAT、LSAT 和 SAT 等多项测试中表现优异，甚至还可以理解图片。</p><p>“有人试过这个功能，他们给 ChatGPT 展示了我的冰箱照片，问我晚餐可以做什么？” Lipson 说。“看到酸奶和草莓后，ChatGPT 建议制作一份完美的比萨饼。这对于 AI 来说是一个重大突破。”</p><p>它可以翻译、计算和编码。对于没有医生资源的人来说，ChatGPT 的知识和专业可以拯救生命。但是，尽管 ChatGPT 带来了很多好处，技术专家们承认这种原始智能是一把双刃剑，如果被恶意利用，可能会造成不可预测的后果。</p><p>“你可以创建垃圾邮件，你可以创建非常令人信服的电子邮件，你可以欺骗人们。”Lipson 警告说。“我认为你可以使用这项技术来设计一些东西，也许我们不应该去设计和制造。”</p><p>ChatGPT-4 版本的训练速度更快，并且新的升级将不断推出。尽管非常先进，AI 仍不能理解并回应肢体语言、八卦或什么都没谈的闲聊。但是ChatGPT 4.0 版本显示，AI 的演化是指数级的。</p><p>“这既令人兴奋又令人恐惧，我必须承认。” Lipson 说。</p><blockquote><p>原文地址：<a href="https://www.fox5ny.com/news/chat-gpt-4-is-here-will-it-change-our-lives">Chat GPT-4 is here - Will it change our lives?</a><br>原文作者：<a href="https://www.fox5ny.com/person/r/ashlie-rodriguez">Ashlie Rodriguez</a><br>译文出自：<a href="https://blog.zisheng.pro/categories/%E6%B4%9B%E7%AB%B9%E7%BF%BB%E8%AF%91%E8%AE%A1%E5%88%92/">紫升翻译计划</a></p></blockquote>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/197cfff33155/</id>
    <link href="https://blog.zisheng.pro/posts/197cfff33155/"/>
    <published>2023-03-17T06:14:20.000Z</published>
    <summary>Chat GPT-4 被誉为改变游戏规则的技术，从许多方面来看它确实是。它在难题标准测试中击败人类，但它和其他 AI 工具将如何影响我们的生活呢？ FOX 5 纽约的 Ashlie Rodriguez 采访了专家，以了解一些情况。</summary>
    <title>Chat GPT-4 已经问世——它会改变我们的生活吗？</title>
    <updated>2026-09-18T08:15:28.629Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="Google" scheme="https://blog.zisheng.pro/categories/Google/"/>
    <category term="大陆" scheme="https://blog.zisheng.pro/tags/%E5%A4%A7%E9%99%86/"/>
    <category term="Google 账号" scheme="https://blog.zisheng.pro/tags/Google-%E8%B4%A6%E5%8F%B7/"/>
    <category term="科学上网" scheme="https://blog.zisheng.pro/tags/%E7%A7%91%E5%AD%A6%E4%B8%8A%E7%BD%91/"/>
    <category term="谷歌账号" scheme="https://blog.zisheng.pro/tags/%E8%B0%B7%E6%AD%8C%E8%B4%A6%E5%8F%B7/"/>
    <content>
      <![CDATA[<p>最近注册 Google 账号卡在了验证手机号码的步骤，应该是 Google 更新了他们的合规政策，具体报错如下图：</p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1678987015915.png" alt="此电话号码无法用于进行验证"></p><h1 id="切换-VPN-到美国"><a href="#切换-VPN-到美国" class="headerlink" title="切换 VPN 到美国"></a>切换 VPN 到美国</h1><p>为了模拟网络 IP，我们最好将 VPN 节点切到 Google 的总部所在国美国。紫升收藏了一些好用的 <a href="/vpn/">科学上网</a> 工具，有需要可以试用。</p><h1 id="切换浏览器时区"><a href="#切换浏览器时区" class="headerlink" title="切换浏览器时区"></a>切换浏览器时区</h1><p>只切换 VPN 节点，我们还是无法成功验证手机号，必须将时区设置成和 VPN 节点一致才可以，使用 <a href="https://whoer.net/">whoer.net</a> 可以看到当前时区不一致：</p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1678987547217.png" alt="紫升"></p><p>使用浏览器扩展 <a href="https://chrome.google.com/webstore/detail/change-timezone-time-shif/nbofeaabhknfdcpoddmfckpokmncimpj?utm_source=chrome-ntp-icon">Change Timezone (Time Shift)</a> 我们可以很方便地切换时区：</p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1678987441408.png" alt="紫升"></p><h1 id="修改浏览器语言"><a href="#修改浏览器语言" class="headerlink" title="修改浏览器语言"></a>修改浏览器语言</h1><p>将浏览器的语言设置成 英语（美国），然后删除其他语言：</p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1678988750775.png" alt="紫升"></p><p>然后再用 <a href="https://whoer.net/">whoer.net</a> 检查一下：</p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1678987676196.png" alt="紫升"></p><p>这个时候你就可以成功注册 Google 了。</p><h1 id="跨浏览器测试平台"><a href="#跨浏览器测试平台" class="headerlink" title="跨浏览器测试平台"></a>跨浏览器测试平台</h1><p>如果以上还不行，那就是 VPN 节点不够纯净，可以使用 <a href="https://saucelabs.com/">sauce labs</a> 开一个浏览器注册。</p><h1 id="扩展知识"><a href="#扩展知识" class="headerlink" title="扩展知识"></a>扩展知识</h1><p>以前无知，以为 ip 地址改了身份就变了，实际能标识一个用户的方面有很多，比如：</p><ul><li>浏览器 Cookie</li><li>浏览器默认 Track</li><li>浏览器历史记录</li><li>浏览器搜索历史</li><li>电脑设置</li><li>电脑分辨率</li><li>浏览器语言</li></ul><p>校验严格的网站会禁止禁用 Cookie，不带 Cookie 或者网络是匿名的都会认为是异常用户，另外批量处理很容易触发封号，最近互关 Twitter，手动点的多了就被锁账号了。</p>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/382c92b68823/</id>
    <link href="https://blog.zisheng.pro/posts/382c92b68823/"/>
    <published>2023-03-16T16:50:37.000Z</published>
    <summary>本文介绍了 2023 年大陆人如何成功创建一个新的 Google 账户，完美解决“此电话号码无法用于进行验证”。</summary>
    <title>2023 大陆人如何成功创建 Google 账户</title>
    <updated>2026-09-18T08:15:28.629Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="工具" scheme="https://blog.zisheng.pro/categories/%E5%B7%A5%E5%85%B7/"/>
    <category term="macOS" scheme="https://blog.zisheng.pro/tags/macOS/"/>
    <category term="VPN" scheme="https://blog.zisheng.pro/tags/VPN/"/>
    <category term="卸载软件" scheme="https://blog.zisheng.pro/tags/%E5%8D%B8%E8%BD%BD%E8%BD%AF%E4%BB%B6/"/>
    <category term="终端" scheme="https://blog.zisheng.pro/tags/%E7%BB%88%E7%AB%AF/"/>
    <category term="苹果电脑" scheme="https://blog.zisheng.pro/tags/%E8%8B%B9%E6%9E%9C%E7%94%B5%E8%84%91/"/>
    <content>
      <![CDATA[<p>公司用的 VPN 在前一段时间强制给电脑添加了全屏水印，但是我的个人电脑为了随时支持，也安装了内网 VPN，这个水印是不能去掉的，所以我就想着卸载这个软件，但是卸载的时候就报错了，不能完成此操作，xxx 已锁定，这个时候我们就需要使用终端来卸载这些软件了。</p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1678980050589.png" alt="不能完成此操作,xxx 已锁定"></p><h1 id="第一步"><a href="#第一步" class="headerlink" title="第一步"></a>第一步</h1><blockquote><p>注意：请替换 xxx 为要删除的 app</p></blockquote><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">/bin/ls -dleO@ /Applications/xxx.app</span><br></pre></td></tr></table></figure><h1 id="第二步"><a href="#第二步" class="headerlink" title="第二步"></a>第二步</h1><blockquote><p>注意：请替换 xxx 为要删除的 app</p></blockquote><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sudo /usr/bin/chflags -R noschg /Applications/xxx.app</span><br></pre></td></tr></table></figure><p>这两行命令执行完后，发现 logo 上的小锁消失了，于是开开心心的单击右键，移除，解决。</p><h1 id="第三步"><a href="#第三步" class="headerlink" title="第三步"></a>第三步</h1><p>卸载了之后，这流氓软件还是会存在一个杀不死的 xxxAgent 进程，真是醉了。脑残的安全部门，不能给个一键卸载的选项吗？</p><p>参考 <a href="https://www.jianshu.com/p/68014c7e5210">mac 解决kill无法强制杀死后台程序的问题</a>，直接删除文件夹，恼人的水印就没有了。</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sudo <span class="built_in">rm</span> -rf /opt/.yunshu/</span><br></pre></td></tr></table></figure>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/a4996cdebca4/</id>
    <link href="https://blog.zisheng.pro/posts/a4996cdebca4/"/>
    <published>2023-03-16T15:15:12.000Z</published>
    <summary>很多公司的内网 VPN 软件都是需要管理员权限才能安装的，但是有时候我们需要卸载这些软件，但是卸载的时候会报错，不能完成此操作，xxx 已锁定，这个时候我们就需要使用终端来卸载这些软件了。</summary>
    <title>「已解决」Mac 电脑删除软件报 “不能完成此操作,xxx 已锁定”</title>
    <updated>2026-09-18T08:15:28.636Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="Google" scheme="https://blog.zisheng.pro/categories/Google/"/>
    <category term="Google AdSense" scheme="https://blog.zisheng.pro/categories/Google/Google-AdSense/"/>
    <category term="紫升翻译计划" scheme="https://blog.zisheng.pro/categories/%E7%B4%AB%E5%8D%87%E7%BF%BB%E8%AF%91%E8%AE%A1%E5%88%92/"/>
    <category term="站点运营" scheme="https://blog.zisheng.pro/categories/%E7%AB%99%E7%82%B9%E8%BF%90%E8%90%A5/"/>
    <category term="广告" scheme="https://blog.zisheng.pro/categories/%E7%AB%99%E7%82%B9%E8%BF%90%E8%90%A5/%E5%B9%BF%E5%91%8A/"/>
    <category term="Google AdSense" scheme="https://blog.zisheng.pro/tags/Google-AdSense/"/>
    <category term="点击率" scheme="https://blog.zisheng.pro/tags/%E7%82%B9%E5%87%BB%E7%8E%87/"/>
    <category term="广告发布商" scheme="https://blog.zisheng.pro/tags/%E5%B9%BF%E5%91%8A%E5%8F%91%E5%B8%83%E5%95%86/"/>
    <category term="广告联盟" scheme="https://blog.zisheng.pro/tags/%E5%B9%BF%E5%91%8A%E8%81%94%E7%9B%9F/"/>
    <content>
      <![CDATA[<p>在互联网营销领域, 点击率对广告商和发布者都非常重要。广告商需要创建广告图形或销售线路，从而获得更高的点击率 (CTR)，而发布者则需要以增加 CTR 的方式放置广告。</p><p>我看到很多人在各个论坛上询问这个问题，所以本篇文章将有助于许多不了解点击率是什么的读者。</p><p>从博客&#x2F;出版商的角度来看，CTR是一项在赚取金钱和留住广告商方面发挥重要作用的因素之一。如果您无法给足够的点击，您的广告商就会离开。那么，让我们深入了解CTR并了解它是什么。</p><h1 id="什么是-CTR"><a href="#什么是-CTR" class="headerlink" title="什么是 CTR"></a>什么是 CTR</h1><p>CTR 代表点击率（Click through rate）。简而言之，它的意思是广告被点击的次数除以广告、广告单元或包含广告的页面被查看的次数。</p><ul><li>CTR &#x3D; 点击数 &#x2F; 展示次数</li></ul><p>简单来说，如果您发布了一则广告，并被 100 人看到，但只得到了 5 次点击，那么您的 CTR 为 5%。需要注意的是，CTR百分比越高，你将获得更好的广告效果。因此，建议在易于点击和可见的地方放置广告。现在，最常见的问题之一是什么是良好的 CTR 或者我的 Adsense 有足够的点击量，但我的收入非常低怎么办。</p><p>让我来揭开关于 Adsense 点击率的迷思，过多地点击 AdSense 广告永远不能保证良好的收入，关键在于针对能给予高带权值广告的点击。这样的广告，“即使很少点击也会产生比 100 次点击更多的收益”。我将继续发布更多有关 Google Adsense 的主题，让您的 Adsense 体验更佳。如果您想不错过任何内容，请订阅 <a href="https://blog.zisheng.pro/atom.xml">紫升的博客</a> 。</p><blockquote><p>原文地址：<a href="https://www.shoutmeloud.com/what-is-ctr-in-google-adsense.html">What is CTR in Google Adsense?</a><br>原文作者：<a href="https://www.shoutmeloud.com/author/admin">Harsh Agrawal</a><br>译文出自：<a href="https://blog.zisheng.pro/categories/%E6%B4%9B%E7%AB%B9%E7%BF%BB%E8%AF%91%E8%AE%A1%E5%88%92/">紫升翻译计划</a></p></blockquote>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/e7eb0384452b/</id>
    <link href="https://blog.zisheng.pro/posts/e7eb0384452b/"/>
    <published>2023-03-16T12:38:10.000Z</published>
    <summary>CTR代表点击率（Click Though rate）。简单来说，它表示广告被点击的次数除以广告被浏览的次数。</summary>
    <title>Google Adsense 的 CTR 是什么？</title>
    <updated>2026-09-18T08:15:28.632Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="问题排查" scheme="https://blog.zisheng.pro/categories/%E9%97%AE%E9%A2%98%E6%8E%92%E6%9F%A5/"/>
    <category term="Ant Design" scheme="https://blog.zisheng.pro/categories/%E9%97%AE%E9%A2%98%E6%8E%92%E6%9F%A5/Ant-Design/"/>
    <category term="React" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/React/"/>
    <category term="前端开发" scheme="https://blog.zisheng.pro/tags/%E5%89%8D%E7%AB%AF%E5%BC%80%E5%8F%91/"/>
    <category term="Ant Design" scheme="https://blog.zisheng.pro/tags/Ant-Design/"/>
    <category term="descriptions" scheme="https://blog.zisheng.pro/tags/descriptions/"/>
    <category term="antd descriptions" scheme="https://blog.zisheng.pro/tags/antd-descriptions/"/>
    <category term="Descriptions" scheme="https://blog.zisheng.pro/tags/Descriptions/"/>
    <content>
      <![CDATA[<h2 id="问题"><a href="#问题" class="headerlink" title="问题"></a>问题</h2><p>在使用 antd 的 Descriptions 组件时，发现 label 后面总是有一个冒号，如下图所示：</p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1678952578255.png" alt="antd descriptions label"></p><p>代码如下：</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">&lt;<span class="title class_">Descriptions</span> title=<span class="string">&#x27;类型统计&#x27;</span> layout=<span class="string">&quot;vertical&quot;</span> column=&#123;<span class="number">4</span>&#125;&gt;</span><br><span class="line">  <span class="language-xml"><span class="tag">&lt;<span class="name">Descriptions.Item</span> <span class="attr">label</span>=<span class="string">&quot;点类型统计&quot;</span>&gt;</span>73<span class="tag">&lt;/<span class="name">Descriptions.Item</span>&gt;</span></span></span><br><span class="line">  <span class="language-xml"><span class="tag">&lt;<span class="name">Descriptions.Item</span> <span class="attr">label</span>=<span class="string">&quot;边类型统计&quot;</span>&gt;</span>29<span class="tag">&lt;/<span class="name">Descriptions.Item</span>&gt;</span></span></span><br><span class="line">&lt;/<span class="title class_">Descriptions</span>&gt;</span><br></pre></td></tr></table></figure><p>正常来说是没问题的，但是 UI 看着不顺眼，走查的时候要求去掉，但是发现没有相关属性可以配置。</p><h2 id="解决方案"><a href="#解决方案" class="headerlink" title="解决方案"></a>解决方案</h2><p>直接改 antd 的 css 样式来隐藏即可：</p><figure class="highlight less"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">:<span class="selector-tag">global</span> &#123;</span><br><span class="line">  <span class="selector-class">.ant-descriptions-item-label</span><span class="selector-pseudo">::after</span> &#123;</span><br><span class="line">    <span class="attribute">content</span>: <span class="string">&#x27;&#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="自定义-label-和-content-的-padding"><a href="#自定义-label-和-content-的-padding" class="headerlink" title="自定义 label 和 content 的 padding"></a>自定义 label 和 content 的 padding</h3><p>同样是因为 UI 走查看不顺眼，要求把 label 和 content 的 padding 值改为 4px，可以通过修改 antd 的样式来实现：</p><figure class="highlight less"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="selector-class">.ant-descriptions-row</span> &gt; <span class="selector-tag">th</span>, <span class="selector-class">.ant-descriptions-row</span> &gt; <span class="selector-tag">td</span> &#123;</span><br><span class="line">  <span class="attribute">padding-bottom</span>: <span class="number">4px</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/6ff432235df3/</id>
    <link href="https://blog.zisheng.pro/posts/6ff432235df3/"/>
    <published>2023-03-16T07:41:41.000Z</published>
    <summary>本文介绍了如何隐藏 antd Descriptions 组件 label 后面的冒号。</summary>
    <title>Antd Descriptions 组件隐藏 label 的冒号</title>
    <updated>2026-09-18T08:15:28.629Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="ESLint" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/ESLint/"/>
    <category term="ESLint" scheme="https://blog.zisheng.pro/tags/ESLint/"/>
    <category term="Typescript" scheme="https://blog.zisheng.pro/tags/Typescript/"/>
    <category term="import/no-unresolved" scheme="https://blog.zisheng.pro/tags/import-no-unresolved/"/>
    <content>
      <![CDATA[<p>我在使用 eslint 检查 typescript 代码时，遇到了一个问题，就是无法解析路径到模块（Unable to resolve path to module）：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> app <span class="keyword">from</span> <span class="string">&#x27;./app&#x27;</span>;</span><br></pre></td></tr></table></figure><p>报错：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">2:17  error  Unable to resolve path to module <span class="string">&#x27;./app&#x27;</span>  import/no-unresolved</span><br></pre></td></tr></table></figure><p>这个问题的解决步骤如下：</p><p>1、设置 parserOptions：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// .eslintrc.json</span></span><br><span class="line">&#123;</span><br><span class="line">  <span class="string">&quot;parser&quot;</span>: <span class="string">&quot;@typescript-eslint/parser&quot;</span>,</span><br><span class="line">  <span class="string">&quot;parserOptions&quot;</span>: &#123;</span><br><span class="line">    <span class="string">&quot;project&quot;</span>: <span class="string">&quot;./tsconfig.json&quot;</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>2、设置 <code>import/resolver</code>：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// .eslintrc.json</span></span><br><span class="line">&#123;</span><br><span class="line">  <span class="string">&quot;settings&quot;</span>: &#123;</span><br><span class="line">    <span class="string">&quot;import/resolver&quot;</span>: &#123;</span><br><span class="line">      <span class="string">&quot;node&quot;</span>: &#123;</span><br><span class="line">        <span class="string">&quot;extensions&quot;</span>: [<span class="string">&quot;.js&quot;</span>, <span class="string">&quot;.jsx&quot;</span>, <span class="string">&quot;.ts&quot;</span>, <span class="string">&quot;.tsx&quot;</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>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/a9cf5ec2f94c/</id>
    <link href="https://blog.zisheng.pro/posts/a9cf5ec2f94c/"/>
    <published>2023-03-14T06:18:48.000Z</published>
    <summary>我在使用 eslint 检查 typescript 代码时，遇到了一个问题，就是无法解析路径到模块（Unable to resolve path to module）</summary>
    <title>「已解决」Using eslint with typescript - Unable to resolve path to module</title>
    <updated>2026-09-18T08:15:28.632Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="TypeScript" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/TypeScript/"/>
    <category term="ESLint" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/ESLint/"/>
    <category term="React" scheme="https://blog.zisheng.pro/tags/React/"/>
    <category term="TypeScript" scheme="https://blog.zisheng.pro/tags/TypeScript/"/>
    <category term="ESLint" scheme="https://blog.zisheng.pro/tags/ESLint/"/>
    <category term="airbnb" scheme="https://blog.zisheng.pro/tags/airbnb/"/>
    <category term="enum" scheme="https://blog.zisheng.pro/tags/enum/"/>
    <content>
      <![CDATA[<h2 id="问题"><a href="#问题" class="headerlink" title="问题"></a>问题</h2><p>我正在使用 TypeScript 和 React 进行开发。这个项目是通过 CRA 生成的，并且我在 <code>.eslintrc.json</code> 中使用了 react-app 的配置。然而，我决定使用 <code>eslint-config-airbnb</code>，因此我添加了它并更改了我的 <code>.eslintrc.json</code> 文件。</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;extends&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">    <span class="string">&quot;airbnb&quot;</span><span class="punctuation">,</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><p>但是，我当我定义一个 <code>enum</code> 时，Eslint 抛出了 <code>Parsing error: The keyword enum is reserved</code> 的错误。</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="string">`Parsing error: The keyword &#x27;enum&#x27; is reserved eslint`</span></span><br></pre></td></tr></table></figure><h2 id="解决方案"><a href="#解决方案" class="headerlink" title="解决方案"></a>解决方案</h2><p>当你使用 <code>eslint-config-airbnb</code> 时，你需要安装 <code>@typescript-eslint/parser</code> 和 <code>@typescript-eslint/eslint-plugin</code>。</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">yarn add @typescript-eslint/parser -D</span><br></pre></td></tr></table></figure><p>然后，你需要在 <code>.eslintrc.json</code> 中添加以下内容：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;parser&quot;</span><span class="punctuation">:</span> <span class="string">&quot;@typescript-eslint/parser&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;plugins&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">    <span class="string">&quot;@typescript-eslint&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><p>此外你可能还需要安装 <code>eslint-plugin-import</code>、<code>eslint-plugin-react</code>、<code>eslint-plugin-jsx-a11y</code></p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">yarn add eslint-plugin-import eslint-plugin-react eslint-plugin-jsx-a11y -D</span><br></pre></td></tr></table></figure><p>然后，你需要在 <code>.eslintrc.json</code> 中添加以下内容：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;plugins&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">    <span class="string">&quot;react&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="string">&quot;jsx-a11y&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><h2 id="推荐方案"><a href="#推荐方案" class="headerlink" title="推荐方案"></a>推荐方案</h2><p>直接使用 <a href="https://www.npmjs.com/package/eslint-config-airbnb-typescript">eslint-config-airbnb-typescript</a> 快速配置。</p>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/2b120c3058c6/</id>
    <link href="https://blog.zisheng.pro/posts/2b120c3058c6/"/>
    <published>2023-03-14T04:10:32.000Z</published>
    <summary>本文介绍了如何解决 Eslint 警告 Parsing error: The keyword 'enum' is reserved</summary>
    <title>「已解决」Parsing error: The keyword 'enum' is reserved</title>
    <updated>2026-09-18T08:15:28.629Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="紫升翻译计划" scheme="https://blog.zisheng.pro/categories/%E7%B4%AB%E5%8D%87%E7%BF%BB%E8%AF%91%E8%AE%A1%E5%88%92/"/>
    <category term="React" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/React/"/>
    <category term="React" scheme="https://blog.zisheng.pro/tags/React/"/>
    <category term="React Router" scheme="https://blog.zisheng.pro/tags/React-Router/"/>
    <category term="SPA" scheme="https://blog.zisheng.pro/tags/SPA/"/>
    <category term="单页应用程序" scheme="https://blog.zisheng.pro/tags/%E5%8D%95%E9%A1%B5%E5%BA%94%E7%94%A8%E7%A8%8B%E5%BA%8F/"/>
    <content>
      <![CDATA[<!-- DRAFT 使用 React Router 优化单页应用程序（SPAs）  --><p>React 通常用于构建单页面应用程序（SPA）。 SPA 往往具有多个页面视图。当从一个页面视图导航到另一个页面视图时，重新加载整个页面视图是一项繁琐且不那么高效的任务。实际上，它会削弱 SPA 的优势。为了正常工作，SPA 必须在需要时呈现部分视图，而不是重新加载整个页面。</p><p>在单页应用程序（SPA）中从一个页面导航到另一个页面时，路由起着重要作用。路由可以分为静态和动态两种。SPA应用程序采用动态路由方式。<strong>在这个教程中，我们将会讨论一种流行的和React应用程序一起使用的路由库，它被称为 <a href="https://reacttraining.com/react-router/web/guides/quick-start">React Router</a></strong>。</p><details class="toggle" ><summary class="toggle-button" style="">原文</summary><div class="toggle-content"><p>React is often used for building single page applications (SPAs). SPAs tend to have multiple page views. When navigating from one-page view to another, reloading the entire page view is a tedious and not so efficient task. In fact, it diminishes the benefits of a SPA. To work as it should, a SPA must render parts of views when required instead of reloading the entire page.</p><p>Routing comes into the picture when navigating from one page to another in a SPA app. Routing can be categorized in two ways. Static and dynamic. SPAs follow dynamic approach. <strong>In this tutorial, we will discuss a popular routing library used with React applications known as <a href="https://reacttraining.com/react-router/web/guides/quick-start">React Router</a></strong>.</p><h1 id="Requirements"><a href="#Requirements" class="headerlink" title="Requirements"></a>Requirements</h1><ul><li>NodeJS v8.x.x or higher installed along with npm&#x2F;yarn</li><li>create-react-app installed globally to on your local dev machine generate a new React project</li></ul><p>Bonus: You can also, use npx to generate a new React project without installing create-react-app.</p><h1 id="Getting-Started"><a href="#Getting-Started" class="headerlink" title="Getting Started"></a>Getting Started</h1><p>To create a new React project run the following command at the desired location on your local machine.</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">create-react-app react-router-v4-demo</span><br></pre></td></tr></table></figure><p>Once the project is generated, traverse inside the newly created directory. This strategy is the default way to generate a new React app.</p><p>React Router as a library contains three different npm packages.</p><ul><li>react-router</li><li>react-router-dom</li><li>react-router-native</li></ul><p>Each of the packages has a different use case. The first one, <code>react-router</code> is the core package and is used with the next two packages listed above. The <code>react-router-dom</code> has to be used when building a web application. This is what we are going to use in this tutorial. The last one, <code>react-router-native</code> tends to be used in a <a href="https://medium.com/crowdbotics/how-to-build-a-real-time-logo-detection-app-with-react-native-google-vision-api-and-crowdbotics-9ed65fbcd15">React Native</a> application.</p><p>To add React Router in the React app, execute the following command from a terminal window.</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">yarn add react-router-dom</span><br></pre></td></tr></table></figure><p>Please note that, for the rest of the tutorial, we will be using yarn as the JavaScript package manager to add or remove dependencies. If you wish to use npm, there is no one stopping you.</p><p>To run the React app, go to the terminal window and execute the command npm start. This will start the development server. You will be welcomed by the default boilerplate project screen in a web browser window on URL <code>http://localhost:3000/</code>.</p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1678720760813.png" alt="Edit src&#x2F;App.js and save to reload"></p><h1 id="First-Route-with-React-Router"><a href="#First-Route-with-React-Router" class="headerlink" title="First Route with React Router"></a>First Route with React Router</h1><p>To create the first route in the React app, let us import BrowserRouter from react-router library.</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">React</span> <span class="keyword">from</span> <span class="string">&quot;react&quot;</span></span><br><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">BrowserRouter</span> <span class="keyword">as</span> <span class="title class_">Router</span>, <span class="title class_">Route</span> &#125; <span class="keyword">from</span> <span class="string">&quot;react-router-dom&quot;</span></span><br></pre></td></tr></table></figure><p>To create a route, we use <code>&lt;Route&gt;</code> from the react-router-dom. This is where the logic of routing is placed. It renders the UI of a component. A <code>&lt;Route&gt;</code> has a prop called path which is always matched with the current location of the app. On the basis of this prop, the desired component gets rendered. When the component is not getting rendered, Routereturns null. The component name is also passed as prop component. Look at the below snippet.</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><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">Router</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">Route</span> <span class="attr">path</span>=<span class="string">&#x27;/&#x27;</span> <span class="attr">component</span>=<span class="string">&#123;Home&#125;</span> /&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">Router</span>&gt;</span></span></span><br><span class="line">  )</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>There is the functional App component that returns a BrowserRouter which holds the very first Route component. The path is currently pointing towards the Home component which has the following UI logic.</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">Home</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>Home Component<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">div</span>&gt;</span></span></span><br><span class="line">    )</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>Now, visit the URL on port 3000 and you will see the Home component being rendered right now.</p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1678721815386.png" alt="紫升"></p><p>This is a bare minimum example. Now let us add another route with the same props like the Home. Call this route About with a similar rendering logic as Home.</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">About</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>About Component<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">div</span>&gt;</span></span></span><br><span class="line">  )</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>Now add this functional component as the second route, below the Home route. Also, add both routes inside a div element. A router component can hold a single child element and adding a div solves this problem and allows the router component to have as many children as we want.</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><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">Router</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">        <span class="tag">&lt;<span class="name">Route</span> <span class="attr">path</span>=<span class="string">&#x27;/&#x27;</span> <span class="attr">component</span>=<span class="string">&#123;Home&#125;</span> /&gt;</span></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">Route</span> <span class="attr">path</span>=<span class="string">&#x27;/about&#x27;</span> <span class="attr">component</span>=<span class="string">&#123;About&#125;</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">    <span class="tag">&lt;/<span class="name">Router</span>&gt;</span></span></span><br><span class="line">  )</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>Try visiting the URL <code>http://localhost:3000/about</code>. You will notice that both the components are being rendered right now on the path <code>/about</code>.</p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1678721636017.png" alt="紫升"></p><p>The reason for this is that regular expression engine that React Router uses internally considers both the routes that are being started with a forward slash &#x2F; equal. To solve this issue, we can use another essential prop on the Home route called exact.</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">&lt;<span class="title class_">Router</span>&gt;</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">Route</span> <span class="attr">path</span>=<span class="string">&#x27;/&#x27;</span> <span class="attr">exact</span> <span class="attr">component</span>=<span class="string">&#123;Home&#125;</span> /&gt;</span></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">Route</span> <span class="attr">path</span>=<span class="string">&#x27;/about&#x27;</span> <span class="attr">component</span>=<span class="string">&#123;About&#125;</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">&lt;/<span class="title class_">Router</span>&gt;</span><br></pre></td></tr></table></figure><p>This exact is also known as a qualifier which states that the path must match the exactly the &#x2F; and nothing after it, such as <code>/about</code> Now, if you visit the browser window at the URL <code>http://localhost:3000/about</code> you will notice that only the about component is getting rendered this time.</p><h1 id="What-is-BrowserRouter"><a href="#What-is-BrowserRouter" class="headerlink" title="What is BrowserRouter?"></a>What is BrowserRouter?</h1><p>Do you remember reading earlier about that <code>react-router-dom</code> is used only in case of web applications? Well, <code>react-router-dom</code> library holds two types of routers API for a React application to use. One is called BrowserRouter that you have seen in action in the previous section. The other one is called HashRouter.</p><p>A BrowserRouter will always listen to URLs like: <code>http://localhost:3000/about</code> whereas a HashRouter will have <code>http://localhost:3000/#/about</code>, as the name suggests, uses a hash # in between. So why did we use the BrowserRouter?</p><p>BrowserRouter is a popular choice among modern day web applications. The main reason behind is that it uses HTML5 History API to keep track of the router history of your React app. The HashRouter has a use case for legacy browsers where window.location.hash is still being used to keep a track of routes in a SPA.</p><h2 id="DIY-Exercise-👇"><a href="#DIY-Exercise-👇" class="headerlink" title="DIY Exercise 👇"></a>DIY Exercise 👇</h2><p>Here is a small task for you. Modify directory structure like below screenshot and separate the two functional components Home and about in their own component files such that, in future, if they grow with more JSX to render.</p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1678721953570.png" alt="紫升"></p><p>You can totally skip this if you want and move on to the next section. But performing this small task will benefit you to have an understanding of the above concepts.</p><h1 id="The-Link-Component"><a href="#The-Link-Component" class="headerlink" title="The Link Component"></a>The Link Component</h1><p>To navigate between to web pages in HTML, there is an <code>&lt;a href=&quot;&quot;&gt;&lt;/a&gt;</code> anchor tag available. However, using this traditional approach will lead to a browser refresh. In order to overcome this, React Router API offers a Link component that can be used to navigate to a particular URL or a component.</p><p>Let us try to create a navigation menu with this new knowledge. Import Link from <code>react-router-dom</code> in <code>App.js</code> file. Here is the modified snippet of App component.</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// App.js</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> <span class="title class_">React</span> <span class="keyword">from</span> <span class="string">&quot;react&quot;</span></span><br><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">BrowserRouter</span> <span class="keyword">as</span> <span class="title class_">Router</span>, <span class="title class_">Route</span>, <span class="title class_">Link</span> &#125; <span class="keyword">from</span> <span class="string">&quot;react-router-dom&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> <span class="title class_">Home</span> <span class="keyword">from</span> <span class="string">&quot;./components/Home&quot;</span></span><br><span class="line"><span class="keyword">import</span> <span class="title class_">About</span> <span class="keyword">from</span> <span class="string">&quot;./components/About&quot;</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">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">Router</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">        <span class="tag">&lt;<span class="name">nav</span> <span class="attr">style</span>=<span class="string">&#123;&#123;</span> <span class="attr">margin:</span> <span class="attr">10</span> &#125;&#125;&gt;</span></span></span><br><span class="line"><span class="language-xml">          <span class="tag">&lt;<span class="name">Link</span> <span class="attr">to</span>=<span class="string">&#x27;/&#x27;</span> <span class="attr">style</span>=<span class="string">&#123;&#123;</span> <span class="attr">padding:</span> <span class="attr">10</span> &#125;&#125;&gt;</span></span></span><br><span class="line"><span class="language-xml">            Home</span></span><br><span class="line"><span class="language-xml">          <span class="tag">&lt;/<span class="name">Link</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">Link</span> <span class="attr">to</span>=<span class="string">&#x27;/about&#x27;</span> <span class="attr">style</span>=<span class="string">&#123;&#123;</span> <span class="attr">padding:</span> <span class="attr">10</span> &#125;&#125;&gt;</span></span></span><br><span class="line"><span class="language-xml">            About</span></span><br><span class="line"><span class="language-xml">          <span class="tag">&lt;/<span class="name">Link</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;/<span class="name">nav</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">Route</span> <span class="attr">path</span>=<span class="string">&#x27;/&#x27;</span> <span class="attr">exact</span> <span class="attr">component</span>=<span class="string">&#123;Home&#125;</span> /&gt;</span></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">Route</span> <span class="attr">path</span>=<span class="string">&#x27;/about&#x27;</span> <span class="attr">component</span>=<span class="string">&#123;About&#125;</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">    <span class="tag">&lt;/<span class="name">Router</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 class_">App</span></span><br></pre></td></tr></table></figure><p>In the above snippet, notice that all the Links are being added before all the Route components. The styling attributes inside style are optional for now. Start the development server and visit the browser window and you will notice a navigation menu pops up at the top. Try clicking links to navigate between different components.</p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1678722050699.png" alt="紫升"></p><p>Wherever a <code>Link</code> is rendered in a React project, an anchor <code>&lt;a&gt;</code> will be rendered in the application’s HTML.</p><h1 id="Active-Links-with-NavLink"><a href="#Active-Links-with-NavLink" class="headerlink" title="Active Links with NavLink"></a>Active Links with NavLink</h1><p>In React Router API, NavLink is the extended version of the <code>Link</code> component. You can say that is a special type of the Link that can style itself as to represent the active state when matches the current route.</p><p>To demonstrate this, first, let us replace all the <code>Link</code> tags with NavLink in <code>App.js</code> file.</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// App.js</span></span><br><span class="line"><span class="keyword">import</span> <span class="title class_">React</span> <span class="keyword">from</span> <span class="string">&quot;react&quot;</span></span><br><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">BrowserRouter</span> <span class="keyword">as</span> <span class="title class_">Router</span>, <span class="title class_">Route</span>, <span class="title class_">NavLink</span> &#125; <span class="keyword">from</span> <span class="string">&quot;react-router-dom&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> <span class="title class_">Home</span> <span class="keyword">from</span> <span class="string">&quot;./components/Home&quot;</span></span><br><span class="line"><span class="keyword">import</span> <span class="title class_">About</span> <span class="keyword">from</span> <span class="string">&quot;./components/About&quot;</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">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">Router</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">        <span class="tag">&lt;<span class="name">nav</span> <span class="attr">style</span>=<span class="string">&#123;&#123;</span> <span class="attr">margin:</span> <span class="attr">10</span> &#125;&#125;&gt;</span></span></span><br><span class="line"><span class="language-xml">          <span class="tag">&lt;<span class="name">NavLink</span> <span class="attr">to</span>=<span class="string">&#x27;/&#x27;</span> <span class="attr">style</span>=<span class="string">&#123;&#123;</span> <span class="attr">padding:</span> <span class="attr">10</span> &#125;&#125;&gt;</span></span></span><br><span class="line"><span class="language-xml">            Home</span></span><br><span class="line"><span class="language-xml">          <span class="tag">&lt;/<span class="name">NavLink</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">NavLink</span> <span class="attr">to</span>=<span class="string">&#x27;/about&#x27;</span> <span class="attr">style</span>=<span class="string">&#123;&#123;</span> <span class="attr">padding:</span> <span class="attr">10</span> &#125;&#125;&gt;</span></span></span><br><span class="line"><span class="language-xml">            About</span></span><br><span class="line"><span class="language-xml">          <span class="tag">&lt;/<span class="name">NavLink</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;/<span class="name">nav</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">Route</span> <span class="attr">path</span>=<span class="string">&#x27;/&#x27;</span> <span class="attr">exact</span> <span class="attr">component</span>=<span class="string">&#123;Home&#125;</span> /&gt;</span></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">Route</span> <span class="attr">path</span>=<span class="string">&#x27;/about&#x27;</span> <span class="attr">component</span>=<span class="string">&#123;About&#125;</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">    <span class="tag">&lt;/<span class="name">Router</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 class_">App</span></span><br></pre></td></tr></table></figure><p>At this point, each NavLink link is going to behave like an ordinary Link component which means there is no change so far. To make a link active, add an activeClassName prop to that. Such as below.</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&lt;<span class="title class_">NavLink</span> to=<span class="string">&#x27;/&#x27;</span> style=&#123;&#123; <span class="attr">padding</span>: <span class="number">10</span> &#125;&#125; activeClassName=<span class="string">&#x27;active&#x27;</span>&gt;</span><br></pre></td></tr></table></figure><p>To set up the corresponding CSS for this to work, open <code>App.css</code> file and add the below.</p><figure class="highlight css"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="selector-tag">a</span> &#123;</span><br><span class="line">  <span class="attribute">padding</span>: <span class="number">10px</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="selector-tag">a</span>,</span><br><span class="line"><span class="selector-tag">a</span><span class="selector-pseudo">:visited</span> &#123;</span><br><span class="line">  <span class="attribute">color</span>: blue;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="selector-tag">a</span><span class="selector-class">.active</span> &#123;</span><br><span class="line">  <span class="attribute">color</span>: red;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>Do not forget to import this file inside <code>App.js</code>. Also, modify the about route to have an activeClassName.</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="string">&quot;./App.css&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// ...</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> (</span><br><span class="line">  &#123;<span class="comment">/* */</span>&#125;</span><br><span class="line">  &lt;nav style=&#123;&#123; <span class="attr">margin</span>: <span class="number">10</span> &#125;&#125;&gt;</span><br><span class="line">      <span class="language-xml"><span class="tag">&lt;<span class="name">NavLink</span> <span class="attr">to</span>=<span class="string">&#x27;/&#x27;</span> <span class="attr">activeClassName</span>=<span class="string">&#x27;active&#x27;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">          Home</span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;/<span class="name">NavLink</span>&gt;</span></span></span><br><span class="line"></span><br><span class="line">      <span class="language-xml"><span class="tag">&lt;<span class="name">NavLink</span> <span class="attr">to</span>=<span class="string">&#x27;/about&#x27;</span> <span class="attr">activeClassName</span>=<span class="string">&#x27;active&#x27;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">          About</span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;/<span class="name">NavLink</span>&gt;</span></span></span><br><span class="line">  &lt;/nav&gt;</span><br><span class="line"></span><br><span class="line"> &#123;<span class="comment">/* */</span>&#125;</span><br><span class="line">)</span><br></pre></td></tr></table></figure><p>Go back to the browser, open develop tools like below and you will notice, at first, the Home route has a class name active.</p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1678722233048.png" alt="紫升"></p><p>Try navigating to the About route and see what happens.</p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1678722243666.png" alt="紫升"></p><p>On navigating to About route did you notice that the active class name is also added to the corresponding route? However, the Home route still has the active class even though the URL matches the <code>/about</code>. Why?</p><p>The way NavLink works is almost similar to Route component in React Router API. To make sure that only one route has the class active state, try modifying the home route in the navigation menu, as below.</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">//App.js</span></span><br><span class="line"></span><br><span class="line">&lt;<span class="title class_">NavLink</span> to=<span class="string">&#x27;/&#x27;</span> exact activeClassName=<span class="string">&#x27;active&#x27;</span>&gt;</span><br><span class="line">  <span class="title class_">Home</span></span><br><span class="line">&lt;/<span class="title class_">NavLink</span>&gt;</span><br></pre></td></tr></table></figure><p>You will get the desired output this time.</p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1678722287934.png" alt="紫升"></p><h1 id="Adding-Parameters-to-the-Routes"><a href="#Adding-Parameters-to-the-Routes" class="headerlink" title="Adding Parameters to the Routes"></a>Adding Parameters to the Routes</h1><p>In this section, you will learn how to create and manage dynamic routes based on a query parameter such as <code>:id</code>. We start by creating a static array in <code>App.js</code> file that will serve as the mock data.</p><p>The idea is to demonstrate a route as <code>/posts</code> which displays all the posts that are coming from the array. However, each post in the array will be having an id or a unique identifier. Using that unique identifier, you will be approaching the concept of dynamic content rendering by writing the logic for URLs such as <code>/posts/:id</code> where :id will be represented by the specific id of a post.</p><p>To start, let us add a bunch of mock posts in the state inside a new component file called <code>components/posts.js</code>.</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Posts.js</span></span><br><span class="line"><span class="keyword">import</span> <span class="title class_">React</span> <span class="keyword">from</span> <span class="string">&quot;react&quot;</span></span><br><span class="line"><span class="keyword">import</span> <span class="string">&quot;../App.css&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">Posts</span> <span class="keyword">extends</span> <span class="title class_ inherited__">React.Component</span> &#123;</span><br><span class="line">  state = &#123;</span><br><span class="line">    <span class="attr">posts</span>: [</span><br><span class="line">      &#123; <span class="attr">id</span>: <span class="number">1</span>, <span class="attr">title</span>: <span class="string">&quot;Hello Blog World!&quot;</span> &#125;,</span><br><span class="line">      &#123; <span class="attr">id</span>: <span class="number">2</span>, <span class="attr">title</span>: <span class="string">&quot;My second post&quot;</span> &#125;,</span><br><span class="line">      &#123; <span class="attr">id</span>: <span class="number">3</span>, <span class="attr">title</span>: <span class="string">&quot;What is React Router?&quot;</span> &#125;</span><br><span class="line">    ]</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="title function_">render</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> <span class="attr">className</span>=<span class="string">&#x27;posts&#x27;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">h1</span>&gt;</span>Posts List<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">div</span>&gt;</span></span></span><br><span class="line">    )</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_">Posts</span></span><br></pre></td></tr></table></figure><p>The corresponding styles to the above are added in <code>App.css</code> file for brevity.</p><figure class="highlight css"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="selector-class">.posts</span> <span class="selector-tag">ul</span> &#123;</span><br><span class="line">  <span class="attribute">list-style</span>: none;</span><br><span class="line">  <span class="attribute">margin</span>: <span class="number">0</span>;</span><br><span class="line">  <span class="attribute">margin-bottom</span>: <span class="number">20px</span>;</span><br><span class="line">  <span class="attribute">padding</span>: <span class="number">0</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="selector-class">.posts</span> <span class="selector-tag">ul</span> <span class="selector-tag">li</span> &#123;</span><br><span class="line">  <span class="attribute">padding</span>: <span class="number">10px</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="selector-class">.posts</span> <span class="selector-tag">a</span> &#123;</span><br><span class="line">  <span class="attribute">text-decoration</span>: none;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>Now, import the newly created component inside App.js where other routes already exist.</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">//App.js</span></span><br><span class="line"><span class="comment">// ...</span></span><br><span class="line"><span class="keyword">import</span> <span class="title class_">Posts</span> <span class="keyword">from</span> <span class="string">&quot;./components/Posts&quot;</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">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">Router</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">        <span class="tag">&lt;<span class="name">nav</span> <span class="attr">style</span>=<span class="string">&#123;&#123;</span> <span class="attr">margin:</span> <span class="attr">10</span> &#125;&#125;&gt;</span></span></span><br><span class="line"><span class="language-xml">          <span class="tag">&lt;<span class="name">NavLink</span> <span class="attr">to</span>=<span class="string">&#x27;/&#x27;</span> <span class="attr">exact</span> <span class="attr">activeClassName</span>=<span class="string">&#x27;active&#x27;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">            Home</span></span><br><span class="line"><span class="language-xml">          <span class="tag">&lt;/<span class="name">NavLink</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">          <span class="tag">&lt;<span class="name">NavLink</span> <span class="attr">to</span>=<span class="string">&#x27;/about&#x27;</span> <span class="attr">activeClassName</span>=<span class="string">&#x27;active&#x27;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">            About</span></span><br><span class="line"><span class="language-xml">          <span class="tag">&lt;/<span class="name">NavLink</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">          <span class="tag">&lt;<span class="name">NavLink</span> <span class="attr">to</span>=<span class="string">&#x27;/posts&#x27;</span> <span class="attr">activeClassName</span>=<span class="string">&#x27;active&#x27;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">            Posts</span></span><br><span class="line"><span class="language-xml">          <span class="tag">&lt;/<span class="name">NavLink</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;/<span class="name">nav</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">Route</span> <span class="attr">path</span>=<span class="string">&#x27;/&#x27;</span> <span class="attr">exact</span> <span class="attr">component</span>=<span class="string">&#123;Home&#125;</span> /&gt;</span></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">Route</span> <span class="attr">path</span>=<span class="string">&#x27;/about&#x27;</span> <span class="attr">component</span>=<span class="string">&#123;About&#125;</span> /&gt;</span></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">Route</span> <span class="attr">path</span>=<span class="string">&#x27;/posts&#x27;</span> <span class="attr">component</span>=<span class="string">&#123;Posts&#125;</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">    <span class="tag">&lt;/<span class="name">Router</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 class_">App</span></span><br></pre></td></tr></table></figure><p>The existing navigation menu has a new route and its called Posts.</p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1678722701566.png" alt="紫升"></p><p>Open Posts.js to rend er the list of Posts and display them as a list whenever the current location in the web browser matches <code>/posts</code>.</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">React</span> <span class="keyword">from</span> <span class="string">&quot;react&quot;</span></span><br><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">Link</span>, <span class="title class_">Route</span> &#125; <span class="keyword">from</span> <span class="string">&quot;react-router-dom&quot;</span></span><br><span class="line"><span class="keyword">import</span> <span class="string">&quot;../App.css&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">Child</span>(<span class="params">&#123; match &#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">h3</span>&gt;</span>ID: &#123;match.params.id&#125;<span class="tag">&lt;/<span class="name">h3</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><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">Posts</span> <span class="keyword">extends</span> <span class="title class_ inherited__">React.Component</span> &#123;</span><br><span class="line">  state = &#123;</span><br><span class="line">    <span class="attr">posts</span>: [</span><br><span class="line">      &#123;</span><br><span class="line">        <span class="attr">id</span>: <span class="number">1</span>,</span><br><span class="line">        <span class="attr">title</span>: <span class="string">&quot;Hello Blog World!&quot;</span></span><br><span class="line">      &#125;,</span><br><span class="line">      &#123;</span><br><span class="line">        <span class="attr">id</span>: <span class="number">2</span>,</span><br><span class="line">        <span class="attr">title</span>: <span class="string">&quot;My second post&quot;</span></span><br><span class="line">      &#125;,</span><br><span class="line">      &#123;</span><br><span class="line">        <span class="attr">id</span>: <span class="number">3</span>,</span><br><span class="line">        <span class="attr">title</span>: <span class="string">&quot;What is React Router?&quot;</span></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="title function_">render</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> &#123; posts &#125; = <span class="variable language_">this</span>.<span class="property">state</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">div</span> <span class="attr">className</span>=<span class="string">&#x27;posts&#x27;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">h1</span>&gt;</span>Posts List<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">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></span></span><br><span class="line"><span class="language-xml">              <span class="tag">&lt;<span class="name">Link</span> <span class="attr">to</span>=<span class="string">&#123;</span>`/<span class="attr">posts</span>/$&#123;<span class="attr">post.id</span>&#125;`&#125;&gt;</span>&#123;post.title&#125;<span class="tag">&lt;/<span class="name">Link</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">            <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">Route</span> <span class="attr">path</span>=<span class="string">&#x27;/posts/:id&#x27;</span> <span class="attr">component</span>=<span class="string">&#123;Child&#125;</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><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_">Posts</span></span><br></pre></td></tr></table></figure><p>Also, the Child component reads anything coming from the URL parameters, such as, in the above case, the id of each post. A match object contains information about how a <code>&lt;Route path&gt;</code> matched the URL, thus, in our case, the id of each post.</p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1678722789204.png" alt="紫升"></p><h1 id="Conclusion"><a href="#Conclusion" class="headerlink" title="Conclusion"></a>Conclusion</h1><p>Hopefully, by now, you are familiar with the basic concepts of how React Router library works. It is a powerful library that helps you build better React apps. If you want to learn more about React Router visit its official documentation here.</p><p>You can find the complete for the tutorial at this Github repo.</p></div></details><blockquote><p>原文地址：<a href="https://www.crowdbotics.com/blog/introduction-to-react-router-v4-with-reactjs">https://www.crowdbotics.com/blog/introduction-to-react-router-v4-with-reactjs</a><br>原文作者：<a href="https://www.crowdbotics.com/author/amanhimself">amanhimself</a><br>译文出自：<a href="https://blog.zisheng.pro/categories/%E6%B4%9B%E7%AB%B9%E7%BF%BB%E8%AF%91%E8%AE%A1%E5%88%92/">紫升翻译计划</a></p></blockquote>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/3c335a370ab4/</id>
    <link href="https://blog.zisheng.pro/posts/3c335a370ab4/"/>
    <published>2023-03-13T15:12:00.000Z</published>
    <summary>React 通常用于构建单页面应用程序（SPA）。 SPA 往往具有多个页面视图。当从一个页面视图导航到另一个页面视图时，重新加载整个页面视图是一项繁琐且不那么高效的任务。实际上，它会削弱 SPA 的优势。为了正常工作，SPA 必须在需要时呈现部分视图，而不是重新加载整个页面。</summary>
    <title>使用 React Router 优化单页应用程序（SPAs）</title>
    <updated>2026-09-18T08:15:28.632Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="紫升翻译计划" scheme="https://blog.zisheng.pro/categories/%E7%B4%AB%E5%8D%87%E7%BF%BB%E8%AF%91%E8%AE%A1%E5%88%92/"/>
    <category term="问题排查" scheme="https://blog.zisheng.pro/categories/%E9%97%AE%E9%A2%98%E6%8E%92%E6%9F%A5/"/>
    <category term="Node.js" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/Node-js/"/>
    <category term="Node.js" scheme="https://blog.zisheng.pro/tags/Node-js/"/>
    <category term="EADDRINUSE" scheme="https://blog.zisheng.pro/tags/EADDRINUSE/"/>
    <category term="address already in use" scheme="https://blog.zisheng.pro/tags/address-already-in-use/"/>
    <category term="进程" scheme="https://blog.zisheng.pro/tags/%E8%BF%9B%E7%A8%8B/"/>
    <content>
      <![CDATA[<h2 id="问题"><a href="#问题" class="headerlink" title="问题"></a>问题</h2><p>当你尝试重启一个 Node 应用时，上一个应用没有正确关闭，你可能会看到一个“listen EADDRINUSE: address already in use”错误，例如：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line">⇒  npm start</span><br><span class="line">&gt; react-app-es6-jest@0.0.1 start /Users/admin/Code/WorkSpace/react-app-es6-jest</span><br><span class="line">&gt; concurrently <span class="string">&quot;npm run start:server&quot;</span> <span class="string">&quot;npm run start:client&quot;</span></span><br><span class="line"></span><br><span class="line">[0]</span><br><span class="line">[0] &gt; react-app-es6-jest@0.0.1 start:server /Users/admin/Code/WorkSpace/react-app-es6-jest</span><br><span class="line">[0] &gt; nodemon src/server/index.js --watch src/server -e js --<span class="built_in">exec</span> babel-node</span><br><span class="line">[0]</span><br><span class="line">[0] [nodemon] 2.0.1</span><br><span class="line">[0] [nodemon] to restart at any time, enter `rs`</span><br><span class="line">[0] [nodemon] watching <span class="built_in">dir</span>(s): src/server/**/*</span><br><span class="line">[0] [nodemon] watching extensions: js</span><br><span class="line">[0] [nodemon] starting `babel-node src/server/index.js`</span><br><span class="line">[0] events.js:174</span><br><span class="line">[0]       throw er; // Unhandled <span class="string">&#x27;error&#x27;</span> event</span><br><span class="line">[0]       ^</span><br><span class="line">[0]</span><br><span class="line">[0] Error: listen EADDRINUSE: address already <span class="keyword">in</span> use :::3000</span><br><span class="line">[0]     at Server.setupListenHandle [as _listen2] (net.js:1279:14)</span><br><span class="line">[0]     at listenInCluster (net.js:1327:12)</span><br><span class="line">[0]     at Server.listen (net.js:1414:7)</span><br><span class="line">[0]     at Function.listen (/Users/admin/Code/WorkSpace/react-app-es6-jest/node_modules/express/lib/application.js:618:24)</span><br><span class="line">[0] [nodemon] app crashed - waiting <span class="keyword">for</span> file changes before starting...</span><br></pre></td></tr></table></figure><h2 id="问题背后的原因"><a href="#问题背后的原因" class="headerlink" title="问题背后的原因"></a>问题背后的原因</h2><p><code>process.on(&#39;exit&#39;, ...)</code> 不会在进程崩溃或被杀死时调用。它只在事件循环结束时调用，因为 <code>server.close()</code> 会结束事件循环（它仍然必须等待当前运行的堆栈），所以将其放在 exit 事件中是没有意义的。</p><blockquote><p>译者注：有时候直接关闭 vscode 会导致进程没有正确关闭，这时候也可能会出现这个问题。</p></blockquote><h2 id="解决方案"><a href="#解决方案" class="headerlink" title="解决方案"></a>解决方案</h2><p>该应用程序的正确修复是</p><ul><li>在 <code>server.listen()</code> 之前检查端口是否已经被占用</li><li>在崩溃时，可以使用 <code>process.on(&#39;uncaughtException&#39;, ..)</code> 进行处理。</li><li>在 <code>kill</code> 操作上，可以使用 <code>process.on(&#39;SIGTERM&#39;, ..)</code> 进行处理。</li></ul><p>当出现 EADDRINUSE 问题时，为了解决它，你需要手动终止该进程。为此，你需要找到进程的进程 ID（PID）。</p><h2 id="手动杀死进程"><a href="#手动杀死进程" class="headerlink" title="手动杀死进程"></a>手动杀死进程</h2><h3 id="Mac-Linux"><a href="#Mac-Linux" class="headerlink" title="Mac&#x2F;Linux"></a>Mac&#x2F;Linux</h3><p>找到与该端口关联的进程 ID（PID）</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">$ lsof -i tcp:3000</span><br><span class="line">COMMAND PID   USER  FD  TYPE DEVICE             SIZE/OFF NODE NAME</span><br><span class="line">node    44475 chen5 31u IPv4 0x8b1721168764e4bf 0t0 TCP *:strexec-s (LISTEN)</span><br></pre></td></tr></table></figure><p>这将返回使用该端口的任何进程的详细信息，包括PID。然后可以使用以下命令杀死该进程：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ <span class="built_in">kill</span> -9 44475</span><br></pre></td></tr></table></figure><p>请注意，<code>-9</code> 选项强制终止进程而不考虑其当前状态，并立即使其退出。当需要强制关闭一个已经挂起或占据端口的进程时，通常需要使用此选项。</p><p>如果你遇到权限问题，请尝试在命令前加上sudo。例如：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sudo <span class="built_in">kill</span> -9 44475</span><br></pre></td></tr></table></figure><h3 id="Windows"><a href="#Windows" class="headerlink" title="Windows"></a>Windows</h3><h4 id="解决方案-1：任务管理器"><a href="#解决方案-1：任务管理器" class="headerlink" title="解决方案 1：任务管理器"></a>解决方案 1：任务管理器</h4><p>打开“任务管理器”应用程序 (taskman.exe)，从进程或服务选项卡根据 PID 列进行排序。要显示 PID 列，请右键单击标头行并从列表中选择 PID。右键单击要停止的进程，然后选择“结束任务”</p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1678416348850.png" alt="example in Windows, source from Internet"></p><h4 id="解决方案-2：命令行"><a href="#解决方案-2：命令行" class="headerlink" title="解决方案 2：命令行"></a>解决方案 2：命令行</h4><p>以管理员身份打开CMD（命令提示符）窗口，通过导航到 <code>开始 &gt; 运行 &gt; 输入cmd &gt; 右键单击“命令提示符”</code>，然后选择“以管理员身份运行”打开 。</p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1678416450947.png" alt="source from Google Search"></p><p>可以使用命令提示符来查找与特定端口相关的进程ID。在管理员模式下打开CMD窗口，然后使用” netstat “命令来获取所有处于活动状态的端口以及占用者的进程ID。以下是要使用的完整命令:</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">$ netstat -ano|findstr <span class="string">&quot;PID :3000&quot;</span></span><br><span class="line">Proto Local Address Foreign Address State PID</span><br><span class="line">TCP 0.0.0.0:3000 0.0.0.0:0 LISTENING 18264</span><br></pre></td></tr></table></figure><p>其中 <code>-a</code> 选项显示所有正在使用的端口（而不仅仅是与当前用户相关的端口）。<code>-n</code> 选项停止主机名查询（这需要很长时间）。<code>-o</code> 选项列出负责端口活动的进程 ID。最后，<code>findstr</code> 命令匹配包含 PID 字符串的标题行，并查找指定的端口。例如， <code>PID :3000</code>。</p><p>然后终止此进程（<code>/f</code> 是强制）：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ taskkill /pid 18264 /f</span><br></pre></td></tr></table></figure><blockquote><p>原文地址：<a href="https://114.li/jscdep">How to kill server when seeing “EADDRINUSE: address already in use”</a><br>原文作者：<a href="https://bindichen.medium.com/">BChen</a><br>译文出自：<a href="https://blog.zisheng.pro/categories/%E6%B4%9B%E7%AB%B9%E7%BF%BB%E8%AF%91%E8%AE%A1%E5%88%92/">紫升翻译计划</a></p></blockquote>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/4475c84c4024/</id>
    <link href="https://blog.zisheng.pro/posts/4475c84c4024/"/>
    <published>2023-03-09T18:25:43.000Z</published>
    <summary>本文介绍了如何解决 EADDRINUSE: address already in use</summary>
    <title>「已解决」EADDRINUSE: address already in use</title>
    <updated>2026-09-18T08:15:28.632Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="npm" scheme="https://blog.zisheng.pro/tags/npm/"/>
    <category term="JavaScript" scheme="https://blog.zisheng.pro/tags/JavaScript/"/>
    <category term="lerna" scheme="https://blog.zisheng.pro/tags/lerna/"/>
    <category term="yarn" scheme="https://blog.zisheng.pro/tags/yarn/"/>
    <content>
      <![CDATA[<p>将大型代码仓库分割成多个独立版本化的 软件包（package）对于代码共享来说非常有用。但是，如果某些更改 跨越了多个代码仓库的话将变得很麻烦并且难以跟踪，并且，跨越多个代码仓库的测试将迅速变得非常复杂。</p><p>为了解决这些（以及许多其它）问题，某些项目会将 代码仓库分割成多个软件包（package），并将每个软件包存放到独立的代码仓库中。但是，例如 Babel、 React、Angular、Ember、Meteor、Jest 等项目以及许多其他项目则是在 一个代码仓库中包含了多个软件包（package）并进行开发。</p><p>Lerna 是一种工具，针对 使用 git 和 npm 管理多软件包代码仓库的工作流程进行优化。</p><h2 id="开始"><a href="#开始" class="headerlink" title="开始"></a>开始</h2><h3 id="全局安装-lerna"><a href="#全局安装-lerna" class="headerlink" title="全局安装 lerna"></a>全局安装 lerna</h3><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ npm install lerna -g</span><br></pre></td></tr></table></figure><h3 id="初始化-lerna-项目"><a href="#初始化-lerna-项目" class="headerlink" title="初始化 lerna 项目"></a>初始化 lerna 项目</h3><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ lerna init --independent</span><br></pre></td></tr></table></figure><p>你的代码仓库目前应该是如下结构：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">lerna-repo/</span><br><span class="line">  packages/</span><br><span class="line">  package.json</span><br><span class="line">  lerna.json</span><br></pre></td></tr></table></figure><h2 id="创建-package"><a href="#创建-package" class="headerlink" title="创建 package"></a>创建 package</h2><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">$ lerna create module-1</span><br><span class="line">$ lerna create module-2</span><br></pre></td></tr></table></figure><h2 id="yarn-workspaces-Lerna-Hoisting"><a href="#yarn-workspaces-Lerna-Hoisting" class="headerlink" title="yarn workspaces &amp; Lerna Hoisting"></a>yarn workspaces &amp; Lerna Hoisting</h2><p>使用 <a href="https://yarnpkg.com/lang/zh-Hans/docs/workspaces/">yarn workspaces</a> 结合 Lerna <code>useWorkspaces</code> 可以实现 <a href="https://github.com/lerna/lerna/blob/main/doc/hoist.md">Lerna Hoisting</a>。这并不是多此一举，这可以让你在统一的地方（根目录）管理依赖，这即节省时间又节省空间。</p><p>配置 lerna.json:</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  ...</span><br><span class="line">  <span class="attr">&quot;npmClient&quot;</span><span class="punctuation">:</span> <span class="string">&quot;yarn&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;useWorkspaces&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><br></pre></td></tr></table></figure><p>顶级 package.json 必须包含一个 workspaces 数组:</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;private&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><br><span class="line">  <span class="attr">&quot;workspaces&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span><span class="string">&quot;packages/*&quot;</span><span class="punctuation">]</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><h2 id="npm-registry"><a href="#npm-registry" class="headerlink" title="npm registry"></a>npm registry</h2><h3 id="搭建-verdaccio"><a href="#搭建-verdaccio" class="headerlink" title="搭建 verdaccio"></a>搭建 verdaccio</h3><blockquote><p>verdaccio 是一个开源轻量的 npm 私服</p></blockquote><p>全局安装：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ npm install verdaccio -g</span><br></pre></td></tr></table></figure><p>配置 <code>~/.config/verdaccio/config.yaml</code> uplinks:</p><figure class="highlight yml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">---</span></span><br><span class="line"><span class="comment"># a list of other known repositories we can talk to</span></span><br><span class="line"><span class="attr">uplinks:</span></span><br><span class="line">  <span class="attr">npmjs:</span></span><br><span class="line">    <span class="attr">url:</span> <span class="string">https://registry.npmjs.org/</span></span><br><span class="line">  <span class="attr">taobao:</span></span><br><span class="line">    <span class="attr">url:</span> <span class="string">https://registry.npm.taobao.org/</span></span><br></pre></td></tr></table></figure><h3 id="设置开机自启动"><a href="#设置开机自启动" class="headerlink" title="设置开机自启动"></a>设置开机自启动</h3><p>0、run <code>sudo npm i -g pm2</code></p><p>1、run <code>pm2 start verdaccio</code> &amp; <code>pm2 startup</code></p><p>outputs:</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">[PM2] Init System found: launchd</span><br><span class="line">[PM2] To setup the Startup Script, copy/paste the following <span class="built_in">command</span>:</span><br><span class="line">sudo <span class="built_in">env</span> PATH=<span class="variable">$PATH</span>:/usr/local/bin /usr/local/lib/node_modules/pm2/bin/pm2 startup launchd -u luozhu --hp /Users/luozhu</span><br></pre></td></tr></table></figure><p>2、run <code>sudo env PATH=$PATH:/usr/local/bin /usr/local/lib/node_modules/pm2/bin/pm2 startup launchd -u luozhu --hp /Users/luozhu</code></p><p>outputs:</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">[PM2] Freeze a process list on reboot via:</span><br><span class="line">$ pm2 save</span><br><span class="line"></span><br><span class="line">[PM2] Remove init script via:</span><br><span class="line">$ pm2 unstartup launchd</span><br></pre></td></tr></table></figure><h3 id="修改-lerna-publishConfig"><a href="#修改-lerna-publishConfig" class="headerlink" title="修改 lerna publishConfig"></a>修改 lerna publishConfig</h3><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// lerna.json</span></span><br><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;ignoreChanges&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">    <span class="string">&quot;ignored-file&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="string">&quot;**/__tests__/**&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="string">&quot;**/*.md&quot;</span></span><br><span class="line">  <span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;command&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;publish&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">      <span class="attr">&quot;registry&quot;</span><span class="punctuation">:</span> <span class="string">&quot;http://localhost:4873&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;version&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">      <span class="attr">&quot;conventionalCommits&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;message&quot;</span><span class="punctuation">:</span> <span class="string">&quot;chore(release): publish&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">&#125;</span></span><br></pre></td></tr></table></figure><blockquote><p>注意：如果子包是带 <code>scope</code> 的，需要为子包设置 <code>publishConfig.access</code></p></blockquote><h2 id="commitlint-commitizen"><a href="#commitlint-commitizen" class="headerlink" title="commitlint &amp; commitizen"></a>commitlint &amp; commitizen</h2><blockquote><p>请参考我之前的文章 <a href="https://juejin.im/post/6877462747631026190">一文搞定规范化 Git Commit</a></p></blockquote><h2 id="开发流程"><a href="#开发流程" class="headerlink" title="开发流程"></a>开发流程</h2><h3 id="install"><a href="#install" class="headerlink" title="install"></a>install</h3><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ yarn install</span><br></pre></td></tr></table></figure><h3 id="package-依赖"><a href="#package-依赖" class="headerlink" title="package 依赖"></a>package 依赖</h3><p>给指定 package 安装依赖：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 或者（推荐）</span></span><br><span class="line">$ lerna add lodash packages/module-1</span><br><span class="line"><span class="comment"># 或者</span></span><br><span class="line">$ lerna add lodash --scope=module-1</span><br><span class="line"><span class="comment"># 或者</span></span><br><span class="line">$ lerna add lodash **/module-1</span><br><span class="line"><span class="comment"># 或者</span></span><br><span class="line">$ yarn workspace module-1 add lodash</span><br></pre></td></tr></table></figure><p>给所有 package 安装依赖：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ lerna add lodash</span><br></pre></td></tr></table></figure><h3 id="workspace-之间的依赖"><a href="#workspace-之间的依赖" class="headerlink" title="workspace 之间的依赖"></a>workspace 之间的依赖</h3><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">$ lerna add module-2 --scope module-1</span><br><span class="line"><span class="comment"># 或者</span></span><br><span class="line">$ lerna add module-2 packages/module-1</span><br></pre></td></tr></table></figure><h3 id="共用的工具依赖"><a href="#共用的工具依赖" class="headerlink" title="共用的工具依赖"></a>共用的工具依赖</h3><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ yarn add -WD typescript</span><br></pre></td></tr></table></figure><h2 id="lerna-json"><a href="#lerna-json" class="headerlink" title="lerna.json"></a>lerna.json</h2><ul><li>version: 当前仓库的版本，Independent mode 请设置为 <code>independent</code></li><li>npmClient: 指定运行命令的客户端程序（默认是 npm）</li><li>ignoreChanges: 一个不包含在 <code>lerna changed/publish</code> 的 glob 数组。使用这个去阻止发布不必要的更新，比如修复 <code>README.md</code></li><li>command<ul><li>publish<ul><li>registry：设置自定义的 npm 代理（比如使用 verdaccio 搭建的私服）</li><li>conventionalCommits：<code>lerna version</code> 会自动决定 version bump 和生成 CHANGELOG 文件</li><li>message：一个 publish 时的自定义 commit 信息。详情请查看<a href="https://github.com/lerna/lerna/blob/main/commands/version#--message-msg">@lerna&#x2F;version</a></li></ul></li></ul></li></ul><h2 id="npm-scripts"><a href="#npm-scripts" class="headerlink" title="npm scripts"></a>npm scripts</h2><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><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;release:beta&quot;</span><span class="punctuation">:</span> <span class="string">&quot;lerna publish --canary --pre-dist-tag=beta --preid=beta --yes&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;release:rc&quot;</span><span class="punctuation">:</span> <span class="string">&quot;lerna publish prerelease --pre-dist-tag=rc --preid=rc&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;release:next&quot;</span><span class="punctuation">:</span> <span class="string">&quot;lerna publish prerelease --pre-dist-tag=next --preid=next&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;release:preminor&quot;</span><span class="punctuation">:</span> <span class="string">&quot;lerna publish preminor --pre-dist-tag=next --preid=next&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;release:premajor&quot;</span><span class="punctuation">:</span> <span class="string">&quot;lerna publish premajor --pre-dist-tag=next --preid=next&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;release&quot;</span><span class="punctuation">:</span> <span class="string">&quot;lerna publish&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;release:minor&quot;</span><span class="punctuation">:</span> <span class="string">&quot;lerna publish minor&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;release:major&quot;</span><span class="punctuation">:</span> <span class="string">&quot;lerna publish major&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;commit&quot;</span><span class="punctuation">:</span> <span class="string">&quot;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><blockquote><p>本文首发于「<a href="https://blog.zisheng.pro/">紫升的博客</a>」，同步于公众号「<a href="https://cdn.jsdelivr.net/gh/youngjuning/images/20210418112129.jpeg">紫升早茶馆</a>」和「<a href="https://juejin.cn/user/325111174662855">掘金专栏</a>」。</p></blockquote>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/4e0dc52b8e3c/</id>
    <link href="https://blog.zisheng.pro/posts/4e0dc52b8e3c/"/>
    <published>2023-03-08T10:26:27.000Z</published>
    <summary>将大型代码仓库分割成多个独立版本化的 软件包（package）对于代码共享来说非常有用。但是，如果某些更改 跨越了多个代码仓库的话将变得很麻烦并且难以跟踪，并且，跨越多个代码仓库的测试将迅速变得非常复杂。</summary>
    <title>基于 lerna 的多包 JavaScript 项目搭建维护</title>
    <updated>2026-09-18T08:15:28.636Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="运维" scheme="https://blog.zisheng.pro/categories/%E8%BF%90%E7%BB%B4/"/>
    <category term="npm" scheme="https://blog.zisheng.pro/tags/npm/"/>
    <category term="GitHub" scheme="https://blog.zisheng.pro/tags/GitHub/"/>
    <category term="Probot" scheme="https://blog.zisheng.pro/tags/Probot/"/>
    <category term="机器人" scheme="https://blog.zisheng.pro/tags/%E6%9C%BA%E5%99%A8%E4%BA%BA/"/>
    <content>
      <![CDATA[<h2 id="关于-Github-Apps"><a href="#关于-Github-Apps" class="headerlink" title="关于 Github Apps"></a>关于 Github Apps</h2><p>GitHub Apps 是 GitHub 中优秀的产品。一个 GitHub App 通过 API 直接使用自己的身份进行操作，这意味着你不需要作为一个单独的用户维护一个机器人或服务账户。</p><p>GitHub Apps 可以被直接安装到组织或者用户账号上，并且可以赋予它们访问指定仓库的权限。它们带有内置的 webhook 和狭窄的特定权限。设置 GitHub 应用程序时，可以选择希望它访问的仓库。比如你可以设置一个叫 <code>MyGitHub</code> 的应用程序，该应用程序有且仅有 <code>octocat</code> 仓库的写入 <code>issues</code> 的权限。安装 GitHub App 需要你是组织的所有者或对仓库拥有 admin 权限。</p><h2 id="关于-Probot"><a href="#关于-Probot" class="headerlink" title="关于 Probot"></a>关于 Probot</h2><p>Probot 是一个基于 Node.js 构建 GitHub Apps 的框架。它旨在消除所有繁琐的事情，如接收和验证 webhooks 以及做认证动作，这样你就可以专注于你想要实现的功能。Probot 应用程序非常容易编写、部署和分享。大多数流行的 Probot 应用被托管，所以你不需要做任何部署和管理工作。这里有几个基于 Probot 构建的应用：</p><ul><li><a href="https://probot.github.io/apps/eslint-disable/">ESLint Disable Watcher</a> - 当 pr 中尝试禁用 eslint 规则时进行评论。</li><li><a href="https://probot.github.io/apps/dco/">Developer Certificate of Origin</a> - 强制 Pull Requests 符合 DCO 规范</li><li><a href="https://probot.github.io/apps/issue-check/">Issue Check</a> - 根据关键字和任务列表检查 issues 以确保包含必要的信息</li><li><a href="https://probot.github.io/apps/pull/">Pull</a> - Keep your forks up-to-date.</li></ul><p>你可以在<a href="https://probot.github.io/apps/">特色应用</a>或<a href="https://github.com/search?q=topic:probot-app&type=Repositories">GitHub probot-app 话题</a>浏览更多应用。</p><h2 id="Hello-Probot"><a href="#Hello-Probot" class="headerlink" title="Hello Probot"></a>Hello Probot</h2><p>一个 Probot 应用本质上是导出一个函数的 <a href="https://nodejs.org/api/modules.html">Node.js 模块</a>：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="variable language_">module</span>.<span class="property">exports</span> = <span class="function"><span class="params">app</span> =&gt;</span> &#123;</span><br><span class="line">  <span class="comment">// your code here</span></span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p><code>app</code> 参数是 <a href="https://probot.github.io/api/latest/classes/probot.html">Probot</a> 类的实例，该实例可以让你访问所有的 GitHub 精华。</p><p><code>aap.on</code> 负责监听所有 <a href="https://probot.github.io/docs/webhooks/">GitHub 触发的 webhook 事件</a>，当 GitHub 上发生任何有趣的事情，你的应用程序想知道的时候，它会通知你。</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="variable language_">module</span>.<span class="property">exports</span> = <span class="function"><span class="params">app</span> =&gt;</span> &#123;</span><br><span class="line">  app.<span class="title function_">on</span>(<span class="string">&#x27;issues.opened&#x27;</span>, <span class="keyword">async</span> context =&gt; &#123;</span><br><span class="line">    <span class="comment">// 一个新的 issue 被打开，我们应该做些什么呢？</span></span><br><span class="line">    context.<span class="property">log</span>.<span class="title function_">info</span>(context.<span class="property">payload</span>);</span><br><span class="line">  &#125;);</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>传递给事件处理程序的上下文包括关于被触发的事件的一切，以及一些有用的属性，以便对事件做出有用的回应。<code>context.octokit</code> 是一个经过认证的 GitHub 客户端，可以用来<a href="https://probot.github.io/docs/github-api/">进行 REST API 和 GraphQL 调用</a>，并允许你以编程方式做几乎任何你可以在 GitHub 上通过浏览器做的事情。</p><p>下面是一个当新打开 issues 时自动评论的 App。</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="variable language_">module</span>.<span class="property">exports</span> = <span class="function"><span class="params">app</span> =&gt;</span> &#123;</span><br><span class="line">  app.<span class="title function_">on</span>(<span class="string">&#x27;issues.opened&#x27;</span>, <span class="keyword">async</span> context =&gt; &#123;</span><br><span class="line">    <span class="comment">// context` 从事件中提取信息，可以传递给 GitHub API 调用。这将返回：</span></span><br><span class="line">    <span class="comment">// &#123; owner: &#x27;yourname&#x27;, repo: &#x27;yourrepo&#x27;, number: 123, body: &#x27;Hello World !&#125;</span></span><br><span class="line">    <span class="keyword">const</span> params = context.<span class="title function_">issue</span>(&#123; <span class="attr">body</span>: <span class="string">&#x27;Hello World!&#x27;</span> &#125;);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 在 issue 上发一条评论</span></span><br><span class="line">    <span class="keyword">return</span> context.<span class="property">octokit</span>.<span class="property">issues</span>.<span class="title function_">createComment</span>(params);</span><br><span class="line">  &#125;);</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><h2 id="开发一个-Probot-app"><a href="#开发一个-Probot-app" class="headerlink" title="开发一个 Probot app"></a>开发一个 Probot app</h2><p>为了开发一个 Probot app，你首先需要安装 Node.js 10.0.0 或更新的版本。</p><h3 id="生成一个新的-app"><a href="#生成一个新的-app" class="headerlink" title="生成一个新的 app"></a>生成一个新的 app</h3><p><a href="https://github.com/probot/create-probot-app">create-probot-app</a> 是开始构建一个新的 app 的最佳方式。它将生成一个新的应用程序，其中包含你所需要的一切，以开始并在生产中运行你的应用程序。</p><p>运行下面的命令生成一个项目：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ npx create-probot-app my-first-app</span><br></pre></td></tr></table></figure><p>该命令会问一系列关于你的 app 的问题，看起来就像：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><span class="line">Let<span class="string">&#x27;s create a Probot app!</span></span><br><span class="line"><span class="string">? App name: my-first-app</span></span><br><span class="line"><span class="string">? Description of app: A &#x27;</span>Hello World<span class="string">&#x27; GitHub App built with Probot.</span></span><br><span class="line"><span class="string">? Author&#x27;</span>s full name: Katie Horne</span><br><span class="line">? Author<span class="string">&#x27;s email address: katie@auth0.com</span></span><br><span class="line"><span class="string">? GitHub user or org name: khorne3</span></span><br><span class="line"><span class="string">? Repository name: my-first-app</span></span><br><span class="line"><span class="string">? Which template would you like to use? (Use arrow keys)</span></span><br><span class="line"><span class="string">❯ basic-js</span></span><br><span class="line"><span class="string">  basic-ts (use this one for TypeScript support)</span></span><br><span class="line"><span class="string">  checks-js</span></span><br><span class="line"><span class="string">  git-data-js</span></span><br><span class="line"><span class="string">  deploy-js</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">Finished scaffolding files!</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">Installing dependencies. This may take a few minutes...</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">Successfully created my-first-app.</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">Begin using your app with:</span></span><br><span class="line"><span class="string">  cd my-first-app</span></span><br><span class="line"><span class="string">  npm start</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">View your app&#x27;</span>s README <span class="keyword">for</span> more usage instructions.</span><br><span class="line"></span><br><span class="line">Visit the Probot docs:</span><br><span class="line">  https://probot.github.io/docs/</span><br><span class="line"></span><br><span class="line">Get <span class="built_in">help</span> from the community:</span><br><span class="line">  https://probot.github.io/community/</span><br><span class="line"></span><br><span class="line">Enjoy building your Probot app!</span><br></pre></td></tr></table></figure><p>创建的最重要的文件是 <code>index.js</code>（你的 app 代码所在的位置）和 <code>package.json</code>（使你的 app 成为标准 <a href="https://docs.npmjs.com/files/package.json">npm module</a>）。</p><h3 id="本地运行-app"><a href="#本地运行-app" class="headerlink" title="本地运行 app"></a>本地运行 app</h3><p>现在你已经准备好在本地运行 app 了。运行 <code>npm start</code> 来开启一个 server 吧：</p><blockquote><p>注意：如果你选择了 TypeScript 模板，请确保执行了 <code>npm run build</code>！</p></blockquote><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">$ yarn start</span><br><span class="line">yarn run v1.22.10</span><br><span class="line">$ probot run ./lib/index.js</span><br><span class="line">INFO (server): Running Probot v11.3.0 (Node.js: v14.15.5)</span><br><span class="line">INFO (server): Forwarding https://smee.io/dz7D1zur24cGNj7 to http://localhost:3000/</span><br><span class="line">INFO (server): Listening on http://localhost:3000</span><br><span class="line">INFO (server): Connected</span><br></pre></td></tr></table></figure><h3 id="配置-GitHub-App"><a href="#配置-GitHub-App" class="headerlink" title="配置 GitHub App"></a>配置 GitHub App</h3><p>下列是自动配置 GitHub App 的步骤：</p><ol><li>在本地命令行中执行 <code>npm start</code>。</li><li>访问 <a href="http://localhost:3000/">http://localhost:3000</a> 查看下一步。</li><li>你会看到类似下面的页面。</li></ol><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0e1601ae833f47a1815d79cb1dea2e6f~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><ol start="4"><li>点击 <strong>Register a GitHub App</strong> 按钮继续。</li><li>接着，你需要给你的 App 取一个没有被占用的名字，注意：如果你看到类似 <code>Name is reserved for the account @tuya</code> 的提示，这意味着你不能使用已存在的 GitHub organization 的名字作为 app 的名字（除非你是该组织的 owner）</li></ol><h2 id="GitHub-Release-时-npm-publish"><a href="#GitHub-Release-时-npm-publish" class="headerlink" title="GitHub Release 时 npm publish"></a>GitHub Release 时 <code>npm publish</code></h2><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a5e7c791feb140cbb537a79c89450842~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><p>实现 GitHub CI 自动发布 NPM 包，主要是为了合理管理对外 npm 发布权限。而比较通用的发布时机是在 GitHub release 时。基于上面流程图的分析，我们可以看出 released 状态时执行 <code>npm publish</code> 最合适。</p><p>我们实现的具体逻辑是，当 Probot app 监听到 <code>release.released</code> 事件时，处理发布前的操作。重要的是我们需要根据 <code>package.json</code> 中的 <code>version</code> 字段匹配出 tag，比如：</p><ul><li>1.0.0：tag 为 latest 的 1.0.0</li><li>1.0.0-beta.0：tag 为 beta 的 1.0.0-beta.0</li><li>1.0.0-alpha.0：tag 为 alpha 的 1.0.0-alpha</li></ul><h2 id="NPM-自动发布实现原理"><a href="#NPM-自动发布实现原理" class="headerlink" title="NPM 自动发布实现原理"></a>NPM 自动发布实现原理</h2><p>发布之前我们需要拉取仓库代码、取出版本和 tag、设置 NPM publish Token 等工作。先上核心代码，后面我们详细解析。</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line">app.<span class="title function_">on</span>(<span class="string">&#x27;release.released&#x27;</span>, <span class="keyword">async</span> context =&gt; &#123;</span><br><span class="line">  <span class="keyword">if</span> (!<span class="title function_">isTuya</span>(context)) <span class="keyword">return</span>;</span><br><span class="line">  app.<span class="title function_">log</span>(<span class="string">&#x27;npm publishing&#x27;</span>);</span><br><span class="line">  <span class="keyword">const</span> &#123; <span class="attr">repository</span>: repo &#125; = context.<span class="property">payload</span>;</span><br><span class="line">  <span class="keyword">const</span> downloadDefaultBranch = <span class="string">`<span class="subst">$&#123;repo.full_name&#125;</span>#<span class="subst">$&#123;context.payload.release.tag_name&#125;</span>`</span>;</span><br><span class="line">  <span class="keyword">const</span> downLoadTempDir = <span class="string">`<span class="subst">$&#123;os.tmpdir()&#125;</span>/<span class="subst">$&#123;repo.full_name&#125;</span>`</span>;</span><br><span class="line">  <span class="keyword">await</span> <span class="title function_">download</span>(downloadDefaultBranch, downLoadTempDir);</span><br><span class="line">  <span class="keyword">const</span> &#123; version, scripts &#125; = <span class="built_in">require</span>(<span class="string">`<span class="subst">$&#123;downLoadTempDir&#125;</span>/package.json`</span>);</span><br><span class="line">  <span class="keyword">const</span> tag = <span class="regexp">/^\d\.\d\.\d-(.*)\.\d$/</span>.<span class="title function_">exec</span>(version)</span><br><span class="line">    ? <span class="regexp">/^\d\.\d\.\d-(.*)\.\d$/</span>.<span class="title function_">exec</span>(version)[<span class="number">1</span>]</span><br><span class="line">    : <span class="string">&#x27;latest&#x27;</span>;</span><br><span class="line">  <span class="comment">// 如果有 build 脚本则先执行 build 脚本</span></span><br><span class="line">  <span class="keyword">if</span> (scripts.<span class="property">build</span>) &#123;</span><br><span class="line">    <span class="keyword">await</span> <span class="title function_">execSh</span>(<span class="string">`cd <span class="subst">$&#123;downLoadTempDir&#125;</span> &amp;&amp; npm install &amp;&amp; npm run build`</span>);</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">try</span> &#123;</span><br><span class="line">    <span class="keyword">const</span> result = <span class="keyword">await</span> <span class="title function_">npmPublish</span>(&#123;</span><br><span class="line">      <span class="attr">package</span>: <span class="string">`<span class="subst">$&#123;downLoadTempDir&#125;</span>/package.json`</span>,</span><br><span class="line">      <span class="attr">token</span>: process.<span class="property">env</span>.<span class="property">NPM_AUTH_TOKEN</span>,</span><br><span class="line">      <span class="attr">registry</span>: <span class="string">&#x27;https://registry.npmjs.org/&#x27;</span>,</span><br><span class="line">      tag,</span><br><span class="line">    &#125;);</span><br><span class="line">    <span class="keyword">if</span> (result.<span class="property">type</span> === <span class="string">&#x27;none&#x27;</span>) &#123;</span><br><span class="line">      app.<span class="property">log</span>.<span class="title function_">error</span>(</span><br><span class="line">        <span class="string">`You can&#x27;t publish duplicate version <span class="subst">$&#123;result.package&#125;</span>@<span class="subst">$&#123;result.version&#125;</span>`</span>,</span><br><span class="line">      );</span><br><span class="line">    &#125;</span><br><span class="line">  &#125; <span class="keyword">catch</span> (error) &#123;</span><br><span class="line">    app.<span class="property">log</span>.<span class="title function_">error</span>(error);</span><br><span class="line">  &#125;</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><h3 id="NPM-Publish-Token"><a href="#NPM-Publish-Token" class="headerlink" title="NPM Publish Token"></a>NPM Publish Token</h3><h4 id="申请-NPM-Publish-Token"><a href="#申请-NPM-Publish-Token" class="headerlink" title="申请 NPM Publish Token"></a>申请 NPM Publish Token</h4><p><strong>1. 访问 npmjs.com 进入 Access Tokens 页面</strong></p><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/2e077e9f9d0a4dc8bde0a24e121461c3~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><p><strong>2. 点击 Generate New Token 按钮</strong></p><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/82e09021f85c413d80ac77ed3df6746e~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><p><strong>3. Token 类型选择 Publish</strong></p><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/256574d0e063410aa50c85d469346809~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><h4 id="保证-NPM-Publish-Token-安全性"><a href="#保证-NPM-Publish-Token-安全性" class="headerlink" title="保证 NPM Publish Token 安全性"></a>保证 NPM Publish Token 安全性</h4><p>NPM Token 是不能被别人看到的，为了达到这个目的，首先项目需要设置为私有的，然后将 Token 放到 <code>.env</code> 中，通过 <code>process.env.NPM_AUTH_TOKEN</code> 获取。另外谨记不要在日志中打印环境变量。</p><h4 id="保证-GitHub-App-安全性"><a href="#保证-GitHub-App-安全性" class="headerlink" title="保证 GitHub App 安全性"></a>保证 GitHub App 安全性</h4><p>如果把 GitHub App 发布为 public 的，那么任何仓库都可以安装该应用，这不是我们想要的结果。解决办法有两个，一是将应用注册为 private 类型的，二是在监听回调中判断是否是允许的组织或者用户。我选择的是第二种方案，校验函数如下：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="title function_">isTuya</span> = context =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> &#123; full_name &#125; = context.<span class="property">payload</span>.<span class="property">repository</span>;</span><br><span class="line">  <span class="keyword">return</span> full_name.<span class="title function_">startsWith</span>(<span class="string">&#x27;youngjuning&#x27;</span>) || full_name.<span class="title function_">startsWith</span>(<span class="string">&#x27;tuya&#x27;</span>);</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><h3 id="下载源码"><a href="#下载源码" class="headerlink" title="下载源码"></a>下载源码</h3><p>我们选择了 download-git-repo 下载 git 仓库，但是该仓库不支持 Promise，我们做一下简单的改造：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> download = <span class="built_in">require</span>(<span class="string">&#x27;download-git-repo&#x27;</span>);</span><br><span class="line"></span><br><span class="line"><span class="variable language_">module</span>.<span class="property">exports</span> = <span class="function">(<span class="params">repo, tempDir</span>) =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">Promise</span>(<span class="function">(<span class="params">resolve, reject</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="title function_">download</span>(repo, tempDir, <span class="function"><span class="params">err</span> =&gt;</span> &#123;</span><br><span class="line">      <span class="keyword">if</span> (err) &#123;</span><br><span class="line">        <span class="title function_">reject</span>(err);</span><br><span class="line">      &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        <span class="title function_">resolve</span>(<span class="literal">null</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><h3 id="npmPublish"><a href="#npmPublish" class="headerlink" title="npmPublish"></a>npmPublish</h3><p>我们选择了 @jsdevtools&#x2F;npm-publish 执行发布动作，该仓库除了编程调用外，还可以作为 GitHub Action 和命令行工具使用。需要注意的是，我们需要用正则取出我们要发布的 tag：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> tag = <span class="regexp">/^\d\.\d\.\d-(.*)\.\d$/</span>.<span class="title function_">exec</span>(version)</span><br><span class="line">  ? <span class="regexp">/^\d\.\d\.\d-(.*)\.\d$/</span>.<span class="title function_">exec</span>(version)[<span class="number">1</span>]</span><br><span class="line">  : <span class="string">&#x27;latest&#x27;</span>;</span><br></pre></td></tr></table></figure><h3 id="lerna-publish"><a href="#lerna-publish" class="headerlink" title="lerna publish"></a>lerna publish</h3><p>lerna 管理版本由于是一次可能发布多个仓库，所以无法使用上面提到的发布流程。针对 lerna，我设计的发布流程是监听到 push 动作后取最新的一条 commit，匹配是否包含 <code>chore(release): publish</code>。具体原理如下：</p><ol><li>判断 push 分支是否是主分支且提交信息包含 <code>chore(release): publish</code></li><li>因为是 lerna publish，所以需要使用 simple-git 这个库 clone 项目。</li><li>由于 lerna publish <a href="https://github.com/lerna/lerna/issues/2404">不支持 token</a>，我们采用将 <code>//registry.npmjs.org/:_authToken=${process.env.NPM_AUTH_TOKEN}</code> 写入 <code>.npmrc</code> 的方式完成带 token 的发布。</li><li>最后，我们需要使用 <code>from-git</code> 的方式执行 <code>lerna publish</code>，<code>from-git</code> 的场景便是本地执行 <code>lerna version</code>，在 CI 中执行 <code>lerna publish</code>。</li></ol><p>完整代码如下：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line">app.<span class="title function_">on</span>(<span class="string">&#x27;push&#x27;</span>, <span class="keyword">async</span> context =&gt; &#123;</span><br><span class="line">  <span class="keyword">if</span> (!<span class="title function_">isTuya</span>(context)) <span class="keyword">return</span>;</span><br><span class="line">  <span class="keyword">if</span> (</span><br><span class="line">    context.<span class="property">payload</span>.<span class="property">ref</span>.<span class="title function_">indexOf</span>(context.<span class="property">payload</span>.<span class="property">repository</span>.<span class="property">default_branch</span>) !==</span><br><span class="line">      -<span class="number">1</span> &amp;&amp;</span><br><span class="line">    context.<span class="property">payload</span>.<span class="property">head_commit</span>.<span class="property">message</span>.<span class="title function_">indexOf</span>(<span class="string">&#x27;chore(release): publish&#x27;</span>) === <span class="number">0</span></span><br><span class="line">  ) &#123;</span><br><span class="line">    app.<span class="title function_">log</span>(<span class="string">&#x27;push event&#x27;</span>);</span><br><span class="line">    <span class="title function_">execSh</span>(<span class="string">`git --version`</span>);</span><br><span class="line">    <span class="keyword">const</span> &#123; <span class="attr">repository</span>: repo &#125; = context.<span class="property">payload</span>;</span><br><span class="line">    <span class="keyword">const</span> cloneTempDir = <span class="string">`<span class="subst">$&#123;os.tmpdir()&#125;</span>/<span class="subst">$&#123;repo.full_name&#125;</span>`</span>;</span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">      <span class="keyword">await</span> git.<span class="title function_">clone</span>(repo.<span class="property">clone_url</span>, cloneTempDir);</span><br><span class="line">      <span class="keyword">const</span> &#123; devDependencies &#125; = <span class="built_in">require</span>(<span class="string">`<span class="subst">$&#123;cloneTempDir&#125;</span>/package.json`</span>);</span><br><span class="line">      <span class="keyword">if</span> (devDependencies[<span class="string">&#x27;lerna&#x27;</span>]) &#123;</span><br><span class="line">        app.<span class="title function_">log</span>(<span class="string">&#x27;lerna publishing&#x27;</span>);</span><br><span class="line">        <span class="keyword">await</span> <span class="title function_">execSh</span>(</span><br><span class="line">          <span class="string">`cd <span class="subst">$&#123;cloneTempDir&#125;</span> &amp;&amp; echo //registry.npmjs.org/:_authToken=<span class="subst">$&#123;process.env.NPM_AUTH_TOKEN&#125;</span> &gt; .npmrc`</span>,</span><br><span class="line">        );</span><br><span class="line">        <span class="keyword">await</span> <span class="title function_">execSh</span>(</span><br><span class="line">          <span class="string">`cd <span class="subst">$&#123;cloneTempDir&#125;</span> &amp;&amp; npm install &amp;&amp; npm run build &amp;&amp; ./node_modules/.bin/lerna publish from-git --yes --no-verify-access`</span>,</span><br><span class="line">        );</span><br><span class="line">      &#125;</span><br><span class="line">      <span class="keyword">await</span> <span class="title function_">execSh</span>(<span class="string">`rm -rf <span class="subst">$&#123;cloneTempDir&#125;</span>`</span>);</span><br><span class="line">    &#125; <span class="keyword">catch</span> (error) &#123;</span><br><span class="line">      <span class="keyword">await</span> <span class="title function_">execSh</span>(<span class="string">`rm -rf <span class="subst">$&#123;cloneTempDir&#125;</span>`</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="Glitch-部署"><a href="#Glitch-部署" class="headerlink" title="Glitch 部署"></a>Glitch 部署</h2><p>如果你有自己的服务器，可以直接将机器人程序部署到自己的服务器。我这里使用官方推荐的 Glitch 服务部署。Glitch 可以免费托管 node 应用并且直接在浏览器中编辑他们。对于简单的应用完全够了。</p><ol><li>注册并在 <a href="https://glitch.com/">Glitch</a> 新建项目，选择 <strong>Import from GitHub</strong>，弹窗写上应用 github 地址，或者使用 <a href="https://github.com/behaviorbot/new-issue-welcome">https://github.com/behaviorbot/new-issue-welcome</a> 作为模板导入后再将自己的代码复制过来。</li><li>打开 <code>.env</code> 文件使用以下内容替代：</li></ol><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">APP_ID=&lt;your app id&gt;</span><br><span class="line">WEBHOOK_SECRET=&lt;your app secret&gt;</span><br><span class="line">PRIVATE_KEY_PATH=&lt;your private_key&gt;</span><br><span class="line">NODE_ENV=production</span><br><span class="line">NPM_AUTH_TOKEN=3c2c104e-9f1f-4fc5-903e-726610b75ce1</span><br><span class="line">INPUT_TOKEN=</span><br></pre></td></tr></table></figure><ol start="4"><li>将 glitch 链接设置为 GitHub App 的 webhook 地址即可，之后更新代码，glitch 会自动更新部署。</li></ol><h2 id="权限"><a href="#权限" class="headerlink" title="权限"></a>权限</h2><p>Probot App 的初始权限在 <code>app.yml</code> 文件中，如果 App 已经创建了，又想要更新权限，可以在 <a href="https://github.com/settings/apps">https://github.com/settings/apps</a> 中更新。我所用的权限配置请点击 <a href="https://glitch.com/edit/#!/tuya-robot?path=app.yml%3A119%3A0">app.yml</a> 查看。</p><blockquote><p>本文首发于「<a href="https://blog.zisheng.pro/">紫升的博客</a>」，同步于公众号「<a href="https://cdn.jsdelivr.net/gh/youngjuning/images/20210418112129.jpeg">紫升早茶馆</a>」和「<a href="https://juejin.cn/user/325111174662855">掘金专栏</a>」。</p></blockquote>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/aa64469afff6/</id>
    <link href="https://blog.zisheng.pro/posts/aa64469afff6/"/>
    <published>2023-03-08T09:21:07.000Z</published>
    <summary>GitHub Apps 是 GitHub 中优秀的产品。一个 GitHub App 通过 API 直接使用自己的身份进行操作，这意味着你不需要作为一个单独的用户维护一个机器人或服务账户。</summary>
    <title>基于 Probot 实现 GitHub NPM 发布机器人🤖</title>
    <updated>2026-09-18T08:15:28.636Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="JavaScript" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/JavaScript/"/>
    <category term="JavaScript" scheme="https://blog.zisheng.pro/tags/JavaScript/"/>
    <category term="Array" scheme="https://blog.zisheng.pro/tags/Array/"/>
    <category term="ECMAScript" scheme="https://blog.zisheng.pro/tags/ECMAScript/"/>
    <content>
      <![CDATA[<p>ECMAScript 数组的每一项可以保存任何类型的数据；而且大小是可以动态调整的，即可以随着数据的添加自动增长以容纳新增数据。</p><h1 id="创建数组"><a href="#创建数组" class="headerlink" title="创建数组"></a>创建数组</h1><h2 id="构造函数"><a href="#构造函数" class="headerlink" title="构造函数"></a>构造函数</h2><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> colors = <span class="keyword">new</span> <span class="title class_">Array</span>() <span class="comment">// []</span></span><br><span class="line">colors = <span class="keyword">new</span> <span class="title class_">Array</span>(<span class="number">10</span>) <span class="comment">// [empty × 10]</span></span><br><span class="line">colors = <span class="keyword">new</span> <span class="title class_">Array</span>(<span class="string">&#x27;red&#x27;</span>, <span class="string">&#x27;blue&#x27;</span>, <span class="string">&#x27;green&#x27;</span>) <span class="comment">// [&quot;red&quot;, &quot;blue&quot;, &quot;green&quot;]</span></span><br></pre></td></tr></table></figure><h2 id="数组字面量表示法"><a href="#数组字面量表示法" class="headerlink" title="数组字面量表示法"></a>数组字面量表示法</h2><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> colors = [] <span class="comment">// 创建一个空数组</span></span><br><span class="line"><span class="keyword">var</span> colors = [<span class="string">&#x27;red&#x27;</span>, <span class="string">&#x27;blue&#x27;</span>, <span class="string">&#x27;green&#x27;</span>] <span class="comment">// 创建一个包含 3 个字符串的数组</span></span><br><span class="line"><span class="keyword">var</span> values = [<span class="number">1</span>, <span class="number">2</span>] <span class="comment">// 不要这样！这样会创建一个包含2或3项的数组</span></span><br><span class="line"><span class="keyword">var</span> options = [, , , , ,] <span class="comment">// 不要这样！这样会创建一个包含5或6项的数组</span></span><br></pre></td></tr></table></figure><h1 id="读取和设置数组的值"><a href="#读取和设置数组的值" class="headerlink" title="读取和设置数组的值"></a>读取和设置数组的值</h1><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> colors = [<span class="string">&#x27;red&#x27;</span>, <span class="string">&#x27;blue&#x27;</span>, <span class="string">&#x27;green&#x27;</span>] <span class="comment">// 创建一个包含3个字符串的数组</span></span><br><span class="line"><span class="title function_">alert</span>(colors[<span class="number">0</span>]) <span class="comment">// 显示第1项</span></span><br><span class="line">colors[<span class="number">2</span>] = <span class="string">&#x27;black&#x27;</span> <span class="comment">// 修改第3项</span></span><br><span class="line">colors[<span class="number">3</span>] = <span class="string">&#x27;brown&#x27;</span> <span class="comment">// 新增第4项</span></span><br></pre></td></tr></table></figure><h1 id="length-属性"><a href="#length-属性" class="headerlink" title="length 属性"></a>length 属性</h1><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> colors = [<span class="string">&#x27;red&#x27;</span>, <span class="string">&#x27;blue&#x27;</span>, <span class="string">&#x27;green&#x27;</span>] <span class="comment">// 创建一个包含3个字符串的数组</span></span><br><span class="line"><span class="title function_">alert</span>(colors.<span class="property">length</span>) <span class="comment">// 显示数组长度</span></span><br><span class="line">colors[colors.<span class="property">length</span>] = <span class="string">&#x27;black&#x27;</span> <span class="comment">// 在位置3添加一种颜色</span></span><br><span class="line">colors[colors.<span class="property">length</span>] = <span class="string">&#x27;brown&#x27;</span> <span class="comment">// 在位置4再添加一种颜色</span></span><br></pre></td></tr></table></figure><h1 id="检测数组"><a href="#检测数组" class="headerlink" title="检测数组"></a>检测数组</h1><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> array = []</span><br><span class="line"><span class="keyword">if</span> (<span class="title class_">Array</span>.<span class="title function_">isArray</span>(array)) &#123;</span><br><span class="line">  <span class="comment">// 对数组执行某些操作</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="Array-isArray-Polyfill"><a href="#Array-isArray-Polyfill" class="headerlink" title="Array.isArray Polyfill"></a>Array.isArray Polyfill</h2><p>如果是实际项目可以配置 <code>@babel/polyfill</code> 或者 <code>transform-runtime</code>，这不在本文的讨论范围</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (!<span class="title class_">Array</span>.<span class="property">isArray</span>) &#123;</span><br><span class="line">  <span class="title class_">Array</span>.<span class="property">isArray</span> = <span class="keyword">function</span>(<span class="params">arg</span>) &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="title class_">Object</span>.<span class="property"><span class="keyword">prototype</span></span>.<span class="property">toString</span>.<span class="title function_">call</span>(arg) === <span class="string">&#x27;[object Array]&#x27;</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h1 id="ES1-方法"><a href="#ES1-方法" class="headerlink" title="ES1 方法"></a>ES1 方法</h1><ul><li><code>Array.prototype.join</code>: <code>join()</code> 方法将一个数组（或一个类数组对象）的所有元素连接成一个字符串并返回这个字符串。如果数组只有一个项目，那么将返回该项目而不使用分隔符。<strong>原始数组不会被改变</strong>。</li></ul><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// join</span></span><br><span class="line"><span class="keyword">var</span> colors = [<span class="string">&#x27;red&#x27;</span>, <span class="string">&#x27;blue&#x27;</span>, <span class="string">&#x27;green&#x27;</span>]</span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(colors.<span class="title function_">join</span>()) <span class="comment">// &quot;red,blue,green&quot;</span></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(colors.<span class="title function_">join</span>(<span class="string">&#x27;|&#x27;</span>)) <span class="comment">// &quot;red|blue|green&quot;</span></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(colors.<span class="title function_">join</span>(<span class="string">&#x27;|&#x27;</span>)) <span class="comment">// &quot;red|blue|green&quot;</span></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">&#x27;red|blue|green&#x27;</span>.<span class="title function_">split</span>(<span class="string">&#x27;|&#x27;</span>)) <span class="comment">// [&quot;red&quot;, &quot;blue&quot;, &quot;green&quot;]</span></span><br></pre></td></tr></table></figure><ul><li><code>Array.prototype.reverse()</code>: <code>reverse()</code> 方法将数组中元素的位置颠倒，并返回该数组。<strong>该方法会改变原数组</strong>。</li><li><code>Array.prototype.sort()</code>: <code>sort()</code> 方法用原地算法对数组的元素进行排序，并返回数组。默认排序顺序是在将元素转换为字符串，然后比较它们的 UTF-16 代码单元值序列时构建的; sort 方法接收一个比较函数作为参数，如果第一参数应该位于第二个参数之前则返回负数，如果第一个参数应该位于第二个之后则返回一个正数。<strong>该方法会改变原数组</strong>。</li></ul><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// reverse</span></span><br><span class="line"><span class="keyword">var</span> colors = [<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>, <span class="number">4</span>, <span class="number">5</span>]</span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(colors.<span class="title function_">reverse</span>()) <span class="comment">// [5, 4, 3, 2, 1]</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// sort</span></span><br><span class="line"><span class="keyword">var</span> colors = [<span class="number">1</span>, <span class="number">10</span>, <span class="number">3</span>, <span class="number">20</span>, <span class="number">5</span>]</span><br><span class="line"><span class="keyword">function</span> <span class="title function_">sort</span>(<span class="params">arr, asc = <span class="literal">true</span></span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> arr.<span class="title function_">sort</span>(<span class="function">(<span class="params">a, b</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> (asc) &#123;</span><br><span class="line">      <span class="keyword">return</span> a - b</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> b - a</span><br><span class="line">  &#125;)</span><br><span class="line">&#125;</span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="title function_">sort</span>(colors)) <span class="comment">// [1, 3, 5, 10, 20]</span></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="title function_">sort</span>(colors, <span class="literal">false</span>)) <span class="comment">// [20, 10, 5, 3, 1]</span></span><br></pre></td></tr></table></figure><h1 id="ES3-方法"><a href="#ES3-方法" class="headerlink" title="ES3 方法"></a>ES3 方法</h1><ul><li><code>Array.prototype.push</code>: <code>push()</code> 方法可以接受任意数量的参数，把它们逐个添加到数组末尾，并返回修改后数组的长度。<strong>该方法会改变原数组</strong>。</li><li><code>Array.prototype.pop</code>: <code>pop()</code> 方法从数组末尾移除最后一项，减少数组的 <code>length</code> 值，并返回该项。<strong>该方法会改变原数组</strong>。</li><li><code>Array.prototype.shift</code>: <code>shift()</code> 方法能够移除数组中的第一项并返回该项, 同时将数组长度减 1。<strong>该方法会改变原数组</strong>。</li><li><code>Array.prototype.unshift</code>: <code>unshift()</code> 方法能够在数组前端添加任意个项，并返回新数组的长度。<strong>该方法会改变原数组</strong>。</li></ul><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// push、pop</span></span><br><span class="line"><span class="keyword">var</span> colors = [<span class="string">&#x27;red&#x27;</span>, <span class="string">&#x27;blue&#x27;</span>, <span class="string">&#x27;green&#x27;</span>]</span><br><span class="line"><span class="keyword">var</span> count = colors.<span class="title function_">push</span>(<span class="string">&#x27;black&#x27;</span>, <span class="string">&#x27;brown&#x27;</span>) <span class="comment">// 向末尾推入两项</span></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(count) <span class="comment">// 5</span></span><br><span class="line"><span class="keyword">var</span> item = colors.<span class="title function_">pop</span>() <span class="comment">// 取得最后一项</span></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(item) <span class="comment">// &quot;brown&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// shift、unshift</span></span><br><span class="line"><span class="keyword">var</span> colors = [<span class="string">&#x27;red&#x27;</span>, <span class="string">&#x27;blue&#x27;</span>, <span class="string">&#x27;green&#x27;</span>]</span><br><span class="line"><span class="keyword">var</span> count = colors.<span class="title function_">unshift</span>(<span class="string">&#x27;black&#x27;</span>, <span class="string">&#x27;brown&#x27;</span>) <span class="comment">// 向开头推入两项</span></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(count) <span class="comment">// 5</span></span><br><span class="line"><span class="keyword">var</span> item = colors.<span class="title function_">shift</span>() <span class="comment">// 取得第一项</span></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(item) <span class="comment">// &quot;red&quot;</span></span><br></pre></td></tr></table></figure><ul><li><code>Array.prototype.concat()</code>: <code>concat()</code> 方法用于合并两个或多个数组。<strong>原始数组不会被改变</strong>，而是返回一个新数组。</li><li><code>Array.prototype.slice()</code>: <code>slice()</code> 方法返回一个新的数组对象，这一对象是一个由 <code>begin</code> 和 <code>end</code> 决定的原数组的浅拷贝（包括 <code>begin</code>，不包括 <code>end</code>）。<strong>原始数组不会被改变</strong>。</li><li><code>Array.prototype.splice()</code>: <code>splice()</code> 方法通过删除或替换现有元素或者原地添加新的元素来修改数组,并以数组形式返回被修改的内容。<strong>此方法会改变原数组</strong>。<ul><li><strong>删除</strong>: 可以删除任意数量的项，只需指定 2 个参数: 要删除的第一项的位置和要删除的项数。例如，<code>splice(0, 2)</code> 会删除数组中的前两项。</li><li><strong>插入</strong>: 可以向指定位置插入任意数量的项，只需要提供 3 个参数：起始位置、0（要删除的项数）和要插入的项。如果要插入多个项，可以再传入任意多个项。例如，<code>splice(2, 0, &quot;red&quot;, &quot;green&quot;)</code> 会从当前数组的位置 2 开始插入字符串 <code>&quot;red&quot;</code> 和 <code>green</code>。</li><li><strong>替换</strong>: 可以向指定位置插入任意数量的项，且同时删除任意数量的项，只需要指定 3 个参数：起始位置、要删除的项数和要插入的任意数量的项。插入的项不必与删除的项数相等。例如，<code>splice(2, 1, &quot;red&quot;, &quot;green&quot;)</code> 会删除当前数组位置 2 的项，然后再从位置 2 开始插入字符串 <code>&quot;red&quot;</code> 和 <code>green</code>。</li></ul></li></ul><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// concat</span></span><br><span class="line"><span class="keyword">var</span> colors = [<span class="string">&#x27;red&#x27;</span>, <span class="string">&#x27;blue&#x27;</span>, <span class="string">&#x27;green&#x27;</span>]</span><br><span class="line"><span class="keyword">var</span> colors1 = [<span class="string">&#x27;black&#x27;</span>, <span class="string">&#x27;brown&#x27;</span>]</span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(colors.<span class="title function_">concat</span>(colors1)) <span class="comment">// [&quot;red&quot;, &quot;blue&quot;, &quot;green&quot;, &quot;black&quot;, &quot;brown&quot;]</span></span><br><span class="line"><span class="comment">// slice</span></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 如果 `slice()` 方法的参数中有一个负数，则用数组长度加上该数来确定相应地位置。例如，在一个包含5项的数组上调用 `slice(-2, -1)` 与调用 `slice(3, 4)` 得到的结果相同。如果结束位置小于起始位置，则返回空数组。</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">var</span> colors = [<span class="string">&#x27;red&#x27;</span>, <span class="string">&#x27;blue&#x27;</span>, <span class="string">&#x27;green&#x27;</span>, <span class="string">&#x27;black&#x27;</span>, <span class="string">&#x27;brown&#x27;</span>]</span><br><span class="line"><span class="keyword">var</span> colors2 = colors.<span class="title function_">slice</span>(<span class="number">1</span>)</span><br><span class="line"><span class="keyword">var</span> colors3 = colors.<span class="title function_">slice</span>(<span class="number">1</span>, <span class="number">4</span>)</span><br><span class="line"></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(colors2) <span class="comment">// [&quot;blue&quot;, &quot;green&quot;, &quot;black&quot;, &quot;brown&quot;]</span></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(colors3) <span class="comment">// [&quot;blue&quot;, &quot;green&quot;, &quot;black&quot;]</span></span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * splice() 方法始终都会返回一个数组，该数组中包含从原始数组中删除的项（如果没有删除任何项，则返回一个空数组）</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">var</span> colors = [<span class="string">&#x27;red&#x27;</span>, <span class="string">&#x27;green&#x27;</span>, <span class="string">&#x27;blue&#x27;</span>]</span><br><span class="line"><span class="keyword">var</span> removed = colors.<span class="title function_">splice</span>(<span class="number">0</span>, <span class="number">1</span>) <span class="comment">// 删除第一项</span></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(colors) <span class="comment">// [&quot;green&quot;, &quot;blue&quot;]</span></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(removed) <span class="comment">// [&quot;red&quot;] 返回的数组中只包含一项</span></span><br><span class="line"></span><br><span class="line">removed = colors.<span class="title function_">splice</span>(<span class="number">1</span>, <span class="number">0</span>, <span class="string">&#x27;yellow&#x27;</span>, <span class="string">&#x27;orange&#x27;</span>) <span class="comment">// 从位置 1 开始插入两项</span></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(colors) <span class="comment">// [&quot;green&quot;, &quot;red&quot;, &quot;orange&quot;, &quot;blue&quot;]</span></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(removed) <span class="comment">// 返回的是一个空数组</span></span><br><span class="line"></span><br><span class="line">removed = colors.<span class="title function_">splice</span>(<span class="number">1</span>, <span class="number">1</span>, <span class="string">&#x27;red&#x27;</span>, <span class="string">&#x27;purple&#x27;</span>) <span class="comment">// 删除一项，增加两项</span></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(colors) <span class="comment">// [&quot;green&quot;, &quot;red&quot;, &quot;purple&quot;, &quot;orange&quot;, &quot;blue&quot;]</span></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(removed) <span class="comment">// yellow，返回的数组中只包含一项</span></span><br></pre></td></tr></table></figure><h1 id="ES5-方法"><a href="#ES5-方法" class="headerlink" title="ES5 方法"></a>ES5 方法</h1><ul><li><code>Array.prototype.indexOf()</code>: <code>indexOf()</code> 方法返回在数组中可以找到一个给定元素的第一个索引，如果不存在，则返回-1。从数组的前面向后查找，从 <code>fromIndex</code> 处开始。</li><li><code>Array。prototype.lastIndexOf)()</code>: <code>lastIndexOf()</code> 方法返回指定元素在数组中的最后一个索引，如果不存在则返回-1。从数组的后面向前查找，从 <code>fromIndex</code> 处开始。</li></ul><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> numbers = [<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>, <span class="number">4</span>, <span class="number">5</span>, <span class="number">4</span>, <span class="number">3</span>, <span class="number">2</span>, <span class="number">1</span>]</span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(numbers.<span class="title function_">indexOf</span>(<span class="number">4</span>)) <span class="comment">// 3</span></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(numbers.<span class="title function_">lastIndexOf</span>(<span class="number">4</span>)) <span class="comment">// 5</span></span><br><span class="line"></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(numbers.<span class="title function_">indexOf</span>(<span class="number">4</span>, <span class="number">4</span>)) <span class="comment">// 5</span></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(numbers.<span class="title function_">lastIndexOf</span>(<span class="number">4</span>, <span class="number">4</span>)) <span class="comment">// 3</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> person = &#123; <span class="attr">name</span>: <span class="string">&#x27;Nicholas&#x27;</span> &#125;</span><br><span class="line"><span class="keyword">var</span> people = [&#123; <span class="attr">name</span>: <span class="string">&#x27;Nicholas&#x27;</span> &#125;]</span><br><span class="line"><span class="keyword">var</span> morePeople</span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * indexOf、lastIndexOf 在比较第一个参数与数组中的每一项时，会使用全等操作符</span></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>(people.<span class="title function_">indexOf</span>(person)) <span class="comment">// -1</span></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(morePeople.<span class="title function_">indexOf</span>(person)) <span class="comment">// 0</span></span><br></pre></td></tr></table></figure><ul><li><code>Array.prototype.every()</code>: 对数组中的每一项运行给定函数，如果该函数对每一项都返回 <code>true</code>，则返回 <code>true</code>。</li><li><code>Array.prototype.some()</code>: 对数组中的每一项运行给定函数，如果该函数对任一项返回 <code>true</code>，则返回 <code>true</code>。</li><li><code>Array.prototype.filter()</code>: 对数组中的每一项运行给定函数，返回该函数会返回 <code>true</code> 的项组成的数组。</li><li><code>Array.prototype.map()</code>: 对数组中的每一项运行给定函数，返回每次函数调用的结果组成的数组。</li><li><code>Array.prototype.forEach()</code>: 对数组中的每一项运行给定函数，这个方法没有返回值。</li></ul><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// every</span></span><br><span class="line"><span class="keyword">var</span> numbers = [<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>, <span class="number">4</span>, <span class="number">5</span>, <span class="number">4</span>, <span class="number">3</span>, <span class="number">2</span>, <span class="number">1</span>]</span><br><span class="line"><span class="keyword">var</span> everyResult = numbers.<span class="title function_">every</span>(<span class="function">(<span class="params">item, index, array</span>) =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">return</span> item &gt; <span class="number">2</span></span><br><span class="line">&#125;)</span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(everyResult) <span class="comment">// false</span></span><br><span class="line"><span class="comment">// some</span></span><br><span class="line"><span class="keyword">var</span> someResult = numbers.<span class="title function_">some</span>(<span class="function">(<span class="params">item, index, array</span>) =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">return</span> item &gt; <span class="number">2</span></span><br><span class="line">&#125;)</span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(someResult) <span class="comment">// true</span></span><br><span class="line"><span class="comment">// filter</span></span><br><span class="line"><span class="keyword">var</span> numbers = [<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>, <span class="number">4</span>, <span class="number">5</span>, <span class="number">4</span>, <span class="number">3</span>, <span class="number">2</span>, <span class="number">1</span>]</span><br><span class="line"><span class="keyword">var</span> filterResult = numbers.<span class="title function_">filter</span>(<span class="function">(<span class="params">item, index, array</span>) =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">return</span> item &gt; <span class="number">2</span></span><br><span class="line">&#125;)</span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(filterResult) <span class="comment">// [3, 4, 5, 4, 3]</span></span><br><span class="line"><span class="comment">// map</span></span><br><span class="line"><span class="keyword">var</span> numbers = [<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>, <span class="number">4</span>, <span class="number">5</span>, <span class="number">4</span>, <span class="number">3</span>, <span class="number">2</span>, <span class="number">1</span>]</span><br><span class="line"><span class="keyword">var</span> mapResult = numbers.<span class="title function_">map</span>(<span class="function">(<span class="params">item, index, array</span>) =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">return</span> item * <span class="number">2</span></span><br><span class="line">&#125;)</span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(mapResult) <span class="comment">// [2, 4, 6, 8, 10, 8, 6, 4, 2]</span></span><br><span class="line"><span class="comment">// forEach</span></span><br><span class="line"><span class="keyword">var</span> numbers = [<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>, <span class="number">4</span>, <span class="number">5</span>, <span class="number">4</span>, <span class="number">3</span>, <span class="number">2</span>, <span class="number">1</span>]</span><br><span class="line">numbers.<span class="title function_">forEach</span>(<span class="function">(<span class="params">item, index, array</span>) =&gt;</span> &#123;</span><br><span class="line">  <span class="comment">// 执行某些操作</span></span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><ul><li><code>Array.prototype.reduce()</code></li><li><code>Array.prototype.reduceRight()</code></li></ul><p><code>reduce()</code> 和 <code>reduceRight()</code> 方法都会迭代数组的所有项，然后构建一个最终返回的值。其中，<code>reduce()</code> 方法从数组的第一项开始，逐个遍历到最后。而 <code>reduceRight()</code> 则从数组的最后一项开始，向前遍历到第一项。</p><p>这两个参数都接受两个参数: 一个在每一项上调用的函数和（可选的）作为归并基础的初始值。传给 <code>reduce</code> 和 <code>reduceRight</code> 的函数接受 4 个参数：前一个值、当前值、项的索引和数组对象。这个函数返回的任何值都会作为第一个参数自动传给下一项。第一次迭代发生在数组的第二项上，因此第一个参数是数组的第一项，第二个参数就是数组的第二项。</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 数组求和</span></span><br><span class="line"><span class="keyword">var</span> values = [<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>, <span class="number">4</span>, <span class="number">5</span>]</span><br><span class="line"><span class="keyword">var</span> sum = values.<span class="title function_">reduce</span>(<span class="function">(<span class="params">prev, cur, index, array</span>) =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">return</span> prev + cur</span><br><span class="line">&#125;)</span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(sum)</span><br><span class="line"><span class="comment">// 数组求乘积</span></span><br><span class="line"><span class="keyword">var</span> values = [<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>, <span class="number">4</span>, <span class="number">5</span>]</span><br><span class="line"><span class="keyword">var</span> pro = values.<span class="title function_">reduce</span>(<span class="function">(<span class="params">prev, cur, index, array</span>) =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">return</span> prev * cur</span><br><span class="line">&#125;)</span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(pro) <span class="comment">// 120</span></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 【累加对象数组里的值】</span></span><br><span class="line"><span class="comment"> * 要累加对象数组中包含的值，必须提供初始值，以便各个item正确通过你的函数。</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">var</span> values = [&#123; <span class="attr">x</span>: <span class="number">1</span> &#125;, &#123; <span class="attr">x</span>: <span class="number">2</span> &#125;, &#123; <span class="attr">x</span>: <span class="number">3</span> &#125;]</span><br><span class="line"><span class="keyword">var</span> sum = values.<span class="title function_">reduce</span>(<span class="function">(<span class="params">prev, cur</span>) =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">return</span> prev + cur.<span class="property">x</span></span><br><span class="line">&#125;, <span class="number">0</span>)</span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(sum) <span class="comment">// 6</span></span><br><span class="line"><span class="comment">// 求最大值</span></span><br><span class="line"><span class="keyword">var</span> values = [<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>, <span class="number">4</span>, <span class="number">5</span>]</span><br><span class="line"><span class="keyword">var</span> max = values.<span class="title function_">reduce</span>(<span class="function">(<span class="params">prev, cur, index, array</span>) =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">return</span> prev &gt; cur ? prev : cur</span><br><span class="line">&#125;)</span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(max) <span class="comment">// 5</span></span><br><span class="line"><span class="comment">// 计算数组中每个元素出现的次数</span></span><br><span class="line"><span class="keyword">var</span> names = [<span class="string">&#x27;Alice&#x27;</span>, <span class="string">&#x27;Bob&#x27;</span>, <span class="string">&#x27;Tiff&#x27;</span>, <span class="string">&#x27;Bruce&#x27;</span>, <span class="string">&#x27;Alice&#x27;</span>]</span><br><span class="line"><span class="keyword">var</span> countedNames = names.<span class="title function_">reduce</span>(<span class="function">(<span class="params">allNames, name</span>) =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">if</span> (allNames.<span class="title function_">indexOf</span>(name) &gt; -<span class="number">1</span>) &#123;</span><br><span class="line">    allNames[name]++</span><br><span class="line">  &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    allNames[name] = <span class="number">1</span></span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">return</span> allNames</span><br><span class="line">&#125;)</span><br><span class="line"><span class="comment">// 按属性对object array分类</span></span><br><span class="line"><span class="keyword">var</span> people = [</span><br><span class="line">  &#123; <span class="attr">name</span>: <span class="string">&#x27;张三&#x27;</span>, <span class="attr">age</span>: <span class="number">26</span> &#125;,</span><br><span class="line">  &#123; <span class="attr">name</span>: <span class="string">&#x27;李扬&#x27;</span>, <span class="attr">age</span>: <span class="number">25</span> &#125;,</span><br><span class="line">  &#123; <span class="attr">name</span>: <span class="string">&#x27;杨六&#x27;</span>, <span class="attr">age</span>: <span class="number">25</span> &#125;,</span><br><span class="line">  &#123; <span class="attr">name</span>: <span class="string">&#x27;王五&#x27;</span>, <span class="attr">age</span>: <span class="number">26</span> &#125;,</span><br><span class="line">]</span><br><span class="line"><span class="keyword">var</span> <span class="title function_">groupBy</span> = (<span class="params">objectArray, property</span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">return</span> objectArray.<span class="title function_">reduce</span>(<span class="function">(<span class="params">acc, cur</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">const</span> key = cur[property]</span><br><span class="line">    <span class="keyword">if</span> (!acc[key]) &#123;</span><br><span class="line">      acc[key] = [cur]</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">      acc[key].<span class="title function_">push</span>(cur)</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> acc</span><br><span class="line">  &#125;, &#123;&#125;)</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">var</span> groupedPeople = <span class="title function_">groupBy</span>(people, <span class="string">&#x27;age&#x27;</span>)</span><br></pre></td></tr></table></figure><p><strong>reduce 高级用法:</strong></p><blockquote><p>搬砖工小王拿到了这样的格式：<code>var arr = [ {n ame: &#39;brick1&#39;}, {name: &#39;brick2&#39;}, {name: &#39;brick3&#39;} ]</code><br>希望得到这样的数据格式：<code>brick1, brick2 &amp; brick3</code><br>当然数组异常流: <code>[{name:&#39;brick1&#39;}]</code> 和空数组传入得到 <code>&quot;brick1&quot;</code> 和空</p></blockquote><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> array = [&#123; <span class="attr">name</span>: <span class="string">&#x27;brick11&#x27;</span> &#125;, &#123; <span class="attr">name</span>: <span class="string">&#x27;brick12&#x27;</span> &#125;, &#123; <span class="attr">name</span>: <span class="string">&#x27;brick13&#x27;</span> &#125;]</span><br><span class="line"><span class="keyword">var</span> <span class="title function_">carryBricks</span> = arr =&gt; &#123;</span><br><span class="line">  <span class="keyword">return</span> arr.<span class="title function_">reduce</span>(<span class="function">(<span class="params">prev, cur, index, array</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> (index === <span class="number">0</span>) &#123;</span><br><span class="line">      <span class="keyword">return</span> cur.<span class="property">name</span></span><br><span class="line">    &#125; <span class="keyword">else</span> <span class="keyword">if</span> (index === array.<span class="property">length</span> - <span class="number">1</span>) &#123;</span><br><span class="line">      <span class="keyword">return</span> prev + <span class="string">&#x27; &amp; &#x27;</span> + cur.<span class="property">name</span></span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">      <span class="keyword">return</span> prev + <span class="string">&#x27;,&#x27;</span> + cur.<span class="property">name</span></span><br><span class="line">    &#125;</span><br><span class="line">  &#125;, <span class="string">&#x27;&#x27;</span>)</span><br><span class="line">&#125;</span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="title function_">carryBricks</span>(array)) <span class="comment">// &quot;brick11,brick12 &amp; brick13&quot;</span></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="title function_">carryBricks</span>([&#123; <span class="attr">name</span>: <span class="string">&#x27;brick1&#x27;</span> &#125;])) <span class="comment">// &quot;brick1&quot;</span></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="title function_">carryBricks</span>([])) <span class="comment">// &quot;&quot;</span></span><br></pre></td></tr></table></figure><h1 id="ES2015-ES6"><a href="#ES2015-ES6" class="headerlink" title="ES2015&#x2F;ES6"></a>ES2015&#x2F;ES6</h1><ul><li><p><code>Array.from</code>：用于将两类对象转为真正的数组：类似数组的对象（array-like object）和可遍历（iterable）的对象（包括 ES6 新增的数据结构 Set 和 Map）</p></li><li><p><code>Array.of</code>：用于将一组值，转换为数组</p></li><li><p><code>arr.fill(value[, start[, end]])</code>：用一个固定值填充一个数组中从起始索引到终止索引内的全部元素。不包括终止索引</p></li><li><p><code>find()</code>：用于找出第一个符合条件的数组成员</p></li><li><p><code>findIndex()</code>：返回第一个符合条件的数组成员的位置，如果所有成员都不符合条件，则返回<code>-1</code></p></li><li><p><code>entries()</code>，<code>keys()</code> 和 values()：<code>keys()</code>是对键名的遍历、<code>values()</code>是对键值的遍历，<code>entries()</code>是对键值对的遍历</p></li></ul><h1 id="ES2016-ES7"><a href="#ES2016-ES7" class="headerlink" title="ES2016&#x2F;ES7"></a>ES2016&#x2F;ES7</h1><ul><li><code>copyWithin()</code>： 方法浅复制数组的一部分到同一数组中的另一个位置，并返回它，不会改变原数组的长度。</li><li><code>includes()</code>：返回一个布尔值，表示某个数组是否包含给定的值</li></ul><h1 id="ES2019-ES10"><a href="#ES2019-ES10" class="headerlink" title="ES2019&#x2F;ES10"></a>ES2019&#x2F;ES10</h1><ul><li><code>flat()</code>：会按照一个可指定的深度递归遍历数组，并将所有元素与遍历到的子数组中的元素合并为一个新数组返回。</li><li><code>flatMap()</code>： 方法首先使用映射函数映射每个元素，然后将结果压缩成一个新数组。它与 <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map">map</a> 连着深度值为1的 <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/flat">flat</a> 几乎相同，但 <code>flatMap</code> 通常在合并成一种方法的效率稍微高一些</li></ul><h1 id="lodash"><a href="#lodash" class="headerlink" title="lodash"></a>lodash</h1><h2 id="数组并集"><a href="#数组并集" class="headerlink" title="数组并集"></a>数组并集</h2><ul><li><code>_.union([arrays])</code>: 创建一个按顺序排列的唯一值的数组。所有给定数组的元素值使用 SameValueZero 做等值比较。（ <code>arrays</code>（数组）的并集，按顺序返回，返回数组的元素是唯一的）</li><li><code>_.unionBy([arrays], [iteratee=_.identity])</code>: 这个方法类似 <code>_.union</code> ，除了它接受一个 <code>iteratee</code> （迭代函数），调用每一个数组（<code>array</code>）的每个元素以产生唯一性计算的标准。<code>iteratee</code> 会传入一个参数：(<code>value</code>)。</li><li><code>_.unionWith([arrays], [comparator])</code>: 这个方法类似 <code>_.union</code>， 除了它接受一个 <code>comparator</code> 调用比较 <code>arrays</code> 数组的每一个元素。 <code>comparator</code> 调用时会传入 2 个参数： (<code>arrVal</code>, <code>othVal</code>)。</li></ul><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> _ <span class="keyword">from</span> <span class="string">&#x27;lodash&#x27;</span></span><br><span class="line"><span class="comment">// union</span></span><br><span class="line">_.<span class="title function_">union</span>([<span class="number">2</span>], [<span class="number">1</span>, <span class="number">2</span>]) <span class="comment">// [2, 1]</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// unionBy</span></span><br><span class="line">_.<span class="title function_">unionBy</span>([<span class="number">2.1</span>], [<span class="number">1.2</span>, <span class="number">2.3</span>], <span class="title class_">Math</span>.<span class="property">floor</span>) <span class="comment">// [2.1, 1.2]</span></span><br><span class="line"><span class="comment">// The `_.property` iteratee shorthand.</span></span><br><span class="line">_.<span class="title function_">unionBy</span>([&#123; <span class="attr">x</span>: <span class="number">1</span> &#125;], [&#123; <span class="attr">x</span>: <span class="number">2</span> &#125;, &#123; <span class="attr">x</span>: <span class="number">1</span> &#125;], <span class="string">&#x27;x&#x27;</span>) <span class="comment">// [&#123; &#x27;x&#x27;: 1 &#125;, &#123; &#x27;x&#x27;: 2 &#125;]</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// unionWith</span></span><br><span class="line"><span class="keyword">var</span> objects = [</span><br><span class="line">  &#123; <span class="attr">x</span>: <span class="number">1</span>, <span class="attr">y</span>: <span class="number">2</span> &#125;,</span><br><span class="line">  &#123; <span class="attr">x</span>: <span class="number">2</span>, <span class="attr">y</span>: <span class="number">1</span> &#125;,</span><br><span class="line">]</span><br><span class="line"><span class="keyword">var</span> others = [</span><br><span class="line">  &#123; <span class="attr">x</span>: <span class="number">1</span>, <span class="attr">y</span>: <span class="number">1</span> &#125;,</span><br><span class="line">  &#123; <span class="attr">x</span>: <span class="number">1</span>, <span class="attr">y</span>: <span class="number">2</span> &#125;,</span><br><span class="line">]</span><br><span class="line">_.<span class="title function_">unionWith</span>(objects, others, _.<span class="property">isEqual</span>) <span class="comment">// [&#123; &#x27;x&#x27;: 1, &#x27;y&#x27;: 2 &#125;, &#123; &#x27;x&#x27;: 2, &#x27;y&#x27;: 1 &#125;, &#123; &#x27;x&#x27;: 1, &#x27;y&#x27;: 1 &#125;]</span></span><br></pre></td></tr></table></figure><h2 id="数组交集"><a href="#数组交集" class="headerlink" title="数组交集"></a>数组交集</h2><ul><li><code>_.intersection([arrays])</code>: 创建唯一值的数组，这个数组包含所有给定数组都包含的元素，使用 SameValueZero 进行相等性比较。（可以理解为给定数组的交集）</li><li><code>_.intersectionBy([arrays], [iteratee=_.identity])</code>: 这个方法类似 <code>_.intersection</code>，区别是它接受一个 <code>iteratee</code> 调用每一个 arrays 的每个值以产生一个值，通过产生的值进行了比较。结果值是从第一数组中选择。iteratee 会传入一个参数：<code>(value)</code>。</li><li><code>_.intersectionWith([arrays], [comparator])</code>: 这个方法类似 <code>_.intersection</code>，区别是它接受一个 <code>comparator</code> 调用比较 arrays 中的元素。结果值是从第一数组中选择。<code>comparator</code> 会传入两个参数：<code>(arrVal, othVal)</code>。</li></ul><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> _ <span class="keyword">from</span> <span class="string">&#x27;lodash&#x27;</span></span><br><span class="line"><span class="comment">// intersection</span></span><br><span class="line">_.<span class="title function_">intersection</span>([<span class="number">2</span>, <span class="number">1</span>], [<span class="number">4</span>, <span class="number">2</span>], [<span class="number">1</span>, <span class="number">2</span>])) <span class="comment">// [2]</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// intersectionBy</span></span><br><span class="line">_.<span class="title function_">intersectionBy</span>([<span class="number">2.1</span>, <span class="number">1.2</span>], [<span class="number">4.3</span>, <span class="number">2.4</span>], <span class="title class_">Math</span>.<span class="property">floor</span>) <span class="comment">// [2.1]</span></span><br><span class="line"><span class="comment">// The `_.property` iteratee shorthand.</span></span><br><span class="line">_.<span class="title function_">intersectionBy</span>([&#123; <span class="string">&#x27;x&#x27;</span>: <span class="number">1</span> &#125;], [&#123; <span class="string">&#x27;x&#x27;</span>: <span class="number">2</span> &#125;, &#123; <span class="string">&#x27;x&#x27;</span>: <span class="number">1</span> &#125;], <span class="string">&#x27;x&#x27;</span>) <span class="comment">// =&gt; [&#123; &#x27;x&#x27;: 1 &#125;]</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// intersectionWith</span></span><br><span class="line"><span class="keyword">var</span> objects = [&#123; <span class="string">&#x27;x&#x27;</span>: <span class="number">1</span>, <span class="string">&#x27;y&#x27;</span>: <span class="number">2</span> &#125;, &#123; <span class="string">&#x27;x&#x27;</span>: <span class="number">2</span>, <span class="string">&#x27;y&#x27;</span>: <span class="number">1</span> &#125;];</span><br><span class="line"><span class="keyword">var</span> others = [&#123; <span class="string">&#x27;x&#x27;</span>: <span class="number">1</span>, <span class="string">&#x27;y&#x27;</span>: <span class="number">1</span> &#125;, &#123; <span class="string">&#x27;x&#x27;</span>: <span class="number">1</span>, <span class="string">&#x27;y&#x27;</span>: <span class="number">2</span> &#125;];</span><br><span class="line">_.<span class="title function_">intersectionWith</span>(objects, others, _.<span class="property">isEqual</span>) <span class="comment">// [&#123; &#x27;x&#x27;: 1, &#x27;y&#x27;: 2 &#125;]</span></span><br></pre></td></tr></table></figure><h2 id="数组差集"><a href="#数组差集" class="headerlink" title="数组差集"></a>数组差集</h2><ul><li><code>_.difference(array, [values])</code>: 创建一个具有唯一 array 值的数组，每个值不包含在其他给定的数组中。（即创建一个新数组，这个数组中的值，为第一个数字（array 参数）排除了给定数组中的值。）该方法使用 SameValueZero 做相等比较。结果值的顺序是由第一个数组中的顺序确定。</li><li><code>_.differenceBy(array, [values], [iteratee=_.identity])</code>: 这个方法类似 _.difference ，除了它接受一个 iteratee （迭代器）， 调用 array 和 values 中的每个元素以产生比较的标准。 结果值是从第一数组中选择。iteratee 会调用一个参数：(value)。（首先使用迭代器分别迭代 array 和 values 中的每个元素，返回的值作为比较值）。</li><li><code>_.differenceWith(array, [values], [comparator])</code>: 这个方法类似 <code>_.difference</code> ，除了它接受一个 <code>comparator</code> （比较器），它调用比较 <code>array</code>，<code>values</code> 中的元素。 结果值是从第一数组中选择。<code>comparator</code> 调用参数有两个：<code>(arrVal, othVal)</code>。</li></ul><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> _ <span class="keyword">from</span> <span class="string">&#x27;lodash&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// difference</span></span><br><span class="line">_.<span class="title function_">difference</span>([<span class="number">3</span>, <span class="number">2</span>, <span class="number">1</span>], [<span class="number">4</span>, <span class="number">2</span>]) <span class="comment">// [3, 1]</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// differenceBy</span></span><br><span class="line">_.<span class="title function_">differenceBy</span>([<span class="number">3.1</span>, <span class="number">2.2</span>, <span class="number">1.3</span>], [<span class="number">4.4</span>, <span class="number">2.5</span>], <span class="title class_">Math</span>.<span class="property">floor</span>) <span class="comment">// [3.1, 1.3]</span></span><br><span class="line"><span class="comment">// The `_.property` iteratee shorthand.</span></span><br><span class="line">_.<span class="title function_">differenceBy</span>([&#123; <span class="attr">x</span>: <span class="number">2</span> &#125;, &#123; <span class="attr">x</span>: <span class="number">1</span> &#125;], [&#123; <span class="attr">x</span>: <span class="number">1</span> &#125;], <span class="string">&#x27;x&#x27;</span>) <span class="comment">// [&#123; &#x27;x&#x27;: 2 &#125;]</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// differenceWith</span></span><br><span class="line"><span class="keyword">var</span> objects = [</span><br><span class="line">  &#123; <span class="attr">x</span>: <span class="number">1</span>, <span class="attr">y</span>: <span class="number">2</span> &#125;,</span><br><span class="line">  &#123; <span class="attr">x</span>: <span class="number">2</span>, <span class="attr">y</span>: <span class="number">1</span> &#125;,</span><br><span class="line">]</span><br><span class="line">_.<span class="title function_">differenceWith</span>(objects, [&#123; <span class="attr">x</span>: <span class="number">1</span>, <span class="attr">y</span>: <span class="number">2</span> &#125;], _.<span class="property">isEqual</span>) <span class="comment">// [&#123; &#x27;x&#x27;: 2, &#x27;y&#x27;: 1 &#125;]</span></span><br></pre></td></tr></table></figure><h2 id="数组分组"><a href="#数组分组" class="headerlink" title="数组分组"></a>数组分组</h2><ul><li><code>_.groupBy(collection, [iteratee=_.identity])</code>: 创建一个对象，<code>key</code> 是 <code>iteratee</code> 遍历 <code>collection</code>(集合) 中的每个元素返回的结果。 分组值的顺序是由他们出现在 <code>collection</code>(集合) 中的顺序确定的。每个键对应的值负责生成 <code>key</code> 的元素组成的数组。<code>iteratee</code> 调用 1 个参数： (<code>value</code>)。</li></ul><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> _ <span class="keyword">from</span> <span class="string">&#x27;lodash&#x27;</span></span><br><span class="line"></span><br><span class="line">_.<span class="title function_">groupBy</span>([<span class="number">6.1</span>, <span class="number">4.2</span>, <span class="number">6.3</span>], <span class="title class_">Math</span>.<span class="property">floor</span>) <span class="comment">// &#123; &#x27;4&#x27;: [4.2], &#x27;6&#x27;: [6.1, 6.3] &#125;</span></span><br><span class="line"><span class="comment">// The `_.property` iteratee shorthand.</span></span><br><span class="line">_.<span class="title function_">groupBy</span>([<span class="string">&#x27;one&#x27;</span>, <span class="string">&#x27;two&#x27;</span>, <span class="string">&#x27;three&#x27;</span>], <span class="string">&#x27;length&#x27;</span>) <span class="comment">// &#123; &#x27;3&#x27;: [&#x27;one&#x27;, &#x27;two&#x27;], &#x27;5&#x27;: [&#x27;three&#x27;] &#125;</span></span><br><span class="line"><span class="keyword">const</span> students = [</span><br><span class="line">  &#123; <span class="attr">name</span>: <span class="string">&#x27;张三&#x27;</span>, <span class="attr">province</span>: <span class="string">&#x27;河南&#x27;</span> &#125;,</span><br><span class="line">  &#123; <span class="attr">name</span>: <span class="string">&#x27;李四&#x27;</span>, <span class="attr">province</span>: <span class="string">&#x27;河南&#x27;</span> &#125;,</span><br><span class="line">  &#123; <span class="attr">name</span>: <span class="string">&#x27;王五&#x27;</span>, <span class="attr">province</span>: <span class="string">&#x27;江苏&#x27;</span> &#125;,</span><br><span class="line">  &#123; <span class="attr">name</span>: <span class="string">&#x27;黄六&#x27;</span>, <span class="attr">province</span>: <span class="string">&#x27;河南&#x27;</span> &#125;,</span><br><span class="line">]</span><br><span class="line">_.<span class="title function_">groupBy</span>(students, <span class="string">&#x27;province&#x27;</span>)</span><br><span class="line"><span class="comment">// &#123; &#x27;江苏&#x27;: [&#123; name: &#x27;王五&#x27;, province: &#x27;江苏&#x27; &#125;], &#x27;河南&#x27;: [&#123; name: &#x27;张三&#x27;, province: &#x27;河南&#x27; &#125;, &#123; name: &#x27;李四&#x27;, province: &#x27;河南&#x27; &#125;,&#123; name: &#x27;黄六&#x27;, province: &#x27;河南&#x27; &#125;] &#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><ul><li><code>remove</code>: 移除数组中 predicate（断言）返回为真值的所有元素，并返回移除元素组成的数组。predicate（断言） 会传入 3 个参数： (<code>value</code>, <code>index</code>, <code>array</code>)。</li></ul><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> _ <span class="keyword">from</span> <span class="string">&#x27;lodash&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> array = [<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>, <span class="number">4</span>]</span><br><span class="line"><span class="keyword">const</span> evens = _.<span class="title function_">remove</span>(array, <span class="function"><span class="params">value</span> =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">return</span> value % <span class="number">2</span> == <span class="number">0</span></span><br><span class="line">&#125;)</span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(array) <span class="comment">// =&gt; [1, 3]</span></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(evens) <span class="comment">// =&gt; [2, 4]</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> students = [</span><br><span class="line">  &#123; <span class="attr">name</span>: <span class="string">&#x27;张三&#x27;</span>, <span class="attr">province</span>: <span class="string">&#x27;河南&#x27;</span> &#125;,</span><br><span class="line">  &#123; <span class="attr">name</span>: <span class="string">&#x27;李四&#x27;</span>, <span class="attr">province</span>: <span class="string">&#x27;河南&#x27;</span> &#125;,</span><br><span class="line">  &#123; <span class="attr">name</span>: <span class="string">&#x27;王五&#x27;</span>, <span class="attr">province</span>: <span class="string">&#x27;江苏&#x27;</span> &#125;,</span><br><span class="line">  &#123; <span class="attr">name</span>: <span class="string">&#x27;黄六&#x27;</span>, <span class="attr">province</span>: <span class="string">&#x27;河南&#x27;</span> &#125;,</span><br><span class="line">]</span><br><span class="line"><span class="keyword">const</span> removedStudent = _.<span class="title function_">remove</span>(students, <span class="function"><span class="params">student</span> =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">return</span> student.<span class="property">province</span> === <span class="string">&#x27;江苏&#x27;</span></span><br><span class="line">&#125;)</span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(students)</span><br><span class="line"><span class="comment">// [&#123;name: &quot;张三&quot;, province: &quot;河南&quot;&#125;, &#123;name: &quot;李四&quot;, province: &quot;河南&quot;&#125;, &#123;name: &quot;黄六&quot;, province: &quot;河南&quot;&#125;]</span></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(removedStudent)</span><br><span class="line"><span class="comment">// [&#123;name: &quot;王五&quot;, province: &quot;江苏&quot;&#125;]</span></span><br></pre></td></tr></table></figure><h3 id="根据给定值删除数组项"><a href="#根据给定值删除数组项" class="headerlink" title="根据给定值删除数组项"></a>根据给定值删除数组项</h3><ul><li><code>_.pull(array, [values])</code>: 移除数组 array 中所有和给定值相等的元素，使用 SameValueZero 进行全等比较。</li><li><code>_.pullAll(array, values)</code>: 这个方法类似 _.pull，区别是这个方法接收一个要移除值的数组。</li><li><code>_.pullAllBy(array, values, [iteratee=_.identity])</code>: 这个方法类似于 <code>_.pullAll</code> ，区别是这个方法接受一个 <code>iteratee</code>（迭代函数） 调用 <code>array</code> 和 <code>values</code>的每个值以产生一个值，通过产生的值进行了比较。<code>iteratee</code> 会传入一个参数： (<code>value</code>)。</li><li><code>_.pullAllWith(array, values, [comparator])</code>: 这个方法类似于 <code>_.pullAll</code>，区别是这个方法接受 <code>comparator</code> 调用 <code>array</code> 中的元素和 <code>values</code> 比较。<code>comparator</code> 会传入两个参数：(<code>arrVal</code>, <code>othVal</code>)。</li></ul><blockquote><p>pull 系列和 difference 系列不同之处在于 pull 系列方法会改变数组 array</p></blockquote><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> _ <span class="keyword">from</span> <span class="string">&#x27;lodash&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// pull</span></span><br><span class="line"><span class="keyword">var</span> array = [<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>, <span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>]</span><br><span class="line">_.<span class="title function_">pull</span>(array, <span class="number">2</span>, <span class="number">3</span>)</span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(array) <span class="comment">// [1, 1]</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// pullAll</span></span><br><span class="line"><span class="keyword">var</span> array = [<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>, <span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>]</span><br><span class="line">_.<span class="title function_">pullAll</span>(array, [<span class="number">2</span>, <span class="number">3</span>])</span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(array) <span class="comment">// [1, 1]</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// pullAllBy</span></span><br><span class="line"><span class="keyword">var</span> array = [&#123; <span class="attr">x</span>: <span class="number">1</span> &#125;, &#123; <span class="attr">x</span>: <span class="number">2</span> &#125;, &#123; <span class="attr">x</span>: <span class="number">3</span> &#125;, &#123; <span class="attr">x</span>: <span class="number">1</span> &#125;]</span><br><span class="line">_.<span class="title function_">pullAllBy</span>(array, [&#123; <span class="attr">x</span>: <span class="number">2</span> &#125;, &#123; <span class="attr">x</span>: <span class="number">3</span> &#125;], <span class="string">&#x27;x&#x27;</span>)</span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(array) <span class="comment">// [&#123; &#x27;x&#x27;: 1 &#125;, &#123; &#x27;x&#x27;: 1 &#125;]</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// pullAllWith</span></span><br><span class="line"><span class="keyword">var</span> array = [</span><br><span class="line">  &#123; <span class="attr">x</span>: <span class="number">1</span>, <span class="attr">y</span>: <span class="number">2</span> &#125;,</span><br><span class="line">  &#123; <span class="attr">x</span>: <span class="number">3</span>, <span class="attr">y</span>: <span class="number">4</span> &#125;,</span><br><span class="line">  &#123; <span class="attr">x</span>: <span class="number">5</span>, <span class="attr">y</span>: <span class="number">6</span> &#125;,</span><br><span class="line">]</span><br><span class="line">_.<span class="title function_">pullAllWith</span>(array, [&#123; <span class="attr">x</span>: <span class="number">3</span>, <span class="attr">y</span>: <span class="number">4</span> &#125;], _.<span class="property">isEqual</span>)</span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(array) <span class="comment">// [&#123; &#x27;x&#x27;: 1, &#x27;y&#x27;: 2 &#125;, &#123; &#x27;x&#x27;: 5, &#x27;y&#x27;: 6 &#125;]</span></span><br></pre></td></tr></table></figure><ul><li><code>_.pullAt(array, [indexes])</code>: 根据索引 <code>indexes</code>，移除 <code>array</code> 中对应的元素，并返回被移除元素的数组。</li></ul><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> _ <span class="keyword">from</span> <span class="string">&#x27;lodash&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> array = [<span class="number">5</span>, <span class="number">10</span>, <span class="number">15</span>, <span class="number">20</span>]</span><br><span class="line"><span class="keyword">var</span> evens = _.<span class="title function_">pullAt</span>(array, <span class="number">1</span>, <span class="number">3</span>)</span><br><span class="line"></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(array) <span class="comment">// [5, 15]</span></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(evens) <span class="comment">// [10, 20]</span></span><br></pre></td></tr></table></figure><ul><li><code>_.without(array, [values])</code>: 创建一个剔除所有给定值的新数组，剔除值的时候，使用 SameValueZero 做相等比较。不像 <code>_.pull</code>, 这个方法会返回一个新数组。</li></ul><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> _ <span class="keyword">from</span> <span class="string">&#x27;lodash&#x27;</span></span><br><span class="line"></span><br><span class="line">_.<span class="title function_">without</span>([<span class="number">2</span>, <span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>], <span class="number">1</span>, <span class="number">2</span>) <span class="comment">// [3]</span></span><br></pre></td></tr></table></figure><h2 id="数组去重"><a href="#数组去重" class="headerlink" title="数组去重"></a>数组去重</h2><ul><li><code>_.uniq(array)</code>: 创建一个去重后的 array 数组副本。使用了 SameValueZero 做等值比较。只有第一次出现的元素才会被保留。</li><li><code>_.uniqBy(array, [iteratee=_.identity])</code>: 这个方法类似 <code>_.uniq</code> ，除了它接受一个 <code>iteratee</code>（迭代函数），调用每一个数组（<code>array</code>）的每个元素以产生唯一性计算的标准。<code>iteratee</code> 调用时会传入一个参数：(<code>value</code>)。</li><li><code>_.uniqWith(array, [comparator])</code>: 这个方法类似 <code>_.uniq</code>， 除了它接受一个 <code>comparator</code> 调用比较 <code>array</code> 数组的每一个元素。 <code>comparator</code> 调用时会传入 2 个参数：(<code>arrVal</code>, <code>othVal</code>)。</li></ul><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> _ <span class="keyword">from</span> <span class="string">&#x27;lodash&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// uniq</span></span><br><span class="line">_.<span class="title function_">uniq</span>([<span class="number">2</span>, <span class="number">1</span>, <span class="number">2</span>]) <span class="comment">// [2, 1]</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// uniqBy</span></span><br><span class="line">_.<span class="title function_">uniqBy</span>([<span class="number">2.1</span>, <span class="number">1.2</span>, <span class="number">2.3</span>], <span class="title class_">Math</span>.<span class="property">floor</span>) <span class="comment">// [2.1, 1.2]</span></span><br><span class="line"><span class="comment">// The `_.property` iteratee shorthand.</span></span><br><span class="line">_.<span class="title function_">uniqBy</span>([&#123; <span class="attr">x</span>: <span class="number">1</span> &#125;, &#123; <span class="attr">x</span>: <span class="number">2</span> &#125;, &#123; <span class="attr">x</span>: <span class="number">1</span> &#125;], <span class="string">&#x27;x&#x27;</span>) <span class="comment">// [&#123; &#x27;x&#x27;: 1 &#125;, &#123; &#x27;x&#x27;: 2 &#125;]</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// uniqWith</span></span><br><span class="line"><span class="keyword">var</span> objects = [</span><br><span class="line">  &#123; <span class="attr">x</span>: <span class="number">1</span>, <span class="attr">y</span>: <span class="number">2</span> &#125;,</span><br><span class="line">  &#123; <span class="attr">x</span>: <span class="number">2</span>, <span class="attr">y</span>: <span class="number">1</span> &#125;,</span><br><span class="line">  &#123; <span class="attr">x</span>: <span class="number">1</span>, <span class="attr">y</span>: <span class="number">2</span> &#125;,</span><br><span class="line">]</span><br><span class="line">_.<span class="title function_">uniqWith</span>(objects, _.<span class="property">isEqual</span>) <span class="comment">// [&#123; &#x27;x&#x27;: 1, &#x27;y&#x27;: 2 &#125;, &#123; &#x27;x&#x27;: 2, &#x27;y&#x27;: 1 &#125;]</span></span><br></pre></td></tr></table></figure><h2 id="数组排序"><a href="#数组排序" class="headerlink" title="数组排序"></a>数组排序</h2><ul><li><code>_.sortBy(collection, [iteratees=[_.identity]])</code>: 创建一个元素数组。 以 <code>iteratee</code> 处理的结果升序排序。 这个方法执行稳定排序，也就是说相同元素会保持原始排序。 <code>iteratees</code> 调用1个参数：(<code>value</code>)。可以使用 <code>Array.prototype.reverse</code> 方法倒序</li></ul><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> _ <span class="keyword">from</span> <span class="string">&#x27;lodash&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> users = [</span><br><span class="line">  &#123; <span class="attr">user</span>: &#123; <span class="attr">name</span>: <span class="string">&#x27;fred&#x27;</span> &#125;, <span class="attr">age</span>: <span class="number">48</span> &#125;,</span><br><span class="line">  &#123; <span class="attr">user</span>: &#123; <span class="attr">name</span>: <span class="string">&#x27;barney&#x27;</span> &#125;, <span class="attr">age</span>: <span class="number">36</span> &#125;,</span><br><span class="line">  &#123; <span class="attr">user</span>: &#123; <span class="attr">name</span>: <span class="string">&#x27;fred&#x27;</span> &#125;, <span class="attr">age</span>: <span class="number">40</span> &#125;,</span><br><span class="line">  &#123; <span class="attr">user</span>: &#123; <span class="attr">name</span>: <span class="string">&#x27;barney&#x27;</span> &#125;, <span class="attr">age</span>: <span class="number">34</span> &#125;,</span><br><span class="line">]</span><br><span class="line"></span><br><span class="line"><span class="comment">// The `_.property` iteratee shorthand.</span></span><br><span class="line">_.<span class="title function_">sortBy</span>(users, <span class="string">&#x27;user.name&#x27;</span>)</span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * [</span></span><br><span class="line"><span class="comment"> *  &#123;age: 34, user: &quot;barney&quot;&#125;,</span></span><br><span class="line"><span class="comment"> *  &#123;age: 36, user: &quot;barney&quot;&#125;,</span></span><br><span class="line"><span class="comment"> *  &#123;age: 40, user: &quot;fred&quot;&#125;,</span></span><br><span class="line"><span class="comment"> *  &#123;age: 48, user: &quot;fred&quot;&#125;</span></span><br><span class="line"><span class="comment"> * ]</span></span><br><span class="line"><span class="comment"> */</span></span><br></pre></td></tr></table></figure><h2 id="数组降维"><a href="#数组降维" class="headerlink" title="数组降维"></a>数组降维</h2><ul><li><code>_.flatten(array)</code>: 减少一级 <code>array</code> 嵌套深度。</li><li><code>_.flattenDeep(array)</code>: 将 <code>array</code> 递归为一维数组。</li><li><code>_.flattenDepth(array, [depth=1])</code>: 根据 <code>depth</code> 递归减少 <code>array</code> 的嵌套层级</li></ul><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> _ <span class="keyword">from</span> <span class="string">&#x27;lodash&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// flatten</span></span><br><span class="line">_.<span class="title function_">flatten</span>([<span class="number">1</span>, [<span class="number">2</span>, [<span class="number">3</span>, [<span class="number">4</span>]], <span class="number">5</span>]]) <span class="comment">// [1, 2, [3, [4]], 5]</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// flattenDeep</span></span><br><span class="line">_.<span class="title function_">flattenDeep</span>([<span class="number">1</span>, [<span class="number">2</span>, [<span class="number">3</span>, [<span class="number">4</span>]], <span class="number">5</span>]]) <span class="comment">// [1, 2, 3, 4, 5]</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// flattenDepth</span></span><br><span class="line"><span class="keyword">var</span> array = [<span class="number">1</span>, [<span class="number">2</span>, [<span class="number">3</span>, [<span class="number">4</span>]], <span class="number">5</span>]]</span><br><span class="line">_.<span class="title function_">flattenDepth</span>(array, <span class="number">1</span>) <span class="comment">// [1, 2, [3, [4]], 5]</span></span><br><span class="line">_.<span class="title function_">flattenDepth</span>(array, <span class="number">2</span>) <span class="comment">// [1, 2, 3, [4], 5]</span></span><br><span class="line">_.<span class="title function_">flattenDepth</span>(array, <span class="number">3</span>) <span class="comment">// [1, 2, 3, 4, 5]</span></span><br></pre></td></tr></table></figure><h2 id="获取部分元素"><a href="#获取部分元素" class="headerlink" title="获取部分元素"></a>获取部分元素</h2><ul><li><code>_.initial(array)</code>: 获取数组 <code>array</code> 中除了最后一个元素之外的所有元素（去除数组array中的最后一个元素）。</li><li><code>_.tail(array)</code>: 获取除了 <code>array</code> 数组第一个元素以外的全部元素。</li></ul>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/bb5ef5a07d1c/</id>
    <link href="https://blog.zisheng.pro/posts/bb5ef5a07d1c/"/>
    <published>2023-03-08T09:17:16.000Z</published>
    <summary>ECMAScript 数组的每一项可以保存任何类型的数据；而且大小是可以动态调整的，即可以随着数据的添加自动增长以容纳新增数据。</summary>
    <title>JavaScript Array 完全指南</title>
    <updated>2026-09-18T08:15:28.629Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="React Native" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/React-Native/"/>
    <category term="紫升翻译计划" scheme="https://blog.zisheng.pro/categories/%E7%B4%AB%E5%8D%87%E7%BF%BB%E8%AF%91%E8%AE%A1%E5%88%92/"/>
    <category term="React Native" scheme="https://blog.zisheng.pro/tags/React-Native/"/>
    <category term="React Native 原生库" scheme="https://blog.zisheng.pro/tags/React-Native-%E5%8E%9F%E7%94%9F%E5%BA%93/"/>
    <content>
      <![CDATA[<p>react-native-create-library 使你可以使用一个命令创建 React Native 原生库的工具</p><h3 id="一、你为什么需要这个？"><a href="#一、你为什么需要这个？" class="headerlink" title="一、你为什么需要这个？"></a>一、你为什么需要这个？</h3><p>如果您要为 React Native 创建原生模块，则需要为要支持的每个平台提供一些原生代码，然后和一些 JavaScript 代码绑定在一起。自己设置可能非常耗时。</p><p>这就是这个工具的用武之地。它创建了一个包含所有当前最佳实践的样板。为什么不用 <code>react-native new-library</code>？不幸的是，该命令不会创建一个最新的库，需要一个已经初始化的 React Native 项目，并且只设置 iOS 方面的东西。</p><blockquote><p>警告：这仅创建没有视图组件的原生模块。</p></blockquote><h3 id="二、安装"><a href="#二、安装" class="headerlink" title="二、安装"></a>二、安装</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ npm install -g react-native-create-library</span><br></pre></td></tr></table></figure><p>这个命令将会创建一个 <code>MyFancyLibrary</code> 文件夹，现在可以执行 <code>yarn install</code> 来为你新创建的 <code>Library</code> 安装依赖。</p><h3 id="三、命令"><a href="#三、命令" class="headerlink" title="三、命令"></a>三、命令</h3><blockquote><p><code>react-native-create-library [options] &lt;name&gt;</code></p></blockquote><p><strong>选项：</strong></p><ul><li><code>-h</code>、<code>--help</code>: 输入有用的信息</li><li><code>-V</code>、<code>--version</code>: 输出版本号</li><li><code>-p</code>、<code>--prefix &lt;prefix&gt;</code>: 库的前缀（默认：<code>RN</code>）</li><li><code>--module-prefix &lt;modulePrefix&gt;</code>: 库的模块前缀（npm）（默认<code>react-native</code>）</li><li><code>--package-identifier &lt;packageIdentifier&gt;</code>: (Android only!) The package name for the Android module (Default: <code>com.reactlibrary</code>)</li><li><code>--namespace &lt;namespace&gt;</code>: (Windows only!) The namespace for the Windows module(Default: The name as PascalCase)</li><li><code>--platforms &lt;platforms&gt;</code>: 支持的平台（用逗号隔开，默认：<code>ios,android,windows</code>）</li><li><code>--github-account &lt;github_account&gt;</code>: 托管库的 github 账号（默认：<code>github_account</code>）</li><li><code>--author-name &lt;name&gt;</code>: 作者的名字（默认：<code>Your Name</code>）</li><li><code>--author-name &lt;email&gt;</code>: 作者的邮箱（默认：<code>yourname@email.com</code>）</li><li><code>--license &lt;license&gt;</code>: The license type of this library (Default: <code>Apache-2.0</code>)</li><li><code>--generate-example &lt;shouldGenerate&gt;</code>: 会生成一个 RN 例子并且 <code>link</code> 刚生成的库（默认：<code>false</code>）</li></ul><h3 id="四、程序化使用"><a href="#四、程序化使用" class="headerlink" title="四、程序化使用"></a>四、程序化使用</h3><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> createLibrary = <span class="built_in">require</span>(<span class="string">&#x27;react-native-create-library&#x27;</span>)</span><br><span class="line"></span><br><span class="line"><span class="title function_">createLibrary</span>(&#123;</span><br><span class="line">  <span class="attr">name</span>: <span class="string">&#x27;MyFancyLibrary&#x27;</span>,</span><br><span class="line">&#125;).<span class="title function_">then</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;Oh yay! My library has been created!&#x27;</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 js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">name</span>: <span class="title class_">String</span>, <span class="comment">/* The name of the library (Default: Library) */</span></span><br><span class="line">  <span class="attr">prefix</span>: <span class="title class_">String</span>, <span class="comment">/* The prefix for the library (Default: RN) */</span></span><br><span class="line">  <span class="attr">modulePrefix</span>: <span class="title class_">String</span>, <span class="comment">/* The module prefix for the library (Default: react-native) */</span></span><br><span class="line">  <span class="attr">platforms</span>: <span class="title class_">Array</span>, <span class="comment">/* Platforms the library will be created for. (Default: [&#x27;ios&#x27;, &#x27;android&#x27;, &#x27;windows&#x27;]) */</span></span><br><span class="line">  <span class="attr">packageIdentifier</span>: <span class="title class_">String</span>, <span class="comment">/* (Android only!) The package name for the Android module (Default: com.reactlibrary) */</span></span><br><span class="line">  <span class="attr">namespace</span>: <span class="title class_">String</span>, <span class="comment">/* (Windows only!) The namespace for the Windows module (Default: The package identifier as PascalCase, which is `Com.Reactlibrary`) */</span></span><br><span class="line">  <span class="attr">githubAccount</span>: <span class="title class_">String</span>, <span class="comment">/* The github account where the library is hosted (Default: `github_account`) */</span></span><br><span class="line">  <span class="attr">authorName</span>: <span class="title class_">String</span>, <span class="comment">/* The author&#x27;s name (Default: `Your Name`) */</span></span><br><span class="line">  <span class="attr">authorEmail</span>: <span class="title class_">String</span>, <span class="comment">/* The author&#x27;s email (Default: `yourname@email.com`) */</span></span><br><span class="line">  <span class="attr">license</span>: <span class="title class_">String</span>, <span class="comment">/* The license type of this library (Default: `Apache-2.0`) */</span></span><br><span class="line">  <span class="attr">generateExample</span>: <span class="title class_">Boolean</span>, <span class="comment">/* Will generate a RN example project and link the new library to it (Default: `false`) */</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/201099a9ba42/</id>
    <link href="https://blog.zisheng.pro/posts/201099a9ba42/"/>
    <published>2023-03-08T08:53:10.000Z</published>
    <summary>react-native-create-library 使你可以使用一个命令创建 React Native 原生库的工具</summary>
    <title>react-native-create-library 中文文档</title>
    <updated>2026-09-18T08:15:28.632Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="紫升翻译计划" scheme="https://blog.zisheng.pro/categories/%E7%B4%AB%E5%8D%87%E7%BF%BB%E8%AF%91%E8%AE%A1%E5%88%92/"/>
    <category term="React 面试题" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/React-%E9%9D%A2%E8%AF%95%E9%A2%98/"/>
    <category term="React.js 面试题" scheme="https://blog.zisheng.pro/tags/React-js-%E9%9D%A2%E8%AF%95%E9%A2%98/"/>
    <category term="React 面试题" scheme="https://blog.zisheng.pro/tags/React-%E9%9D%A2%E8%AF%95%E9%A2%98/"/>
    <category term="React 面试" scheme="https://blog.zisheng.pro/tags/React-%E9%9D%A2%E8%AF%95/"/>
    <category term="React 面试题与答案" scheme="https://blog.zisheng.pro/tags/React-%E9%9D%A2%E8%AF%95%E9%A2%98%E4%B8%8E%E7%AD%94%E6%A1%88/"/>
    <category term="前端面试" scheme="https://blog.zisheng.pro/tags/%E5%89%8D%E7%AB%AF%E9%9D%A2%E8%AF%95/"/>
    <category term="前端面试题" scheme="https://blog.zisheng.pro/tags/%E5%89%8D%E7%AB%AF%E9%9D%A2%E8%AF%95%E9%A2%98/"/>
    <category term="React 教程" scheme="https://blog.zisheng.pro/tags/React-%E6%95%99%E7%A8%8B/"/>
    <category term="React.js 教程" scheme="https://blog.zisheng.pro/tags/React-js-%E6%95%99%E7%A8%8B/"/>
    <content>
      <![CDATA[<details class="folding-tag" green><summary> 1. 什么是 React？ </summary>              <div class='content'>              <p>React 是一个<strong>开源前端 JavaScript 库</strong>，用于构建用户界面，尤其是单页应用程序。它用于处理 Web 和移动应用程序的视图层。React 是由 Facebook 的软件工程师 <a href="https://github.com/jordwalke">乔丹·沃克</a> 创建的。React 于 2011 年在 Facebook 的 News Feed 上首次发布，2012 年首次在 Instagram 发布。</p>              </div>            </details><details class="folding-tag" green><summary> 2. React 的主要特性是什么？ </summary>              <div class='content'>              <p>React 的主要特性是:</p><ul><li>考虑到 DOM 操作内存开销大，React 使用<strong>虚拟 DOM（VirtualDOM）</strong> 替代了真实 DOM（RealDOM）</li><li>支持<strong>服务端渲染</strong></li><li>遵循<strong>单向</strong>数据流或数据绑定</li><li>使用<strong>可复用&#x2F;可组合</strong>的 UI 组件来进行视图开发</li></ul>              </div>            </details><details class="folding-tag" green><summary> 3. 什么是 JSX？ </summary>              <div class='content'>              <p>JSX 是 ECMAScript 的类似 XML 的语法扩展（缩写是 JavaScript XML）。实际上，它只是为 <code>React.createElement()</code> 函数提供语法糖，为我们提供了在 JavaScript 中使用类 HTML 模板语法的能力。</p><p>下面的示例中，<code>&lt;h1&gt;</code> 标签内的文本会作为 JavaScript 函数（<code>React.createElement()</code>）的返回值返回给 render 函数：</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">App</span> <span class="keyword">extends</span> <span class="title class_ inherited__">React.Component</span> &#123;</span><br><span class="line">  <span class="title function_">render</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>&#123;&#x27;Welcome to React world!&#x27;&#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">div</span>&gt;</span></span></span><br><span class="line">    );</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>              </div>            </details><details class="folding-tag" green><summary> 4. 元素（Element）和组件（Component）的区别是什么？ </summary>              <div class='content'>              <p>元素是一个普通的对象，它描述了你希望以 DOM 节点或其他组件的形式出现在屏幕上的内容。元素可以在其 props 中包含其他 元素。创建一个 React 元素开销很小。一旦创建了元素，就永远不会对其进行修改。</p><p>React Element 的对象表示如下：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> element = <span class="title class_">React</span>.<span class="title function_">createElement</span>(<span class="string">&#x27;div&#x27;</span>, &#123; <span class="attr">id</span>: <span class="string">&#x27;login-btn&#x27;</span> &#125;, <span class="string">&#x27;Login&#x27;</span>);</span><br></pre></td></tr></table></figure><p>上面的 <code>React.createElement()</code> 函数会返回一个如下的对象：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">type</span>: <span class="string">&#x27;div&#x27;</span>,</span><br><span class="line">  <span class="attr">props</span>: &#123;</span><br><span class="line">    <span class="attr">children</span>: <span class="string">&#x27;Login&#x27;</span>,</span><br><span class="line">    <span class="attr">id</span>: <span class="string">&#x27;login-btn&#x27;</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>最终，它会使用 <code>ReactDOM.render()</code> 将元素渲染到 DOM：</p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">div</span> <span class="attr">id</span>=<span class="string">&quot;login-btn&quot;</span>&gt;</span>Login<span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br></pre></td></tr></table></figure><p>而<strong>组件</strong>可以用几种不同的方式声明。它可以是带有 <code>render()</code> 方法的类。或者，你可以简单地将其定义为一个函数。无论哪种情况，它都将 props 作为输入，并返回 JSX 树作为输出：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="title function_">Button</span> = (<span class="params">&#123; onLogin &#125;</span>) =&gt; (</span><br><span class="line">  <span class="language-xml"><span class="tag">&lt;<span class="name">div</span> <span class="attr">id</span>=<span class="string">&#123;</span>&#x27;<span class="attr">login-btn</span>&#x27;&#125; <span class="attr">onClick</span>=<span class="string">&#123;onLogin&#125;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">    Login</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></pre></td></tr></table></figure><p>然后将 JSX 编译成 <code>React.createElement()</code> 函数：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="title function_">Button</span> = (<span class="params">&#123; onLogin &#125;</span>) =&gt;</span><br><span class="line">  <span class="title class_">React</span>.<span class="title function_">createElement</span>(<span class="string">&#x27;div&#x27;</span>, &#123; <span class="attr">id</span>: <span class="string">&#x27;login-btn&#x27;</span>, <span class="attr">onClick</span>: onLogin &#125;, <span class="string">&#x27;Login&#x27;</span>);</span><br></pre></td></tr></table></figure>              </div>            </details><details class="folding-tag" green><summary> 5. 如何在 React 中创建组件？ </summary>              <div class='content'>              <p>这里有两种可以用方式来创建一个组件：</p><ol><li><strong>函数组件：</strong> 这是创建组件最简单的方式。它们是纯 JavaScript 函数，接受 props 对象作为第一个参数并返回 React 元素：</li></ol><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">Greeting</span>(<span class="params">&#123; message &#125;</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>&#123;`Hello, $&#123;message&#125;`&#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><ol start="2"><li><strong>类组件：</strong> 你也可以使用 ES6 的 class 语法来定义一个组件。上面的函数组件可以被改写为：</li></ol><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Greeting</span> <span class="keyword">extends</span> <span class="title class_ inherited__">React.Component</span> &#123;</span><br><span class="line">  <span class="title function_">render</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>&#123;`Hello, $&#123;this.props.message&#125;`&#125;<span class="tag">&lt;/<span class="name">h1</span>&gt;</span></span>;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>              </div>            </details><details class="folding-tag" green><summary> 6. 何时选择类组件或函数组件 </summary>              <div class='content'>              <p>如果一个组件需要状态或者生命周期方法，则使用类组件，否则使用函数组件。</p><p>然而，从 React 16.8 引入 Hooks 后，你可以在函数组件中使用之前只在类组件中有的特性，比如状态和生命周期方法。</p>              </div>            </details><details class="folding-tag" green><summary> 7. Pure Components 是什么？ </summary>              <div class='content'>              <p><code>React.PureComponent</code> 与 <code>React.Component</code> 几乎完全相同，不同之处在于它为你处理了 <code>shouldComponentUpdate()</code> 方法。当 props 和 state 改变时，PureComponent 将对 props 和 state 进行浅表比较。另一方面，Component 初始不会将当前 props 和 state 与 nextPorps 和 nextState 进行比较。因此，每当调用 <code>shouldComponentUpdate</code> 时，组件将默认重新渲染。</p>              </div>            </details><details class="folding-tag" green><summary> 8. React 中的 state 是什么？ </summary>              <div class='content'>              <p>组件的状态是一个对象，其中包含一些在组件的生命周期中可能会发生变化的信息。我们应该始终尝试使状态尽可能简单，并最大程度减少有状态组件的数量。</p><p>让我们来创建一个带有 message 状态的 user 组件：</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">User</span> <span class="keyword">extends</span> <span class="title class_ inherited__">React.Component</span> &#123;</span><br><span class="line">  <span class="title function_">constructor</span>(<span class="params">props</span>) &#123;</span><br><span class="line">    <span class="variable language_">super</span>(props);</span><br><span class="line"></span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">state</span> = &#123;</span><br><span class="line">      <span class="attr">message</span>: <span class="string">&#x27;Welcome to React world&#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="title function_">render</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>&#123;this.state.message&#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">div</span>&gt;</span></span></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><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/8472c6438dfe48068002c5fdf1fbf098~tplv-k3u1fbpfcp-zoom-1.image" alt="state"></p><p>状态类似于 Props，但它是私有的，并由组件完全控制。也就是说除了拥有和设置它的组件之外，其他任何组件都无法访问它。</p>              </div>            </details><details class="folding-tag" green><summary> 9. React 中的 props 是什么 </summary>              <div class='content'>              <p>Props 是组件的输入。它们是单个值或包含一组值的对象，这些对象在创建时会使用类似于 HTML 标签属性的命名约定传递给组件。它们是从父组件传递到子组件的数据。</p><p>React 中 props 的主要目的是提供以下组件功能：</p><ol><li>将自定义数据传递到你的组件。</li><li>触发状态更改。</li><li>通过组件的 <code>render()</code> 方法中的 <code>this.props.reactProp</code> 使用。</li></ol><p>举个例子，让我们创建一个带有 <code>reactProp</code> 属性的元素：</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&lt;<span class="title class_">Element</span> reactProp=&#123;<span class="string">&#x27;1&#x27;</span>&#125; /&gt;</span><br></pre></td></tr></table></figure><p>然后，这个 <code>reactProp</code>（或你想出的任何名称）成为附加到 React 的本地 props 对象的属性，该对象最初已经存在于使用 React 库创建的所有组件上。</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">props.<span class="property">reactProp</span>;</span><br></pre></td></tr></table></figure>              </div>            </details><details class="folding-tag" green><summary> 10. state 和 props 的区别是什么？ </summary>              <div class='content'>              <p>props 和 state 都是普通的 JavaScript 对象。尽管它们两者都拥有影响渲染输出的信息，但它们在组件层面的功能却有所不同。将 props 传递给组件类似于传递参数给函数，而 state 则类似于函数中声明的变量一样在组件内进行管理。</p>              </div>            </details><details class="folding-tag" green><summary> 11. 为什么我们不能直接更新状态？ </summary>              <div class='content'>              <p>如果你尝试直接更新状态，React 组件并不会重新渲染。</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 错误❌</span></span><br><span class="line"><span class="variable language_">this</span>.<span class="property">state</span>.<span class="property">message</span> = <span class="string">&#x27;Hello world&#x27;</span>;</span><br></pre></td></tr></table></figure><p>正确的做法是使用 <code>setState()</code> 方法。它会计划一个对组件状态对象的更新。当状态更改时，组件通过重新渲染进行响应。</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 正确✅</span></span><br><span class="line"><span class="variable language_">this</span>.<span class="title function_">setState</span>(&#123; <span class="attr">message</span>: <span class="string">&#x27;Hello World&#x27;</span> &#125;);</span><br></pre></td></tr></table></figure><blockquote><p>**注意：**你可以使用构造函数或者最新的 javascript class 字段声明语法直接将其分配给状态对象。</p></blockquote>              </div>            </details><details class="folding-tag" yellow><summary> 12. 回调函数作为 `setState()` 的参数的目的是什么？ </summary>              <div class='content'>              <p>setState 完成并重新渲染组件后，将调用回调函数。由于 setState() 是异步的，因此回调函数可用于任何后续操作。</p><blockquote><p>**注意：**我们建议使用生命周期方法而不是这个回调函数</p></blockquote><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="title function_">setState</span>(&#123; <span class="attr">name</span>: <span class="string">&#x27;John&#x27;</span> &#125;, <span class="function">() =&gt;</span></span><br><span class="line">  <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">&#x27;The name has updated and component re-rendered&#x27;</span>),</span><br><span class="line">);</span><br></pre></td></tr></table></figure>              </div>            </details><details class="folding-tag" yellow><summary> 13. HTML 和 React 的事件处理有什么不同？ </summary>              <div class='content'>              <p>下面是一些 HTML 和 React 的事件处理的主要不同：</p><ol><li>在 HTML 中，事件名应该是全小写的：</li></ol><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">button</span> <span class="attr">onclick</span>=<span class="string">&quot;activateLasers()&quot;</span>&gt;</span><span class="tag">&lt;/<span class="name">button</span>&gt;</span></span><br></pre></td></tr></table></figure><p>然而在 React 中事件名遵循小驼峰 格式：</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&lt;button onClick=&#123;activateLasers&#125;&gt;</span><br></pre></td></tr></table></figure><ol start="2"><li>在 HTML 中，你应该返回 <code>false</code> 来阻止默认行为：</li></ol><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">a</span> <span class="attr">href</span>=<span class="string">&quot;#&quot;</span> <span class="attr">onclick</span>=<span class="string">&#x27;console.log(&quot;The link was clicked.&quot;); return false;&#x27;</span> /&gt;</span></span><br></pre></td></tr></table></figure><p>然后在 React 中你必须明确地调用 <code>preventDefault()</code></p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">handleClick</span>(<span class="params">event</span>) &#123;</span><br><span class="line">  event.<span class="title function_">preventDefault</span>();</span><br><span class="line">  <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">&#x27;The link was clicked.&#x27;</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ol start="3"><li>在 HTML 中，你调用函数时需要加上 <code>()</code>：</li></ol><p>然后在 React 中你不应该在函数名后带上 <code>()</code>。（比如前面示例中的 <code>activateLasers</code> 函数）</p>              </div>            </details><details class="folding-tag" yellow><summary> 14. 如何在 JSX 回调函数中绑定方法或事件处理器 </summary>              <div class='content'>              <p>这里有 3 个方法做到这一点：</p><ol><li><strong>在构造器中绑定：</strong> 在 JavaScript 类中，默认情况下不绑定方法。同样的事情也适用于定义为类方法的 React 事件处理器。通常我们将它们绑定在构造函数中。</li></ol><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Component</span> <span class="keyword">extends</span> <span class="title class_ inherited__">React.Component</span> &#123;</span><br><span class="line">  <span class="title function_">constructor</span>(<span class="params">props</span>) &#123;</span><br><span class="line">    <span class="variable language_">super</span>(props);</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">handleClick</span> = <span class="variable language_">this</span>.<span class="property">handleClick</span>.<span class="title function_">bind</span>(<span class="variable language_">this</span>);</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="title function_">handleClick</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ol start="2"><li><strong>类的公共字段语法：</strong> 如果你不喜欢使用绑定的方式，也可以使用类的公共字段语法来正确绑定回调：</li></ol><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">handleClick = <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;this is:&#x27;</span>, <span class="variable language_">this</span>);</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&lt;button onClick=&#123;<span class="variable language_">this</span>.<span class="property">handleClick</span>&#125;&gt;&#123;<span class="string">&#x27;Click me&#x27;</span>&#125;&lt;/button&gt;</span><br></pre></td></tr></table></figure><ol start="3"><li><strong>箭头函数作为回调：</strong> 你可以直接在回调中使用箭头函数</li></ol><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&lt;button onClick=&#123;<span class="function"><span class="params">event</span> =&gt;</span> <span class="variable language_">this</span>.<span class="title function_">handleClick</span>(event)&#125;&gt;&#123;<span class="string">&#x27;Click me&#x27;</span>&#125;&lt;/button&gt;</span><br></pre></td></tr></table></figure><blockquote><p><strong>注意：</strong> 如果回调作为 prop 传递给子组件，这些组件可能会触发额外的重渲染。在这些场景中，考虑到性能因素，最佳的选择是使用 <code>.bind()</code> 或类的公共字段语法。</p></blockquote>              </div>            </details><details class="folding-tag" yellow><summary> 15. 如何传递参数给事件处理器或回调？ </summary>              <div class='content'>              <p>你可以使用一个箭头函数来包裹一个事件处理器并传递参数：</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&lt;button onClick=&#123;<span class="function">() =&gt;</span> <span class="variable language_">this</span>.<span class="title function_">handleClick</span>(id)&#125; /&gt;</span><br></pre></td></tr></table></figure><p>这等价于调用 <code>.bind</code> 函数：</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&lt;button onClick=&#123;<span class="variable language_">this</span>.<span class="property">handleClick</span>.<span class="title function_">bind</span>(<span class="variable language_">this</span>, id)&#125; /&gt;</span><br></pre></td></tr></table></figure><p>除了这两种办法，你也可以传递参数给一个箭头函数：</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">&lt;button onClick=&#123;<span class="variable language_">this</span>.<span class="title function_">handleClick</span>(id)&#125; /&gt;;</span><br><span class="line">handleClick = <span class="function"><span class="params">id</span> =&gt;</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;Hello, your ticket number is&#x27;</span>, id);</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>              </div>            </details><details class="folding-tag" yellow><summary> 16. React 中的合成事件是什么？ </summary>              <div class='content'>              <p><code>SyntheticEvent</code> 是基于浏览器本地事件的跨浏览器包装。它的 API 与浏览器的本地事件相同，包括 <code>stopPropagation()</code> 和 <code>preventDefault()</code>，但事件在所有浏览器中的表现均一致。</p>              </div>            </details><details class="folding-tag" yellow><summary> 17. 什么是内联条件表达式？ </summary>              <div class='content'>              <p>你可以使用 JS 可用的 <code>if</code> 语句或三元表达式来有条件地渲染表达式。 除了这些方法之外，还可以通过将所有表达式括在花括号中然后在其后跟 JS 逻辑运算符 <code>&amp;&amp;</code> 来将任何表达式嵌入 JSX。</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">&lt;h1&gt;<span class="title class_">Hello</span>!&lt;/h1&gt;;</span><br><span class="line">&#123;</span><br><span class="line">  messages.<span class="property">length</span> &gt; <span class="number">0</span> &amp;&amp; !isLogin ? (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">h2</span>&gt;</span>You have &#123;messages.length&#125; unread messages.<span class="tag">&lt;/<span class="name">h2</span>&gt;</span></span></span><br><span class="line">  ) : (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">h2</span>&gt;</span>You don&#x27;t have unread messages.<span class="tag">&lt;/<span class="name">h2</span>&gt;</span></span></span><br><span class="line">  );</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>              </div>            </details><details class="folding-tag" yellow><summary> 18. 什么是 `key` prop？在元素数组中使用它的好处是什么？ </summary>              <div class='content'>              <p><code>key</code> 是当你创建一个元素数组时应该包含的一个特殊的字符串属性。<code>key</code> prop 会帮助 React 识别具体哪一项被修改、添加或被移除。</p><p>通常，我们将数据中的 ID 用作 <code>key</code>：</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> todoItems = todos.<span class="title function_">map</span>(<span class="function"><span class="params">todo</span> =&gt;</span> <span class="language-xml"><span class="tag">&lt;<span class="name">li</span> <span class="attr">key</span>=<span class="string">&#123;todo.id&#125;</span>&gt;</span>&#123;todo.text&#125;<span class="tag">&lt;/<span class="name">li</span>&gt;</span></span>);</span><br></pre></td></tr></table></figure><p>如果呈现的项目没有稳定的 ID，退而求其次，我们可以将 <code>index</code> 作为 <code>key</code>：</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> todoItems = todos.<span class="title function_">map</span>(<span class="function">(<span class="params">todo, index</span>) =&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;index.toString()&#125;</span>&gt;</span>&#123;todo.text&#125;<span class="tag">&lt;/<span class="name">li</span>&gt;</span></span></span><br><span class="line">));</span><br></pre></td></tr></table></figure><p><strong>注意：</strong></p><ol><li>如果列表项可能改变，不建议使用 <code>indexes</code> 作为 <code>keys</code>。这可能会对性能产生负面影响，并可能导致组件状态出现问题。</li><li>如果你将列表项提取为单独的组件，则在列表组件上应用 <code>keys</code> 而不是 <code>li</code> 标签。</li><li>如果列表项中不存在 <code>key</code> prop，则控制台中将出现警告消息。</li></ol>              </div>            </details><details class="folding-tag" yellow><summary> 19. refs 有什么用？ </summary>              <div class='content'>              <p>refs 用于返回对该元素的引用。在大多数情况下，应避免使用它们，但是，当你需要直接访问 DOM 元素或组件的实例时，它们会很有用。</p>              </div>            </details><h2 id="20-如何创建-refs？"><a href="#20-如何创建-refs？" class="headerlink" title="20. 如何创建 refs？"></a>20. 如何创建 refs？</h2><p>这里有两种方式</p><p>1.这是最近添加的方法。使用 <code>React.createRef()</code> 方法创建 refs，并通过 ref 属性附加到 React 元素。为了在整个组件中使用 refs，只需将 ref 分配给构造函数中的 instance 属性。</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">MyComponent</span> <span class="keyword">extends</span> <span class="title class_ inherited__">React.Component</span> &#123;</span><br><span class="line">  <span class="title function_">constructor</span>(<span class="params">props</span>) &#123;</span><br><span class="line">    <span class="variable language_">super</span>(props);</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">myRef</span> = <span class="title class_">React</span>.<span class="title function_">createRef</span>();</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="title function_">render</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">div</span> <span class="attr">ref</span>=<span class="string">&#123;this.myRef&#125;</span> /&gt;</span></span>;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ol start="2"><li>无论 React 版本如何，你都可以使用 ref 回调方法。例如，搜索栏组件的输入元素的访问方式如下。</li></ol><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">SearchBar</span> <span class="keyword">extends</span> <span class="title class_ inherited__">Component</span> &#123;</span><br><span class="line">  <span class="title function_">constructor</span>(<span class="params">props</span>) &#123;</span><br><span class="line">    <span class="variable language_">super</span>(props);</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">txtSearch</span> = <span class="literal">null</span>;</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">state</span> = &#123; <span class="attr">term</span>: <span class="string">&#x27;&#x27;</span> &#125;;</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">setInputSearchRef</span> = <span class="function"><span class="params">e</span> =&gt;</span> &#123;</span><br><span class="line">      <span class="variable language_">this</span>.<span class="property">txtSearch</span> = e;</span><br><span class="line">    &#125;;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="title function_">onInputChange</span>(<span class="params">event</span>) &#123;</span><br><span class="line">    <span class="variable language_">this</span>.<span class="title function_">setState</span>(&#123; <span class="attr">term</span>: <span class="variable language_">this</span>.<span class="property">txtSearch</span>.<span class="property">value</span> &#125;);</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="title function_">render</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">input</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">        <span class="attr">value</span>=<span class="string">&#123;this.state.term&#125;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">        <span class="attr">onChange</span>=<span class="string">&#123;this.onInputChange.bind(this)&#125;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">        <span class="attr">ref</span>=<span class="string">&#123;this.setInputSearchRef&#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><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>你也可以使用闭包在函数组件中使用 refs。</p><blockquote><p><strong>注意：</strong> 你也可以使用内联 ref 回调，即使这不是推荐的方法</p></blockquote><h2 id="21-refs-转发是什么？"><a href="#21-refs-转发是什么？" class="headerlink" title="21. refs 转发是什么？"></a>21. refs 转发是什么？</h2><p><em>Ref 转发</em> 是让某些组件可以使用它们接收的 <code>ref</code> 的特性，这些组件还可以进一步将其传递给子组件。</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="title class_">ButtonElement</span> = <span class="title class_">React</span>.<span class="title function_">forwardRef</span>(<span class="function">(<span class="params">props, ref</span>) =&gt;</span> (</span><br><span class="line">  <span class="language-xml"><span class="tag">&lt;<span class="name">button</span> <span class="attr">ref</span>=<span class="string">&#123;ref&#125;</span> <span class="attr">className</span>=<span class="string">&quot;CustomButton&quot;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">    &#123;props.children&#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"></span><br><span class="line"><span class="comment">// Create ref to the DOM button:</span></span><br><span class="line"><span class="keyword">const</span> ref = <span class="title class_">React</span>.<span class="title function_">createRef</span>();</span><br><span class="line"><span class="language-xml"><span class="tag">&lt;<span class="name">ButtonElement</span> <span class="attr">ref</span>=<span class="string">&#123;ref&#125;</span>&gt;</span>&#123;&#x27;Forward Ref&#x27;&#125;<span class="tag">&lt;/<span class="name">ButtonElement</span>&gt;</span></span>;</span><br></pre></td></tr></table></figure><h2 id="22-refs-回调和-findDOMNode-哪个是首选项？"><a href="#22-refs-回调和-findDOMNode-哪个是首选项？" class="headerlink" title="22. refs 回调和 findDOMNode() 哪个是首选项？"></a>22. refs 回调和 <code>findDOMNode()</code> 哪个是首选项？</h2><p>最好使用 refs 回调 而不是 <code>findDOMNode()</code> API。因为 <code>findDOMNode()</code> 将来会阻止对 React 的某些改进。</p><p>使用 <code>findDOMNode</code> 的“传统”方法：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">MyComponent</span> <span class="keyword">extends</span> <span class="title class_ inherited__">Component</span> &#123;</span><br><span class="line">  <span class="title function_">componentDidMount</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="title function_">findDOMNode</span>(<span class="variable language_">this</span>).<span class="title function_">scrollIntoView</span>();</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="title function_">render</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">div</span> /&gt;</span></span>;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>推荐的方式是：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">MyComponent</span> <span class="keyword">extends</span> <span class="title class_ inherited__">Component</span> &#123;</span><br><span class="line">  <span class="title function_">constructor</span>(<span class="params">props</span>) &#123;</span><br><span class="line">    <span class="variable language_">super</span>(props);</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">node</span> = <span class="title function_">createRef</span>();</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="title function_">componentDidMount</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">node</span>.<span class="property">current</span>.<span class="title function_">scrollIntoView</span>();</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="title function_">render</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">div</span> <span class="attr">ref</span>=<span class="string">&#123;this.node&#125;</span> /&gt;</span></span>;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="23-为什么-Strings-Refs-被遗弃了？"><a href="#23-为什么-Strings-Refs-被遗弃了？" class="headerlink" title="23. 为什么 Strings Refs 被遗弃了？"></a>23. 为什么 Strings Refs 被遗弃了？</h2><p>如果你以前使用过 React，那么你可能会熟悉一个较旧的 API，其中的<code>ref</code> 属性是一个字符串，例如 <code>ref = &#123;textInput&#39;&#125;</code>，并且 DOM 节点作为<code>this.refs.textInput</code> 访问。我们建议你不要这样做，因为 String 引用有以下问题，并且被认为是旧版的。字符串引用已经在 <strong>React v16 中被删除</strong>。</p><ol><li><p>他们迫使 React 跟踪当前正在执行的组件。这是有问题的，因为它使 React 模块成为有状态的，并因此在打包 React 模块时冲突而引起奇怪的错误。</p></li><li><p>它们是“不可组合的” — 如果库在传递的子项上放置了引用，则用户不能在其上放置其他引用。回调引用完全可以组合。</p></li><li><p>他们不能和静态分析工具配合（比如 Flow）。Flow 无法猜测出框架 <code>this.refs</code> 上出现的字符串引用及其类型（可能不同）。 回调引用对静态分析更友好。</p></li><li><p>它无法像大多数人期望的那样使用“渲染回调”模式（例如）</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">MyComponent</span> <span class="keyword">extends</span> <span class="title class_ inherited__">Component</span> &#123;</span><br><span class="line">  renderRow = <span class="function"><span class="params">index</span> =&gt;</span> &#123;</span><br><span class="line">    <span class="comment">// This won&#x27;t work. Ref will get attached to DataTable rather than MyComponent:</span></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">ref</span>=<span class="string">&#123;</span>&#x27;<span class="attr">input-</span>&#x27; + <span class="attr">index</span>&#125; /&gt;</span></span>;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// This would work though! Callback refs are awesome.</span></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">ref</span>=<span class="string">&#123;input</span> =&gt;</span> (this[&#x27;input-&#x27; + index] = input)&#125; /&gt;</span>;</span><br><span class="line">  &#125;;</span><br><span class="line"></span><br><span class="line">  <span class="title function_">render</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">DataTable</span> <span class="attr">data</span>=<span class="string">&#123;this.props.data&#125;</span> <span class="attr">renderRow</span>=<span class="string">&#123;this.renderRow&#125;</span> /&gt;</span></span>;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></li></ol><h2 id="24-虚拟-DOM-是什么？"><a href="#24-虚拟-DOM-是什么？" class="headerlink" title="24. 虚拟 DOM 是什么？"></a>24. 虚拟 DOM 是什么？</h2><p><em>Virtual DOM</em>（VDOM）是<em>Real DOM</em>的内存表示形式。 UI 的表示形式保留在内存中，并与“真实” DOM 同步。 这是在调用渲染函数和在屏幕上显示元素之间发生的一步。 这整个过程称为 <a href="https://zh-hans.reactjs.org/docs/reconciliation.html">协调</a>。</p><h2 id="25-虚拟-DOM-原理"><a href="#25-虚拟-DOM-原理" class="headerlink" title="25. 虚拟 DOM 原理"></a>25. 虚拟 DOM 原理</h2><p>虚拟 DOM 工作原理只有三个简单的步骤。</p><ol><li>无论何时任何基础数据发生更改，整个 UI 都将以虚拟 DOM 表现形式重新呈现。</li></ol><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9f8a99a43f7049feb7806a1bd54e5929~tplv-k3u1fbpfcp-zoom-1.image" alt="vdom"></p><ol start="2"><li>然后，计算先前的 DOM 表现形式与新的 DOM 表现形式之间的差异。</li></ol><p><img src="https://github.com/sudheerj/reactjs-interview-questions/raw/master/images/vdom2.png" alt="vdom2"></p><ol start="3"><li>一旦完成计算，将只会更新内容真正改变的那部分真是 DOM。</li></ol><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/461ba77627524f96b2bf270c1935b969~tplv-k3u1fbpfcp-zoom-1.image" alt="vdom3"></p><h2 id="26-Shadow-DOM-和-Virtual-DOM-有什么区别？"><a href="#26-Shadow-DOM-和-Virtual-DOM-有什么区别？" class="headerlink" title="26. Shadow DOM 和 Virtual DOM 有什么区别？"></a>26. Shadow DOM 和 Virtual DOM 有什么区别？</h2><p>Shadow DOM 是一种浏览器技术，主要用于确定 web components 中的变量和 CSS。Virtual DOM 是由浏览器 API 之上的 JavaScript 库实现的概念。</p><h2 id="27-React-Fiber-是什么"><a href="#27-React-Fiber-是什么" class="headerlink" title="27. React Fiber 是什么?"></a>27. React Fiber 是什么?</h2><p>Fiber 是 React v16 中新的 <a href="https://zh-hans.reactjs.org/docs/reconciliation.html">协调</a> 引擎或核心算法的重新实现。React Fiber 的目标是提高其在动画、布局、手势、暂停、中止或重用工作的能力，以及为不同类型的更新分配优先级等方面的适用性和新的并发原语。</p><h2 id="28-React-Fiber-的主要设计目的是什么？"><a href="#28-React-Fiber-的主要设计目的是什么？" class="headerlink" title="28. React Fiber 的主要设计目的是什么？"></a>28. React Fiber 的主要设计目的是什么？</h2><p>React Fiber 的目标是提高其对动画、布局和手势等领域的适用性。它的 headline 功能是<strong>增量渲染</strong>：能够将渲染工作拆分为多个块并将其分布到多个帧中。</p><h2 id="29-受控组件是什么？"><a href="#29-受控组件是什么？" class="headerlink" title="29. 受控组件是什么？"></a>29. 受控组件是什么？</h2><p>在用户输入后能够控制表单中输入元素的组件被称为“受控组件”，比如每一个状态概念都将有一个相关的处理函数</p><p>例如下面的例子中，为了将名字转换为全大写，我们使用 <code>handleChange</code>：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="title function_">handleChange</span>(<span class="params">event</span>) &#123;</span><br><span class="line">   <span class="variable language_">this</span>.<span class="title function_">setState</span>(&#123;<span class="attr">value</span>: event.<span class="property">target</span>.<span class="property">value</span>.<span class="title function_">toUpperCase</span>()&#125;)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="30-非受控组件是什么？"><a href="#30-非受控组件是什么？" class="headerlink" title="30. 非受控组件是什么？"></a>30. 非受控组件是什么？</h2><p>非受控组件是那些把状态维护在其内部的组件，当你想要获得当前值时需要使用 ref 查询 DOM。这有一点像传统的 HTML。</p><p>在下面的 <code>UserProfile</code> 组件中，<code>name</code> 输入被使用 <code>ref</code> 获取：</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">UserProfile</span> <span class="keyword">extends</span> <span class="title class_ inherited__">React.Component</span> &#123;</span><br><span class="line">  <span class="title function_">constructor</span>(<span class="params">props</span>) &#123;</span><br><span class="line">    <span class="variable language_">super</span>(props);</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">handleSubmit</span> = <span class="variable language_">this</span>.<span class="property">handleSubmit</span>.<span class="title function_">bind</span>(<span class="variable language_">this</span>);</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">input</span> = <span class="title class_">React</span>.<span class="title function_">createRef</span>();</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="title function_">handleSubmit</span>(<span class="params">event</span>) &#123;</span><br><span class="line">    <span class="title function_">alert</span>(<span class="string">&#x27;A name was submitted: &#x27;</span> + <span class="variable language_">this</span>.<span class="property">input</span>.<span class="property">current</span>.<span class="property">value</span>);</span><br><span class="line">    event.<span class="title function_">preventDefault</span>();</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="title function_">render</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">onSubmit</span>=<span class="string">&#123;this.handleSubmit&#125;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">label</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">          &#123;&#x27;Name:&#x27;&#125;</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">ref</span>=<span class="string">&#123;this.input&#125;</span> /&gt;</span></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;/<span class="name">label</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;submit&quot;</span> <span class="attr">value</span>=<span class="string">&quot;Submit&quot;</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><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="31-createElement-和-cloneElement-的区别是什么？"><a href="#31-createElement-和-cloneElement-的区别是什么？" class="headerlink" title="31. createElement 和 cloneElement 的区别是什么？"></a>31. createElement 和 cloneElement 的区别是什么？</h2><p>JSX 元素将被转换为 <code>React.createElement()</code> 函数以创建 React 元素，这些元素将用于 UI 的对象表示。而 <code>cloneElement</code> 用于克隆元素并将新的 <code>props</code> 传递给它。</p><p><strong>课后扩展：</strong></p><ul><li><a href="https://zh-hans.reactjs.org/docs/react-api.html">React 顶层 API</a></li></ul><h2 id="32-React-中的状态提升是什么？"><a href="#32-React-中的状态提升是什么？" class="headerlink" title="32. React 中的状态提升是什么？"></a>32. React 中的状态提升是什么？</h2><p>当多个组件需要共享相同的变化数据时，建议将共享状态提升到它们最接近的共同祖先。这意味着，如果两个子组件共享来自其父组件的相同数据，则将状态移到父组件，而不是在两个子组件中都保持内部状态。</p><h2 id="33-组件生命周期有哪些不同阶段？"><a href="#33-组件生命周期有哪些不同阶段？" class="headerlink" title="33. 组件生命周期有哪些不同阶段？"></a>33. 组件生命周期有哪些不同阶段？</h2><p>组件生命周期具有三个不同的生命周期阶段。</p><ol><li><strong>Mounting：</strong> 组件已准备好安装在浏览器 DOM 中。这个阶段涵盖了生命周期方法 <code>constructor()</code>、<code>getDerivedStateFromProps()</code>、 <code>render()</code> 和 <code>componentDidMount()</code> 的初始化。</li><li><strong>Updating：</strong> 在此阶段，组件以两种方式进行更新，即发送新 <code>props</code> 和从 <code>setState()</code> 或 <code>forceUpdate()</code> 更新状态。此阶段涵盖了<code>getDerivedStateFromProps()</code>，<code>shouldComponentUpdate()</code>，<code>render()</code> 、<code>getSnapshotBeforeUpdate()</code> 和 <code>componentDidUpdate()</code> 生命周期方法。</li><li><strong>Unmounting：</strong> 在最后一个阶段，不再需要该组件并从浏览器 DOM 上卸载该组件。 这个阶段包括 <code>componentWillUnmount()</code> 生命周期方法。</li></ol><p>值得一提的是，在将更改应用于 DOM 时，React 内部具有阶段性概念。 它们分开如下</p><ol><li><strong>Render：</strong> 该组件将渲染而没有任何副作用。这适用于 Pure 组件，在此阶段，React 可以暂停、中止或重新启动渲染。</li><li><strong>Pre-commit：</strong> 在组件将更改实际应用于 DOM 之前，有一段时间可以让 React 通过 <code>getSnapshotBeforeUpdate()</code> 从 DOM 中读取内容。</li><li><strong>Commit：</strong> React 与 DOM 一起工作并分别执行最终的生命周期：<code>componentDidMount()</code> 用于安装，<code>componentDidUpdate()</code> 用于更新，以及 <code>componentWillUnmount()</code> 用于卸载。</li></ol><p>React 16.3+ (或者 <a href="http://projects.wojtekmaj.pl/react-lifecycle-methods-diagram/">在线交互版本</a>)</p><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/fac807b8490c4c11a9630a9f29e467d3~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><p>React 16.3 之前的版本：</p><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/33a918a89bdd49c08e80732f5f6af367~tplv-k3u1fbpfcp-zoom-1.image" alt="phases 16.2"></p><h2 id="34-React-生命周期有哪些？"><a href="#34-React-生命周期有哪些？" class="headerlink" title="34. React 生命周期有哪些？"></a>34. React 生命周期有哪些？</h2><p>React 16.3 以前的版本：</p><ul><li><strong>componentWillMount：</strong> 在渲染之前执行，用于根组件中的应用程序级别配置。</li><li><strong>componentDidMount：</strong> 在首次渲染之后执行，所有 AJAX 请求，DOM 或状态更新以及设置事件侦听器都应在此执行。</li><li><strong>componentWillReceiveProps：</strong> 在特定属性更新以触发状态转换时执行。</li><li><strong>shouldComponentUpdate：</strong> 确定是否要更新组件。默认情况下，它返回 <code>true</code>。如果你确定在状态或属性更新后不需要渲染组件，则可以返回 <code>false</code> 值。这是提高性能的好地方，因为如果组件收到新的 <code>props</code>，它可以防止重新渲染。</li><li><strong>componentWillUpdate：</strong> 当有属性或状态改变被<code>shouldComponentUpdate()</code> 确认并返回 <code>true</code> 时，在重新渲染组件之前执行。</li><li><strong>componentDidUpdate：</strong> 通常，它用于响应属性或状态更改来更新 DOM。</li><li><strong>componentWillUnmount：</strong> 它将用于取消任何传出的网络请求，或删除与该组件关联的所有事件侦听器。</li></ul><p>React 16.3+ 版本</p><ul><li><strong>getDerivedStateFromProps：</strong> 在调用 <code>render()</code> 之前被调用，并且在每次渲染中都会被调用。对于需要派生状态的罕见用例，这是存在的。<a href="https://reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html">如果您需要派生状态</a> 值得一读。</li><li><strong>componentDidMount：</strong> 在首次渲染之后执行，并且所有 AJAX 请求、DOM 或状态更新以及设置事件侦听器都应在此发生。</li><li><strong>shouldComponentUpdate：</strong> 确定是否将更新组件。默认情况下，它返回 <code>true</code>。如果你确定在状态或属性更新后不需要渲染组件，则可以返回 <code>false</code>值。这是提高性能的好地方，因为如果组件接收到新的属性，它可以防止重新渲染。</li><li><strong>getSnapshotBeforeUpdate：</strong> 在将呈现的输出提交给 DOM 之前立即执行。此方法返回的任何值都将传递到 <code>componentDidUpdate()</code> 中。 这对于从 DOM（即滚动位置）捕获信息很有用。</li><li><strong>componentDidUpdate：</strong> 通常，它用于响应属性或状态更改来更新 DOM。如果 <code>shouldComponentUpdate()</code> 返回 <code>false</code>，则不会触发。</li><li><strong>componentWillUnmount：</strong> 它将用于取消任何传出的网络请求，或删除与该组件关联的所有事件侦听器。</li></ul><h2 id="35-高阶组件是什么"><a href="#35-高阶组件是什么" class="headerlink" title="35. 高阶组件是什么"></a>35. 高阶组件是什么</h2><p>高阶组件（HOC）是接收组件并返回新组件的函数。基本上，这是从 React 的组成性质衍生出来的一种模式。</p><p>我们称它们为纯组件，因为它们可以接受任何动态提供的子组件，但是它们不会修改或复制其输入组件中的任何行为。</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="title class_">EnhancedComponent</span> = <span class="title function_">higherOrderComponent</span>(<span class="title class_">WrappedComponent</span>);</span><br></pre></td></tr></table></figure><p>HOC 可以用到很多场景中：</p><ol><li>代码重用，逻辑和引导程序抽象。</li><li>渲染劫持。</li><li>状态抽象和操纵。</li><li>props 操作。</li></ol><h2 id="36-如何为-HOC-组件-创建-props-代理？"><a href="#36-如何为-HOC-组件-创建-props-代理？" class="headerlink" title="36. 如何为 HOC 组件 创建 props 代理？"></a>36. 如何为 HOC 组件 创建 props 代理？</h2><p>您可以使用属性代理模式添加或编辑传递给组件的属性，如下所示：</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">HOC</span>(<span class="params">WrappedComponent</span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="keyword">class</span> <span class="title class_">Test</span> <span class="keyword">extends</span> <span class="title class_ inherited__">Component</span> &#123;</span><br><span class="line">    <span class="title function_">render</span>(<span class="params"></span>) &#123;</span><br><span class="line">      <span class="keyword">const</span> newProps = &#123;</span><br><span class="line">        <span class="attr">title</span>: <span class="string">&#x27;New Header&#x27;</span>,</span><br><span class="line">        <span class="attr">footer</span>: <span class="literal">false</span>,</span><br><span class="line">        <span class="attr">showFeatureX</span>: <span class="literal">false</span>,</span><br><span class="line">        <span class="attr">showFeatureY</span>: <span class="literal">true</span>,</span><br><span class="line">      &#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">WrappedComponent</span> &#123;<span class="attr">...this.props</span>&#125; &#123;<span class="attr">...newProps</span>&#125; /&gt;</span></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><strong>课后扩展：</strong></p><ul><li><a href="https://juejin.cn/post/6844903641074106381">react 高阶组件的代理模式</a></li></ul><h2 id="37-context-是什么？"><a href="#37-context-是什么？" class="headerlink" title="37. context 是什么？"></a>37. context 是什么？</h2><p><code>Context</code> 提供了一种通过组件树传递数据的方法，而不需要一层一层手动传递属性。</p><p>例如，需要由许多组件在应用程序中访问经过身份验证的用户，本地设置首选项，UI 主题等。</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> &#123; <span class="title class_">Provider</span>, <span class="title class_">Consumer</span> &#125; = <span class="title class_">React</span>.<span class="title function_">createContext</span>(defaultValue);</span><br></pre></td></tr></table></figure><h2 id="38-什么是-children-属性？"><a href="#38-什么是-children-属性？" class="headerlink" title="38. 什么是 children 属性？"></a>38. 什么是 children 属性？</h2><p><code>Children</code> 是一个 prop（<code>this.props.children</code>），允许你将组件作为数据传递给其他组件，就像你使用的任何其他 prop 一样。放置在组件的开始标记和结束标记之间的组件树将作为 <code>children</code> 道具传递给该组件。</p><p>React API 中有许多方法可作为该属性。其中包括 <code>React.Children.map</code>、<code>React.Children.forEach</code>，<code>React.Children.count</code>、<code>React.Children.only</code> 和 <code>React.Children.toArray</code>。</p><p>children 的简单用法如下所示：</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="title class_">MyDiv</span> = <span class="title class_">React</span>.<span class="title function_">createClass</span>(&#123;</span><br><span class="line">  <span class="attr">render</span>: <span class="keyword">function</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">div</span>&gt;</span>&#123;this.props.children&#125;<span class="tag">&lt;/<span class="name">div</span>&gt;</span></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="title class_">ReactDOM</span>.<span class="title function_">render</span>(</span><br><span class="line">  <span class="language-xml"><span class="tag">&lt;<span class="name">MyDiv</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;&#x27;Hello&#x27;&#125;<span class="tag">&lt;/<span class="name">span</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;&#x27;World&#x27;&#125;<span class="tag">&lt;/<span class="name">span</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">  <span class="tag">&lt;/<span class="name">MyDiv</span>&gt;</span></span>,</span><br><span class="line">  node,</span><br><span class="line">);</span><br></pre></td></tr></table></figure><h2 id="39-React-中如何写注释？"><a href="#39-React-中如何写注释？" class="headerlink" title="39. React 中如何写注释？"></a>39. React 中如何写注释？</h2><p>React JSX 中的注释和 JavaScript 的多行注释很像，但是用大括号括起来。</p><p><strong>单行注释：</strong></p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">&lt;div&gt;</span><br><span class="line">  &#123;<span class="comment">/* 这里是单行注释 */</span>&#125;</span><br><span class="line">  &#123;<span class="string">`Welcome <span class="subst">$&#123;user&#125;</span>, let&#x27;s play React`</span>&#125;</span><br><span class="line">&lt;/div&gt;</span><br></pre></td></tr></table></figure><p><strong>多行注释：</strong></p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">&lt;div&gt;</span><br><span class="line">  &#123;<span class="comment">/* Multi-line comments for more than</span></span><br><span class="line"><span class="comment">   one line */</span>&#125;</span><br><span class="line">  &#123;<span class="string">`Welcome <span class="subst">$&#123;user&#125;</span>, let&#x27;s play React`</span>&#125;</span><br><span class="line">&lt;/div&gt;</span><br></pre></td></tr></table></figure><h2 id="40-在-constructor-中给-super-函数传递-props-的目的是什么？"><a href="#40-在-constructor-中给-super-函数传递-props-的目的是什么？" class="headerlink" title="40. 在 constructor 中给 super 函数传递 props 的目的是什么？"></a>40. 在 constructor 中给 <code>super</code> 函数传递 props 的目的是什么？</h2><p>一个子类构造器在 <code>super()</code> 方法调用之前是无法拿到 <code>this</code> 引用的。这同样也适用于 ES6 中的 sub-classes。调用 <code>super()</code> 时传递 props 的主要是为了在子类的构造器中访问 <code>this.props</code>。</p><p><strong>传递 props：</strong></p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">MyComponent</span> <span class="keyword">extends</span> <span class="title class_ inherited__">React.Component</span> &#123;</span><br><span class="line">  <span class="title function_">constructor</span>(<span class="params">props</span>) &#123;</span><br><span class="line">    <span class="variable language_">super</span>(props);</span><br><span class="line"></span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="variable language_">this</span>.<span class="property">props</span>); <span class="comment">// 打印 &#123; name: &#x27;John&#x27;, age: 42 &#125;</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>不传递 props：</strong></p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">MyComponent</span> <span class="keyword">extends</span> <span class="title class_ inherited__">React.Component</span> &#123;</span><br><span class="line">  <span class="title function_">constructor</span>(<span class="params">props</span>) &#123;</span><br><span class="line">    <span class="variable language_">super</span>();</span><br><span class="line"></span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="variable language_">this</span>.<span class="property">props</span>); <span class="comment">// 打印 undefined</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 但是 props 参数依然可以访问</span></span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">log</span>(props); <span class="comment">// 打印 &#123; name: &#x27;John&#x27;, age: 42 &#125;</span></span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="title function_">render</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="comment">// 在 constructor 之外没有影响</span></span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="variable language_">this</span>.<span class="property">props</span>); <span class="comment">// 打印 &#123; name: &#x27;John&#x27;, age: 42 &#125;</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>上面的代码片段揭示了 <code>this.props</code> 仅在构造函数中有所不同。在构造函数外部表现将是相同的。</p><p>更多信息可以参考 <a href="https://overreacted.io/zh-hans/why-do-we-write-super-props/">为什么我们要写 super(props) ？</a></p><p>大多数场景中，我们建议使用受控组件来代替表单组件。</p><h2 id="41-协调（reconciliation）是什么？"><a href="#41-协调（reconciliation）是什么？" class="headerlink" title="41. 协调（reconciliation）是什么？"></a>41. 协调（reconciliation）是什么？</h2><p>当一个组件的 props 或 state 发生变化时，React 通过比较新返回的元素和之前渲染的元素来决定是否有必要进行实际的 DOM 更新。当它们不相等时，React 将更新 DOM。这个过程被称为 <em>协调（reconciliation）</em>。</p><h2 id="42-如何用一个动态键名来设置状态？"><a href="#42-如何用一个动态键名来设置状态？" class="headerlink" title="42. 如何用一个动态键名来设置状态？"></a>42. 如何用一个动态键名来设置状态？</h2><p>如果你使用 ES6 或 Babel 转码器来转换你的 JSX 代码，那么你可以用计算属性命名完成。</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="title function_">handleInputChange</span>(<span class="params">event</span>) &#123;</span><br><span class="line">  <span class="variable language_">this</span>.<span class="title function_">setState</span>(&#123; [event.<span class="property">target</span>.<span class="property">id</span>]: event.<span class="property">target</span>.<span class="property">value</span> &#125;)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="43-每次组件渲染时，函数被调用的常见错误是什么？"><a href="#43-每次组件渲染时，函数被调用的常见错误是什么？" class="headerlink" title="43. 每次组件渲染时，函数被调用的常见错误是什么？"></a>43. 每次组件渲染时，函数被调用的常见错误是什么？</h2><p>你需要确保在传递函数作为参数时，没有调用该函数。</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="title function_">render</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="comment">// 错误❌： handleClick 被调用而不是作为引用被传入</span></span><br><span class="line">  <span class="keyword">return</span> <span class="language-xml"><span class="tag">&lt;<span class="name">button</span> <span class="attr">onClick</span>=<span class="string">&#123;this.handleClick()&#125;</span>&gt;</span>&#123;&#x27;Click Me&#x27;&#125;<span class="tag">&lt;/<span class="name">button</span>&gt;</span></span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>取而代之的是传递函数本身，不加圆括号。</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="title function_">render</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="comment">// 正确：handleClick 是作为一个引用传递的!</span></span><br><span class="line">  <span class="keyword">return</span> <span class="language-xml"><span class="tag">&lt;<span class="name">button</span> <span class="attr">onClick</span>=<span class="string">&#123;this.handleClick&#125;</span>&gt;</span>&#123;&#x27;Click Me&#x27;&#125;<span class="tag">&lt;/<span class="name">button</span>&gt;</span></span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="44-lazy-函数是否支持命名导出？"><a href="#44-lazy-函数是否支持命名导出？" class="headerlink" title="44. lazy 函数是否支持命名导出？"></a>44. lazy 函数是否支持命名导出？</h2><p>不，目前 <code>React.lazy</code> 函数只支持默认出口。如果你想导入被命名导出的模块，你可以创建一个中间模块，将其作为默认出口。这也保证了摇树的工作，不会拉取未使用的组件。</p><p>让我们来看看一个导出多个命名组件的组件文件。</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// MoreComponents.js</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> <span class="title class_">SomeComponent</span> = <span class="comment">/* ... */</span>;</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> <span class="title class_">UnusedComponent</span> = <span class="comment">/* ... */</span>;</span><br></pre></td></tr></table></figure><p>并在一个中间文件 <code>IntermediateComponent.js</code> 中重新导出 <code>MoreComponents.js</code> 组件</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// IntermediateComponent.js</span></span><br><span class="line"><span class="keyword">export</span> &#123; <span class="title class_">SomeComponent</span> <span class="keyword">as</span> <span class="keyword">default</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;./MoreComponents.js&#x27;</span>;</span><br></pre></td></tr></table></figure><p>现在你可以使用下面的 lazy 函数导入该模块。</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">React</span>, &#123; lazy &#125; <span class="keyword">from</span> <span class="string">&#x27;react&#x27;</span>;</span><br><span class="line"><span class="keyword">const</span> <span class="title class_">SomeComponent</span> = <span class="title function_">lazy</span>(<span class="function">() =&gt;</span> <span class="keyword">import</span>(<span class="string">&#x27;./IntermediateComponent.js&#x27;</span>));</span><br></pre></td></tr></table></figure><h2 id="45-为什么-React-使用-className-而不是-class-属性？"><a href="#45-为什么-React-使用-className-而不是-class-属性？" class="headerlink" title="45. 为什么 React 使用 className 而不是 class 属性？"></a>45. 为什么 React 使用 <code>className</code> 而不是 <code>class</code> 属性？</h2><p><code>class</code> 是 JavaScript 的一个关键字，而 JSX 是 JavaScript 的一个扩展。这就是为什么 React 使用 <code>className</code> 而不是 <code>class</code> 的主要原因。传递一个字符串作为 <code>className</code> prop。</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="title function_">render</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">span</span> <span class="attr">className</span>=<span class="string">&#123;</span>&#x27;<span class="attr">menu</span> <span class="attr">navigation-menu</span>&#x27;&#125;&gt;</span>&#123;&#x27;Menu&#x27;&#125;<span class="tag">&lt;/<span class="name">span</span>&gt;</span></span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="46-片段（fragments）是什么？"><a href="#46-片段（fragments）是什么？" class="headerlink" title="46. 片段（fragments）是什么？"></a>46. 片段（fragments）是什么？</h2><p>这是 React 中常见的模式，用于一个组件返回多个元素。片段让你可以对一个 children 的列表进行分组，而无需在 DOM 中添加额外的节点。</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="title function_">render</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">React.Fragment</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">ChildA</span> /&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">ChildB</span> /&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">ChildC</span> /&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">React.Fragment</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 jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="title function_">render</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;&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">ChildA</span> /&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">ChildB</span> /&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">ChildC</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="47-为什么片段（fragments）比-div-容器要好？"><a href="#47-为什么片段（fragments）比-div-容器要好？" class="headerlink" title="47. 为什么片段（fragments）比 div 容器要好？"></a>47. 为什么片段（fragments）比 div 容器要好？</h2><ol><li>片段的速度更快一些，并且由于没有创建额外的 DOM 节点而使用更少的内存。这只有在非常大和深的树上才会体现出真正的好处。</li><li>一些 CSS 机制，如 Flexbox 和 CSS Grid 有一个特殊的父子关系，在中间添加 div 会使其难以保持所需的布局。</li><li>DOM 检查器不那么杂乱。</li></ol><h2 id="48-什么是-React-中的传递门（Portal）？"><a href="#48-什么是-React-中的传递门（Portal）？" class="headerlink" title="48. 什么是 React 中的传递门（Portal）？"></a>48. 什么是 React 中的传递门（Portal）？</h2><p>传递门是一种推荐的方式，可以将子节点渲染到父组件的 DOM 层次结构之外的 DOM 节点中。</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="title class_">ReactDOM</span>.<span class="title function_">createPortal</span>(child, container);</span><br></pre></td></tr></table></figure><p>第一个参数是任何可渲染的 React children，比如一个元素、字符串或片段。第二个参数是一个 DOM 元素。</p><h2 id="49-什么是无状态组件"><a href="#49-什么是无状态组件" class="headerlink" title="49. 什么是无状态组件?"></a>49. 什么是无状态组件?</h2><p>如果行为是独立于其状态的，那么它可以是一个无状态组件。你可以使用函数或类来创建无状态组件。但除非你需要在你的组件中使用生命周期钩子，否则你应该选择函数组件。如果你决定在这里使用函数组件，会有很多好处；它们易于编写、理解和测试，速度稍快，而且你可以完全避免使用 <code>this</code> 关键字。</p><h2 id="50-什么是状态组件"><a href="#50-什么是状态组件" class="headerlink" title="50. 什么是状态组件?"></a>50. 什么是状态组件?</h2><p>如果一个组件的行为依赖于该组件的状态（state），那么它可以被称为有状态的组件。这些有状态的组件总是类组件，并且有一个在构造器（<code>constructor</code>）中被初始化的状态。</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">App</span> <span class="keyword">extends</span> <span class="title class_ inherited__">Component</span> &#123;</span><br><span class="line">  <span class="comment">// 也可以使用类字段语法</span></span><br><span class="line">  <span class="title function_">constructor</span>(<span class="params">props</span>) &#123;</span><br><span class="line">    <span class="variable language_">super</span>(props);</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">state</span> = &#123; <span class="attr">count</span>: <span class="number">0</span> &#125;;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="title function_">render</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>React 16.8 更新：</strong></p><p>Hooks 让你在不写类的情况下使用状态和其他 React 功能。</p><p>等效的函数组件</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">React</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">const</span> <span class="title function_">App</span> = (<span class="params">props</span>) =&gt; &#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="comment">// JSX</span></span><br><span class="line">  )</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="51-如何在-React-中对-props-进行验证？"><a href="#51-如何在-React-中对-props-进行验证？" class="headerlink" title="51. 如何在 React 中对 props 进行验证？"></a>51. 如何在 React 中对 props 进行验证？</h2><p>当应用程序运行在开发模式时，React 会自动检查我们在组件上设置的所有 props，以确保它们具有正确的类型。如果类型不正确，React 会在控制台生成警告信息。由于对性能的影响，它在生产模式中被禁用。必需 props 是用 <code>isRequired</code> 定义的。</p><p>预定义的 props 类型集合。</p><ol><li><code>PropTypes.number</code></li><li><code>PropTypes.string</code></li><li><code>PropTypes.array</code></li><li><code>PropTypes.object</code></li><li><code>PropTypes.func</code></li><li><code>PropTypes.node</code></li><li><code>PropTypes.element</code></li><li><code>PropTypes.bool</code></li><li><code>PropTypes.symbol</code></li><li><code>PropTypes.any</code></li></ol><p>我们可以为 <code>User</code> 组件定义 <code>propTypes</code>，如下所示。</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">React</span> <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_">PropTypes</span> <span class="keyword">from</span> <span class="string">&#x27;prop-types&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">User</span> <span class="keyword">extends</span> <span class="title class_ inherited__">React.Component</span> &#123;</span><br><span class="line">  <span class="keyword">static</span> propTypes = &#123;</span><br><span class="line">    <span class="attr">name</span>: <span class="title class_">PropTypes</span>.<span class="property">string</span>.<span class="property">isRequired</span>,</span><br><span class="line">    <span class="attr">age</span>: <span class="title class_">PropTypes</span>.<span class="property">number</span>.<span class="property">isRequired</span>,</span><br><span class="line">  &#125;;</span><br><span class="line"></span><br><span class="line">  <span class="title function_">render</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;&gt;</span></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">h1</span>&gt;</span>&#123;`Welcome, $&#123;this.props.name&#125;`&#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">h2</span>&gt;</span>&#123;`Age, $&#123;this.props.age&#125;`&#125;<span class="tag">&lt;/<span class="name">h2</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><span class="line">&#125;</span><br></pre></td></tr></table></figure><blockquote><p>注意：在 React v15.5 中，<code>PropTypes</code> 被从 <code>React.PropTypes</code> 移到 <code>prop-types</code>库中。</p></blockquote><p>等效的函数式组件：</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">React</span> <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_">PropTypes</span> <span class="keyword">from</span> <span class="string">&#x27;prop-types&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">User</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;&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">h1</span>&gt;</span>&#123;`Welcome, $&#123;this.props.name&#125;`&#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">h2</span>&gt;</span>&#123;`Age, $&#123;this.props.age&#125;`&#125;<span class="tag">&lt;/<span class="name">h2</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><span class="line"></span><br><span class="line"><span class="title class_">User</span>.<span class="property">propTypes</span> = &#123;</span><br><span class="line">  <span class="attr">name</span>: <span class="title class_">PropTypes</span>.<span class="property">string</span>.<span class="property">isRequired</span>,</span><br><span class="line">  <span class="attr">age</span>: <span class="title class_">PropTypes</span>.<span class="property">number</span>.<span class="property">isRequired</span>,</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><h2 id="52-React-的优势是什么？"><a href="#52-React-的优势是什么？" class="headerlink" title="52. React 的优势是什么？"></a>52. React 的优势是什么？</h2><p>以下是 React的 主要优势。</p><ol><li>通过虚拟 DOM 提高应用程序的性能。</li><li>JSX 使代码易于阅读和编写。</li><li>它在客户端和服务器端都能进行渲染（SSR）。</li><li>易于与框架（Angular, Backbone）集成，因为它只是一个视图库。</li><li>使用 Jest 等工具容易编写单元和集成测试。</li></ol><h2 id="53-React-的局限性是什么？"><a href="#53-React-的局限性是什么？" class="headerlink" title="53. React 的局限性是什么？"></a>53. React 的局限性是什么？</h2><p>除了优点之外，React 也有一些限制。</p><ol><li>React 只是一个视图库，不是一个完整的框架。</li><li>对于刚接触网络开发的初学者来说，有一个学习曲线。</li><li>将 React 整合到传统的 MVC 框架中需要一些额外的配置。</li><li>代码的复杂性随着内联模板和 JSX 的增加而增加。</li><li>太多的小组件导致了过度工程化或模板化。</li></ol><h2 id="54-什么是-React-v16-中的错误边界（Error-Boundary）？"><a href="#54-什么是-React-v16-中的错误边界（Error-Boundary）？" class="headerlink" title="54. 什么是 React v16 中的错误边界（Error Boundary）？"></a>54. 什么是 React v16 中的错误边界（Error Boundary）？</h2><p>错误边界是指在其子组件树的任何地方捕获 JavaScript 错误的组件，记录这些错误，并显示一个后备 UI ，而不是崩溃的组件树。</p><p>如果一个类组件定义了一个新的生命周期方法 <code>componentDidCatch(error, info)</code> 或 <code>static getDerivedStateFromError()</code> ，它就成为一个错误边界。</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">ErrorBoundary</span> <span class="keyword">extends</span> <span class="title class_ inherited__">React.Component</span> &#123;</span><br><span class="line">  <span class="title function_">constructor</span>(<span class="params">props</span>) &#123;</span><br><span class="line">    <span class="variable language_">super</span>(props);</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">state</span> = &#123; <span class="attr">hasError</span>: <span class="literal">false</span> &#125;;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="title function_">componentDidCatch</span>(<span class="params">error, info</span>) &#123;</span><br><span class="line">    <span class="comment">// 你也可以把错误记录到一个错误报告服务中去</span></span><br><span class="line">    <span class="title function_">logErrorToMyService</span>(error, info)。</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">static</span> <span class="title function_">getDerivedStateFromError</span>(<span class="params">error</span>) &#123;</span><br><span class="line">    <span class="comment">// 更新状态，以便下次渲染时显示回退的用户界面。</span></span><br><span class="line">    <span class="keyword">return</span> &#123; <span class="attr">hasError</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="title function_">render</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="keyword">if</span> (<span class="variable language_">this</span>.<span class="property">state</span>.<span class="property">hasError</span>) &#123;</span><br><span class="line">      <span class="comment">// 你可以渲染任何自定义的回退用户界面</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>&#123;&#x27;Something went wrong.&#x27;&#125;<span class="tag">&lt;/<span class="name">h1</span>&gt;</span></span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> <span class="variable language_">this</span>.<span class="property">props</span>.<span class="property">children</span>。</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>之后把它作为一个普通的组件使用。</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">&lt;<span class="title class_">ErrorBoundary</span>&gt;</span><br><span class="line">  <span class="language-xml"><span class="tag">&lt;<span class="name">MyWidget</span> /&gt;</span></span></span><br><span class="line">&lt;/<span class="title class_">ErrorBoundary</span>&gt;</span><br></pre></td></tr></table></figure><h2 id="55-React-v15-中是如何处理错误边界的？"><a href="#55-React-v15-中是如何处理错误边界的？" class="headerlink" title="55. React v15 中是如何处理错误边界的？"></a>55. React v15 中是如何处理错误边界的？</h2><p>React v15 使用 <code>unstable_handleError</code> 方法为错误边界提供了非常基本的支持。在 React v16 中，它已经被重新命名为 <code>componentDidCatch</code>。</p><h2 id="56-静态类型检查的推荐方式是什么？"><a href="#56-静态类型检查的推荐方式是什么？" class="headerlink" title="56. 静态类型检查的推荐方式是什么？"></a>56. 静态类型检查的推荐方式是什么？</h2><p>通常我们使用 PropTypes 库（<code>React.PropTypes</code> 从 React v15.5 开始转移到 <code>prop-types</code> 包）来进行 React 应用中的类型检查。对于大型代码库，建议使用静态类型检查器，如 Flow 或 TypeScript，在编译时进行类型检查并提供自动补全功能。</p><h2 id="57-react-dom-包有什么用？"><a href="#57-react-dom-包有什么用？" class="headerlink" title="57. react-dom 包有什么用？"></a>57. <code>react-dom</code> 包有什么用？</h2><p><code>react-dom</code> 包提供了 DOM 特定的方法，可以在你的应用程序的顶层使用。大多数组件不需要使用此模块。这个包的一些方法是：</p><ol><li><code>render()</code></li><li><code>hydrate()</code></li><li><code>unmountComponentAtNode()</code></li><li><code>findDOMNode()</code></li><li><code>createPortal()</code></li></ol><h2 id="58-react-dom-的-render-方法的目的是什么？"><a href="#58-react-dom-的-render-方法的目的是什么？" class="headerlink" title="58. react-dom 的 render 方法的目的是什么？"></a>58. <code>react-dom</code> 的 render 方法的目的是什么？</h2><p>此方法用于将 React 元素渲染到提供的容器中的 DOM 中，并返回对组件的引用。如果 React 元素之前已渲染到容器中，它将对其执行更新，并且仅在必要时更改 DOM 以反映最新更改。</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="title class_">ReactDOM</span>.<span class="title function_">render</span>(element, container[, callback])</span><br></pre></td></tr></table></figure><p>如果提供了可选的回调，它将在组件渲染或更新后执行。</p><h2 id="59-什么是-ReactDOMServer？"><a href="#59-什么是-ReactDOMServer？" class="headerlink" title="59. 什么是 ReactDOMServer？"></a>59. 什么是 ReactDOMServer？</h2><p><code>ReactDOMServer</code> 对象使你能够将组件呈现为静态标记（通常用于节点服务器）。该对象主要用于服务器端渲染（SSR）。以下方法可用于服务器和浏览器环境：</p><ol><li><code>renderToString()</code></li><li><code>renderToStaticMarkup()</code></li></ol><p>例如，你通常运行基于 Node 的 Web 服务器（如 Express、Hapi 或 Koa），然后调用 <code>renderToString</code> 将根组件渲染为字符串，然后将其作为响应发送。</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 使用 Express</span></span><br><span class="line"><span class="keyword">import</span> &#123; renderToString &#125; <span class="keyword">from</span> <span class="string">&#x27;react-dom/server&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> <span class="title class_">MyPage</span> <span class="keyword">from</span> <span class="string">&#x27;./MyPage&#x27;</span>;</span><br><span class="line"></span><br><span class="line">app.<span class="title function_">get</span>(<span class="string">&#x27;/&#x27;</span>, <span class="function">(<span class="params">req, res</span>) =&gt;</span> &#123;</span><br><span class="line">  res.<span class="title function_">write</span>(<span class="string">&#x27;&lt;!DOCTYPE html&gt;&lt;html&gt;&lt;head&gt;&lt;title&gt;My Page&lt;/title&gt;&lt;/head&gt;&lt;body&gt;&#x27;</span>);</span><br><span class="line">  res.<span class="title function_">write</span>(<span class="string">&#x27;&lt;div id=&quot;content&quot;&gt;&#x27;</span>);</span><br><span class="line">  res.<span class="title function_">write</span>(<span class="title function_">renderToString</span>(<span class="language-xml"><span class="tag">&lt;<span class="name">MyPage</span> /&gt;</span></span>));</span><br><span class="line">  res.<span class="title function_">write</span>(<span class="string">&#x27;&lt;/div&gt;&lt;/body&gt;&lt;/html&gt;&#x27;</span>);</span><br><span class="line">  res.<span class="title function_">end</span>();</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><h2 id="60-如何在-React-中使用-innerHTML？"><a href="#60-如何在-React-中使用-innerHTML？" class="headerlink" title="60. 如何在 React 中使用 innerHTML？"></a>60. 如何在 React 中使用 innerHTML？</h2><p><code>dangerouslySetInnerHTML</code> 属性是 React 在浏览器 DOM 中使用 <code>innerHTML</code> 的替代品。就像 <code>innerHTML</code> 一样，考虑到跨站点脚本 (XSS) 攻击，使用此属性是有风险的。你只需要传递一个 <code>__html</code> 对象作为键和 HTML 文本作为值。</p><p>在这个例子中，MyComponent 使用 <code>dangerouslySetInnerHTML</code> 属性来设置 HTML 标记：</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">createMarkup</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> &#123; <span class="attr">__html</span>: <span class="string">&#x27;First &amp;middot; Second&#x27;</span> &#125;;</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">return</span> <span class="language-xml"><span class="tag">&lt;<span class="name">div</span> <span class="attr">dangerouslySetInnerHTML</span>=<span class="string">&#123;createMarkup()&#125;</span> /&gt;</span></span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="61-如何在-React-使用样式？"><a href="#61-如何在-React-使用样式？" class="headerlink" title="61. 如何在 React 使用样式？"></a>61. 如何在 React 使用样式？</h2><p><code>style</code> 属性接受一个小驼峰命名法属性的 JavaScript 对象，而不是一个 CSS 字符串。这与 DOM 风格的 JavaScript 属性一致，更有效率，并能防止 XSS 安全漏洞。</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> divStyle = &#123;</span><br><span class="line">  <span class="attr">color</span>: <span class="string">&#x27;blue&#x27;</span>,</span><br><span class="line">  <span class="attr">backgroundImage</span>: <span class="string">`url(<span class="subst">$&#123;imgUrl&#125;</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_">HelloWorldComponent</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">div</span> <span class="attr">style</span>=<span class="string">&#123;divStyle&#125;</span>&gt;</span>Hello World!<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>样式键名是符合驼峰命名法的，以便与在 JavaScript 中访问 DOM 节点的属性相一致（例如 <code>node.style.backgroundImage</code>）。</p><h2 id="62-事件在-React-中有何不同？"><a href="#62-事件在-React-中有何不同？" class="headerlink" title="62. 事件在 React 中有何不同？"></a>62. 事件在 React 中有何不同？</h2><p>Handling events in React elements has some syntactic differences:</p><p>在 React 元素上处理事件有一些语法上的不同：</p><ol><li>React 事件处理程序使用小驼峰命名，而不是小写。</li><li>使用 JSX，你传递一个函数作为事件处理程序，而不是一个字符串。</li></ol><h2 id="63-如果你在构造函数中使用-setState-，会发生什么？"><a href="#63-如果你在构造函数中使用-setState-，会发生什么？" class="headerlink" title="63. 如果你在构造函数中使用 setState()，会发生什么？"></a>63. 如果你在构造函数中使用 <code>setState()</code>，会发生什么？</h2><p>当你使用 <code>setState()</code> 时，除了分配给对象的状态外，React 还重新渲染组件和它的所有子组件。你会得到这样的错误：只能更新一个已挂载或正在挂载的组件。所以我们需要使用 <code>this.state</code> 来初始化构造函数中的变量。</p><h2 id="64-索引作为键的影响是什么？"><a href="#64-索引作为键的影响是什么？" class="headerlink" title="64. 索引作为键的影响是什么？"></a>64. 索引作为键的影响是什么？</h2><p>键应该是稳定的、可预测的和唯一的，这样 React 就可以跟踪元素。</p><p>在下面的代码片段中，每个元素的键都是基于索引的，而不是与被表示的数据相联系。这限制了 React 可以做的优化。</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  todos.<span class="title function_">map</span>(<span class="function">(<span class="params">todo, index</span>) =&gt;</span> <span class="language-xml"><span class="tag">&lt;<span class="name">Todo</span> &#123;<span class="attr">...todo</span>&#125; <span class="attr">key</span>=<span class="string">&#123;index&#125;</span> /&gt;</span></span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>如果你使用元素数据作为唯一键，假设 <code>todo.id</code> 在这个列表中是唯一的，并且是稳定的，React 将能够对元素进行重新排序，而不需要像以前那样重新计算它们。</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  todos.<span class="title function_">map</span>(<span class="function"><span class="params">todo</span> =&gt;</span> <span class="language-xml"><span class="tag">&lt;<span class="name">Todo</span> &#123;<span class="attr">...todo</span>&#125; <span class="attr">key</span>=<span class="string">&#123;todo.id&#125;</span> /&gt;</span></span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="65-在-componentWillMount-方法中使用-setState-真的好吗"><a href="#65-在-componentWillMount-方法中使用-setState-真的好吗" class="headerlink" title="65. 在 componentWillMount() 方法中使用 setState 真的好吗?"></a>65. 在 <code>componentWillMount()</code> 方法中使用 <code>setState</code> 真的好吗?</h2><p>是的，在 <code>componentWillMount()</code> 方法中使用 <code>setState()</code> 是安全的。但同时，建议避免在<code>componentWillMount()</code> 生命周期方法中进行异步初始化。<code>componentWillMount()</code> 在挂载发生前立即被调用。它在 <code>render()</code> 之前被调用，因此在这个方法中设置状态不会触发重新渲染。避免在这个方法中引入任何副作用或订阅。我们需要确保组件初始化的异步调用发生在 <code>componentDidMount()</code> 而不是 <code>componentWillMount()</code>。</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="title function_">componentDidMount</span>(<span class="params"></span>) &#123;</span><br><span class="line">  axios.<span class="title function_">get</span>(<span class="string">`api/todos`</span>).<span class="title function_">then</span>(<span class="function">(<span class="params">result</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="variable language_">this</span>.<span class="title function_">setState</span>(&#123;</span><br><span class="line">      <span class="attr">messages</span>: [...result.<span class="property">data</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="66-如果你在初始状态下使用-props，会发生什么？"><a href="#66-如果你在初始状态下使用-props，会发生什么？" class="headerlink" title="66. 如果你在初始状态下使用 props，会发生什么？"></a>66. 如果你在初始状态下使用 props，会发生什么？</h2><p>如果组件上的 props 被改变而组件没有被刷新，新的 props 值将永远不会被显示，因为构造函数永远不会更新组件的当前状态。来自 props 的状态初始化只在组件第一次被创建时运行。</p><p>下面这个组件就不会显示更新的输入值。</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">MyComponent</span> <span class="keyword">extends</span> <span class="title class_ inherited__">React.Component</span> &#123;</span><br><span class="line">  <span class="title function_">constructor</span>(<span class="params">props</span>) &#123;</span><br><span class="line">    <span class="variable language_">super</span>(props);</span><br><span class="line"></span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">state</span> = &#123;</span><br><span class="line">      <span class="attr">records</span>: [],</span><br><span class="line">      <span class="attr">inputValue</span>: <span class="variable language_">this</span>.<span class="property">props</span>.<span class="property">inputValue</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="title function_">render</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">div</span>&gt;</span>&#123;this.state.inputValue&#125;<span class="tag">&lt;/<span class="name">div</span>&gt;</span></span>;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在 render 方法中使用 props 将更新数值。</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">MyComponent</span> <span class="keyword">extends</span> <span class="title class_ inherited__">React.Component</span> &#123;</span><br><span class="line">  <span class="title function_">constructor</span>(<span class="params">props</span>) &#123;</span><br><span class="line">    <span class="variable language_">super</span>(props);</span><br><span class="line"></span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">state</span> = &#123;</span><br><span class="line">      <span class="attr">record</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="title function_">render</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">div</span>&gt;</span>&#123;this.props.inputValue&#125;<span class="tag">&lt;/<span class="name">div</span>&gt;</span></span>;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="67-你如何有条件地渲染组件？"><a href="#67-你如何有条件地渲染组件？" class="headerlink" title="67. 你如何有条件地渲染组件？"></a>67. 你如何有条件地渲染组件？</h2><p>在某些情况下，你想根据一些状态来渲染不同的组件。JSX 不渲染 <code>false</code> 或 <code>undefined</code>，所以你可以使用条件性短路来渲染你的组件的某一部分，只有当某个条件为真时。</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="title function_">MyComponent</span> = (<span class="params">&#123; name, address &#125;</span>) =&gt; (</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">h2</span>&gt;</span>&#123;name&#125;<span class="tag">&lt;/<span class="name">h2</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">    &#123;address &amp;&amp; <span class="tag">&lt;<span class="name">p</span>&gt;</span>&#123;address&#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></pre></td></tr></table></figure><p>如果你需要一个 <code>if-else</code> 条件，则使用三元运算符。</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="title function_">MyComponent</span> = (<span class="params">&#123; name, address &#125;</span>) =&gt; (</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">h2</span>&gt;</span>&#123;name&#125;<span class="tag">&lt;/<span class="name">h2</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">    &#123;address ? <span class="tag">&lt;<span class="name">p</span>&gt;</span>&#123;address&#125;<span class="tag">&lt;/<span class="name">p</span>&gt;</span> : <span class="tag">&lt;<span class="name">p</span>&gt;</span>&#123;&#x27;Address is not available&#x27;&#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></pre></td></tr></table></figure><h2 id="68-为什么我们在-DOM-元素上传递-props-时需要谨慎？"><a href="#68-为什么我们在-DOM-元素上传递-props-时需要谨慎？" class="headerlink" title="68. 为什么我们在 DOM 元素上传递 props 时需要谨慎？"></a>68. 为什么我们在 DOM 元素上传递 props 时需要谨慎？</h2><p>当我们传递 props 时，我们会遇到添加未知的 HTML 属性的风险，这是一个不好的做法。相反，我们可以使用带有 <code>...rest</code> 操作符的 prop 解构，所以它将只添加需要的 prop。</p><p>比如说。</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="title function_">ComponentA</span> = (<span class="params"></span>) =&gt; (</span><br><span class="line">  <span class="language-xml"><span class="tag">&lt;<span class="name">ComponentB</span> <span class="attr">isDisplay</span>=<span class="string">&#123;true&#125;</span> <span class="attr">className</span>=<span class="string">&#123;</span>&#x27;<span class="attr">componentStyle</span>&#x27;&#125; /&gt;</span></span></span><br><span class="line">);</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">ComponentB</span> = (<span class="params">&#123; isDisplay, ...domProps &#125;</span>) =&gt; (</span><br><span class="line">  <span class="language-xml"><span class="tag">&lt;<span class="name">div</span> &#123;<span class="attr">...domProps</span>&#125;&gt;</span>&#123;&#x27;ComponentB&#x27;&#125;<span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line">);</span><br></pre></td></tr></table></figure><h2 id="69-如何在-React-中使用装饰器？"><a href="#69-如何在-React-中使用装饰器？" class="headerlink" title="69. 如何在 React 中使用装饰器？"></a>69. 如何在 React 中使用装饰器？</h2><p>你可以对你的类组件进行装饰，这与将组件传入一个函数是一样的。<strong>装饰器</strong>是修改组件功能的灵活和可读的方式。</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line">@<span class="title function_">setTitle</span>(<span class="string">&#x27;Profile&#x27;</span>)</span><br><span class="line"><span class="keyword">class</span> <span class="title class_">Profile</span> <span class="keyword">extends</span> <span class="title class_ inherited__">React.Component</span> &#123;</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="comment">title 是一个字符串，将被设置为文档标题。WrappedComponent 是我们的装饰器在以下情况下会收到的东西直接放在一个组件类上面时，我们的装饰器会收到这样的信息，如上面的例子所示</span></span><br><span class="line"><span class="comment">*/</span></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">setTitle</span> = title =&gt; <span class="function"><span class="params">WrappedComponent</span> =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="keyword">class</span> <span class="title class_">extends</span> <span class="title class_">React</span>.<span class="property">Component</span> &#123;</span><br><span class="line">    <span class="title function_">componentDidMount</span>(<span class="params"></span>) &#123;</span><br><span class="line">      <span class="variable language_">document</span>.<span class="property">title</span> = title;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="title function_">render</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">WrappedComponent</span> &#123;<span class="attr">...this.props</span>&#125; /&gt;</span></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><strong>注意：</strong> 装饰器是一个没有进入 ES7 的功能，但目前是一个第二阶段的建议。</p><h2 id="70-如何-memo-化一个组件？"><a href="#70-如何-memo-化一个组件？" class="headerlink" title="70. 如何 memo 化一个组件？"></a>70. 如何 memo 化一个组件？</h2><p>有一些可用的缓存库，可以用于函数组件。</p><p>例如，<code>moize</code> 库可以在另一个组件中对组件进行 memo 化。</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> moize <span class="keyword">from</span> <span class="string">&#x27;moize&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> <span class="title class_">Component</span> <span class="keyword">from</span> <span class="string">&#x27;./components/Component&#x27;</span>; <span class="comment">// 本模块导出一个非 memo 组件</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="title class_">MemoizedFoo</span> = moize.<span class="title function_">react</span>(<span class="title class_">Component</span>);</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">Consumer</span> = (<span class="params"></span>) =&gt; &#123;</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;&#x27;I will memoize the following entry:&#x27;&#125;</span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;<span class="name">MemoizedFoo</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">&#125;;</span><br></pre></td></tr></table></figure><p><strong>更新：</strong> 从 React v16.6.0 开始，我们有一个 <code>React.memo</code>。它提供了一个更高阶的组件，除非 props 发生变化，否则会将组件缓存。要使用它，只需在使用前用 <code>React.memo</code> 包住组件。</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="title class_">MemoComponent</span> = <span class="title class_">React</span>.<span class="title function_">memo</span>(<span class="keyword">function</span> <span class="title function_">MemoComponent</span>(<span class="params">props</span>) &#123;</span><br><span class="line">  <span class="comment">/* render using props */</span></span><br><span class="line">&#125;);</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="title class_">React</span>.<span class="title function_">memo</span>(<span class="title class_">MyFunctionComponent</span>);</span><br></pre></td></tr></table></figure><h2 id="71-你如何实现服务器端渲染或SSR？"><a href="#71-你如何实现服务器端渲染或SSR？" class="headerlink" title="71. 你如何实现服务器端渲染或SSR？"></a>71. 你如何实现服务器端渲染或SSR？</h2><p>React 已经具备了在 Nod e服务器上处理渲染的能力。有一个特殊版本的 DOM 渲染器，它与客户端的模式相同。</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">ReactDOMServer</span> <span class="keyword">from</span> <span class="string">&#x27;react-dom/server&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> <span class="title class_">App</span> <span class="keyword">from</span> <span class="string">&#x27;./App&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="title class_">ReactDOMServer</span>.<span class="title function_">renderToString</span>(<span class="language-xml"><span class="tag">&lt;<span class="name">App</span> /&gt;</span></span>)。</span><br></pre></td></tr></table></figure><p>这个方法将把常规的 HTML 输出为一个字符串，然后可以作为服务器响应的一部分放在页面主体内。在客户端，React 检测到预渲染的内容，并无缝地衔接该内容。</p><h2 id="72-如何在-React-中启用生产模式？"><a href="#72-如何在-React-中启用生产模式？" class="headerlink" title="72. 如何在 React 中启用生产模式？"></a>72. 如何在 React 中启用生产模式？</h2><p>你应该使用 Webpack 的 <code>DefinePlugin</code> 方法来设置 <code>NODE_ENV</code> 为 <code>production</code>，通过它来剥离诸如 propType 验证和额外警告的东西。除此之外，如果你对代码进行最小化处理，例如 Uglify 的无效代码消除法，剥离出只用于开发的代码和注释，这将极大地减少你的包的大小。</p><h2 id="73-什么是-CRA-以及它的好处？"><a href="#73-什么是-CRA-以及它的好处？" class="headerlink" title="73. 什么是 CRA 以及它的好处？"></a>73. 什么是 CRA 以及它的好处？</h2><p><code>create-react-app</code> CLI 工具允许你快速创建和运行React应用程序，无需配置步骤。</p><p>让我们使用 CRA 创建 Todo 应用程序。</p><figure class="highlight console"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_"># </span><span class="language-bash">安装</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">npm install -g create-react-app</span></span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">创建新项目</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">create-react-app todo-app</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash"><span class="built_in">cd</span> todo-app</span></span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">构建、测试、运行</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">npm run build</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">npm run <span class="built_in">test</span></span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">npm start</span></span><br></pre></td></tr></table></figure><p>它包括我们建立一个 React 应用程序所需要的一切。</p><ol><li>支持 React、JSX、ES6 和 Flow 语法。</li><li>超越 ES6 的语言额外功能，如对象传播操作者。</li><li>自动前缀的 CSS，所以你不需要 <code>-webkit-</code> 或其他前缀。</li><li>一个快速的交互式单元测试运行器，内置支持覆盖率报告。</li><li>一个实时的开发服务器，对常见的错误发出警告。</li><li>一个构建脚本，用于捆绑 JS、CSS 和图片，并提供哈希和源码图。</li></ol><h2 id="74-安装中的生命周期方法的顺序是什么？"><a href="#74-安装中的生命周期方法的顺序是什么？" class="headerlink" title="74. 安装中的生命周期方法的顺序是什么？"></a>74. 安装中的生命周期方法的顺序是什么？</h2><p>当一个组件的实例被创建并插入到 DOM 中时，生命周期方法按以下顺序被调用。</p><ol><li><code>constructor()</code></li><li><code>static getDerivedStateFromProps()</code></li><li><code>render()</code></li><li><code>componentDidMount()</code></li></ol><h2 id="75-在-React-v16-中，有哪些生命周期方法将被废弃？"><a href="#75-在-React-v16-中，有哪些生命周期方法将被废弃？" class="headerlink" title="75. 在 React v16 中，有哪些生命周期方法将被废弃？"></a>75. 在 React v16 中，有哪些生命周期方法将被废弃？</h2><p>以下生命周期方法将是不安全的编码做法，在异步渲染中会出现更多问题。</p><ol><li><code>componentWillMount()</code></li><li><code>componentWillReceiveProps()</code></li><li><code>componentWillUpdate()</code></li></ol><p>从 React v16.3 开始，这些方法被别名为 <code>UNSAFE_</code> 前缀，未加前缀的版本将在 React v17 中被移除。</p><h2 id="76-getDerivedStateFromProps-生命周期方法的目的是什么？"><a href="#76-getDerivedStateFromProps-生命周期方法的目的是什么？" class="headerlink" title="76. getDerivedStateFromProps() 生命周期方法的目的是什么？"></a>76. <code>getDerivedStateFromProps()</code> 生命周期方法的目的是什么？</h2><p>新的静态的 <code>getDerivedStateFromProps()</code> 生命周期方法在一个组件实例化后以及重新渲染前被调用。它可以返回一个对象来更新状态，也可以返回 <code>null</code> 来表示新的 props 不需要任何状态更新。</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">MyComponent</span> <span class="keyword">extends</span> <span class="title class_ inherited__">React.Component</span> &#123;</span><br><span class="line">  <span class="keyword">static</span> <span class="title function_">getDerivedStateFromProps</span>(<span class="params">props, state</span>) &#123;</span><br><span class="line">    <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>componentDidUpdate()</code> 一起涵盖了 <code>componentWillReceiveProps()</code> 的所有用例。</p><h2 id="77-getSnapshotBeforeUpdate-生命周期方法的目的是什么？"><a href="#77-getSnapshotBeforeUpdate-生命周期方法的目的是什么？" class="headerlink" title="77. getSnapshotBeforeUpdate() 生命周期方法的目的是什么？"></a>77. <code>getSnapshotBeforeUpdate()</code> 生命周期方法的目的是什么？</h2><p>新的 <code>getSnapshotBeforeUpdate()</code> 生命周期方法会在 DOM 更新前被调用。这个方法的返回值将作为第三个参数传递给 <code>componentDidUpdate()</code>。</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">MyComponent</span> <span class="keyword">extends</span> <span class="title class_ inherited__">React.Component</span> &#123;</span><br><span class="line">  <span class="title function_">getSnapshotBeforeUpdate</span>(<span class="params">prevProps, prevState</span>) &#123;</span><br><span class="line">    <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>componentDidUpdate()</code> 一起涵盖了 <code>componentWillUpdate()</code> 的所有用例。</p><h2 id="78-Hooks-是否取代了渲染-props-和高阶组件？"><a href="#78-Hooks-是否取代了渲染-props-和高阶组件？" class="headerlink" title="78. Hooks 是否取代了渲染 props 和高阶组件？"></a>78. Hooks 是否取代了渲染 props 和高阶组件？</h2><p>渲染 props 和高阶组件都只渲染一个 children，但在大多数情况下，Hooks 是一种更简单的方式，通过减少树中的嵌套来达到这个目的。</p><h2 id="79-推荐用什么方式来命名组件？"><a href="#79-推荐用什么方式来命名组件？" class="headerlink" title="79. 推荐用什么方式来命名组件？"></a>79. 推荐用什么方式来命名组件？</h2><p>建议通过引用来命名组件，而不是使用 <code>displayName</code>。</p><p>使用 <code>displayName</code> 来命名组件。</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="title class_">React</span>.<span class="title function_">createClass</span>(&#123;</span><br><span class="line">  <span class="attr">displayName</span>: <span class="string">&#x27;TodoApp&#x27;</span>,</span><br><span class="line">  <span class="comment">// ...</span></span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p>推荐的方法。</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="keyword">class</span> <span class="title class_">TodoApp</span> <span class="keyword">extends</span> <span class="title class_ inherited__">React.Component</span> &#123;</span><br><span class="line">  <span class="comment">// ...</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="80-建议在组件类中方法的排序是什么？"><a href="#80-建议在组件类中方法的排序是什么？" class="headerlink" title="80. 建议在组件类中方法的排序是什么？"></a>80. 建议在组件类中方法的排序是什么？</h2><p>建议从安装到渲染阶段的方法的排序。</p><ol><li><code>static</code> 方法</li><li><code>constructor()</code></li><li><code>getChildContext()</code></li><li><code>componentWillMount()</code></li><li><code>componentDidMount()</code></li><li><code>componentWillReceiveProps()</code></li><li><code>shouldComponentUpdate()</code></li><li><code>componentWillUpdate()</code></li><li><code>componentDidUpdate()</code></li><li><code>componentWillUnmount()</code></li><li>点击处理程序或事件处理程序，如 <code>onClickSubmit()</code> 或 <code>onChangeDescription()</code></li><li>渲染的 getter 方法，如 <code>getSelectReason()</code> 或 <code>getFooterContent()</code></li><li>可选的渲染方法，如 <code>renderNavigation()</code> 或 <code>renderProfilePicture()</code></li><li>render()</li></ol><h2 id="81-什么是切换组件？"><a href="#81-什么是切换组件？" class="headerlink" title="81. 什么是切换组件？"></a>81. 什么是切换组件？</h2><p>切换组件是一个渲染许多组件中的一个组件。我们需要使用对象来将 props 值映射到组件。</p><p>例如，一个切换组件可以根据 <code>page</code> props 显示不同的页面。</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">HomePage</span> <span class="keyword">from</span> <span class="string">&#x27;./HomePage&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> <span class="title class_">AboutPage</span> <span class="keyword">from</span> <span class="string">&#x27;./AboutPage&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> <span class="title class_">ServicesPage</span> <span class="keyword">from</span> <span class="string">&#x27;./ServicesPage&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> <span class="title class_">ContactPage</span> <span class="keyword">from</span> <span class="string">&#x27;./ContactPage&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="variable constant_">PAGES</span> = &#123;</span><br><span class="line">  <span class="attr">home</span>: <span class="title class_">HomePage</span>,</span><br><span class="line">  <span class="attr">about</span>: <span class="title class_">AboutPage</span>,</span><br><span class="line">  <span class="attr">services</span>: <span class="title class_">ServicesPage</span>,</span><br><span class="line">  <span class="attr">contact</span>: <span class="title class_">ContactPage</span>,</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">Page</span> = props =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> <span class="title class_">Handler</span> = <span class="variable constant_">PAGES</span>[props.<span class="property">page</span>] || <span class="title class_">ContactPage</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">Handler</span> &#123;<span class="attr">...props</span>&#125; /&gt;</span></span>;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="comment">// PAGES 对象的键可以在 props 类型中使用，以捕捉开发时间错误。</span></span><br><span class="line"><span class="title class_">Page</span>.<span class="property">propTypes</span> = &#123;</span><br><span class="line">  <span class="attr">page</span>: <span class="title class_">PropTypes</span>.<span class="title function_">oneOf</span>(<span class="title class_">Object</span>.<span class="title function_">keys</span>(<span class="variable constant_">PAGES</span>)).<span class="property">isRequired</span>,</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><h2 id="82-为什么我们需要向-setState-传递一个函数？"><a href="#82-为什么我们需要向-setState-传递一个函数？" class="headerlink" title="82. 为什么我们需要向 setState() 传递一个函数？"></a>82. 为什么我们需要向 <code>setState()</code> 传递一个函数？</h2><p>这背后的原因是，<code>setState()</code> 是一个异步操作。出于性能的考虑，React 会对状态变化进行批处理，所以在调用 <code>setState()</code> 后，状态可能不会立即发生变化。这意味着你在调用 <code>setState()</code> 时不应该依赖当前的状态，因为你不能确定这个状态会是什么。解决办法是将一个函数传递给 <code>setState()</code>，并将之前的状态作为参数。通过这样做，你可以避免由于 <code>setState()</code> 的异步性而导致用户在访问时获得旧的状态值的问题。</p><p>假设初始计数值为 0。在连续三次递增操作后，该值将只递增一个。</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 假设 this.state.count === 0</span></span><br><span class="line"><span class="variable language_">this</span>.<span class="title function_">setState</span>(&#123; <span class="attr">count</span>: <span class="variable language_">this</span>.<span class="property">state</span>.<span class="property">count</span> + <span class="number">1</span> &#125;);</span><br><span class="line"><span class="variable language_">this</span>.<span class="title function_">setState</span>(&#123; <span class="attr">count</span>: <span class="variable language_">this</span>.<span class="property">state</span>.<span class="property">count</span> + <span class="number">1</span> &#125;);</span><br><span class="line"><span class="variable language_">this</span>.<span class="title function_">setState</span>(&#123; <span class="attr">count</span>: <span class="variable language_">this</span>.<span class="property">state</span>.<span class="property">count</span> + <span class="number">1</span> &#125;);</span><br><span class="line"><span class="comment">// this.state.count === 1，而不是 3</span></span><br></pre></td></tr></table></figure><p>如果我们给 <code>setState()</code> 传递一个函数，计数就会被正确地递增。</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="variable language_">this</span>.<span class="title function_">setState</span>(<span class="function">(<span class="params">prevState, props</span>) =&gt;</span> (&#123;</span><br><span class="line">  <span class="attr">count</span>: prevState.<span class="property">count</span> + props.<span class="property">increment</span>,</span><br><span class="line">&#125;));</span><br><span class="line"><span class="comment">// this.state.count === 3</span></span><br></pre></td></tr></table></figure><h2 id="83-为什么在-setState-中首选函数而不是对象？"><a href="#83-为什么在-setState-中首选函数而不是对象？" class="headerlink" title="83. 为什么在 setState() 中首选函数而不是对象？"></a>83. 为什么在 <code>setState()</code> 中首选函数而不是对象？</h2><p>React 可以将多个 <code>setState()</code> 的调用批量化为一次更新，以提高性能。因为 <code>this.props</code> 和 <code>this.state</code> 可能被异步更新，你不应该依赖它们的值来计算下一个状态。</p><p>这个计数器的例子将无法按预期更新。</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 错误❌</span></span><br><span class="line"><span class="variable language_">this</span>.<span class="title function_">setState</span>(&#123;</span><br><span class="line">  <span class="attr">counter</span>: <span class="variable language_">this</span>.<span class="property">state</span>.<span class="property">counter</span> + <span class="variable language_">this</span>.<span class="property">props</span>.<span class="property">increment</span>,</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p>首选的方法是用函数而不是对象调用 <code>setState()</code>。该函数将接收先前的状态作为第一个参数，并将应用更新时的 props 作为第二个参数。</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 正确✅</span></span><br><span class="line"><span class="variable language_">this</span>.<span class="title function_">setState</span>(<span class="function">(<span class="params">prevState, props</span>) =&gt;</span> (&#123;</span><br><span class="line">  <span class="attr">counter</span>: prevState.<span class="property">counter</span> + props.<span class="property">increment</span>,</span><br><span class="line">&#125;));</span><br></pre></td></tr></table></figure><h2 id="84-React-中的严格模式是什么？"><a href="#84-React-中的严格模式是什么？" class="headerlink" title="84. React 中的严格模式是什么？"></a>84. React 中的严格模式是什么？</h2><p><code>React.StrictMode</code> 是一个有用的组件，用于暴露应用程序中的潜在问题。就像 <code>&lt;Fragment&gt;</code>，<code>&lt;StrictMode&gt;</code>不会渲染任何额外的 DOM 元素。它为其后代激活了额外的检查和警告。这些检查只适用于开发模式。</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">React</span> <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_">ExampleApplication</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">React.StrictMode</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">          <span class="tag">&lt;<span class="name">ComponentOne</span> /&gt;</span></span></span><br><span class="line"><span class="language-xml">          <span class="tag">&lt;<span class="name">ComponentTwo</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">      <span class="tag">&lt;/<span class="name">React.StrictMode</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>在上面的例子中，严格模式检查只适用于 <code>&lt;ComponentOne&gt;</code> 和 <code>&lt;ComponentTwo&gt;</code> 组件。</p><h2 id="85-为什么-isMounted-是一个反模式，正确的解决方案是什么？"><a href="#85-为什么-isMounted-是一个反模式，正确的解决方案是什么？" class="headerlink" title="85. 为什么 isMounted() 是一个反模式，正确的解决方案是什么？"></a>85. 为什么 <code>isMounted()</code> 是一个反模式，正确的解决方案是什么？</h2><p><code>isMounted()</code> 的主要用例是避免在组件被卸载后调用 <code>setState()</code>，因为它会发出警告。</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (<span class="variable language_">this</span>.<span class="title function_">isMounted</span>()) &#123;</span><br><span class="line">  <span class="variable language_">this</span>.<span class="title function_">setState</span>(&#123;...&#125;)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在调用 <code>setState()</code> 之前检查 <code>isMounted()</code> 确实可以消除警告，但这也违背了警告的目的。使用 <code>isMounted()</code> 是一种代码异味，因为你检查的唯一原因是你认为你可能在组件卸载后还持有一个引用。</p><p>一个最佳的解决方案是找到在组件卸载后可能调用 <code>setState()</code> 的地方，并修复它们。这种情况通常是由于回调引起的，当一个组件在等待一些数据时，在数据到达之前被卸载。理想情况下，任何回调都应该在 <code>componentWillUnmount()</code> 中取消（在解除挂载之前）。</p><blockquote><p>代码异味 (Code smell)：程序开发领域，代码中的任何可能导致深层次问题的症状都可以叫做代码异味。 通常，在对代码做简短的反馈迭代时，代码异味会暴露出一些深层次的问题，这里的反馈迭代，是指以一种小范围的、可控的方式重构代码。</p></blockquote><h2 id="86-React-中支持哪些指针事件？"><a href="#86-React-中支持哪些指针事件？" class="headerlink" title="86. React 中支持哪些指针事件？"></a>86. React 中支持哪些指针事件？</h2><p>指针事件提供了一个处理所有输入事件的统一方法。在过去，我们有一个鼠标和各自的事件监听器来处理它们，但现在我们有许多设备与拥有鼠标不相关，如带有触摸表面的手机或笔。我们需要记住，这些事件只能在支持 Pointer Events 规范的浏览器中工作。</p><p>以下事件类型现在在 React DOM 中可用。</p><ol><li><code>onPointerDown</code></li><li><code>onPointerMove</code></li><li><code>onPointerUp</code></li><li><code>onPointerCancel</code></li><li><code>onGotPointerCapture</code></li><li><code>onLostPointerCapture</code></li><li><code>onPointerEnter</code></li><li><code>onPointerLeave</code></li><li><code>onPointerOver</code></li><li><code>onPointerOut</code></li></ol><h2 id="87-为什么组件名称要以大写字母开头？"><a href="#87-为什么组件名称要以大写字母开头？" class="headerlink" title="87. 为什么组件名称要以大写字母开头？"></a>87. 为什么组件名称要以大写字母开头？</h2><p>如果你使用 JSX 渲染你的组件，该组件的名称必须以大写字母开头，否则 React 将抛出一个错误，即未识别的标签。这个惯例是因为只有 HTML 元素和 SVG 标签可以以小写字母开头。</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">SomeComponent</span> <span class="keyword">extends</span> <span class="title class_ inherited__">Component</span> &#123;</span><br><span class="line">  <span class="comment">// 掘金不止，代码不停</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>你可以定义名称以小写字母开头的组件类，但当它被导入时，它应该是大写字母。在这里，小写就可以了。</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">myComponent</span> <span class="keyword">extends</span> <span class="title class_ inherited__">Component</span> &#123;</span><br><span class="line">  <span class="title function_">render</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">div</span> /&gt;</span></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> myComponent;</span><br></pre></td></tr></table></figure><p>而当导入另一个文件时，它应该以大写字母开始。</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">MyComponent</span> <span class="keyword">from</span> <span class="string">&#x27;./MyComponent&#x27;</span>;</span><br></pre></td></tr></table></figure><h3 id="关于-React-组件的命名，有哪些例外情况？"><a href="#关于-React-组件的命名，有哪些例外情况？" class="headerlink" title="关于 React 组件的命名，有哪些例外情况？"></a>关于 React 组件的命名，有哪些例外情况？</h3><p>组件名称应以大写字母开头，但这一惯例也有少数例外。带点的小写标签名（属性访问器）仍被认为是有效的组件名。</p><p>例如，下面的标签可以被编译成一个有效的组件。</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="title function_">render</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">obj.component</span> /&gt;</span></span> <span class="comment">// `React.createElement(obj.component)`</span></span><br><span class="line">   )</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="88-React-v16-中支持自定义-DOM-属性吗？"><a href="#88-React-v16-中支持自定义-DOM-属性吗？" class="headerlink" title="88. React v16 中支持自定义 DOM 属性吗？"></a>88. React v16 中支持自定义 DOM 属性吗？</h2><p>是的，在过去，React 习惯于忽略未知的 DOM 属性。如果你写的 JSX 有一个 React 不认识的属性，React 会直接跳过它。</p><p>例如，让我们看一下下面的属性。</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&lt;div mycustomattribute=&#123;<span class="string">&#x27;something&#x27;</span>&#125; /&gt;</span><br></pre></td></tr></table></figure><p>用 React v15 渲染一个空的 div 到 DOM 上。</p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">div</span> /&gt;</span></span><br></pre></td></tr></table></figure><p>在 React v16 中，任何未知的属性最终都会出现在 DOM 中。</p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">div</span> <span class="attr">mycustomattribute</span>=<span class="string">&quot;something&quot;</span> /&gt;</span></span><br></pre></td></tr></table></figure><p>这对于提供浏览器特定的非标准属性，尝试新的 DOM API，以及与有主见的第三方库集成是非常有用的。</p><h2 id="89-constructor-和-getInitialState-的区别是什么？"><a href="#89-constructor-和-getInitialState-的区别是什么？" class="headerlink" title="89. constructor 和 getInitialState 的区别是什么？"></a>89. constructor 和 getInitialState 的区别是什么？</h2><p>当使用 ES6 类时，你应该在构造函数中初始化状态，而当使用 <code>React.createClass()</code> 时，应该在 <code>getInitialState()</code> 方法中初始化状态。</p><p><strong>使用 ES6 类：</strong></p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">MyComponent</span> <span class="keyword">extends</span> <span class="title class_ inherited__">React.Component</span> &#123;</span><br><span class="line">  <span class="title function_">constructor</span>(<span class="params">props</span>) &#123;</span><br><span class="line">    <span class="variable language_">super</span>(props);</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">state</span> = &#123;</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><p><strong>使用 <code>React.createClass()</code>：</strong></p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="title class_">MyComponent</span> = <span class="title class_">React</span>.<span class="title function_">createClass</span>(&#123;</span><br><span class="line">  <span class="title function_">getInitialState</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="keyword">return</span> &#123;</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><p><strong>注意：</strong> <code>React.createClass()</code> 在 React v16 中已被废弃并删除。请使用普通的 JavaScript 类来代替。</p><h2 id="90-你能在不调用-setState-的情况下强制一个组件重新渲染吗？"><a href="#90-你能在不调用-setState-的情况下强制一个组件重新渲染吗？" class="headerlink" title="90. 你能在不调用 setState 的情况下强制一个组件重新渲染吗？"></a>90. 你能在不调用 setState 的情况下强制一个组件重新渲染吗？</h2><p>默认情况下，当你的组件的状态或 props 改变时，你的组件会重新渲染。如果你的 <code>render()</code> 方法依赖于其他数据，你可以通过调用 <code>forceUpdate()</code> 告诉 React 该组件需要重新渲染。</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">component.<span class="title function_">forceUpdate</span>(callback);</span><br></pre></td></tr></table></figure><p>建议避免使用 <code>forceUpdate()</code>，只在 <code>render()</code> 中读取<code>this.props</code> 和 <code>this.state</code>。</p><h2 id="91-在-React-中使用-ES6-类的，super-和-super-props-之间有什么区别？"><a href="#91-在-React-中使用-ES6-类的，super-和-super-props-之间有什么区别？" class="headerlink" title="91. 在  React 中使用 ES6 类的，super() 和 super(props) 之间有什么区别？"></a>91. 在  React 中使用 ES6 类的，<code>super()</code> 和 <code>super(props)</code> 之间有什么区别？</h2><p>当你想在 <code>constructor()</code> 中访问 <code>this.props</code> 时，你应该把 props 传给 <code>super()</code> 方法。</p><p><strong>使用 <code>super(props)</code>：</strong></p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">MyComponent</span> <span class="keyword">extends</span> <span class="title class_ inherited__">React.Component</span> &#123;</span><br><span class="line">  <span class="title function_">constructor</span>(<span class="params">props</span>) &#123;</span><br><span class="line">    <span class="variable language_">super</span>(props);</span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="variable language_">this</span>.<span class="property">props</span>); <span class="comment">// &#123; name: &#x27;John&#x27;, ... &#125;</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>使用 <code>super()</code>：</strong></p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">MyComponent</span> <span class="keyword">extends</span> <span class="title class_ inherited__">React.Component</span> &#123;</span><br><span class="line">  <span class="title function_">constructor</span>(<span class="params">props</span>) &#123;</span><br><span class="line">    <span class="variable language_">super</span>();</span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="variable language_">this</span>.<span class="property">props</span>); <span class="comment">// undefined</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在 <code>constructor()</code> 之外，两者都会显示相同的 <code>this.props</code> 的值。</p><h2 id="92-如何在-JSX-内循环？"><a href="#92-如何在-JSX-内循环？" class="headerlink" title="92. 如何在 JSX 内循环？"></a>92. 如何在 JSX 内循环？</h2><p>你可以简单地使用 <code>Array.prototype.map</code> 与 ES6 箭头函数语法。</p><p>例如，对象的 <code>items</code> 数组被映射成组件的数组。</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">&lt;tbody&gt;</span><br><span class="line">  &#123;items.<span class="title function_">map</span>(<span class="function"><span class="params">item</span> =&gt;</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">SomeComponent</span> <span class="attr">key</span>=<span class="string">&#123;item.id&#125;</span> <span class="attr">name</span>=<span class="string">&#123;item.name&#125;</span> /&gt;</span></span></span><br><span class="line">  ))&#125;</span><br><span class="line">&lt;/tbody&gt;</span><br></pre></td></tr></table></figure><p>但你不能用 <code>for</code> 循环来迭代。</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">&lt;tbody&gt;</span><br><span class="line"><span class="keyword">for</span> (<span class="keyword">let</span> i = <span class="number">0</span>; i &lt; items.<span class="property">length</span>; i++) &#123;</span><br><span class="line">  <span class="language-xml"><span class="tag">&lt;<span class="name">SomeComponent</span> <span class="attr">key</span>=<span class="string">&#123;items[i].id&#125;</span> <span class="attr">name</span>=<span class="string">&#123;items[i].name&#125;</span> /&gt;</span></span></span><br><span class="line">&#125;</span><br><span class="line">&lt;/tbody&gt;</span><br></pre></td></tr></table></figure><p>这是因为 JSX 标签被转换为函数调用，而且你不能在表达式中使用语句。这可能会改变，因为 <code>do</code> 表达式是第一阶段的建议。</p><h2 id="93-你如何在属性引号中访问-props？"><a href="#93-你如何在属性引号中访问-props？" class="headerlink" title="93. 你如何在属性引号中访问 props？"></a>93. 你如何在属性引号中访问 props？</h2><p>React（或 JSX）不支持属性值内的变量插值。下面的表示方法就不能用了。</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&lt;img className=<span class="string">&quot;image&quot;</span> src=<span class="string">&quot;images/&#123;this.props.image&#125;&quot;</span> /&gt;</span><br></pre></td></tr></table></figure><p>但你可以把任何 JS 表达式放在大括号内作为整个属性值。所以下面的表达式是有效的。</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&lt;img className=<span class="string">&quot;image&quot;</span> src=&#123;<span class="string">&#x27;images/&#x27;</span> + <span class="variable language_">this</span>.<span class="property">props</span>.<span class="property">image</span>&#125; /&gt;</span><br></pre></td></tr></table></figure><p>使用模板字符串也可以。</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&lt;img className=<span class="string">&quot;image&quot;</span> src=&#123;<span class="string">`images/<span class="subst">$&#123;<span class="variable language_">this</span>.props.image&#125;</span>`</span>&#125; /&gt;</span><br></pre></td></tr></table></figure><h2 id="94-什么是带-shape-的-React-原型数组？"><a href="#94-什么是带-shape-的-React-原型数组？" class="headerlink" title="94. 什么是带 shape 的 React 原型数组？"></a>94. 什么是带 shape 的 React 原型数组？</h2><p>如果你想把一个对象数组传递给一个具有特定 shape 的组件，那么使用 <code>React.PropTypes.shape()</code> 作为 <code>React.PropTypes.arrayOf()</code> 的一个参数。</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="title class_">ReactComponent</span>.<span class="property">propTypes</span> = &#123;</span><br><span class="line">  <span class="attr">arrayWithShape</span>: <span class="title class_">React</span>.<span class="property">PropTypes</span>.<span class="title function_">arrayOf</span>(</span><br><span class="line">    <span class="title class_">React</span>.<span class="property">PropTypes</span>.<span class="title function_">shape</span>(&#123;</span><br><span class="line">      <span class="attr">color</span>: <span class="title class_">React</span>.<span class="property">PropTypes</span>.<span class="property">string</span>.<span class="property">isRequired</span>,</span><br><span class="line">      <span class="attr">fontSize</span>: <span class="title class_">React</span>.<span class="property">PropTypes</span>.<span class="property">number</span>.<span class="property">isRequired</span>,</span><br><span class="line">    &#125;),</span><br><span class="line">  ).<span class="property">isRequired</span>,</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><h2 id="95-如何有条件地应用类属性？"><a href="#95-如何有条件地应用类属性？" class="headerlink" title="95. 如何有条件地应用类属性？"></a>95. 如何有条件地应用类属性？</h2><p>你不应该在引号内使用大括号，因为它将被计算为一个字符串。</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&lt;div className=<span class="string">&quot;btn-panel &#123;this.props.visible ? &#x27;show&#x27; : &#x27;hidden&#x27;&#125;&quot;</span>&gt;</span><br></pre></td></tr></table></figure><p>相反，你需要把大括号移到外面（别忘了在类名之间包括空格）。</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&lt;div className=&#123;<span class="string">&#x27;btn-panel &#x27;</span> + (<span class="variable language_">this</span>.<span class="property">props</span>.<span class="property">visible</span> ? <span class="string">&#x27;show&#x27;</span> : <span class="string">&#x27;hidden&#x27;</span>)&#125;&gt;</span><br></pre></td></tr></table></figure><p>模板字符串也可以使用。</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&lt;div className=&#123;<span class="string">`btn-panel <span class="subst">$&#123;<span class="variable language_">this</span>.props.visible ? <span class="string">&#x27;show&#x27;</span> : <span class="string">&#x27;hidden&#x27;</span>&#125;</span>`</span>&#125;&gt;</span><br></pre></td></tr></table></figure><h2 id="96-React-和-ReactDOM-之间有什么区别？"><a href="#96-React-和-ReactDOM-之间有什么区别？" class="headerlink" title="96. React 和 ReactDOM 之间有什么区别？"></a>96. React 和 ReactDOM 之间有什么区别？</h2><p><code>react</code> 包包含 <code>React.createElement()</code>、<code>React.Component</code>、<code>React.Children</code>, 以及其他与元素和组件类相关的帮助函数。你可以把这些看作是你构建组件所需要的同构或通用助手。<code>react-dom</code> 包包含 <code>ReactDOM.render()</code>，在 <code>react-dom/server</code> 中，我们有 <code>ReactDOMServer.renderToString()</code> 和 <code>ReactDOMServer.renderToStaticMarkup()</code> 的服务器端渲染支持。</p><h2 id="97-为什么-ReactDOM-要从-React-中分离出来？"><a href="#97-为什么-ReactDOM-要从-React-中分离出来？" class="headerlink" title="97. 为什么 ReactDOM 要从 React 中分离出来？"></a>97. 为什么 ReactDOM 要从 React 中分离出来？</h2><p>React 团队致力于将所有与 DOM 相关的功能提取到一个单独的库中，称为 ReactDOM。React v0.14 是第一个分割库的版本。通过查看一些包，<code>react-native</code>、<code>react-art</code>、<code>react-canvas</code>和 <code>react-three</code>，已经很清楚，React 的优秀和本质与浏览器或 DOM 无关。</p><p>为了建立更多 React 可以渲染的环境，React 团队计划将主 React 包分成两个：<code>react</code> 和 <code>react-dom</code>。这就为编写可以在网络版 React 和 React Native 之间共享的组件铺平了道路。</p><h2 id="98-如何使用-React-label-元素？"><a href="#98-如何使用-React-label-元素？" class="headerlink" title="98. 如何使用 React label 元素？"></a>98. 如何使用 React label 元素？</h2><p>如果你试图用标准的 <code>for</code> 属性渲染一个绑定在文本输入上的 <code>&lt;label&gt;</code> 元素，那么它产生的 HTML 会缺少该属性，并在控制台打印出警告。</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">&lt;label <span class="keyword">for</span>=&#123;<span class="string">&#x27;user&#x27;</span>&#125;&gt;&#123;<span class="string">&#x27;User&#x27;</span>&#125;&lt;/label&gt;</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">&#123;</span>&#x27;<span class="attr">text</span>&#x27;&#125; <span class="attr">id</span>=<span class="string">&#123;</span>&#x27;<span class="attr">user</span>&#x27;&#125; /&gt;</span></span></span><br></pre></td></tr></table></figure><p>由于 <code>for</code> 在 JavaScript 中是一个保留关键字，我们可以使用 <code>htmlFor</code> 代替。</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">&lt;label htmlFor=&#123;<span class="string">&#x27;user&#x27;</span>&#125;&gt;&#123;<span class="string">&#x27;User&#x27;</span>&#125;&lt;/label&gt;</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">&#123;</span>&#x27;<span class="attr">text</span>&#x27;&#125; <span class="attr">id</span>=<span class="string">&#123;</span>&#x27;<span class="attr">user</span>&#x27;&#125; /&gt;</span></span></span><br></pre></td></tr></table></figure><h2 id="99-如何组合多个内联样式对象？"><a href="#99-如何组合多个内联样式对象？" class="headerlink" title="99. 如何组合多个内联样式对象？"></a>99. 如何组合多个内联样式对象？</h2><p>你可以在常规 React 中使用展开语法。</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">&lt;button style=&#123;&#123; ...styles.<span class="property">panel</span>.<span class="property">button</span>, ...styles.<span class="property">panel</span>.<span class="property">submitButton</span> &#125;&#125;&gt;</span><br><span class="line">  &#123;<span class="string">&#x27;Submit&#x27;</span>&#125;</span><br><span class="line">&lt;/button&gt;</span><br></pre></td></tr></table></figure><p>如果你使用的是 React Native，那么你可以使用数组符号。</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">&lt;button style=&#123;[styles.<span class="property">panel</span>.<span class="property">button</span>, styles.<span class="property">panel</span>.<span class="property">submitButton</span>]&#125;&gt;</span><br><span class="line">  &#123;<span class="string">&#x27;Submit&#x27;</span>&#125;</span><br><span class="line">&lt;/button&gt;</span><br></pre></td></tr></table></figure><h2 id="100-如何在浏览器调整大小时重新渲染视图？"><a href="#100-如何在浏览器调整大小时重新渲染视图？" class="headerlink" title="100. 如何在浏览器调整大小时重新渲染视图？"></a>100. 如何在浏览器调整大小时重新渲染视图？</h2><p>你可以在 <code>componentDidMount()</code> 中监听 <code>resize</code> 事件，然后更新尺寸（<code>width</code> 和 <code>height</code>）。你应该在 <code>componentWillUnmount()</code> 方法中移除监听器。</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">WindowDimensions</span> <span class="keyword">extends</span> <span class="title class_ inherited__">React.Component</span> &#123;</span><br><span class="line"></span><br><span class="line">  <span class="title function_">componentWillMount</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="variable language_">this</span>.<span class="title function_">updateDimensions</span>();</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="title function_">componentDidMount</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="variable language_">window</span>.<span class="title function_">addEventListener</span>(<span class="string">&#x27;resize&#x27;</span>, <span class="variable language_">this</span>.<span class="property">updateDimensions</span>);</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="title function_">componentWillUnmount</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="variable language_">window</span>.<span class="title function_">removeEventListener</span>(<span class="string">&#x27;resize&#x27;</span>, <span class="variable language_">this</span>.<span class="property">updateDimensions</span>);</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  updateDimensions = <span class="function">() =&gt;</span> &#123;</span><br><span class="line">    <span class="variable language_">this</span>.<span class="title function_">setState</span>(&#123; <span class="attr">width</span>: <span class="variable language_">window</span>.<span class="property">innerWidth</span>, <span class="attr">height</span>: <span class="variable language_">window</span>.<span class="property">innerHeight</span> &#125;);</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="title function_">render</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">span</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">        &#123;this.state.width&#125; x &#123;this.state.height&#125;</span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;/<span class="name">span</span>&gt;</span></span></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="101-setState-和-replaceState-方法之间的区别是什么？"><a href="#101-setState-和-replaceState-方法之间的区别是什么？" class="headerlink" title="101. setState() 和 replaceState() 方法之间的区别是什么？"></a>101. <code>setState()</code> 和 <code>replaceState()</code> 方法之间的区别是什么？</h2><p>当你使用 <code>setState()</code> 时，当前和之前的状态被合并。 <code>replaceState()</code> 抛出当前的状态，只用你提供的内容来替换它。通常 <code>setState()</code> 会被使用，除非你真的因为某些原因需要删除所有之前的键。你也可以在 <code>setState()</code> 中把状态设置为 <code>false</code>&#x2F;<code>null</code>，而不是使用 <code>replaceState()</code>。</p><h2 id="102-如何监听状态变化？"><a href="#102-如何监听状态变化？" class="headerlink" title="102. 如何监听状态变化？"></a>102. 如何监听状态变化？</h2><p>当状态发生变化时，<code>componentDidUpdate</code> 生命周期方法将被调用。你可以将提供的状态和 props 值与当前的状态和 props 进行比较，以确定是否有意义的变化。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">componentDidUpdate(object prevProps, object prevState)</span><br></pre></td></tr></table></figure><p><strong>注意：</strong> 以前的 ReactJS 版本也使用 <code>componentWillUpdate(object nextProps, object nextState)</code> 监听状态改变。在最新的版本中，它已被弃用。</p><h2 id="103-在-React-状态下，删除数组元素的推荐方法是什么？"><a href="#103-在-React-状态下，删除数组元素的推荐方法是什么？" class="headerlink" title="103. 在 React 状态下，删除数组元素的推荐方法是什么？"></a>103. 在 React 状态下，删除数组元素的推荐方法是什么？</h2><p>更好的方法是使用 <code>Array.prototype.filter()</code> 方法。</p><p>例如，让我们创建一个 <code>removeItem()</code> 方法来更新状态。</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="title function_">removeItem</span>(<span class="params">index</span>) &#123;</span><br><span class="line">  <span class="variable language_">this</span>.<span class="title function_">setState</span>(&#123;</span><br><span class="line">    <span class="attr">data</span>: <span class="variable language_">this</span>.<span class="property">state</span>.<span class="property">data</span>.<span class="title function_">filter</span>(<span class="function">(<span class="params">item, i</span>) =&gt;</span> i !== index)</span><br><span class="line">  &#125;)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="104-有没有可能在不渲染-HTML-的情况下使用-React-呢？"><a href="#104-有没有可能在不渲染-HTML-的情况下使用-React-呢？" class="headerlink" title="104. 有没有可能在不渲染 HTML 的情况下使用 React 呢？"></a>104. 有没有可能在不渲染 HTML 的情况下使用 React 呢？</h2><p>在最新版本（&gt;&#x3D;16.2）中可以实现。以下是可用选项。</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="title function_">render</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="literal">false</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="title function_">render</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><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="title function_">render</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> []</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="title function_">render</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">React.Fragment</span>&gt;</span><span class="tag">&lt;/<span class="name">React.Fragment</span>&gt;</span></span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="title function_">render</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="language-xml"><span class="tag">&lt;&gt;</span><span class="tag">&lt;/&gt;</span></span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>返回 <code>undefined</code> 是不行的。</p><h2 id="105-如何用-React-打印漂亮的-JSON？"><a href="#105-如何用-React-打印漂亮的-JSON？" class="headerlink" title="105. 如何用 React 打印漂亮的 JSON？"></a>105. 如何用 React 打印漂亮的 JSON？</h2><p>我们可以使用 <code>&lt;pre&gt;</code> 标签，这样可以保留 <code>JSON.stringify()</code> 的格式。</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> data = &#123; <span class="attr">name</span>: <span class="string">&#x27;John&#x27;</span>, <span class="attr">age</span>: <span class="number">42</span> &#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">User</span> <span class="keyword">extends</span> <span class="title class_ inherited__">React.Component</span> &#123;</span><br><span class="line">  <span class="title function_">render</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">pre</span>&gt;</span>&#123;JSON.stringify(data, null, 2)&#125;<span class="tag">&lt;/<span class="name">pre</span>&gt;</span></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="title class_">React</span>.<span class="title function_">render</span>(<span class="language-xml"><span class="tag">&lt;<span class="name">User</span> /&gt;</span></span>, <span class="variable language_">document</span>.<span class="title function_">getElementById</span>(<span class="string">&#x27;container&#x27;</span>));</span><br></pre></td></tr></table></figure><h2 id="106-为什么你不能在-React-中更新-props？"><a href="#106-为什么你不能在-React-中更新-props？" class="headerlink" title="106. 为什么你不能在 React 中更新 props？"></a>106. 为什么你不能在 React 中更新 props？</h2><p>React 的理念是，props 应该是<strong>不可变的</strong>和<strong>自上而下</strong>的。这意味着父组件可以向子组件发送任何 props 值，但子组件不能修改收到的 props。</p><h2 id="107-如何在页面加载时聚焦一个输入框？"><a href="#107-如何在页面加载时聚焦一个输入框？" class="headerlink" title="107. 如何在页面加载时聚焦一个输入框？"></a>107. 如何在页面加载时聚焦一个输入框？</h2><p>你可以通过为 <code>input</code> 元素创建 ref 并在 <code>componentDidMount()</code> 中使用它。</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">App</span> <span class="keyword">extends</span> <span class="title class_ inherited__">React.Component</span> &#123;</span><br><span class="line">  <span class="title function_">componentDidMount</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="keyword">if</span> (<span class="variable language_">this</span>.<span class="property">nameInput</span>) &#123;</span><br><span class="line">      <span class="variable language_">this</span>.<span class="property">nameInput</span>.<span class="title function_">focus</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="title function_">render</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">input</span> <span class="attr">defaultValue</span>=<span class="string">&#123;</span>&quot;<span class="attr">Won</span>&#x27;<span class="attr">t</span> <span class="attr">focus</span>&quot;&#125; /&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">ref</span>=<span class="string">&#123;input</span> =&gt;</span> (this.nameInput = input)&#125;</span></span><br><span class="line"><span class="language-xml">          defaultValue=&#123;&#x27;Will focus&#x27;&#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">div</span>&gt;</span></span></span><br><span class="line">    );</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="title class_">ReactDOM</span>.<span class="title function_">render</span>(<span class="language-xml"><span class="tag">&lt;<span class="name">App</span> /&gt;</span></span>, <span class="variable language_">document</span>.<span class="title function_">getElementById</span>(<span class="string">&#x27;app&#x27;</span>));</span><br></pre></td></tr></table></figure><h2 id="108-更新状态中的对象的方式有哪些？"><a href="#108-更新状态中的对象的方式有哪些？" class="headerlink" title="108. 更新状态中的对象的方式有哪些？"></a>108. 更新状态中的对象的方式有哪些？</h2><ol><li><strong>合并状态和对象后调用 <code>setState()</code>：</strong></li></ol><ul><li>使用 <code>Object.assign()</code> 创建对象的拷贝：</li></ul><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> user = <span class="title class_">Object</span>.<span class="title function_">assign</span>(&#123;&#125;, <span class="variable language_">this</span>.<span class="property">state</span>.<span class="property">user</span>, &#123; <span class="attr">age</span>: <span class="number">42</span> &#125;);</span><br><span class="line"><span class="variable language_">this</span>.<span class="title function_">setState</span>(&#123; user &#125;);</span><br></pre></td></tr></table></figure><ul><li>使用展开操作符：</li></ul><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> user = &#123; ...<span class="variable language_">this</span>.<span class="property">state</span>.<span class="property">user</span>, <span class="attr">age</span>: <span class="number">42</span> &#125;;</span><br><span class="line"><span class="variable language_">this</span>.<span class="title function_">setState</span>(&#123; user &#125;);</span><br></pre></td></tr></table></figure><ol start="2"><li><strong>调用 <code>setState()</code> 时传入函数：</strong></li></ol><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="variable language_">this</span>.<span class="title function_">setState</span>(<span class="function"><span class="params">prevState</span> =&gt;</span> (&#123;</span><br><span class="line">  <span class="attr">user</span>: &#123;</span><br><span class="line">    ...prevState.<span class="property">user</span>,</span><br><span class="line">    <span class="attr">age</span>: <span class="number">42</span>,</span><br><span class="line">  &#125;,</span><br><span class="line">&#125;));</span><br></pre></td></tr></table></figure><h2 id="109-我们如何在浏览器中查看运行时的-React-的版本？"><a href="#109-我们如何在浏览器中查看运行时的-React-的版本？" class="headerlink" title="109. 我们如何在浏览器中查看运行时的 React 的版本？"></a>109. 我们如何在浏览器中查看运行时的 React 的版本？</h2><p>你可以使用 <code>React.version</code> 来获取版本。</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="variable constant_">REACT_VERSION</span> = <span class="title class_">React</span>.<span class="property">version</span>;</span><br><span class="line"></span><br><span class="line"><span class="title class_">ReactDOM</span>.<span class="title function_">render</span>(</span><br><span class="line">  <span class="language-xml"><span class="tag">&lt;<span class="name">div</span>&gt;</span>&#123;`React version: $&#123;REACT_VERSION&#125;`&#125;<span class="tag">&lt;/<span class="name">div</span>&gt;</span></span>,</span><br><span class="line">  <span class="variable language_">document</span>.<span class="title function_">getElementById</span>(<span class="string">&#x27;app&#x27;</span>),</span><br><span class="line">);</span><br></pre></td></tr></table></figure><h2 id="110-在-create-react-app-中包含-polyfills-的方法是什么？"><a href="#110-在-create-react-app-中包含-polyfills-的方法是什么？" class="headerlink" title="110. 在 create-react-app 中包含 polyfills 的方法是什么？"></a>110. 在 <code>create-react-app</code> 中包含 polyfills 的方法是什么？</h2><p>有一些方法可以在 create-react-app 中包含 polyfills。</p><ol><li><strong>手动从 <code>core-js</code> 引入：</strong></li></ol><p>创建一个名为（类似）<code>polyfills.js</code> 的文件并将其导入根 <code>index.js</code> 文件。运行 <code>npm install core-js</code> 或 <code>yarn add core-js</code> 并导入你所需要的特定功能。</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="string">&#x27;core-js/fn/array/find&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> <span class="string">&#x27;core-js/fn/array/includes&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> <span class="string">&#x27;core-js/fn/number/is-nan&#x27;</span>;</span><br></pre></td></tr></table></figure><ol start="2"><li><strong>使用 Polyfill 服务：</strong></li></ol><p>使用 polyfill.io CDN，通过在 <code>index.html</code> 中添加这一行来检索自定义的、针对浏览器的 polyfills。</p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">script</span> <span class="attr">src</span>=<span class="string">&quot;https://cdn.polyfill.io/v2/polyfill.min.js?features=default,Array.prototype.includes&quot;</span>&gt;</span><span class="tag">&lt;/<span class="name">script</span>&gt;</span></span><br></pre></td></tr></table></figure><p>在上面的脚本中，我们必须明确请求 <code>Array.prototype.includes</code> 功能，因为它不包括在默认功能集中。</p><h2 id="111-如何在-create-react-app-中使用-https-而不是-http？"><a href="#111-如何在-create-react-app-中使用-https-而不是-http？" class="headerlink" title="111. 如何在 create-react-app 中使用 https 而不是 http？"></a>111. 如何在 create-react-app 中使用 https 而不是 http？</h2><p>你只需要是用 <code>HTTPS=true</code> 配置。你可以编辑 <code>package.json</code> scripts 部分：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><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;start&quot;</span><span class="punctuation">:</span> <span class="string">&quot;set HTTPS=true &amp;&amp; react-scripts start&quot;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p>或者运行 <code>set HTTPS=true &amp;&amp; npm start</code></p><h2 id="112-如何避免在-create-react-app-中使用相对路径导入？"><a href="#112-如何避免在-create-react-app-中使用相对路径导入？" class="headerlink" title="112. 如何避免在 create-react-app 中使用相对路径导入？"></a>112. 如何避免在 create-react-app 中使用相对路径导入？</h2><p>在项目里根目录创建一个叫 <code>.env</code> 的文件并写入导入的路径：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">NODE_PATH=src/app</span><br></pre></td></tr></table></figure><p>然后重启调试服务器。现在你应该能够在 <code>src/app</code> 目录下不使用相对路径导入任何东西。</p><h2 id="113-如何在-React-Router-中添加-Google-Analytics？"><a href="#113-如何在-React-Router-中添加-Google-Analytics？" class="headerlink" title="113. 如何在 React Router 中添加 Google Analytics？"></a>113. 如何在 React Router 中添加 Google Analytics？</h2><p>在 <code>history</code> 对象上添加一个监听器，以记录每个页面的浏览。</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">history.<span class="title function_">listen</span>(<span class="keyword">function</span>(<span class="params">location</span>) &#123;</span><br><span class="line">  <span class="variable language_">window</span>.<span class="title function_">ga</span>(<span class="string">&#x27;set&#x27;</span>, <span class="string">&#x27;page&#x27;</span>, location.<span class="property">pathname</span> + location.<span class="property">search</span>);</span><br><span class="line">  <span class="variable language_">window</span>.<span class="title function_">ga</span>(<span class="string">&#x27;send&#x27;</span>, <span class="string">&#x27;pageview&#x27;</span>, location.<span class="property">pathname</span> + location.<span class="property">search</span>);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><h2 id="114-如何每秒更新一次组件？"><a href="#114-如何每秒更新一次组件？" class="headerlink" title="114. 如何每秒更新一次组件？"></a>114. 如何每秒更新一次组件？</h2><p>你需要使用 <code>setInterval()</code> 来触发变化，但你也需要在组件卸载时清除计时器以防止错误和内存泄漏。</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="title function_">componentDidMount</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="variable language_">this</span>.<span class="property">interval</span> = <span class="built_in">setInterval</span>(<span class="function">() =&gt;</span> <span class="variable language_">this</span>.<span class="title function_">setState</span>(&#123; <span class="attr">time</span>: <span class="title class_">Date</span>.<span class="title function_">now</span>() &#125;), <span class="number">1000</span>)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="title function_">componentWillUnmount</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="built_in">clearInterval</span>(<span class="variable language_">this</span>.<span class="property">interval</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="115-如何在-React-中对内联样式使用-CSS-厂商前缀？"><a href="#115-如何在-React-中对内联样式使用-CSS-厂商前缀？" class="headerlink" title="115. 如何在 React 中对内联样式使用 CSS 厂商前缀？"></a>115. 如何在 React 中对内联样式使用 CSS 厂商前缀？</h2><p>React 不会自动应用 CSS 厂商前缀。你需要手动添加 CSS 厂商前缀。</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">&lt;div</span><br><span class="line">  style=&#123;&#123;</span><br><span class="line">    <span class="attr">transform</span>: <span class="string">&#x27;rotate(90deg)&#x27;</span>,</span><br><span class="line">    <span class="title class_">WebkitTransform</span>: <span class="string">&#x27;rotate(90deg)&#x27;</span>, <span class="comment">// 注意大写 &#x27;W&#x27;</span></span><br><span class="line">    <span class="attr">msTransform</span>: <span class="string">&#x27;rotate(90deg)&#x27;</span>, <span class="comment">// &#x27;ms&#x27; 是全小写</span></span><br><span class="line">  &#125;&#125;</span><br><span class="line">/&gt;</span><br></pre></td></tr></table></figure><h2 id="116-如何使用-React-和-ES6-导入和导出组件？"><a href="#116-如何使用-React-和-ES6-导入和导出组件？" class="headerlink" title="116. 如何使用 React 和 ES6 导入和导出组件？"></a>116. 如何使用 React 和 ES6 导入和导出组件？</h2><p>你应该使用默认值来导出组件</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">React</span> <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_">User</span> <span class="keyword">from</span> <span class="string">&#x27;user&#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">class</span> <span class="title class_">MyProfile</span> <span class="keyword">extends</span> <span class="title class_ inherited__">React.Component</span> &#123;</span><br><span class="line">  <span class="title function_">render</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">User</span> <span class="attr">type</span>=<span class="string">&quot;customer&quot;</span>&gt;</span>//...<span class="tag">&lt;/<span class="name">User</span>&gt;</span></span>;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>有了导出指定符，MyProfile 将成为成员并导出到这个模块，同样可以在其他组件中导入而不提及名称。</p><h2 id="117-为什么组件构造器只会被调用一次？"><a href="#117-为什么组件构造器只会被调用一次？" class="headerlink" title="117. 为什么组件构造器只会被调用一次？"></a>117. 为什么组件构造器只会被调用一次？</h2><p>React 的 reconciliation（协调） 算法假定，在没有任何相反信息的情况下，如果一个自定义组件在随后的渲染中出现在相同的地方，它就是之前的那个组件，所以 React 重用之前的实例而不是创建一个新的。</p><h2 id="118-如何在-React-中定义常量？"><a href="#118-如何在-React-中定义常量？" class="headerlink" title="118. 如何在 React 中定义常量？"></a>118. 如何在 React 中定义常量？</h2><p>你可以使用 ES7 的 <code>静态</code> 字段来定义常量。</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">MyComponent</span> <span class="keyword">extends</span> <span class="title class_ inherited__">React.Component</span> &#123;</span><br><span class="line">  <span class="keyword">static</span> <span class="variable constant_">DEFAULT_PAGINATION</span> = <span class="number">10</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>静态字段是类字段第三阶段提案的一部分。</p><h2 id="119-如何在-React-中以编程方式触发点击事件？"><a href="#119-如何在-React-中以编程方式触发点击事件？" class="headerlink" title="119. 如何在 React 中以编程方式触发点击事件？"></a>119. 如何在 React 中以编程方式触发点击事件？</h2><p>你可以使用 ref props 通过回调获得对底层 <code>HTMLInputElement</code> 对象的引用，将该引用存储为类属性，然后使用该引用从事件处理程序中使用 <code>HTMLElement.click</code> 方法触发点击。</p><p>这可以分两步进行。</p><ol><li>在 render 方法中创建 ref：</li></ol><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&lt;input ref=&#123;<span class="function"><span class="params">input</span> =&gt;</span> (<span class="variable language_">this</span>.<span class="property">inputElement</span> = input)&#125; /&gt;</span><br></pre></td></tr></table></figure><ol start="2"><li>在你的事件处理程序中应用点击事件。</li></ol><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="variable language_">this</span>.<span class="property">inputElement</span>.<span class="title function_">click</span>();</span><br></pre></td></tr></table></figure><h2 id="120-有可能在纯-React-中使用-async-await-吗？"><a href="#120-有可能在纯-React-中使用-async-await-吗？" class="headerlink" title="120. 有可能在纯 React 中使用 async&#x2F;await 吗？"></a>120. 有可能在纯 React 中使用 async&#x2F;await 吗？</h2><p>如果你想在 React 中使用 <code>async</code>&#x2F;<code>await</code>，你将需要 Babel 和 <a href="https://babeljs.io/docs/en/babel-plugin-transform-async-to-generator">transform-async-to-generator</a> 插件。React Native 已经包含了 Babel 和一系列的转换功能。</p><details class="folding-tag" green><summary> 121. React 的常见文件夹结构是什么？ </summary>              <div class='content'>              <p>React 项目文件结构有两种常见做法。</p><ol><li><strong>按特性或路由分组：</strong>*</li></ol><p>一种常见的项目结构方式是将 CSS、JS 和测试放在一起，按特性或路由分组。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line">common/</span><br><span class="line">├─ Avatar.js</span><br><span class="line">├─ Avatar.css</span><br><span class="line">├─ APIUtils.js</span><br><span class="line">└─ APIUtils.test.js</span><br><span class="line">feed/</span><br><span class="line">├─ index.js</span><br><span class="line">├─ Feed.js</span><br><span class="line">├─ Feed.css</span><br><span class="line">├─ FeedStory.js</span><br><span class="line">├─ FeedStory.test.js</span><br><span class="line">└─ FeedAPI.js</span><br><span class="line">profile/</span><br><span class="line">├─ index.js</span><br><span class="line">├─ Profile.js</span><br><span class="line">├─ ProfileHeader.js</span><br><span class="line">├─ ProfileHeader.css</span><br><span class="line">└─ ProfileAPI.js</span><br></pre></td></tr></table></figure><ol start="2"><li><strong>按文件类型分组：</strong></li></ol><p>另一种流行的项目结构方式是将类似的文件分组。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">api/</span><br><span class="line">├─ APIUtils.js</span><br><span class="line">├─ APIUtils.test.js</span><br><span class="line">├─ ProfileAPI.js</span><br><span class="line">└─ UserAPI.js</span><br><span class="line">components/</span><br><span class="line">├─ Avatar.js</span><br><span class="line">├─ Avatar.css</span><br><span class="line">├─ Feed.js</span><br><span class="line">├─ Feed.css</span><br><span class="line">├─ FeedStory.js</span><br><span class="line">├─ FeedStory.test.js</span><br><span class="line">├─ Profile.js</span><br><span class="line">├─ ProfileHeader.js</span><br><span class="line">└─ ProfileHeader.css</span><br></pre></td></tr></table></figure>              </div>            </details><details class="folding-tag" green><summary> 122. 有哪些流行的动画包？ </summary>              <div class='content'>              <p>React Transition Group 和 React Motion 是 React 生态系统中流行的动画包。</p>              </div>            </details><details class="folding-tag" green><summary> 123. 样式模块的好处是什么？ </summary>              <div class='content'>              <p>我们建议避免在组件中硬编码样式值。任何可能在不同的 UI 组件中使用的值都应该被提取到它们自己的模块中。</p><p>例如，这些样式可以被提取到一个单独的组件中。</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> colors = &#123;</span><br><span class="line">  white,</span><br><span class="line">  black,</span><br><span class="line">  blue,</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> space = [<span class="number">0</span>, <span class="number">8</span>, <span class="number">16</span>, <span class="number">32</span>, <span class="number">64</span>];</span><br></pre></td></tr></table></figure><p>然后在其他组件中单独导入。</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; space, colors &#125; <span class="keyword">from</span> <span class="string">&#x27;./styles&#x27;</span>;</span><br></pre></td></tr></table></figure>              </div>            </details><details class="folding-tag" green><summary> 124. 有哪些流行的 React 专用 linter？ </summary>              <div class='content'>              <p>ESLint 是一个很流行的 JavaScript linter。有一些插件可以用来分析特定的代码风格。其中最常见的 React 插件是一个名为 <code>eslint-plugin-react</code> 的 npm 包。默认情况下，它将检查一些最佳实践，其规则是检查从迭代器中的键到一整套道具类型的东西。</p><p>另一个流行的插件是 <code>eslint-plugin-jsx-a11y</code>，它将帮助修复可访问性方面的常见问题。由于 JSX 提供了与常规 HTML 稍有不同的语法，例如 <code>alt</code> 文本和 <code>tabindex</code> 的问题将不会被常规插件发现。</p>              </div>            </details><details class="folding-tag" green><summary> 125. 如何进行 AJAX 调用，应该在哪个组件的生命周期方法中进行 AJAX 调用？ </summary>              <div class='content'>              <p>你可以使用 AJAX 库，如 Axios、jQuery AJAX，以及浏览器内置的 <code>fetch</code>。你应该在 <code>componentDidMount()</code> 生命周期方法中获取数据。这样你就可以在获取数据时使用 <code>setState()</code> 来更新你的组件。</p><p>例如，从 API 获取的雇员名单并设置本地状态。</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">MyComponent</span> <span class="keyword">extends</span> <span class="title class_ inherited__">React.Component</span> &#123;</span><br><span class="line">  <span class="title function_">constructor</span>(<span class="params">props</span>) &#123;</span><br><span class="line">    <span class="variable language_">super</span>(props);</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">state</span> = &#123;</span><br><span class="line">      <span class="attr">employees</span>: [],</span><br><span class="line">      <span class="attr">error</span>: <span class="literal">null</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="title function_">componentDidMount</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="title function_">fetch</span>(<span class="string">&#x27;https://api.example.com/items&#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">      .<span class="title function_">then</span>(</span><br><span class="line">        <span class="function"><span class="params">result</span> =&gt;</span> &#123;</span><br><span class="line">          <span class="variable language_">this</span>.<span class="title function_">setState</span>(&#123;</span><br><span class="line">            <span class="attr">employees</span>: result.<span class="property">employees</span>,</span><br><span class="line">          &#125;);</span><br><span class="line">        &#125;,</span><br><span class="line">        <span class="function"><span class="params">error</span> =&gt;</span> &#123;</span><br><span class="line">          <span class="variable language_">this</span>.<span class="title function_">setState</span>(&#123; error &#125;);</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="title function_">render</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="keyword">const</span> &#123; error, employees &#125; = <span class="variable language_">this</span>.<span class="property">state</span>;</span><br><span class="line">    <span class="keyword">if</span> (error) &#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>Error: &#123;error.message&#125;<span class="tag">&lt;/<span class="name">div</span>&gt;</span></span>;</span><br><span class="line">    &#125; <span class="keyword">else</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">ul</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">          &#123;employees.map(employee =&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;employee.name&#125;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">              &#123;employee.name&#125;-&#123;employee.experience&#125;</span></span><br><span class="line"><span class="language-xml">            <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">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>              </div>            </details><details class="folding-tag" green><summary> 126. 什么是 render props？ </summary>              <div class='content'>              <p><strong>render props</strong> 是一种简单的技术，使用一个 props 在组件之间共享代码，其值是一个函数。下面的组件使用渲染 props，它返回一个 React 元素。</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&lt;<span class="title class_">DataProvider</span> render=&#123;<span class="function"><span class="params">data</span> =&gt;</span> <span class="language-xml"><span class="tag">&lt;<span class="name">h1</span>&gt;</span>&#123;`Hello $&#123;data.target&#125;`&#125;<span class="tag">&lt;/<span class="name">h1</span>&gt;</span></span>&#125; /&gt;</span><br></pre></td></tr></table></figure><p>React Router 和 DownShift 等库正在使用这种模式。</p>              </div>            </details>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/220ed3fe9370/</id>
    <link href="https://blog.zisheng.pro/posts/220ed3fe9370/"/>
    <published>2023-03-06T11:30:00.000Z</published>
    <summary>前 500 个 ReactJS 面试必知必会问题与答案，是紫升诚意翻译的前端 React 面试必备系列。</summary>
    <title>2023 React 面试题与答案</title>
    <updated>2026-09-18T08:15:28.632Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="紫升翻译计划" scheme="https://blog.zisheng.pro/categories/%E7%B4%AB%E5%8D%87%E7%BF%BB%E8%AF%91%E8%AE%A1%E5%88%92/"/>
    <category term="React 面试题" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/React-%E9%9D%A2%E8%AF%95%E9%A2%98/"/>
    <category term="React Router" scheme="https://blog.zisheng.pro/tags/React-Router/"/>
    <category term="React 面试题" scheme="https://blog.zisheng.pro/tags/React-%E9%9D%A2%E8%AF%95%E9%A2%98/"/>
    <category term="React 面试" scheme="https://blog.zisheng.pro/tags/React-%E9%9D%A2%E8%AF%95/"/>
    <category term="React 面试题与答案" scheme="https://blog.zisheng.pro/tags/React-%E9%9D%A2%E8%AF%95%E9%A2%98%E4%B8%8E%E7%AD%94%E6%A1%88/"/>
    <category term="前端面试题" scheme="https://blog.zisheng.pro/tags/%E5%89%8D%E7%AB%AF%E9%9D%A2%E8%AF%95%E9%A2%98/"/>
    <category term="React Router 面试题" scheme="https://blog.zisheng.pro/tags/React-Router-%E9%9D%A2%E8%AF%95%E9%A2%98/"/>
    <content>
      <![CDATA[<h1 id="1-React-Router-是什么？"><a href="#1-React-Router-是什么？" class="headerlink" title="1. React Router 是什么？"></a>1. React Router 是什么？</h1><p>React Router 是一个建立在 React 之上的强大的路由库，它可以帮助你快速添加新的屏幕和流程到你的应用程序，同时保持 URL 与页面上显示的内容同步。</p><h1 id="2-React-Router-与-history-库有什么不同？"><a href="#2-React-Router-与-history-库有什么不同？" class="headerlink" title="2. React Router 与 history 库有什么不同？"></a>2. React Router 与 history 库有什么不同？</h1><p>React Router 是一个围绕 <code>history</code> 库的包装器，它处理与浏览器的 <code>window.history</code> 和哈希历史记录的交互。 它还提供了内存历史，这对没有全局历史的环境很有用，如移动应用开发（React Native）和 Node 的单元测试。</p><h1 id="3-React-Router-v4-的-组件是什么？"><a href="#3-React-Router-v4-的-组件是什么？" class="headerlink" title="3. React Router v4 的 &lt;Router&gt; 组件是什么？"></a>3. React Router v4 的 <code>&lt;Router&gt;</code> 组件是什么？</h1><p>React Router v4 提供以下 3 个 <code>&lt;Router&gt;</code> 组件。</p><ol><li><code>&lt;BrowserRouter&gt;</code></li><li><code>&lt;HashRouter&gt;</code></li><li><code>&lt;MemoryRouter&gt;</code></li></ol><p>上述组件将创建<em>browser</em>、<em>hash</em>和<em>memory</em>历史实例。React Router v4 使与你的路由相关的 <code>history</code> 实例的属性和方法通过 <code>router</code> 对象中的上下文可用。</p><h1 id="4-history-的-push-和-replace-方法的用处是什么？"><a href="#4-history-的-push-和-replace-方法的用处是什么？" class="headerlink" title="4. history 的 push() 和 replace() 方法的用处是什么？"></a>4. <code>history</code> 的 <code>push()</code> 和 <code>replace()</code> 方法的用处是什么？</h1><p>一个 history 实例有两个方法用于导航。</p><ol><li><code>push()</code></li><li><code>replace()</code></li></ol><p>如果你把 history 看作是一个访问过的位置的数组， <code>push()</code> 将在数组中添加一个新的位置，<code>replace()</code> 将用新的位置替换数组中的当前位置。</p><h1 id="5-如何用-React-Router-v4-进行程序化导航？"><a href="#5-如何用-React-Router-v4-进行程序化导航？" class="headerlink" title="5. 如何用 React Router v4 进行程序化导航？"></a>5. 如何用 React Router v4 进行程序化导航？</h1><p>There are three different ways to achieve programmatic routing&#x2F;navigation within components.</p><ol><li><strong>Using the <code>withRouter()</code> higher-order function:</strong></li></ol><p>The <code>withRouter()</code> higher-order function will inject the history object as a prop of the component. This object provides <code>push()</code> and <code>replace()</code> methods to avoid the usage of context.</p><figure class="highlight jsx"><figcaption><span>| pure</span></figcaption><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; withRouter &#125; <span class="keyword">from</span> <span class="string">&#x27;react-router-dom&#x27;</span>; <span class="comment">// this also works with &#x27;react-router-native&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="title class_">Button</span> = <span class="title function_">withRouter</span>(<span class="function">(<span class="params">&#123; history &#125;</span>) =&gt;</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">type</span>=<span class="string">&quot;button&quot;</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">      history.push(&#x27;/new-location&#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;&#x27;Click Me!&#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></pre></td></tr></table></figure><ol start="2"><li><strong>Using <code>&lt;Route&gt;</code> component and render props pattern:</strong></li></ol><p>The <code>&lt;Route&gt;</code> component passes the same props as <code>withRouter()</code>, so you will be able to access the history methods through the history prop.</p><figure class="highlight jsx"><figcaption><span>| pure</span></figcaption><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">Route</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;react-router-dom&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">Button</span> = (<span class="params"></span>) =&gt; (</span><br><span class="line">  <span class="language-xml"><span class="tag">&lt;<span class="name">Route</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">    <span class="attr">render</span>=<span class="string">&#123;(&#123;</span> <span class="attr">history</span> &#125;) =&gt;</span> (</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">type</span>=<span class="string">&quot;button&quot;</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">          history.push(&#x27;/new-location&#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;&#x27;Click Me!&#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">    )&#125;</span></span><br><span class="line"><span class="language-xml">  /&gt;</span></span><br><span class="line">);</span><br></pre></td></tr></table></figure><ol start="3"><li><strong>Using context:</strong></li></ol><p>This option is not recommended and treated as unstable API.</p><figure class="highlight jsx"><figcaption><span>| pure</span></figcaption><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="title function_">Button</span> = (<span class="params">props, context</span>) =&gt; (</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">type</span>=<span class="string">&quot;button&quot;</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">      context.history.push(&#x27;/new-location&#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;&#x27;Click Me!&#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"></span><br><span class="line"><span class="title class_">Button</span>.<span class="property">contextTypes</span> = &#123;</span><br><span class="line">  <span class="attr">history</span>: <span class="title class_">React</span>.<span class="property">PropTypes</span>.<span class="title function_">shape</span>(&#123;</span><br><span class="line">    <span class="attr">push</span>: <span class="title class_">React</span>.<span class="property">PropTypes</span>.<span class="property">func</span>.<span class="property">isRequired</span>,</span><br><span class="line">  &#125;),</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><h1 id="6-如何在-React-Router-v4-中获取查询参数？"><a href="#6-如何在-React-Router-v4-中获取查询参数？" class="headerlink" title="6. 如何在 React Router v4 中获取查询参数？"></a>6. 如何在 React Router v4 中获取查询参数？</h1><p>The ability to parse query strings was taken out of React Router v4 because there have been user requests over the years to support different implementation. So the decision has been given to users to choose the implementation they like. The recommended approach is to use query strings library.</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> queryString = <span class="built_in">require</span>(<span class="string">&#x27;query-string&#x27;</span>);</span><br><span class="line"><span class="keyword">const</span> parsed = queryString.<span class="title function_">parse</span>(props.<span class="property">location</span>.<span class="property">search</span>);</span><br></pre></td></tr></table></figure><p>You can also use <code>URLSearchParams</code> if you want something native:</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> params = <span class="keyword">new</span> <span class="title class_">URLSearchParams</span>(props.<span class="property">location</span>.<span class="property">search</span>);</span><br><span class="line"><span class="keyword">const</span> foo = params.<span class="title function_">get</span>(<span class="string">&#x27;name&#x27;</span>);</span><br></pre></td></tr></table></figure><p>You should use a <em>polyfill</em> for IE11.</p><h1 id="7-为什么你得到-Router-may-have-only-one-child-element-的警告？"><a href="#7-为什么你得到-Router-may-have-only-one-child-element-的警告？" class="headerlink" title="7. 为什么你得到 Router may have only one child element 的警告？"></a>7. 为什么你得到 <code>Router may have only one child element</code> 的警告？</h1><p>You have to wrap your Route’s in a <code>&lt;Switch&gt;</code> block because <code>&lt;Switch&gt;</code> is unique in that it renders a route exclusively.</p><p>At first you need to add <code>Switch</code> to your imports:</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">Switch</span>, <span class="title class_">Router</span>, <span class="title class_">Route</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;react-router&#x27;</span>;</span><br></pre></td></tr></table></figure><p>Then define the routes within <code>&lt;Switch&gt;</code> block:</p><figure class="highlight jsx"><figcaption><span>| pure</span></figcaption><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">&lt;<span class="title class_">Router</span>&gt;</span><br><span class="line">  <span class="language-xml"><span class="tag">&lt;<span class="name">Switch</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;<span class="name">Route</span> &#123;/* <span class="attr">...</span> */&#125; /&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;<span class="name">Route</span> &#123;/* <span class="attr">...</span> */&#125; /&gt;</span></span></span><br><span class="line"><span class="language-xml">  <span class="tag">&lt;/<span class="name">Switch</span>&gt;</span></span></span><br><span class="line">&lt;/<span class="title class_">Router</span>&gt;</span><br></pre></td></tr></table></figure><h1 id="8-如何在-React-Router-v4-中向-history-push-方法传递参数？"><a href="#8-如何在-React-Router-v4-中向-history-push-方法传递参数？" class="headerlink" title="8. 如何在 React Router v4 中向 history.push 方法传递参数？"></a>8. 如何在 React Router v4 中向 <code>history.push</code> 方法传递参数？</h1><p>While navigating you can pass props to the <code>history</code> object:</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="variable language_">this</span>.<span class="property">props</span>.<span class="property">history</span>.<span class="title function_">push</span>(&#123;</span><br><span class="line">  <span class="attr">pathname</span>: <span class="string">&#x27;/template&#x27;</span>,</span><br><span class="line">  <span class="attr">search</span>: <span class="string">&#x27;?name=sudheer&#x27;</span>,</span><br><span class="line">  <span class="attr">state</span>: &#123; <span class="attr">detail</span>: response.<span class="property">data</span> &#125;,</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p>The <code>search</code> property is used to pass query params in <code>push()</code> method.</p><h1 id="9-如何实现-default-或-NotFound-页面？"><a href="#9-如何实现-default-或-NotFound-页面？" class="headerlink" title="9. 如何实现 default 或 NotFound 页面？"></a>9. 如何实现 <em>default</em> 或 <em>NotFound</em> 页面？</h1><p>A <code>&lt;Switch&gt;</code> renders the first child <code>&lt;Route&gt;</code> that matches. A <code>&lt;Route&gt;</code> with no path always matches. So you just need to simply drop path attribute as below</p><figure class="highlight jsx"><figcaption><span>| pure</span></figcaption><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">&lt;<span class="title class_">Switch</span>&gt;</span><br><span class="line">  <span class="language-xml"><span class="tag">&lt;<span class="name">Route</span> <span class="attr">exact</span> <span class="attr">path</span>=<span class="string">&quot;/&quot;</span> <span class="attr">component</span>=<span class="string">&#123;Home&#125;</span> /&gt;</span></span></span><br><span class="line">  <span class="language-xml"><span class="tag">&lt;<span class="name">Route</span> <span class="attr">path</span>=<span class="string">&quot;/user&quot;</span> <span class="attr">component</span>=<span class="string">&#123;User&#125;</span> /&gt;</span></span></span><br><span class="line">  <span class="language-xml"><span class="tag">&lt;<span class="name">Route</span> <span class="attr">component</span>=<span class="string">&#123;NotFound&#125;</span> /&gt;</span></span></span><br><span class="line">&lt;/<span class="title class_">Switch</span>&gt;</span><br></pre></td></tr></table></figure><h1 id="10-如何在-React-Router-v4-上获得历史记录？"><a href="#10-如何在-React-Router-v4-上获得历史记录？" class="headerlink" title="10. 如何在 React Router v4 上获得历史记录？"></a>10. 如何在 React Router v4 上获得历史记录？</h1><p>Below are the list of steps to get history object on React Router v4,</p><ol><li>Create a module that exports a <code>history</code> object and import this module across the project.</li></ol><p>For example, create <code>history.js</code> file:</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; createBrowserHistory &#125; <span class="keyword">from</span> <span class="string">&#x27;history&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="title function_">createBrowserHistory</span>(&#123;</span><br><span class="line">  <span class="comment">/* pass a configuration object here if needed */</span></span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><ol start="2"><li>You should use the <code>&lt;Router&gt;</code> component instead of built-in routers. Imported the above <code>history.js</code> inside <code>index.js</code> file:</li></ol><figure class="highlight jsx"><figcaption><span>| pure</span></figcaption><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">Router</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;react-router-dom&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> history <span class="keyword">from</span> <span class="string">&#x27;./history&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> <span class="title class_">App</span> <span class="keyword">from</span> <span class="string">&#x27;./App&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="title class_">ReactDOM</span>.<span class="title function_">render</span>(</span><br><span class="line">  <span class="language-xml"><span class="tag">&lt;<span class="name">Router</span> <span class="attr">history</span>=<span class="string">&#123;history&#125;</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">Router</span>&gt;</span></span>,</span><br><span class="line">  holder,</span><br><span class="line">);</span><br></pre></td></tr></table></figure><ol start="3"><li>You can also use push method of <code>history</code> object similar to built-in history object:</li></ol><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// some-other-file.js</span></span><br><span class="line"><span class="keyword">import</span> history <span class="keyword">from</span> <span class="string">&#x27;./history&#x27;</span>;</span><br><span class="line"></span><br><span class="line">history.<span class="title function_">push</span>(<span class="string">&#x27;/go-here&#x27;</span>);</span><br></pre></td></tr></table></figure><h1 id="11-如何在登录后执行自动重定向？"><a href="#11-如何在登录后执行自动重定向？" class="headerlink" title="11. 如何在登录后执行自动重定向？"></a>11. 如何在登录后执行自动重定向？</h1><p>The <code>react-router</code> package provides <code>&lt;Redirect&gt;</code> component in React Router. Rendering a <code>&lt;Redirect&gt;</code> will navigate to a new location. Like server-side redirects, the new location will override the current location in the history stack.</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">React</span>, &#123; <span class="title class_">Component</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;react&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">Redirect</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;react-router&#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">class</span> <span class="title class_">LoginComponent</span> <span class="keyword">extends</span> <span class="title class_ inherited__">Component</span> &#123;</span><br><span class="line">  <span class="title function_">render</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="keyword">if</span> (<span class="variable language_">this</span>.<span class="property">state</span>.<span class="property">isLoggedIn</span> === <span class="literal">true</span>) &#123;</span><br><span class="line">      <span class="keyword">return</span> <span class="language-xml"><span class="tag">&lt;<span class="name">Redirect</span> <span class="attr">to</span>=<span class="string">&quot;/your/redirect/page&quot;</span> /&gt;</span></span>;</span><br><span class="line">    &#125; <span class="keyword">else</span> &#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;&#x27;Login Please&#x27;&#125;<span class="tag">&lt;/<span class="name">div</span>&gt;</span></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>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/3f5f64fb94e2/</id>
    <link href="https://blog.zisheng.pro/posts/3f5f64fb94e2/"/>
    <published>2023-03-06T11:30:00.000Z</published>
    <summary>前 500 个 ReactJS 面试必知必会问题与答案，这是紫升诚意翻译的前端 React 面试必备系列的 React Router 面试篇。</summary>
    <title>2023 React Router 面试题与答案</title>
    <updated>2026-09-18T08:15:28.632Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="React" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/React/"/>
    <category term="React" scheme="https://blog.zisheng.pro/tags/React/"/>
    <category term="性能优化" scheme="https://blog.zisheng.pro/tags/%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/"/>
    <category term="React.memo" scheme="https://blog.zisheng.pro/tags/React-memo/"/>
    <category term="useMemo" scheme="https://blog.zisheng.pro/tags/useMemo/"/>
    <content>
      <![CDATA[<h2 id="React-Profiler"><a href="#React-Profiler" class="headerlink" title="React Profiler"></a>React Profiler</h2><p>React 16.5 添加了对开发者工具的 Profiler 插件的支持。该插件使用了 React 的 Profiler API 来收集所有组件渲染的耗时，目的是为了找出 React 应用程序的性能瓶颈。</p><p>这个“Profiler”面板初始为空，你可以点击 record 按钮开始分析：</p><p><img src="https://zh-hans.reactjs.org/static/bae8d10e17f06eeb8c512c91c0153cff/ad997/start-profiling.png" alt="紫升"></p><p>当你开始记录之后，开发者工具将在每次应用程序渲染时自动收集性能数据。 你可以和平常一样使用你的应用程序， 当你完成分析之后，请点击“Stop”按钮。</p><p><img src="https://zh-hans.reactjs.org/static/45619de03bed468869f7a0878f220586/ad997/stop-profiling.png" alt="紫升"></p><h2 id="优化法则"><a href="#优化法则" class="headerlink" title="优化法则"></a>优化法则</h2><ul><li>法则一：动静分离，将变的部分与不变的部分分离。</li><li>法则二：缓存，复杂计算和有昂贵消耗的组件 memo 化，比如 React 的 useMemo、useCallback，Redux 的 useSelector。</li></ul><p>下面我们将根据这些法则结合实际开发中案例进行分析。</p><h2 id="案例一"><a href="#案例一" class="headerlink" title="案例一"></a>案例一</h2><p>我们都知道，当 state 更新后，整个组件以及它的子组件都会重新更新，尽管子组件没有依赖任何 state，下面的例子就能很好地展示这个问题。</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><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">function</span> <span class="title function_">Random</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>&#123;Math.random()&#125;<span class="tag">&lt;/<span class="name">h1</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_">Count</span>(<span class="params">props: &#123; count: <span class="built_in">number</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">h2</span>&gt;</span>&#123;props.count&#125;<span class="tag">&lt;/<span class="name">h2</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_">App</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 class="keyword">const</span> <span class="title function_">onAdd</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">    <span class="title function_">setCount</span>(count + <span class="number">1</span>)</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">const</span> <span class="title function_">onMinus</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">    <span class="title function_">setCount</span>(count - <span class="number">1</span>)</span><br><span class="line">  &#125;</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">className</span>=<span class="string">&quot;App&quot;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">Random</span> /&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">Count</span> <span class="attr">count</span>=<span class="string">&#123;count&#125;/</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;onMinus&#125;</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">button</span> <span class="attr">onClick</span>=<span class="string">&#123;onAdd&#125;</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">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 class_">App</span>;</span><br></pre></td></tr></table></figure><p>在以上示例中，我们声明了一个显示随机数的组件和一个依赖 count 状态的数字显示组件，使用 React Profiler 工具分析如下：</p><p><img src="https://s2.loli.net/2022/09/25/zKn9HslamJi6Ro5.png" alt="紫升"></p><p>就像预期的那样，App、Count 和 Random 组件都更新了，从工具中我们可以看到它们更新的原因：</p><ul><li>App：Hook 1 changed.</li><li>Count：Props changed（count）.</li><li>Random：The parent component rendered.</li></ul><p>我们可以看到这里边除了 Count 是因为 count 更新之外，其他组件的更新都是被无辜牵连的。根据法则一，我们可以尝试将 Count 组件和它依赖的状态封装起来。</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><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">function</span> <span class="title function_">Random</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>&#123;Math.random()&#125;<span class="tag">&lt;/<span class="name">h1</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_">Count</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 class="keyword">const</span> <span class="title function_">onAdd</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">    <span class="title function_">setCount</span>(count + <span class="number">1</span>)</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">const</span> <span class="title function_">onMinus</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">    <span class="title function_">setCount</span>(count - <span class="number">1</span>)</span><br><span class="line">  &#125;</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">h2</span>&gt;</span>&#123;count&#125;<span class="tag">&lt;/<span class="name">h2</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;onMinus&#125;</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">button</span> <span class="attr">onClick</span>=<span class="string">&#123;onAdd&#125;</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;/&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_">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> <span class="attr">className</span>=<span class="string">&quot;App&quot;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">Random</span> /&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">Count</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><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="title class_">App</span>;</span><br></pre></td></tr></table></figure><p>从下图 Profiler 分析可以看到当 count 更新时，只有 Count 组件更新了。</p><p><img src="https://s2.loli.net/2022/09/25/YheAM9ZuviaJDmo.png" alt="紫升"></p><p>上面的示例太过理想化，大部分时候，负责更新状态的组件和负责展示状态的组件可能并不在一起，也就没办法抽离动态组件。比如我们将示例中的组件位置做下调整：</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><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">return</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;App&quot;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">Count</span> <span class="attr">count</span>=<span class="string">&#123;count&#125;/</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">Random</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;onMinus&#125;</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">button</span> <span class="attr">onClick</span>=<span class="string">&#123;onAdd&#125;</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">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.children 属性来实现，原理上是一种依靠缓存的 bailout 优化方案。</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">React</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">interface</span> <span class="title class_">CountProps</span> &#123;</span><br><span class="line">  children?: <span class="variable constant_">JSX</span>.<span class="property">Element</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_">Random</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>&#123;Math.random()&#125;<span class="tag">&lt;/<span class="name">h1</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_">Count</span>(<span class="params">props: CountProps</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 class="keyword">const</span> <span class="title function_">onAdd</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">    <span class="title function_">setCount</span>(count + <span class="number">1</span>)</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">const</span> <span class="title function_">onMinus</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">    <span class="title function_">setCount</span>(count - <span class="number">1</span>)</span><br><span class="line">  &#125;</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">h2</span>&gt;</span>&#123;count&#125;<span class="tag">&lt;/<span class="name">h2</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      &#123;props.children&#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;onMinus&#125;</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">button</span> <span class="attr">onClick</span>=<span class="string">&#123;onAdd&#125;</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;/&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_">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> <span class="attr">className</span>=<span class="string">&quot;App&quot;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">Count</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">Random</span> /&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;/<span class="name">Count</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><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="title class_">App</span>;</span><br></pre></td></tr></table></figure><h1 id="bailout"><a href="#bailout" class="headerlink" title="bailout"></a>bailout</h1><p>想要解释为什么案例一中使用 props.children 可以解决重复渲染，就要了解一下 React bailout 机制。</p><p>bailout（bail out of re-rendering）可以简单理解为是否重新渲染。</p><p>要触发 bailout 函数，需要同时满足以下条件：</p><ol><li>oldProps &#x3D;&#x3D;&#x3D; newProps 并且 Context 没有改变</li><li>!includesSomeLane(renderLanes, updateLanes)</li></ol><p>当前 fiber 上是否存在更新，如果存在那么更新的优先级是否和本次整棵 Fiber 树调度的优先级一致？如果一致代表该组件上存在更新，需要走 render 逻辑。</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// beginWork 函数</span></span><br><span class="line"><span class="keyword">if</span> (current !== <span class="literal">null</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> oldProps = current.<span class="property">memoizedProps</span>;</span><br><span class="line">  <span class="keyword">const</span> newProps = workInProgress.<span class="property">pendingProps</span>;</span><br><span class="line">  <span class="keyword">if</span> (</span><br><span class="line">    oldProps !== newProps ||</span><br><span class="line">    <span class="title function_">hasLegacyContextChanged</span>()</span><br><span class="line">  ) &#123;</span><br><span class="line">    didReceiveUpdate = <span class="literal">true</span>;</span><br><span class="line">  &#125; <span class="keyword">else</span> <span class="keyword">if</span> (!<span class="title function_">includesSomeLane</span>(renderLanes, updateLanes)) &#123;</span><br><span class="line">    didReceiveUpdate = <span class="literal">false</span>;</span><br><span class="line">    <span class="comment">// 命中 bailoutOnAlreadyFinishedWork</span></span><br><span class="line">    <span class="keyword">return</span> <span class="title function_">bailoutOnAlreadyFinishedWork</span>(current, workInProgress, renderLanes);</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>bailout 函数逻辑大致是：尽量复用 fiber，不进行 render。fiber 复用，判断 fiber 的子树是否有 work。有，返回 child，继续遍历子树。无，返回 null，跳过子树。</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// bailoutOnAlreadyFinishedWork 函数</span></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">bailoutOnAlreadyFinishedWork</span>(<span class="params"></span></span><br><span class="line"><span class="params">  current: Fiber | <span class="literal">null</span>,</span></span><br><span class="line"><span class="params">  workInProgress: Fiber,</span></span><br><span class="line"><span class="params">  renderLanes: Lanes,</span></span><br><span class="line"><span class="params"></span>): <span class="title class_">Fiber</span> | <span class="literal">null</span> &#123;</span><br><span class="line">  <span class="keyword">if</span> (current !== <span class="literal">null</span>) &#123;</span><br><span class="line">    <span class="comment">// 重用以前的 context 依赖关系</span></span><br><span class="line">    workInProgress.<span class="property">dependencies</span> = current.<span class="property">dependencies</span>;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 检测子树(childLanes)是否有 work</span></span><br><span class="line">  <span class="keyword">if</span> (!<span class="title function_">includesSomeLane</span>(renderLanes, workInProgress.<span class="property">childLanes</span>)) &#123;</span><br><span class="line">    <span class="comment">//无，跳过子树</span></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">// 虽然 fiber 没有 work，但是它的子树有，克隆子树的 fiber 然后继续检查</span></span><br><span class="line">  <span class="title function_">cloneChildFibers</span>(current, workInProgress);</span><br><span class="line">  <span class="keyword">return</span> workInProgress.<span class="property">child</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>案例一中，由于 children Random 组件的 props 没有发生改变，并且 lanes 也不在 renderLanes 上 ，Diff 组件命中 bailoutOnAlreadyFinishedWork。</p><p>lane 是 React 调度模型中的优先级模型。想象一下不同的赛车疾驰在不同的赛道。内圈的赛道总长度更短，外圈更长。某几个临近的赛道的长度可以看作差不多长。</p><p>lane 模型借鉴了同样的概念，使用 31 位的二进制表示 31 条赛道，位数越小的赛道优先级越高，某些相邻的赛道拥有相同优先级。</p><h2 id="案例二"><a href="#案例二" class="headerlink" title="案例二"></a>案例二</h2><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">React</span>, &#123; useState, useContext &#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> countContext = <span class="title class_">React</span>.<span class="property">createContext</span>&lt;<span class="built_in">number</span>&gt;(<span class="number">0</span>);</span><br><span class="line"><span class="keyword">const</span> <span class="title class_">CountProvider</span> = countContext.<span class="property">Provider</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">Random</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>&#123;Math.random()&#125;<span class="tag">&lt;/<span class="name">h1</span>&gt;</span></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_">CountProps</span> &#123;</span><br><span class="line">  children?: <span class="variable constant_">JSX</span>.<span class="property">Element</span></span><br><span class="line">  <span class="attr">onAdd</span>: <span class="function">() =&gt;</span> <span class="built_in">void</span></span><br><span class="line">  <span class="attr">onMinus</span>: <span class="function">() =&gt;</span> <span class="built_in">void</span></span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">function</span> <span class="title function_">Count</span>(<span class="params">props: CountProps</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> count = <span class="title function_">useContext</span>(countContext);</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">h2</span>&gt;</span>&#123;count&#125;<span class="tag">&lt;/<span class="name">h2</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;props?.onMinus&#125;</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">button</span> <span class="attr">onClick</span>=<span class="string">&#123;props?.onAdd&#125;</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;/&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_">App</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 class="keyword">const</span> <span class="title function_">onAdd</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">    <span class="title function_">setCount</span>(count + <span class="number">1</span>)</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">const</span> <span class="title function_">onMinus</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">    <span class="title function_">setCount</span>(count - <span class="number">1</span>)</span><br><span class="line">  &#125;</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">className</span>=<span class="string">&quot;App&quot;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">CountProvider</span> <span class="attr">value</span>=<span class="string">&#123;count&#125;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">Count</span> <span class="attr">onAdd</span>=<span class="string">&#123;onAdd&#125;</span> <span class="attr">onMinus</span>=<span class="string">&#123;onMinus&#125;</span> /&gt;</span></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">Random</span> /&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;/<span class="name">CountProvider</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><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="title class_">App</span>;</span><br></pre></td></tr></table></figure><p>该案例中我们引入了 Context，当 context 的 value 改变时，</p><h2 id="参考链接"><a href="#参考链接" class="headerlink" title="参考链接"></a>参考链接</h2><ul><li><a href="https://zh-hans.reactjs.org/blog/2018/09/10/introducing-the-react-profiler.html">React Profiler 介绍</a></li><li><a href="https://blog.wuct.me/react-internal-reconciliation-bailout-587695eb05a8?gi=c4bc84e7c1df">深入 React Reconciliation Bailout 機制</a></li><li><a href="https://jishuin.proginn.com/p/763bfbd32552">我在大厂写React学到了什么？性能优化篇</a></li><li><a href="https://juejin.cn/post/7090466770774917150">react 性能优化 - children如何避免不必要的渲染</a></li><li><a href="https://juejin.cn/post/7092593300233781285#heading-4">React 性能优化，你需要知道的一切</a></li></ul>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/8132b809f088/</id>
    <link href="https://blog.zisheng.pro/posts/8132b809f088/"/>
    <published>2023-03-05T09:22:41.000Z</published>
    <summary>React 组件性能优化的核心是减少渲染真实 DOM 节点的频率，减少 VirtualDOM 比对的频率。</summary>
    <title>React 性能优化实践</title>
    <updated>2026-09-18T08:15:28.632Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="组件化" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/%E7%BB%84%E4%BB%B6%E5%8C%96/"/>
    <category term="React Native" scheme="https://blog.zisheng.pro/tags/React-Native/"/>
    <category term="前端" scheme="https://blog.zisheng.pro/tags/%E5%89%8D%E7%AB%AF/"/>
    <category term="组件化" scheme="https://blog.zisheng.pro/tags/%E7%BB%84%E4%BB%B6%E5%8C%96/"/>
    <category term="组件库" scheme="https://blog.zisheng.pro/tags/%E7%BB%84%E4%BB%B6%E5%BA%93/"/>
    <content>
      <![CDATA[<blockquote><p>⚠️本文为掘金社区首发签约文章，未获授权禁止转载</p></blockquote><blockquote><p>大家好，我是<a href="https://blog.zisheng.pro/">紫升🎋</a>，一只住在杭城的木系前端🧚🏻‍♀️，如果你喜欢我的文章📚，可以通过点赞帮我聚集灵力⭐️。</p></blockquote><h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>在 <a href="https://juejin.cn/post/6983854006124675108">《每个前端都应该拥有自己的组件库,就像每个夏天都有西瓜🍉》</a> 一文中，紫升带领小黑从零搭建了一个组件库项目，完成了项目结构、构建、测试、文档等基础工程化工作并完成了第一个组件 Icon。本期延续上期的组件工程化的主题，夏日炎热，点上一杯杨枝甘露，和紫升赴一场 Button 开发之约吧。赴约后，你将会收获以下的内容：</p><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/58285f3cce144972b2ccb88e11efd73f~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><blockquote><p>PS：配合<a href="https://github.com/youngjuning/vant-react-native">仓库</a>和<a href="https://vant-react-native.js.org/">组件库文档</a>阅读本文效果更佳喲！</p></blockquote><h2 id="Button-与设计心理学"><a href="#Button-与设计心理学" class="headerlink" title="Button 与设计心理学"></a>Button 与设计心理学</h2><p>作为前端工程师，入行至今接触最多的就是设计师了。耳濡目染下虽说没学会什么设计工具，但是对设计与人的心理有了一定认识。</p><p>紫升认为任何事物都不可能凭空出现，自有其传承。使用广泛的基础界面元素 Button 也不例外，我们生活中就有随处可见的按钮。举个栗子🌰，每天上班下班必然要按的电梯按钮、手机音量按钮、小米 9 鸡肋的小爱同学唤起按钮。要搞清楚为什么需要按钮，我们有必要探究下生活中这些按钮的作用。</p><h3 id="点一下按钮的快感"><a href="#点一下按钮的快感" class="headerlink" title="点一下按钮的快感"></a>点一下按钮的快感</h3><p>想象一下把键盘按键换成触摸屏，你最在乎的一定是完美还原物理键的敲击感，像紫升用手机虚拟键盘就喜欢设置按键震动和音效。通过打击（点击）获得快感是较为普遍的人性。按钮在按下、松开时有丰富的质感和交互感，完美满足了人们点一下的快感。</p><p><img src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/542a6bbb880e4594ae384ee0bd1e511d~tplv-k3u1fbpfcp-watermark.image" alt="紫升"></p><h3 id="现实的实用性"><a href="#现实的实用性" class="headerlink" title="现实的实用性"></a>现实的实用性</h3><p>从 BB 机到诺基亚再到如今的智能机，实体按钮削减到只剩下音量键和开关机键。按键虽然光秃秃没有任何标识，但我们就是知道它的功能。试想一下没有这个来自远古时代的开关键，你手里的手机就是一块板砖。</p><h3 id="疯狂暗示用户，达到不可告人目的"><a href="#疯狂暗示用户，达到不可告人目的" class="headerlink" title="疯狂暗示用户，达到不可告人目的"></a>疯狂暗示用户，达到不可告人目的</h3><p>小米 9 单独唤起小爱同学的按键经常会被误按，之前我还不理解这么蠢的设计的目的。在简单研究了点设计心理学我明白了。小爱的设计者为了 产品日活和 AI 训练就是故意这个设计的。</p><p>小米 10 虽然移除了单独的唤起键，却把原来的电源键改成了一键多用。每次想要重启手机还得先唤起一下小爱同学。不得不说，小爱同学小米亲女儿。</p><p><img src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/633381b8c2c342ed94bf024dbc19cf76~tplv-k3u1fbpfcp-watermark.image" alt="紫升"></p><p>吐槽归吐槽，小米这个按钮确实起到了培养用户习惯的任务。当用户知悉某个按钮能指向某个操作，或者获取某类信息后，长此以往用户就会形成使用习惯。如果某操作能够为用户和厂商持续带来价值，那就可以让按钮的位置更加醒目，持续培养用户点击习惯。</p><h3 id="指引用户操作"><a href="#指引用户操作" class="headerlink" title="指引用户操作"></a>指引用户操作</h3><p>这个在 Web 开发中是最常见的使用场景，每个可交互页面上都有这类按钮的出现，用来指引用户下一步该怎么做。比如表单的提交和重置。</p><p><img src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d2c2a8abbe84471f96a67e18196c53fb~tplv-k3u1fbpfcp-watermark.image" alt="紫升"></p><p>虽然按钮也常作为表单元素，但是区别于其他表单元素，按钮因其天然地自说明性，不需要 Label 对其进行辅助说明，啰嗦这么多，掘友们应该在看到一个按钮时，应该也会有从设计上品鉴的意识了，欢迎将对下图的品鉴在评论区告诉紫升。</p><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/88c6754b28984fc3866319c35790fc03~tplv-k3u1fbpfcp-watermark.image" alt="紫升"></p><h2 id="组件主题化"><a href="#组件主题化" class="headerlink" title="组件主题化"></a>组件主题化</h2><p>在开始开发具体组件之前，我们必须先约定好组件主题化的规范。之前 antd-mobile-rn 就因为设计问题，中途花费大力气重构。几乎所有的组件库都会将色彩、布局这些以 css 变量的形式提供给使用者和开发者为，React Native 不同的是样式基于 CSS in JS，不过道理相通，参照 vant 的<a href="https://github.com/youzan/vant/blob/dev/src/style/var.less">设计资源</a>，我们抽出了一套 JavaScript 常量：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// packages/themes</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">interface</span> <span class="title class_">Theme</span> &#123;</span><br><span class="line">  <span class="string">&#x27;animation-duration-base&#x27;</span>: <span class="built_in">string</span>;</span><br><span class="line">  <span class="string">&#x27;animation-duration-fast&#x27;</span>: <span class="built_in">string</span>;</span><br><span class="line">  <span class="string">&#x27;animation-timing-function-enter&#x27;</span>: <span class="built_in">string</span>;</span><br><span class="line">  <span class="string">&#x27;animation-timing-function-leave&#x27;</span>: <span class="built_in">string</span>;</span><br><span class="line">  <span class="string">&#x27;font-size-xs&#x27;</span>: <span class="built_in">number</span>;</span><br><span class="line">  <span class="string">&#x27;font-size-sm&#x27;</span>: <span class="built_in">number</span>;</span><br><span class="line">  <span class="string">&#x27;font-size-md&#x27;</span>: <span class="built_in">number</span>;</span><br><span class="line">  <span class="string">&#x27;font-size-lg&#x27;</span>: <span class="built_in">number</span>;</span><br><span class="line">  <span class="string">&#x27;font-weight-bold&#x27;</span>: <span class="built_in">number</span>;</span><br><span class="line">  <span class="comment">// 变量过多，这里仅展示部分变量</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>有了这些 JS 常量，我们就可以设计主题系统。基于 CSS in JS 的主题化设计一般是基于 React Context 实现，需要提供 ThemeProvider 传入主题上下文，ThemeConsumer、WithTheme（高阶类组件）、withTheme（高阶函数组件） 或 useTheme（React Hooks）作为消费者获取上下文。自己实现也不难，不过更文任务比较紧急，我们先基于 <a href="https://github.com/cssinjs/theming">cssinjs&#x2F;theming</a> 实现功能，后期有需要再回来造轮子也不迟。下面👇就是我们基于 theming 的 <code>createTheming</code> 函数创建自定义主题上下文。</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; createTheming &#125; <span class="keyword">from</span> <span class="string">&#x27;theming&#x27;</span>;</span><br><span class="line"><span class="keyword">const</span> context = <span class="title class_">React</span>.<span class="title function_">createContext</span>(defaultTheme);</span><br><span class="line"><span class="keyword">const</span> theming = <span class="title function_">createTheming</span>(context);</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> &#123; <span class="title class_">ThemeProvider</span>, withTheme, useTheme &#125; = theming;</span><br></pre></td></tr></table></figure><blockquote><p>主题功能是通用的，因此我将主题相关的能力都放在 <code>@vant-react-native/theme</code> 包中发布。</p></blockquote><h2 id="Button-的实现"><a href="#Button-的实现" class="headerlink" title="Button 的实现"></a>Button 的实现</h2><p>React Native 内置的 Button 组件的样式是固定的，只能进行一些简单的设置。且内置的 Button 组件在 Android 和 ios 两个平台上的表现并不一致。所以我们需要根据更底层的组件进行封装。我们对比 ant-design-mobile-rn 和 react-native-elements 后采用了前者使用的 <code>TouchableHighlight</code> 组件。由于继承自 TouchableHighlight，所以我们组件的 Props 类型如下：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">TouchableHighlightProps</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;react-native&#x27;</span>;</span><br><span class="line"><span class="keyword">interface</span> <span class="title class_">ButtonProps</span> <span class="keyword">extends</span> <span class="title class_">TouchableHighlightProps</span> &#123;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="按钮类型"><a href="#按钮类型" class="headerlink" title="按钮类型"></a>按钮类型</h3><p>vant 的 <a href="https://youzan.github.io/vant/#/zh-CN/button#an-niu-lei-xing">Button</a> 支持 <code>default</code>、<code>primary</code>、<code>info</code>、<code>warning</code>、<code>danger</code> 五种类型，默认为 <code>default</code>。现在，组件的基本定义如下：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// ...</span></span><br><span class="line"><span class="keyword">import</span> <span class="title class_">React</span>, &#123; <span class="title class_">FunctionComponent</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;react&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">Text</span>, <span class="title class_">View</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">interface</span> <span class="title class_">ButtonProps</span> &#123;</span><br><span class="line">  <span class="keyword">type</span>?: <span class="string">&#x27;default&#x27;</span> | <span class="string">&#x27;primary&#x27;</span> | <span class="string">&#x27;info&#x27;</span> | <span class="string">&#x27;warning&#x27;</span> | <span class="string">&#x27;danger&#x27;</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="title class_">Button</span>: <span class="title class_">FunctionComponent</span>&lt;<span class="title class_">ButtonProps</span>&gt; = <span class="function"><span class="params">props</span> =&gt;</span> &#123;</span><br><span class="line">  <span class="comment">// ...</span></span><br><span class="line">&#125;;</span><br><span class="line"><span class="comment">// ...</span></span><br></pre></td></tr></table></figure><p>我们的组件为了适应主题化需求，样式不能是写死在组件里的，而是要通过上下文获取样式常量。我们思路是首先使用 <code>useTheme</code> 从上下文中获取主题，然后由于样式定义较多，我们为每个组件编写一个 <code>useStyle</code> hook 放在单独的 style.ts 文件中：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">StyleSheet</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;react-native&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">Theme</span>, useTheme &#125; <span class="keyword">from</span> <span class="string">&#x27;@vant-react-native/theme&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> <span class="title function_">useStyle</span> = props =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> theme = useTheme&lt;<span class="title class_">Theme</span>&gt;();</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> <span class="title function_">getBackgroundColor</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">    <span class="keyword">switch</span> (props.<span class="property">type</span>) &#123;</span><br><span class="line">      <span class="keyword">case</span> <span class="string">&#x27;primary&#x27;</span>:</span><br><span class="line">        <span class="keyword">return</span> theme[<span class="string">&#x27;success-color&#x27;</span>];</span><br><span class="line">      <span class="keyword">case</span> <span class="string">&#x27;info&#x27;</span>:</span><br><span class="line">        <span class="keyword">return</span> theme[<span class="string">&#x27;primary-color&#x27;</span>];</span><br><span class="line">      <span class="keyword">case</span> <span class="string">&#x27;warning&#x27;</span>:</span><br><span class="line">        <span class="keyword">return</span> theme[<span class="string">&#x27;warning-color&#x27;</span>];</span><br><span class="line">      <span class="keyword">case</span> <span class="string">&#x27;danger&#x27;</span>:</span><br><span class="line">        <span class="keyword">return</span> theme[<span class="string">&#x27;danger-color&#x27;</span>];</span><br><span class="line">      <span class="attr">default</span>:</span><br><span class="line">        <span class="keyword">return</span> theme.<span class="property">white</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> <span class="title function_">getTextColor</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">    <span class="keyword">if</span> (props.<span class="property">type</span> === <span class="string">&#x27;default&#x27;</span>) &#123;</span><br><span class="line">      <span class="keyword">return</span> theme.<span class="property">black</span>;</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">      <span class="keyword">return</span> theme.<span class="property">white</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> <span class="title function_">getBorderRadius</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">    <span class="keyword">if</span> (props.<span class="property">round</span>) &#123;</span><br><span class="line">      <span class="keyword">return</span> theme[<span class="string">&#x27;border-radius-max&#x27;</span>];</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">if</span> (props.<span class="property">square</span>) &#123;</span><br><span class="line">      <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> theme[<span class="string">&#x27;border-radius-sm&#x27;</span>];</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">alignItems</span>: <span class="string">&#x27;center&#x27;</span>,</span><br><span class="line">      <span class="attr">backgroundColor</span>: <span class="title function_">getBackgroundColor</span>(),</span><br><span class="line">      <span class="attr">borderColor</span>: <span class="title function_">getBorderColor</span>(),</span><br><span class="line">      <span class="attr">borderRadius</span>: theme[<span class="string">&#x27;border-radius-sm&#x27;</span>],</span><br><span class="line">      <span class="attr">borderWidth</span>: theme[<span class="string">&#x27;border-width-base&#x27;</span>],</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">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">      <span class="attr">opacity</span>: <span class="number">1</span>,</span><br><span class="line">      <span class="attr">paddingHorizontal</span>: <span class="number">15</span>,</span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="attr">indicator</span>: &#123;</span><br><span class="line">      <span class="attr">marginRight</span>: theme[<span class="string">&#x27;padding-xs&#x27;</span>],</span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="attr">textStyle</span>: &#123;</span><br><span class="line">      <span class="attr">color</span>: <span class="title function_">getTextColor</span>(),</span><br><span class="line">      <span class="attr">fontSize</span>: <span class="number">14</span>,</span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="attr">wrapper</span>: &#123;</span><br><span class="line">      <span class="attr">borderRadius</span>: theme[<span class="string">&#x27;border-radius-sm&#x27;</span>],</span><br><span class="line">      <span class="attr">height</span>: <span class="number">44</span>,</span><br><span class="line">    &#125;,</span><br><span class="line">  &#125;);</span><br><span class="line">  <span class="keyword">return</span> styles;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>基于 <code>useStyle</code> 我们便可完成一个支持多类型的 Button 组件：</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="title class_">Button</span>: <span class="title class_">FunctionComponent</span>&lt;<span class="title class_">ButtonProps</span>&gt; = <span class="function"><span class="params">props</span> =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">const</span> styles = <span class="title function_">useStyle</span>(props);</span><br><span class="line">  <span class="keyword">const</span> &#123; style, ...restProps &#125; = props;</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">TouchableHighlight</span> <span class="attr">style</span>=<span class="string">&#123;[styles.wrapper,</span> <span class="attr">style</span>]&#125; &#123;<span class="attr">...restProps</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;styles.container&#125;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">        &#123;typeof props.children === &#x27;string&#x27; ? (</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.textStyle&#125;</span>&gt;</span>&#123;props.children&#125;<span class="tag">&lt;/<span class="name">Text</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">        ) : (</span></span><br><span class="line"><span class="language-xml">          props.children</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">TouchableHighlight</span>&gt;</span></span></span><br><span class="line">  );</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><blockquote><p>注意：子组件可能是字符串，也可能是组件，所以需要判断类型。</p></blockquote><p>实现效果如下：</p><p><img src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/4a0b412c227e4dc994b818294165240d~tplv-k3u1fbpfcp-watermark.image" alt="紫升"></p><h3 id="朴素按钮"><a href="#朴素按钮" class="headerlink" title="朴素按钮"></a>朴素按钮</h3><p>朴素按钮的文字为按钮颜色，背景为白色，我们通过 <code>plain</code> 属性将按钮设置为朴素按钮。调研了 antd 和 react-native-elements 发现它们都是定义了很多样式，然后在组件内通过逻辑判断计算具体样式的值。个人很不喜欢这种方式，不是彻底的 CSS in JS，我的处理方式是将所有有关样式计算的都封装在每个组件的 <code>useStyle</code> 钩子中，比如当引入朴素按钮属性时，相对于普通按钮改变的有容器背景色、容器边框和字体颜色。所以我们将这三个属性的值都通过一个单独的函数计算。对比 antd 的源码，会发现不仅代码更易读，甚至代码量也少了。</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="title function_">getBackgroundColor</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">if</span> (props.<span class="property">plain</span>) &#123;</span><br><span class="line">    <span class="keyword">return</span> theme.<span class="property">white</span>;</span><br><span class="line">  &#125;</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="keyword">const</span> <span class="title function_">getTextColor</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">if</span> (props.<span class="property">plain</span>) &#123;</span><br><span class="line">    <span class="keyword">switch</span> (props.<span class="property">type</span>) &#123;</span><br><span class="line">      <span class="keyword">case</span> <span class="string">&#x27;primary&#x27;</span>:</span><br><span class="line">        <span class="keyword">return</span> theme[<span class="string">&#x27;success-color&#x27;</span>];</span><br><span class="line">      <span class="keyword">case</span> <span class="string">&#x27;info&#x27;</span>:</span><br><span class="line">        <span class="keyword">return</span> theme[<span class="string">&#x27;primary-color&#x27;</span>];</span><br><span class="line">      <span class="keyword">case</span> <span class="string">&#x27;warning&#x27;</span>:</span><br><span class="line">        <span class="keyword">return</span> theme[<span class="string">&#x27;warning-color&#x27;</span>];</span><br><span class="line">      <span class="keyword">case</span> <span class="string">&#x27;danger&#x27;</span>:</span><br><span class="line">        <span class="keyword">return</span> theme[<span class="string">&#x27;danger-color&#x27;</span>];</span><br><span class="line">      <span class="attr">default</span>:</span><br><span class="line">        <span class="keyword">return</span> theme[<span class="string">&#x27;gray-3&#x27;</span>];</span><br><span class="line">    &#125;</span><br><span class="line">  &#125; <span class="keyword">else</span> <span class="keyword">if</span> (props.<span class="property">type</span> === <span class="string">&#x27;default&#x27;</span>) &#123;</span><br><span class="line">    <span class="keyword">return</span> theme.<span class="property">black</span>;</span><br><span class="line">  &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> theme.<span class="property">white</span>;</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://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0fc1bedf56fe4dd0a31e6d18b048665c~tplv-k3u1fbpfcp-watermark.image" alt="紫升"></p><h3 id="细边框"><a href="#细边框" class="headerlink" title="细边框"></a>细边框</h3><p>vant 实现细边框是通过设置 <code>hairline</code> 属性可以展示 0.5px 的细边框。但是手机上由于分辨率的影响，贸然设置 0.5 会导致边框不显示的兼容问题。好在 React Native 为我们提供了 <code>StyleSheet.hairlineWidth</code> 常量来兼容最细边框问题，下面是官方对它的定义：</p><blockquote><p>hairlineWidth 这一常量始终是一个整数的像素值（线看起来会像头发丝一样细），并会尽量符合当前平台最细的线的标准。可以用作边框或是两个元素间的分隔线。然而，你不能把它“视为一个常量”，因为不同的平台和不同的屏幕像素密度会导致不同的结果。</p><p>如果模拟器缩放过，可能会看不到这么细的线。</p></blockquote><p>由于 <code>hairline</code> 只影响了容器 <code>borderWidth</code> 属性，我们不需要编写单独的样式计算函数：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><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="comment">// ...</span></span><br><span class="line">  <span class="attr">container</span>: &#123;</span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">    <span class="attr">borderWidth</span>: props.<span class="property">hairline</span> ? theme[<span class="string">&#x27;border-width-hairline&#x27;</span>] : theme[<span class="string">&#x27;border-width-base&#x27;</span>],</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://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/43d80d537b07487d9043d718e6541f7e~tplv-k3u1fbpfcp-watermark.image" alt="紫升"></p><h3 id="禁用状态"><a href="#禁用状态" class="headerlink" title="禁用状态"></a>禁用状态</h3><p>表单元素或者说可触摸可点击的元素一般都有禁用状态，vant 中是通过 disabled 属性来禁用按钮，禁用状态下按钮不可点击。TouchableHighlight 继承地有 <code>disabled</code> 属性，我们只需要设置一些禁用状态下的按钮样式就可以，查看 vant 源码我们发现只需要修改透明度为 0.5 即可：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><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="comment">// ...</span></span><br><span class="line">    <span class="attr">opacity</span>: props.<span class="property">disabled</span> ? <span class="number">0.5</span> : <span class="number">1</span>,</span><br><span class="line">    <span class="comment">// ...</span></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://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/892589086d07426a8346319d8466bccf~tplv-k3u1fbpfcp-watermark.image" alt="紫升"></p><h3 id="加载状态"><a href="#加载状态" class="headerlink" title="加载状态"></a>加载状态</h3><p>vant 是通过 <code>loading</code> 属性设置按钮为加载状态，加载状态下默认会隐藏按钮文字，可以通过 <code>loading-text</code> 设置加载状态下的文字。我们借助 React Native 的 ActivityIndicator 组件可以轻松实现这个特性：</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// ...</span></span><br><span class="line">&lt;<span class="title class_">TouchableHighlight</span> &#123;...restProps&#125;&gt;</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.contentWrapper&#125;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">    &#123;props.loading ? (</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">ActivityIndicator</span> <span class="attr">size</span>=<span class="string">&quot;small&quot;</span> <span class="attr">color</span>=<span class="string">&#123;indicatorColor&#125;</span> <span class="attr">style</span>=<span class="string">&#123;styles.indicator&#125;</span> /&gt;</span></span></span><br><span class="line"><span class="language-xml">        &#123;props.loadingText ? <span class="tag">&lt;<span class="name">Text</span> <span class="attr">style</span>=<span class="string">&#123;styles.textStyle&#125;</span>&gt;</span>&#123;props.loadingText&#125;<span class="tag">&lt;/<span class="name">Text</span>&gt;</span> : null&#125;</span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;/&gt;</span></span></span><br><span class="line">    ) : <span class="literal">null</span>&#125;</span><br><span class="line">  &lt;/<span class="title class_">View</span>&gt;</span><br><span class="line">&lt;/<span class="title class_">TouchableHighlight</span>&gt;</span><br><span class="line"><span class="comment">// ...</span></span><br></pre></td></tr></table></figure><p>样式如下：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> useIndicatorColor = (<span class="attr">props</span>: <span class="title class_">ButtonProps</span>): <span class="function"><span class="params">string</span> =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">const</span> theme = useTheme&lt;<span class="title class_">Theme</span>&gt;();</span><br><span class="line">  <span class="keyword">if</span> (props.<span class="property">plain</span>) &#123;</span><br><span class="line">    <span class="keyword">switch</span> (props.<span class="property">type</span>) &#123;</span><br><span class="line">      <span class="keyword">case</span> <span class="string">&#x27;primary&#x27;</span>:</span><br><span class="line">        <span class="keyword">return</span> theme[<span class="string">&#x27;success-color&#x27;</span>];</span><br><span class="line">      <span class="keyword">case</span> <span class="string">&#x27;info&#x27;</span>:</span><br><span class="line">        <span class="keyword">return</span> theme[<span class="string">&#x27;primary-color&#x27;</span>];</span><br><span class="line">      <span class="keyword">case</span> <span class="string">&#x27;warning&#x27;</span>:</span><br><span class="line">        <span class="keyword">return</span> theme[<span class="string">&#x27;warning-color&#x27;</span>];</span><br><span class="line">      <span class="keyword">case</span> <span class="string">&#x27;danger&#x27;</span>:</span><br><span class="line">        <span class="keyword">return</span> theme[<span class="string">&#x27;danger-color&#x27;</span>];</span><br><span class="line">      <span class="attr">default</span>:</span><br><span class="line">        <span class="keyword">return</span> theme.<span class="property">black</span>;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125; <span class="keyword">else</span> <span class="keyword">if</span> (props.<span class="property">type</span> === <span class="string">&#x27;default&#x27;</span>) &#123;</span><br><span class="line">    <span class="keyword">return</span> theme.<span class="property">black</span>;</span><br><span class="line">  &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> theme.<span class="property">white</span>;</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://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/27c4340df2254245a3c830bf667a2e28~tplv-k3u1fbpfcp-watermark.image" alt="紫升"></p><h3 id="按钮形状"><a href="#按钮形状" class="headerlink" title="按钮形状"></a>按钮形状</h3><p>默认的按钮有值为 2 的圆角，vant 中通过 <code>square</code> 设置方形按钮，通过 <code>round</code> 设置圆形按钮。按例，我们通过判断设置样式：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="title function_">getBorderRadius</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">if</span> (props.<span class="property">round</span>) &#123;</span><br><span class="line">    <span class="keyword">return</span> theme[<span class="string">&#x27;border-radius-max&#x27;</span>];</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">if</span> (props.<span class="property">square</span>) &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">return</span> theme[<span class="string">&#x27;border-radius-sm&#x27;</span>];</span><br><span class="line">&#125;;</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">borderColor</span>: <span class="title function_">getBorderColor</span>(),</span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="attr">wrapper</span>: &#123;</span><br><span class="line">    <span class="attr">borderRadius</span>: <span class="title function_">getBorderRadius</span>(),</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://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d1402517566e49d1a1920fd9b3f7bb20~tplv-k3u1fbpfcp-watermark.image" alt="紫升"></p><h3 id="按钮尺寸"><a href="#按钮尺寸" class="headerlink" title="按钮尺寸"></a>按钮尺寸</h3><p>Antd RN 只提供了 large、small 两个尺寸，而在 vant 中支持 large、normal、small、mini 四种尺寸，默认为 normal。虽然写到这里已经很疲倦了，杨枝甘露也早喝完了，但是为了完整复原，还是续上一杯咖啡继续肝。根据 vant 设计稿我们新增三个样式获取函数并动态化指定样式：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="title function_">getSizeHeight</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">switch</span> (props.<span class="property">size</span>) &#123;</span><br><span class="line">    <span class="keyword">case</span> <span class="string">&#x27;large&#x27;</span>:</span><br><span class="line">      <span class="keyword">return</span> <span class="number">50</span>;</span><br><span class="line">    <span class="keyword">case</span> <span class="string">&#x27;small&#x27;</span>:</span><br><span class="line">      <span class="keyword">return</span> <span class="number">32</span>;</span><br><span class="line">    <span class="keyword">case</span> <span class="string">&#x27;mini&#x27;</span>:</span><br><span class="line">      <span class="keyword">return</span> <span class="number">24</span>;</span><br><span class="line">    <span class="attr">default</span>:</span><br><span class="line">      <span class="keyword">return</span> <span class="number">44</span>;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;;</span><br><span class="line"><span class="keyword">const</span> <span class="title function_">getSizePadding</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">switch</span> (props.<span class="property">size</span>) &#123;</span><br><span class="line">    <span class="keyword">case</span> <span class="string">&#x27;small&#x27;</span>:</span><br><span class="line">      <span class="keyword">return</span> <span class="number">8</span>;</span><br><span class="line">    <span class="keyword">case</span> <span class="string">&#x27;mini&#x27;</span>:</span><br><span class="line">      <span class="keyword">return</span> <span class="number">4</span>;</span><br><span class="line">    <span class="attr">default</span>:</span><br><span class="line">      <span class="keyword">return</span> <span class="number">15</span>;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;;</span><br><span class="line"><span class="keyword">const</span> <span class="title function_">getSizeFontSize</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">switch</span> (props.<span class="property">size</span>) &#123;</span><br><span class="line">    <span class="keyword">case</span> <span class="string">&#x27;large&#x27;</span>:</span><br><span class="line">      <span class="keyword">return</span> <span class="number">16</span>;</span><br><span class="line">    <span class="keyword">case</span> <span class="string">&#x27;small&#x27;</span>:</span><br><span class="line">      <span class="keyword">return</span> <span class="number">12</span>;</span><br><span class="line">    <span class="keyword">case</span> <span class="string">&#x27;mini&#x27;</span>:</span><br><span class="line">      <span class="keyword">return</span> <span class="number">10</span>;</span><br><span class="line">    <span class="attr">default</span>:</span><br><span class="line">      <span class="keyword">return</span> <span class="number">14</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> 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">paddingHorizontal</span>: <span class="title function_">getSizePadding</span>(),</span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="attr">textStyle</span>: &#123;</span><br><span class="line">    <span class="attr">fontSize</span>: <span class="title function_">getSizeFontSize</span>(),</span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="attr">wrapper</span>: &#123;</span><br><span class="line">    <span class="attr">height</span>: <span class="title function_">getSizeHeight</span>(),</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://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/59c01002f25b477d85ed26fa1e21c86b~tplv-k3u1fbpfcp-watermark.image" alt="紫升"></p><h3 id="自定义颜色"><a href="#自定义颜色" class="headerlink" title="自定义颜色"></a>自定义颜色</h3><p>如果不是自己亲自复刻 Vant，是没想到一个 Button 能玩出这么多花，支持特性这么多耐心和代码管理都是一个挑战。当然了，紫升采取的样式管理方式比较偏激，大家有好的方式也可以在评论区讨论。</p><p>通过 <code>color</code> 属性自定义按钮的颜色。我们可以得出需求，不管 type 是什么，<code>color</code> 属性需始终覆盖原有样式，color 能影响的就是背景色、字体颜色和边框颜色，所以我们修改 <code>getBackgroundColor</code>、<code>getTextColor</code>、<code>getBorderColor</code> 样式函数在合适的地方加上以下代码即可：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (props.<span class="property">color</span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> props.<span class="property">color</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>实现效果如下：</p><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/8efffdbfd6424618be6152895a53e8ee~tplv-k3u1fbpfcp-watermark.image" alt="紫升"></p><h2 id="双击事件的实现"><a href="#双击事件的实现" class="headerlink" title="双击事件的实现"></a>双击事件的实现</h2><p>我们从 React Native 内置的 TouchableHighlight 组件继承了很多事件，其中 onPress、onLongPress 分别代表单击和长按。但唯独“双击 666”的双击事件没有姓名。之前在实际业务曾经封装过双击事件，这次我们就直接就内置了。</p><p>实现思路是延时执行单击事件（默认 200 毫秒），然后记录点击次数和两次时间间隔，当识别为第二次点击且时间间隔小于单击延时时间。那么就取消单击事件延时，并立即执行双击事件。完整代码如下：</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> lastTime = <span class="number">0</span>;</span><br><span class="line"><span class="keyword">let</span> clickCount = <span class="number">1</span>;</span><br><span class="line"><span class="keyword">let</span> timeout = <span class="literal">null</span>;</span><br><span class="line"><span class="keyword">const</span> <span class="title function_">_onPress</span> = (<span class="params">event: GestureResponderEvent</span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> now = <span class="title class_">Date</span>.<span class="title function_">now</span>();</span><br><span class="line">  <span class="keyword">if</span> (timeout) &#123;</span><br><span class="line">    <span class="built_in">clearTimeout</span>(timeout);</span><br><span class="line">  &#125;</span><br><span class="line">  timeout = <span class="built_in">setTimeout</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">    props.<span class="title function_">onPress</span>(event);</span><br><span class="line">    clickCount = <span class="number">1</span>;</span><br><span class="line">    lastTime = <span class="number">0</span>;</span><br><span class="line">  &#125;, props.<span class="property">delayDoublePress</span>);</span><br><span class="line">  <span class="keyword">if</span> (clickCount === <span class="number">2</span> &amp;&amp; now - lastTime &lt;= props.<span class="property">delayDoublePress</span>) &#123;</span><br><span class="line">    <span class="built_in">clearTimeout</span>(timeout);</span><br><span class="line">    clickCount = <span class="number">1</span>;</span><br><span class="line">    lastTime = <span class="number">0</span>;</span><br><span class="line">    props.<span class="title function_">onDoublePress</span>(event);</span><br><span class="line">  &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    clickCount++;</span><br><span class="line">    lastTime = now;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>大家会发现这里的实现糅合了函数防抖、节流以及计数器的原理，有兴趣的小伙伴可以自行复习下原理，这里就不展开了。</p><h2 id="API-文档"><a href="#API-文档" class="headerlink" title="API 文档"></a>API 文档</h2><p>一个组件的文档，除了 Demo，还需要展示出来可用的 Props，Dumi 内置的 <code>&lt;API&gt;&lt;/API&gt;</code> 组件可以根据组件自动生成 API 文档。首先我们像下面一样编写 Props 注释：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">interface</span> <span class="title class_">ButtonProps</span> <span class="keyword">extends</span> <span class="title class_">TouchableHighlightProps</span> &#123;</span><br><span class="line">  <span class="comment">/**</span></span><br><span class="line"><span class="comment">   * <span class="doctag">@description</span>       Can be set to primary、info、warning、danger</span></span><br><span class="line"><span class="comment">   * <span class="doctag">@description</span>.zh-CN 类型，可选值为 primary、info、warning、danger</span></span><br><span class="line"><span class="comment">   */</span></span><br><span class="line">  <span class="keyword">type</span>?: <span class="string">&#x27;default&#x27;</span> | <span class="string">&#x27;primary&#x27;</span> | <span class="string">&#x27;info&#x27;</span> | <span class="string">&#x27;warning&#x27;</span> | <span class="string">&#x27;danger&#x27;</span>;</span><br><span class="line">  <span class="comment">/**</span></span><br><span class="line"><span class="comment">   * <span class="doctag">@description</span>       Can be set to large、small、mini</span></span><br><span class="line"><span class="comment">   * <span class="doctag">@description</span>.zh-CN 尺寸，可选值为</span></span><br><span class="line"><span class="comment">   */</span></span><br><span class="line">  size?: <span class="string">&#x27;large&#x27;</span> | <span class="string">&#x27;normal&#x27;</span> | <span class="string">&#x27;small&#x27;</span> | <span class="string">&#x27;mini&#x27;</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>然后在 Markdown 中引入 API 组件即可：</p><figure class="highlight md"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="language-xml"><span class="tag">&lt;<span class="name">API</span> <span class="attr">src</span>=<span class="string">&quot;./index.tsx&quot;</span>&gt;</span></span><span class="language-xml"><span class="tag">&lt;/<span class="name">API</span>&gt;</span></span></span><br></pre></td></tr></table></figure><p>内置组件 API 没有处理继承的情况，我们后续会自定义一个 API 组件，这里就不展开了，浏览 <a href="https://vant-react-native.js.org/components/button#api">Button 文档</a> 可以查看现在的效果：</p><p><img src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/7bedac7240334ff79c4142005a66b158~tplv-k3u1fbpfcp-watermark.image" alt="紫升"></p><h2 id="工程化串讲"><a href="#工程化串讲" class="headerlink" title="工程化串讲"></a>工程化串讲</h2><p>由于很难在一篇文章中将组件开发相关的工程化讲完，我们需要在每篇实战中串讲一下。</p><h3 id="组件创建脚手架"><a href="#组件创建脚手架" class="headerlink" title="组件创建脚手架"></a>组件创建脚手架</h3><blockquote><p>小黑：紫升，<code>lerna create</code> 命令创建出来的模块并不是我们想要的，以后要创建很多很多组件，我们可以写一个创建组件模块的脚手架吗？</p></blockquote><p>lerna 使用起来是有不少痛点的，<code>lerna create</code> 命令没办法指定模板，考虑到之后的几十上百个组件每次创建都要进行项目结构、Typescript 配置、单元测试配置、Babel 配置等等工作步骤，我们有必要写一个脚手架。</p><h4 id="模板解析"><a href="#模板解析" class="headerlink" title="模板解析"></a>模板解析</h4><p>说到模板解析，相信大家和我一样想到的是 vue-cli 的 template 解析。通过阅读 <a href="https://cdn.jsdelivr.net/npm/vue-cli@2.9.6/lib/generate.js">vue-cli@2.9.6 generate.js</a> 源码，我们可以分析出尤大是基于 metalsmith、handlebars、consolidate 这三个包来实现模板解析能力的。让人不安的是其中 metalsmith 库有长达 5 年没有维护了，紫升挑选开源项目一般对维护度很敏感，本着轮子要用自己造的原则，我翻看了 Metalsmith 的 Readme 发现这个插件无非是通过递归读文件的方式渲染模板，并且它的静态网站生成的能力对我们模板解析的需求也是多余的。</p><p>说干就干，在和 <a href="https://juejin.cn/user/3175045313873943">@林小帅</a> 同学简单沟通后，我动手造了 <a href="https://github.com/youngjuning/handlebars-template-compiler">handlebars-template-compiler</a> 这个轮子，其主要原理如下：</p><ol><li>使用 recursive-readdir 递归获取所有文件路径</li></ol><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> files = <span class="keyword">await</span> <span class="title function_">recursive</span>(rootDir);</span><br></pre></td></tr></table></figure><ol start="2"><li>使用 <code>handlebars.compile</code> 方法使用元数据对模板进行渲染</li></ol><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> content = fs.<span class="title function_">readFileSync</span>(file).<span class="title function_">toString</span>();</span><br><span class="line"><span class="keyword">const</span> result = handlebars.<span class="title function_">compile</span>(content)(meta);</span><br></pre></td></tr></table></figure><ol start="3"><li>使用 <code>fs.writeFileSync</code> API 重写文件</li></ol><p>另外，通过引入 <a href="https://www.npmjs.com/package/micromatch">glob</a> 模式匹配实现了 <code>exclude</code> 配置以及只处理指定后缀（默认 <code>**/*.tpl.*</code>）的文件来避免不必要的渲染。（PS：NPM 一周有了 300 多下载，有需要的掘友值得一试😄）</p><h4 id="Node-CLI（-vant-react-native-scripts）搭建"><a href="#Node-CLI（-vant-react-native-scripts）搭建" class="headerlink" title="Node CLI（@vant-react-native&#x2F;scripts）搭建"></a>Node CLI（@vant-react-native&#x2F;scripts）搭建</h4><p>这里紫升尝试用最简洁的语言为大家描述一个脚手架的诞生，源码在 <a href="https://github.com/youngjuning/vant-react-native/tree/main/packages/scripts">packages&#x2F;scripts</a> 目录下，没有接触过 CLI 的掘友请相信我，Node CLI 很容易上手的。接触过的同学也可以查漏补缺借鉴一二。</p><ol><li><code>package.json</code> 文件的 <code>bin</code> 字段是我们脚手架的入口</li></ol><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 指定可执行文件的位置以及别名</span></span><br><span class="line"><span class="attr">&quot;bin&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;vant&quot;</span><span class="punctuation">:</span> <span class="string">&quot;./bin/cli.js&quot;</span></span><br><span class="line"><span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br></pre></td></tr></table></figure><ol start="2"><li>定义 <code>./bin/cli.js</code> 为可执行文件并调用 <code>init</code> 方法。</li></ol><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 由于我们的脚本是 Node 编写的，所以需要指定 node 所在位置</span></span><br><span class="line">#!<span class="regexp">/usr/</span>bin/env node</span><br><span class="line"><span class="keyword">const</span> &#123; init &#125; = <span class="built_in">require</span>(<span class="string">&#x27;../lib&#x27;</span>);</span><br><span class="line"><span class="comment">// 这个地方参考了 create-react-native 的设计</span></span><br><span class="line"><span class="comment">// 本文点赞过 300，下一篇紫升带小黑为大家带来《基于 TypeScript 重构 create-react-native》</span></span><br><span class="line"><span class="title function_">init</span>();</span><br></pre></td></tr></table></figure><ol start="3"><li>然后在 <code>src/index.ts</code> 中初始化 commander 这个久负盛名的命令行框架</li></ol><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> init = (): <span class="function"><span class="params">void</span> =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">const</span> packageJson = <span class="built_in">require</span>(<span class="string">&#x27;../package.json&#x27;</span>);</span><br><span class="line">  program.<span class="title function_">version</span>(packageJson.<span class="property">version</span>).<span class="title function_">description</span>(packageJson.<span class="property">description</span>);</span><br><span class="line">  <span class="comment">// ...</span></span><br><span class="line">  program.<span class="title function_">parse</span>(process.<span class="property">argv</span>);</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><ol start="4"><li>为了方便管理命令，我们将命令都放置在 <code>src/commands</code> 目录下并通过 <code>fs.readdirSync</code> API 动态扫描注册。</li></ol><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> init = (): <span class="function"><span class="params">void</span> =&gt;</span> &#123;</span><br><span class="line">  <span class="comment">// 这段代码借鉴自 NeteaseCloudMusicApi 项目，作者的代码很有设计感，推荐阅读。</span></span><br><span class="line">  fs.<span class="title function_">readdirSync</span>(path.<span class="title function_">join</span>(__dirname, <span class="string">&#x27;commands&#x27;</span>)).<span class="title function_">forEach</span>(<span class="function">(<span class="params">file: <span class="built_in">string</span></span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> (!file.<span class="title function_">endsWith</span>(<span class="string">&#x27;.js&#x27;</span>)) <span class="keyword">return</span>;</span><br><span class="line">    <span class="built_in">require</span>(path.<span class="title function_">join</span>(__dirname, <span class="string">&#x27;commands&#x27;</span>, file));</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><ol start="5"><li>最后在 <code>commands</code> 目录下新建一个 <code>create.ts</code> 文件编写命令</li></ol><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; program &#125; <span class="keyword">from</span> <span class="string">&#x27;commander&#x27;</span>;</span><br><span class="line">program</span><br><span class="line">  .<span class="title function_">command</span>(<span class="string">&#x27;create &lt;name&gt; [loc]&#x27;</span>)</span><br><span class="line">  .<span class="title function_">description</span>(<span class="string">&#x27;Create a new vant-react-native package&#x27;</span>)</span><br><span class="line">  .<span class="title function_">action</span>(<span class="function">(<span class="params">name,loc</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;Hello Luozhu&#x27;</span>);</span><br><span class="line">  &#125;)</span><br></pre></td></tr></table></figure><h4 id="脚手架实现"><a href="#脚手架实现" class="headerlink" title="脚手架实现"></a>脚手架实现</h4><p>上一小结，我们初始化了 CLI 并添加了 <code>create</code> 命令，这一小节我们就来实现一下脚手架功能。</p><p>我们首先在 <code>packages/scripts</code> 目录下创建组件模板</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">.</span><br><span class="line">├── README.tpl.md <span class="comment"># tpl 后缀在生成组件模板的时候会被 handlebars-template-compiler 自动去掉。</span></span><br><span class="line">├── package.tpl.json</span><br><span class="line">├── src</span><br><span class="line">│   └── index.ts <span class="comment"># 没有 tpl 后缀则不会被编译，模板很大时可以节省时间。</span></span><br><span class="line">└── tsconfig.json</span><br></pre></td></tr></table></figure><p>然后我们明确我们的模板元数据的数据结构，我这里的数据结构是：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">interface</span> <span class="title class_">IMeta</span> &#123;</span><br><span class="line">  <span class="attr">name</span>: <span class="built_in">string</span>;</span><br><span class="line">  <span class="attr">version</span>: <span class="built_in">string</span>;</span><br><span class="line">  <span class="attr">description</span>: <span class="built_in">string</span>;</span><br><span class="line">  <span class="attr">author</span>: <span class="built_in">string</span>;</span><br><span class="line">  <span class="attr">email</span>: <span class="built_in">string</span>;</span><br><span class="line">  <span class="attr">url</span>: <span class="built_in">string</span>;</span><br><span class="line">  <span class="attr">directory</span>: <span class="built_in">string</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>有了数据结构，我们就可以使用 inquirer 模块引导用户输入信息。</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> inquirer <span class="keyword">from</span> <span class="string">&#x27;inquirer&#x27;</span>;</span><br><span class="line"><span class="comment">// ...</span></span><br><span class="line"><span class="comment">// getQuestions 过长，感兴趣的同学可以查看：http://tny.im/UFbg</span></span><br><span class="line"><span class="keyword">const</span> <span class="attr">answer</span>: <span class="title class_">IMeta</span> = <span class="keyword">await</span> inquirer.<span class="title function_">prompt</span>(<span class="title function_">getQuestions</span>(name));</span><br><span class="line"><span class="comment">// ...</span></span><br></pre></td></tr></table></figure><p>下一步，我们使用 <code>tmp-promise</code> 模块创建一个系统临时文件夹，并将前文提到的 template 文件夹的内容拷贝进去：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> tmp <span class="keyword">from</span> <span class="string">&#x27;tmp-promise&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> fs <span class="keyword">from</span> <span class="string">&#x27;fs-extra&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> path <span class="keyword">from</span> <span class="string">&#x27;path&#x27;</span>;</span><br><span class="line"><span class="comment">// ...</span></span><br><span class="line"><span class="keyword">const</span> tmpdir = <span class="keyword">await</span> tmp.<span class="title function_">dir</span>(&#123; <span class="attr">unsafeCleanup</span>: <span class="literal">true</span> &#125;);</span><br><span class="line">fs.<span class="title function_">copySync</span>(path.<span class="title function_">join</span>(__dirname, <span class="string">&#x27;../../template&#x27;</span>), tmpdir.<span class="property">path</span>);</span><br></pre></td></tr></table></figure><p>最后，我们对临时文件夹的内容进行编译再拷贝到指定位置即可：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> htc <span class="keyword">from</span> <span class="string">&#x27;handlebars-template-compiler&#x27;</span>;</span><br><span class="line"><span class="comment">// ...</span></span><br><span class="line"><span class="keyword">await</span> htc&lt;<span class="title class_">IMeta</span>&gt;(answer, tmpdir.<span class="property">path</span>);</span><br><span class="line">fs.<span class="title function_">copySync</span>(tmpdir.<span class="property">path</span>, <span class="string">`<span class="subst">$&#123;process.cwd()&#125;</span>/packages/<span class="subst">$&#123;locPath&#125;</span>`</span>);</span><br><span class="line"><span class="comment">// ...</span></span><br></pre></td></tr></table></figure><p>折腾这一顿，让我们来看下成果吧：</p><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/4ba477bcbd8b4ef6b880c7e8e5a095bc~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><h3 id="Github-CODEOWENERS"><a href="#Github-CODEOWENERS" class="headerlink" title="Github CODEOWENERS"></a>Github CODEOWENERS</h3><p>大型的开源项目最难的不是技术问题，技术大咖永远不会缺。最难的其实是协作和后期维护。试想一下一个成百上千人参与的项目当有新的 pr 时，正常人根本无力去快速检索出需要谁去 review 代码。我们的 vant-react-native 由于是将每个组件单独发包维护，当参与的小伙伴多了也会产生这个困扰。</p><p>而 GitHub CODEOWNERS（代码所有者）就是为了解决这个问题的，在 5000+ 贡献者参与的 <a href="https://github.com/DefinitelyTyped/DefinitelyTyped">DefinitelyTyped</a> 项目中我们就可以看到它的身影。官方对代码所有者定义如下：</p><blockquote><p>你可以使用 CODEOWNERS 文件定义负责仓库代码的个人或团队。当有人修改代码并打开一个 pull request 时，将自动请求代码所有者进行审查。</p></blockquote><p>CODEOWNERS 文件使用遵循 gitignore 文件中所用大多数规则的模式，CODEOWNERS 文件位置一般位于 <code>.github/</code> 目录下。</p><p>在 vant-react-native，紫升是仓库的最终负责人，所以是期望每个 pr 都可以分配给自己审查一下的。那么我们这就来实验一下吧，新建一个 <code>.github/CODEOWNERS</code> 文件并写入以下内容：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"># This is a comment.</span><br><span class="line"># Each line is a file pattern followed by one or more owners.</span><br><span class="line"></span><br><span class="line"># These owners will be the default owners for everything in</span><br><span class="line"># the repo. Unless a later match takes precedence,</span><br><span class="line"># @youngjuning will be requested for review when someone opens a pull request.</span><br><span class="line">*       @youngjuning</span><br><span class="line"></span><br><span class="line"># In this example, @doctocat owns any files in the build/logs</span><br><span class="line"># directory at the root of the repository and any of its</span><br><span class="line"># subdirectories.</span><br><span class="line">/packages/ @luozhu1994</span><br></pre></td></tr></table></figure><p>一般如果文件具有代码所有者，则在打开拉取请求之前可以看到代码所有者是谁。在仓库中，你可以找到文件并悬停于一个锁图标上，悬浮之后会告诉你该文件所有者是谁：</p><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/726bd5144a104902ae27ad31e1d46f93~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><p>然后我们提交一个 pr 看看效果：</p><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/bf4cb8b7ef5c41989660e735e621fc4e~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><h3 id="NPM-发包自动化"><a href="#NPM-发包自动化" class="headerlink" title="NPM 发包自动化"></a>NPM 发包自动化</h3><p>发包权限一般只有仓库所有者一个人拥有，但是 owner 同时维护好几个 NPM 账号，或者是 owner 忽然很忙将发布权限交给其他人管理员但是不便告知 NPM 账号该怎么办呢？答案是将 NPM 发包 CD（持续部署）化，公司一般会基于 Gitlab 或自建平台实现该功能。作为开源项目，我们当然是使用 GitHub Action。</p><p>正常的单包项目，使用 <a href="https://github.com/JS-DevTools/npm-publish">npm-publish</a> 或 <a href="https://github.com/pascalgn/npm-publish-action">npm-publish-action</a> 这两个 GitHub Action，这并没有好讲的。但是基于 lerna 的多包单体仓库并没有现成的插件可以用，照例，我们来看下自己实现的步骤：</p><ol><li>判断 commit message 是否以 <code>chore(release):</code> 开头<blockquote><p>通过 GitHub Action <code>startsWith(github.event.head_commit.message, &#39;chore(release):&#39;)</code> 实现</p></blockquote></li><li>通过 NPM publish token 认证登录<blockquote><p>通过 <code>npm config set //registry.npmjs.org/:_authToken=$&#123;&#123; secrets.NPM_TOKEN &#125;&#125;</code> 认证</p></blockquote></li><li>执行 <code>lerna publish from-package --yes</code> 发布<blockquote><p>需要本地先执行 <code>lerna version</code> 系列命令提升版本</p></blockquote></li></ol><p>完整 GitHub Action 实现如下：</p><figure class="highlight yml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">name:</span> <span class="string">npm-publish</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><br><span class="line">      <span class="bullet">-</span> <span class="string">main</span></span><br><span class="line"></span><br><span class="line"><span class="attr">jobs:</span></span><br><span class="line">  <span class="attr">npm-publish:</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">if:</span> <span class="string">startsWith(github.event.head_commit.message,</span> <span class="string">&#x27;chore(release):&#x27;</span><span class="string">)</span></span><br><span class="line">    <span class="attr">steps:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">uses:</span> <span class="string">actions/checkout@v2</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">uses:</span> <span class="string">c-hive/gha-yarn-cache@v2</span> <span class="comment"># 缓存 node_modules 加快构建速度</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Install</span> <span class="string">Packages</span></span><br><span class="line">        <span class="attr">run:</span> <span class="string">yarn</span> <span class="string">install</span> <span class="string">--registry=https://registry.npmjs.org/</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Authenticate</span> <span class="string">with</span> <span class="string">Registry</span></span><br><span class="line">        <span class="attr">run:</span> <span class="string">|</span></span><br><span class="line"><span class="string">          npm config set //registry.npmjs.org/:_authToken=$&#123;NPM_TOKEN&#125;</span></span><br><span class="line"><span class="string"></span>        <span class="attr">env:</span></span><br><span class="line">          <span class="attr">NPM_TOKEN:</span> <span class="string">$&#123;&#123;</span> <span class="string">secrets.NPM_TOKEN</span> <span class="string">&#125;&#125;</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Publish</span> <span class="string">package</span></span><br><span class="line">        <span class="attr">run:</span> <span class="string">lerna</span> <span class="string">publish</span> <span class="string">from-package</span> <span class="string">--yes</span></span><br></pre></td></tr></table></figure><p>为了在发布后及时获取通知，紫升使用了 <code>peter-evans/commit-comment</code> 插件在发布失败或成功后对相应 commit 进行评论，这样我们就可以收到邮件和站内通知。</p><figure class="highlight yml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Create</span> <span class="string">commit</span> <span class="string">comment</span> <span class="string">after</span> <span class="string">publish</span> <span class="string">successfully</span></span><br><span class="line">  <span class="attr">if:</span> <span class="string">$&#123;&#123;</span> <span class="string">success()</span> <span class="string">&#125;&#125;</span></span><br><span class="line">  <span class="attr">uses:</span> <span class="string">peter-evans/commit-comment@v1</span></span><br><span class="line">  <span class="attr">with:</span></span><br><span class="line">    <span class="attr">body:</span> <span class="string">|</span></span><br><span class="line"><span class="string">      Hello Dear @youngjuning. This commit has been publish to NPM successfully.</span></span><br><span class="line"><span class="string">      &gt; Created by [commit-comment][1]</span></span><br><span class="line"><span class="string"></span></span><br><span class="line">      [<span class="number">1</span>]<span class="string">:</span> <span class="string">https://github.com/peter-evans/commit-comment</span></span><br><span class="line"><span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Create</span> <span class="string">commit</span> <span class="string">comment</span> <span class="string">after</span> <span class="string">publish</span> <span class="string">unsuccessfully</span></span><br><span class="line">  <span class="attr">if:</span> <span class="string">$&#123;&#123;</span> <span class="string">failure()</span> <span class="string">&#125;&#125;</span></span><br><span class="line">  <span class="attr">uses:</span> <span class="string">peter-evans/commit-comment@v1</span></span><br><span class="line">  <span class="attr">with:</span></span><br><span class="line">    <span class="attr">body:</span> <span class="string">|</span></span><br><span class="line"><span class="string">      Hello Dear @youngjuning. This commit has been publish to NPM unsuccessfully.</span></span><br><span class="line"><span class="string">      &gt; Created by [commit-comment][1]</span></span><br><span class="line"><span class="string"></span></span><br><span class="line">      [<span class="number">1</span>]<span class="string">:</span> <span class="string">https://github.com/peter-evans/commit-comment</span></span><br></pre></td></tr></table></figure>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/f4ea430d1d6d/</id>
    <link href="https://blog.zisheng.pro/posts/f4ea430d1d6d/"/>
    <published>2023-03-03T02:36:18.000Z</published>
    <summary>以 Button 为例拆解 React Native 组件库的主题、交互与 API 设计，讨论组件开发中如何在设计语义、可复用性和工程约束之间做取舍。</summary>
    <title>前端组件化实战之 Button</title>
    <updated>2026-09-18T08:15:28.636Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="VS Code" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/VS-Code/"/>
    <category term="掘金" scheme="https://blog.zisheng.pro/tags/%E6%8E%98%E9%87%91/"/>
    <category term="VS Code" scheme="https://blog.zisheng.pro/tags/VS-Code/"/>
    <category term="Webview" scheme="https://blog.zisheng.pro/tags/Webview/"/>
    <category term="扩展" scheme="https://blog.zisheng.pro/tags/%E6%89%A9%E5%B1%95/"/>
    <content>
      <![CDATA[<blockquote><p>⚠️本文为掘金社区首发签约文章，未获授权禁止转载</p></blockquote><blockquote><p>温馨提示：结合本文配套<a href="https://github.com/youngjuning/juejin-me">源码</a>阅读体验更佳！</p></blockquote><h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>在团队降本提效的基建中，紫升开发了一款 vscode 插件，第一版我使用的是 vscode 内置 UI，虽说也能用，但是用户体验欠佳。由于 vscode 内置 UI 不够灵活，一番调研后我决定使用 webview 重构。</p><p>开发过 vscode 插件的同学可能对插件开发知识点多、文档阅读困难、参考资料少有所体会。基于 webview 开发插件更是如此，寻遍网络，虽然有优秀的项目，但却没有完整且优秀的教程。为了修炼 vscode 开发灵力，不妨和紫升一起挑战从零到一开发一款基于 webview 的 vscode 插件。</p><h2 id="Hello-vscode"><a href="#Hello-vscode" class="headerlink" title="Hello vscode"></a>Hello vscode</h2><p>英雄多起于市井，高楼皆起于平地。再伟大的软件也都是从 Hello World 开始的，本章尽量用最简洁的语言描述一个 vscode 插件 Hello World 的诞生。</p><h3 id="初始化项目"><a href="#初始化项目" class="headerlink" title="初始化项目"></a>初始化项目</h3><p>安装 <a href="http://yeoman.io/">Yeoman</a> 和 <a href="https://www.npmjs.com/package/generator-code">VS Code Extension Generator</a>：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ npm install -g yo generator-code</span><br></pre></td></tr></table></figure><p>这个脚手架会生成一个可以立马开发的项目。运行生成器，然后填好下列字段：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line">$ yo code</span><br><span class="line"><span class="comment">#     _-----_     ╭──────────────────────────╮</span></span><br><span class="line"><span class="comment">#    |       |    │   Welcome to the Visual  │</span></span><br><span class="line"><span class="comment">#    |--(o)--|    │   Studio Code Extension  │</span></span><br><span class="line"><span class="comment">#   `---------´   │        generator!        │</span></span><br><span class="line"><span class="comment">#    ( _´U`_ )    ╰──────────────────────────╯</span></span><br><span class="line"><span class="comment">#    /___A___\   /</span></span><br><span class="line"><span class="comment">#     |  ~  |</span></span><br><span class="line"><span class="comment">#   __&#x27;.___.&#x27;__</span></span><br><span class="line"><span class="comment"># ´   `  |° ´ Y `</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># ? What type of extension do you want to create? New Extension (TypeScript)</span></span><br><span class="line"><span class="comment"># ? What&#x27;s the name of your extension? Juejin Posts</span></span><br><span class="line"><span class="comment"># ? What&#x27;s the identifier of your extension? juejin-posts</span></span><br><span class="line"><span class="comment"># ? What&#x27;s the description of your extension? 掘金文章管理</span></span><br><span class="line"><span class="comment"># ? Initialize a git repository? Yes</span></span><br><span class="line"><span class="comment"># ? Bundle the source code with webpack? No</span></span><br><span class="line"><span class="comment"># ? Which package manager to use? yarn</span></span><br><span class="line"></span><br><span class="line">$ code ./juejin-posts</span><br></pre></td></tr></table></figure><blockquote><p>提交记录：<a href="https://is.gd/IaJdlW">hello world</a></p></blockquote><h3 id="代码规范"><a href="#代码规范" class="headerlink" title="代码规范"></a>代码规范</h3><p>默认的脚手架生成的也有 ESLint 配置，但是 Editor、Prettier 的配置都没有，并且 ESLint 配置也不符合我的习惯。紫升关于前端工程化的包都在 <a href="https://github.com/youngjuning/luozhu">youngjuning&#x2F;luozhu</a>， ESlint 配置的包是 <code>@luozhu/eslint-config-*</code>。由于我们开发插件使用的是 Typescript，所以我们选择 <code>@luozhu/eslint-config-typescript</code>。</p><p><strong>安装依赖：</strong></p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ yarn add @luozhu/eslint-config-typescript @luozhu/prettier-config prettier -D</span><br></pre></td></tr></table></figure><p><strong>具体配置：</strong></p><p>配置涉及文件较多，请参考 <a href="https://github.com/youngjuning/luozhu#coding-style">coding-style</a>，不关心的同学也可以直接略过。</p><p><strong>提交检测：</strong></p><p>安装依赖：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ yarn add lint-staged yorkie -D</span><br></pre></td></tr></table></figure><p>修改配置：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// package.json</span></span><br><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;gitHooks&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;pre-commit&quot;</span><span class="punctuation">:</span> <span class="string">&quot;lint-staged&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;lint-staged&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;**/*.&#123;js,jsx,ts,tsx&#125;&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span><span class="string">&quot;eslint --fix&quot;</span><span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;**/*.&#123;md,json&#125;&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span><span class="string">&quot;prettier --write&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><strong>eslint –fix：</strong></p><p>修改完配置之后需要执行 fix 对所有文件格式化一次。</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ yarn lint --fix</span><br></pre></td></tr></table></figure><blockquote><p>提交记录：<a href="https://is.gd/xzFAVE">chore: code style config</a></p></blockquote><h3 id="约定式提交"><a href="#约定式提交" class="headerlink" title="约定式提交"></a>约定式提交</h3><p>约定式提交我使用的是渐进式脚手架 <code>@luozhu/create-commitlint</code>，在项目中执行 <code>npx @luozhu/create-commitlint</code> 即可使项目符合规范化提交的配置。对规范化提交不了解的同学，强烈建议读一下 <a href="https://juejin.cn/post/6877462747631026190/">一文搞定 Conventional Commits </a>。</p><blockquote><p>提交记录：<a href="https://is.gd/Ld142y">chore: npx @luozhu&#x2F;create-commitlint</a></p></blockquote><h3 id="调试"><a href="#调试" class="headerlink" title="调试"></a>调试</h3><p>按下 <code>F5</code> 开启调试会出现[扩展开发宿主]窗口，然后按 <code>Command+Shift+P</code> 组件键输入 <code>Hello World</code> 命令。如下图所示 vscode 弹出了 <code>Hello World from Juejin Posts!</code> 的提示。</p><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/bc26d7d429f84564b88b55e9df60fcd7~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><p>同时我们的开发窗口中，会出现一个 watch 任务的终端：</p><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/75a15d0bd2cc4096af1a3f76173cbbe1~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><p>开发窗口的调试控制台会输出插件运行日志（忽略红色的警告）：</p><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c31068ea0bca4d4793e35bdb16d70c0a~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><p>调试执行的任务是在 <code>.vscode/tasks.json</code> 中配置的：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// See https://go.microsoft.com/fwlink/?LinkId=733558</span></span><br><span class="line"><span class="comment">// for the documentation about the tasks.json format</span></span><br><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line"><span class="attr">&quot;version&quot;</span><span class="punctuation">:</span> <span class="string">&quot;2.0.0&quot;</span><span class="punctuation">,</span> <span class="comment">// 配置的版本号。</span></span><br><span class="line"><span class="attr">&quot;tasks&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span> <span class="comment">// 任务配置。通常是外部任务运行程序中已定义任务的扩充。</span></span><br><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line"><span class="attr">&quot;type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;npm&quot;</span><span class="punctuation">,</span> <span class="comment">// 要自定义的任务类型。</span></span><br><span class="line"><span class="attr">&quot;script&quot;</span><span class="punctuation">:</span> <span class="string">&quot;watch&quot;</span><span class="punctuation">,</span> <span class="comment">// 要自定义的 npm 脚本。</span></span><br><span class="line"><span class="attr">&quot;problemMatcher&quot;</span><span class="punctuation">:</span> <span class="string">&quot;$tsc-watch&quot;</span><span class="punctuation">,</span> <span class="comment">// 要使用的问题匹配程序。可以是一个字符串或一个问题匹配程序定义，也可以是一个字符串数组和多个问题匹配程序。</span></span><br><span class="line"><span class="attr">&quot;isBackground&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span><span class="punctuation">,</span> <span class="comment">// 执行的任务是否保持活动状态并在后台运行。</span></span><br><span class="line"><span class="attr">&quot;presentation&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span> <span class="comment">// 配置用于显示任务输出并读取其输入的面板。</span></span><br><span class="line"><span class="attr">&quot;reveal&quot;</span><span class="punctuation">:</span> <span class="string">&quot;never&quot;</span> <span class="comment">// 控制运行任务的终端是否显示。可按选项 &quot;revealProblems&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;group&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span> <span class="comment">// 定义此任务属于的执行组。它支持 &quot;build&quot; 以将其添加到生成组，也支持 &quot;test&quot; 以将其添加到测试组。</span></span><br><span class="line"><span class="attr">&quot;kind&quot;</span><span class="punctuation">:</span> <span class="string">&quot;build&quot;</span><span class="punctuation">,</span> <span class="comment">// 任务的执行组。</span></span><br><span class="line"><span class="attr">&quot;isDefault&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span> <span class="comment">// 定义此任务是否为组中的默认任务。</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><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><p>我们的插件开发完成前，想要分享给小伙伴体验可以吗？答案是肯定的，vscode 为我们提供了 <a href="https://github.com/microsoft/vscode-vsce">vsce</a> 实现这个需求，我们将 vsce 模块安装到全局，然后使用 <code>vsce package</code> 命令尝试打包：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">$ vsce package</span><br><span class="line"> ERROR  Missing publisher name. Learn more: https://code.visualstudio.com/api/working-with-extensions/publishing-extension<span class="comment">#publishing-extensions</span></span><br></pre></td></tr></table></figure><p>啊，咋还报错了？<code>publisher</code> 是啥？？一脸懵逼。不慌，按<a href="https://code.visualstudio.com/api/working-with-extensions/publishing-extension#create-a-publisher">链接</a> 我知道了 publisher 是一个可以将扩展发布到Visual Studio Code Marketplace 的身份。每个扩展都需要在其 <code>package.json</code> 文件中包含一个发布者名称。如果注册发布者我们后面详说，这里我们把 <code>publisher</code> 设置为 <code>luozhu</code>。</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">$ vsce package</span><br><span class="line"> INFO  Detected presence of yarn.lock. Using <span class="string">&#x27;yarn&#x27;</span> instead of <span class="string">&#x27;npm&#x27;</span> (to override this pass <span class="string">&#x27;--no-yarn&#x27;</span> on the <span class="built_in">command</span> line).</span><br><span class="line"> ERROR  Make sure to edit the README.md file before you package or publish your extension.</span><br></pre></td></tr></table></figure><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/cf561381232b4690a5d7cd0378c267d7~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><p>额，裂开，这咋还报错，假装淡定，读一下提示原来是要我们编辑一下 README.md，没错，vscode 模板里有初始的 README，我们需要编辑一下才可以打包。修改后再次尝试 <code>vsce package</code>：</p><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/acfe2e957c9545578c7b6990371a3f0b~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><p>终于，打包成功！为了追求完美，最后我们再来做一些优化工作：</p><ol><li>执行 <code>vsce package</code> 的时候加上 <code>--no-yarn</code></li><li>在 <em>package.json</em> 中加上 <code>repository</code> 字段即可看不到任何警告。</li><li>为了便捷，我们将 vsce 安装到项目中，然后把 <code>vsce package --no-yarn</code> 添加到 npm scripts 中。</li><li><em>package.json</em> 加上 <code>license</code> 字段。</li></ol><p>然后再次尝试 <code>yarn package</code> 就完美了：</p><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/4a9a251834b945c8a882039020f56e4f~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><blockquote><p>提示：vsce package 会先执行 <code>vscode:prepublish</code> 这个预发布脚本去编译项目。</p></blockquote><blockquote><p>提交记录：<a href="https://is.gd/ZCp4qU">chore: config vsce package</a></p></blockquote><h3 id="打包原理"><a href="#打包原理" class="headerlink" title="打包原理"></a>打包原理</h3><p>如过你也跟着一路敲到了这里，此时你会在项目根目录发现 <code>vsix</code> 结尾的文件：</p><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f2c0846bd66747ceab0c09524a209b28~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><p>这就是 vscode 插件的安装包，我们先不急着安装，先一起来看一下这个文件是个什么东西。尝试用归档工具解压后得到如下目录文件夹：</p><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/182c365a0690467cb0bd8b0d1490c175~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><p>我们可以看到编译后的文件夹 <code>out</code> 和其他一些文件是被直接压缩进安装包的，聪明的你肯定发现了 <code>.cz-config.js</code>、<code>.prettierrc.js</code> 和 <code>commitlint.config.js</code> 这种开发时文件也被压缩了，运行插件完全用不到，这明显不合理。其实和其他插件体系一样，vscode 也提供了 <code>.vscodeignore</code> 来实现打包忽略配置，我们将以上无关文件忽略重新打包即可。</p><p>原理就这？不存在的，我们打开 <code>extension.js</code> 会发现引用了 <code>vscode</code> 这个包：</p><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/bb42881e49a94456aeac417f6b5cba71~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><p>但是我们的安装包中并没有 <em>node_modules</em>，那么 vscode 这个包存在在哪里呢？我猜的是挂在 node 环境上了，读了<a href="https://is.gd/33GTcH">源码</a>后我发现我竟然是对的：</p><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5e796fcf81b64fd7a18d9c3e36fbefdf~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><p>vscode 实现了拦截器在加载 Node 环境的时候将 vscode 给添加到了内置包中，这样的好处是减小插件的体积。</p><p>那么我们如果使用三方插件呢？以常用的 lodash 为例，安装 lodash 之后重新打包：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">$ yarn package</span><br><span class="line">yarn run v1.22.10</span><br><span class="line">$ vsce package --no-yarn</span><br><span class="line">Executing prepublish script <span class="string">&#x27;npm run vscode:prepublish&#x27;</span>...</span><br><span class="line"></span><br><span class="line">&gt; juejin-posts@0.0.1 vscode:prepublish</span><br><span class="line">&gt; yarn run compile</span><br><span class="line"></span><br><span class="line">$ tsc -p ./</span><br><span class="line">This extension consists of 1060 files, out of <span class="built_in">which</span> 1049 are JavaScript files. For performance reasons, you should bundle your extension: https://aka.ms/vscode-bundle-extension . You should also exclude unnecessary files by adding them to your .vscodeignore: https://aka.ms/vscode-vscodeignore</span><br><span class="line"> DONE  Packaged: /Users/luozhu/Desktop/playground/juejin-posts/juejin-posts-0.0.1.vsix (1060 files, 644.72KB)</span><br><span class="line">✨  Done <span class="keyword">in</span> 5.54s.</span><br></pre></td></tr></table></figure><p>这个时候提示我们有 1000 多个文件，大概率 <em>node_modules</em> 文件夹被打包了，我们来解压下见证一下：</p><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/917b7742991640c9a0b03314cef24ce6~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><p>不出所料，vscode 默认的打包方式就是简单的编译拷贝，通过忽略文件减小体积也是杯水车薪。而且 vscode 扩展的规模往往增长很快。它们是在多个源文件中编写的，并依赖于 npm 的模块。分解和重用是开发的最佳实践，但在安装和运行扩展时，它们是有代价的。加载 100 个小文件要比加载一个大文件慢得多。这就是我们推荐捆绑的原因。捆绑是将多个小的源文件合并成一个文件的过程。</p><p>在 JavaScript 中，有不同的打包工具可以用，流行的有 rollup.js、Parcel、esbuild 和 webpack，官方脚手架默认只能选 webpack，我们这里推荐直接使用更快更强的 esbuild。</p><blockquote><p>提交记录：<a href="https://is.gd/ZCp4qU">chore: ignore config file when package</a>、<a href="https://is.gd/ggpQmv">chore: add esModuleInterop to tsconfig</a></p></blockquote><h3 id="使用-esbuild-优化打包"><a href="#使用-esbuild-优化打包" class="headerlink" title="使用 esbuild 优化打包"></a>使用 esbuild 优化打包</h3><p><strong>安装依赖：</strong></p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ yarn add -D esbuild</span><br></pre></td></tr></table></figure><p><strong>npm scripts：</strong></p><figure class="highlight diff"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">&quot;scripts&quot;: &#123;</span><br><span class="line"><span class="deletion">-    &quot;vscode:prepublish&quot;: &quot;yarn run compile&quot;,</span></span><br><span class="line"><span class="deletion">-    &quot;compile&quot;: &quot;tsc -p ./&quot;,</span></span><br><span class="line"><span class="deletion">-    &quot;watch&quot;: &quot;tsc -watch -p ./&quot;,</span></span><br><span class="line"><span class="deletion">-    &quot;pretest&quot;: &quot;yarn run compile &amp;&amp; yarn run lint&quot;,</span></span><br><span class="line"><span class="addition">+    &quot;vscode:prepublish&quot;: &quot;yarn esbuild-base --minify&quot;,</span></span><br><span class="line"><span class="addition">+    &quot;esbuild-base&quot;: &quot;esbuild ./src/extension.ts --bundle --outfile=out/extension.js --external:vscode --format=cjs --platform=node&quot;,</span></span><br><span class="line"><span class="addition">+    &quot;esbuild&quot;: &quot;yarn esbuild-base --sourcemap&quot;,</span></span><br><span class="line"><span class="addition">+    &quot;esbuild-watch&quot;: &quot;yarn esbuild-base --sourcemap --watch&quot;,</span></span><br><span class="line"><span class="addition">+    &quot;test-compile&quot;: &quot;tsc -p ./&quot;,</span></span><br><span class="line"><span class="addition">+    &quot;pretest&quot;: &quot;yarn test-compile &amp;&amp; yarn lint&quot;,</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><blockquote><p>注意：由于 watch 改成了 esbuild-watch，所以 <em>.vscode&#x2F;tasks.json</em> 中的 scripts 子段也需要做相应修改。</p></blockquote><p><strong>vscode tasks：</strong></p><p>理论上我们把打包命令改成 esbuild 之后，应该将 vscode 任务中的问题匹配程序设置为 <code>$esbuild-watch</code>，但是 vscode 会提示我们无法识别的问题匹配程序：</p><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/4b51c79ee1344b6f92a3f2efaf9ef7aa~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><p>尝试搜索扩展，果然有一个 esbuild Problem Matchers 插件，我们将其安装并添加 <code>&quot;connor4312.esbuild-problem-matchers&quot;</code> 到 <em>.vscode&#x2F;extensions.json</em> 文件的 <code>recommendations</code> 中。</p><p><strong>忽略文件：</strong></p><p>我们使用 esbuild 打包后会将使用到的代码都打包进 <code>out/extension.js</code>，但是 vsce 的打包机制是不管你有没有用到都会把 <code>dependencies</code> 中的包打进安装包中，所以我们需要将 <em><em>node_modules</em></em> 忽略掉。</p><p><strong>成果展示：</strong></p><p>从图中我们可以看到，安装包的体积大大减小了。</p><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ba86286be7fc41f4b8325a3e0be0d2d2~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><blockquote><p>提交记录：<a href="https://is.gd/F12xNk">chore: config esbuild</a></p></blockquote><h2 id="集成-umijs"><a href="#集成-umijs" class="headerlink" title="集成 umijs"></a>集成 umijs</h2><h3 id="初始化-umi-项目"><a href="#初始化-umi-项目" class="headerlink" title="初始化 umi 项目"></a>初始化 umi 项目</h3><p>使用 umi 脚手架在根目录新建一个 <em>web</em> 目录。</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ <span class="built_in">mkdir</span> web &amp;&amp; <span class="built_in">cd</span> web</span><br></pre></td></tr></table></figure><p>通过官方工具创建项目：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ yarn create @umijs/umi-app</span><br></pre></td></tr></table></figure><p>修改 <em>.umirc.ts</em> 配置：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; defineConfig, <span class="title class_">IConfig</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;umi&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="title function_">defineConfig</span>(&#123;</span><br><span class="line">  <span class="attr">nodeModulesTransform</span>: &#123;</span><br><span class="line">    <span class="attr">type</span>: <span class="string">&#x27;none&#x27;</span>,</span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="attr">routes</span>: [&#123; <span class="attr">path</span>: <span class="string">&#x27;/&#x27;</span>, <span class="attr">component</span>: <span class="string">&#x27;@/pages/index&#x27;</span> &#125;],</span><br><span class="line">  <span class="attr">fastRefresh</span>: &#123;&#125;, <span class="comment">// 开发时可以保持组件状态，同时编辑提供即时反馈。</span></span><br><span class="line">  <span class="attr">history</span>: &#123;</span><br><span class="line">    <span class="attr">type</span>: <span class="string">&#x27;memory&#x27;</span>, <span class="comment">// 默认的类型是 `browser`，但是由于 vscode webview 环境不存在浏览器路由，改成 `memory` 和 `hash` 都可以</span></span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="attr">devServer</span>: &#123;</span><br><span class="line">    <span class="comment">// 需要在 dev 时写文件到输出目录，这样保证开发阶段有 js/css 文件</span></span><br><span class="line">    <span class="attr">writeToDisk</span>: <span class="function"><span class="params">filePath</span> =&gt;</span></span><br><span class="line">      [<span class="string">&#x27;umi.js&#x27;</span>, <span class="string">&#x27;umi.css&#x27;</span>].<span class="title function_">some</span>(<span class="function"><span class="params">name</span> =&gt;</span> filePath.<span class="title function_">endsWith</span>(name)),</span><br><span class="line">  &#125;,</span><br><span class="line">&#125; <span class="keyword">as</span> <span class="title class_">IConfig</span>);</span><br></pre></td></tr></table></figure><p>修改 <code>package.json</code> 加入 <code>name</code>、<code>version</code>、<code>description</code>：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><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;web&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;0.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;web for juejin-posts&quot;</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><p><strong>.gitignore：</strong></p><p>将 vscode 扩展和 umijs 脚手架生成的 gitignore 合并为一下内容：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"># See https://help.github.com/articles/ignoring-files/ for more about ignoring files.</span><br><span class="line"></span><br><span class="line"># vscode</span><br><span class="line">.vscode-test/</span><br><span class="line">*.vsix</span><br><span class="line"></span><br><span class="line"># dependencies</span><br><span class="line">node_modules</span><br><span class="line">npm-debug.log</span><br><span class="line">yarn-error.log</span><br><span class="line">package-lock.json</span><br><span class="line"></span><br><span class="line"># production</span><br><span class="line">out</span><br><span class="line">dist</span><br><span class="line"></span><br><span class="line"># misc</span><br><span class="line">.DS_Store</span><br><span class="line"></span><br><span class="line"># umi</span><br><span class="line">**/src/.umi</span><br><span class="line">**/src/.umi-production</span><br><span class="line">**/src/.umi-test</span><br><span class="line">**/.env.local</span><br><span class="line">web/yarn.lock</span><br></pre></td></tr></table></figure><p><strong>.vscodeignore：</strong></p><p>由于 vscode 打包的时候只需要获取 umijs 打包后的产物，所有加入 <code>web/**</code> 和 <code>!web/dist/**</code> 将无用的文件忽略掉。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line">.vscode/**</span><br><span class="line">.vscode-test/**</span><br><span class="line">out/test/**</span><br><span class="line"></span><br><span class="line">src/**</span><br><span class="line">.gitignore</span><br><span class="line">.yarnrc</span><br><span class="line">vsc-extension-quickstart.md</span><br><span class="line">**/tsconfig.json</span><br><span class="line">**/*.map</span><br><span class="line">**/*.ts</span><br><span class="line"></span><br><span class="line">.cz-config.js</span><br><span class="line">.prettierrc.js</span><br><span class="line">.commitlintrc.js</span><br><span class="line">**/node_modules/**</span><br><span class="line">yarn-error.log</span><br><span class="line">web/**</span><br><span class="line">!web/dist/**</span><br></pre></td></tr></table></figure><h3 id="yarn-workspace"><a href="#yarn-workspace" class="headerlink" title="yarn workspace"></a>yarn workspace</h3><p>由于我们的项目是 vscode 扩展和 web 项目混合的项目。为了方便管理脚本和依赖，我们引入了 <code>yarn workspace</code> 来管理项目。在根目录的 <em>package.json</em> 中加入以下配置即可：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;private&quot;</span><span class="punctuation">:</span> <span class="string">&quot;true&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;workspaces&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span><span class="string">&quot;web&quot;</span><span class="punctuation">]</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><h3 id="调试-1"><a href="#调试-1" class="headerlink" title="调试"></a>调试</h3><p>由于我们的 web 项目也需要编译，所以我们需要修改一下 vscode <code>launch.json</code> 加入 web 项目的编译任务。配置参考了 <a href="http://tny.im/bOqQT">appworks</a>。</p><p>首先在根目录的 <code>package.json</code> 的 scripts 中添加:</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><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;web-build&quot;</span><span class="punctuation">:</span> <span class="string">&quot;yarn workspace web run build&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;web-watch&quot;</span><span class="punctuation">:</span> <span class="string">&quot;yarn workspace web run start&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></pre></td></tr></table></figure><p>然后修改 <em>.vscode&#x2F;launch.json</em> 配置为：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// A launch configuration that compiles the extension and then opens it inside a new window</span></span><br><span class="line"><span class="comment">// Use IntelliSense to learn about possible attributes.</span></span><br><span class="line"><span class="comment">// Hover to view descriptions of existing attributes.</span></span><br><span class="line"><span class="comment">// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387</span></span><br><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;version&quot;</span><span class="punctuation">:</span> <span class="string">&quot;0.2.0&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;compounds&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">    <span class="comment">// 复合列表。每个复合可引用多个配置，这些配置将一起启动。</span></span><br><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;Debug Extension&quot;</span><span class="punctuation">,</span> <span class="comment">// 复合的名称。在启动配置下拉菜单中显示。</span></span><br><span class="line">      <span class="attr">&quot;configurations&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">        <span class="comment">// 将作为此复合的一部分启动的配置名称。</span></span><br><span class="line">        <span class="string">&quot;Run Extension&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="string">&quot;Watch Webview&quot;</span></span><br><span class="line">      <span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;presentation&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">        <span class="attr">&quot;order&quot;</span><span class="punctuation">:</span> <span class="number">0</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><span class="line">  <span class="attr">&quot;configurations&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;name&quot;</span><span class="punctuation">:</span> <span class="string">&quot;Watch Webview&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;request&quot;</span><span class="punctuation">:</span> <span class="string">&quot;attach&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;node&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;preLaunchTask&quot;</span><span class="punctuation">:</span> <span class="string">&quot;npm: web-watch&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;name&quot;</span><span class="punctuation">:</span> <span class="string">&quot;Run Extension&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;extensionHost&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;request&quot;</span><span class="punctuation">:</span> <span class="string">&quot;launch&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;--extensionDevelopmentPath=$&#123;workspaceFolder&#125;&quot;</span><span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;outFiles&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span><span class="string">&quot;$&#123;workspaceFolder&#125;/out/**/*.js&quot;</span><span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;preLaunchTask&quot;</span><span class="punctuation">:</span> <span class="string">&quot;$&#123;defaultBuildTask&#125;&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><br></pre></td></tr></table></figure><p>完成后进入 VS Code，按下<code>F5</code>，你会立即看到一个<strong>插件发开主机</strong>窗口，其中就运行着插件。这时候运行你会发现控制台报一下错误 ❌：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">error TS6059: File &#x27;/Users/luozhu/Desktop/github/juejin-posts/web/src/pages/index.tsx&#x27; is not under &#x27;rootDir&#x27; &#x27;/Users/luozhu/Desktop/github/juejin-posts/src&#x27;. &#x27;rootDir&#x27; is expected to contain all source files.</span><br><span class="line">  The file is in the program because:</span><br><span class="line">    Matched by include pattern &#x27;**/*&#x27; in &#x27;/Users/luozhu/Desktop/github/juejin-posts/tsconfig.json&#x27;</span><br></pre></td></tr></table></figure><p>原因是因为 umi 的约定的项目结构和 vscode extension 都包含 <em>src</em> 目录。由于 vscode 插件和 umi 的编译是分开的，我们在根目录的 <em>tsconfig.json</em> 中将 <em>web</em> 目录忽略即可：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;exclude&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span><span class="string">&quot;web&quot;</span><span class="punctuation">]</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p>现在，你可以按下 <code>F5</code> 看到<strong>插件发开主机</strong>窗口的同时还会看到两个调试任务：</p><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d4b3b6f2522643fb9bc574e2f9982775~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><blockquote><p>注意📢：请选择 Debug Extension 调试任务而不是 Run Extension</p></blockquote><h3 id="其他优化工作"><a href="#其他优化工作" class="headerlink" title="其他优化工作"></a>其他优化工作</h3><ol><li>由于基于 yarn workspace，我们把公用的依赖合并</li><li>合并 Eslint 配置并使用 <code>@luozhu/eslint-config-react-typescrip</code></li><li>合并 Editorconfig 和 Prettier 配置</li><li>添加 <code>prestart</code> 和 <code>prebuild</code> script</li><li>设置 <code>HTML=none umi build</code></li></ol><blockquote><p>提交记录：<a href="https://is.gd/bt3WHr">chore: config umijs</a></p></blockquote><h2 id="vscode-插件开发核心概念"><a href="#vscode-插件开发核心概念" class="headerlink" title="vscode 插件开发核心概念"></a>vscode 插件开发核心概念</h2><p>在开始 webview 能力开发之前，我们有必要了解一下 vscode 插件开发的核心概念。为了有个全局的理解，我们先来看下我们现在项目的主要目录结构：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">.</span><br><span class="line">├── CHANGELOG.md <span class="comment"># 基于 standard-version 生成的更新日志文件</span></span><br><span class="line">├── README.md</span><br><span class="line">├── package.json <span class="comment"># vscode 包配置文件，诸如插件 LOGO、名字、描述、注册激活事件</span></span><br><span class="line">├── src</span><br><span class="line">│   └── extension.ts <span class="comment"># 插件入口文件，暴露 activate 方法用于注册命令和初始化一些配置，暴露 deactivate 方法用于插件关闭前执行清理工作</span></span><br><span class="line">├── tsconfig.json <span class="comment"># vscode 的编译配置</span></span><br><span class="line">├── web <span class="comment"># 基于 umi 的 web，也是我们后边 webview 要承载的内容</span></span><br><span class="line">└── yarn.lock</span><br></pre></td></tr></table></figure><p>从目录结构可以看出，关键的文件是 <code>package.json</code> 和 <code>extension.ts</code>，我们以 helloWorld 命令为例介绍下 vscode 插件的三个核心概念。</p><h3 id="1-激活事件"><a href="#1-激活事件" class="headerlink" title="1. 激活事件"></a>1. 激活事件</h3><p><strong>激活事件</strong>是在 <code>package.json</code> 中的 <code>activationEvents</code> 字段声明的一个 JSON 数组对象。为了注册 helloWorld 这个命令，第一步就是注册激活事件，激活事件类型有很多，注册命令的激活事件是 <code>onCommand</code>:</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;activationEvents&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span><span class="string">&quot;onCommand:juejin-posts.helloWorld&quot;</span><span class="punctuation">]</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><h3 id="2-发布内容配置"><a href="#2-发布内容配置" class="headerlink" title="2. 发布内容配置"></a>2. 发布内容配置</h3><p>发布内容配置（ 即 VS Code 为插件扩展提供的配置项）是 <code>package.json</code> 的 <code>contributes</code> 字段，你可以在其中注册各种配置项扩展 VS Code 的能力。上一步我们注册的 helloWorld 激活事件只是告诉了 vscode 可以通过 <code>juejin-posts.helloWorld</code> 命令触发。我们还需要再 <code>contributes.commands</code> 中注册我们的 <code>juejin-posts.helloWorld</code> 命令：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;contributes&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;commands&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;command&quot;</span><span class="punctuation">:</span> <span class="string">&quot;juejin-posts.helloWorld&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;title&quot;</span><span class="punctuation">:</span> <span class="string">&quot;Hello World&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><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><h3 id="3-VS-Code-API"><a href="#3-VS-Code-API" class="headerlink" title="3. VS Code API"></a>3. VS Code API</h3><p><strong>VS Code API</strong> 是 VS Code 提供给插件使用的一系列 Javascript API。通过前两个核心概念的能力，我们已经注册好了命令和事件，那么下一步必然就是注册事件回调。事件回调在 vscode 中是通过 <code>vscode.commands.registerCommand</code> 函数来注册的，下面 👇🏻 是我们在入口文件 <code>src/extension.ts</code> 中注册 <code>juejin-posts.helloWorld</code> 命令。</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// vscode 这个模块包含了 VS Code 扩展的 API</span></span><br><span class="line"><span class="keyword">import</span> vscode <span class="keyword">from</span> <span class="string">&#x27;vscode&#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">function</span> <span class="title function_">activate</span>(<span class="params">context: vscode.ExtensionContext</span>) &#123;</span><br><span class="line">  <span class="comment">// 当你的扩展被激活时，这行代码将只被执行一次</span></span><br><span class="line">  <span class="comment">//</span></span><br><span class="line">  <span class="comment">// 使用 console.log 输出日志信息或使用 console.error 输出错误信息。</span></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;Congratulations, your extension &quot;juejin-posts&quot; is now active!&#x27;</span>);</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 入口命令已经在 package.json 文件中定义好了，现在调用 registerCommand 方法</span></span><br><span class="line">  <span class="comment">// registerCommand 中的参数必须与 package.json 中的 command 保持一致</span></span><br><span class="line">  <span class="keyword">const</span> disposable = vscode.<span class="property">commands</span>.<span class="title function_">registerCommand</span>(<span class="string">&#x27;juejin-posts.helloWorld&#x27;</span>, <span class="function">() =&gt;</span> &#123;</span><br><span class="line">    <span class="comment">// 把你的代码写在这里，每次命令执行时都会调用这里的代码</span></span><br><span class="line">    <span class="comment">// 给用户显示一个消息提示</span></span><br><span class="line">    vscode.<span class="property">window</span>.<span class="title function_">showInformationMessage</span>(<span class="string">&#x27;Hello World from Juejin Posts!&#x27;</span>);</span><br><span class="line">  &#125;);</span><br><span class="line"></span><br><span class="line">  context.<span class="property">subscriptions</span>.<span class="title function_">push</span>(disposable);</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">function</span> <span class="title function_">deactivate</span>(<span class="params"></span>) &#123;&#125;</span><br></pre></td></tr></table></figure><h2 id="集成-webview"><a href="#集成-webview" class="headerlink" title="集成 webview"></a>集成 webview</h2><h3 id="注册命令"><a href="#注册命令" class="headerlink" title="注册命令"></a>注册命令</h3><p>1、<em>package.json</em> 激活事件（<code>activationEvents</code>）中添加 <code>&quot;onCommand:juejin-posts.start&quot;</code></p><p>2、<em>package.json</em> 命令（<code>commands</code>）中添加：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><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;juejin-posts.start&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;title&quot;</span><span class="punctuation">:</span> <span class="string">&quot;start&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;category&quot;</span><span class="punctuation">:</span> <span class="string">&quot;Juejin Posts&quot;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p>3、<em>src&#x2F;extension.ts</em> 中注册命令</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">context.<span class="property">subscriptions</span>.<span class="title function_">push</span>(</span><br><span class="line">  vscode.<span class="property">commands</span>.<span class="title function_">registerCommand</span>(<span class="string">&#x27;juejin-posts.start&#x27;</span>, <span class="function">() =&gt;</span> &#123;</span><br><span class="line">    <span class="comment">// Truth is endless. Keep coding...</span></span><br><span class="line">  &#125;)</span><br><span class="line">)</span><br></pre></td></tr></table></figure><h3 id="创建-webview-面板"><a href="#创建-webview-面板" class="headerlink" title="创建 webview 面板"></a>创建 webview 面板</h3><h4 id="创建一个空白的面板"><a href="#创建一个空白的面板" class="headerlink" title="创建一个空白的面板"></a>创建一个空白的面板</h4><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> vscode <span class="keyword">from</span> <span class="string">&#x27;vscode&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 创建并显示新的webview</span></span><br><span class="line"><span class="keyword">const</span> panel = vscode.<span class="property">window</span>.<span class="title function_">createWebviewPanel</span>(</span><br><span class="line">  <span class="string">&#x27;juejin-posts&#x27;</span>, <span class="comment">// 只供内部使用，这个 webview 的标识</span></span><br><span class="line">  <span class="string">&#x27;Juejin Posts&#x27;</span>, <span class="comment">// 给用户显示的面板标题</span></span><br><span class="line">  vscode.<span class="property">vscode</span>.<span class="property">ViewColumn</span>.<span class="property">One</span>, <span class="comment">// 给新的 webview 面板一个编辑器视图</span></span><br><span class="line">  &#123;</span><br><span class="line">    <span class="attr">enableScripts</span>: <span class="literal">true</span>, <span class="comment">// 启用 javascript 脚本</span></span><br><span class="line">    <span class="attr">retainContextWhenHidden</span>: <span class="literal">true</span>, <span class="comment">// 隐藏时保留上下文</span></span><br><span class="line">  &#125; <span class="comment">// webview 面板的内容配置</span></span><br><span class="line">);</span><br></pre></td></tr></table></figure><p>我们使用了 <a href="https://vscode-api.js.org/modules/window.html#createWebviewPanel">window.createWebviewPanel</a> API 创建了一个 webview 面板，现在我们尝试运行 <code>juejin-posts.start</code> 就可以打开一个 webview 面板：</p><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d85ad985232542f587376e5073feb1a1~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><h4 id="给面板设置内容"><a href="#给面板设置内容" class="headerlink" title="给面板设置内容"></a>给面板设置内容</h4><p>上面我们创建了一个空白的面板，那么我们如何给面板添加内容呢？我们可以使用 <code>panel.webview.html</code> 来设置 HTML 内容：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">getWebviewContent</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="string">`</span></span><br><span class="line"><span class="string">    &lt;!DOCTYPE html&gt;</span></span><br><span class="line"><span class="string">    &lt;html lang=&quot;en&quot;&gt;</span></span><br><span class="line"><span class="string">    &lt;head&gt;</span></span><br><span class="line"><span class="string">        &lt;meta charset=&quot;UTF-8&quot;&gt;</span></span><br><span class="line"><span class="string">        &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;&gt;</span></span><br><span class="line"><span class="string">        &lt;title&gt;Juejin Posts&lt;/title&gt;</span></span><br><span class="line"><span class="string">        &lt;style&gt;</span></span><br><span class="line"><span class="string">          html, body &#123;</span></span><br><span class="line"><span class="string">            padding: 0px;</span></span><br><span class="line"><span class="string">            height: 100vh;</span></span><br><span class="line"><span class="string">            position: relative;</span></span><br><span class="line"><span class="string">            margin: 0;</span></span><br><span class="line"><span class="string">            padding: 0;</span></span><br><span class="line"><span class="string">            overflow: hidden;</span></span><br><span class="line"><span class="string">          &#125;</span></span><br><span class="line"><span class="string">          #yoyo &#123;</span></span><br><span class="line"><span class="string">            position: absolute;</span></span><br><span class="line"><span class="string">            bottom: 50px;</span></span><br><span class="line"><span class="string">            right: -90px;</span></span><br><span class="line"><span class="string">            opacity: 0;</span></span><br><span class="line"><span class="string">            transition: .25s ease-in-out</span></span><br><span class="line"><span class="string">          &#125;</span></span><br><span class="line"><span class="string">          #yoyo:hover &#123;</span></span><br><span class="line"><span class="string">            opacity: 1;</span></span><br><span class="line"><span class="string">            right: 0;</span></span><br><span class="line"><span class="string">          &#125;</span></span><br><span class="line"><span class="string">        &lt;/style&gt;</span></span><br><span class="line"><span class="string">    &lt;/head&gt;</span></span><br><span class="line"><span class="string">    &lt;body&gt;</span></span><br><span class="line"><span class="string">      &lt;a href=&quot;https://juejin.cn&quot;&gt;&lt;img id=&quot;yoyo&quot; src=&quot;https://cdn.jsdelivr.net/gh/youngjuning/images/20210817163229.png&quot; width=&quot;100&quot; /&gt;&lt;/a&gt;</span></span><br><span class="line"><span class="string">    &lt;/body&gt;</span></span><br><span class="line"><span class="string">    &lt;/html&gt;</span></span><br><span class="line"><span class="string">  `</span>;</span><br><span class="line">&#125;</span><br><span class="line">...</span><br><span class="line"><span class="comment">// 给 webview panel 设置 HTML 内容</span></span><br><span class="line">panel.<span class="property">webview</span>.<span class="property">html</span> = <span class="title function_">getWebviewContent</span>();</span><br><span class="line">...</span><br></pre></td></tr></table></figure><p>重新使用 <code>juejin-posts.start</code> 命令就可以调戏悠悠船长了：</p><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5160d0c76536441b8d1ac3cfcedc0d0e~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><h4 id="限制-webview-视图为一个"><a href="#限制-webview-视图为一个" class="headerlink" title="限制 webview 视图为一个"></a>限制 webview 视图为一个</h4><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">function</span> <span class="title function_">activate</span>(<span class="params">context: vscode.ExtensionContext</span>) &#123;</span><br><span class="line">  <span class="comment">// 追踪当前 webview 面板</span></span><br><span class="line">  <span class="keyword">let</span> <span class="attr">currentPanel</span>: vscode.<span class="property">WebviewPanel</span> | <span class="literal">undefined</span> = <span class="literal">undefined</span>;</span><br><span class="line"></span><br><span class="line">  context.<span class="property">subscriptions</span>.<span class="title function_">push</span>(</span><br><span class="line">    vscode.<span class="property">commands</span>.<span class="title function_">registerCommand</span>(<span class="string">&#x27;juejin-posts.start&#x27;</span>, <span class="function">() =&gt;</span> &#123;</span><br><span class="line">      <span class="comment">// 获取当前活动的编辑器</span></span><br><span class="line">      <span class="keyword">const</span> columnToShowIn = vscode.<span class="property">window</span>.<span class="property">activeTextEditor</span></span><br><span class="line">        ? vscode.<span class="property">window</span>.<span class="property">activeTextEditor</span>.<span class="property">viewColumn</span></span><br><span class="line">        : <span class="literal">undefined</span>;</span><br><span class="line"></span><br><span class="line">      <span class="keyword">if</span> (currentPanel) &#123;</span><br><span class="line">        <span class="comment">// 如果我们已经有了一个面板，那就把它显示到目标列布局中</span></span><br><span class="line">        currentPanel.<span class="title function_">reveal</span>(columnToShowIn);</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">        currentPanel = vscode.<span class="property">window</span>.<span class="title function_">createWebviewPanel</span>();</span><br><span class="line">        <span class="comment">// 当前面板被关闭后重置</span></span><br><span class="line">        currentPanel.<span class="title function_">onDidDispose</span>(</span><br><span class="line">          <span class="function">() =&gt;</span> &#123;</span><br><span class="line">            currentPanel = <span class="literal">undefined</span>;</span><br><span class="line">          &#125;,</span><br><span class="line">          <span class="literal">null</span>,</span><br><span class="line">          context.<span class="property">subscriptions</span></span><br><span class="line">        );</span><br><span class="line">      &#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><ul><li><a href="https://vscode-api-cn.js.org/modules/window.html#activeTextEditor">vscode.window.activeTextEditor</a>：获取当前活动的文本编辑器</li><li><a href="https://vscode-api-cn.js.org/interfaces/WebviewPanel.html#reveal">currentPanel.reveal()</a>：调用 <code>reveal()</code> 或者拖动 webview 面板到新的编辑布局中去。</li></ul><h4 id="设置-Icon"><a href="#设置-Icon" class="headerlink" title="设置 Icon"></a>设置 Icon</h4><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 设置 Logo</span></span><br><span class="line">panel.<span class="property">iconPath</span> = vscode.<span class="property">Uri</span>.<span class="title function_">file</span>(</span><br><span class="line">  path.<span class="title function_">join</span>(context.<span class="property">extensionPath</span>, <span class="string">&#x27;assets&#x27;</span>, <span class="string">&#x27;icon-juejin.png&#x27;</span>)</span><br><span class="line">);</span><br></pre></td></tr></table></figure><p>在 vscode 扩展中我们需要通过 <code>vscode.Uri.file</code> 方法获取磁盘上的资源路径。</p><h4 id="webview-获取内容的-Uri"><a href="#webview-获取内容的-Uri" class="headerlink" title="webview 获取内容的 Uri"></a>webview 获取内容的 Uri</h4><p>你应该使用 <code>asWebviewUri</code> 管理插件资源。不要硬编码 <code>vscode-resource://</code>，而是使用 <code>asWebviewUri</code> 确保你的插件在云端环境也能正常运行。</p><p>在 <a href="http://tny.im/jb4go">@luozhu&#x2F;vscode-utils</a> 中我们对获取本地资源路径做了封装：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 获取内容的 Uri</span></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">getDiskPath</span> = (<span class="params">fileName: <span class="built_in">string</span></span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">return</span> webviewPanel.<span class="property">webview</span>.<span class="title function_">asWebviewUri</span>(</span><br><span class="line">    vscode.<span class="property">Uri</span>.<span class="title function_">file</span>(path.<span class="title function_">join</span>(context.<span class="property">extensionPath</span>, rootPath, <span class="string">&#x27;dist&#x27;</span>, fileName))</span><br><span class="line">  );</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><h3 id="使用-umi-开发-webview"><a href="#使用-umi-开发-webview" class="headerlink" title="使用 umi 开发 webview"></a>使用 umi 开发 webview</h3><p>上一节我们通过调戏悠悠船长熟悉了 webview 面板的创建，这一节我们来看下如何使用 umijs 来代替 HTML 的内容。</p><p><code>panel.webview.html</code> 中的内容其实就是正常的 HTML+JavaScript+CSS 代码。你可以使用任何前端技术去编写它的内容，比如 jquery、bootstrap、Vue 以及 React。虽然本文的例子是基于 umijs 开发 webview 的内容，但是其他技术原理是一样的，紫升在后续也会提供多个技术的 vscode webview 开发脚手架。</p><h4 id="封装获取-umijs-打包产物的方法"><a href="#封装获取-umijs-打包产物的方法" class="headerlink" title="封装获取 umijs 打包产物的方法"></a>封装获取 umijs 打包产物的方法</h4><p>我们知道 <code>umi build</code> 命令会在 <em>web&#x2F;dist</em> 产生 index.html、umi.js、umi.css 三个文件，我们根据 index.html 改造前面的 getWebviewContent 方法如下：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> vscode <span class="keyword">from</span> <span class="string">&#x27;vscode&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> path <span class="keyword">from</span> <span class="string">&#x27;path&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 获取基于 umijs 的 webview 内容</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> context 扩展上下文</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> webviewPanel webview 面板对象</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> rootPath webview 所在路径，默认 web</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> umiVersion umi 版本</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@returns</span> <span class="variable">string</span></span></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_">getUmiContent</span> = (<span class="params"></span></span><br><span class="line"><span class="params">  context: vscode.ExtensionContext,</span></span><br><span class="line"><span class="params">  webviewPanel: vscode.WebviewPanel,</span></span><br><span class="line"><span class="params">  umiVersion?: <span class="built_in">string</span>,</span></span><br><span class="line"><span class="params">  rootPath = <span class="string">&#x27;web&#x27;</span></span></span><br><span class="line"><span class="params"></span>) =&gt; &#123;</span><br><span class="line">  <span class="comment">// 获取磁盘上的资源路径</span></span><br><span class="line">  <span class="keyword">const</span> <span class="title function_">getDiskPath</span> = (<span class="params">fileName: <span class="built_in">string</span></span>) =&gt; &#123;</span><br><span class="line">    <span class="keyword">return</span> webviewPanel.<span class="property">webview</span>.<span class="title function_">asWebviewUri</span>(</span><br><span class="line">      vscode.<span class="property">Uri</span>.<span class="title function_">file</span>(path.<span class="title function_">join</span>(context.<span class="property">extensionPath</span>, rootPath, <span class="string">&#x27;dist&#x27;</span>, fileName))</span><br><span class="line">    );</span><br><span class="line">  &#125;;</span><br><span class="line">  <span class="keyword">return</span> <span class="string">`</span></span><br><span class="line"><span class="string">    &lt;html&gt;</span></span><br><span class="line"><span class="string">      &lt;head&gt;</span></span><br><span class="line"><span class="string">        &lt;meta charset=&quot;utf-8&quot; /&gt;</span></span><br><span class="line"><span class="string">        &lt;meta</span></span><br><span class="line"><span class="string">          name=&quot;viewport&quot;</span></span><br><span class="line"><span class="string">          content=&quot;width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no&quot;</span></span><br><span class="line"><span class="string">        /&gt;</span></span><br><span class="line"><span class="string">        &lt;link rel=&quot;stylesheet&quot; href=&quot;<span class="subst">$&#123;getDiskPath(<span class="string">&#x27;umi.css&#x27;</span>)&#125;</span>&quot; /&gt;</span></span><br><span class="line"><span class="string">        &lt;style&gt;</span></span><br><span class="line"><span class="string">          html, body, #root &#123;</span></span><br><span class="line"><span class="string">            width: 100%;</span></span><br><span class="line"><span class="string">            height: 100%;</span></span><br><span class="line"><span class="string">            margin: 0;</span></span><br><span class="line"><span class="string">            padding: 0;</span></span><br><span class="line"><span class="string">            overflow: hidden;</span></span><br><span class="line"><span class="string">          &#125;</span></span><br><span class="line"><span class="string">        &lt;/style&gt;</span></span><br><span class="line"><span class="string">        &lt;script&gt;</span></span><br><span class="line"><span class="string">          //! umi version: <span class="subst">$&#123;umiVersion&#125;</span></span></span><br><span class="line"><span class="string">        &lt;/script&gt;</span></span><br><span class="line"><span class="string">      &lt;/head&gt;</span></span><br><span class="line"><span class="string">      &lt;body&gt;</span></span><br><span class="line"><span class="string">        &lt;div id=&quot;root&quot;&gt;&lt;/div&gt;</span></span><br><span class="line"><span class="string">        &lt;script src=&quot;<span class="subst">$&#123;getDiskPath(<span class="string">&#x27;umi.js&#x27;</span>)&#125;</span>&quot;&gt;&lt;/script&gt;</span></span><br><span class="line"><span class="string">      &lt;/body&gt;</span></span><br><span class="line"><span class="string">    &lt;/html&gt;</span></span><br><span class="line"><span class="string">  `</span>;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><blockquote><p>提示：上面的方法我已经封装在 <a href="https://github.com/youngjuning/luozhu/tree/main/packages/vscode-utils">@luozhu&#x2F;vscode-utils</a> 的中。</p></blockquote><p>我们使用 getUmiContent 重新前面的代码：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; getUmiContent &#125; <span class="keyword">from</span> <span class="string">&#x27;@luozhu/vscode-utils&#x27;</span>;</span><br><span class="line">...</span><br><span class="line">panel.<span class="property">webview</span>.<span class="property">html</span> = <span class="title function_">getUmiContent</span>(context, panel, <span class="string">&#x27;3.5.17&#x27;</span>);</span><br></pre></td></tr></table></figure><h4 id="优化打包"><a href="#优化打包" class="headerlink" title="优化打包"></a>优化打包</h4><p>由于我们封装了 <code>getUmiContent</code> 方法，<code>umi build</code> 生成的 index.html 就没有用了，我们可以使用 <code>HTML=none umi build</code> 命令在打包的时候不生成 index.html 文件。</p><p>另外目前 <a href="https://github.com/umijs/umi/issues/7132">umijs 的 mfsu 不支持 writeToDisk 方法</a>，如果后续支持了可以使用 mfsu 优化调试速度。</p><blockquote><p>创建 webview 面板的任务大部分都比较重复，为了沉淀最佳实践，我在 <a href="https://github.com/youngjuning/luozhu/tree/main/packages/vscode-utils">@luozhu&#x2F;vscode-utils</a> 封装了 <a href="http://tny.im/bHLQx">createUmiWebviewPanel</a> 方法。</p></blockquote><h3 id="给-webview-内容加上主题"><a href="#给-webview-内容加上主题" class="headerlink" title="给 webview 内容加上主题"></a>给 webview 内容加上主题</h3><p>webview 可以基于当前的 VS Code 主题和 CSS 改变自身的样式。VS Code 将主题分成 3 种类别，而且在 body 元素上加上了特殊类名以表明当前主题，我们在 umi 中全局加入下面的样式：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line">body.<span class="property">vscode</span>-light &#123;</span><br><span class="line">  h1, h2, h3, h4, h5, h6 &#123;</span><br><span class="line">    <span class="attr">color</span>: black;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="attr">color</span>: black;</span><br><span class="line">  background-<span class="attr">color</span>: <span class="title function_">var</span>(--vscode-editor-background);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">body.<span class="property">vscode</span>-dark &#123;</span><br><span class="line">  h1, h2, h3, h4, h5, h6 &#123;</span><br><span class="line">    <span class="attr">color</span>: white;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="attr">color</span>: white;</span><br><span class="line">  background-<span class="attr">color</span>: <span class="title function_">var</span>(--vscode-editor-background);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">body.<span class="property">vscode</span>-high-contrast &#123;</span><br><span class="line">  h1, h2, h3, h4, h5, h6 &#123;</span><br><span class="line">    <span class="attr">color</span>: red;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="attr">color</span>: red;</span><br><span class="line">  background-<span class="attr">color</span>: <span class="title function_">var</span>(--vscode-editor-background);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>由于这部分适配大部分是通用的，所以我也将它封装进了 <code>@luozhu/vscode-utils</code> 的 <code>getUmiContent</code> 中了。</p><h2 id="webview-与-vscode-交互"><a href="#webview-与-vscode-交互" class="headerlink" title="webview 与 vscode 交互"></a>webview 与 vscode 交互</h2><h3 id="webview-中执行脚本"><a href="#webview-中执行脚本" class="headerlink" title="webview 中执行脚本"></a>webview 中执行脚本</h3><p>vscode 中的 webview 本质就是一个 iframe，因此我们是可以再 webview 中执行脚本的，只不过在 vscode 中 webview 默认禁用了 JavaScript，我们在调用 <code>createWebviewPanel</code> API 时传入 <code>enableScripts: true</code> 即可。</p><h3 id="插件传递信息给-webview"><a href="#插件传递信息给-webview" class="headerlink" title="插件传递信息给 webview"></a>插件传递信息给 webview</h3><p>webview 的脚本能做到任何普通网页脚本能做到的事情，但是 webview 运行在自己的上下文中，脚本是不能访问 VS Code API 的。我们需要借助 postMessage 这种事件的方式传递信息。在 vscode 中，我们在 vscode 侧可以使用 <a href="https://vscode-api-cn.js.org/interfaces/Webview.html#postMessage">Webview.postMessage</a> 发布事件并发送任何序列化的 JSON 数据，在 webview 侧则使用 <code>window.addEventListener(&#39;message&#39; event =&gt; { ... })</code> 来处理这些信息：</p><p><strong>vscode 侧</strong>：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 注册一个新的命令</span></span><br><span class="line">context.<span class="property">subscriptions</span>.<span class="title function_">push</span>(</span><br><span class="line">  vscode.<span class="property">commands</span>.<span class="title function_">registerCommand</span>(<span class="string">&#x27;juejin-me.author&#x27;</span>, <span class="function">() =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> (!currentPanel) &#123;</span><br><span class="line">      <span class="keyword">return</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 把信息发送到 webview</span></span><br><span class="line">    <span class="comment">// 你可以发送任何序列化的 JSON 数据</span></span><br><span class="line">    currentPanel.<span class="property">webview</span>.<span class="title function_">postMessage</span>(&#123; <span class="attr">method</span>: <span class="string">&#x27;showAuthor&#x27;</span> &#125;);</span><br><span class="line">  &#125;)</span><br><span class="line">);</span><br></pre></td></tr></table></figure><p><strong>webview 侧</strong>：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">Modal</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;antd&#x27;</span>;</span><br><span class="line">...</span><br><span class="line"><span class="variable language_">window</span>.<span class="title function_">addEventListener</span>(<span class="string">&#x27;message&#x27;</span>, <span class="function"><span class="params">event</span> =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">const</span> message = event.<span class="property">data</span>;</span><br><span class="line">  <span class="keyword">switch</span> (message.<span class="property">method</span>) &#123;</span><br><span class="line">    <span class="keyword">case</span> <span class="string">&#x27;showAuthor&#x27;</span>: &#123;</span><br><span class="line">      <span class="title class_">Modal</span>.<span class="title function_">info</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">content</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></span><br><span class="line"><span class="language-xml">            <span class="tag">&lt;<span class="name">a</span> <span class="attr">href</span>=<span class="string">&quot;https://juejin.cn/user/325111174662855/posts&quot;</span>&gt;</span>点赞<span class="tag">&lt;/<span class="name">a</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">        <span class="attr">okText</span>: <span class="language-xml"><span class="tag">&lt;<span class="name">a</span> <span class="attr">href</span>=<span class="string">&quot;https://juejin.cn/user/325111174662855/posts&quot;</span>&gt;</span>点赞 o(￣▽￣)ｄ<span class="tag">&lt;/<span class="name">a</span>&gt;</span></span>,</span><br><span class="line">      &#125;);</span><br><span class="line">      <span class="keyword">break</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="attr">default</span>:</span><br><span class="line">      <span class="keyword">break</span>;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p><strong>效果</strong>：</p><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0dd7dd37db954d84925ac1c0f60cb965~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><h3 id="webview-传递信息给插件"><a href="#webview-传递信息给插件" class="headerlink" title="webview 传递信息给插件"></a>webview 传递信息给插件</h3><p>webview 反向传递信息给插件的原理也是一样的，只不过由于 webview 的上下文限制，我们只能通过 <code>acquireVsCodeApi</code> 函数获取阉割版的 VS Code API 对象，这个阉割的对象上有一个 <code>postMessage</code> 函数可以供我们发送事件用。注意 <code>acquireVsCodeApi</code> 个会话中只能调用一次，重复调用会报错。而在插件侧则可以通过 <a href="https://vscode-api-cn.js.org/interfaces/Webview.html#onDidReceiveMessage">Webview.onDidReceiveMessage</a> 处理 webview 传递的信息。我们来写一个在 webview 中调用 <code>vscode.window.showInformationMessage</code> 的例子：</p><p><strong>webview 侧</strong>：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> vscode = <span class="title function_">acquireVsCodeApi</span>();</span><br><span class="line">vscode.<span class="title function_">postMessage</span>(&#123;</span><br><span class="line">  <span class="attr">method</span>: <span class="string">&#x27;showMessage&#x27;</span>,</span><br><span class="line">  <span class="attr">params</span>: &#123;</span><br><span class="line">    <span class="attr">text</span>: <span class="string">`为人民服务`</span>,</span><br><span class="line">  &#125;,</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p><strong>插件侧</strong>：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 处理 webview 中的信息</span></span><br><span class="line">currentPanel.<span class="property">webview</span>.<span class="title function_">onDidReceiveMessage</span>(</span><br><span class="line">  <span class="function"><span class="params">message</span> =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> (message.<span class="property">method</span> === <span class="string">&#x27;showMessage&#x27;</span>) &#123;</span><br><span class="line">      vscode.<span class="property">window</span>.<span class="title function_">showInformationMessage</span>(message.<span class="property">params</span>.<span class="property">content</span>);</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="literal">undefined</span>,</span><br><span class="line">  context.<span class="property">subscriptions</span></span><br><span class="line">);</span><br></pre></td></tr></table></figure><p><strong>效果</strong>：</p><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c32c8b374b534d6fac1cb552287de6ca~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><h3 id="在-webview-中请求接口"><a href="#在-webview-中请求接口" class="headerlink" title="在 webview 中请求接口"></a>在 webview 中请求接口</h3><p>一开始，我以为这是个轻松的工作，直到遇到跨域半天解决不了后我绝望了，在 <a href="https://juejin.cn/post/6844903966799577101#heading-3">VSCode WebView插件（扩展）开发实战</a> 一文中我终于知道了 vscode webview 内部是不允许发送 ajax 请求，所有 ajax 请求都是跨域的，因为 webview 本身是没有 host 的。</p><p>人裂开了，这什么鬼呀，我们核心的需求就是请求掘金的接口获取我们的文章列表呀，那我们还有办法吗？答案是肯定的，其实还是借助上面我们提到的通信机制把请求接口的任务交给 vscode 去处理，完事再让 vscode 把数据通过 <code>postMessage</code> 返回给我们，多说无益，我们来看代码：</p><p><strong>webview 侧</strong>：</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="title class_">React</span>.<span class="title function_">useEffect</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">  <span class="comment">// @ts-ignore</span></span><br><span class="line">  <span class="keyword">const</span> vscode = <span class="keyword">typeof</span> acquireVsCodeApi === <span class="string">&#x27;function&#x27;</span> ? <span class="title function_">acquireVsCodeApi</span>() : <span class="literal">null</span>;</span><br><span class="line">  vscode.<span class="title function_">postMessage</span>(&#123;</span><br><span class="line">    <span class="attr">method</span>: <span class="string">&#x27;queryPosts&#x27;</span>,</span><br><span class="line">  &#125;);</span><br><span class="line">  <span class="variable language_">window</span>.<span class="title function_">addEventListener</span>(<span class="string">&#x27;message&#x27;</span>, <span class="function"><span class="params">event</span> =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> (method === <span class="string">&#x27;queryPosts&#x27;</span>) &#123;</span><br><span class="line">      <span class="keyword">const</span> message = event.<span class="property">data</span>;</span><br><span class="line">      <span class="variable language_">console</span>.<span class="title function_">log</span>(message);</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><strong>vscode 侧</strong>：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 处理 webview 中的信息，并返回接口请求的数据</span></span><br><span class="line">currentPanel.<span class="property">webview</span>.<span class="title function_">onDidReceiveMessage</span>(</span><br><span class="line">  <span class="keyword">async</span> message =&gt; &#123;</span><br><span class="line">    <span class="keyword">const</span> data = <span class="keyword">await</span> <span class="title function_">events</span>(message);</span><br><span class="line">    currentPanel?.<span class="property">webview</span>.<span class="title function_">postMessage</span>(&#123; data &#125;);</span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="literal">undefined</span>,</span><br><span class="line">  context.<span class="property">subscriptions</span></span><br><span class="line">);</span><br></pre></td></tr></table></figure><h3 id="luozhu-vscode-channel"><a href="#luozhu-vscode-channel" class="headerlink" title="@luozhu&#x2F;vscode-channel"></a>@luozhu&#x2F;vscode-channel</h3><p>前面我们知道了使用 <a href="https://vscode-api-cn.js.org/interfaces/Webview.html#postMessage">Webview.postMessage</a>、<a href="https://vscode-api-cn.js.org/interfaces/Webview.html#onDidReceiveMessage">Webview.onDidReceiveMessage</a>、<code>acquireVsCodeApi().postMessage</code> 和 <code>window.addEventListener</code> 就可以满足各种通信需求了，那 <code>@luozhu/vscode-channel</code> 又是什么呢？</p><p>受 <a href="https://www.npmjs.com/package/js-channel">js-channel</a> 启发，<code>@luozhu/vscode-channel</code> 主要是封装了 webview 与 vscode 交互流程，核心原理是通过暴露 <code>call</code>、<code>bind</code> 方法抹平 API 的差异，减少重复代码量。其中参考 appworks 和 cs-channel 使用 uuid 保证交互的可靠性。Talk is cheap, show you the code：</p><p><strong>webview 侧</strong>：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 创建 channel 对象</span></span><br><span class="line"><span class="keyword">const</span> channel = <span class="keyword">new</span> <span class="title class_">Channel</span>();</span><br><span class="line"><span class="keyword">const</span> <span class="title function_">getData</span> = <span class="keyword">async</span> (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">  <span class="comment">// 发起一个请求，并等待其返回数据</span></span><br><span class="line">  <span class="keyword">const</span> &#123; payload &#125; = <span class="keyword">await</span> channel.<span class="title function_">call</span>(&#123; <span class="attr">method</span>: <span class="string">&#x27;queryPosts&#x27;</span> &#125;);</span><br><span class="line">  <span class="variable language_">console</span>.<span class="title function_">log</span>(payload);</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><blockquote><p>webview 中由于 acquireVsCodeApi 只能调用一次，之后又需要在多个地方使用，所以我们在 <code>wev/src/layouts/index.ts</code> 中创建一次并挂载到 <code>window</code> 对象上比较合适。</p></blockquote><p><strong>vscode 侧</strong>：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// vscode 侧的 channel 需要依赖上下文和 WebviewPanel 实例</span></span><br><span class="line"><span class="keyword">const</span> channel = <span class="keyword">new</span> <span class="title class_">Channel</span>(context, currentPanel);</span><br><span class="line"><span class="comment">// 绑定一个回调函数，一般只需要创建一个，然后根据约定做分发即可</span></span><br><span class="line">channel.<span class="title function_">bind</span>(<span class="keyword">async</span> message =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> &#123; eventType, method, params &#125; = message;</span><br><span class="line">  <span class="comment">// 实际发起请求获取数据的地方</span></span><br><span class="line">  <span class="keyword">const</span> data = <span class="keyword">await</span> events[eventType][method](params);</span><br><span class="line">  <span class="comment">// 这里将获取的数据直接返回即可，channel 内部会进行消息合并和回传。</span></span><br><span class="line">  <span class="comment">// 如果只是执行一个功能，不写 return 语句即可，内部会进行判断降级成单工通信。</span></span><br><span class="line">  <span class="keyword">return</span> data;</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><h2 id="vscode-国际化"><a href="#vscode-国际化" class="headerlink" title="vscode 国际化"></a>vscode 国际化</h2><p>我们都知道 vscode 中是可以切换语言环境的，一款优秀的 vscode 扩展至少要支持中英两种语言。而且支持国际化可以让你的插件受众直接突破国界限制。vscode 国际化分为三部分，一部分是配置的国际化，一部分是代码中的国际化，另一部分则是 webview 中 umijs 的国际化。本章我们就来具体看一下如何在 vscode 中实现国际化。</p><h3 id="配置国际化"><a href="#配置国际化" class="headerlink" title="配置国际化"></a>配置国际化</h3><p>我们已经知道 vscode 中的配置都是在 <em>package.json</em> 中，而配置的国际化是约定在 <code>package.nls.json</code> 和 <code>package.nls.zh-cn.json</code> 这种文件中编写。比如我们要在中英文环境下命令配置中英文版本，我们可以在 <code>package.nls.json</code> 中写：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;contributes.category.juejin-me&quot;</span><span class="punctuation">:</span> <span class="string">&quot;Juejin Me&quot;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p>在 <code>package.nls.zh-cn.json</code> 写：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;contributes.category.juejin-me&quot;</span><span class="punctuation">:</span> <span class="string">&quot;掘金一下&quot;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p>然后 <code>package.json</code> 中写：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;contributes&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;commands&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><br><span class="line">        <span class="attr">&quot;category&quot;</span><span class="punctuation">:</span> <span class="string">&quot;%contributes.category.juejin-me%&quot;</span></span><br><span class="line">      <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">    <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><h3 id="代码中国际化"><a href="#代码中国际化" class="headerlink" title="代码中国际化"></a>代码中国际化</h3><p>推荐使用紫升贡献过代码的 <a href="https://github.com/axetroy/vscode-nls-i18n">vscode-nls-i18n</a>，使用方法也很简单，配置的话和上一节一样，在 <code>src/extension.ts</code> 中使用 <code>init</code> 方法初始化，然后使用 <code>localize</code> 方法实现国际化：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; init, localize &#125; <span class="keyword">from</span> <span class="string">&#x27;vscode-nls-i18n&#x27;</span>;</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">function</span> <span class="title function_">activate</span>(<span class="params">context: vscode.ExtensionContext</span>) &#123;</span><br><span class="line">  <span class="title function_">init</span>(context.<span class="property">extensionPath</span>); <span class="comment">// 初始化国际化配置。只用在扩展激活时初始化一次</span></span><br><span class="line">  <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="title function_">localize</span>(<span class="string">&#x27;extension.activeLog&#x27;</span>)); <span class="comment">// 之后就可以在各个文件中使用。</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="umijs-国际化"><a href="#umijs-国际化" class="headerlink" title="umijs 国际化"></a>umijs 国际化</h3><p>umijs 的国际化需要使用 <code>@umijs/plugin-locale</code> 插件支持，这个插件封装了 <code>react-intl</code>，配置方式如下：</p><p>1、.umirc.ts 中配置 <code>local</code></p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">locale</span>: &#123;&#125;</span><br></pre></td></tr></table></figure><p>2、在 <em>src</em> 目录下创建 <code>locales</code> 并创建 <code>en.ts</code> 或 <code>zh-CN.ts</code></p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// src/locales/en.js</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> &#123;</span><br><span class="line">  <span class="attr">WELCOME_TO_UMI_WORLD</span>: <span class="string">&quot;welcome to umi&#x27;s world&quot;</span>,</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// src/locales/zh-CN.js</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> &#123;</span><br><span class="line">  <span class="attr">WELCOME_TO_UMI_WORLD</span>: <span class="string">&#x27;欢迎光临  umi  的世界&#x27;</span>,</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>3、使用国际化</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">React</span> <span class="keyword">from</span> <span class="string">&#x27;react&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; useIntl &#125; <span class="keyword">from</span> <span class="string">&#x27;umi&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="attr">default</span>: <span class="title class_">React</span>.<span class="property">FunctionComponent</span> = <span class="function">(<span class="params">props</span>) =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">const</span> intl = <span class="title function_">useIntl</span>()</span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">     &lt;div&gt;</span><br><span class="line">     &#123;intl.<span class="title function_">formatMessage</span>(</span><br><span class="line">        &#123;</span><br><span class="line">          <span class="attr">id</span>: <span class="string">&#x27;WELCOME_TO_UMI_WORLD&#x27;</span>,</span><br><span class="line">        &#125;</span><br><span class="line">      )&#125;&lt;div&gt;</span><br><span class="line">   )</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>4、切换语言</p><p>切换语言，我们需要使用 <code>setLocale</code> 方法，需要注意的是我们给这个方法第二个参数传入 <code>false</code> 来实现无刷新动态切换。</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; setLocale &#125; <span class="keyword">from</span> <span class="string">&#x27;umi&#x27;</span>;</span><br><span class="line"><span class="comment">// 不刷新页面</span></span><br><span class="line"><span class="title function_">setLocale</span>(<span class="string">&#x27;zh-CN&#x27;</span>, <span class="literal">false</span>);</span><br></pre></td></tr></table></figure><p>不过，切换语言的时机在什么时候呢？切换时机就是我们语言环境改变的时机。在 vscode webview 环境中，其实当使用 <code>Config display language</code> 方法切换语言环境后，会要求 vscode 重启。也就说我们只需要在 webview 创建时设置一次语言环境即可。由于 vscode 和 webview 传值太困难，我们选择在 <code>getUmiHTMLContent</code> 时传如 <code>vscode.env</code>：</p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">script</span>&gt;</span><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript">  <span class="variable language_">window</span>.<span class="property">vscodeEnv</span> = $&#123;<span class="title class_">JSON</span>.<span class="title function_">stringify</span>(vscode.<span class="property">env</span>)&#125;</span></span><br><span class="line"><span class="language-javascript"></span><span class="tag">&lt;/<span class="name">script</span>&gt;</span></span><br></pre></td></tr></table></figure><p>然后，我们在 <code>web/src/layouts/index.ts</code> 中设置一下即可：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="title function_">setLocale</span>(<span class="variable language_">window</span>.<span class="property">vscodeEnv</span>.<span class="property">language</span>, <span class="literal">false</span>);</span><br></pre></td></tr></table></figure><h2 id="“掘金一下”-扩展核心实现"><a href="#“掘金一下”-扩展核心实现" class="headerlink" title="“掘金一下” 扩展核心实现"></a>“掘金一下” 扩展核心实现</h2><p>灵感来源于现实，作为掘金的重度使用者，几乎每篇文章和笔记都同步在这里。当有些知识忘记需要查阅或拷贝代码时，我就有在掘金搜索我的文章的需求。但是掘金的搜索是全站的，就算加上自己的名字搜索也会出现大量无关记录。“掘金一下” 这个名字就像插件功能一样，在你想搜索自己掘金文章的时候就可以打开插件“掘金一下” 进行搜索。</p><p>其实为了只搜索到自己的文章，我想到的还有开发 chrome 插件来实现。但是考虑到市场和便捷性，我最终还是决定开发 vscode 插件来落地这个灵感。本章就是综合前面的经验实现 “掘金一下” 的核心逻辑。</p><h3 id="juejin-me-start-命令"><a href="#juejin-me-start-命令" class="headerlink" title="juejin-me.start 命令"></a><code>juejin-me.start</code> 命令</h3><h4 id="vscode-侧开启-channel-通信"><a href="#vscode-侧开启-channel-通信" class="headerlink" title="vscode 侧开启 channel 通信"></a>vscode 侧开启 channel 通信</h4><p>vscode 侧通过 <code>channel.bind</code> 绑定一个事件处理函数。</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> events <span class="keyword">from</span> <span class="string">&#x27;./events&#x27;</span>;</span><br><span class="line">...</span><br><span class="line">context.<span class="property">subscriptions</span>.<span class="title function_">push</span>(</span><br><span class="line">  vscode.<span class="property">commands</span>.<span class="title function_">registerCommand</span>(<span class="string">&#x27;juejin-me.start&#x27;</span>, <span class="keyword">async</span> () =&gt; &#123;</span><br><span class="line">    currentPanel = <span class="title function_">createUmiWebviewPanel</span>(</span><br><span class="line">      context,</span><br><span class="line">      <span class="string">&#x27;juejin-me&#x27;</span>,</span><br><span class="line">      <span class="title function_">localize</span>(<span class="string">&#x27;extension.webview-panel.title&#x27;</span>),</span><br><span class="line">      <span class="string">&#x27;assets/icon-luozhu.png&#x27;</span>,</span><br><span class="line">      <span class="string">&#x27;3.5.17&#x27;</span></span><br><span class="line">    );</span><br><span class="line">    <span class="comment">// 处理 webview 中的信息</span></span><br><span class="line">    channel = <span class="keyword">new</span> <span class="title class_">Channel</span>(context, currentPanel);</span><br><span class="line">    channel.<span class="title function_">bind</span>(<span class="keyword">async</span> message =&gt; &#123;</span><br><span class="line">      <span class="keyword">const</span> &#123; eventType, method, params &#125; = message;</span><br><span class="line">      <span class="comment">// 根据事件类型、方法、参数来完成一次 api 调用，内置的 eventType 有 request、command 和 variable。</span></span><br><span class="line">      <span class="keyword">const</span> data = <span class="keyword">await</span> events[eventType][method](params);</span><br><span class="line">      <span class="keyword">return</span> data;</span><br><span class="line">    &#125;, vscode);</span><br><span class="line">  &#125;)</span><br><span class="line">);</span><br></pre></td></tr></table></figure><blockquote><p>注意：我们不需要给定监听事件名，内部会根据 eventId 保证可靠性和全局唯一性</p></blockquote><h4 id="注册-events"><a href="#注册-events" class="headerlink" title="注册 events"></a>注册 events</h4><p><strong>events&#x2F;index.ts</strong>：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> requests <span class="keyword">from</span> <span class="string">&#x27;./requests&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> &#123;</span><br><span class="line">  <span class="attr">request</span>: requests,</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p><strong>events&#x2F;requests</strong>：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> vscode <span class="keyword">from</span> <span class="string">&#x27;vscode&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> request <span class="keyword">from</span> <span class="string">&#x27;../utils/request&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> queryPosts = <span class="keyword">async</span> (<span class="attr">params</span>: &#123; <span class="attr">cursor</span>: <span class="built_in">string</span> &#125;): <span class="title class_">Promise</span>&lt;<span class="built_in">any</span>&gt; =&gt; &#123;</span><br><span class="line">  <span class="comment">// 这里我们根据 vscode 配置动态取的用户 id</span></span><br><span class="line">  <span class="keyword">const</span> &#123; userId &#125; = vscode.<span class="property">workspace</span>.<span class="title function_">getConfiguration</span>(<span class="string">&#x27;juejin-me&#x27;</span>);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> &#123; cursor &#125; = params;</span><br><span class="line">  <span class="keyword">const</span> data = <span class="keyword">await</span> request.<span class="title function_">post</span>(<span class="string">&#x27;/article/query_list&#x27;</span>, &#123;</span><br><span class="line">    <span class="attr">cursor</span>: <span class="string">`<span class="subst">$&#123;cursor&#125;</span>`</span>,</span><br><span class="line">    <span class="attr">sort_type</span>: <span class="number">2</span>,</span><br><span class="line">    <span class="attr">user_id</span>: userId,</span><br><span class="line">  &#125;);</span><br><span class="line">  <span class="keyword">return</span> data;</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> &#123;</span><br><span class="line">  queryPosts,</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p><strong>utils&#x2F;request</strong>：</p><p>这里简单封装了基于 axios 的请求对象。</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/* eslint-disable no-param-reassign */</span></span><br><span class="line"><span class="keyword">import</span> axios <span class="keyword">from</span> <span class="string">&#x27;axios&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> vscode <span class="keyword">from</span> <span class="string">&#x27;vscode&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> qs <span class="keyword">from</span> <span class="string">&#x27;qs&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 中文文档: http://t.cn/ROfXFuj</span></span><br><span class="line"><span class="comment">// 创建实例</span></span><br><span class="line"><span class="keyword">const</span> request = axios.<span class="title function_">create</span>(&#123;</span><br><span class="line">  <span class="attr">baseURL</span>: <span class="string">&#x27;https://api.juejin.cn/content_api/v1/&#x27;</span>,</span><br><span class="line">  <span class="attr">timeout</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">request.<span class="property">interceptors</span>.<span class="property">request</span>.<span class="title function_">use</span>(</span><br><span class="line">  <span class="function"><span class="params">config</span> =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> (config.<span class="property">method</span> === <span class="string">&#x27;get&#x27;</span>) &#123;</span><br><span class="line">      config.<span class="property">paramsSerializer</span> = <span class="function"><span class="params">params</span> =&gt;</span> qs.<span class="title function_">stringify</span>(params, &#123; <span class="attr">arrayFormat</span>: <span class="string">&#x27;repeat&#x27;</span> &#125;);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> config;</span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="function"><span class="params">error</span> =&gt;</span> &#123;</span><br><span class="line">    vscode.<span class="property">window</span>.<span class="title function_">showErrorMessage</span>(error.<span class="property">message</span>);</span><br><span class="line">    <span class="keyword">return</span> <span class="title class_">Promise</span>.<span class="title function_">reject</span>(error);</span><br><span class="line">  &#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">request.<span class="property">interceptors</span>.<span class="property">response</span>.<span class="title function_">use</span>(</span><br><span class="line">  <span class="function"><span class="params">response</span> =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">const</span> &#123; data &#125; = response;</span><br><span class="line">    <span class="keyword">return</span> data;</span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="function"><span class="params">error</span> =&gt;</span> &#123;</span><br><span class="line">    vscode.<span class="property">window</span>.<span class="title function_">showErrorMessage</span>(error.<span class="property">message</span>);</span><br><span class="line">    <span class="keyword">return</span> <span class="title class_">Promise</span>.<span class="title function_">reject</span>(error);</span><br><span class="line">  &#125;</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> request;</span><br></pre></td></tr></table></figure><h3 id="webview-中调用接口"><a href="#webview-中调用接口" class="headerlink" title="webview 中调用接口"></a>webview 中调用接口</h3><p>channel 是在 <code>web/src/layouts/index.tsx</code> 中初始化并挂载到 window 上的，我们在 <code>web/src/pages/index.tsx</code> 中调用 <code>window.channel.call</code> 即可调用指定接口。由于我们需要模糊搜索所有的文章，所以我们需要在初始化页面时一次请求完所有数据。</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="title function_">Homepage</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> <span class="title function_">getData</span> = <span class="keyword">async</span> (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">    <span class="keyword">const</span> &#123; payload &#125; = (<span class="keyword">await</span> <span class="variable language_">window</span>.<span class="property">channel</span>.<span class="title function_">call</span>(&#123;</span><br><span class="line">      <span class="attr">eventType</span>: <span class="string">&#x27;request&#x27;</span>,</span><br><span class="line">      <span class="attr">method</span>: <span class="string">&#x27;queryPosts&#x27;</span>,</span><br><span class="line">      <span class="attr">params</span>: &#123; cursor &#125;,</span><br><span class="line">    &#125;)) <span class="keyword">as</span> <span class="built_in">any</span>;</span><br><span class="line">    tempData = tempData.<span class="title function_">concat</span>(payload.<span class="property">data</span>);</span><br><span class="line">    <span class="title function_">setData</span>(tempData);</span><br><span class="line">    <span class="keyword">if</span> (!payload.<span class="property">has_more</span>) &#123;</span><br><span class="line">      <span class="title function_">setInitLoading</span>(<span class="literal">false</span>);</span><br><span class="line">      <span class="title function_">setCategories</span>(<span class="title function_">_union</span>([<span class="string">&#x27;全部&#x27;</span>, ...tempData.<span class="title function_">map</span>(<span class="function"><span class="params">item</span> =&gt;</span> item.<span class="property">category</span>.<span class="property">category_name</span>)]));</span><br><span class="line">      tempData = [];</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">      cursor += <span class="number">10</span>;</span><br><span class="line">      <span class="title function_">getData</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>更多具体实现细节就是一些页面编写逻辑，不是本文的重点，感兴趣的同学可以直接进查看<a href="https://github.com/youngjuning/juejin-me/tree/main/web">源码</a>。</p><h3 id="配置掘金-ID"><a href="#配置掘金-ID" class="headerlink" title="配置掘金 ID"></a>配置掘金 ID</h3><p><strong>声明配置</strong>：</p><p>vscode 的配置我们需要借助 package.json 的 <code>contributes.configuration</code> 属性，我们的掘金 ID 是 string，所以声明如下：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;contributes&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;configuration&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">      <span class="attr">&quot;title&quot;</span><span class="punctuation">:</span> <span class="string">&quot;%configuration.title%&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;object&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;properties&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">        <span class="attr">&quot;juejin-me.userId&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">          <span class="attr">&quot;type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;string&quot;</span><span class="punctuation">,</span></span><br><span class="line">          <span class="attr">&quot;default&quot;</span><span class="punctuation">:</span> <span class="string">&quot;325111174662855&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;%configuration.properties.juejin-me.userId%&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">&#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><strong>修改配置的命令</strong>：</p><p>让用户打开设置去修改配置也可以，但是为了用户体验，我们提供了 <code>juejin-me.configUserId</code> 命令，我们来看下命令的实现：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">context.<span class="property">subscriptions</span>.<span class="title function_">push</span>(</span><br><span class="line">  vscode.<span class="property">commands</span>.<span class="title function_">registerCommand</span>(<span class="string">&#x27;juejin-me.configUserId&#x27;</span>, <span class="keyword">async</span> () =&gt; &#123;</span><br><span class="line">    <span class="keyword">const</span> userId = <span class="keyword">await</span> vscode.<span class="property">window</span>.<span class="title function_">showInputBox</span>(&#123;</span><br><span class="line">      <span class="attr">placeHolder</span>: <span class="title function_">localize</span>(<span class="string">&#x27;extension.juejin-me.configUserId.placeHolder&#x27;</span>),</span><br><span class="line">      <span class="attr">validateInput</span>: <span class="function"><span class="params">value</span> =&gt;</span> &#123;</span><br><span class="line">        <span class="keyword">if</span> (value) &#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 class="keyword">return</span> <span class="title function_">localize</span>(<span class="string">&#x27;extension.juejin-me.configUserId.validateInput&#x27;</span>);</span><br><span class="line">      &#125;,</span><br><span class="line">    &#125;);</span><br><span class="line">    <span class="keyword">const</span> config = vscode.<span class="property">workspace</span>.<span class="title function_">getConfiguration</span>(<span class="string">&#x27;juejin-me&#x27;</span>);</span><br><span class="line"></span><br><span class="line">    config.<span class="title function_">update</span>(<span class="string">&#x27;userId&#x27;</span>, userId, <span class="literal">true</span>);</span><br><span class="line">  &#125;)</span><br><span class="line">);</span><br></pre></td></tr></table></figure><ul><li><a href="https://vscode-api-cn.js.org/modules/window.html#showInputBox">vscode.window.showInputBox</a>：打开一个输入框，提示用户输入掘金用户 ID</li><li><a href="https://vscode-api-cn.js.org/modules/workspace.html#getConfiguration">vscode.workspace.getConfiguration</a>：获取工作空间的配置对象</li><li><a href="https://vscode-api-cn.js.org/interfaces/WorkspaceConfiguration.html#update">WorkspaceConfiguration.update</a>：更新一个配置值。</li><li><a href="https://vscode-api-cn.js.org/interfaces/InputBoxOptions.html#validateInput">InputBoxOptions.validateInput</a>：一个可选的函数，被调用来验证输入信息并提示用户</li></ul><h3 id="插件效果展示"><a href="#插件效果展示" class="headerlink" title="插件效果展示"></a>插件效果展示</h3><p>感兴趣的话你也可以直接在扩展中搜索“掘金一下”自行体验。</p><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ac7d2ab498b2469486e8f237fc3b7997~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/91ae0fdbff7043db9513e539640fcc2e~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><h2 id="彩蛋"><a href="#彩蛋" class="headerlink" title="彩蛋"></a>彩蛋</h2><h3 id="luozhu-create-vscode-webview"><a href="#luozhu-create-vscode-webview" class="headerlink" title="@luozhu&#x2F;create-vscode-webview"></a>@luozhu&#x2F;create-vscode-webview</h3><p>本文中有很多最佳实践，为了方便之后创建新的项目时减少重复工作，紫升抽离出了一个简单的模板。掘友直接使用 <code>yarn create @luozhu/vscode-webview  myvscode</code> 即可创建出一个属于自己的 vscode 扩展。参考本文的一些实践再加一些你的创意即可完成一个出色的基于 webview 的 vscode 扩展。</p><h3 id="Word-Count-Juejin"><a href="#Word-Count-Juejin" class="headerlink" title="Word Count Juejin"></a>Word Count Juejin</h3><p>为了答谢掘金平台和掘友一直以来的支持，我编写了一款专为掘金适配的 Markdown 文件字数统计 VS Code 扩展，字数统计会实时显示在状态栏。比起来 vscode 官方的 Word Count，我们支持中文字数统计，比起来 Word Count CJK，我们支持中英文混排。如果你也喜欢使用 VS Code 的 Markdown 编辑能力，那么一定不要错过紫升的这款插件，下载请认准：</p><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/08c060326c954849b3eb2d34f2e6b0e5~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><p>如果你还在犹豫要不要下载，那不妨看下三个插件的统计对比，我们拿 <code>i love juejin. 我爱掘金</code> 这个字符串测试一下三款插件的功能：</p><table><thead><tr><th>Word Count</th><th>Word Count CJK</th><th>Word Count Juejin</th></tr></thead><tbody><tr><td>4 个字</td><td>4 个字</td><td>7 个字</td></tr><tr><td>中文算成了一个字</td><td>直接忽略了英文</td><td>中文4 个字加英文三个字，格局正好</td></tr></tbody></table><h3 id="vscode-api-cn"><a href="#vscode-api-cn" class="headerlink" title="vscode api cn"></a>vscode api cn</h3><p>在学习和开发 vscode 插件的过程中，最大的痛点无过于 API 文档翻译的缺失。哪怕是硬着头皮看英文原版 API 文档，阅读体验也很差。为了方便自己、回馈社区，我和 <a href="https://juejin.cn/user/703340610597064">寒草</a> 等小伙伴决定翻译 vscode api 类型声明并使用 Typedoc 承载，另外在完工后我们也会输出 <code>@types/vscode-cn</code> 类型包代替 <code>@types/vscode</code> 进一步方便 vscode 插件开发者。团队成员现状：</p><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/aa80c1603b2e4482883fe39e1b44f62e~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><p>翻译是一件带有侠义精神的事业，欢迎更多的小伙伴加入我们。你可以浏览<a href="https://github.com/vscode-cn/vscode-api-cn">仓库</a>和<a href="https://vscode-api-cn.js.org/">官网</a>了解具体情况。</p><h2 id="后记"><a href="#后记" class="headerlink" title="后记"></a>后记</h2><p>这是第一次尝试写这么长的文章，断断续续经历了有半个月，本着对读者负责任的态度，文中的实践都是经过反复测试以及和同事朋友的讨论。当然 vscode 插件开发的概念和 API 比较多，一篇文章也很难讲全，讲透彻。如果大家感兴趣，可以在评论区告诉紫升，我可以继续更新这方面的教程。</p><blockquote><p>本文首发于「掘金专栏」，同步于公众号「程序人生」。</p></blockquote>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/609531a3ab52/</id>
    <link href="https://blog.zisheng.pro/posts/609531a3ab52/"/>
    <published>2023-02-28T16:55:47.000Z</published>
    <summary>在团队降本提效的基建中，紫升开发了一款 vscode 插件，第一版我使用的是 vscode 内置 UI，虽说也能用，但是用户体验欠佳。由于 vscode 内置 UI 不够灵活，一番调研后我决定使用 webview 重构。</summary>
    <title>掘金一下 | 从零开发一款基于 webview 的 vscode 扩展</title>
    <updated>2026-09-18T08:15:28.640Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="VS Code" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/VS-Code/"/>
    <category term="VS Code" scheme="https://blog.zisheng.pro/tags/VS-Code/"/>
    <category term="vscode 插件开发" scheme="https://blog.zisheng.pro/tags/vscode-%E6%8F%92%E4%BB%B6%E5%BC%80%E5%8F%91/"/>
    <category term="yarn.lock" scheme="https://blog.zisheng.pro/tags/yarn-lock/"/>
    <category term="vscode 中文文档" scheme="https://blog.zisheng.pro/tags/vscode-%E4%B8%AD%E6%96%87%E6%96%87%E6%A1%A3/"/>
    <category term="vscode webview" scheme="https://blog.zisheng.pro/tags/vscode-webview/"/>
    <content>
      <![CDATA[<blockquote><p><a href="https://marketplace.visualstudio.com/items?itemName=youngjuning.yarn-lock-preview">Yarn Lock Preview</a> 插件已发布，亲测已比较稳定，源码在 <a href="https://github.com/youngjuning/vscode-yarn-lock-preview">vscode-yarn-lock-preview</a> ，如果对你些许帮助和启发，不妨赏一个 Star。</p></blockquote><h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>在 <a href="https://juejin.cn/post/7000589186898231333">《从零开发一款基于 webview 的 vscode 扩展》</a> 一文中，紫升通过实战阐述了如何基于 webview 实现在 vscode 中展示自定义 UI 的功能，另外文章配套产物 <a href="vscode:extension/youngjuning.juejin-me">掘金一下</a> 插件也已经发布，欢迎试用。</p><p>人们👨🏻‍💻总是喜欢炫耀自己的新技能，紫升也不例外，毕竟新技能需要大量的实战练习。在入门了 VS Code 扩展开发后，遇到开发痛点我就习惯地用 VS Code 插件的思路去解决。不过大多扩展 idea 都已经名花有主，直到我想到可以将 <code>yarn.lock</code> 文件可视化并搜索相关插件无果后，我知道机会来了。</p><h2 id="什么是自定义编辑器❓"><a href="#什么是自定义编辑器❓" class="headerlink" title="什么是自定义编辑器❓"></a>什么是自定义编辑器❓</h2><p>在开始开发之前，我们有必要先了解一下什么编辑器？什么又是自定义编辑器？</p><p>VS Code 中编辑器（Editor）其实就是我们使用频率最高的编码的地方。如下图，VS Code 的用户界面分为 5 个部分，分别是 Activity Bar（活动栏）、Side Bar（侧边栏）、Editor Groups（编辑器组）、Panel（面板） 和 Status Bar（状态栏）。</p><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/301eab5bbab04c65aec4a927b7d306a9~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><p>自定义编辑器指的是 VS Code 中相对于默认文本编辑器的编辑器类型。根据用途不同，自定义编辑器又分为 <a href="https://vscode-api-cn.js.org/interfaces/vscode.CustomTextEditorProvider.html">自定义文本编辑器</a>、<a href="https://vscode-api-cn.js.org/interfaces/vscode.CustomEditorProvider.html">自定义编辑器</a> 和 <a href="https://vscode-api-cn.js.org/interfaces/vscode.CustomReadonlyEditorProvider.html">自定义只读编辑器</a>。</p><p>自定义文本编辑器常被用来为 JSON、XML、CSV、JSON 或者任意 <a href="https://vscode-api-cn.js.org/interfaces/vscode.TextDocument.html">文本文档</a> 提供自定义的视觉渲染。比如 <a href="https://marketplace.visualstudio.com/items?itemName=SimonSiefke.svg-preview">Svg Preview</a>、<a href="https://marketplace.visualstudio.com/items?itemName=shd101wyy.markdown-preview-enhanced">Markdown Preview Enhanced</a>、<a href="https://marketplace.visualstudio.com/items?itemName=vstirbu.vscode-mermaid-preview">Mermaid Preview</a> 和 <a href="https://marketplace.visualstudio.com/items?itemName=saber2pr.todolist">Todo List</a>。</p><p>自定义编辑器常被用来预览资产(assets)文件，比如 <a href="https://marketplace.visualstudio.com/items?itemName=slevesque.vscode-3dviewer">3D Viewer for VSCode</a>、<a href="https://marketplace.visualstudio.com/items?itemName=hediet.vscode-drawio">Draw.io Integration</a> 和 <a href="https://marketplace.visualstudio.com/items?itemName=elypia.magick-image-reader">Magick Image Reader</a></p><p>但他们的共同点都是使用了 VS Code 自定义编辑器这个功能。本文的目标其实就是实现一个和上述插件一样的 Preview 类型的扩展。</p><h2 id="为什么开发-Yarn-Lock-Preview？"><a href="#为什么开发-Yarn-Lock-Preview？" class="headerlink" title="为什么开发 Yarn Lock Preview？"></a>为什么开发 Yarn Lock Preview？</h2><p>本来文章是没有这一章的。在插件刚发布的时候，没等文章写完，我就迫不及待地向同事和朋友以及常混迹的几个群推荐了我的插件，然后就被一个群友灵魂拷问了“你这有什么意义？”：</p><p><img src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c054b9bca3b44b61b7b9ecc7206240a4~tplv-k3u1fbpfcp-watermark.image" alt="image.png"></p><p><img src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/643f9dd16c734e8487866f6a3306f0f3~tplv-k3u1fbpfcp-watermark.image" alt="image.png"></p><p>开发一款自定义编辑器插件要比单纯的开发插件和 webview 插件都要麻烦，没有人会闲的无聊牺牲业余时间，做一个没有意思的事情。从学习角度来讲，可以掌握了大量 VSCode API，从作用来讲，更直观地查看 <code>yarn.lock</code> 并支持搜索某个包以及依赖该包的包。这在你想确定应用的间接引用了哪些包时很有帮助：</p><p><img src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c2faa24d3cee4c9088e83c4d0ee06c63~tplv-k3u1fbpfcp-watermark.image" alt="screenshot.gif"></p><p>这其实是一个来自于实际工作中的一个痛点，React Native 中如果同时依赖两个不同版本的原生包，运行项目的时候就会因为重复注册某个 <code>View</code> 造成应用崩溃。入职涂鸦一年来，帮助业务同学排查了 N 次这个问题。由于某些版本的依赖并不是直接在 <code>dependencies</code> 中写的，而是某个包间接依赖的，我的办法是就是在 <code>yarn.lock</code> 中 <code>cmd+f</code> 搜索造成崩溃的包被哪些包依赖了，然后再人肉分析这个有结构的纯文本文件：</p><p><img src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/4ee531d6d54d4d269984174b5bae97b8~tplv-k3u1fbpfcp-watermark.image" alt="image.png"></p><p>虽然我已经是处理这个问题的老司机了，但总归是会耽误不少我和同事的时间。上面的这个问题可能比较极端，还有一个我们经常遇到的开发的反馈是引入了某个原生包之后导致应用崩溃。这其实是因为我们的 React Native 应用是以嵌套的形式集成到已有原生涂鸦 App 内的，所以原生库的支持也是依赖 App 提供的版本。我们在打包的时候也有相应的检验工具，但是用户有可能没有直接依赖这个库，而是这个库间接或者间接地间接库引用了这个库。这个时候我们就得在上万行纯文本 yarn.lock 中开启人肉分析。</p><p>比如，App 目前支持的 react-native-svg 版本是 5.5.1。用户使用的 <code>react-native-svg-charts</code> 最新版依赖的是 react-native-svg <code>^6.2.1</code> 或者 <code>^7.0.3</code>。用户按照指引直接安装使用运行崩溃，然后找到我们质疑。那如果用了这款插件，我们就可以直接在项目中搜索：</p><p><img src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/95d979f9b7c549e68f8321dac0aaa481~tplv-k3u1fbpfcp-watermark.image" alt="image.png"></p><p>最后一个就是你一定可能会遇到的场景，比如开发声明的依赖是 <code>tuya-panel-kit@^4.6.0</code>，然后遇到问题，直接截图告诉你我没有升级过版本呀，为啥报错了或者为啥表现不一样了的问题。然后我们就得让用户看一下实际依赖的版本是什么。有的开发会去 <code>node_modules</code> 下面找，这其实不靠谱，层级那么复杂，想快速定位是很难的；有的开发会在 <code>yarn.lock</code> 中人肉搜索；或者有的开发直接让我们帮忙排查。那么有了这款插件，我们就可以直接让开发自己搜，然后截图提 issues 给我们：</p><p><img src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/7bc01f9c7df543db90d6f6b31444eeca~tplv-k3u1fbpfcp-watermark.image" alt="image.png"></p><blockquote><p>温馨提示：<code>yarn list --pattern tuya-panel-kit</code> 命令也可以列出简要的依赖信息。</p></blockquote><p>叨逼叨这么多，相信读者已经和我达成共识了，如果没有，欢迎评论区 Battle。废话不多说，下面就让我们来开始展示真正的技术吧。</p><p><img src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/7c9f81662bba4518b8d075d132d870e1~tplv-k3u1fbpfcp-watermark.image" alt="image.png"></p><h2 id="初始化项目"><a href="#初始化项目" class="headerlink" title="初始化项目"></a>初始化项目</h2><h3 id="使用官方脚手架"><a href="#使用官方脚手架" class="headerlink" title="使用官方脚手架"></a>使用官方脚手架</h3><ol><li>安装 <a href="http://yeoman.io/">Yeoman</a> 和 <a href="https://www.npmjs.com/package/generator-code">VS Code Extension Generator</a>：<code>npm install -g yo generator-code</code></li><li>生成项目：<code>yo code</code></li></ol><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line">$ yo code</span><br><span class="line"><span class="comment">#     _-----_     ╭──────────────────────────╮</span></span><br><span class="line"><span class="comment">#    |       |    │   Welcome to the Visual  │</span></span><br><span class="line"><span class="comment">#    |--(o)--|    │   Studio Code Extension  │</span></span><br><span class="line"><span class="comment">#   `---------´   │        generator!        │</span></span><br><span class="line"><span class="comment">#    ( _´U`_ )    ╰──────────────────────────╯</span></span><br><span class="line"><span class="comment">#    /___A___\   /</span></span><br><span class="line"><span class="comment">#     |  ~  |</span></span><br><span class="line"><span class="comment">#   __&#x27;.___.&#x27;__</span></span><br><span class="line"><span class="comment"># ´   `  |° ´ Y `</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># ? What type of extension do you want to create? New Extension (TypeScript)</span></span><br><span class="line"><span class="comment"># ? What&#x27;s the name of your extension? Yarn Lock Preview</span></span><br><span class="line"><span class="comment"># ? What&#x27;s the identifier of your extension? yarn-lock-preview</span></span><br><span class="line"><span class="comment"># ? What&#x27;s the description of your extension? Previews yarn.lock file</span></span><br><span class="line"><span class="comment"># ? Initialize a git repository? Yes</span></span><br><span class="line"><span class="comment"># ? Bundle the source code with webpack? No</span></span><br><span class="line"><span class="comment"># ? Which package manager to use? yarn</span></span><br><span class="line"></span><br><span class="line">$ code ./vscode-yarn-lock-preview</span><br></pre></td></tr></table></figure><h3 id="规范化项目"><a href="#规范化项目" class="headerlink" title="规范化项目"></a>规范化项目</h3><ol><li>代码规范配置 - <code>npx @luozhu/create-coding-style</code></li><li>按照新的代码规范格式化代码 - <code>yarn lint --fix</code></li><li>Git Commit 规范配置 - <code>npx @luozhu/create-commitlint</code></li></ol><h3 id="扩展信息配置"><a href="#扩展信息配置" class="headerlink" title="扩展信息配置"></a>扩展信息配置</h3><ol><li>扩展信息配置（package.json）<ol><li>配置 <a href="https://code.visualstudio.com/api/working-with-extensions/publishing-extension#create-a-publisher">publisher</a></li><li>添加上 <code>license</code> 字段。</li><li>添加 <code>repository</code> 字段。</li><li>配置 <code>icon</code>：128 x 128 像素图标的路径。</li></ol></li><li>修改 readme：因为初始的没法通过 <code>vsce package</code> 校验</li><li>使用 <code>npx vsce package</code> 尝试打包，确保没有错误和警告</li></ol><h3 id="使用-esbuild-打包"><a href="#使用-esbuild-打包" class="headerlink" title="使用 esbuild 打包"></a>使用 esbuild 打包</h3><p>在 <a href="https://juejin.cn/post/7000589186898231333/#heading-8">从零开发一款基于 webview 的 vscode 扩展</a> 中我们讲述了使用 esbuild 打包可以减小打包产物的体积以及加快调试速度。参考上一篇文章即可完成配置，这里就不赘述了。</p><h3 id="Don’t-repeat-yourself"><a href="#Don’t-repeat-yourself" class="headerlink" title="Don’t repeat yourself"></a>Don’t repeat yourself</h3><p>本着重复的工作都可以用脚本代替的原则，我 vscode 插件开发的最佳实践沉淀为了一个脚手架，执行 <code>yarn create @luozhu/vscode-extension</code> 即可快速开始开发 vscode 扩展。</p><h2 id="自定义编辑器原理"><a href="#自定义编辑器原理" class="headerlink" title="自定义编辑器原理"></a>自定义编辑器原理</h2><h3 id="工作机制"><a href="#工作机制" class="headerlink" title="工作机制"></a>工作机制</h3><p>前面我们已经知道了在 VS Code 中自定义编辑器有三种：自定义文本编辑器、自定义编辑器和自定义只读编辑器。三种类型的编辑器都会替换 VS Code 中的标准文本编辑器展示的位置。不同的是自定义文本编辑器由于是基于 VS Code 的标准文本文档模型，不需要开发者提供，比如 <a href="https://marketplace.visualstudio.com/items?itemName=SimonSiefke.svg-preview">Svg Preview</a> 插件。而自定义编辑器用于二进制文件的预览，因此需要开发者自己提供文档模型并自行实现诸如保存和备份这些功能比如 <a href="https://marketplace.visualstudio.com/items?itemName=hediet.vscode-drawio">Draw.io Integration</a> 插件。自定义只读编辑器则用于预览二进制文件，比如 <a href="https://marketplace.visualstudio.com/items?itemName=elypia.magick-image-reader">Magick Image Reader</a>。</p><p>了解了三种自定义编辑器的区别，我们来看下我们的插件属于哪种自定义编辑器。其实答案显而易见，<code>yarn.lock</code> 文件属于 VS Code 标准文档模型，我们选用最简单的自定义文本编辑器即可。</p><p>编写一个自定义编辑器会涉及视图、插件、文档模型、底层资源文件以及它们之间的交互，其大概原理如下图：</p><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/116948b627f4406ba51faa0353f147a7~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><p>由于预览 <code>yarn.lock</code> 不需要我们提供文档模型，因此我们只需要基于 <a href="https://vscode-api-cn.js.org/interfaces/vscode.CustomTextEditorProvider.html">CustomTextEditorProvider</a> 编写一个 CustomEditorProvider 给插件注册使用，然后编写我们的视图（用户界面），最后实现视图和插件的交互即可。</p><p>其中我们需要关注的点有如何开发视图、视图和插件如何通信、如何实现 CustomEditorProvider 以及如何注册 CustomEditorProvider。</p><h3 id="如何开发视图"><a href="#如何开发视图" class="headerlink" title="如何开发视图"></a>如何开发视图</h3><p>视图是通过 webview 实现的，所以你可以通过标准的 HTML、CSS 和 JavaScript 构建用户体验，也可以使用你熟悉的前端框架实现。</p><h3 id="视图如何与插件通信"><a href="#视图如何与插件通信" class="headerlink" title="视图如何与插件通信"></a>视图如何与插件通信</h3><p>由于 VS Code 的限制，Webview 是不能直接访问 VS Code API 以及发送网络请求的，但是它可以通过 postMessage 与插件进行双向通信。但是手写双向通信比较麻烦，尤其是进行网络请求时，webview 需要先发送发起网络请求的消息，插件侧注册的监听事件发起网络请求，插件等待网络请求返回后再发送消息将数据发给 webview，最后 webview 通过注册的监听事件获取信息。简要的流程如下：</p><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/20f34a967c234945afbaf9aaf2901163~tplv-k3u1fbpfcp-watermark.image" alt="webview 通信原理.png"></p><p>抛开具体实现来看这个交互就已经很反人类了，而且具体使用时还会面对 webview 和插件用到的 API 也不一样导致的转换成本和心智负担。为了解决这个痛点，我封装了 <a href="https://www.npmjs.com/package/@luozhu/vscode-channel">@luozhu&#x2F;vscode-channel</a> 来实现好用的双向通信。你可以简单地在 webview 侧通过 <code>call</code> 方法调用方法并等待处理结果，插件侧通过 <code>bind</code> 方法绑定事件处理。</p><h3 id="如何实现-CustomEditorProvider"><a href="#如何实现-CustomEditorProvider" class="headerlink" title="如何实现 CustomEditorProvider"></a>如何实现 CustomEditorProvider</h3><p>参考官方 Demo <a href="https://github.com/microsoft/vscode-extension-samples/blob/main/custom-editor-sample/src/catScratchEditor.ts#L16">catScratchEditor</a> 我们可以看到 <code>CatScratchEditorProvider</code> 是通过实现了 <code>vscode.CustomTextEditorProvider</code> 接口创建的自定义编辑器 Provider。而 <code>vscode.CustomTextEditorProvider</code> 这个接口只有一个方法  <code>resolveCustomTexEditor</code>。所以原理上我们只需要实现 <code>resolveCustomTexEditor</code> 这个方法即可。</p><h3 id="如何注册-CustomEditorProvider"><a href="#如何注册-CustomEditorProvider" class="headerlink" title="如何注册 CustomEditorProvider"></a>如何注册 CustomEditorProvider</h3><p>实现了自定义编辑器 Provider 之后，我们需要将其注册到插件中，VS Code 提供了 <code>vscode.window.registerCustomEditorProvider</code> 方法用来完成这项任务。</p><h2 id="自定义编辑器实现"><a href="#自定义编辑器实现" class="headerlink" title="自定义编辑器实现"></a>自定义编辑器实现</h2><p>纸上得来终觉浅，绝知此事要躬行。如果对前面的概念感到困惑，那接下来的实战可能会让你豁然开朗。</p><h3 id="声明自定义编辑器"><a href="#声明自定义编辑器" class="headerlink" title="声明自定义编辑器"></a>声明自定义编辑器</h3><p>自定义编辑器是通过 <code>package.json</code> 的贡献内容（<code>contributes</code>）的 <code>customEditors</code> 属性声明的，<code>customEditors</code> 提供自定义编辑器。它是一个数组，也就是说我们可以在一个扩展中提供多个自定义编辑器。我们声明的自定义编辑器如下：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">&quot;contributes&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;activationEvents&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">    <span class="string">&quot;onCustomEditor:yarn-lock-preview.yarnLock&quot;</span></span><br><span class="line">  <span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;customEditors&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;viewType&quot;</span><span class="punctuation">:</span> <span class="string">&quot;yarn-lock-preview.yarnLock&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;displayName&quot;</span><span class="punctuation">:</span> <span class="string">&quot;Preview yarn.lock&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;selector&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;filenamePattern&quot;</span><span class="punctuation">:</span> <span class="string">&quot;yarn.lock&quot;</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><span class="line">      <span class="attr">&quot;priority&quot;</span><span class="punctuation">:</span> <span class="string">&quot;option&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><ul><li><code>activationEvents</code> - 注册激活事件<ul><li><code>onCustomEditor:*</code> - 激活自定义编辑器的事件</li></ul></li><li><code>customEditors</code> - 提供的自定义编辑器。<ul><li><code>viewType</code> - 自定义编辑器的标识符。它在所有自定义编辑器中都必须是唯一的，因此建议将扩展 ID 作为 <code>viewType</code> 的一部分包括在内。在使用 <code>vscode.registerCustomEditorProvider</code> 和在 <code>onCustomEditor:${id}</code> 激活事件中注册自定义编辑器时，使用 <code>viewType</code>。</li><li><code>displayName</code> - 自定义编辑器的用户可读名称。当选择要使用的编辑器时，向用户显示此名称。</li><li><code>selector</code> - 为其启用了自定义编辑器的一组 glob。</li><li><code>priority</code> - （可选）确定自定义编辑器的使用时机。这个字段控制合适使用特定的自定义编辑器。<ul><li><code>option</code> - 在用户打开资源时不会自动使用此编辑器，但用户可使用 <code>Reopen With</code> 命令切换到此编辑器。</li><li><code>default</code> - 在用户打开资源时自动使用此编辑器，前提是没有为该资源注册其他默认的自定义编辑器。</li></ul></li></ul></li></ul><p>我们现在可以打开一个 <code>yarn.lock</code> 文件，并在命令面板中输入 <code>Reopen with</code> 选择我前面注册的 <code>Preview yarn.lock</code>：</p><p><img src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/376a6b66b7c24de589fb48a54d29efb6~tplv-k3u1fbpfcp-watermark.image" alt="Kapture 2021-09-14 at 11.28.37.gif"></p><p>这时我们可以看到一个空白的编辑器以及顶部的不会停止的加载进度条。</p><p>打开命令面板输出命令的方式对用户来说不是很友好，我们可以在编辑器菜单中添加一个切换按钮实现快速切换编辑器模式。首先我们在 <code>package.json</code> 中配置命令和菜单：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">&quot;contributes&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;commands&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;command&quot;</span><span class="punctuation">:</span> <span class="string">&quot;yarn-lock-preview.switchEditorMode&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;title&quot;</span><span class="punctuation">:</span> <span class="string">&quot;switch editor mode&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;icon&quot;</span><span class="punctuation">:</span> <span class="string">&quot;$(rocket)&quot;</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><span class="line">  <span class="attr">&quot;menus&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;editor/title&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;command&quot;</span><span class="punctuation">:</span> <span class="string">&quot;yarn-lock-preview.switchEditorMode&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;group&quot;</span><span class="punctuation">:</span> <span class="string">&quot;navigation&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><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><ul><li><code>commands</code> - 对命令面板提供命令。</li><li><code>menus</code> - 向编辑器提供菜单项。</li></ul><p>然后在 <code>src/extension</code> 中的 active 函数中注册命令及实现命令回调：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; commands, <span class="title class_">ExtensionContext</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;vscode&#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_">activate</span>(<span class="params">context: ExtensionContext</span>) &#123;</span><br><span class="line">  <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">&#x27;Congratulations, your extension &quot;yarn-lock-preview&quot; is now active!&#x27;</span>);</span><br><span class="line"></span><br><span class="line">  context.<span class="property">subscriptions</span>.<span class="title function_">push</span>(</span><br><span class="line">    commands.<span class="title function_">registerCommand</span>(<span class="string">&#x27;yarn-lock-preview.switchEditorMode&#x27;</span>, <span class="function">() =&gt;</span> &#123;</span><br><span class="line">      commands.<span class="title function_">executeCommand</span>(<span class="string">&#x27;workbench.action.reopenWithEditor&#x27;</span>);</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>我只找到了 <code>workbench.action.reopenWithEditor</code> 这个可以触发的命令，我本意是实现类似 git 的<strong>打开文件</strong>和<strong>打开更改</strong>的功能。由于学艺不精，<a href="https://github.dev/microsoft/vscode/blob/f66a3e06bcb9f000e5dc0ad0040ff9b32fc75c78/extensions/git/src/commands.ts#L655-L656">git.openFile</a> 的实现我还需要再研究一下，如果有大佬看到这来可以指点一下。目前效果如下：</p><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/cb03abea51634bd3a0a8c1a27b155ec0~tplv-k3u1fbpfcp-watermark.image" alt="Kapture 2021-09-14 at 12.46.00.gif"></p><h3 id="注册自定义编辑器-Provider"><a href="#注册自定义编辑器-Provider" class="headerlink" title="注册自定义编辑器 Provider"></a>注册自定义编辑器 Provider</h3><p>现在我们已经注册了自定义文档类型 <code>yarn-lock-preview.yarnLock</code> 和 <code>onCustomEditor:yarn-lock-preview.yarnLock</code> 激活事件。现在我们需要借助 <a href="https://vscode-api-cn.js.org/modules/window.html#registerCustomEditorProvider">window.registerCustomEditorProvider</a> 方法注册对应的自定义编辑器 Provider。</p><p>如果这时候尝试调用 <code>registerCustomEditorProvider</code> 方法，你会发现我们并没有一个自定义编辑器 provider 可以使用，下一节我们会实现自定义编辑器 Provider。</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> vscode <span class="keyword">from</span> <span class="string">&#x27;vscode&#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_">activate</span>(<span class="params">context: vscode.ExtensionContext</span>) &#123;</span><br><span class="line">  <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">&#x27;Congratulations, your extension &quot;yarn-lock-preview&quot; is now active!&#x27;</span>);</span><br><span class="line"></span><br><span class="line">  context.<span class="property">subscriptions</span>.<span class="title function_">push</span>(</span><br><span class="line">    vscode.<span class="property">window</span>.<span class="title function_">registerCustomEditorProvider</span>(</span><br><span class="line">      <span class="string">&#x27;yarn-lock-preview.yarnLock&#x27;</span>,</span><br><span class="line">      provider <span class="comment">// 自定义编辑器 provider 实例</span></span><br><span class="line">    );</span><br><span class="line">  );</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="实现自定义编辑器-Provider"><a href="#实现自定义编辑器-Provider" class="headerlink" title="实现自定义编辑器 Provider"></a>实现自定义编辑器 Provider</h3><p>由于我们要预览的文件是 VS Code 的标准文档模型，所以我们需要基于 <code>CustomTextEditorProvider</code> 这个接口封装一个类来实现它。为此我们新建一个 <code>YarnLockEditorProvider.ts</code> 文件，该文件的最小实现如下：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> vscode <span class="keyword">from</span> <span class="string">&#x27;vscode&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">YarnLockEditorProvider</span> <span class="keyword">implements</span> vscode.<span class="property">CustomTextEditorProvider</span> &#123;</span><br><span class="line">  <span class="comment">// 将 context 注入 this 对象</span></span><br><span class="line">  <span class="title function_">constructor</span>(<span class="params"><span class="keyword">private</span> <span class="keyword">readonly</span> context: vscode.ExtensionContext</span>) &#123;&#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">/**</span></span><br><span class="line"><span class="comment">   * 当自定义编辑器打开时调用。</span></span><br><span class="line"><span class="comment">   */</span></span><br><span class="line">  <span class="keyword">async</span> <span class="title function_">resolveCustomTextEditor</span>(</span><br><span class="line">    <span class="attr">_document</span>: vscode.<span class="property">TextDocument</span>,</span><br><span class="line">    <span class="attr">webviewPanel</span>: vscode.<span class="property">WebviewPanel</span>,</span><br><span class="line">    <span class="attr">_token</span>: vscode.<span class="property">CancellationToken</span></span><br><span class="line">  ): <span class="title class_">Promise</span>&lt;<span class="built_in">void</span>&gt; &#123;</span><br><span class="line">    <span class="comment">// 给 webview 设置初始内容</span></span><br><span class="line">    webviewPanel.<span class="property">webview</span>.<span class="property">options</span> = &#123;</span><br><span class="line">      <span class="attr">enableScripts</span>: <span class="literal">true</span>, <span class="comment">// 允许在 webview 中运行脚本</span></span><br><span class="line">    &#125;;</span><br><span class="line">    webviewPanel.<span class="property">webview</span>.<span class="property">html</span> = <span class="variable language_">this</span>.<span class="title function_">getHtmlForWebview</span>();</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">private</span> <span class="title function_">getHtmlForWebview</span>(): <span class="built_in">string</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="string">`</span></span><br><span class="line"><span class="string">        &lt;!DOCTYPE html&gt;</span></span><br><span class="line"><span class="string">        &lt;html lang=&quot;en&quot;&gt;</span></span><br><span class="line"><span class="string">        &lt;head&gt;</span></span><br><span class="line"><span class="string">            &lt;meta charset=&quot;UTF-8&quot;&gt;</span></span><br><span class="line"><span class="string">            &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;&gt;</span></span><br><span class="line"><span class="string">            &lt;title&gt;Cat Coding&lt;/title&gt;</span></span><br><span class="line"><span class="string">        &lt;/head&gt;</span></span><br><span class="line"><span class="string">        &lt;body&gt;</span></span><br><span class="line"><span class="string">            &lt;img src=&quot;https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif&quot; width=&quot;300&quot; /&gt;</span></span><br><span class="line"><span class="string">        &lt;/body&gt;</span></span><br><span class="line"><span class="string">        &lt;/html&gt;</span></span><br><span class="line"><span class="string">    `</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="title class_">YarnLockEditorProvider</span>;</span><br></pre></td></tr></table></figure><ul><li><code>class YarnLockEditorProvider implements vscode.CustomTextEditorProvider</code> - 声明一个类实现自定义文本编辑器 provider。</li><li><a href="https://vscode-api-cn.js.org/interfaces/CustomTextEditorProvider.html#resolveCustomTextEditor">resolveCustomTextEditor</a>：根据给定的文本资源解析自定义编辑器。当用户第一次打开一个 <code>CustomTextEditorProvider</code> 的资源时，或者当他们使用这个 <code>CustomTextEditorProvider</code> 重新打开一个现有的编辑器时，该方法将被调用。</li><li><code>webviewPanel.webview.options</code> - 配置 webview 选项，这里我们配置了允许使用脚本。</li><li><code>webviewPanel.webview.html = this.getHtmlForWebview()</code> - 为 HTML 设置初始内容。</li></ul><p>为了简化初始化操作，我们为 <code>YarnLockEditorProvider</code> 类封装一个 <code>register</code> 静态方法：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">static</span> <span class="title function_">register</span>(<span class="attr">context</span>: vscode.<span class="property">ExtensionContext</span>): vscode.<span class="property">Disposable</span> &#123;</span><br><span class="line">  <span class="keyword">const</span> provider = <span class="keyword">new</span> <span class="title class_">YarnLockEditorProvider</span>(context);</span><br><span class="line">  <span class="keyword">const</span> providerRegistration = vscode.<span class="property">window</span>.<span class="title function_">registerCustomEditorProvider</span>(</span><br><span class="line">    <span class="title class_">YarnLockEditorProvider</span>.<span class="property">viewType</span>,</span><br><span class="line">    provider,</span><br><span class="line">    &#123;</span><br><span class="line">      <span class="attr">webviewOptions</span>: &#123;</span><br><span class="line">        <span class="attr">retainContextWhenHidden</span>: <span class="literal">true</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> providerRegistration;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">readonly</span> viewType = <span class="string">&#x27;yarn-lock-preview.yarnLock&#x27;</span>;</span><br></pre></td></tr></table></figure><p>现在我们就可以注册我们的自定义编辑器 provider 了，在 <code>src/extension.ts</code> 的 <code>activate</code> 方法中调用 <code>YarnLockEditorProvider.register(context)</code> 得到注册的自定义编辑器，然后 <code>push</code> 到代理监听数组中即可：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> vscode <span class="keyword">from</span> <span class="string">&#x27;vscode&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> <span class="title class_">YarnLockEditorProvider</span> <span class="keyword">from</span> <span class="string">&#x27;./YarnLockEditorProvider&#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_">activate</span>(<span class="params">context: vscode.ExtensionContext</span>) &#123;</span><br><span class="line">  context.<span class="property">subscriptions</span>.<span class="title function_">push</span>(<span class="title class_">YarnLockEditorProvider</span>.<span class="title function_">register</span>(context));</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这样一个简单但完整的自定义编辑器就完成了：</p><p><img src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e6addd1cb739448db52db1767154fe65~tplv-k3u1fbpfcp-watermark.image" alt="Kapture 2021-09-14 at 17.00.55.gif"></p><h2 id="yarn-lock-可视化预览实现"><a href="#yarn-lock-可视化预览实现" class="headerlink" title="yarn.lock 可视化预览实现"></a>yarn.lock 可视化预览实现</h2><p>前面我们已经顺利带大家实现了在自定义编辑器中看猫的功能。当然了，我们的最终目标可不是看黑猫敲代码。这一章我们将挑战最后的 Boss：实现一个可以搜索的 yarn.lock 依赖 Json 树。</p><h3 id="解析-yarn-lock-文件"><a href="#解析-yarn-lock-文件" class="headerlink" title="解析 yarn.lock 文件"></a>解析 yarn.lock 文件</h3><p>解决了技术问题，现在我们来看下业务问题。我们的痛点是 yarn.lock 文件是纯文本的，阅读起来比较困难，需要一个更好的展示形式。那我们可以第一步肯定是将文本文件转成更易处理的资源，作为前端，当然首选 JSON。借助 Yarn 官方的 <a href="https://www.npmjs.com/package/@yarnpkg/lockfile">@yarnpkg&#x2F;lockfile</a> 工具我们就可以实现这个功能。我们来编写 demo 试一下。</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> * <span class="keyword">as</span> lockfile <span class="keyword">from</span> <span class="string">&#x27;@yarnpkg/lockfile&#x27;</span>;</span><br><span class="line"><span class="keyword">class</span> <span class="title class_">YarnLockEditorProvider</span> <span class="keyword">implements</span> vscode.<span class="property">CustomTextEditorProvider</span> &#123;</span><br><span class="line"></span><br><span class="line">  <span class="comment">/**</span></span><br><span class="line"><span class="comment">   * 当自定义编辑器打开时调用。</span></span><br><span class="line"><span class="comment">   */</span></span><br><span class="line">  <span class="keyword">async</span> <span class="title function_">resolveCustomTextEditor</span>(</span><br><span class="line">    <span class="attr">document</span>: vscode.<span class="property">TextDocument</span>,</span><br><span class="line">    <span class="attr">webviewPanel</span>: vscode.<span class="property">WebviewPanel</span>,</span><br><span class="line">    <span class="attr">_token</span>: vscode.<span class="property">CancellationToken</span></span><br><span class="line">  ): <span class="title class_">Promise</span>&lt;<span class="built_in">void</span>&gt; &#123;</span><br><span class="line">    ...</span><br><span class="line">    <span class="comment">// 获取纯文本并解析成 json 数据</span></span><br><span class="line">    <span class="keyword">const</span> json = lockfile.<span class="title function_">parse</span>(<span class="variable language_">document</span>.<span class="title function_">getText</span>()).<span class="property">object</span>;</span><br><span class="line">    <span class="comment">// 将 JSON 字符串传递给 HTML 拼装方法展示</span></span><br><span class="line">    webviewPanel.<span class="property">webview</span>.<span class="property">html</span> = <span class="variable language_">this</span>.<span class="title function_">getHtmlForWebview</span>(<span class="title class_">JSON</span>.<span class="title function_">stringify</span>(json));</span><br><span class="line">    ...</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">private</span> <span class="title function_">getHtmlForWebview</span>(<span class="attr">json</span>: <span class="built_in">string</span>): <span class="built_in">string</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="string">`</span></span><br><span class="line"><span class="string">        &lt;!DOCTYPE html&gt;</span></span><br><span class="line"><span class="string">        &lt;html lang=&quot;en&quot;&gt;</span></span><br><span class="line"><span class="string">        &lt;head&gt;</span></span><br><span class="line"><span class="string">            &lt;meta charset=&quot;UTF-8&quot;&gt;</span></span><br><span class="line"><span class="string">            &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;&gt;</span></span><br><span class="line"><span class="string">            &lt;title&gt;Cat Coding&lt;/title&gt;</span></span><br><span class="line"><span class="string">        &lt;/head&gt;</span></span><br><span class="line"><span class="string">        &lt;body&gt;</span></span><br><span class="line"><span class="string">          &lt;h1&gt;JSON 数据&lt;/h1&gt;</span></span><br><span class="line"><span class="string">          &lt;p&gt;<span class="subst">$&#123;json&#125;</span>&lt;/p&gt;</span></span><br><span class="line"><span class="string">        &lt;/body&gt;</span></span><br><span class="line"><span class="string">        &lt;/html&gt;</span></span><br><span class="line"><span class="string">    `</span>;</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://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/979363c7be8c42d59e2aa564cf687c26~tplv-k3u1fbpfcp-watermark.image" alt="Kapture 2021-09-14 at 18.59.12.gif"></p><h3 id="webview-集成-umijs"><a href="#webview-集成-umijs" class="headerlink" title="webview 集成 umijs"></a>webview 集成 umijs</h3><p>参考 <a href="https://juejin.cn/post/7000589186898231333/#heading-9">《# 从零开发一款基于 webview 的 vscode 扩展》</a> 和 <a href="https://github.com/youngjuning/vscode-juejin-me">vscode-juejin-me</a> 初始化 umijs 项目并做一些修剪适配工作。然后使用 <code>@luozhu/vscode-utils</code> 的 <code>getUmiHTMLContent</code> 方法获取 HTML 内容：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">webviewPanel.<span class="property">webview</span>.<span class="property">html</span> = <span class="title function_">getUmiHTMLContent</span>(<span class="variable language_">this</span>.<span class="property">context</span>, webviewPanel, &#123;</span><br><span class="line">  <span class="attr">title</span>: <span class="string">&#x27;Yarn Lock Preview&#x27;</span>,</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><h3 id="webview-内容更新"><a href="#webview-内容更新" class="headerlink" title="webview 内容更新"></a>webview 内容更新</h3><p>集成 umijs 后我们通过 <code>webviewPanel.webview.html</code> 设置的初始内容是空的。如果我们想要将文本传递从插件传递到 webview，我们需要进行一次通信。VS Code 中由于种种限制造成了编写通信的代码很繁琐。基本依赖 webview 的插件都会将通信机制给封装了。我这里的封装思路是借鉴了 <a href="https://www.npmjs.com/package/js-channel">js-channel</a> 实现了一个使用起来心智负担最小的 <a href="https://github.com/youngjuning/luozhu/tree/main/packages/vscode-channel#readme">@luozhu&#x2F;vscode-channel</a>。借助这个工具我们可以很方便地实现更新 webview 的操作：</p><p><strong>插件侧发送更新消息：</strong></p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">Channel</span> <span class="keyword">from</span> <span class="string">&#x27;@luozhu/vscode-channel&#x27;</span>;</span><br><span class="line"><span class="keyword">class</span> <span class="title class_">YarnLockEditorProvider</span> <span class="keyword">implements</span> vscode.<span class="property">CustomTextEditorProvider</span> &#123;</span><br><span class="line">  <span class="keyword">async</span> <span class="title function_">resolveCustomTextEditor</span>(</span><br><span class="line">    <span class="attr">document</span>: vscode.<span class="property">TextDocument</span>,</span><br><span class="line">    <span class="attr">webviewPanel</span>: vscode.<span class="property">WebviewPanel</span>,</span><br><span class="line">    <span class="attr">_token</span>: vscode.<span class="property">CancellationToken</span></span><br><span class="line">  ): <span class="title class_">Promise</span>&lt;<span class="built_in">void</span>&gt; &#123;</span><br><span class="line">    <span class="comment">// 给 webview 设置初始内容</span></span><br><span class="line">    webviewPanel.<span class="property">webview</span>.<span class="property">options</span> = &#123;</span><br><span class="line">      <span class="attr">enableScripts</span>: <span class="literal">true</span>,</span><br><span class="line">    &#125;;</span><br><span class="line"></span><br><span class="line">    webviewPanel.<span class="property">webview</span>.<span class="property">html</span> = <span class="title function_">getUmiHTMLContent</span>(<span class="variable language_">this</span>.<span class="property">context</span>, webviewPanel, &#123;</span><br><span class="line">      <span class="attr">title</span>: <span class="string">&#x27;Yarn Lock Preview&#x27;</span>,</span><br><span class="line">    &#125;);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 初始化一个 channel 对象</span></span><br><span class="line">    <span class="keyword">const</span> channel = <span class="keyword">new</span> <span class="title class_">Channel</span>(<span class="variable language_">this</span>.<span class="property">context</span>, webviewPanel);</span><br><span class="line">    <span class="keyword">const</span> json = lockfile.<span class="title function_">parse</span>(<span class="variable language_">document</span>.<span class="title function_">getText</span>()).<span class="property">object</span>;</span><br><span class="line">    <span class="comment">// 触发 updateWebview 事件，并将文本作为参数传入</span></span><br><span class="line">    channel.<span class="title function_">call</span>(<span class="string">&#x27;updateWebview&#x27;</span>, json);</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>webview 侧监听消息：</strong></p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">Channel</span> <span class="keyword">from</span> <span class="string">&#x27;@luozhu/vscode-channel&#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_">HomePage</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> [data, setData] = <span class="title class_">React</span>.<span class="title function_">useState</span>(&#123;&#125;);</span><br><span class="line">  <span class="title class_">React</span>.<span class="title function_">useEffect</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">    channel.<span class="title function_">bind</span>(<span class="string">&#x27;updateWebview&#x27;</span>, <span class="function"><span class="params">message</span> =&gt;</span> &#123;</span><br><span class="line">      <span class="title function_">setData</span>(message.<span class="property">params</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="文本内容同步"><a href="#文本内容同步" class="headerlink" title="文本内容同步"></a>文本内容同步</h3><p>前面一章我们实现了初始内容的同步，但是内容不是一成不变的，<code>yarn.lock</code> 很可能会变化，这时候我们也需要相应地更新我们内容，这个需求我们需要借助 <code>vscode.workspace.onDidChangeTextDocument</code> 事件监听来实现：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 仍然是在 resolveCustomTextEditor 方法中实现</span></span><br><span class="line"><span class="comment">// 由于需要对 json 数据处理和重复调用更新方法，封装了此方法</span></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">updateWebview</span>(<span class="params">textDocument: vscode.TextDocument</span>) &#123;</span><br><span class="line">  <span class="keyword">let</span> json = lockfile.<span class="title function_">parse</span>(textDocument.<span class="title function_">getText</span>());</span><br><span class="line">  <span class="keyword">switch</span> (json.<span class="property">type</span>) &#123;</span><br><span class="line">    <span class="keyword">case</span> <span class="string">&#x27;merge&#x27;</span>:</span><br><span class="line">      <span class="comment">// <span class="doctag">TODO:</span> 处理 merge type</span></span><br><span class="line">      <span class="keyword">break</span>;</span><br><span class="line">    <span class="keyword">case</span> <span class="string">&#x27;conflict&#x27;</span>:</span><br><span class="line">      <span class="comment">// <span class="doctag">TODO:</span> 处理 conflict type</span></span><br><span class="line">      <span class="keyword">break</span>;</span><br><span class="line">    <span class="attr">default</span>:</span><br><span class="line">      json = json.<span class="property">object</span>;</span><br><span class="line">  &#125;</span><br><span class="line">  channel.<span class="title function_">call</span>(<span class="string">&#x27;updateWebview&#x27;</span>, json);</span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">// 注册钩子事件处理程序，这样我们就可以使 webview 与文本文档同步。</span></span><br><span class="line"><span class="comment">//</span></span><br><span class="line"><span class="comment">// 文本文件作为我们的模型，所以我们必须将文件中的变化同步到我们的编辑器。</span></span><br><span class="line"><span class="comment">// 请记住，一个文本文件也可以在多个自定义编辑器之间共享（例如，当你分割一个自定义编辑器时就会发生这种情况）。</span></span><br><span class="line"><span class="keyword">const</span> changeDocumentSubscription = vscode.<span class="property">workspace</span>.<span class="title function_">onDidChangeTextDocument</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">document</span>.<span class="property">uri</span>.<span class="title function_">toString</span>() === <span class="variable language_">document</span>.<span class="property">uri</span>.<span class="title function_">toString</span>()) &#123;</span><br><span class="line">    <span class="title function_">updateWebview</span>(e.<span class="property">document</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">webviewPanel.<span class="title function_">onDidDispose</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">  changeDocumentSubscription.<span class="title function_">dispose</span>();</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><h3 id="实现可搜索的-JSON-View"><a href="#实现可搜索的-JSON-View" class="headerlink" title="实现可搜索的 JSON View"></a>实现可搜索的 JSON View</h3><p>搜索实现不复杂，大家感兴趣可以直接阅读源码，UI 用的是 umi 自带的 antd。JSON View 我选用了 <code>react-json-view</code>，这个有几点比较有意思可以讲一下。</p><ol><li>数据量过大时渲染性能差需要判断是否折叠</li></ol><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">ReactJson</span> <span class="keyword">from</span> <span class="string">&#x27;react-json-view&#x27;</span>;</span><br><span class="line">...</span><br><span class="line">&lt;<span class="title class_">ReactJson</span></span><br><span class="line">  shouldCollapse=&#123;<span class="function"><span class="params">filed</span> =&gt;</span> &#123;</span><br><span class="line">    <span class="comment">// 除了根目录都折叠</span></span><br><span class="line">    <span class="keyword">if</span> (filed.<span class="property">name</span>) &#123;</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;&#125;</span><br><span class="line">/&gt;</span><br></pre></td></tr></table></figure><ol start="2"><li>样式符合当前主题</li></ol><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">ReactJson</span> <span class="keyword">from</span> <span class="string">&#x27;react-json-view&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">getCssVar</span> = (<span class="params">cssVar: <span class="built_in">string</span></span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> htmlStyle = <span class="variable language_">document</span>.<span class="property">documentElement</span>.<span class="property">style</span>;</span><br><span class="line">  <span class="keyword">return</span> htmlStyle.<span class="title function_">getPropertyValue</span>(cssVar).<span class="title function_">trim</span>();</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line">...</span><br><span class="line">&lt;<span class="title class_">ReactJson</span></span><br><span class="line">  style=&#123;&#123;</span><br><span class="line">    <span class="attr">backgroundColor</span>: <span class="title function_">getCssVar</span>(<span class="string">&#x27;--vscode-editor-background&#x27;</span>),</span><br><span class="line">    <span class="attr">fontSize</span>: <span class="title function_">getCssVar</span>(<span class="string">&#x27;--vscode-editor-font-size&#x27;</span>),</span><br><span class="line">  &#125;&#125;</span><br><span class="line">/&gt;</span><br></pre></td></tr></table></figure><ol start="3"><li>亮暗主题适配</li></ol><p>完美的适配肯定是要随着主题的更改切换 ReactJson 的主题，我们需要在插件侧发起通知：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 初始主题</span></span><br><span class="line">channel.<span class="title function_">call</span>(<span class="string">&#x27;updateColorTheme&#x27;</span>, vscode.<span class="property">window</span>.<span class="property">activeColorTheme</span>);</span><br><span class="line"><span class="comment">// 监听主题改变事件</span></span><br><span class="line">vscode.<span class="property">window</span>.<span class="title function_">onDidChangeActiveColorTheme</span>(<span class="function"><span class="params">colorTheme</span> =&gt;</span> &#123;</span><br><span class="line">  channel.<span class="title function_">call</span>(<span class="string">&#x27;updateColorTheme&#x27;</span>, colorTheme);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p>然后在 webview 侧监听 <code>updateColorTheme</code> 事件：</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> [theme, setTheme] = <span class="title class_">React</span>.<span class="property">useState</span>&lt;<span class="title class_">ThemeKeys</span>&gt;();</span><br><span class="line">...</span><br><span class="line"><span class="title class_">React</span>.<span class="title function_">useEffect</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">channel.<span class="title function_">bind</span>(<span class="string">&#x27;updateColorTheme&#x27;</span>, <span class="function"><span class="params">message</span> =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">const</span> &#123; kind &#125; = message.<span class="property">params</span>;</span><br><span class="line">  <span class="title function_">setTheme</span>(kind === <span class="number">1</span> ? <span class="string">&#x27;rjv-default&#x27;</span> : <span class="string">&#x27;monokai&#x27;</span>);</span><br><span class="line">&#125;);</span><br><span class="line">&#125;, []);</span><br><span class="line">...</span><br><span class="line">&lt;<span class="title class_">ReactJson</span></span><br><span class="line">  theme=&#123;theme&#125;</span><br><span class="line">/&gt;</span><br></pre></td></tr></table></figure><p>文章开头已经演示了用户界面，这里放一张黑色主题效果吧：</p><p><img src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/8449b0b270aa423da2ede94f4ad10805~tplv-k3u1fbpfcp-watermark.image" alt="image.png"></p><h2 id="涉及-API-附录"><a href="#涉及-API-附录" class="headerlink" title="涉及 API 附录"></a>涉及 API 附录</h2><p>VS Code 插件开发涉及知识点比较多，每种插件类型都有自己的套路和 API。基于关注分离原则，这里列出了和自定义编辑器相关的一些 API，方便大家以及未来的我查阅。</p><h3 id="vscode-commands-registerCommand"><a href="#vscode-commands-registerCommand" class="headerlink" title="vscode.commands.registerCommand"></a>vscode.commands.registerCommand</h3><p>注册一个可以被键映射、菜单项、动作或直接调用的命令。用一个现有的命令标识符注册两次一个命令将导致错误。</p><h3 id="vscode-TextDocument"><a href="#vscode-TextDocument" class="headerlink" title="vscode.TextDocument"></a>vscode.TextDocument</h3><h3 id="vscode-window-registerCustomEditorProvider"><a href="#vscode-window-registerCustomEditorProvider" class="headerlink" title="vscode.window.registerCustomEditorProvider"></a>vscode.window.registerCustomEditorProvider</h3><p>为 <code>customEditors</code> 扩展功能点贡献的 <code>viewType</code> 注册一个自定义编辑器的 Provider。</p><p>当打开一个自定义编辑器被时，会触发一个 <code>onCustomEditor:viewType</code> 活动事件。你的扩展必须为 <code>viewType</code> 注册一个 <code>CustomTextEditorProvider</code>、<code>CustomReadonlyEditor</code>  或 <code>CustomEditorProvider</code> 作为激活的一部分。</p><h3 id="vscode-CustomTextEditorProvider"><a href="#vscode-CustomTextEditorProvider" class="headerlink" title="vscode.CustomTextEditorProvider"></a>vscode.CustomTextEditorProvider</h3><p>基于文本的自定义编辑器 provider。</p><p>基于文本的自定义编辑器使用 TextDocument 作为其数据模型。因为它允许编辑器处理许多常见的操作，如撤销和备份。provider 负责在 webview 和 TextDocument 之间同步文本变化。</p><h4 id="CustomTextEditorProvider-resolveCustomTextEditor"><a href="#CustomTextEditorProvider-resolveCustomTextEditor" class="headerlink" title="CustomTextEditorProvider.resolveCustomTextEditor"></a>CustomTextEditorProvider.resolveCustomTextEditor</h4><p>根据给定文本资源解析一个自定义编辑器。该方法将在用户第一次为 <code>CustomTextEditorProvider</code> 打开一个资源时，或者他们使用这个 <code>CustomTextEditorProvider</code> 重新打开已经存在的编辑器时被调用。</p><h3 id="vscode-CustomReadonlyEditorProvider"><a href="#vscode-CustomReadonlyEditorProvider" class="headerlink" title="vscode.CustomReadonlyEditorProvider"></a>vscode.CustomReadonlyEditorProvider</h3><p>使用自定义文档模型的只读自定义编辑器 Provider。</p><p>自定义只读编辑器使用 <code>CustomDocument</code>，而不是 <code>TextDocument</code>。</p><p>当处理二进制文件或者更复杂的场景时，你应该使用这个类型的自定义编辑器。简单的基于文本的文档请使用 <code>CustomTextEditorProvider</code>。</p><h3 id="vscode-CustomEditorProvider"><a href="#vscode-CustomEditorProvider" class="headerlink" title="vscode.CustomEditorProvider"></a>vscode.CustomEditorProvider</h3><p>使用自定义文档模型的可编辑自定义编辑器的 provider。</p><p>自定义编辑器使用 <code>CustomDocument</code>，而不是 <code>TextDocument</code>。这使得扩展程序可以完全控制编辑、保存和备份等操作。</p><p>当处理二进制文件或者更复杂的场景时，你应该使用这个类型的自定义编辑器。简单的基于文本的文档请使用 <code>CustomTextEditorProvider</code>。</p><h3 id="vscode-WebviewPanel"><a href="#vscode-WebviewPanel" class="headerlink" title="vscode.WebviewPanel"></a>vscode.WebviewPanel</h3><p>一个包含 webview 的面板。</p><blockquote><p>本文首发于 <a href="https://juejin.cn/user/325111174662855/posts">掘金专栏</a>，同步于 <a href="https://blog.zisheng.pro/">紫升的博客</a> 和公众号 <a href="https://cdn.jsdelivr.net/gh/youngjuning/images/20210418112129.jpeg">紫升早茶馆</a>。</p></blockquote>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/1bbed3d2a249/</id>
    <link href="https://blog.zisheng.pro/posts/1bbed3d2a249/"/>
    <published>2023-02-26T05:56:50.000Z</published>
    <summary>人们总是喜欢炫耀自己的新技能，紫升也不例外，毕竟新技能需要大量的实战练习。在入门了 VS Code 扩展开发后，遇到开发痛点我就习惯地用 VS Code 插件的思路去解决。不过大多扩展 idea 都已经名花有主，直到我想到可以将 yarn.lock 文件可视化并搜索相关插件无果后，我知道机会来了。</summary>
    <title>如何开发一款 VS Code yarn.lock 预览插件</title>
    <updated>2026-09-18T08:15:28.636Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="Google" scheme="https://blog.zisheng.pro/categories/Google/"/>
    <category term="Google AdSense" scheme="https://blog.zisheng.pro/categories/Google/Google-AdSense/"/>
    <category term="站点运营" scheme="https://blog.zisheng.pro/categories/%E7%AB%99%E7%82%B9%E8%BF%90%E8%90%A5/"/>
    <category term="广告" scheme="https://blog.zisheng.pro/categories/%E7%AB%99%E7%82%B9%E8%BF%90%E8%90%A5/%E5%B9%BF%E5%91%8A/"/>
    <category term="Google AdSense" scheme="https://blog.zisheng.pro/tags/Google-AdSense/"/>
    <category term="google adsense cpc" scheme="https://blog.zisheng.pro/tags/google-adsense-cpc/"/>
    <category term="google adsense cpc 优化" scheme="https://blog.zisheng.pro/tags/google-adsense-cpc-%E4%BC%98%E5%8C%96/"/>
    <category term="优化 google adsense cpc" scheme="https://blog.zisheng.pro/tags/%E4%BC%98%E5%8C%96-google-adsense-cpc/"/>
    <content>
      <![CDATA[<blockquote><p>2025 年更新：RPM 同样适用！！！</p></blockquote><p>作为一个技术博客的博主，我一直在寻找提高 Google Adsense CPC 的方法，因为 CPC 是 Adsense 的收入来源，所以 CPC 越高，收入越多。本文就将介绍如何提高 Google Adsense CPC。</p><h2 id="CPC-是什么"><a href="#CPC-是什么" class="headerlink" title="CPC 是什么"></a>CPC 是什么</h2><p>每次点击费用 (CPC，Cost Per Click) 是每次用户点击您的广告时您赚取的金额，即单价。任何广告的 CPC 均由广告商确定；一些广告商可能愿意为每次点击支付比其他人更多的费用，这取决于他们宣传的内容。</p><h3 id="CTR-vs-CPC"><a href="#CTR-vs-CPC" class="headerlink" title="CTR vs CPC"></a>CTR vs CPC</h3><p>对于标准广告，广告点击率 (CTR，Clickthrough Rate) 是指广告的点击次数与其展示次数的比值。例如，如果您的广告通过 1000 次广告展示获得了 5 次点击，则您的广告点击率为 0.5%。</p><p>CTR 固然重要，但是如果你的单价很低，那么你的收入也会很低。因此，某种意义上来说 CPC 的优化是比 CTR 更紧迫的，前期没有大流量的情况下，CPC 尤其重要。</p><h2 id="影响-CPC-的几个因素"><a href="#影响-CPC-的几个因素" class="headerlink" title="影响 CPC 的几个因素"></a>影响 CPC 的几个因素</h2><ul><li>市场趋势：比如学生暑假、雨伞卖家、羽绒服等等。你可以通过 Google Trends 和 Google Adsense 的报告来查看市场趋势。</li><li>屏蔽控制：可以屏蔽一些敏感类别，注意不能屏蔽太多，否则会影响广告收入。</li><li>广告单元尺寸：将固定尺寸的广告单元替换为自适应尺寸的广告单元。<br><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1677292096541.png" alt="Google Adsense 广告单元尺寸"></li><li>主题(关键字)：你要选择一些高CPC的主题去经营你的博客。</li><li>内容：你必须写一些可以回答读者查询的内容。因此，在撰写博客之前，请尝试查找读者在互联网上搜索的内容，然后尝试向他们提供解决方案。请记住，您应该始终直接与读者互动。</li><li>区域：可以提高单价(CPC)的最重要因素之一就是您要定位的国家&#x2F;地区。例如，点击来自美国的广告可以支付高达2美元到3美元的费用，点击来自印度的同一广告最终只能支付20到30美分。</li><li>图文形式：图片广告在点击率较高(CTR)方面表现较好，但每次点击费用(CPC)不如文字广告效果好。</li><li>位置：在博文中放置 2 个广告，在帖子外放置 1 个广告。至于放置内部然后在标题下方的博客顶部放置一个广告，并在博客帖子之间放置第二个广告。剩余的一个广告可以放在您帖子外面的右侧。最高点击次数和更高每次点击费用将来自博客帖子中的 2 个广告，尤其是在顶部。</li></ul><h2 id="高-CPC-关键词"><a href="#高-CPC-关键词" class="headerlink" title="高 CPC 关键词"></a>高 CPC 关键词</h2><ol><li>保险（Insurance）：$61 CPC</li><li>燃气&#x2F;电力（Gas&#x2F;Electricity）：$58 CPC</li><li>贷款（Loans）：$50 CPC</li><li>律师（Attorney）：$48 CPC</li><li>抵押（Mortgage）：$46 CPC</li><li>律师（Lawyer）：$42 CPC</li><li>捐助（Donate）：$42 CPC</li><li>电话会议（Conference Call）：$42 CPC</li></ol><p>上面列的不一定符合博客的主题，这里列出一些其他高回报率的主题(关键字)，我按照CPC递减的顺序进行排列：</p><ul><li>域名 – 互联网域名博客像GoDaddy，Namecheap 等。它支付最高的CPC；</li><li>电子产品(Gadgets) – 比如苹果类的科技电子产品</li><li>GOOGLE – GOOGLE产品</li><li>微软产品 – 微软办公类产品</li><li>数字加密货币</li></ul><blockquote><p>注意：约会和浪漫 CPC 最低</p></blockquote><h2 id="屏蔽控制功能"><a href="#屏蔽控制功能" class="headerlink" title="屏蔽控制功能"></a>屏蔽控制功能</h2><p>善用该功能可以确保所展示的广告不会影响您的品牌形象，这些工具可以帮助您在网站上提供最佳广告体验。我们最关心当然还是如何帮助提高 CPC。</p><h3 id="广告客户网址"><a href="#广告客户网址" class="headerlink" title="广告客户网址"></a>广告客户网址</h3><p>该功能用以屏蔽由具体广告客户投放的广告，可以通过下面的方式访问：</p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1677287023394.png" alt="Google Adsense 广告客户网址"></p><p>然后将下面的网址添加到屏蔽列表中：</p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1677287190848.png" alt="Google Adsense 低 CPC 广告客户网站"></p><div class="note primary modern"><p>数据全是自己手动统计，如有需要可以加我微信：<code>AAIII1689888</code>（备注 CPC），发 <code>$10</code> 红包获取。</p></div><h3 id="敏感类别"><a href="#敏感类别" class="headerlink" title="敏感类别"></a>敏感类别</h3><p>该功能用以根据您的受众群体，管理您网站上最适合展示的广告类型，禁用不适合您网站的广告类型。例如，您的网站可能包含有关性内容，但您不希望在网站上显示有关性广告。您可以在此处禁用这些广告类型。</p><p>这样做不仅可以帮助您遵守法律法规，还可以帮助您提高广告收入，下图是我禁用的一些类别。</p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1677288027340.png" alt="Google Adsense 敏感类别"></p><h3 id="常规类别"><a href="#常规类别" class="headerlink" title="常规类别"></a>常规类别</h3><p>屏蔽较常规的广告类别。注意，这样做可能会导致收入减少。但是也有一些大家公认的低 CPC 类别，建议屏蔽掉：</p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/202311022322047.png" alt="Google Adsense 常规类别"></p><p>可以看到健康类别展示占比 4%，收入占比 3.7%，虽然名字很健康，但是效益比较差。</p><h3 id="广告联盟"><a href="#广告联盟" class="headerlink" title="广告联盟"></a>广告联盟</h3><h4 id="低-CPC-广告联盟"><a href="#低-CPC-广告联盟" class="headerlink" title="低 CPC 广告联盟"></a>低 CPC 广告联盟</h4><p>你可以屏蔽不适合自己品牌的第三方广告联盟，但也有一些公认的低 CPC 广告联盟，建议屏蔽掉：</p><div class="note primary modern"><p>数据全是自己手动统计，如有需要可以加我微信：<code>AAIII1689888</code>（备注 CPC），发 <code>$20</code> 红包获取。</p></div><p>除此之外你还可以通过分析广告网络报告来决定是否屏蔽某个广告联盟。</p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1677288686112.png" alt="Google Adsense 广告网络"></p><p>屏蔽原则是：低 CPC 的广告联盟，低 CTR 的广告联盟，低收入的广告联盟，都可以屏蔽掉。</p><h4 id="高-CPC-广告联盟"><a href="#高-CPC-广告联盟" class="headerlink" title="高 CPC 广告联盟"></a>高 CPC 广告联盟</h4><details class="toggle" ><summary class="toggle-button" style="">高 CPC 广告联盟</summary><div class="toggle-content"><ol><li>HearstDigitalMedia：$10.80</li><li>InPowered：$10.19</li><li>Kapanlagi：$10.13</li><li>Yandex.ru：$10.11</li><li>OpenInventory：$9.24</li><li>WebAds：$8.71</li><li>MadhouseMobile：$8.12</li><li>CPXInteractive：$7.75</li><li>GuruMedia：$7.41</li><li>AdvertisingAlliance：$6.59</li><li>CompareGroup：$6.33</li><li>transcosmos：$6.09</li><li>eHealthcareSolutions：$6.06</li><li>KPISolutions：$5.70</li><li>ScaleOut：$5.65</li><li>Bannerconnect：$5.43</li><li>IndependentTraveler：$5.40</li><li>Quantcast：$5.29</li><li>MediaScience：$5.17</li><li>AdKnowledge：$5.11</li><li>BaronsMedia：$4.91</li><li>SpecificMedia：$4.81</li><li>InterCLICK：$4.73</li><li>Dun&amp;Bradstreet：$4.70</li><li>AdvantageMedia：$4.45</li><li>BridgeMarketing：$4.41</li><li>Belgacom：$4.39</li><li>Connexity：$4.32</li><li>HealthiNation：$4.30</li><li>AdvertisingTechnologies：$4.28</li><li>remerge：$4.19</li><li>MediaDecision：$4.17</li><li>PerformanceAdvertising：$4.04</li><li>AdGear：$3.93</li><li>BusinessAd：$3.93</li><li>CyberCommunications：$3.69</li><li>Motricity：$3.66</li><li>TruEffect：$3.60</li><li>TCSBank：$3.46</li><li>MediaMind：$3.43</li><li>GroupM：$3.36</li><li>Videology：$3.29</li><li>SEEKCommercial：$3.26</li><li>Proclivity：$3.15</li><li>AdvanceMedia：$3.13</li><li>Neustar：$3.11</li><li>iPromote：$3.10</li><li>AdLabs：$3.09</li><li>InteractiveMediaSales：$3.07</li><li>Technorati：$3.05</li><li>MetaNetwork：$3.02</li><li>Yandex：$2.95</li><li>Adform：$2.90</li><li>REAGroup：$2.86</li><li>Fiksu：$2.84</li><li>ActiveAgent：$2.81</li><li>StartApp：$2.77</li><li>Didit：$2.74</li><li>Weborama：$2.74</li><li>TremorMedia：$2.72</li><li>PlatformOne：$2.71</li><li>Sogou：$2.63</li><li>Hybrid：$2.62</li><li>DownstreamMarketing：$2.61</li><li>PCWorldCommunications：$2.58</li><li>Digitize&#x2F;Medialink1.USAccount：$2.57</li><li>ARAnet：$2.54</li><li>UnrulyMedia：$2.51</li><li>OpenX：$2.49</li><li>OutsideHub：$2.45</li><li>CyberAgent：$2.44</li><li>AutoScout24：$2.39</li><li>CollectiveMedia：$2.39</li><li>MediaCity：$2.38</li><li>MediaNewsGroup：$2.35</li><li>eMar：$2.30</li><li>CarWale：$2.29</li><li>VideoInternational：$2.28</li><li>InternetBrands：$2.24</li><li>Undertone：$2.21</li><li>CirrusMedia：$2.17</li><li>AgendaMedia：$2.14</li><li>Decisive：$2.14</li><li>IgniteVideo：$2.14</li><li>Twenga：$2.04</li><li>AMNET：$2.01</li><li>PlatformID：$2.01</li><li>LookSmart：$2.00</li><li>NirvanaInformation：$1.99</li><li>Innity：$1.93</li><li>BurstMedia：$1.91</li><li>CBSInteractive：$1.89</li><li>Tencent：$1.89</li><li>Demandbase：$1.88</li><li>MediaWorks：$1.88</li><li>MobileTheory：$1.88</li><li>iContext：$1.87</li><li>Adience：$1.85</li><li>Baidu：$1.84</li><li>Affinity：$1.80</li><li>Inneractive：$1.79</li><li>DigitalResponse：$1.78</li><li>MarsTechnologies：$1.77</li><li>IQM：$1.75</li><li>Rubedo：$1.72</li><li>Magnetic：$1.70</li><li>InspiredMobile：$1.68</li><li>Timesinternet：$1.67</li><li>Torrential：$1.65</li><li>Scripps：$1.60</li><li>SteelHouse：$1.60</li><li>DennisPublishing：$1.59</li><li>PRIMEDIA：$1.58</li><li>FutureUS：$1.57</li><li>BreakMedia：$1.56</li><li>CareerOne：$1.55</li><li>SiftMedia：$1.55</li><li>Trovit：$1.54</li><li>evania：$1.53</li><li>OneScreen：$1.52</li><li>Conversant：$1.51</li><li>Netmining：$1.48</li><li>Carsales：$1.47</li><li>OzoneMedia：$1.46</li><li>AdView：$1.45</li><li>Glimr：$1.45</li><li>One97：$1.45</li><li>Xad：$1.45</li><li>xBid：$1.45</li><li>DemandMedia：$1.44</li><li>ConradAdvertising：$1.43</li><li>VPON：$1.43</li><li>Digitouch：$1.42</li><li>DynamicVideo：$1.41</li><li>Quartic：$1.41</li><li>Pilot：$1.38</li><li>ikoo：$1.36</li><li>FinancialTimes：$1.34</li><li>MantaMedia：$1.34</li><li>YuMe：$1.34</li><li>Ritorno：$1.31</li><li>Verizonw&#x2F;DBM：$1.31</li><li>Adjust：$1.29</li><li>PickmeUp：$1.29</li><li>Unique：$1.29</li><li>TheReachGroup：$1.27</li><li>AdPepper：$1.26</li><li>BlackBoxMedia：$1.26</li><li>Lotame：$1.26</li><li>NetEdge：$1.26</li><li>Voetbal：$1.26</li><li>eprofessional：$1.25</li><li>PriceRunner：$1.24</li><li>Begun：$1.23</li><li>VoodooVideo：$1.22</li><li>SGN：$1.20</li><li>InteractiveOne：$1.19</li><li>Wikia：$1.19</li><li>GREE：$1.17</li><li>Longtail：$1.17</li><li>Mediasmart：$1.17</li><li>AnswerMedia：$1.16</li><li>Pulse360：$1.16</li><li>Turn：$1.15</li><li>Emerse：$1.14</li><li>Microsoftw&#x2F;AppNexus：$1.13</li><li>ZAPR：$1.13</li><li>SmartMedia：$1.11</li><li>Sojern：$1.11</li><li>Micro：$1.10</li><li>Allrecipes.com：$1.06</li><li>TVNZ：$1.06</li><li>OneSpot：$1.05</li><li>Smartclip：$1.05</li><li>YD：$1.05</li><li>eType：$1.04</li><li>IMT：$1.04</li><li>Bizo：$1.02</li><li>DataLabUSA：$1.01</li><li>SET.tv：$1.01</li><li>Merkle：$0.99</li><li>Tekka：$0.99</li><li>TheBridge：$0.99</li><li>AdMedia：$0.96</li><li>Housing.com：$0.96</li><li>Metacafe：$0.95</li><li>BellCanada：$0.94</li><li>HarvestDigital：$0.93</li><li>advancedSTORE：$0.92</li><li>IgnitionOne：$0.91</li><li>RocketFuel：$0.91</li><li>Adwell：$0.90</li><li>BEHE：$0.89</li><li>Kargo：$0.89</li><li>Theorem：$0.88</li><li>TapValue：$0.87</li><li>DataMind：$0.86</li><li>SalesSpider：$0.86</li><li>India.com：$0.85</li><li>Suite66：$0.85</li><li>HealthGrades：$0.83</li><li>Philly.com：$0.83</li><li>Wavenet：$0.83</li><li>OandaCorporation：$0.82</li><li>Trulia：$0.79</li><li>Bonnier：$0.78</li><li>Mixmarket：$0.77</li><li>RakutenUS：$0.76</li><li>Belo：$0.75</li><li>SheKnows：$0.75</li><li>Opera：$0.74</li><li>Papaya：$0.74</li><li>BBCw&#x2F;DBM：$0.73</li><li>Chango：$0.73</li><li>Pocketmath：$0.72</li><li>Hurra：$0.71</li><li>NetSeer：$0.71</li><li>Admeta：$0.70</li><li>Alveo：$0.69</li><li>ibibo：$0.69</li><li>WiseMedia：$0.69</li><li>Jubii：$0.68</li><li>Kadam：$0.68</li><li>Avocet：$0.67</li><li>DoublePositive：$0.67</li><li>MartiniMedia：$0.66</li><li>Rakuten：$0.64</li><li>Axonix：$0.63</li><li>TravelClick：$0.63</li><li>Yoyi：$0.62</li><li>ReduxMedia：$0.60</li><li>ComuneS&#x2F;A：$0.59</li><li>DigitalThrottle：$0.57</li><li>TribalFusion：$0.55</li><li>Batanga：$0.54</li><li>NewsInternational：$0.54</li><li>Xplosion：$0.54</li><li>FunBox：$0.53</li><li>Sokrati：$0.52</li><li>Ntree：$0.51</li><li>OrbitzWorldwide：$0.51</li><li>Rockerbox：$0.51</li><li>USMediaConsulting：$0.51</li><li>EJAM：$0.47</li><li>3Interactive：$0.46</li><li>AccountNow：$0.45</li><li>BabylonLimited：$0.44</li><li>ZiffDavis：$0.44</li><li>Populis：$0.43</li><li>Bridgewell：$0.42</li></ol></div></details><h2 id="广告审核中心"><a href="#广告审核中心" class="headerlink" title="广告审核中心"></a>广告审核中心</h2><p>为了保证广告的质量和效益，可以综合前文所述对广告进行审核，禁用不适合您网站的广告类型。</p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1677290992695.png" alt="Google Adsense 广告审核中心"></p><h2 id="广告尺寸和类型"><a href="#广告尺寸和类型" class="headerlink" title="广告尺寸和类型"></a>广告尺寸和类型</h2><p>如图，一定要选择让 Google 来为您优化移动广告的尺寸</p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1684731891963.png" alt="广告尺寸和类型"></p>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/729a93dae902/</id>
    <link href="https://blog.zisheng.pro/posts/729a93dae902/"/>
    <published>2023-02-24T15:58:16.000Z</published>
    <summary>作为一个技术博客的博主，我一直在寻找优化 Google Adsense CPC 并提高广告收入的方法，因为 CPC 是 Adsense 的收入来源，所以 CPC 越高，收入越多。本文就将介绍如何提高 Google Adsense CPC。</summary>
    <title>如何优化 Google Adsense CPC 提高广告收入</title>
    <updated>2026-09-18T08:15:28.636Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="站点运营" scheme="https://blog.zisheng.pro/categories/%E7%AB%99%E7%82%B9%E8%BF%90%E8%90%A5/"/>
    <category term="Hexo" scheme="https://blog.zisheng.pro/tags/Hexo/"/>
    <category term="hexo-theme-butterfly" scheme="https://blog.zisheng.pro/tags/hexo-theme-butterfly/"/>
    <category term="魔改" scheme="https://blog.zisheng.pro/tags/%E9%AD%94%E6%94%B9/"/>
    <category term="美化" scheme="https://blog.zisheng.pro/tags/%E7%BE%8E%E5%8C%96/"/>
    <content>
      <![CDATA[<blockquote><p>当前魔改基于 hexo 6.3.0 和 hexo-theme-butterfly 4.7.0</p></blockquote><h2 id="patch-package-配置"><a href="#patch-package-配置" class="headerlink" title="patch-package 配置"></a>patch-package 配置</h2><p>patch-package 可以将你的魔改记录保存到 <code>patches</code> 文件夹下，方便下次更新主题后应用魔改。按照下面的代码修改 package.json 即可：</p><figure class="highlight diff"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  &quot;scripts&quot;: &#123;</span><br><span class="line"><span class="addition">+    &quot;postinstall&quot;: &quot;npx patch-package&quot;</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>如果你想直接应用我的魔改，可以直接复制本文的代码到 <code>patches/hexo-theme-butterfly+4.7.0.patch</code></p><h1 id="两个小人"><a href="#两个小人" class="headerlink" title="两个小人"></a>两个小人</h1><div class="tabs"><div class="nav-tabs"><button type="button" class="tab active">改动点</button><button type="button" class="tab">效果预览</button></div><div class="tab-contents"><div class="tab-item-content active"><figure class="highlight diff"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">diff --git a/node_modules/hexo-theme-butterfly/layout/includes/widget/card_announcement.pug b/node_modules/hexo-theme-butterfly/layout/includes/widget/card_announcement.pug</span></span><br><span class="line"><span class="comment">index 9e63627..905342a 100644</span></span><br><span class="line"><span class="comment">--- a/node_modules/hexo-theme-butterfly/layout/includes/widget/card_announcement.pug</span></span><br><span class="line"><span class="comment">+++ b/node_modules/hexo-theme-butterfly/layout/includes/widget/card_announcement.pug</span></span><br><span class="line"><span class="meta">@@ -4,3 +4,4 @@</span> if theme.aside.card_announcement.enable</span><br><span class="line">       i.fas.fa-bullhorn.fa-shake</span><br><span class="line">       span= _p(&#x27;aside.card_announcement&#x27;)</span><br><span class="line">     .announcement_content!= theme.aside.card_announcement.content</span><br><span class="line"><span class="addition">+      .twopeople!= &#x27;&lt;div class=&quot;twopeople&quot;&gt;&lt;div class=&quot;container&quot; style=&quot;height:200px;&quot;&gt;&lt;canvas class=&quot;illo&quot; width=&quot;800&quot; height=&quot;800&quot; style=&quot;max-width: 200px; max-height: 200px; touch-action: none; width: 640px; height: 640px;&quot;&gt;&lt;/canvas&gt;&lt;/div&gt;&lt;script src=&quot;https://cdn.jsdelivr.net/gh/Justlovesmile/CDN/js/twopeople1.js&quot;&gt;&lt;/script&gt;&lt;script src=&quot;https://cdn.jsdelivr.net/gh/Justlovesmile/CDN/js/zdog.dist.js&quot;&gt;&lt;/script&gt;&lt;script id=&quot;rendered-js&quot; src=&quot;https://cdn.jsdelivr.net/gh/Justlovesmile/CDN/js/twopeople.js&quot;&gt;&lt;/script&gt;&lt;style&gt;.twopeople&#123;margin: 0;align-items: center;justify-content: center;text-align: center;&#125;canvas &#123;display: block;margin: 0 auto;cursor: move;&#125;&lt;/style&gt;&lt;/div&gt;&#x27;</span></span><br></pre></td></tr></table></figure></div><div class="tab-item-content"><p><img src="https://picbed.qunarzz.com/f3d661e2088ea4fd706709ee1024adce.png" alt="效果预览"></p></div></div><div class="tab-to-top"><button type="button" aria-label="scroll to top"><i class="fas fa-arrow-up"></i></button></div></div><blockquote><p>鸣谢 <a href="https://guole.fun/posts/butterfly-custom/">https://guole.fun/posts/butterfly-custom/</a></p></blockquote><h2 id="站点-Logo-圆角"><a href="#站点-Logo-圆角" class="headerlink" title="站点 Logo 圆角"></a>站点 Logo 圆角</h2><figure class="highlight diff"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">diff --git a/node_modules/hexo-theme-butterfly/source/css/_layout/head.styl b/node_modules/hexo-theme-butterfly/source/css/_layout/head.styl</span></span><br><span class="line"><span class="comment">index ade0db3..956a69e 100644</span></span><br><span class="line"><span class="comment">--- a/node_modules/hexo-theme-butterfly/source/css/_layout/head.styl</span></span><br><span class="line"><span class="comment">+++ b/node_modules/hexo-theme-butterfly/source/css/_layout/head.styl</span></span><br><span class="line"><span class="meta">@@ -289,6 +289,7 @@</span></span><br><span class="line">       margin-right: 6px</span><br><span class="line">       height: 36px</span><br><span class="line">       vertical-align: middle</span><br><span class="line"><span class="addition">+      border-radius: 50%</span></span><br><span class="line"></span><br><span class="line">   #toggle-menu</span><br><span class="line">     display: none</span><br></pre></td></tr></table></figure>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/3f81e8c6ddd1/</id>
    <link href="https://blog.zisheng.pro/posts/3f81e8c6ddd1/"/>
    <published>2023-02-22T06:18:09.000Z</published>
    <summary>本文将介绍本站对 hexo-theme-butterfly 进行的一些美化和魔改。</summary>
    <title>hexo-theme-butterfly 魔改与美化</title>
    <updated>2026-09-18T08:15:28.632Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="软考" scheme="https://blog.zisheng.pro/categories/%E8%BD%AF%E8%80%83/"/>
    <category term="信息系统项目管理师" scheme="https://blog.zisheng.pro/tags/%E4%BF%A1%E6%81%AF%E7%B3%BB%E7%BB%9F%E9%A1%B9%E7%9B%AE%E7%AE%A1%E7%90%86%E5%B8%88/"/>
    <category term="软考高项" scheme="https://blog.zisheng.pro/tags/%E8%BD%AF%E8%80%83%E9%AB%98%E9%A1%B9/"/>
    <category term="信息化" scheme="https://blog.zisheng.pro/tags/%E4%BF%A1%E6%81%AF%E5%8C%96/"/>
    <category term="信息系统" scheme="https://blog.zisheng.pro/tags/%E4%BF%A1%E6%81%AF%E7%B3%BB%E7%BB%9F/"/>
    <category term="信息化和信息系统" scheme="https://blog.zisheng.pro/tags/%E4%BF%A1%E6%81%AF%E5%8C%96%E5%92%8C%E4%BF%A1%E6%81%AF%E7%B3%BB%E7%BB%9F/"/>
    <content>
      <![CDATA[<div class="note warning modern"><p>我在备考的时候教材换成了，也因为其他事情就中断了备考，等我调整好时间会继续更新的，如果你有什么问题可以在评论区留言，我会尽快回复的。</p></div><h2 id="信息系统与信息化"><a href="#信息系统与信息化" class="headerlink" title="信息系统与信息化"></a>信息系统与信息化</h2><h3 id="信息的基本概念"><a href="#信息的基本概念" class="headerlink" title="信息的基本概念"></a>信息的基本概念</h3><h4 id="信息的定义"><a href="#信息的定义" class="headerlink" title="信息的定义"></a>信息的定义</h4><p>信息就是用来消除不确定性的东西。</p><h4 id="信息的特征"><a href="#信息的特征" class="headerlink" title="信息的特征"></a>信息的特征</h4><ol><li><strong>客观性</strong>：信息是客观事物在人脑中的反映，而反映的对象则有主观和客观的区别，因此，信息可分为主观信息（例如，决策、指令和计划等）和客观信息（例如，国际形式、经济发展和一年四季等）。主观信息必然要转化成客观信息，例如，决策和计划等主观信息要转化成实际行动。</li><li><strong>普遍性</strong>：物质具有普遍性，信息属于物质，物质的普遍性决定了信息的普遍存在。</li><li><strong>无限性</strong>：客观世界是无限的，信息是客观事物&#x2F;世界在人脑中的反映，反映客观世界的信息自然也是无限的。无限性可分为两个层次，一是无限的事物产生无限的信息，即信息的总量是无限的；二是每个具体事物或有限个食物的集合所能产生的信息也可以是无限的。</li><li><strong>动态性</strong>：信息是随着时间的变化而变化的。</li><li><strong>相对性</strong>：不同的认识主体从同一事物中获取的信息及信息量可能是不同的。这会取决于认识主体的生长环境、教育、工作环境、工作性质等外界因素。</li><li><strong>依附性</strong>：信息的依附性可以从两个方面来解释，一方面，信息是客观世界的反映，任何信息必然由客观事物所产生，不存在无源的信息；另一方面，任何信息都要依附于一定的载体而存在，需要有物质的承担者，信息不能完全脱离物质而独立存在。</li><li><strong>变换性</strong>：信息通过处理可以实现变化或转换，使其形式和内容发生变化，以适应特定的需要。比较明显的是数据可视化分过程，实现信息从文字到表格，最终到图表的展示，通过变化，信息的可读性越来越强。</li><li><strong>传递性</strong>：信息在时间上的传递就是存储，在空间上的传递就是转移或扩散。</li><li><strong>层次性</strong>：客观世界是分层次的，反映它的信息也是分层次的。</li><li><strong>系统性</strong>：信息可以表示为一种合集，不同类别的信息可以形成不同的整体。因此，可以形成与现实世界相对应的信息系统。例如：经济领域中的商业系统和金融系统，自然界中的水利系统和生态系统等。</li><li><strong>转化性</strong>：信息的产生不能没有物质，信息的传递不能没有能量，但有效地使用信息，可以使信息转化为物质或能量。</li></ol><p><strong>信息质量属性⭐</strong></p><ol><li><strong>精确性</strong>：对事物状态描述的精准程度。</li><li><strong>完整性</strong>：对事物状态描述的全面程度，完整信息应包括所有重要事实。</li><li><strong>可靠性</strong>：指信息的来源、采集方法、传输过程是可以信任的，符合预期。</li><li><strong>及时性</strong>，指获得信息的时刻与事件发生时刻的间隔长短。</li><li><strong>经济性</strong>，指信息获取、传输带来的成本在可以接受的范围之内。</li><li><strong>可验证性</strong>：指信息的主要质量属性可以被证实或者证伪的程度。</li><li><strong>安全性</strong>：指在信息的生命周期中，信息可以被非授权访问的可能性，可能性越低，安全性越高。</li></ol><h4 id="信息的功能"><a href="#信息的功能" class="headerlink" title="信息的功能"></a>信息的功能</h4><p>无重要考点</p><h4 id="信息的传输模型⭐"><a href="#信息的传输模型⭐" class="headerlink" title="信息的传输模型⭐"></a>信息的传输模型⭐</h4><ul><li><strong>信源</strong>：产生信息的实体，信息产生后，由这个实体向外传播。</li><li><strong>信宿</strong>：信息的归宿或者接受者。</li><li><strong>信道</strong>：传送信息的通道。</li><li><strong>编码器</strong>：在信息论中是泛指所有变换信号的设备，实际上就是终端机的发送部分。它包括从信源到信道的所有设备，如量化器、压缩编码器、调制器等，使信源输出的信号转换成适用于信道传送的信号。</li><li><strong>译码器</strong>：译码器是编码器的逆变换设备，把信道上传来的信号（原始信号与噪声的叠加）转换成信宿能接受的信号。</li><li><strong>噪声</strong>：噪声可以理解为干扰。</li></ul><p><img src="https://picbed.qunarzz.com/771ed4f1600bfe927bc97c5f5e6e2512.png" alt="信息传输模型"></p><h3 id="信息系统的基本概念"><a href="#信息系统的基本概念" class="headerlink" title="信息系统的基本概念"></a>信息系统的基本概念</h3><h4 id="系统的特性"><a href="#系统的特性" class="headerlink" title="系统的特性"></a>系统的特性</h4><p><strong>总体特性</strong></p><ol><li><strong>目的性</strong>：定义一个系统、组成一个系统或者抽象出一个系统，都有明确的目标或者目的，目标性决定了系统的功能。</li><li><strong>整体性</strong>：系统是一个整体，元素是为了达到一定的目的，按照一定的原则，有序地排列起来组成系统，从而产生出系统的特定功能。</li><li><strong>层次性</strong>：系统化是由多个元素组成的，系统和元素是相对的概念。元素是相对于它所处的系统而言的。系统是从它包含元素的角度来看的，如果研究问题的角度变一变，系统就成为更高一级的元素，也成为子系统。</li><li><strong>稳定性</strong>：是规则的约束，系统的内部结构和秩序应该是可以预见的；系统的状态以及演化路径有限并能被预测；系统的功能发生作用导致的后果也是可以预估的。稳定性强的系统使得系统在受到外部作用的同时，内部结构和秩序仍然能够保持。</li><li><strong>突变性</strong>：突变性是指系统通过失稳，从一种状态进入另一种状态的一种剧烈变化过程，它是系统质变的一种基本形式。</li><li><strong>自组织性</strong>：开放系统在系统内外因素的作用下，自发组织起来，使系统从无序到有序，从低级有序到高级有序。</li><li><strong>相似性</strong>：系统具有同构和同态的性质，体现在系统结构、存在方式和演化过程具有共同性。系统具有相似性，根本原因在于世界的物质统一性。</li><li><strong>相关性</strong>：元素是可分的和相互联系的，组成系统的元素必须有明确的边界，可以与别的元素区分开来。另外，元素之间是相互联系的，不是哲学上所说的那种普遍联系，而是实实在在的、具体的联系。</li><li><strong>适应环境性</strong>：系统总处在一定的环境中，与环境发生相互作用。系统和环境之间总是在发生着一定的物质和能量交换。</li></ol><p><strong>突出特征</strong></p><ol><li>开放性：系统的开放性是指系统的可访问性。</li><li>脆弱性🔥：这个特性与系统的稳定性相应，即系统可能存在着丧失结构、功能、秩序的特性，这个特性往往是隐藏不易被外界感知的。</li><li>健壮性：系统具有能够抵御出现非预期状态的特性称为健壮性，也称鲁棒性（robustness）。要求具有高可用性的信息系统，会采用冗余技术、容错技术、身份识别技术、可靠性技术等来抵御系统出现的非预期的状态，保持系统的稳定性。</li></ol><h4 id="信息系统定义"><a href="#信息系统定义" class="headerlink" title="信息系统定义"></a>信息系统定义</h4><p>简单地说，信息系统就是输入数据，通过加工处理，产生信息的系统。信息系统可以是手工的，也可以是计算机化的。</p><h4 id="信息系统集成"><a href="#信息系统集成" class="headerlink" title="信息系统集成"></a>信息系统集成</h4><p>采用现代管理理论（例如，软件工程、项目管理等）作为计划、设计、控制的方法论，将硬件、软件、数据库、网络等部件按照规划的结构和秩序，有机地整合一个有清晰边界的信息系统中，以达到既定系统的目标，这个过程称为信息系统集成。</p><h3 id="信息化的基本概念"><a href="#信息化的基本概念" class="headerlink" title="信息化的基本概念"></a>信息化的基本概念</h3><h4 id="信息化从“小”到“大”的五个层次⭐️"><a href="#信息化从“小”到“大”的五个层次⭐️" class="headerlink" title="信息化从“小”到“大”的五个层次⭐️"></a>信息化从“小”到“大”的五个层次⭐️</h4><ol><li><strong>产品信息化</strong></li><li><strong>企业信息化</strong></li><li><strong>产业信息化</strong></li><li><strong>国民经济信息化</strong></li><li><strong>社会生活信息化</strong></li></ol><h4 id="信息化的内涵🔥"><a href="#信息化的内涵🔥" class="headerlink" title="信息化的内涵🔥"></a>信息化的内涵🔥</h4><ol><li><strong>信息化的主体</strong>：<strong>全体社会成员</strong>，包括政府、企业、事业、团体和个人。</li><li><strong>时域</strong>：一个长期的过程</li><li><strong>空域</strong>：政治、经济、文化、军事和社会的一切领域</li><li><strong>手段</strong>：基于现代信息技术的先进社会生产工具</li><li><strong>途径</strong>：是创建信息时代的社会生产力，推动社会生产关系及社会上层建筑的改革</li><li><strong>目标</strong>：使国家的综合实力、社会的文明素质和人民的生活质量全面提升</li></ol><h4 id="国家级信息系统"><a href="#国家级信息系统" class="headerlink" title="国家级信息系统"></a>国家级信息系统</h4><ul><li>两网：是指政务内网和政务外网</li><li>一站：是指政府门户网站</li><li>四库：即建立人口、法人单位、空间地理和自然资源、宏观经济等四大基础数据库。</li><li>十二金</li></ul><h4 id="信息化的六要素🔥"><a href="#信息化的六要素🔥" class="headerlink" title="信息化的六要素🔥"></a>信息化的六要素🔥</h4><ol><li><strong>信息资源</strong>：信息资源的开发和利用是国家信息化的核心人任务，是国家信息化建设取得实效的关键，也是我国信息化的薄弱环节。（信息技术、设备、设施、信息生产者等）</li><li><strong>信息网络</strong>：信息网络是信息资源开发和利用的基础设施，包括电信网，广播电视和计算机网络。</li><li><strong>信息技术应用</strong>：是指把信息技术广泛应用于经济和社会各个领域，他直接反映了效率、效果、效益。信息技术应用是信息化体系六要素中的龙头，是国家信息化建设的主阵地，集中体现了国家信息化建设的需求与效益。</li><li><strong>信息技术与产业</strong>：是信息化的物质基础</li><li><strong>信息化人才</strong>：人才是信息化的成功之本</li><li><strong>信息化政策法规和标准规范</strong>：信息化政策和法规、标准、规范用于规范和协调信息化体系要素之间的关系，是国家信息化快速，有序，健康和持续发展的保障。</li></ol><p><img src="https://picbed.qunarzz.com/81eb01b74890743fd48386c1e2d6c876.png" alt="信息化的六要素"></p><h3 id="信息系统生命周期"><a href="#信息系统生命周期" class="headerlink" title="信息系统生命周期"></a>信息系统生命周期</h3><h4 id="4-大方面"><a href="#4-大方面" class="headerlink" title="4 大方面"></a>4 大方面</h4><ol><li>立项（系统规划）</li><li>开发（系统分析、系统设计、系统实施）</li><li>运维（运行维护）</li><li>消亡</li></ol><h4 id="5-小方面"><a href="#5-小方面" class="headerlink" title="5 小方面"></a>5 小方面</h4><ol><li>系统规划</li><li>系统分析</li><li>系统设计</li><li>系统实施</li><li>运行维护</li></ol><h2 id="信息系统开发方法"><a href="#信息系统开发方法" class="headerlink" title="信息系统开发方法"></a>信息系统开发方法</h2><h3 id="结构化方法"><a href="#结构化方法" class="headerlink" title="结构化方法"></a>结构化方法</h3><p><img src="https://picbed.qunarzz.com/b5330d132e19a9931bfb32134f234fdf.png" alt="孵小鸡"></p><h4 id="定义"><a href="#定义" class="headerlink" title="定义"></a>定义</h4><p>也称为生命周期法，是一种传统的信息系统开发方法。</p><h4 id="组成"><a href="#组成" class="headerlink" title="组成"></a>组成</h4><ul><li>结构分析（Structured Analysis，SA）</li><li>结构化设计（Structured Design，SD）</li><li>结构化程序设计（Structured Programming，SP）</li></ul><h4 id="精髓🔥"><a href="#精髓🔥" class="headerlink" title="精髓🔥"></a>精髓🔥</h4><p>自顶向下、逐步求精和模块化设计</p><h4 id="主要特点"><a href="#主要特点" class="headerlink" title="主要特点"></a>主要特点</h4><ol><li>开发目标清晰化</li><li>开发工作阶段化</li><li>开发文档规范化</li><li>设计方法结构化</li></ol><h4 id="适用场景"><a href="#适用场景" class="headerlink" title="适用场景"></a>适用场景</h4><p>特别适合于数据处理领域的问题</p><h4 id="不适用场景"><a href="#不适用场景" class="headerlink" title="不适用场景"></a>不适用场景</h4><p>不适用于规模较大、比较复杂的系统开发</p><h4 id="缺点"><a href="#缺点" class="headerlink" title="缺点"></a>缺点</h4><ol><li>开发周期较长</li><li>难以适应需求变化</li><li>很少考虑数据结构</li></ol><h3 id="面向对象方法"><a href="#面向对象方法" class="headerlink" title="面向对象方法"></a>面向对象方法</h3><p><img src="https://picbed.qunarzz.com/e66b60d74e7b7591ca378194405f3841.png" alt="面向对象-面向小鸡"></p><h4 id="定义-1"><a href="#定义-1" class="headerlink" title="定义"></a>定义</h4><p>面向对象（OO）方法认为，客观世界是由各种对象组成的（一切皆对象）。</p><h4 id="与结构化方法的联系"><a href="#与结构化方法的联系" class="headerlink" title="与结构化方法的联系"></a>与结构化方法的联系</h4><p>与结构化方法类似，OO 方法也划分阶段，但其中的系统分析、系统设计和系统实现三个阶段之间已经没有“缝隙”，也就是说，这三个阶段的界限变得不明确。</p><h4 id="优点🔥"><a href="#优点🔥" class="headerlink" title="优点🔥"></a>优点🔥</h4><ol><li>符合人们的思维习惯</li><li>有利于系统开发过程中用户与开发人员的沟通与交流，缩短开发周期</li></ol><h4 id="缺点🔥"><a href="#缺点🔥" class="headerlink" title="缺点🔥"></a>缺点🔥</h4><p>必须依靠一定的 OO 技术支持，在大型项目开发上具有一定的局限性，不能涉足系统分析以前的开发环节。</p><h4 id="应用场景"><a href="#应用场景" class="headerlink" title="应用场景"></a>应用场景</h4><p>当前，一些大型信息系统的开发，通常是将结构化方法和 OO 方法结合起来，首先，使用结构化方法进行自顶向下的整体划分；然后，自底向上地采用 OO 方法进行开发。</p><h3 id="原型化方法"><a href="#原型化方法" class="headerlink" title="原型化方法"></a>原型化方法</h3><p><img src="https://picbed.qunarzz.com/1c7a2ea4d0e571de005d72cf898a1352.png" alt="原型化方法"></p><h4 id="定义-2"><a href="#定义-2" class="headerlink" title="定义"></a>定义</h4><p>原型化方法也称为快速原型法，或者简称原型法。它是一种根据用户初步需求，利用系统开发工具，快速建立一个系统模型给用户，在此基础上与用户交流，最终实现用户需求的信息系统快速的开发方法。</p><h4 id="分类"><a href="#分类" class="headerlink" title="分类"></a>分类</h4><p>按是否实现功能划分：水平原型、垂直原型<br>按最终结果划分：抛弃式原型、演化式原型</p><h4 id="开发过程🔥"><a href="#开发过程🔥" class="headerlink" title="开发过程🔥"></a>开发过程🔥</h4><ol><li>确定用户的基本需求</li><li>设计系统原型</li><li>试用和评价原型</li><li>修改和完善原型</li><li>整理原型、提供文档</li></ol><p><img src="https://picbed.qunarzz.com/8a4081f6975446bb19470ea120cd78b1.png" alt="原型法的开发过程"></p><h4 id="特点🔥"><a href="#特点🔥" class="headerlink" title="特点🔥"></a>特点🔥</h4><ol><li>原型法可以使系统开发的周期缩短，成本和风险降低，速度加快，获得较高的综合开发效益。</li><li>原型法是以用户为中心来开发系统，用户参与的程度大大提高，开发的系统符合用户的需求，因而增加客户的满意度，提高了系统开发的成功率。</li><li>由于用户参与了系统开发的全过程，对系统的功能和结构容易理解和接受，有利于系统的移交，有利于系统的运行和维护。</li></ol><h4 id="不足🔥"><a href="#不足🔥" class="headerlink" title="不足🔥"></a>不足🔥</h4><ol><li>开发环境要求高</li><li>管理水平要求高</li></ol><h4 id="适用场景🔥"><a href="#适用场景🔥" class="headerlink" title="适用场景🔥"></a>适用场景🔥</h4><ol><li>原型法适用于那些需求不明确的系统开发</li><li>事实上，对于分析层面难度大，技术层面难度不大的系统，适用于原型法开发</li><li>而对于技术层面的困难远大于其分析层面的系统，则不宜用原型法。</li></ol><h3 id="面向服务"><a href="#面向服务" class="headerlink" title="面向服务"></a>面向服务</h3><p><img src="https://picbed.qunarzz.com/145bb50bf142fc865414fc290012e273.png" alt="面向服务"></p><h4 id="产生的背景"><a href="#产生的背景" class="headerlink" title="产生的背景"></a>产生的背景</h4><p>OO 的应用构建在类和对象上，随后发展起来的建模技术将相关对象按业务功能进行分组，就形成了构件（Component）的概念。</p><h4 id="交互的方式"><a href="#交互的方式" class="headerlink" title="交互的方式"></a>交互的方式</h4><p>对于跨构件的功能调用，则采用<strong>接口</strong>的形式暴露出来。进一步将接口的定义与实现进项解耦，则催生了服务和面向服务（Service-Oriented，SO）的开发方法。</p><h4 id="优点"><a href="#优点" class="headerlink" title="优点"></a>优点</h4><ol><li>提高系统可复用性</li><li>信息资源共享</li><li>系统之间的互操作性</li></ol><h2 id="常规信息系统集成技术"><a href="#常规信息系统集成技术" class="headerlink" title="常规信息系统集成技术"></a>常规信息系统集成技术</h2><h3 id="网络标准与网络协议"><a href="#网络标准与网络协议" class="headerlink" title="网络标准与网络协议"></a>网络标准与网络协议</h3><h4 id="OSI-协议"><a href="#OSI-协议" class="headerlink" title="OSI 协议"></a>OSI 协议</h4><ol><li>物理层</li></ol><ul><li>定义：盖层包括物理联网媒介，比如电缆连线连接器。</li><li>协议：RS232、V.35、<strong>RJ-45</strong>（水晶头）、FDDI<br>2.数据链路层</li><li>定义：它控制网络层与物理层之间的通信。</li><li>协议：<strong>IEEE 802.3&#x2F;.2</strong>、AHDLC、PPP、TM</li></ul><ol start="3"><li>网络层</li></ol><ul><li>定义：其主要功能是将网络地址（例如：IP 地址）翻译成对应的物理地址（例如：MAC 地址），并决定如何将数据从发送方路由到接收方。</li><li>协议：<strong>IP</strong>、<strong>ICMP</strong>、<strong>IGMP</strong>、IPX、<strong>ARP</strong>、<strong>RARP</strong></li></ul><ol start="4"><li>传输层</li></ol><ul><li>定义：主要负责确保数据可靠、顺序、无错地从 A 传输到 B 点。</li><li>协议：<strong>TCP</strong>、<strong>UDP</strong>、SPX</li></ul><ol start="5"><li>会话层</li></ol><ul><li>定义：负责在网络中的两个节点之间建立和维持通信，以及提供交互会话的管理功能。如三种数据流方向控制，即一路交互、两路交互和两路同时会话模式。</li><li>协议：RPC、SQL、NFS</li></ul><ol start="6"><li>表示层</li></ol><ul><li>定义：如同应用程序和网络之间的翻译官，在表示层，数据将按照网络能理解的方案进行格式化；这种格式化也因所使用的网络的类型不同而不同。表示层管理数据的解密加密、数据转换、格式化和文本压缩。</li><li>协议：<strong>JPEG</strong>、<strong>ASCLL</strong>、<strong>GIF</strong>、<strong>DES</strong>、<strong>MPEG</strong></li></ul><ol start="7"><li>应用层</li></ol><ul><li>定义：负责对软件提供接口以使程序能使用网路服务，如事务处理程序、文件传送协议和网络管理等。</li><li>协议：<strong>FTP</strong>、<strong>TFTP</strong>、<strong>HTTP</strong>、<strong>SMTP</strong>、<strong>DHCP</strong>、<strong>Telnet</strong>、<strong>DNS</strong>、<strong>SNMP</strong></li></ul><h4 id="网络协议与标准"><a href="#网络协议与标准" class="headerlink" title="网络协议与标准"></a>网络协议与标准</h4><ul><li>IEEE 802.3（局域网协议）</li><li>IEEE 802.11（无线局域网协议）</li></ul><h4 id="TCP-IP"><a href="#TCP-IP" class="headerlink" title="TCP&#x2F;IP"></a>TCP&#x2F;IP</h4><p>TCP&#x2F;IP 协议是 Internet 的核心</p><ol><li>应用层协议</li></ol><ul><li><strong>FTP</strong>（File Transport Protocol，文件传输协议）：运行在<em>TCP</em>之上，FTP 在客户端与服务器之间需要建立两条 TCP 连接，一条用于传输控制信息（使用 21 号端口）：另外一条用于传送文件内容（使用 20 端口）。</li><li><strong>TFTP</strong>（Trivial File Transfer Protocol），简单文件传输协议），建立在<em>UDP</em>之上，提供不可靠的数据流传输服务。</li><li><strong>HTTP</strong>（HyperText Transfer Protocol，超文本传输协议）：是用于从 WWW 服务器传输超文本到本地浏览器的传输协议，建立在<em>TCP</em>之上。</li><li><strong>SMTP</strong>（Simple Mail Transfer Protocol，简单邮件传输协议）：建立在<em>TCP</em>之上，是一种提供可靠且有效的电子邮件传输协议。</li><li><strong>DHCP</strong>（Dynamic Host Configuration Protocol，动态主机配置协议）：建立在<em>UDP</em>之上，基于客户端&#x2F;服务器模型设计的。所有的 IP 网络设定数据都由 DHCP 服务器集中管理，并负责客户端的 DHCP 请求。</li><li><strong>Telnet</strong>（远程登录协议）：是登录和仿真程序，建立在<em>TCP</em>之上，他的基础功能是允许用户登录进入远程计算机。</li><li><strong>DNS</strong>（Domain Name System，域名系统）：实现域名解析，建立在<em>UDP</em>之上。</li><li><strong>SNMP</strong>（Simple Network Management Protocol，简单网络管理协议）：由一组网络管理的标准组成，包含一个应用层协议、数据库模型和一组资源对象。该协议能够支持网络管理系统，用以监测连接到网络上的设备是否有任何引起管理上注意的情况。</li></ul><ol start="2"><li>传输层协议</li></ol><ul><li><strong>TCP</strong>（Transmission Control Protocol，传输控制协议）：TCP 提供了一个可靠的，面向连接的，全双工的数据传输服务。TCP 一般用于传输数据量比较少，且对可靠性要求高的场合。</li><li><strong>UDP</strong>（User Datagram Protocol，用户数据报协议）：UDP 是一种不可靠的，无连接的协议，可以保证应用程序间的通信，与 TCP 相比，UDP 是一种无连接的协议，它的错误检测功能要弱的多，UDP 协议一般用于传输数据量大，对于可靠性要求不是很高，但要求速度快的场合。</li></ul><ol start="3"><li>网络层协议</li></ol><ul><li><strong>IP</strong>（Internet Protocol，互联网协议）：所提供的服务是无连接和可靠的。</li><li><strong>ICMP</strong>（Internet Control Message Protocol，互联网控制报文协议）：一种专门用于发送错误报文的协议，即传送的数据可能丢失、重复、延迟或乱序传递，所以需要一种尽量避免差错并能在发生差错时报告的机制，这就是 ICMP 的功能。</li><li><strong>IGMP</strong>（Internet Group Management Protocol，互联网组管理协议）：允许在 Internet 中的计算机参加多播，是计算机用做向相邻多路由器报告多目组成员的协议。</li><li><strong>ARP</strong>（Address Resolution Protocol，地址解析协议）：用于动态地完成 IP 到物理地址的转换。</li><li><strong>RARP</strong> （Reverse Address Resolution Protocol，反向地址解析协议）：用于动态地完成物理地址到 IP 地址的转换。</li></ul><h3 id="网络设备"><a href="#网络设备" class="headerlink" title="网络设备"></a>网络设备</h3><h4 id="网络交换定义"><a href="#网络交换定义" class="headerlink" title="网络交换定义"></a>网络交换定义</h4><p>通过一定的设备，如交换机等，将不同的信号或者信号转换为对方可识别的信号类型从而达到通信目的的一种交换形式。</p><h4 id="网络交换分类"><a href="#网络交换分类" class="headerlink" title="网络交换分类"></a>网络交换分类</h4><ol><li>物理交换（如电话网）</li><li>链路层交换（二层交换，对 MAC 地址进行变更🔥）</li><li>网络层交换（三层交换，对 IP 地址进行变更）</li><li>传输层交换（四层交换，对端口进行变更，比较少见）</li><li>应用层交换</li></ol><h4 id="互联设备定义"><a href="#互联设备定义" class="headerlink" title="互联设备定义"></a>互联设备定义</h4><p>在网络互连时，各节点一般不能简单地直接相连，而是需要通过一个中间设备来实现。</p><h4 id="互联设备分类"><a href="#互联设备分类" class="headerlink" title="互联设备分类"></a>互联设备分类</h4><ol><li>中继器（实现物理层协议转换，在电缆间进行二进制信号）</li><li>网桥（实现物理层和数据链路层协议转换）</li><li>路由器（实现网络层协议和以下各层协议的转换）（🔥在网络层）</li><li>网关（提供从最底层到传输层或以上各层的协议转换）</li><li>交换机</li></ol><h4 id="无线网络设备"><a href="#无线网络设备" class="headerlink" title="无线网络设备"></a>无线网络设备</h4><ol><li>无线网卡</li><li>无线 AP</li><li>无线网桥</li><li>无线路由器</li></ol><h3 id="网络服务器"><a href="#网络服务器" class="headerlink" title="网络服务器"></a>网络服务器</h3><blockquote><p>无重要考点</p></blockquote><h3 id="网络存储技术"><a href="#网络存储技术" class="headerlink" title="网络存储技术"></a>网络存储技术</h3><h4 id="DAS"><a href="#DAS" class="headerlink" title="DAS"></a>DAS</h4><p><strong>定义🔥</strong></p><p>直接附加存储（Direct Attached Storage），DAS 将存储设备通过 SCSI 电缆直接连到服务器，其本身是硬件的堆叠，存储依赖于服务器，不带有任何存储操作系统。</p><p><strong>缺点🔥</strong></p><p>当存储容量增加时，DAS 方式很难扩展，这对存储容量的升级是一个巨大的瓶颈。</p><h4 id="NAS"><a href="#NAS" class="headerlink" title="NAS"></a>NAS</h4><p><strong>定义🔥</strong></p><p>网络附加存储（Network Attached Storage），通过网络接口与网络相连，由用户通过网络访问。</p><p><strong>工作原理🔥</strong></p><p>NAS 技术支持多种 TCP&#x2F;IP 网络协议，主要是 NFS（网络文件存储系统）和 CIFS（通用 Internet 文件）来进行文件访问。</p><p><strong>特点🔥</strong></p><ul><li>即插即用</li><li>小文件级的共享存取</li></ul><p><img src="https://picbed.qunarzz.com/7bb699e595dcd42ffa6d6a6b9ad5e982.png" alt="NAS 存储系统的结构"></p><h4 id="SAN🔥"><a href="#SAN🔥" class="headerlink" title="SAN🔥"></a>SAN🔥</h4><p><strong>定义</strong></p><p>存储区域网络（Storage Area Network），通过专用交换机将磁盘阵列与服务器连接起来的高速专用子网。</p><p><strong>技术分类</strong></p><ol><li><strong>FC SAN</strong>：光纤通道的主要特性有：热插拔性、高速带宽、远程连接数量大。</li><li><strong>IP SAN</strong>：基于 IP 网络实现数据块级别存储方式的存储网络。</li><li><strong>IB SAN</strong>：是一种交换结构 I&#x2F;O 技术，其设计思路是通过一套中心机构在远程存储器、网络以及服务器等设备之间建立一个单一的连接链路，并由 IB 交换机来指挥流量。</li></ol><p><img src="https://picbed.qunarzz.com/61c61873ce628e907c850fe99eb9fe3a.png" alt="紫升"></p><h3 id="网络接入技术"><a href="#网络接入技术" class="headerlink" title="网络接入技术"></a>网络接入技术</h3><h4 id="有线接入"><a href="#有线接入" class="headerlink" title="有线接入"></a>有线接入</h4><ol><li>PSTN</li><li>ISDN</li><li>ADSL</li><li>FTTX+LAN</li><li>HFC<br>1.3.5网络接入技术</li></ol><h4 id="无线接入"><a href="#无线接入" class="headerlink" title="无线接入"></a>无线接入</h4><p>无线网络是指以无线电波作为信息传输媒介🔥</p><p><strong>技术分类：</strong></p><ol><li>GPRS</li><li>无线接入</li><li>3G</li><li>4G</li><li>WIFI</li></ol><h3 id="网络规划与设计"><a href="#网络规划与设计" class="headerlink" title="网络规划与设计"></a>网络规划与设计</h3><h4 id="网路分层设计🔥"><a href="#网路分层设计🔥" class="headerlink" title="网路分层设计🔥"></a>网路分层设计🔥</h4><ul><li><strong>核心层🔥</strong>：主要目的通过<strong>高速的转发通信</strong>，提供优化，可靠的骨干传输结构，因此，核心层交换机应拥有更高的可靠性，性能和吞吐量。</li><li><strong>汇聚层🔥</strong>：是核心层与接入层的分界面，完成<strong>网络访问策略控制，数据包处理、过滤、寻址</strong>、以及其他的数据处理任务。</li><li><strong>接入层🔥</strong>：目的是允许终端用户连接到网络，因此，接入层交换机（或路由器）接入层具有<strong>低成</strong>本和<strong>高密度</strong>特性。</li></ul><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1677082670910.png" alt="网络规划分层"></p><h4 id="网络设计工作"><a href="#网络设计工作" class="headerlink" title="网络设计工作"></a>网络设计工作</h4><ol><li>网络拓扑结构设计</li><li>主干网络设计（核心层）设计</li><li>汇聚层和接入层设计</li><li>广域网连接与远程访问设计</li><li>无线网络设计</li><li>网络安全设计</li></ol><ul><li>信息安全的基本要素🔥</li></ul><ol start="7"><li>设备选型</li></ol><h4 id="信息安全的基本要素🔥"><a href="#信息安全的基本要素🔥" class="headerlink" title="信息安全的基本要素🔥"></a>信息安全的基本要素🔥</h4><ol><li><strong>机密性🔥</strong>：确保信息不暴露给未授权的实体或进程</li><li><strong>完整性🔥</strong>：只有得到允许的人才能修改数据，并且能够判别出来数据是否已被篡改。</li><li><strong>可用性🔥</strong>：得到授权的实体在需要的时候可访问数据，即攻击者不能占用所有的资源而阻碍授权者的工作</li><li><strong>可控性🔥</strong>：可以控制授权范围内的信息流向及行为方式</li><li><strong>可审查性🔥</strong>：对出现的网络安全问题提供调查的依据和手段</li></ol><h3 id="数据库管理系统"><a href="#数据库管理系统" class="headerlink" title="数据库管理系统"></a>数据库管理系统</h3><h4 id="关系型数据库"><a href="#关系型数据库" class="headerlink" title="关系型数据库"></a>关系型数据库</h4><ul><li><strong>Oracle</strong></li><li><strong>MySQL</strong></li><li><strong>SqlServer</strong></li><li>DB2</li><li>PostgreSQL</li><li>SQLite</li><li>Access</li></ul><h4 id="非关系型数据库"><a href="#非关系型数据库" class="headerlink" title="非关系型数据库"></a>非关系型数据库</h4><ul><li><strong>MongoDB</strong></li><li>Redis</li><li>HBase</li><li>Neo4j</li><li>Elasticsearch</li></ul><h3 id="数据仓库技术"><a href="#数据仓库技术" class="headerlink" title="数据仓库技术"></a>数据仓库技术</h3><h4 id="特点🔥-1"><a href="#特点🔥-1" class="headerlink" title="特点🔥"></a>特点🔥</h4><p>数据仓库是一个面向主题的，集成的，非易失的，且随时间变化的数据集合，用于支持管理决策。</p><h4 id="体系结构"><a href="#体系结构" class="headerlink" title="体系结构"></a>体系结构</h4><ol><li><strong>数据源</strong>：是数据仓库系统的基础，是整个系统数据源泉。</li><li><strong>数据的存储与管理</strong>：是整个系统数据仓库系统的核心。</li><li><strong>OLAP（Online Analysis Processor，联机分析处理）服务器</strong>：对分析需要的数据进行有效集成，按多维模型予以组织，以便进行多角度、多层次的分析，并发现趋势。</li><li><strong>前端工具</strong>：主要包括各种查询工具，报表工具，分析工具，数据挖掘工具以及各种基于数据仓库或数据集的应用开发工具。其中数据分析工具主要针对OLAP，报表工具，数据挖掘工具主要针对数据仓库。</li></ol><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1677083599886.png" alt="数据仓库体系结构"></p><h2 id="练一练"><a href="#练一练" class="headerlink" title="练一练"></a>练一练</h2><p>1、对象和类是面向对象中两个重要的概念，关于对象和类，不正确的是（）。 <em>19年11月第1题</em></p><p>A、对象是类的实例<br>B、类是对象的抽象<br>C、一个类只能产生一个对象<br>D、类中包含方法和属性</p><div class="hide-block"><button type="button" class="hide-button" style="">答案解析</button><div class="hide-content"><p>【重要性】一定要掌握<br>【答案】<strong>C</strong><br>【解析】一个类可以产生多个对象</p></div></div><hr><p>2、TCP&#x2F;IP 模型中，Telnet 属于（）协议。 <em>19年5月19题</em></p><p>A、接口层<br>B、网络层<br>C、传输层<br>D、应用层</p><div class="hide-block"><button type="button" class="hide-button" style="">答案解析</button><div class="hide-content"><p>【重要性】一定要掌握<br>【答案】<strong>D</strong><br>【解析】Telnet 是应用层协议。</p></div></div><hr><p>3、在信息系统的生命周期中，开发阶段不包括（）。 <em>19年5月3题</em></p><p>A、系统规划<br>B、系统设计<br>C、系统分析<br>D、系统实施</p><div class="hide-block"><button type="button" class="hide-button" style="">答案解析</button><div class="hide-content"><p>【重要性】一定要掌握<br>【答案】<strong>A</strong><br>【解析】系统规划是立项阶段的，高级教材第三版 P10。</p></div></div><hr><p>4、（）的设计方法满足了信息系统快速响应需求与环境的变化，组织内部、组织之间各种应用系统的互相通信要求，提高了系统可复用性、信息资源共享和系统之间的互操作性。 <em>18年11月28题</em></p><p>A、面向对象<br>B、面向过程<br>C、基于构件<br>D、面向服务</p><div class="hide-block"><button type="button" class="hide-button" style="">答案解析</button><div class="hide-content"><p>【重要性】一定要掌握<br>【答案】<strong>D</strong><br>【解析】从应用的角度来看，组织内部、组织之间各种应用系统的互相通信和互操作性直接影响着组织<br>对信息的掌握程度和处理速度。如何使信息系统快速响应需求与环境变化，提高系统可复用性、信息资<br>源共享和系统之间的互操作性，成为影响信息化建设效率的关键问题，而SO的思维方式恰好满足了这<br>种需求。</p></div></div><hr><p>5、在OSI七层协议中，UDP是（）的协议。 <em>18年11月第19题</em></p><p>A、网络层<br>B、传输层<br>C、会话层<br>D、应用层</p><div class="hide-block"><button type="button" class="hide-button" style="">答案解析</button><div class="hide-content"><p>【重要性】一定要掌握<br>【答案】<strong>B</strong><br>【解析】(4)传输层：主要负责确保数据可靠、顺序、无错地从A点传输到B点。如提供建立、维护相拆<br>除传送连接的功能；选择网络层提供最合适的服务；在系统之间提供可靠的透明的数据传送，提供端到<br>端的错误恢复和流量控制。在TCP&#x2F;IP协议中，具体协议有TCP、UDP、SPX。</p></div></div><hr><p>6、（）的任务是：根据系统说明书规定的功能要求，考虑实际条件，具体设计实现逻辑模型的技术方<br>案。 <em>18年11月第3题</em></p><p>A、系统规划阶段<br>B、系统分析阶段<br>C、系统设计阶段<br>D、系统实施阶段</p><div class="hide-block"><button type="button" class="hide-button" style="">答案解析</button><div class="hide-content"><p>【重要性】一定要掌握<br>【答案】<strong>C</strong><br>【解析】简单地说，系统分析阶段的任务是回答系统“做什么”的问题，而系统设计阶段要回答的问题<br>是“怎么做”。该阶段的任务是根据系统说明书中规定的功能要求，考虑实际条件，具体设计实现逻辑<br>模型的技术方案，也就是设计新系统的物理模型。这个阶段又称为物理设计阶段，可分为总体设计（概<br>要设计）和详细设计两个子阶段。这个阶段的技术文档是系统设计说明书。</p></div></div><hr><p>7、IEEE 802 规范定义了网卡如何访问传输介质，以及如何在传输介质上传输数据的方法。其中，（）<br>是重要的局域网协议。 <em>18年5月第11题</em></p><p>A、IEEE 802.1<br>B、IEEE 802.3<br>C、IEEE 802.6<br>D、IEEE 802.11</p><div class="hide-block"><button type="button" class="hide-button" style="">答案解析</button><div class="hide-content"><p>【重要性】一定要掌握<br>【答案】<strong>B</strong><br>【解析】 IEEE 802.3 ：以太网介质访问控制协议 （CSMA&#x2F;CD）及物理层技术规范 [2]。<br>IEEE 802.11：无线局域网（WLAN）的介质访问控制协议及物理层技术规范。</p></div></div><hr><p>8、在开放系统互连参考模型(OSI)中，（ ）的主要功能是将网络地址翻译成对应的物理地址，并决定如<br>何将数据从发送方经路由送达到接收方。 <em>18年5月第20题</em></p><p>A、数据链路层<br>B、物理层<br>C、网络层<br>D、传输层</p><div class="hide-block"><button type="button" class="hide-button" style="">答案解析</button><div class="hide-content"><p>【重要性】一定要掌握<br>【答案】<strong>C</strong><br>【解析】网络层中的协议主要有IP、ICMP、IGMP、ARP、和RARP等。 （1）IP，所提供的服务是无连接和不可靠的。<br>（2）ICMP(Internet Control Message Protocol，网络控制报文协议)，一种专门用于发送错报文的<br>协议，即传送的数据可能丢失、重复、延迟、或乱序传递，所以需要一种尽量避免差错并能发生差错时<br>报告的机制，这就是ICMP的功能。<br>（3）IGMP(Internet Group Management Protocol，网际组管理协议)允许在Internet中的计算机参<br>加多播，是计算机用做向相邻多路由器报告多目组成员的协议。<br>（4）ARP(Address Resolution Protocol，地址解析协议) 用于动态地完成IP到物理地址的转换。<br>（5）RARP(Reverse Address Resolution Protocol，反向地址解析协议)用于动态完成物理地址向IP<br>地址的转换。</p></div></div><hr><p>9、 TCP&#x2F;IP是internet的核心协议，应用程序通过用应用层协议利用网络完成数据交互的任务，其中，<br>（）是用来在客户机与服务器之间进行简单文件的传输的协议，提供不复杂，开销不大的文件传输服务。 <em>18年5月第19题</em></p><p>A、FTP<br>B、TFTP<br>C、HTTP<br>D、SMTP</p><div class="hide-block"><button type="button" class="hide-button" style="">答案解析</button><div class="hide-content"><p>【重要性】一定要掌握<br>【答案】<strong>B</strong><br>【解析】TFTP（Trivial File Transfer Protocol,简单文件传输协议）是TCP&#x2F;IP协议族中的一个用来在客<br>户机与服务器之间进行简单文件传输的协议，提供不复杂、开销不大的文件传输服务。端口号为69。</p></div></div><hr><p>10、某快消品连锁企业委托科技公司A开发部署电子商务平台，A公司根据系统设计任务书所确定的范<br>围，确定系统的基本目标和逻辑功能要求，提出新系统的逻辑模型，这属于信息系统生命周期中（）阶<br>段的工作。 <em>18年5月第3题</em></p><p>A、系统规划<br>B、系统分析<br>C、系统设计<br>D、系统实施</p><div class="hide-block"><button type="button" class="hide-button" style="">答案解析</button><div class="hide-content"><p>【重要性】一定要掌握<br>【答案】<strong>B</strong><br>【解析】系统分析阶段的任务是根据系统设计任务书所确定的范围，对现行系统进行详细调查，描述现<br>行系统的业务流程，指出现行系统的局限性和不足之处，确定新系统的基本目标和逻辑功能要求，即提<br>出新系统的逻辑模型。</p></div></div><hr><p>11、在软件需求分析中，（）分别用来表示功能模型和行为模型。 <em>19年11月第7题</em></p><p>A、数据流图、状态转换图<br>B、状态转换图、E-R图<br>C、状态转换图、数据流图<br>D、E-R图、状态转换图</p><div class="hide-block"><button type="button" class="hide-button" style="">答案解析</button><div class="hide-content"><p>【重要性】一定要掌握<br>【答案】<strong>A</strong><br>【解析】E-R图是用来表示数据模型的，行为一般与状态有关。</p></div></div><hr><p>12、中间件是一种独立的系统软件或服务程序，（）不属于中间件。 <em>19年11月第2题</em><br>A、Tomcat<br>B、Websphere<br>C、ODBC<br>D、Python</p><div class="hide-block"><button type="button" class="hide-button" style="">答案解析</button><div class="hide-content"><p>【重要性】一定要掌握<br>【答案】<strong>D</strong><br>【解析】Python 属于开发语言</p></div></div><hr><p>13、项目成员张工绘制了四张类图，其中不正确的是（）。 <em>19年5月第27题</em></p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1677084628615.png" alt="紫升"></p><div class="hide-block"><button type="button" class="hide-button" style="">答案解析</button><div class="hide-content"><p>【答案】<strong>B</strong></p></div></div><hr><p>14、在 UML 的顺序图中，通常由左向右分层排列各个对象，正确的排列方法是（）。<em>19年5月第26题</em></p><p>A、执行者角色、控制类、用户接口、业务层、后台数据库<br>B、执行者角色、用户接口、控制类、业务层、后台数据库<br>C、执行者角色、控制类、用户接口、后台数据库、业务层<br>D、执行者角色、用户接口、业务层、控制类、后台数据库</p><div class="hide-block"><button type="button" class="hide-button" style="">答案解析</button><div class="hide-content"><p>【重要性】一定要掌握<br>【答案】<strong>A</strong></p></div></div><hr><p>15、执行者与用例之间的关系是（）。 <em>19年5月第25题</em></p><p>A、包含关系<br>B、泛化关系<br>C、关联关系<br>D、扩展关系</p><div class="hide-block"><button type="button" class="hide-button" style="">答案解析</button><div class="hide-content"><p>【重要性】一定要掌握<br>【答案】C<br>【解析】在用例图中执行者与用例之间是关联关系。</p></div></div><hr><p>16、UML的（）描述了一个特定对象的所有可能状态以及由于各种事件的发生而引起的状态之间的转移。 <em>18年11月第27题</em></p><p>A、控制图<br>B、状态图<br>C、协作图<br>D、序列图</p><div class="hide-block"><button type="button" class="hide-button" style="">答案解析</button><div class="hide-content"><p>【重要性】一定要掌握<br>【答案】<strong>B</strong><br>【解析】状态图（state diagram）：状态图描述一个状态机，它由状态、转移、事件和活动组成。状态图给出了对象的动态视图。它对于接口、类或协作的行为建模尤为重要，而且它强调事件导致的对象行为，这非常有助于对反应式系统建模。</p></div></div><hr><p>17、某企业要建设信息系统平台，要求系统可用性达到99.99%。系统A平均无故障时间10000小时，故<br>障平均维修时间1小时：系统B平均无故障时间14000小时，故障平均维修时间1.5小时。以下说法中，<br>正确的是（）。 <em>18年11月第22题</em></p><p>A、只有系统A符合可用性要求<br>B、系统A和系统B均符合可用性要求<br>C、只有系统B符合可用性要求<br>D、系统A和系统B都不符合可用性要求</p><div class="hide-block"><button type="button" class="hide-button" style="">答案解析</button><div class="hide-content"><p>【重要性】一定要掌握<br>【答案】<strong>A</strong><br>【解析】计算机系统的可用性用平均无故障时间(MTTF)来度量，即计算机系统平均能够正常运行多长<br>时间，才发生一次故障。系统的可用性越高，平均无故障时间越长。可维护性用平均维修时间(MTTR)<br>来度量，即系统发生故障后维修和重新恢复正常运行平均花费的时间。系统的可维护性越好，平均维修<br>时间越短。计算机系统的可用性定义为：MTTF&#x2F; (MTTF+MTTR) ,kl00%。由此可见，计算机系统的可<br>用性定义为系统保持正常运行时间的百分比。所以，想要提高一个系统的可用性，要么提升系统的单次<br>正常工作的时长，要么减少故障修复时间。<br>A的可用性&#x3D;10000&#x2F;10001&#x3D;99.99%<br>B的可用性&#x3D;14000&#x2F;14001.5 &#x3D; 99.98%<br>只有系统A负荷可用性要求</p></div></div><hr><p>18、关于网络存储技术的描述，正确的是：（）。<br>A、DAS 是一种易于扩展的存储技术<br>B、NAS 系统与 DAS 系统相同，都没有自己的文件系统<br>C、NAS 可以使用 TCP&#x2F;IP 作为其网络传输协议<br>D、SAN 采用了文件共享存取方式</p><div class="hide-block"><button type="button" class="hide-button" style="">答案解析</button><div class="hide-content"><p>【重要性】一定要掌握<br>【答案】<strong>C</strong><br>【解析】NAS 技术支持多种 TCP&#x2F;IP 网络协议，主要是NFS (Net File System，网络文件系统)和 CIFS<br>（Common Internet File System，通用 Internet 文件系统）来进行文件访问，所以 NAS 的性能特点是<br>进行小文件级的共享存取。在具体使用时，NAS设备通常配置为文件服务器，通过使用基于 Web 的管<br>理界面来实现系统资源的配置、用户配置管理和用户访问登录等。</p></div></div><hr><p>19、软件需求是多层次的，包括业务需求、用户需求、系统需求，其中业务需求（）。 <em>18年11月第7题</em></p><p>A、反应了企业或客户对系统高层次的目标需求<br>B、描述了用户具体目标或者用户要求系统必须完成的任务<br>C、从系统角度来说明软件的需求，包括功能需求、非功能需求和设计约束<br>D、描述了用户任务系统应该具备的功能和性能</p><div class="hide-block"><button type="button" class="hide-button" style="">答案解析</button><div class="hide-content"><p>【重要性】一定要掌握<br>【答案】<strong>A</strong><br>【解析】业务需求。业务需求是指反映企业或客户对系统高层次的目标要求，通常来自项目投资人、购买产品的客户、客户单位的管理人员、市场营销部门或产品策划部门等。通过业务需求可以确定项目视图和范围，项目视图和范围文档把业务需求集中在一个简单、紧凑的文档中，该文档为以后的开发工作奠定了基础。</p></div></div><hr><p>20、UML图不包括（）。 <em>18年5月第27题</em></p><p>A、用例图<br>B、序列图<br>C、组件图<br>D、继承图</p><div class="hide-block"><button type="button" class="hide-button" style="">答案解析</button><div class="hide-content"><p>【重要性】一定要掌握<br>【答案】<strong>D</strong><br>【解析】类图表示类、接口和它们之间的协作关系，没有继承图</p></div></div>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/e896d9d3a8e1/</id>
    <link href="https://blog.zisheng.pro/posts/e896d9d3a8e1/"/>
    <published>2023-02-19T15:40:41.000Z</published>
    <summary>信息系统项目管理师考试属于全国计算机技术与软件专业技术资格考试中的一个高级考试。</summary>
    <title>信息系统项目管理师笔记の第一章、信息化和信息系统</title>
    <updated>2026-09-18T08:15:28.636Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="软考" scheme="https://blog.zisheng.pro/categories/%E8%BD%AF%E8%80%83/"/>
    <category term="信息系统项目管理师" scheme="https://blog.zisheng.pro/tags/%E4%BF%A1%E6%81%AF%E7%B3%BB%E7%BB%9F%E9%A1%B9%E7%9B%AE%E7%AE%A1%E7%90%86%E5%B8%88/"/>
    <category term="软考高项" scheme="https://blog.zisheng.pro/tags/%E8%BD%AF%E8%80%83%E9%AB%98%E9%A1%B9/"/>
    <category term="信息化" scheme="https://blog.zisheng.pro/tags/%E4%BF%A1%E6%81%AF%E5%8C%96/"/>
    <category term="信息系统" scheme="https://blog.zisheng.pro/tags/%E4%BF%A1%E6%81%AF%E7%B3%BB%E7%BB%9F/"/>
    <category term="高项" scheme="https://blog.zisheng.pro/tags/%E9%AB%98%E9%A1%B9/"/>
    <category term="计算机" scheme="https://blog.zisheng.pro/tags/%E8%AE%A1%E7%AE%97%E6%9C%BA/"/>
    <content>
      <![CDATA[<h2 id="考试介绍"><a href="#考试介绍" class="headerlink" title="考试介绍"></a>考试介绍</h2><p>全国计算机技术与软件专业技术资格考试设三个级别层次，五个专业，共有 27 种岗位资格考试。而<strong>信息系统项目管理师</strong>考试属于全国计算机技术与软件专业技术资格考试中的一个<strong>高级考试</strong>。</p><h2 id="报考条件"><a href="#报考条件" class="headerlink" title="报考条件"></a>报考条件</h2><p>信息系统项目管理师考试报考条件不设学历与资历条件、年龄以及专业等限制，考生可根据自己的技术水平选择合适的级别以及合适的资格进行报考。</p><blockquote><p>注意：每次考试每个人只允许报考一种资格。</p></blockquote><h2 id="考试流程"><a href="#考试流程" class="headerlink" title="考试流程"></a>考试流程</h2><ol><li><a href="https://bm.ruankao.org.cn/sign/welcome">报名</a>（2月 - 3月）</li><li>打印准考证（考前一周）</li><li>考试（5 月 27 日）</li><li>公布成绩（考后一个月左右）</li><li>领取证书（9 月份左右）</li></ol><h2 id="考题分析"><a href="#考题分析" class="headerlink" title="考题分析"></a>考题分析</h2><table><thead><tr><th align="center">科目</th><th align="center">内容</th><th align="center">时长</th><th align="center">考试时间</th></tr></thead><tbody><tr><td align="center">综合知识</td><td align="center">75道选择题，知识范围很广，包含：IT 知识、时事政治、项目管理、5 分的运筹学、5 分英语题</td><td align="center">150 分钟</td><td align="center">上午<br />9:00 - 11:00</td></tr><tr><td align="center">案例分析</td><td align="center">3 道大题：1 道计算题，2 道分析题</td><td align="center">90 分钟</td><td align="center">下午<br />1:30 - 3:00</td></tr><tr><td align="center">论文写作</td><td align="center">1 道论文题，考试范围：<strong>十大管理</strong>、安全管理、合同管理、招投标</td><td align="center">120 分钟</td><td align="center">下午<br />3:20 - 5:20</td></tr></tbody></table><h2 id="学习计划"><a href="#学习计划" class="headerlink" title="学习计划"></a>学习计划</h2><p>计划共历时 100 天，每天投入 2 - 3 小时，总投入预计 200 - 300 小时。</p><ul><li>第一阶段（35 天）(2023&#x2F;2&#x2F;15 - 2023&#x2F;3&#x2F;21)<ul><li>基础知识</li><li>计算专题</li></ul></li><li>第二阶段（50 天）(2023&#x2F;3&#x2F;22 - 2023&#x2F;5&#x2F;10)<ul><li>历年真题</li><li>案例分析</li><li>写作专题</li></ul></li><li>第三阶段 (15 天)（2023&#x2F;5&#x2F;11 - 2023&#x2F;5&#x2F;26）<ul><li>考前冲刺</li></ul></li></ul><h2 id="资源"><a href="#资源" class="headerlink" title="资源"></a>资源</h2><ul><li><a href="https://www.ruankao.org.cn/">中国计算机技术资格网</a></li><li>软考真题 App<ul><li><a href="https://apps.apple.com/hk/app/%E8%BD%AF%E8%80%83%E7%9C%9F%E9%A2%98-2022%E4%BF%A1%E6%81%AF%E9%A1%B9%E7%9B%AE%E7%AE%A1%E7%90%86-%E7%B3%BB%E7%BB%9F%E9%9B%86%E6%88%90/id1468131830">iOS</a></li><li><a href="https://www.wandoujia.com/apps/7873801">Android</a></li></ul></li><li>软考通 App<ul><li><a href="https://apps.apple.com/cn/app/%E8%BD%AF%E8%80%83%E9%80%9A-2022%E5%B9%B4%E8%BD%AF%E4%BB%B6%E6%B0%B4%E5%B9%B3%E8%80%83%E8%AF%95%E6%8F%90%E5%88%86%E7%A5%9E%E5%99%A8/id1498444438">iOS</a></li><li><a href="https://www.wandoujia.com/apps/8095598">Android</a></li></ul></li><li>软考刷题王<ul><li>#小程序:&#x2F;&#x2F;软考刷题王&#x2F;I4Jk3d4HRskRbaw</li></ul></li><li><img src="https://cdn.jsdelivr.net/gh/youngjuning/images@main/1677062492249.png" alt="杭州 E 类人才福利"></li></ul>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/9698ac1384d7/</id>
    <link href="https://blog.zisheng.pro/posts/9698ac1384d7/"/>
    <published>2023-02-15T16:04:55.000Z</published>
    <summary>信息系统项目管理师考试属于全国计算机技术与软件专业技术资格考试中的一个高级考试。</summary>
    <title>信息系统项目管理师备考指南</title>
    <updated>2026-09-18T08:15:28.636Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="CSS" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/CSS/"/>
    <category term="CSS" scheme="https://blog.zisheng.pro/tags/CSS/"/>
    <category term="word-wrap" scheme="https://blog.zisheng.pro/tags/word-wrap/"/>
    <category term="word-break" scheme="https://blog.zisheng.pro/tags/word-break/"/>
    <content>
      <![CDATA[<p>word-wrap 和 word-break 是一对用于单词断句的双生属性，但是又有微妙的区别，每次要用的时候还要重新翻一下 MDN 文档，本文力求通过一张流程图快速记忆两者如何使用。</p><h2 id="word-wrap"><a href="#word-wrap" class="headerlink" title="word-wrap"></a>word-wrap</h2><p>css 的 word-wrap 属性用来标明是否允许浏览器在单词内进行断句，这是为了防止当一个字符串太长而找不到它的自然断句点时产生溢出现象。</p><ul><li>normal：就是大家平常见得最多的正常的换行规则。</li><li>break-word：一行单词中实在没有其他靠谱的换行点的时候在单词内换行。</li></ul><h2 id="word-break"><a href="#word-break" class="headerlink" title="word-break"></a>word-break</h2><p>css 的 word-break 属性用来标明怎么样进行单词内的断句。</p><ul><li>normal：使用默认的换行规则，即如果一个单词很长，导致一行中剩下的空间已经放不下它时，则浏览器会把这个单词挪到下一行去。</li><li>break-all：允许单词断行，即如果一个单词很长，导致一行中剩下的空间已经放不下它时，则浏览器不会把这个单词挪到下一行去，而是在单词内换行。</li></ul><h2 id="搭配使用，换行不累"><a href="#搭配使用，换行不累" class="headerlink" title="搭配使用，换行不累"></a>搭配使用，换行不累</h2><p><img src="https://picbed.qunarzz.com/3d7e329ab089d28c7bde5be75573f1d3.png" alt="紫升"></p><blockquote><p>参考资料来自 <a href="https://www.cnblogs.com/2050/archive/2012/08/10/2632256.html">你真的了解word-wrap和word-break的区别吗？</a> 和 <a href="https://www.zhangxinxu.com/wordpress/2015/11/diff-word-break-break-all-word-wrap-break-word/">word-break:break-all和word-wrap:break-word的区别</a></p></blockquote>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/c607ec2b5110/</id>
    <link href="https://blog.zisheng.pro/posts/c607ec2b5110/"/>
    <published>2023-02-13T05:47:56.000Z</published>
    <summary>word-wrap 和 word-break 是一对用于单词断句的双生属性，但是又有微妙的区别，每次要用的时候还要重新翻一下 MDN 文档，本文力求通过一张流程图快速记忆两者如何使用。</summary>
    <title>word-wrap 和 word-break</title>
    <updated>2026-09-18T08:15:28.636Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="npm" scheme="https://blog.zisheng.pro/tags/npm/"/>
    <category term="命令" scheme="https://blog.zisheng.pro/tags/%E5%91%BD%E4%BB%A4/"/>
    <content>
      <![CDATA[<h2 id="全局"><a href="#全局" class="headerlink" title="全局"></a>全局</h2><ul><li><code>npm root --location=global</code>：查看全局包安装路径<blockquote><p>Mac 下默认的全局路径是：<code>/usr/local/lib/node_modules</code><br>Ubuntu 下默认的全局路径是: <code>/usr/lib/node_modules</code></p></blockquote></li><li><code>npm ls --location=global</code>：查看全局包列表<ul><li><code>npm ls --location=global --depth 0</code>：仅查看一级包</li><li><code>npm ls --location=global --depth 0 | grep packageName</code>：查看一级包并搜索相关包</li></ul></li></ul><h2 id="owner"><a href="#owner" class="headerlink" title="owner"></a>owner</h2><p>管理已经发布的包的所有权</p><ul><li><code>npm owner add &lt;user&gt; [&lt;@scope&gt;/]&lt;pkg&gt;</code>：给某个包添加用户</li><li><code>npm owner rm &lt;user&gt; [&lt;@scope&gt;/]&lt;pkg&gt;</code>：删除某个包的某个用户</li><li><code>npm owner ls [&lt;@scope&gt;/]&lt;pkg&gt;</code>：列出某个包的所有用户</li></ul><h2 id="将指定版本设置为指定标签"><a href="#将指定版本设置为指定标签" class="headerlink" title="将指定版本设置为指定标签"></a>将指定版本设置为指定标签</h2><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ npm dist-tag add @youngjuning/playground@1.0.3 latest|alpha|beta</span><br></pre></td></tr></table></figure><h2 id="展示-dist-tag"><a href="#展示-dist-tag" class="headerlink" title="展示 dist-tag"></a>展示 dist-tag</h2><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ npm dist-tag <span class="built_in">ls</span> @youngjuning/playground</span><br></pre></td></tr></table></figure><h2 id="废弃某个版本"><a href="#废弃某个版本" class="headerlink" title="废弃某个版本"></a>废弃某个版本</h2><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ npm deprecate @youngjuning/playground@1.0.3 <span class="string">&quot;垃圾&quot;</span></span><br></pre></td></tr></table></figure><h2 id="package-json-字段解释"><a href="#package-json-字段解释" class="headerlink" title="package.json 字段解释"></a>package.json 字段解释</h2><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br></pre></td><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;wx-promise-pro&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;3.2.2&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;强大的、优雅的小程序 Promise 库&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;main&quot;</span><span class="punctuation">:</span> <span class="string">&quot;dist/wx-promise-pro.js&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;types&quot;</span><span class="punctuation">:</span> <span class="string">&quot;index.d.ts&quot;</span><span class="punctuation">,</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;build&quot;</span><span class="punctuation">:</span> <span class="string">&quot;rollup -c&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;postversion&quot;</span><span class="punctuation">:</span> <span class="string">&quot;yarn build &amp;&amp; git push --tags &amp;&amp; git push &amp;&amp; npm publish&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;repository&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;git&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;url&quot;</span><span class="punctuation">:</span> <span class="string">&quot;git+https://github.com/youngjuning/wx-promise-pro.git&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;keywords&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">    <span class="string">&quot;promise&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="string">&quot;es6&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="string">&quot;then&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="string">&quot;catch&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="string">&quot;finally&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="string">&quot;小程序&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="string">&quot;weapp&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="string">&quot;miniapp&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="string">&quot;weixin&quot;</span></span><br><span class="line">  <span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;author&quot;</span><span class="punctuation">:</span> <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;紫升&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;email&quot;</span><span class="punctuation">:</span> <span class="string">&quot;youngjuning@aliyun.com&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;url&quot;</span><span class="punctuation">:</span> <span class="string">&quot;https://blog.zisheng.pro&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;license&quot;</span><span class="punctuation">:</span> <span class="string">&quot;MIT&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;bugs&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;url&quot;</span><span class="punctuation">:</span> <span class="string">&quot;https://github.com/youngjuning/wx-promise-pro/issues&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;homepage&quot;</span><span class="punctuation">:</span> <span class="string">&quot;https://github.com/youngjuning/wx-promise-pro#readme&quot;</span><span class="punctuation">,</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;@babel/core&quot;</span><span class="punctuation">:</span> <span class="string">&quot;^7.8.7&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;@babel/preset-env&quot;</span><span class="punctuation">:</span> <span class="string">&quot;^7.8.7&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;rollup&quot;</span><span class="punctuation">:</span> <span class="string">&quot;^1.28.0&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;rollup-plugin-babel&quot;</span><span class="punctuation">:</span> <span class="string">&quot;^4.3.3&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;rollup-plugin-commonjs&quot;</span><span class="punctuation">:</span> <span class="string">&quot;^10.1.0&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;rollup-plugin-json&quot;</span><span class="punctuation">:</span> <span class="string">&quot;^4.0.0&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;rollup-plugin-node-resolve&quot;</span><span class="punctuation">:</span> <span class="string">&quot;^5.2.0&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;rollup-plugin-terser&quot;</span><span class="punctuation">:</span> <span class="string">&quot;^5.1.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>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/8e944d3670b0/</id>
    <link href="https://blog.zisheng.pro/posts/8e944d3670b0/"/>
    <published>2022-11-17T04:02:58.000Z</published>
    <summary>汇总 npm 全局包、包所有权、dist-tag、弃用版本与 package.json 字段等日常维护命令，适用于发布和维护 JavaScript 包时快速查阅。</summary>
    <title>NPM 常用命令</title>
    <updated>2026-09-18T08:15:28.629Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="前端开发" scheme="https://blog.zisheng.pro/tags/%E5%89%8D%E7%AB%AF%E5%BC%80%E5%8F%91/"/>
    <category term="npm" scheme="https://blog.zisheng.pro/tags/npm/"/>
    <category term="前端" scheme="https://blog.zisheng.pro/tags/%E5%89%8D%E7%AB%AF/"/>
    <category term="前端工程化" scheme="https://blog.zisheng.pro/tags/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/"/>
    <category term="swiper" scheme="https://blog.zisheng.pro/tags/swiper/"/>
    <content>
      <![CDATA[<p>帮助开发者从 NPM 包选型的角度感知和建立前端工程化体系。</p><h2 id="测试"><a href="#测试" class="headerlink" title="测试"></a>测试</h2><h3 id="单元测试"><a href="#单元测试" class="headerlink" title="单元测试"></a>单元测试</h3><p><a href="https://www.npmjs.com/package/tape">tape</a></p><h2 id="编译工具"><a href="#编译工具" class="headerlink" title="编译工具"></a>编译工具</h2><ul><li><a href="https://www.npmjs.com/package/babel">babel</a></li></ul><h2 id="构建工具"><a href="#构建工具" class="headerlink" title="构建工具"></a>构建工具</h2><ul><li><a href="https://www.npmjs.com/package/webpack">webpack</a></li><li><a href="https://www.npmjs.com/package/rollup">rollup</a></li><li><a href="https://www.npmjs.com/package/esbuild">esbuild</a></li></ul><h2 id="dotenv"><a href="#dotenv" class="headerlink" title="dotenv"></a>dotenv</h2><ul><li><a href="https://www.npmjs.com/package/dotenv">dotenv</a></li></ul><h2 id="Node-js-CLI"><a href="#Node-js-CLI" class="headerlink" title="Node.js CLI"></a>Node.js CLI</h2><h3 id="框架"><a href="#框架" class="headerlink" title="框架"></a>框架</h3><ul><li><a href="https://www.npmjs.com/package/commander">commander</a>: Node.js命令行界面的完整解决方案,受Ruby Commander启发。 前端开发node cli 必备技能。</li><li><a href="https://www.npmjs.com/package/yargs">yargs</a>: 功能比较全，但是使用起来比较复杂。</li></ul><h3 id="命令行参数解析"><a href="#命令行参数解析" class="headerlink" title="命令行参数解析"></a>命令行参数解析</h3><ul><li><a href="https://www.npmjs.com/package/yargs-parser">yargs-parser</a>：yargs 命令行框架内置的命令行参数解析库，功能比较全，比较适合工程化实用。</li><li><a href="https://www.npmjs.com/package/minimist">minimist</a>：如果你喜欢轻量级的技术，那么 minimist 足够简单好用，代码量也很少（只有几百行），非常适合研读。</li><li>inquire.js</li><li>chalk: 美化命令行的模块</li><li>ora: 优雅的 spinner</li><li>ncp: 异步的拷贝文件，包含空文件夹</li><li>recursive-readdir: 递归地列出目录和子目录下的所有文件，不包含目录本身。</li><li>shelljs: 在 Node.js 中执行 Unix shell 命令</li><li>log-symbols: 打印日志的特殊标志</li><li><a href="https://www.npmjs.com/package/download-git-repo">download-git-repo</a>: Download and extract a git repository (GitHub, GitLab, Bitbucket) from node</li><li><a href="https://www.npmjs.com/package/global-prefix">global-prefix</a>: 获取 npm 全局安装的前缀</li><li><a href="https://www.npmjs.com/package/exec-sh">exec-sh</a>: 执行 shell 命令转发所有 stdio 流，比 shelljs exec 命令好用</li><li><a href="https://www.npmjs.com/package/wml">wml</a>: wml 侦听某个文件夹中的更改（使用 watchman），然后将更改的文件复制到另一个文件夹中。</li><li><a href="https://blog.ihaiu.com/cmd-open/">cmd-open</a>: 扩展 Windows 命令 open</li><li><a href="https://github.com/shelljs/shx">shx</a>: shx is a wrapper around ShellJS Unix commands, providing an easy solution for simple Unix-like, cross-platform commands in npm package scripts.</li></ul><h2 id="CSS"><a href="#CSS" class="headerlink" title="CSS"></a>CSS</h2><h3 id="css-modules"><a href="#css-modules" class="headerlink" title="css modules"></a>css modules</h3><ul><li><a href="https://www.npmjs.com/package/clsx">clsx</a></li><li><a href="https://www.npmjs.com/package/classnames">classnames</a></li></ul><h3 id="css-in-js"><a href="#css-in-js" class="headerlink" title="css-in-js"></a>css-in-js</h3><ul><li><a href="https://www.npmjs.com/package/styled-components">styled-components</a></li></ul><h2 id="React"><a href="#React" class="headerlink" title="React"></a>React</h2><h3 id="状态管理"><a href="#状态管理" class="headerlink" title="状态管理"></a>状态管理</h3><ul><li><a href="https://www.npmjs.com/package/redux">redux</a></li></ul>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/db7a22999938/</id>
    <link href="https://blog.zisheng.pro/posts/db7a22999938/"/>
    <published>2022-11-02T07:58:31.000Z</published>
    <summary>本文主要收了一些常用 NPM 包，帮助开发者从 NPM 包选型的角度感知和建立前端工程化体系。</summary>
    <title>2023 常用 NPM 包推荐</title>
    <updated>2026-09-18T08:15:28.629Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="网络编程" scheme="https://blog.zisheng.pro/tags/%E7%BD%91%E7%BB%9C%E7%BC%96%E7%A8%8B/"/>
    <category term="Token 刷新" scheme="https://blog.zisheng.pro/tags/Token-%E5%88%B7%E6%96%B0/"/>
    <content>
      <![CDATA[<p>为了安全考虑，一般后端都要求在请求接口时传递登录的 Token。为了防止 Token 泄漏的风险，服务器的 Token 一般不会设置太长时间，比如我最近联调的，两个小时就会过期，token 过期就需要重新登陆。频繁要求登陆会造成用户体验差，所以后端同时会提供刷新 Token 的接口，本文就是记录一下几种无感知刷新的方法。</p><h2 id="方案一"><a href="#方案一" class="headerlink" title="方案一"></a>方案一</h2><p>在登录时，后端返回过期时间，前端每次请求就判断 token 的过期时间，如果快到过期时间，就去调用刷新 token 接口，我们可以封装一个 refreshToken 方法：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="title function_">refreshToken</span> = <span class="keyword">async</span> (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">if</span> (<span class="title function_">dayjs</span>().<span class="title function_">diff</span>(<span class="params">LOCAL_REFRESH_TIME &gt; LOCAL_EXPIRE</span>) &#123;</span><br><span class="line">    <span class="keyword">if</span> (<span class="variable language_">global</span>.<span class="property">workPromise</span>) &#123;</span><br><span class="line">      <span class="keyword">return</span> <span class="variable language_">global</span>.<span class="property">workPromise</span></span><br><span class="line">    &#125;</span><br><span class="line">    <span class="variable language_">global</span>.<span class="property">workPromise</span> = <span class="keyword">new</span> <span class="title class_">Promise</span>(<span class="keyword">async</span> (resolve) =&gt; &#123;</span><br><span class="line">      <span class="keyword">const</span> &#123;data&#125; = <span class="keyword">await</span> <span class="title function_">request</span>(&#123;</span><br><span class="line">        <span class="attr">url</span>: <span class="string">`https://api.com/login`</span>,</span><br><span class="line">        <span class="comment">// other config</span></span><br><span class="line">        <span class="attr">method</span>: <span class="string">&#x27;POST&#x27;</span>,</span><br><span class="line">      &#125;);</span><br><span class="line">      <span class="comment">// 更新 LOCAL_REFRESH_TIME</span></span><br><span class="line">      <span class="variable language_">global</span>.<span class="property">workPromise</span> = <span class="literal">null</span>;</span><br><span class="line">      <span class="title function_">resolve</span>()</span><br><span class="line">    &#125;)</span><br><span class="line">    <span class="keyword">return</span> <span class="variable language_">global</span>.<span class="property">workPromise</span></span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">return</span> <span class="title class_">Access</span>_Token</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>该方法主要原理是通过将 refresh 接口请求存在全局的 workPromise 中来保证在并发请求接口时只存在一个请求。</p><h2 id="方案二"><a href="#方案二" class="headerlink" title="方案二"></a>方案二</h2><p>登录时设置定时器刷新 token 接口，请求的时候判断当前是否有 workPromise 存在，如果存在就等刷新完成。</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 登录后设置定时器</span></span><br><span class="line"><span class="built_in">setInterval</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">  <span class="variable language_">global</span>.<span class="property">workPromise</span> = <span class="keyword">new</span> <span class="title class_">Promise</span>(<span class="keyword">async</span> (resolve) =&gt; &#123;</span><br><span class="line">    <span class="keyword">const</span> &#123;data&#125; = <span class="keyword">await</span> <span class="title function_">request</span>(&#123;</span><br><span class="line">      <span class="attr">url</span>: <span class="string">`https://api.com/login`</span>,</span><br><span class="line">      <span class="comment">// other config</span></span><br><span class="line">      <span class="attr">method</span>: <span class="string">&#x27;POST&#x27;</span>,</span><br><span class="line">    &#125;);</span><br><span class="line">    cookies.<span class="title function_">set</span>(<span class="string">&#x27;refresh_time&#x27;</span>)</span><br><span class="line">    <span class="variable language_">global</span>.<span class="property">workPromise</span> = <span class="literal">null</span>;</span><br><span class="line">    <span class="title function_">resolve</span>()</span><br><span class="line">  &#125;)</span><br><span class="line">&#125;, <span class="variable constant_">EXPIRE</span> / <span class="number">2</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">request</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">if</span> (<span class="variable language_">global</span>.<span class="property">workPromise</span>) &#123;</span><br><span class="line">    <span class="keyword">await</span> <span class="variable language_">global</span>.<span class="title function_">workPromise</span>()</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><p>如果使用了 axios，可以在请求响应拦截器中拦截，判断 token 返回过期后，调用刷新 token 接口。</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> axios <span class="keyword">from</span> <span class="string">&#x27;axios&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 是否正在刷新的标记</span></span><br><span class="line"><span class="keyword">let</span> isRefreshing = <span class="literal">false</span></span><br><span class="line"><span class="comment">//重试队列</span></span><br><span class="line"><span class="keyword">let</span> requests = []</span><br><span class="line">service.<span class="property">interceptors</span>.<span class="property">response</span>.<span class="title function_">use</span>(</span><br><span class="line">  <span class="function"><span class="params">response</span> =&gt;</span> &#123;</span><br><span class="line">    <span class="comment">//约定code 409 token 过期</span></span><br><span class="line">    <span class="keyword">if</span> (response.<span class="property">data</span>.<span class="property">code</span> === <span class="number">409</span>) &#123;</span><br><span class="line">      <span class="keyword">if</span> (!isRefreshing) &#123;</span><br><span class="line">        isRefreshing = <span class="literal">true</span></span><br><span class="line">        <span class="comment">//调用刷新token的接口</span></span><br><span class="line">        <span class="keyword">return</span> <span class="title function_">refreshToken</span>().<span class="title function_">then</span>(<span class="function"><span class="params">res</span> =&gt;</span> &#123;</span><br><span class="line">          <span class="keyword">const</span> &#123; token &#125; = res.<span class="property">data</span></span><br><span class="line">          <span class="comment">// 替换token</span></span><br><span class="line">          <span class="title function_">setToken</span>(token)</span><br><span class="line">          response.<span class="property">headers</span>.<span class="property">Authorization</span> = <span class="string">`<span class="subst">$&#123;token&#125;</span>`</span></span><br><span class="line">           <span class="comment">// token 刷新后将数组的方法重新执行</span></span><br><span class="line">          requests.<span class="title function_">forEach</span>(<span class="function">(<span class="params">cb</span>) =&gt;</span> <span class="title function_">cb</span>(token))</span><br><span class="line">          requests = [] <span class="comment">// 重新请求完清空</span></span><br><span class="line">          <span class="keyword">return</span> <span class="title function_">service</span>(response.<span class="property">config</span>)</span><br><span class="line">        &#125;).<span class="title function_">catch</span>(<span class="function"><span class="params">err</span> =&gt;</span> &#123;</span><br><span class="line">          <span class="comment">//跳到登录页</span></span><br><span class="line">          <span class="title function_">removeToken</span>()</span><br><span class="line">          router.<span class="title function_">push</span>(<span class="string">&#x27;/login&#x27;</span>)</span><br><span class="line">          <span class="keyword">return</span> <span class="title class_">Promise</span>.<span class="title function_">reject</span>(err)</span><br><span class="line">        &#125;).<span class="title function_">finally</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">          isRefreshing = <span class="literal">false</span></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">// 返回未执行 resolve 的 Promise</span></span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">Promise</span>(<span class="function"><span class="params">resolve</span> =&gt;</span> &#123;</span><br><span class="line">          <span class="comment">// 用函数形式将 resolve 存入，等待刷新后再执行</span></span><br><span class="line">          requests.<span class="title function_">push</span>(<span class="function"><span class="params">token</span> =&gt;</span> &#123;</span><br><span class="line">            response.<span class="property">headers</span>.<span class="property">Authorization</span> = <span class="string">`<span class="subst">$&#123;token&#125;</span>`</span></span><br><span class="line">            <span class="title function_">resolve</span>(<span class="title function_">service</span>(response.<span class="property">config</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">    <span class="keyword">return</span> response &amp;&amp; response.<span class="property">data</span></span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="function">(<span class="params">error</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="title class_">Promise</span>.<span class="title function_">reject</span>(error)</span><br><span class="line">  &#125;</span><br><span class="line">)</span><br></pre></td></tr></table></figure>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/ac2c22966f31/</id>
    <link href="https://blog.zisheng.pro/posts/ac2c22966f31/"/>
    <published>2022-10-31T05:43:33.000Z</published>
    <summary>为了安全考虑，一般后端都要求在请求接口时传递登录的 Token。为了防止 Token 泄漏的风险，服务器的 Token 一般不会设置太长时间，比如我最近联调的，两个小时就会过期，token 过期就需要重新登陆。</summary>
    <title>前端无感知刷新 Token</title>
    <updated>2026-09-18T08:15:28.636Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="JavaScript" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/JavaScript/"/>
    <category term="隐式类型转换" scheme="https://blog.zisheng.pro/tags/%E9%9A%90%E5%BC%8F%E7%B1%BB%E5%9E%8B%E8%BD%AC%E6%8D%A2/"/>
    <category term="JavaScript 相等操作符" scheme="https://blog.zisheng.pro/tags/JavaScript-%E7%9B%B8%E7%AD%89%E6%93%8D%E4%BD%9C%E7%AC%A6/"/>
    <content>
      <![CDATA[<p>隐式类型转换多存在于使用 <code>==</code> 进行比较的场景，例如：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> a = <span class="number">1</span>;</span><br><span class="line"><span class="keyword">var</span> b = <span class="literal">true</span>;</span><br><span class="line"></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">&#x27;a == b&#x27;</span>); <span class="comment">// true</span></span><br></pre></td></tr></table></figure><p>不同类型的值用相等运算符比较后的结果，可以用下表总结。</p><p><img src="https://s2.loli.net/2022/04/16/WZboPTdBRmXQkja.png" alt="紫升"></p><p><code>toNumber</code> 方法对不同类型返回的结果如下。</p><p><img src="https://s2.loli.net/2022/04/17/VQ7BxmTqcOFUh34.png" alt="紫升"></p><p><code>toPrimitive()</code>：如果对象的 <code>valueOf</code> 方法的结果是原始值，返回原始值；如果对象的 <code>toString</code> 方法返回原始值，就返回这个值；其他情况都返回一个错误。</p><p>用例子来验证一下表格中的结果。</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="literal">true</span> == <span class="string">&#x27;true&#x27;</span>); <span class="comment">// false</span></span><br></pre></td></tr></table></figure><ul><li>首先，布尔值会被 <code>toNumber</code> 方法转换为数字，因此得到 <code>true == 1</code></li><li>其次，用 <code>toNumber</code> 转换字符串值。因为字符串包含字母，所以会被转成 <code>NaN</code>，因此表达式就变成了 <code>1 == NaN</code>，结果就是 false。</li></ul>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/f1f7395da421/</id>
    <link href="https://blog.zisheng.pro/posts/f1f7395da421/"/>
    <published>2022-04-16T09:32:32.000Z</published>
    <summary>JavaScript 隐式类型转换之相等操作符</summary>
    <title>JavaScript 隐式类型转换之相等操作符</title>
    <updated>2026-09-18T08:15:28.629Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="JavaScript" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/JavaScript/"/>
    <category term="面试题" scheme="https://blog.zisheng.pro/tags/%E9%9D%A2%E8%AF%95%E9%A2%98/"/>
    <category term="for...in" scheme="https://blog.zisheng.pro/tags/for-in/"/>
    <category term="for...of" scheme="https://blog.zisheng.pro/tags/for-of/"/>
    <category term="JavaScript Array" scheme="https://blog.zisheng.pro/tags/JavaScript-Array/"/>
    <category term="JavaScript Map" scheme="https://blog.zisheng.pro/tags/JavaScript-Map/"/>
    <category term="JavaScript Set" scheme="https://blog.zisheng.pro/tags/JavaScript-Set/"/>
    <content>
      <![CDATA[<h2 id="迭代的输入不同"><a href="#迭代的输入不同" class="headerlink" title="迭代的输入不同"></a>迭代的输入不同</h2><ul><li><code>for...in</code> 可以对任何对象进行操作；它通常被用作检查对象的属性。</li><li><code>for...of</code> 只能对可迭代对象进行操作；它通常被用作迭代数组的元素。</li></ul><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> obj = &#123;</span><br><span class="line">  <span class="attr">a</span>: <span class="number">1</span>,</span><br><span class="line">  <span class="attr">b</span>: <span class="number">2</span>,</span><br><span class="line">  <span class="attr">c</span>: <span class="number">3</span></span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> (<span class="keyword">var</span> key <span class="keyword">in</span> obj) &#123;</span><br><span class="line">  <span class="variable language_">console</span>.<span class="title function_">log</span>(key); <span class="comment">// &quot;a&quot;, &quot;b&quot;, &quot;c&quot;</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> (<span class="keyword">var</span> value <span class="keyword">of</span> obj) &#123; <span class="comment">// Uncaught TypeError: obj is not iterable</span></span><br><span class="line">  <span class="variable language_">console</span>.<span class="title function_">log</span>(value);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="迭代的输出不同"><a href="#迭代的输出不同" class="headerlink" title="迭代的输出不同"></a>迭代的输出不同</h2><ul><li><code>for...in</code> 返回被迭代对象的键列表</li><li><code>for...of</code> 返回被迭代对象的数字属性的值列表</li></ul><h3 id="Array"><a href="#Array" class="headerlink" title="Array"></a>Array</h3><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> list = [<span class="number">4</span>, <span class="number">5</span>, <span class="number">6</span>];</span><br><span class="line">list.<span class="property">foo</span> = <span class="string">&quot;bar&quot;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> (<span class="keyword">let</span> i <span class="keyword">in</span> list) &#123;</span><br><span class="line">  <span class="variable language_">console</span>.<span class="title function_">log</span>(i); <span class="comment">// &quot;0&quot;, &quot;1&quot;, &quot;2&quot;, &quot;foo&quot;</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> (<span class="keyword">let</span> i <span class="keyword">of</span> list) &#123;</span><br><span class="line">  <span class="variable language_">console</span>.<span class="title function_">log</span>(i); <span class="comment">// &quot;&quot;4&quot;, &quot;5&quot;, &quot;6&quot;</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="Set"><a href="#Set" class="headerlink" title="Set"></a>Set</h3><p><img src="https://s2.loli.net/2022/04/12/RycBMqHSUPr8uXg.png" alt="JavaScript Set"></p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> pets = <span class="keyword">new</span> <span class="title class_">Set</span>([<span class="string">&#x27;cat&#x27;</span>, <span class="string">&#x27;dog&#x27;</span>, <span class="string">&#x27;bat&#x27;</span>]);</span><br><span class="line">pets[<span class="string">&quot;species&quot;</span>] = <span class="string">&quot;mammal&quot;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> (<span class="keyword">let</span> pet <span class="keyword">in</span> pets) &#123;</span><br><span class="line">  <span class="variable language_">console</span>.<span class="title function_">log</span>(pet); <span class="comment">// &quot;species&quot;</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> (<span class="keyword">let</span> pet <span class="keyword">of</span> pets) &#123;</span><br><span class="line">  <span class="variable language_">console</span>.<span class="title function_">log</span>(pet); <span class="comment">// &quot;cat&quot;, &quot;dog&quot;, &quot;bat&quot;</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="Map"><a href="#Map" class="headerlink" title="Map"></a>Map</h3><p><img src="https://s2.loli.net/2022/04/12/x3WkK54cEUq2pDn.png" alt="JavaScript Map"></p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> pets = <span class="keyword">new</span> <span class="title class_">Map</span>([[<span class="string">&quot;1&quot;</span>, <span class="string">&quot;cat&quot;</span>],[<span class="string">&quot;2&quot;</span>, <span class="string">&quot;dog&quot;</span>],[<span class="string">&quot;3&quot;</span>, <span class="string">&quot;bat&quot;</span>]]);</span><br><span class="line">pets[<span class="string">&quot;species&quot;</span>] = <span class="string">&quot;mammal&quot;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> (<span class="keyword">let</span> pet <span class="keyword">in</span> pets) &#123;</span><br><span class="line">  <span class="variable language_">console</span>.<span class="title function_">log</span>(pet); <span class="comment">// &quot;species&quot;</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> (<span class="keyword">let</span> pet <span class="keyword">of</span> pets) &#123;</span><br><span class="line">  <span class="variable language_">console</span>.<span class="title function_">log</span>(pet); <span class="comment">// [&quot;1&quot;, &quot;cat&quot;], [&quot;2&quot;, &quot;dog&quot;], [&quot;3&quot;, &quot;bat&quot;]</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h1 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h1><ul><li><a href="https://stackoverflow.com/questions/29285897/what-is-the-difference-between-for-in-and-for-of-statements">What is the difference between ( for… in ) and ( for… of ) statements?</a></li></ul>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/124e984dfd7e/</id>
    <link href="https://blog.zisheng.pro/posts/124e984dfd7e/"/>
    <published>2022-04-12T09:13:30.000Z</published>
    <summary>本文详细介绍了 for-in 和 for-of 的区别，包括迭代的输入不同、迭代的输出不同、Array、Set、Map 等。</summary>
    <title>for-in 和 for-of 的区别</title>
    <updated>2026-09-18T08:15:28.632Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="全栈开发" scheme="https://blog.zisheng.pro/categories/%E5%85%A8%E6%A0%88%E5%BC%80%E5%8F%91/"/>
    <category term="MongoDB" scheme="https://blog.zisheng.pro/tags/MongoDB/"/>
    <content>
      <![CDATA[<h1 id="MongoDB-介绍"><a href="#MongoDB-介绍" class="headerlink" title="MongoDB 介绍"></a>MongoDB 介绍</h1><blockquote><p>一个面向文档对象的数据库。</p></blockquote><p>MongoDB 是一个基于分布式文件存储的数据库。由 C++ 语言编写。旨在为 WEB 应用提供可扩展的高性能数据存储解决方案。</p><p>MongoDB 是一个介于关系数据库和非关系数据库之间的产品，是非关系数据库当中功能最丰富，最像关系数据库的。</p><h1 id="管理-MongoDB"><a href="#管理-MongoDB" class="headerlink" title="管理 MongoDB"></a>管理 MongoDB</h1><h2 id="安装-MongoDB"><a href="#安装-MongoDB" class="headerlink" title="安装 MongoDB"></a>安装 MongoDB</h2><p>首先确保你的包是最新的：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sudo apt update &amp;&amp; sudo apt upgrade -y</span><br></pre></td></tr></table></figure><p>然后安装 MongoDB：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sudo apt install mongodb</span><br></pre></td></tr></table></figure><p>检查 mongodb 服务的状态：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sudo systemctl status mongodb</span><br></pre></td></tr></table></figure><p><img src="https://s2.loli.net/2022/04/13/YX7SqhNajgwHuZv.png" alt="紫升"></p><p>可以看到，服务正常启动了。</p><h2 id="管理-MongoDB-1"><a href="#管理-MongoDB-1" class="headerlink" title="管理 MongoDB"></a>管理 MongoDB</h2><h3 id="服务状态管理"><a href="#服务状态管理" class="headerlink" title="服务状态管理"></a>服务状态管理</h3><p>MongoDB 目前是一个 <code>systemd</code> 服务，因此我们使用 <code>systemctl</code> 来检查和修改它的状态，使用以下命令：</p><ul><li>查看运行状态：<code>sudo systemctl status mongodb</code></li><li>停止服务状态：<code>sudo systemctl stop mongodb</code></li><li>启动服务状态：<code>sudo systemctl start mongodb</code></li><li>重启服务状态：<code>sudo systemctl restart mongodb</code></li><li>启用开机自启动：<code>sudo systemctl enable mongodb</code></li><li>禁用开机自启动：<code>sudo systemctl disable mongodb</code></li></ul><h3 id="配置管理"><a href="#配置管理" class="headerlink" title="配置管理"></a>配置管理</h3><ul><li>查看配置文件：<code>sudo nano /etc/mongodb.conf</code></li></ul><h3 id="日志管理"><a href="#日志管理" class="headerlink" title="日志管理"></a>日志管理</h3><ul><li>查看日志：<code>sudo journalctl -u mongodb</code></li><li>查看错误日志：<code>sudo journalctl -u mongodb --no-pager -n 100 -f | grep -i error</code></li><li>查看警告日志：<code>sudo journalctl -u mongodb --no-pager -n 100 -f | grep -i warning</code></li><li>查看提示日志：<code>sudo journalctl -u mongodb --no-pager -n 100 -f | grep -i notice</code></li></ul><h3 id="卸载-MongoDB"><a href="#卸载-MongoDB" class="headerlink" title="卸载 MongoDB"></a>卸载 MongoDB</h3><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">sudo systemctl stop mongodb</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">sudo apt purge mongodb</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">sudo apt autoremove</span></span><br></pre></td></tr></table></figure><h2 id="远程管理-MongoDB"><a href="#远程管理-MongoDB" class="headerlink" title="远程管理 MongoDB"></a>远程管理 MongoDB</h2><p>默认无法远程访问 MongoDB，我们需要先在服务器安全组中开放 <code>27017</code> 端口，然后再进行配置：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">sudo /etc/mongodb.conf</span></span><br></pre></td></tr></table></figure><p>修改  <code>bind_ip = 0.0.0.0</code> 为 <code>bind_ip = 0.0.0.0</code>，然后重启服务：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">sudo systemctl restart mongodb</span></span><br></pre></td></tr></table></figure><p>安装 <a href="https://studio3t.com/free">Studio 3T for Free</a>，然后连接：</p><p><img src="https://s2.loli.net/2022/04/13/tUBEgnVpSAc715F.png" alt="紫升"></p><h2 id="安全管理"><a href="#安全管理" class="headerlink" title="安全管理"></a>安全管理</h2><p>默认我们不需要认证即可连接 MongoDB，这是很危险的，我们需要设置一个管理员用户来认证：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">mongo</span><br><span class="line"><span class="meta prompt_">&gt; </span><span class="language-bash">use admin</span></span><br><span class="line"><span class="meta prompt_">&gt; </span><span class="language-bash">db.createUser(&#123;user:<span class="string">&quot;admin&quot;</span>, <span class="built_in">pwd</span>:<span class="string">&quot;123456&quot;</span>, roles:[&#123;role:<span class="string">&quot;userAdminAnyDatabase&quot;</span>, db:<span class="string">&quot;admin&quot;</span>&#125;]&#125;)</span></span><br></pre></td></tr></table></figure><p>然后配置 MongoDB 的安全策略：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">sudo nano /etc/mongodb.conf</span></span><br></pre></td></tr></table></figure><p>将 <code>security.authorization = enabled</code> 写入文件，然后重启服务：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sudo systemctl restart mongodb</span><br></pre></td></tr></table></figure><p>在 Studio 3T 中配置用户名密码即可，需要注意的是 Authentication DB 需要填写对应的。</p><p><img src="https://s2.loli.net/2022/04/13/fLhd9t1sZ8KTaRA.png" alt="紫升"></p><p>更多安全知识参考 <a href="https://help.aliyun.com/document_detail/112035.html">MongoDB数据库未授权访问漏洞防御最佳实践</a></p><h1 id="MongoDB-概念"><a href="#MongoDB-概念" class="headerlink" title="MongoDB 概念"></a>MongoDB 概念</h1><ul><li>database：数据库，是 MongoDB 的核心，用来存储数据，每个数据库都有自己的集合，集合中存储的是数据。</li><li>collection：集合，相当于关系型数据库中的表（table）</li><li>document：文档，相当于关系型数据库中的行（row）</li><li>field：字段，相当于关系型数据库中的列（column）</li><li>index：索引，相当于关系型数据库中的索引</li><li>primary key：主键，相当于关系型数据库中的主键，MongoDB 中主键的名称默认为 <code>_id</code></li></ul><h2 id="数据库"><a href="#数据库" class="headerlink" title="数据库"></a>数据库</h2><p>一个 mongodb 中可以建立多个数据库。 MongoDB 的默认数据库为 <code>db</code>，该数据库存储在 data 目录中。</p><p>MongoDB 的单个实例可以容纳多个独立的数据库，每一个都有自己的集合和权限，不同的数据库也防止在不同的文件中。</p><p><code>show dbs</code> 命令可以显示所有数据的列表。</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">mongo</span></span><br><span class="line">MongoDB shell version v3.6.3</span><br><span class="line">connecting to: mongodb://127.0.0.1:27017</span><br><span class="line">MongoDB server version: 3.6.3</span><br><span class="line"><span class="meta prompt_">&gt; </span><span class="language-bash">show dbs</span></span><br><span class="line">admin   0.000GB</span><br><span class="line">config  0.000GB</span><br><span class="line">local   0.000GB</span><br></pre></td></tr></table></figure><p>运行 <code>use</code> 命令可以切换数据库，如果不存在则会自动创建。</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">&gt; </span><span class="language-bash">use <span class="built_in">test</span></span></span><br><span class="line">switched to db test</span><br></pre></td></tr></table></figure><p>执行 <code>db</code> 命令可以显示当前数据库对象或集合。</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">&gt; </span><span class="language-bash">db</span></span><br><span class="line">test</span><br></pre></td></tr></table></figure><h3 id="数据库命名规范"><a href="#数据库命名规范" class="headerlink" title="数据库命名规范"></a>数据库命名规范</h3><ul><li>不能是空字符</li><li>不能含有 <code>&#39; &#39;</code>（空格）、<code>.</code>、<code>/</code>、<code>\</code> 和 <code>\0</code>（空字符）</li><li>应全部小写</li><li>最多 64 字节</li></ul><h3 id="默认数据库"><a href="#默认数据库" class="headerlink" title="默认数据库"></a>默认数据库</h3><ul><li>admin：从权限的角度来看，这是 root 数据库。要是将一个用户添加到这个数据库，这个用户自动继承所有数据库的权限。一些特定的服务器端命令也只能从这个数据库运行，比如列出所有的数据库或者关闭服务器。</li><li>local：这个数据永远不会被复制，可以用来存储限于本地单台服务器的任意集合。</li><li>config：当 Mongo 用于分片设置时，config 数据库在内部使用，用于保存分片的相关信息。</li></ul><h1 id="连接-MongoDB"><a href="#连接-MongoDB" class="headerlink" title="连接 MongoDB"></a>连接 MongoDB</h1><p>标准 URI 连接语法：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">mongodb://[username:password@]host1[:port1][,host2[:port2],...[,hostN[:portN]]][/[database][?options]]</span><br></pre></td></tr></table></figure><ul><li><code>mongodb://</code>：这是固定的格式，必须指定</li><li><code>username:password@</code>：可选项，如果设置，在连接数据库服务器之后，驱动都会尝试登录这个数据库</li><li><code>host1</code>：必须的，指定至少一个 host，host1 是这个 URI 唯一要填写的。它指定了要连接服务器的地址。如果要连接复制集，请指定多个主机地址。</li><li><code>portX</code>：可选的指定端口，如果不填，默认为 27017</li><li><code>/database</code>：如果指定 <code>username:password@</code>，连接并验证登录指定数据库。若不指定，默认打开 test 数据库。</li><li><code>?options</code>：是连接选项。如果不使用 <code>/database</code>，则前面需要加上 <code>/</code>。所有连接选项都是键值对 <code>name=value</code>，键值对之间通过 <code>&amp;</code> 或 <code>;</code> 隔开</li></ul><h2 id="nodejs"><a href="#nodejs" class="headerlink" title="nodejs"></a>nodejs</h2><h3 id="安装驱动"><a href="#安装驱动" class="headerlink" title="安装驱动"></a>安装驱动</h3><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">yarn add mongodb</span></span><br></pre></td></tr></table></figure><h3 id="连接数据库"><a href="#连接数据库" class="headerlink" title="连接数据库"></a>连接数据库</h3><p>要在 MongoDB 中创建一个数据库，首先我们需要创建一个 MongoClient 对象，然后配置好指定的 URL 和端口号。</p><p>如果数据库不存在，MongoDB 将创建数据库并建立连接。</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> <span class="title class_">MongoClient</span> = <span class="built_in">require</span>(<span class="string">&#x27;mongodb&#x27;</span>).<span class="property">MongoClient</span>;</span><br><span class="line"><span class="keyword">var</span> url = <span class="string">&quot;mongodb://localhost:27017/luozhu&quot;</span>;</span><br><span class="line"></span><br><span class="line"><span class="title class_">MongoClient</span>.<span class="title function_">connect</span>(url, <span class="keyword">function</span>(<span class="params">err, db</span>) &#123;</span><br><span class="line">  <span class="keyword">if</span> (err) <span class="keyword">throw</span> err;</span><br><span class="line">  <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">&quot;数据库已创建！&quot;</span>);</span><br><span class="line">  db.<span class="title function_">close</span>();</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><h3 id="创建集合"><a href="#创建集合" class="headerlink" title="创建集合"></a>创建集合</h3><p>我们可以使用 <code>createCollection()</code> 方法来创建一个集合，这个集合可以是空的，也可以包含数据。</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> <span class="title class_">MongoClient</span> = <span class="built_in">require</span>(<span class="string">&#x27;mongodb&#x27;</span>).<span class="property">MongoClient</span>;</span><br><span class="line"><span class="keyword">var</span> url = <span class="string">&quot;mongodb://localhost:27017/luozhu&quot;</span>;</span><br><span class="line"></span><br><span class="line"><span class="title class_">MongoClient</span>.<span class="title function_">connect</span>(url, <span class="keyword">function</span>(<span class="params">err, db</span>) &#123;</span><br><span class="line">  <span class="keyword">if</span> (err) <span class="keyword">throw</span> err;</span><br><span class="line">  <span class="keyword">var</span> dbo = db.<span class="title function_">db</span>(<span class="string">&quot;luozhu&quot;</span>);</span><br><span class="line">  dbo.<span class="title function_">createCollection</span>(<span class="string">&quot;site&quot;</span>, <span class="keyword">function</span>(<span class="params">err, res</span>) &#123;</span><br><span class="line">    <span class="keyword">if</span> (err) <span class="keyword">throw</span> err;</span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">&quot;集合已创建！&quot;</span>);</span><br><span class="line">    db.<span class="title function_">close</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>以下实例我们连接数据库 luozhu 的 site 表，并插入一条数据。使用 <code>insertOne()</code> 。</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> <span class="title class_">MongoClient</span> = <span class="built_in">require</span>(<span class="string">&#x27;mongodb&#x27;</span>).<span class="property">MongoClient</span>;</span><br><span class="line"><span class="keyword">var</span> url = <span class="string">&quot;mongodb://localhost:27017/luozhu&quot;</span>;</span><br><span class="line"></span><br><span class="line"><span class="title class_">MongoClient</span>.<span class="title function_">connect</span>(url, <span class="keyword">function</span>(<span class="params">err, db</span>) &#123;</span><br><span class="line">  <span class="keyword">if</span> (err) <span class="keyword">throw</span> err;</span><br><span class="line">  <span class="keyword">var</span> dbo = db.<span class="title function_">db</span>(<span class="string">&quot;luozhu&quot;</span>);</span><br><span class="line">  <span class="keyword">var</span> myobj = &#123; <span class="attr">name</span>: <span class="string">&quot;Google&quot;</span>, <span class="attr">address</span>: <span class="string">&quot;https://google.com&quot;</span> &#125;;</span><br><span class="line"></span><br><span class="line">  dbo.<span class="title function_">collection</span>(<span class="string">&quot;site&quot;</span>).<span class="title function_">insertOne</span>(myobj, <span class="keyword">function</span>(<span class="params">err, res</span>) &#123;</span><br><span class="line">    <span class="keyword">if</span> (err) <span class="keyword">throw</span> err;</span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">&quot;文档插入成功&quot;</span>);</span><br><span class="line">    db.<span class="title function_">close</span>();</span><br><span class="line">  &#125;);</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><p>如果要插入多条数据，可以使用 <code>insertMany()</code> 。</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> <span class="title class_">MongoClient</span> = <span class="built_in">require</span>(<span class="string">&quot;mongodb&quot;</span>).<span class="property">MongoClient</span>;</span><br><span class="line"><span class="keyword">var</span> url = <span class="string">&quot;mongodb://localhost:27017/&quot;</span>;</span><br><span class="line"></span><br><span class="line"><span class="title class_">MongoClient</span>.<span class="title function_">connect</span>(url, <span class="keyword">function</span>(<span class="params">err, db</span>) &#123;</span><br><span class="line">  <span class="keyword">if</span> (err) <span class="variable language_">console</span>.<span class="title function_">log</span>(err);</span><br><span class="line">  <span class="keyword">var</span> dbo = db.<span class="title function_">db</span>(<span class="string">&quot;luozhu&quot;</span>);</span><br><span class="line">  <span class="keyword">var</span> myobj = [</span><br><span class="line">    &#123; <span class="attr">name</span>: <span class="string">&#x27;Facebook&#x27;</span>, <span class="attr">website</span>: <span class="string">&#x27;https://www.facebook.com/&#x27;</span> &#125;,</span><br><span class="line">    &#123; <span class="attr">name</span>: <span class="string">&#x27;Github&#x27;</span>, <span class="attr">website</span>: <span class="string">&#x27;https://github.com&#x27;</span>&#125;</span><br><span class="line">  ]</span><br><span class="line">  dbo.<span class="title function_">collection</span>(<span class="string">&quot;site&quot;</span>).<span class="title function_">insertMany</span>(myobj, <span class="keyword">function</span>(<span class="params">err, res</span>) &#123;</span><br><span class="line">    <span class="keyword">if</span> (err) <span class="variable language_">console</span>.<span class="title function_">log</span>(err);</span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">&quot;文档插入成功&quot;</span>, res.<span class="property">insertedCount</span>);</span><br><span class="line">    db.<span class="title function_">close</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><h4 id="查询所有数据"><a href="#查询所有数据" class="headerlink" title="查询所有数据"></a>查询所有数据</h4><p>可以使用 <code>find()</code> 来查找数据。<code>find()</code> 可以返回符合条件的所有数据。如果未指定条件，<code>find()</code> 返回集合中的所有数据。</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> <span class="title class_">MongoClient</span> = <span class="built_in">require</span>(<span class="string">&quot;mongodb&quot;</span>).<span class="property">MongoClient</span>;</span><br><span class="line"><span class="keyword">var</span> url = <span class="string">&quot;mongodb://localhost:27017/&quot;</span>;</span><br><span class="line"></span><br><span class="line"><span class="title class_">MongoClient</span>.<span class="title function_">connect</span>(url, <span class="keyword">function</span>(<span class="params">err, db</span>) &#123;</span><br><span class="line">  <span class="keyword">if</span>(err) <span class="variable language_">console</span>.<span class="title function_">log</span>(err);</span><br><span class="line">  <span class="keyword">var</span> dbo = db.<span class="title function_">db</span>(<span class="string">&quot;luozhu&quot;</span>);</span><br><span class="line"></span><br><span class="line">  dbo.<span class="title function_">collection</span>(<span class="string">&quot;site&quot;</span>).<span class="title function_">find</span>(&#123;&#125;).<span class="title function_">toArray</span>(<span class="keyword">function</span>(<span class="params">err, result</span>) &#123; <span class="comment">// 返回集合中所有数据</span></span><br><span class="line">    <span class="keyword">if</span>(err) <span class="variable language_">console</span>.<span class="title function_">log</span>(err);</span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">log</span>(result);</span><br><span class="line">    db.<span class="title function_">close</span>();</span><br><span class="line">  &#125;)</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 js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> <span class="title class_">MongoClient</span> = <span class="built_in">require</span>(<span class="string">&quot;mongodb&quot;</span>).<span class="property">MongoClient</span>;</span><br><span class="line"><span class="keyword">var</span> url = <span class="string">&quot;mongodb://localhost:27017/&quot;</span>;</span><br><span class="line"></span><br><span class="line"><span class="title class_">MongoClient</span>.<span class="title function_">connect</span>(url, <span class="keyword">function</span>(<span class="params">err, db</span>) &#123;</span><br><span class="line">  <span class="keyword">if</span>(err) <span class="variable language_">console</span>.<span class="title function_">log</span>(err);</span><br><span class="line">  <span class="keyword">var</span> dbo = db.<span class="title function_">db</span>(<span class="string">&quot;luozhu&quot;</span>);</span><br><span class="line">  <span class="keyword">var</span> whereStr = &#123;<span class="string">&quot;name&quot;</span>:<span class="string">&#x27;Github&#x27;</span>&#125;;  <span class="comment">// 查询条件</span></span><br><span class="line">  dbo.<span class="title function_">collection</span>(<span class="string">&quot;site&quot;</span>).<span class="title function_">find</span>(whereStr).<span class="title function_">toArray</span>(<span class="keyword">function</span>(<span class="params">err, result</span>) &#123;</span><br><span class="line">    <span class="keyword">if</span>(err) <span class="variable language_">console</span>.<span class="title function_">log</span>(err);</span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">log</span>(result);</span><br><span class="line">    db.<span class="title function_">close</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><h4 id="更新一条文档"><a href="#更新一条文档" class="headerlink" title="更新一条文档"></a>更新一条文档</h4><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> <span class="title class_">MongoClient</span> = <span class="built_in">require</span>(<span class="string">&quot;mongodb&quot;</span>).<span class="property">MongoClient</span>;</span><br><span class="line"><span class="keyword">var</span> url = <span class="string">&quot;mongodb://localhost:27017/&quot;</span>;</span><br><span class="line"></span><br><span class="line"><span class="title class_">MongoClient</span>.<span class="title function_">connect</span>(url, <span class="keyword">function</span>(<span class="params">err, db</span>) &#123;</span><br><span class="line">  <span class="keyword">if</span>(err) <span class="variable language_">console</span>.<span class="title function_">log</span>(err);</span><br><span class="line">  <span class="keyword">var</span> dbo = db.<span class="title function_">db</span>(<span class="string">&quot;luozhu&quot;</span>);</span><br><span class="line">  <span class="keyword">var</span> whereStr = &#123; <span class="string">&quot;name&quot;</span>: <span class="string">&#x27;Github&#x27;</span> &#125;;  <span class="comment">// 查询条件</span></span><br><span class="line">  <span class="keyword">var</span> updateStr = &#123; <span class="attr">$set</span>:&#123; <span class="string">&quot;website&quot;</span>: <span class="string">&quot;https://github.com/youngjuning&quot;</span> &#125;&#125;;  <span class="comment">// 更新内容</span></span><br><span class="line">  dbo.<span class="title function_">collection</span>(<span class="string">&quot;site&quot;</span>).<span class="title function_">updateOne</span>(whereStr, updateStr, <span class="keyword">function</span> (<span class="params">err, res</span>) &#123;</span><br><span class="line">    <span class="keyword">if</span>(err) <span class="variable language_">console</span>.<span class="title function_">log</span>(err);</span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">&quot;文档更新成功&quot;</span>, res.<span class="property">matchedCount</span>);</span><br><span class="line">    db.<span class="title function_">close</span>();</span><br><span class="line">  &#125;)</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><p>执行成功后，进入 mongo 管理工具查看数据已修改：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">&gt; db.site.find(&#123; name: <span class="string">&quot;GitHub&quot;</span> &#125;).<span class="function"><span class="title">pretty</span></span>()</span><br><span class="line">&#123;</span><br><span class="line">    <span class="string">&quot;_id&quot;</span> : ObjectId(<span class="string">&quot;5a794e36763eb821b24db854&quot;</span>),</span><br><span class="line">    <span class="string">&quot;name&quot;</span> : <span class="string">&quot;Github&quot;</span>,</span><br><span class="line">    <span class="string">&quot;url&quot;</span> : <span class="string">&quot;https://www.github.com&quot;</span>     <span class="comment"># 已修改为 https</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="更新多条文档"><a href="#更新多条文档" class="headerlink" title="更新多条文档"></a>更新多条文档</h4><p>如果要跟新所有符合条件的文档数据，可以使用 <code>updateMany()</code> 。</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> <span class="title class_">MongoClient</span> = <span class="built_in">require</span>(<span class="string">&#x27;mongodb&#x27;</span>).<span class="property">MongoClient</span>;</span><br><span class="line"><span class="keyword">var</span> url = <span class="string">&quot;mongodb://localhost:27018/&quot;</span>;</span><br><span class="line"></span><br><span class="line"><span class="title class_">MongoClient</span>.<span class="title function_">connect</span>(url, <span class="function">(<span class="params">err, db</span>) =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">if</span> (err) <span class="variable language_">console</span>.<span class="title function_">log</span>(err);</span><br><span class="line">  <span class="keyword">var</span> dbo = db.<span class="title function_">db</span>(<span class="string">&quot;luozhu&quot;</span>);</span><br><span class="line">  <span class="keyword">var</span> whereStr = &#123;<span class="string">&quot;country&quot;</span>: <span class="string">&#x27;us&#x27;</span>&#125;;  <span class="comment">// 查询条件</span></span><br><span class="line">  <span class="keyword">var</span> updateStr = &#123;<span class="attr">$set</span>: &#123; <span class="string">&quot;country&quot;</span> : <span class="string">&quot;American&quot;</span> &#125;&#125;;</span><br><span class="line">  dbo.<span class="title function_">collection</span>(<span class="string">&quot;site&quot;</span>).<span class="title function_">updateMany</span>(whereStr, updateStr, <span class="function">(<span class="params">err, res</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> (err) <span class="variable language_">console</span>.<span class="title function_">log</span>(err);</span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">log</span>(res.<span class="property">modifiedCount</span> + <span class="string">&quot; 条文档被更新&quot;</span>);</span><br><span class="line">    db.<span class="title function_">close</span>();</span><br><span class="line">  &#125;)</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p><code>res.modifiedCount</code> 属性返回修改的文档数量。</p><h3 id="删除数据"><a href="#删除数据" class="headerlink" title="删除数据"></a>删除数据</h3><h4 id="删除一条数据"><a href="#删除一条数据" class="headerlink" title="删除一条数据"></a>删除一条数据</h4><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> <span class="title class_">MongoClient</span> = <span class="built_in">require</span>(<span class="string">&#x27;mongodb&#x27;</span>).<span class="property">MongoClient</span>;</span><br><span class="line"><span class="keyword">var</span> url = <span class="string">&quot;mongodb://localhost:27018/&quot;</span>;</span><br><span class="line"></span><br><span class="line"><span class="title class_">MongoClient</span>.<span class="title function_">connect</span>(url, <span class="function">(<span class="params">err, db</span>) =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">if</span>(err) <span class="variable language_">console</span>.<span class="title function_">log</span>(err);</span><br><span class="line">  <span class="keyword">var</span> dbo = db.<span class="title function_">db</span>(<span class="string">&quot;luozhu&quot;</span>);</span><br><span class="line">  <span class="keyword">var</span> whereStr = &#123;<span class="string">&quot;name&quot;</span>: <span class="string">&#x27;Github&#x27;</span>&#125;;  <span class="comment">// 查询条件</span></span><br><span class="line">  dbo.<span class="title function_">collection</span>(<span class="string">&quot;site&quot;</span>).<span class="title function_">deleteOne</span>(whereStr, <span class="function">(<span class="params">err, res</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">if</span>(err) <span class="variable language_">console</span>.<span class="title function_">log</span>(err);</span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">log</span>(res.<span class="property">deletedCount</span> + <span class="string">&quot; 条文档被删除&quot;</span>);</span><br><span class="line">    db.<span class="title function_">close</span>();</span><br><span class="line">  &#125;);</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><h4 id="删除多条数据"><a href="#删除多条数据" class="headerlink" title="删除多条数据"></a>删除多条数据</h4><p>如果要删除多条语句可以使用 <code>deleteMany()</code> 方法。</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> <span class="title class_">MongoClient</span> = <span class="built_in">require</span>(<span class="string">&#x27;mongodb&#x27;</span>).<span class="property">MongoClient</span>;</span><br><span class="line"><span class="keyword">var</span> url = <span class="string">&quot;mongodb://localhost:27018/&quot;</span>;</span><br><span class="line"></span><br><span class="line"><span class="title class_">MongoClient</span>.<span class="title function_">connect</span>(url, <span class="keyword">function</span>(<span class="params">err, db</span>) &#123;</span><br><span class="line">  <span class="keyword">if</span> (err) <span class="variable language_">console</span>.<span class="title function_">log</span>(err);</span><br><span class="line">  <span class="keyword">var</span> dbo = db.<span class="title function_">db</span>(<span class="string">&quot;luozhu&quot;</span>);</span><br><span class="line">  <span class="keyword">var</span> whereStr = &#123;<span class="attr">country</span>: <span class="string">&quot;American&quot;</span>&#125;;  <span class="comment">// 查询条件</span></span><br><span class="line">  dbo.<span class="title function_">collection</span>(<span class="string">&quot;site&quot;</span>).<span class="title function_">deleteMany</span>(whereStr, <span class="function">(<span class="params">err, res</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">if</span>(err) <span class="variable language_">console</span>.<span class="title function_">log</span>(err);</span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">log</span>(res.<span class="property">deletedCount</span>, <span class="string">&quot;条文档被删除&quot;</span>);</span><br><span class="line">    db.<span class="title function_">close</span>();</span><br><span class="line">  &#125;)</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><p><code>res.deletedCount</code> 属性返回删除的文档数量。</p><h1 id="参考链接"><a href="#参考链接" class="headerlink" title="参考链接"></a>参考链接</h1><ul><li><a href="https://zhuanlan.zhihu.com/p/76349679">如何在 Ubuntu 上安装 MongoDB</a></li></ul>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/69a1dcb04fe1/</id>
    <link href="https://blog.zisheng.pro/posts/69a1dcb04fe1/"/>
    <published>2022-04-12T08:55:53.000Z</published>
    <summary>从安装、systemd 服务管理到常用查询与索引，整理 MongoDB 入门阶段最常用的操作；文中的系统包安装方式适合用于理解，生产部署应以官方当前文档为准。</summary>
    <title>MongoDB 学习笔记</title>
    <updated>2026-09-18T08:15:28.629Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="Node.js" scheme="https://blog.zisheng.pro/tags/Node-js/"/>
    <category term="VS Code" scheme="https://blog.zisheng.pro/tags/VS-Code/"/>
    <category term="ide" scheme="https://blog.zisheng.pro/tags/ide/"/>
    <category term="code-sever" scheme="https://blog.zisheng.pro/tags/code-sever/"/>
    <category term="webide" scheme="https://blog.zisheng.pro/tags/webide/"/>
    <content>
      <![CDATA[<h2 id="WebIDE-概述"><a href="#WebIDE-概述" class="headerlink" title="WebIDE 概述"></a>WebIDE 概述</h2><h3 id="WebIDE-介绍"><a href="#WebIDE-介绍" class="headerlink" title="WebIDE 介绍"></a>WebIDE 介绍</h3><p>WebIDE 是基于 H5 支持多语言的在线集成开发环境。相较于传统的本地 IDE，WebIDE 是开箱即用的，你不用配置 git 环境、node 环境、react 环境甚至 React Native 环境。</p><h3 id="WebIDE-发展与现状"><a href="#WebIDE-发展与现状" class="headerlink" title="WebIDE 发展与现状"></a>WebIDE 发展与现状</h3><ul><li>基于 <a href="https://codemirror.net/">CodeMirror</a> 和 <a href="https://ace.c9.io/">Ace</a> 实现编辑器部分，其余部分自己实现</li><li>以 <a href="https://codesandbox.io/">CodeSandbox</a> 为代表的的在线代码编辑器服务，私有部署较麻烦</li><li><a href="https://gitlab.com/youngjuning/react">Gitlab ide</a>：集成在 Gitlab 中的 IDE</li><li><a href="https://github.com/facebook/react/">Github ide</a>：基于 vscode 实现的与 github 结合的 IDE，限制是命令行使用有限制，</li><li><a href="https://codingcorp.cloudstudio.net/ws/qmbngm">Cloud Studio</a>：可以理解为国产 CodeSandbox，以前是基于 Ace 做的，现在也改成了基于 vscode，自定义功能通过 vscode 的插件体系来做</li></ul><p>通过比对市场上常见的 WebIDE 产品，我们能发现趋势都趋于统一了，就是依托 vscode 强大的能力来实现的，扩展能力也是基于 vscode 来实现。</p><h3 id="WebIDE-通用架构"><a href="#WebIDE-通用架构" class="headerlink" title="WebIDE 通用架构"></a>WebIDE 通用架构</h3><p><img src="https://s2.loli.net/2022/03/23/UfjaX4QpTmg5MKu.png" alt="紫升"></p><h3 id="WebIDE-的优势"><a href="#WebIDE-的优势" class="headerlink" title="WebIDE 的优势"></a>WebIDE 的优势</h3><ul><li>在具有一致开发环境的任何设备上编写代码</li><li>使用云服务器加速测试、编译、下载等来节省旅途中的笔记本电量</li><li>所有密集型任务都在您的服务器上运行，节省笔记本内存</li></ul><h3 id="工程化意义"><a href="#工程化意义" class="headerlink" title="工程化意义"></a>工程化意义</h3><ul><li>安全性：服务部署在内网，完全不用担心安全问题，且插件服务也可以内网部署。</li><li>规范性：通过定制编辑器，我们可以规范开发阶段的流程与环境</li><li>提效性：通过实时预览技术，可以实现开发阶段通过 mock 数据即可预览面板。另外由于是针对 OS 业务定制，所以开发套件都是定制好的，开发只需要关心代码</li><li>平台化：可以和内部任何平台打通交互，比如一键打包、一键发布</li></ul><h2 id="基于-code-server-搭建-WebIDE-平台"><a href="#基于-code-server-搭建-WebIDE-平台" class="headerlink" title="基于 code-server 搭建 WebIDE 平台"></a>基于 code-server 搭建 WebIDE 平台</h2><p>code-server 是一个免费的基于浏览器的 IDE，它是基于 vscode 封装的跨平台 IDE。</p><h3 id="部署"><a href="#部署" class="headerlink" title="部署"></a>部署</h3><h4 id="安装-nodejs"><a href="#安装-nodejs" class="headerlink" title="安装 nodejs"></a>安装 nodejs</h4><p><strong>安装源</strong></p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">curl -sL https://deb.nodesource.com/setup_14.x | sudo -E bash -</span><br></pre></td></tr></table></figure><p><strong>安装 nodejs</strong></p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sudo apt-get install -y nodejs</span><br></pre></td></tr></table></figure><p><strong>更新 npm</strong></p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sudo npm install -g npm</span><br></pre></td></tr></table></figure><h4 id="安装-code-server"><a href="#安装-code-server" class="headerlink" title="安装 code-server"></a>安装 code-server</h4><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">curl -fsSL https://code-server.dev/install.sh | sh</span><br></pre></td></tr></table></figure><p>执行下面的命令以支持 systemd 方式启动并开始开机自启动：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sudo systemctl <span class="built_in">enable</span> --now code-server@<span class="variable">$USER</span></span><br></pre></td></tr></table></figure><p>之后如果修改了配置文件，就可以使用 <code>sudo systemctl restart code-server@$USER</code> 重启服务。</p><p>或者你不需要后台服务，你可以执行下面的命令：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">code-server</span><br></pre></td></tr></table></figure><h4 id="nginx-反向代理"><a href="#nginx-反向代理" class="headerlink" title="nginx 反向代理"></a>nginx 反向代理</h4><p>安装 nginx：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">sudo apt update</span><br><span class="line">sudo apt install -y nginx</span><br></pre></td></tr></table></figure><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">server &#123;</span><br><span class="line">    listen 80;</span><br><span class="line">    listen [::]:80;</span><br><span class="line">    server_name mydomain.com;</span><br><span class="line"></span><br><span class="line">    location / &#123;</span><br><span class="line">      proxy_pass http://localhost:8080/;</span><br><span class="line">      proxy_set_header Host <span class="variable">$host</span>;</span><br><span class="line">      proxy_set_header Upgrade <span class="variable">$http_upgrade</span>;</span><br><span class="line">      proxy_set_header Connection upgrade;</span><br><span class="line">      proxy_set_header Accept-Encoding gzip;</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><ul><li><a href="https://www.zhihu.com/question/41050429">要实现一个Web IDE 需要哪些前端技术？</a></li><li><a href="https://developer.aliyun.com/article/646273">WebIDE 使用指南 - 阿里云</a></li><li><a href="https://docs.gitlab.com/ee/user/project/web_ide/">网络IDE - GitLab</a></li><li><a href="https://www.youtube.com/watch?v=lfYo4mfOyM8">What is CodeSandbox</a></li><li><a href="https://juejin.cn/post/7008428269317914661">如何在团队快速落地 WebIDE</a></li><li><a href="https://learnku.com/articles/42581">Ubuntu 安装最新版本 Node.js</a></li><li><a href="https://www.jianshu.com/p/648c48f146c6">腾讯云登录Root用户</a></li><li><a href="https://blog.zisheng.pro/21bdf0b4bd15/">写给前端的Linux实战教程</a></li></ul>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/dfd0faf39738/</id>
    <link href="https://blog.zisheng.pro/posts/dfd0faf39738/"/>
    <published>2022-03-19T10:00:00.000Z</published>
    <summary>WebIDE 是基于 H5 支持多语言的在线集成开发环境。相较于传统的本地 IDE，WebIDE 是开箱即用的，你不用配置 git 环境、node 环境、react 环境甚至 React Native 环境。</summary>
    <title>基于 code-server 实现 WebIDE 的探索</title>
    <updated>2026-09-18T08:15:28.632Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="加密货币" scheme="https://blog.zisheng.pro/categories/%E5%8A%A0%E5%AF%86%E8%B4%A7%E5%B8%81/"/>
    <category term="区块链" scheme="https://blog.zisheng.pro/categories/%E5%8A%A0%E5%AF%86%E8%B4%A7%E5%B8%81/%E5%8C%BA%E5%9D%97%E9%93%BE/"/>
    <category term="MetaMask" scheme="https://blog.zisheng.pro/tags/MetaMask/"/>
    <category term="BSC" scheme="https://blog.zisheng.pro/tags/BSC/"/>
    <category term="币安智能链" scheme="https://blog.zisheng.pro/tags/%E5%B8%81%E5%AE%89%E6%99%BA%E8%83%BD%E9%93%BE/"/>
    <category term="区块链" scheme="https://blog.zisheng.pro/tags/%E5%8C%BA%E5%9D%97%E9%93%BE/"/>
    <category term="加密货币" scheme="https://blog.zisheng.pro/tags/%E5%8A%A0%E5%AF%86%E8%B4%A7%E5%B8%81/"/>
    <category term="以太坊" scheme="https://blog.zisheng.pro/tags/%E4%BB%A5%E5%A4%AA%E5%9D%8A/"/>
    <category term="Ethereum" scheme="https://blog.zisheng.pro/tags/Ethereum/"/>
    <category term="以太坊钱包" scheme="https://blog.zisheng.pro/tags/%E4%BB%A5%E5%A4%AA%E5%9D%8A%E9%92%B1%E5%8C%85/"/>
    <category term="Ethereum Wallet" scheme="https://blog.zisheng.pro/tags/Ethereum-Wallet/"/>
    <category term="以太坊钱包应用" scheme="https://blog.zisheng.pro/tags/%E4%BB%A5%E5%A4%AA%E5%9D%8A%E9%92%B1%E5%8C%85%E5%BA%94%E7%94%A8/"/>
    <content>
      <![CDATA[<p>币安智能链是目前世界上最大的以太坊交易所，MetaMask 则是专注服务于以太坊的钱包应用。</p><h2 id="入口"><a href="#入口" class="headerlink" title="入口"></a>入口</h2><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images/202112301451827.png" alt="紫升"></p><h2 id="配置"><a href="#配置" class="headerlink" title="配置"></a>配置</h2><ul><li>网络名称: <code>币安智能链</code></li><li>RPC URL: <code>https://bsc-dataseed1.binance.org/</code></li><li>链 ID: <code>56</code></li><li>Currency Symbol: <code>WBNB</code></li><li>区块浏览器: <code>https://bscscan.com/</code></li></ul><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ul><li><a href="https://academy.binance.com/zh/articles/connecting-metamask-to-binance-smart-chain">在币安智能链中关联MetaMask钱包</a></li></ul>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/5cb0c0ce25b8/</id>
    <link href="https://blog.zisheng.pro/posts/5cb0c0ce25b8/"/>
    <published>2021-12-30T02:36:17.000Z</published>
    <summary>币安智能链是目前世界上最大的以太坊交易所，MetaMask 则是专注服务于以太坊的钱包应用。</summary>
    <title>使用 MetaMask 连接到币安智能链（BSC）</title>
    <updated>2026-09-18T08:15:28.636Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="React" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/React/"/>
    <category term="React" scheme="https://blog.zisheng.pro/tags/React/"/>
    <category term="hooks" scheme="https://blog.zisheng.pro/tags/hooks/"/>
    <category term="useEffect" scheme="https://blog.zisheng.pro/tags/useEffect/"/>
    <category term="useState" scheme="https://blog.zisheng.pro/tags/useState/"/>
    <content>
      <![CDATA[<h2 id="Warning-Can’t-perform-a-React-state-update-on-an-unmounted-component"><a href="#Warning-Can’t-perform-a-React-state-update-on-an-unmounted-component" class="headerlink" title="Warning: Can’t perform a React state update on an unmounted component"></a>Warning: Can’t perform a React state update on an unmounted component</h2><p>由于 <code>useEffect</code> 中有异步操作，当组件卸载后异步操作结束并执行 state 操作，React 就会提醒存在内存泄漏的风险。在 React Hooks 中，解决方法是在异步操作中判断是否组件被卸载了。代码如下：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">useAsync</span>(<span class="params">asyncFn, onSuccess</span>) &#123;</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">let</span> isActive = <span class="literal">true</span>;</span><br><span class="line">    <span class="title function_">asyncFn</span>().<span class="title function_">then</span>(<span class="function"><span class="params">data</span> =&gt;</span> &#123;</span><br><span class="line">      <span class="keyword">if</span> (isActive) <span class="title function_">onSuccess</span>(data);</span><br><span class="line">    &#125;);</span><br><span class="line">    <span class="keyword">return</span> <span class="function">() =&gt;</span> &#123; isActive = <span class="literal">false</span> &#125;;</span><br><span class="line">  &#125;, [asyncFn, onSuccess]);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>实际项目中，我们可以使用 ahooks 提供的功能更强大的 <code>useAsyncEffect</code>：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; useAsyncEffect &#125; <span class="keyword">from</span> <span class="string">&#x27;ahooks&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> <span class="title class_">React</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">function</span> <span class="title function_">mockCheck</span>(<span class="params"></span>): <span class="title class_">Promise</span>&lt;boolean&gt; &#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">Promise</span>(<span class="function">(<span class="params">resolve</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="built_in">setTimeout</span>(<span class="function">() =&gt;</span> <span class="title function_">resolve</span>(<span class="literal">true</span>), <span class="number">3000</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> () =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> [pass, setPass] = useState&lt;boolean&gt;(<span class="literal">null</span>);</span><br><span class="line"></span><br><span class="line">  <span class="title function_">useAsyncEffect</span>(<span class="keyword">async</span> () =&gt; &#123;</span><br><span class="line">    <span class="title function_">setPass</span>(<span class="keyword">await</span> <span class="title function_">mockCheck</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;pass === null &amp;&amp; &#x27;Checking...&#x27;&#125;</span></span><br><span class="line"><span class="language-xml">      &#123;pass === true &amp;&amp; &#x27;Check passed.&#x27;&#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>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/c22a00ea88bc/</id>
    <link href="https://blog.zisheng.pro/posts/c22a00ea88bc/"/>
    <published>2021-12-10T02:44:31.000Z</published>
    <summary>本文记录了一些 React Hooks 使用中的问题</summary>
    <title>React Hooks 问题记录</title>
    <updated>2026-09-18T08:15:28.632Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="加密货币" scheme="https://blog.zisheng.pro/categories/%E5%8A%A0%E5%AF%86%E8%B4%A7%E5%B8%81/"/>
    <category term="区块链" scheme="https://blog.zisheng.pro/categories/%E5%8A%A0%E5%AF%86%E8%B4%A7%E5%B8%81/%E5%8C%BA%E5%9D%97%E9%93%BE/"/>
    <category term="Next.js" scheme="https://blog.zisheng.pro/tags/Next-js/"/>
    <category term="BSC" scheme="https://blog.zisheng.pro/tags/BSC/"/>
    <category term="DApp" scheme="https://blog.zisheng.pro/tags/DApp/"/>
    <content>
      <![CDATA[<h2 id="项目初始化"><a href="#项目初始化" class="headerlink" title="项目初始化"></a>项目初始化</h2><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npx create-next-app@latest --typescript</span><br></pre></td></tr></table></figure><p>安装完成后，根据指示开启开发模式的 server。然后尝试编辑 <code>pages/index.tsx</code> 并在浏览器查看结果。</p><h2 id="开发规范"><a href="#开发规范" class="headerlink" title="开发规范"></a>开发规范</h2><h3 id="commitlint"><a href="#commitlint" class="headerlink" title="commitlint"></a>commitlint</h3><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npx @luozhu/create-commitlint</span><br></pre></td></tr></table></figure><h3 id="prettier"><a href="#prettier" class="headerlink" title="prettier"></a>prettier</h3><h4 id="安装依赖"><a href="#安装依赖" class="headerlink" title="安装依赖"></a>安装依赖</h4><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">yarn add prettier eslint-config-prettier eslint-plugin-prettier @luozhu/prettier-config -D</span><br></pre></td></tr></table></figure><h4 id="配置-eslintrc-json"><a href="#配置-eslintrc-json" class="headerlink" title="配置 .eslintrc.json"></a>配置 .eslintrc.json</h4><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;extends&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span><span class="string">&quot;next/core-web-vitals&quot;</span><span class="punctuation">,</span> <span class="string">&quot;plugin:prettier/recommended&quot;</span><span class="punctuation">,</span> <span class="string">&quot;prettier&quot;</span><span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;rules&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;no-unused-vars&quot;</span><span class="punctuation">:</span> <span class="number">1</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;react-hooks/exhaustive-deps&quot;</span><span class="punctuation">:</span> <span class="number">0</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;@next/next/no-img-element&quot;</span><span class="punctuation">:</span> <span class="number">0</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="配置-prettierrc-js"><a href="#配置-prettierrc-js" class="headerlink" title="配置 .prettierrc.js"></a>配置 .prettierrc.js</h4><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="variable language_">module</span>.<span class="property">exports</span> = <span class="built_in">require</span>(<span class="string">&#x27;@luozhu/prettier-config&#x27;</span>);</span><br></pre></td></tr></table></figure><h3 id="editorconfig"><a href="#editorconfig" class="headerlink" title=".editorconfig"></a>.editorconfig</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"># EditorConfig is awesome: http://EditorConfig.org</span><br><span class="line"></span><br><span class="line"># top-most EditorConfig file</span><br><span class="line">root = true</span><br><span class="line"></span><br><span class="line"># Unix-style newlines with a newline ending every file</span><br><span class="line">[*]</span><br><span class="line">quote_type = single # Fix Prettier &quot;prettier.singleQuote&quot; not working in 1.40 vs code</span><br><span class="line">indent_style = space</span><br><span class="line">indent_size = 2</span><br><span class="line">end_of_line = lf</span><br><span class="line">charset = utf-8</span><br><span class="line">trim_trailing_whitespace = true</span><br><span class="line">insert_final_newline = true</span><br><span class="line"></span><br><span class="line">[*.md]</span><br><span class="line">trim_trailing_whitespace = false</span><br><span class="line"></span><br><span class="line">[Makefile]</span><br><span class="line">indent_style = tab</span><br></pre></td></tr></table></figure><h3 id="lint-staged"><a href="#lint-staged" class="headerlink" title="lint-staged"></a>lint-staged</h3><p>安装 lint-staged：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">yarn add lint-staged -D</span><br></pre></td></tr></table></figure><p>在 <code>package.json</code> 文件中如下配置即可：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;gitHooks&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;pre-commit&quot;</span><span class="punctuation">:</span> <span class="string">&quot;lint-staged&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;commit-msg&quot;</span><span class="punctuation">:</span> <span class="string">&quot;commitlint -e -V&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;lint-staged&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;**/*.&#123;js,jsx,ts,tsx&#125;&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">      <span class="string">&quot;eslint --fix&quot;</span></span><br><span class="line">    <span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;**/*.&#123;md,json&#125;&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">      <span class="string">&quot;prettier --write&quot;</span></span><br><span class="line">    <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><h2 id="antd-开发"><a href="#antd-开发" class="headerlink" title="antd 开发"></a>antd 开发</h2><h3 id="安装依赖-1"><a href="#安装依赖-1" class="headerlink" title="安装依赖"></a>安装依赖</h3><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">yarn add antd</span><br></pre></td></tr></table></figure><h3 id="NextJS-使用-Less-编译-Antd"><a href="#NextJS-使用-Less-编译-Antd" class="headerlink" title="NextJS 使用 Less 编译 Antd"></a>NextJS 使用 Less 编译 Antd</h3><p>使用 yarn 安装 next-with-less 包，并顺带最新版本的 less 和 less-loader：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">yarn add next-with-less less less-loader -D</span><br></pre></td></tr></table></figure><p>并修改 <code>next.config.js</code> 配置文件：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// next.config.js</span></span><br><span class="line"><span class="keyword">const</span> withLess = <span class="built_in">require</span>(<span class="string">&quot;next-with-less&quot;</span>);</span><br><span class="line"></span><br><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">let</span> config = &#123;</span><br><span class="line">  <span class="attr">reactStrictMode</span>: <span class="literal">true</span></span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line">config = <span class="title function_">withLess</span>(&#123;</span><br><span class="line">  ...config,</span><br><span class="line">  <span class="attr">lessLoaderOptions</span>: &#123;</span><br><span class="line">    <span class="attr">lessOptions</span>: &#123;</span><br><span class="line">      <span class="attr">modifyVars</span>: &#123;</span><br><span class="line">        <span class="string">&quot;@primary-color&quot;</span>: <span class="string">&quot;#f74a49&quot;</span>,</span><br><span class="line">        <span class="string">&quot;@border-radius-base&quot;</span>: <span class="string">&quot;.5em&quot;</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"></span><br><span class="line"><span class="variable language_">module</span>.<span class="property">exports</span> = config</span><br></pre></td></tr></table></figure><h3 id="引入-antd-less"><a href="#引入-antd-less" class="headerlink" title="引入 antd less"></a>引入 antd less</h3><p>在 <code>pages/_app.tsx</code> 中引入 less</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="string">&#x27;antd/lib/style/themes/default.less&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> <span class="string">&#x27;antd/dist/antd.less&#x27;</span></span><br></pre></td></tr></table></figure><h3 id="使用-dayjs-替换-moment-js"><a href="#使用-dayjs-替换-moment-js" class="headerlink" title="使用 dayjs 替换 moment.js"></a>使用 dayjs 替换 moment.js</h3><p>按照 <a href="https://ant.design/docs/react/replace-moment-cn#%E8%87%AA%E5%AE%9A%E4%B9%89%E7%BB%84%E4%BB%B6">自定义组件</a> 的方式自定义组件在 Next.js 中是不够的，需要使用 next-transpile-modules 做进一步的处理：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// next.config.js</span></span><br><span class="line"><span class="keyword">const</span> withTM = <span class="built_in">require</span>(<span class="string">&#x27;next-transpile-modules&#x27;</span>)([</span><br><span class="line">  <span class="string">&#x27;rc-picker&#x27;</span>,</span><br><span class="line">  <span class="string">&#x27;rc-util&#x27;</span>,</span><br><span class="line">  <span class="string">&#x27;antd&#x27;</span>,</span><br><span class="line">  <span class="string">&#x27;rc-pagination&#x27;</span>,</span><br><span class="line">  <span class="string">&#x27;rc-notification&#x27;</span>,</span><br><span class="line">  <span class="string">&#x27;@ant-design/icons&#x27;</span>,</span><br><span class="line">]);</span><br><span class="line"></span><br><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">let</span> config = &#123;</span><br><span class="line">  <span class="attr">reactStrictMode</span>: <span class="literal">true</span></span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line">config = <span class="title function_">withTM</span>(config);</span><br><span class="line"></span><br><span class="line"><span class="variable language_">module</span>.<span class="property">exports</span> = config</span><br></pre></td></tr></table></figure><p>在 <code>_app.tsx</code> 中初始化 dayjs：</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> dayjs <span class="keyword">from</span> <span class="string">&#x27;dayjs&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 初始化 dayjs</span></span><br><span class="line"><span class="keyword">import</span> <span class="string">&#x27;dayjs/locale/zh-cn&#x27;</span>;</span><br><span class="line">dayjs.<span class="title function_">locale</span>(<span class="string">&#x27;zh-cn&#x27;</span>);</span><br></pre></td></tr></table></figure><h3 id="布局"><a href="#布局" class="headerlink" title="布局"></a>布局</h3><p>布局使用的是 antd 的 Layout，我选用的是侧边栏布局。</p><h4 id="Layout-tsx"><a href="#Layout-tsx" class="headerlink" title="Layout.tsx"></a>Layout.tsx</h4><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">Layout</span>, <span class="title class_">Menu</span>, <span class="title class_">Breadcrumb</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;antd&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> <span class="title class_">Sider</span> <span class="keyword">from</span> <span class="string">&#x27;./Sider&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> &#123; <span class="title class_">Header</span>, <span class="title class_">Content</span>, <span class="title class_">Footer</span> &#125; = <span class="title class_">Layout</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">AppLayout</span> = (<span class="params">&#123;children&#125;</span>) =&gt; &#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">Layout</span> <span class="attr">style</span>=<span class="string">&#123;&#123;</span> <span class="attr">minHeight:</span> &#x27;<span class="attr">100vh</span>&#x27; &#125;&#125;&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">Sider</span> /&gt;</span></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">Layout</span> <span class="attr">className</span>=<span class="string">&quot;site-layout&quot;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">          <span class="tag">&lt;<span class="name">Header</span> <span class="attr">style</span>=<span class="string">&#123;&#123;</span> <span class="attr">padding:</span> <span class="attr">0</span>, <span class="attr">backgroundColor:</span> &#x27;#<span class="attr">ffffff</span>&#x27; &#125;&#125; /&gt;</span></span></span><br><span class="line"><span class="language-xml">          <span class="tag">&lt;<span class="name">Content</span> <span class="attr">style</span>=<span class="string">&#123;&#123;</span> <span class="attr">margin:</span> &#x27;<span class="attr">0</span> <span class="attr">16px</span>&#x27; &#125;&#125;&gt;</span>&#123;children&#125;<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> <span class="attr">style</span>=<span class="string">&#123;&#123;</span> <span class="attr">textAlign:</span> &#x27;<span class="attr">center</span>&#x27; &#125;&#125;&gt;</span></span></span><br><span class="line"><span class="language-xml">            Crypto Meta ©2021 Powered by&#123;&#x27; &#x27;&#125;</span></span><br><span class="line"><span class="language-xml">            <span class="tag">&lt;<span class="name">a</span> <span class="attr">className</span>=<span class="string">&quot;default&quot;</span> <span class="attr">href</span>=<span class="string">&quot;https://nextjs.org/&quot;</span> <span class="attr">target</span>=<span class="string">&quot;_blank&quot;</span> <span class="attr">rel</span>=<span class="string">&quot;noreferrer&quot;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">              Next.js</span></span><br><span class="line"><span class="language-xml">            <span class="tag">&lt;/<span class="name">a</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">Layout</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">Layout</span>&gt;</span></span></span><br><span class="line">  )</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="Sider-tsx"><a href="#Sider-tsx" class="headerlink" title="Sider.tsx"></a>Sider.tsx</h4><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">Layout</span>, <span class="title class_">Menu</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;antd&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> <span class="title class_">React</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> &#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> <span class="title class_">Link</span> <span class="keyword">from</span> <span class="string">&#x27;next/link&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> menus <span class="keyword">from</span> <span class="string">&#x27;config/menus&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> &#123; <span class="title class_">Sider</span> &#125; = <span class="title class_">Layout</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">AppSider</span> = (<span class="params"></span>) =&gt; &#123;</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> [collapsed, setCollapsed] = <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_">onCollapse</span> = (<span class="params">collapsed: <span class="built_in">boolean</span></span>) =&gt; &#123;</span><br><span class="line">    <span class="title function_">setCollapsed</span>(collapsed);</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;&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">Sider</span> <span class="attr">breakpoint</span>=<span class="string">&quot;lg&quot;</span> <span class="attr">collapsible</span> <span class="attr">collapsed</span>=<span class="string">&#123;collapsed&#125;</span> <span class="attr">onCollapse</span>=<span class="string">&#123;onCollapse&#125;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">Menu</span> <span class="attr">theme</span>=<span class="string">&quot;dark&quot;</span> <span class="attr">mode</span>=<span class="string">&quot;inline&quot;</span> <span class="attr">selectedKeys</span>=<span class="string">&#123;[router.pathname]&#125;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">          &#123;menus.map(menu =&gt; (</span></span><br><span class="line"><span class="language-xml">            <span class="tag">&lt;<span class="name">Menu.Item</span> <span class="attr">key</span>=<span class="string">&#123;menu.route&#125;</span> <span class="attr">icon</span>=<span class="string">&#123;menu.icon&#125;</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">href</span>=<span class="string">&#123;menu.route&#125;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">                <span class="tag">&lt;<span class="name">a</span>&gt;</span>&#123;menu.name&#125;<span class="tag">&lt;/<span class="name">a</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">              <span class="tag">&lt;/<span class="name">Link</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">            <span class="tag">&lt;/<span class="name">Menu.Item</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">Menu</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;/<span class="name">Sider</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><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="title class_">AppSider</span>;</span><br></pre></td></tr></table></figure><h4 id="config-menus-tsx"><a href="#config-menus-tsx" class="headerlink" title="config&#x2F;menus.tsx"></a>config&#x2F;menus.tsx</h4><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> [</span><br><span class="line">  &#123;</span><br><span class="line">    <span class="attr">route</span>: <span class="string">&#x27;/cryptoyou&#x27;</span>,</span><br><span class="line">    <span class="attr">name</span>: <span class="string">&#x27;Crypto You&#x27;</span>,</span><br><span class="line">    <span class="attr">icon</span>: (</span><br><span class="line">      <span class="language-xml"><span class="tag">&lt;<span class="name">img</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">        <span class="attr">src</span>=<span class="string">&quot;https://cdn.jsdelivr.net/gh/youngjuning/images/202112081051602.png&quot;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">        <span class="attr">width</span>=<span class="string">&quot;30&quot;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">        <span class="attr">alt</span>=<span class="string">&quot;thecryptoyou&quot;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">      /&gt;</span></span></span><br><span class="line">    ),</span><br><span class="line">  &#125;,</span><br><span class="line">  &#123;</span><br><span class="line">    <span class="attr">route</span>: <span class="string">&#x27;/squid&#x27;</span>,</span><br><span class="line">    <span class="attr">name</span>: <span class="string">&#x27;Squid NFT&#x27;</span>,</span><br><span class="line">    <span class="attr">icon</span>: (</span><br><span class="line">      <span class="language-xml"><span class="tag">&lt;<span class="name">img</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">        <span class="attr">src</span>=<span class="string">&quot;https://cdn.jsdelivr.net/gh/youngjuning/images/202112082111091.png&quot;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">        <span class="attr">width</span>=<span class="string">&quot;30&quot;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">        <span class="attr">alt</span>=<span class="string">&quot;thecryptoyou&quot;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">      /&gt;</span></span></span><br><span class="line">    ),</span><br><span class="line">  &#125;,</span><br><span class="line">];</span><br></pre></td></tr></table></figure><h3 id="国际化"><a href="#国际化" class="headerlink" title="国际化"></a>国际化</h3><p>antd 提供了一个 React 组件 ConfigProvider 用于全局配置国际化文案。新建 <code>components/Layout/Provider.tsx</code>：</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">React</span> <span class="keyword">from</span> <span class="string">&#x27;react&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">ConfigProvider</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;antd&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> zhCN <span class="keyword">from</span> <span class="string">&#x27;antd/lib/locale/zh_CN&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">Provider</span> = (<span class="params">&#123; children &#125;</span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="language-xml"><span class="tag">&lt;<span class="name">ConfigProvider</span> <span class="attr">locale</span>=<span class="string">&#123;zhCN&#125;</span>&gt;</span>&#123;children&#125;<span class="tag">&lt;/<span class="name">ConfigProvider</span>&gt;</span></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="title class_">Provider</span>;</span><br></pre></td></tr></table></figure><p>然后在 <code>components/Layout/index.tsx</code> 中引入：</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">Provider</span> <span class="keyword">from</span> <span class="string">&#x27;./Provider&#x27;</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_">AppLayout</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">Provider</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">Provider</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>参考 <a href="https://stackoverflow.com/questions/64541235/breadcrumbs-and-nextjs">Breadcrumbs and NextJS</a> 封装了一个 Breadcrumb 组件：</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">React</span> <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_">Breadcrumbs</span> <span class="keyword">from</span> <span class="string">&#x27;nextjs-antd-breadcrumbs&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">Example</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="language-xml"><span class="tag">&lt;<span class="name">Breadcrumbs</span> <span class="attr">rootLabel</span>=<span class="string">&quot;Home&quot;</span> <span class="attr">omitRootLabel</span>=<span class="string">&#123;false&#125;/</span>&gt;</span></span>;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><h2 id="Next-js-开发"><a href="#Next-js-开发" class="headerlink" title="Next.js 开发"></a>Next.js 开发</h2><h3 id="基于根目录导入模块"><a href="#基于根目录导入模块" class="headerlink" title="基于根目录导入模块"></a>基于根目录导入模块</h3><p><code>baseUrl</code> 配置选项允许您直接从项目的根目录导入。</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// tsconfig.json or jsconfig.json</span></span><br><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;compilerOptions&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;baseUrl&quot;</span><span class="punctuation">:</span> <span class="string">&quot;.&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="模块路径别名"><a href="#模块路径别名" class="headerlink" title="模块路径别名"></a>模块路径别名</h3><p>在 tsconfig.json 中加入以下配置：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;compilerOptions&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;baseUrl&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;paths&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">      <span class="attr">&quot;@/components/*&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span><span class="string">&quot;./components/*&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>其他的地址依葫芦画瓢加到 paths 对象中即可。</p><h3 id="redirects-永久重定向"><a href="#redirects-永久重定向" class="headerlink" title="redirects 永久重定向"></a>redirects 永久重定向</h3><p>永久重定向不同于重写路由，它会在 url 中表现出来，在 Next.js 中重定向是在 <code>next.config.js</code> 中配置的：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> config = &#123;</span><br><span class="line">  <span class="keyword">async</span> <span class="title function_">redirects</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="keyword">return</span> [</span><br><span class="line">      &#123;</span><br><span class="line">        <span class="attr">source</span>: <span class="string">&#x27;/&#x27;</span>,</span><br><span class="line">        <span class="attr">destination</span>: <span class="string">&#x27;/cryptoyou&#x27;</span>,</span><br><span class="line">        <span class="attr">permanent</span>: <span class="literal">true</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><span class="line"></span><br><span class="line"><span class="variable language_">module</span>.<span class="property">exports</span> = config;</span><br></pre></td></tr></table></figure><h3 id="在-pages-目录包含非页面文件"><a href="#在-pages-目录包含非页面文件" class="headerlink" title="在 pages 目录包含非页面文件"></a>在 pages 目录包含非页面文件</h3><p>要把测试文件、生成的文件或其他组件使用的文件放在 <code>pages</code> 目录中，你可以在扩展名前加上类似 <code>page</code> 的字样。</p><p>打开 <code>next.config.js</code> 并添加 <code>pageExtensions</code> 配置：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="variable language_">module</span>.<span class="property">exports</span> = &#123;</span><br><span class="line">  <span class="attr">pageExtensions</span>: [<span class="string">&#x27;page.tsx&#x27;</span>, <span class="string">&#x27;page.ts&#x27;</span>, <span class="string">&#x27;page.jsx&#x27;</span>, <span class="string">&#x27;page.js&#x27;</span>],</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>然后重命名你的页面，使其有一个包括 <code>.page</code> 的文件扩展名（例如，重命名 <code>MyPage.tsx</code> 为<code>MyPage.page.tsx</code>）。</p><blockquote><p>注意：确保你也重命名 <code>_document.js</code>、<code>_app.js</code>、<code>_middleware.js</code>，以及 <code>pages/api/</code> 下的文件。</p></blockquote><h3 id="styled-jsx"><a href="#styled-jsx" class="headerlink" title="styled-jsx"></a>styled-jsx</h3><p>next.js 内置支持 styled-jsx，我们要做的是配置支持 sass，首先安装 <code>@styled-jsx/plugin-sass</code>：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">yarn add @styled-jsx/plugin-sass sass node-sass -D</span><br></pre></td></tr></table></figure><p>然后配置 <code>.babelrc</code>：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;presets&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">    <span class="punctuation">[</span></span><br><span class="line">      <span class="string">&quot;next/babel&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="punctuation">&#123;</span></span><br><span class="line">        <span class="attr">&quot;styled-jsx&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">          <span class="attr">&quot;plugins&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span><span class="string">&quot;@styled-jsx/plugin-sass&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">]</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><blockquote><p>注意：开启 babel 后，会自动禁用 swc，目前还没有 swc 的支持方案，进度请关注 <a href="https://github.com/Thream/styled-jsx-plugin-sass/issues/100">[Improvement] Next.JS 12 support + SWC</a></p></blockquote><h3 id="自定义错误页面"><a href="#自定义错误页面" class="headerlink" title="自定义错误页面"></a>自定义错误页面</h3><h4 id="404-tsx"><a href="#404-tsx" class="headerlink" title="404.tsx"></a>404.tsx</h4><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">Result</span>, <span class="title class_">Button</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;antd&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> <span class="title class_">Link</span> <span class="keyword">from</span> <span class="string">&#x27;next/link&#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_">Custom404</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">Result</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">      <span class="attr">status</span>=<span class="string">&quot;404&quot;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">      <span class="attr">title</span>=<span class="string">&quot;404&quot;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">      <span class="attr">subTitle</span>=<span class="string">&quot;Sorry, the page you visited does not exist.&quot;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">      <span class="attr">extra</span>=<span class="string">&#123;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">        &lt;<span class="attr">Button</span> <span class="attr">type</span>=<span class="string">&quot;primary&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">href</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">a</span>&gt;</span>返回首页<span class="tag">&lt;/<span class="name">a</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">          <span class="tag">&lt;/<span class="name">Link</span>&gt;</span></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">      &#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><h4 id="500-tsx"><a href="#500-tsx" class="headerlink" title="500.tsx"></a>500.tsx</h4><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">Result</span>, <span class="title class_">Button</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;antd&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> <span class="title class_">Link</span> <span class="keyword">from</span> <span class="string">&#x27;next/link&#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_">Custom500</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">Result</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">      <span class="attr">status</span>=<span class="string">&quot;500&quot;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">      <span class="attr">title</span>=<span class="string">&quot;500&quot;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">      <span class="attr">subTitle</span>=<span class="string">&quot;Sorry, something went wrong.&quot;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">      <span class="attr">extra</span>=<span class="string">&#123;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">        &lt;<span class="attr">Button</span> <span class="attr">type</span>=<span class="string">&quot;primary&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">href</span>=<span class="string">&quot;/&quot;</span>&gt;</span>返回首页<span class="tag">&lt;/<span class="name">Link</span>&gt;</span></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">      &#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><h2 id="Dapp"><a href="#Dapp" class="headerlink" title="Dapp"></a>Dapp</h2><h3 id="小狐狸钱包"><a href="#小狐狸钱包" class="headerlink" title="小狐狸钱包"></a>小狐狸钱包</h3><ol><li><a href="https://zhuanlan.zhihu.com/p/368736357">小狐狸钱包MetaMask新手使用教程</a></li><li><a href="https://blog.csdn.net/qq_33583069/article/details/115727642?ivk_sa=1024320u">使用MetaMask连接到币安智能链(BSC)</a></li></ol><h3 id="usedapp-core"><a href="#usedapp-core" class="headerlink" title="@usedapp&#x2F;core"></a>@usedapp&#x2F;core</h3><h4 id="安装依赖-2"><a href="#安装依赖-2" class="headerlink" title="安装依赖"></a>安装依赖</h4><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ yarn add @usedapp/core</span><br></pre></td></tr></table></figure><h4 id="设置-Provider"><a href="#设置-Provider" class="headerlink" title="设置 Provider"></a>设置 Provider</h4><p>你需要做的第一件事是用可选的配置设置 DAppProvider，并将你的整个应用程序包裹在其中。本文中我们编辑 <code>components/Layout/Provider.tsx</code>：</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">React</span> <span class="keyword">from</span> <span class="string">&#x27;react&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">DAppProvider</span>, <span class="title class_">Config</span>, <span class="title class_">ChainId</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;@usedapp/core&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="attr">config</span>: <span class="title class_">Config</span> = &#123;</span><br><span class="line">  <span class="attr">readOnlyChainId</span>: <span class="title class_">ChainId</span>.<span class="property">BSC</span>,</span><br><span class="line">  <span class="attr">readOnlyUrls</span>: &#123;</span><br><span class="line">    [<span class="title class_">ChainId</span>.<span class="property">BSC</span>]: <span class="string">&#x27;https://bsc-dataseed1.binance.org/&#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">const</span> <span class="title function_">Provider</span> = (<span class="params">&#123; children &#125;</span>) =&gt; &#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">DAppProvider</span> <span class="attr">config</span>=<span class="string">&#123;config&#125;</span>&gt;</span>&#123;children&#125;<span class="tag">&lt;/<span class="name">DAppProvider</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 class_">Provider</span>;</span><br></pre></td></tr></table></figure><h4 id="连接钱包"><a href="#连接钱包" class="headerlink" title="连接钱包"></a>连接钱包</h4><p>然后你需要使用 activateBrowserWallet 来激活 provider。最好是在用户点击 “Connect” 按钮时进行。本文中我们新建组件 <code>components/ConnectButton.tsx</code>：</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">React</span> <span class="keyword">from</span> <span class="string">&#x27;react&#x27;</span>;</span><br><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;antd&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; useEthers &#125; <span class="keyword">from</span> <span class="string">&#x27;@usedapp/core&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">LoginOutlined</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;@ant-design/icons&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">ConnectButton</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> &#123; activateBrowserWallet &#125; = <span class="title function_">useEthers</span>();</span><br><span class="line"></span><br><span class="line">  <span class="keyword">function</span> <span class="title function_">handleConnectWallet</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="title function_">activateBrowserWallet</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">Button</span> <span class="attr">size</span>=<span class="string">&quot;large&quot;</span> <span class="attr">type</span>=<span class="string">&quot;primary&quot;</span> <span class="attr">icon</span>=<span class="string">&#123;</span>&lt;<span class="attr">LoginOutlined</span> /&gt;</span>&#125; onClick=&#123;handleConnectWallet&#125;&gt;</span></span><br><span class="line"><span class="language-xml">      Connect MetaMask</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">export</span> <span class="keyword">default</span> <span class="title class_">ConnectButton</span>;</span><br></pre></td></tr></table></figure><h4 id="钱包余额"><a href="#钱包余额" class="headerlink" title="钱包余额"></a>钱包余额</h4><p><code>useEtherBalance(address: string)</code></p><p>提供一种获取账户余额的方法。以账户地址为参数，当数据不可用时（即未连接），返回 ·<code>BigNumber</code> 或 <code>undefined</code>。要获得当前连接的账户，请使用 <code>useEthers()</code>。</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; formatEther &#125; <span class="keyword">from</span> <span class="string">&#x27;@ethersproject/units&#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_">EtherBalance</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> &#123; account &#125; = <span class="title function_">useEthers</span>()</span><br><span class="line">  <span class="keyword">const</span> etherBalance = <span class="title function_">useEtherBalance</span>(account)</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;etherBalance &amp;&amp; <span class="tag">&lt;<span class="name">p</span>&gt;</span>Balance: &#123;formatEther(etherBalance)&#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></pre></td></tr></table></figure><h4 id="代币余额"><a href="#代币余额" class="headerlink" title="代币余额"></a>代币余额</h4><p><code>useTokenBalance(address: string, tokenAddress: string)</code></p><p>提供一种方法来获取由 <code>tokenAddress</code> 指定的 ERC20 代币在所提供地址的余额。当数据不可用时，返回 <code>BigNumber</code> 或 <code>undefined</code>。</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; formatUnits &#125; <span class="keyword">from</span> <span class="string">&#x27;@ethersproject/units&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="variable constant_">BABY</span> = <span class="string">&#x27;0x53e562b9b7e5e94b81f10e96ee70ad06df3d2657&#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_">TokenBalance</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> &#123; account &#125; = <span class="title function_">useEthers</span>()</span><br><span class="line">  <span class="keyword">const</span> tokenBalance = <span class="title function_">useTokenBalance</span>(<span class="variable constant_">BABY</span>, account)</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;tokenBalance &amp;&amp; <span class="tag">&lt;<span class="name">p</span>&gt;</span>BABY: &#123;formatUnits(tokenBalance, 18)&#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></pre></td></tr></table></figure><h4 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h4><ul><li><a href="https://dev.to/jacobedawson/build-a-web3-dapp-in-react-login-with-metamask-4chp">Build a Web3 Dapp in React &amp; Login with MetaMask</a></li></ul><h2 id="部署"><a href="#部署" class="headerlink" title="部署"></a>部署</h2><h3 id="GitHub-Pages"><a href="#GitHub-Pages" class="headerlink" title="GitHub Pages"></a>GitHub Pages</h3><p>如果是 vercel 付费用户，推荐使用 vercel。GitHub Pages 只能部署静态内容，所以需要使用 <code>next export</code> 将静态内容导出部署。</p><p>首先配置 npm scripts：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><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;preexport&quot;</span><span class="punctuation">:</span> <span class="string">&quot;yarn build&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;export&quot;</span><span class="punctuation">:</span> <span class="string">&quot;next export&quot;</span></span><br><span class="line"><span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br></pre></td></tr></table></figure><p>然后添加 <code>.github/workflows/gh-pages.yml</code>：</p><figure class="highlight yml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">name:</span> <span class="string">github</span> <span class="string">pages</span></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><br><span class="line">      <span class="bullet">-</span> <span class="string">main</span> <span class="comment"># default branch</span></span><br><span class="line"><span class="attr">jobs:</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-18.04</span></span><br><span class="line">    <span class="attr">steps:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">uses:</span> <span class="string">actions/checkout@v2</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">uses:</span> <span class="string">c-hive/gha-yarn-cache@v2</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Install</span> <span class="string">Dependencies</span></span><br><span class="line">        <span class="attr">run:</span> <span class="string">yarn</span> <span class="string">--non-interactive</span> <span class="string">--silent</span> <span class="string">--ignore-scripts</span> <span class="string">--production=false</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Build</span> <span class="string">Website</span></span><br><span class="line">        <span class="attr">run:</span> <span class="string">yarn</span> <span class="string">export</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Deploy</span> <span class="string">Website</span></span><br><span class="line">        <span class="attr">uses:</span> <span class="string">peaceiris/actions-gh-pages@v3</span></span><br><span class="line">        <span class="attr">with:</span></span><br><span class="line">          <span class="attr">personal_token:</span> <span class="string">$&#123;&#123;</span> <span class="string">secrets.PERSONAL_TOKEN</span> <span class="string">&#125;&#125;</span></span><br><span class="line">          <span class="attr">external_repository:</span> <span class="string">crypto-meta/crypto-meta.github.io</span></span><br><span class="line">          <span class="attr">publish_dir:</span> <span class="string">./out</span></span><br><span class="line"></span><br></pre></td></tr></table></figure>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/cba1320bcd71/</id>
    <link href="https://blog.zisheng.pro/posts/cba1320bcd71/"/>
    <published>2021-12-05T14:58:44.000Z</published>
    <summary>本文记录了基于 Next.js 开发 BSC DApp</summary>
    <title>基于 Next.js 开发 BSC DApp</title>
    <updated>2026-09-18T08:15:28.636Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="微前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/%E5%BE%AE%E5%89%8D%E7%AB%AF/"/>
    <category term="微前端" scheme="https://blog.zisheng.pro/tags/%E5%BE%AE%E5%89%8D%E7%AB%AF/"/>
    <category term="single-spa" scheme="https://blog.zisheng.pro/tags/single-spa/"/>
    <category term="single-spa 原理解析" scheme="https://blog.zisheng.pro/tags/single-spa-%E5%8E%9F%E7%90%86%E8%A7%A3%E6%9E%90/"/>
    <category term="源码解析" scheme="https://blog.zisheng.pro/tags/%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90/"/>
    <content>
      <![CDATA[<h2 id="single-spa-是什么？"><a href="#single-spa-是什么？" class="headerlink" title="single-spa 是什么？"></a>single-spa 是什么？</h2><p>Single-spa 是一个将多个单页面应用聚合为一个整体应用的 JavaScript 微前端框架。 使用 single-spa 进行前端架构设计可以带来很多好处，例如:</p><ul><li>在同一页面上使用多个前端框架而不用刷新页面 (React, AngularJS, Angular, Ember, 你正在使用的框架)</li><li>独立部署每一个单页面应用</li><li>新功能使用新框架，旧的单页应用不用重写可以共存</li><li>改善初始加载时间，延迟加载代码</li></ul><p>single-spa 可以说是微前端的鼻祖了，市面上纷杂的解决方案多少都受到了它的影响，以下两款流行的生产级框架则更是基于 single-spa 开发的。</p><ul><li><a href="https://qiankun.umijs.org/zh/guide">qiankun</a>：蚂蚁金服出品的易用的微前端框架。</li><li><a href="https://github.com/namecheap/ilc">Isomorphic Layout Composer（ILC）</a>：一个将微前端组成部分支持 SSR 的完整的解决方案。</li></ul><h2 id="single-spa-架构"><a href="#single-spa-架构" class="headerlink" title="single-spa 架构"></a>single-spa 架构</h2><p>single-spa 借鉴了组件生命周期的思想，它为应用设置了针对路由的生命周期。当应用匹配路由&#x2F;处于激活状态时，应用会把自身的内容挂载到页面上；反之则卸载。典型的 single-spa 由 html 页面、应用注册脚本、应用脚本自身构成。应用注册内容包含：</p><ul><li>appName：应用名</li><li>loadingFunction：加载应用程序的代码。</li><li>activityFunction 函数：确定应用程序何时处于活动状态&#x2F;非活动状态。</li><li>customProps：自定义 props，可以不填</li></ul><p>single-spa 又约定应用脚本包含以下生命周期：</p><ul><li><code>load</code>：当应用匹配路由时就会加载脚本（非函数，只是一种状态）</li><li><code>bootstrap</code>：引导函数（对接 html，应用内容首次挂载到页面前调用）</li><li><code>mount</code>：挂载函数</li><li><code>unmount</code>：卸载函数（须移除事件绑定等内容）</li><li><code>unload</code>：非必要（unload 之后会重新启动 bootstrap 流程；借助 unload 可实现热更新）。</li></ul><p>生命周期函数获得参数包含 name（应用名）、singleSpa（实例）、mountParcel（手动挂载函数）、customProps（自定义信息），生命周期函数必须返回 Promise 或其本身为 async 函数，<code>bootstrap</code>、mount、unmount 生命周期函数不可缺省，生命周期函数可以指定多个，它们会构成异步调用链，逐个调用。简要流程图如下：</p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images/202111221452146.png" alt="紫升"></p><h2 id="示例"><a href="#示例" class="headerlink" title="示例"></a>示例</h2><p>源码参考：<a href="https://github.com/youngjuning/simple-single-spa-webpack-example">youngjuning&#x2F;simple-single-spa-webpack-example</a></p><h3 id="1-创建一个-html-文件"><a href="#1-创建一个-html-文件" class="headerlink" title="1.创建一个 html 文件"></a>1.创建一个 html 文件</h3><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">html</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">body</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">script</span> <span class="attr">src</span>=<span class="string">&quot;single-spa-config.js&quot;</span>&gt;</span><span class="tag">&lt;/<span class="name">script</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">body</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">html</span>&gt;</span></span><br></pre></td></tr></table></figure><h3 id="2-创建一个-single-spa-config。"><a href="#2-创建一个-single-spa-config。" class="headerlink" title="2.创建一个 single-spa-config。"></a>2.创建一个 single-spa-config。</h3><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> * <span class="keyword">as</span> singleSpa <span class="keyword">from</span> <span class="string">&#x27;single-spa&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">/* loading 是一个返回 promise 的函数，用于 加载/解析 应用代码。</span></span><br><span class="line"><span class="comment"> * 它的目的是为延迟加载提供便利 —— single-spa 只有在需要时才会下载应用程序的代码。</span></span><br><span class="line"><span class="comment"> * 在这个示例中，在 webpack 中支持 import ()并返回 Promise，但是 single-spa 可以使用任何返回 Promise 的加载函数。</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line">singleSpa.<span class="title function_">registerApplication</span>(<span class="string">&#x27;app-1&#x27;</span>, <span class="function">() =&gt;</span></span><br><span class="line">  <span class="keyword">import</span> (<span class="string">&#x27;../app1/app1.js&#x27;</span>), <span class="title function_">pathPrefix</span>(<span class="string">&#x27;/app1&#x27;</span>));</span><br><span class="line">singleSpa.<span class="title function_">registerApplication</span>(<span class="string">&#x27;app-2&#x27;</span>, <span class="function">() =&gt;</span></span><br><span class="line">  <span class="keyword">import</span> (<span class="string">&#x27;../app2/app2.js&#x27;</span>), <span class="title function_">pathPrefix</span>(<span class="string">&#x27;/app2&#x27;</span>));</span><br><span class="line"></span><br><span class="line">singleSpa.<span class="title function_">start</span>();</span><br><span class="line"></span><br><span class="line"><span class="comment">/* Single-spa 配置顶级路由，以确定哪个应用程序对于指定 url 是活动的。</span></span><br><span class="line"><span class="comment"> * 你可以以任何你喜欢的方式实现此路由。</span></span><br><span class="line"><span class="comment"> * 一种有用的约定是在 url 前面加上活动应用程序的名称，以使顶层路由保持简单。</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">pathPrefix</span>(<span class="params">prefix</span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="keyword">function</span>(<span class="params">location</span>) &#123;</span><br><span class="line">    <span class="keyword">return</span> location.<span class="property">pathname</span>.<span class="title function_">startsWith</span>(<span class="string">`<span class="subst">$&#123;prefix&#125;</span>`</span>);</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="3-创建一个应用程序。"><a href="#3-创建一个应用程序。" class="headerlink" title="3.创建一个应用程序。"></a>3.创建一个应用程序。</h3><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">React</span> <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_">ReactDOM</span> <span class="keyword">from</span> <span class="string">&#x27;react-dom&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> singleSpaReact <span class="keyword">from</span> <span class="string">&#x27;single-spa-react&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> <span class="title class_">Root</span> <span class="keyword">from</span> <span class="string">&#x27;./root.component.tsx&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> reactLifecycles = <span class="title function_">singleSpaReact</span>(&#123;</span><br><span class="line">  <span class="title class_">React</span>,</span><br><span class="line">  <span class="title class_">ReactDOM</span>,</span><br><span class="line">  <span class="attr">rootComponent</span>: <span class="title class_">Root</span>,</span><br><span class="line">  domElementGetter,</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_">bootstrap</span>(<span class="params">props</span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> reactLifecycles.<span class="title function_">bootstrap</span>(props);</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_">mount</span>(<span class="params">props</span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> reactLifecycles.<span class="title function_">mount</span>(props);</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_">unmount</span>(<span class="params">props</span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> reactLifecycles.<span class="title function_">unmount</span>(props);</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_">domElementGetter</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="comment">// 确保这里有一个 div 供渲染用</span></span><br><span class="line">  <span class="keyword">let</span> el = <span class="variable language_">document</span>.<span class="title function_">getElementById</span>(<span class="string">&#x27;app1&#x27;</span>);</span><br><span class="line">  <span class="keyword">if</span> (!el) &#123;</span><br><span class="line">    el = <span class="variable language_">document</span>.<span class="title function_">createElement</span>(<span class="string">&#x27;div&#x27;</span>);</span><br><span class="line">    el.<span class="property">id</span> = <span class="string">&#x27;app1&#x27;</span>;</span><br><span class="line">    <span class="variable language_">document</span>.<span class="property">body</span>.<span class="title function_">appendChild</span>(el);</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> el;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="源码解析"><a href="#源码解析" class="headerlink" title="源码解析"></a>源码解析</h2><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images/202111221420019.png" alt="紫升"></p><p>其中 avigation、lifecycles、applications 这三个文件夹即可，是整个源码的核心。</p><h3 id="核心方法分析"><a href="#核心方法分析" class="headerlink" title="核心方法分析"></a>核心方法分析</h3><p>绿底白字则是 single-spa 的三个核心方法 <a href="http://tny.im/T5UaE">registerApplication</a>、<a href="http://tny.im/tsub">reroute</a> 和 <a href="http://tny.im/9CvCH">start</a>，脑图描述了这三个方法具体做了什么：</p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images/202111221417558.png" alt="紫升"></p><h3 id="初始化加载流程"><a href="#初始化加载流程" class="headerlink" title="初始化加载流程"></a>初始化加载流程</h3><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images/202111221423159.png" alt="紫升"></p><h3 id="应用启动和挂载流程"><a href="#应用启动和挂载流程" class="headerlink" title="应用启动和挂载流程"></a>应用启动和挂载流程</h3><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images/202111221423521.png" alt="紫升"></p><h2 id="经验"><a href="#经验" class="headerlink" title="经验"></a>经验</h2><ol><li>加载操作一般都是异步的，比如加载插件、加载应用。所以原则上只要会使用 Promise，我们就能很快模仿一个自己的微前端架构出来。</li><li>本次分享没有涉及微前端的另个一个重要概念沙箱，有机会单独分享。</li><li>技术发展都是有迹可循的，搞清发展脉络对于理解应用一个技术也很有帮助。</li></ol><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ul><li><a href="https://juejin.cn/post/6941402712890638367">微前端框架single-spa源码解析、项目实战、手写简版single-spa</a></li><li><a href="https://juejin.cn/post/6862661545592111111#heading-43">微前端框架 之 single-spa 从入门到精通</a></li><li><a href="https://zhuanlan.zhihu.com/p/107059106">single-spa 实现前端微服务</a></li></ul>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/89f69cf72fa9/</id>
    <link href="https://blog.zisheng.pro/posts/89f69cf72fa9/"/>
    <published>2021-11-21T09:00:00.000Z</published>
    <summary>Single-spa 是一个将多个单页面应用聚合为一个整体应用的 JavaScript 微前端框架。 使用 single-spa 进行前端架构设计可以带来很多好处</summary>
    <title>single-spa 原理解析</title>
    <updated>2026-09-18T08:15:28.632Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="VS Code" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/VS-Code/"/>
    <category term="VS Code" scheme="https://blog.zisheng.pro/tags/VS-Code/"/>
    <category term="vscode 插件开发" scheme="https://blog.zisheng.pro/tags/vscode-%E6%8F%92%E4%BB%B6%E5%BC%80%E5%8F%91/"/>
    <category term="vscode 插件发布" scheme="https://blog.zisheng.pro/tags/vscode-%E6%8F%92%E4%BB%B6%E5%8F%91%E5%B8%83/"/>
    <category term="vscode 发布插件" scheme="https://blog.zisheng.pro/tags/vscode-%E5%8F%91%E5%B8%83%E6%8F%92%E4%BB%B6/"/>
    <category term="vscode 插件市场" scheme="https://blog.zisheng.pro/tags/vscode-%E6%8F%92%E4%BB%B6%E5%B8%82%E5%9C%BA/"/>
    <category term="vsce" scheme="https://blog.zisheng.pro/tags/vsce/"/>
    <category term="Visual Studio Code" scheme="https://blog.zisheng.pro/tags/Visual-Studio-Code/"/>
    <category term="access token" scheme="https://blog.zisheng.pro/tags/access-token/"/>
    <category term="personal token" scheme="https://blog.zisheng.pro/tags/personal-token/"/>
    <content>
      <![CDATA[<h2 id="安装发布工具-vsce"><a href="#安装发布工具-vsce" class="headerlink" title="安装发布工具 vsce"></a>安装发布工具 vsce</h2><p><a href="https://github.com/Microsoft/vsce">vsce</a> 是一个用于将插件发布到<a href="https://marketplace.visualstudio.com/">vscode 插件市场</a>上的命令行工具。</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ npm install vsce -g</span><br></pre></td></tr></table></figure><h2 id="注册-vscode-开发者信息"><a href="#注册-vscode-开发者信息" class="headerlink" title="注册 vscode 开发者信息"></a>注册 vscode 开发者信息</h2><h3 id="新建-Azure-DevPos-组织"><a href="#新建-Azure-DevPos-组织" class="headerlink" title="新建 Azure DevPos 组织"></a>新建 Azure DevPos 组织</h3><ol><li>登录 <a href="https://azure.microsoft.com/zh-cn/services/devops/">Azure DevOps</a>。</li><li>点击 <strong>New organization</strong>。</li></ol><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images/20210812201149.png" alt="vscode 新建 Azure DevPos 组织"></p><ol start="3"><li>确认信息，然后选择 <strong>Continue</strong>。</li></ol><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images/20210812201210.png" alt="vscode 新建 Azure DevPos 组织"></p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images/20210812201223.png" alt="vscode 新建 Azure DevPos 组织"></p><p>到这里，一个组织就创建成功了，之后随时登录到你的组织<a href="https://dev.azure.com/tuyaworks">https://dev.azure.com/tuyaworks</a>。</p><h3 id="获取-Personal-Access-Token"><a href="#获取-Personal-Access-Token" class="headerlink" title="获取 Personal Access Token"></a>获取 Personal Access Token</h3><p>下面的例子里，我们假设组织名为 vscode，从你的组织主页（例如：<a href="https://dev.azure.com/tuyaworks">https://dev.azure.com/tuyaworks</a> ） 进入 Personal access tokens 页面：</p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images/20210812201259.png" alt="vscode 获取 Personal Access Token"></p><p>点击 <strong>New Token</strong> 创建一个新的 Personal Access Token：</p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images/20210812202141.png" alt="vscode 获取 Personal Access Token"></p><p>给 Personal Access Token 添加描述，过期时间等等，你最好把过期时间设置为 1 年，这样你接下就能方便很多，选择 <strong>custom defined（用户自定义）</strong> 范围，然后点击 <strong>Show all scopes(显示全部)</strong> ，在这个列表中找到 Marketplace，并勾选 Acquire 和 Manage：</p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images/20210812202337.png" alt="vscode 获取 Personal Access Token"></p><p>点击 <strong>Create</strong>，你就会看到新创建的 Personal Access Token 了，复制好，你接下来就会用到这个 token 来创建一个发行方了。</p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images/20210812202402.png" alt="vscode 获取 Personal Access Token"></p><h3 id="创建一个发行方"><a href="#创建一个发行方" class="headerlink" title="创建一个发行方"></a>创建一个发行方</h3><p><strong>发行方</strong>是 VS Code 市场有权发布插件的唯一标识，每个插件的 package.json 文件都包含着 publisher 字段。<br>现在我们已经有了 Personal Access Token，我们马上可以通过 <a href="https://aka.ms/vscode-create-publisher">vscode-create-publisher</a> 创建一个发行方。</p><h3 id="发行方登录"><a href="#发行方登录" class="headerlink" title="发行方登录"></a>发行方登录</h3><p>如果你已经有发行方账号了：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ vsce login (publisher name)</span><br></pre></td></tr></table></figure><p><code>vsce</code> 会记住这个 Personal Access Token，日后你需要再次使用的时候会自动带上。</p><p>你也可以用命令参数 <code>-p &lt;token&gt;</code> 直接登录然后立即发布插件：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ vsce publish -p &lt;token&gt;</span><br></pre></td></tr></table></figure><h2 id="vscode-插件版本维护"><a href="#vscode-插件版本维护" class="headerlink" title="vscode 插件版本维护"></a>vscode 插件版本维护</h2><h3 id="增量更新插件版本"><a href="#增量更新插件版本" class="headerlink" title="增量更新插件版本"></a>增量更新插件版本</h3><p>用 SemVer 语义标识符：major，minor，patch 增量更新插件版本号。</p><p>例如，你想把插件从 1.0.0 更新到 1.1.0，那么加上 minor：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ vsce publish minor</span><br></pre></td></tr></table></figure><p>插件 <em>package.json</em> 的 version 会先更新，然后才发布插件。</p><p>你也可以通过命令行指定版本号：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ vsce publish 2.0.1</span><br></pre></td></tr></table></figure><h3 id="下架插件"><a href="#下架插件" class="headerlink" title="下架插件"></a>下架插件</h3><p>通过指定插件 idpublisher.extension 下架插件：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ vsce unpublish (publisher name).(extension name)</span><br></pre></td></tr></table></figure><blockquote><p>当你下架插件的时候，市场会移除所有插件的历史统计数据，请在下架前再三考虑，最好还是更新插件吧。</p></blockquote><h3 id="VS-Code-版本兼容性"><a href="#VS-Code-版本兼容性" class="headerlink" title="VS Code 版本兼容性"></a>VS Code 版本兼容性</h3><p>当你制作插件的时候，你需要描述插件对 VS Code 的版本兼容性——修改 <em>package.json</em> 中的 engines.vscode：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;engines&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;vscode&quot;</span><span class="punctuation">:</span> <span class="string">&quot;^1.8.0&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>1.8.0 表示你的插件只能兼容 1.8.0 版本的 VS Code，^1.8.0 则表示你的插件向上兼容，包括 1.8.1, 1.9.0 等等。</p><p>使用 <code>engines.vscode</code> 可以确保插件安装环境包含了插件依赖的 API。这个机制在稳定版和 Insider 版本都适用。</p><p>现在我们假设最新的稳定版 API 是 1.8.0，而 1.9.0 引入了新的 API，所以你可以用 1.9.0-insider 标识插件在 Insider 版中也可正常使用。 如果你想使用这些刚刚引入的 API，则将依赖版本设置为 ^1.9.0，你的插件则只能安装在 &gt;&#x3D;1.9.0 的 VS Code 上，也就意味着所有当前的 Insider 版本都可以用得上，而稳定版只有在更新到 1.9.0 才能使用你的插件。</p><h2 id="vscode-插件打包"><a href="#vscode-插件打包" class="headerlink" title="vscode 插件打包"></a>vscode 插件打包</h2><p>你也可能只是想打包一下插件，而不是发布到商店里。用下列命令将插件打包到 <code>.vsix</code> 文件中：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ vsce package</span><br></pre></td></tr></table></figure><p>这个命令会在当前目录生成一个 .vsix 文件，直接从 .vsix 安装插件是允许的，查看<a href="https://github.com/Microsoft/vscode-docs/blob/master/docs/editor/extension-gallery.md#install-from-a-vsix">从 VSIX 安装插件</a>了解更多内容。</p><h2 id="相关链接"><a href="#相关链接" class="headerlink" title="相关链接"></a>相关链接</h2><ul><li><a href="https://vscode-api-cn.js.org/">VS Code API 中文文档</a></li><li><a href="https://marketplace.visualstudio.com/">vscode 插件市场</a></li><li><a href="https://azure.microsoft.com/zh-cn/services/devops/">Azure Devops</a></li></ul>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/4b349879ced6/</id>
    <link href="https://blog.zisheng.pro/posts/4b349879ced6/"/>
    <published>2021-08-12T12:46:38.000Z</published>
    <summary>VsCode 插件开发的最后一步也是最繁琐的就是插件发布，本文详细介绍了如何使用 vsce 将 vscode 插件发布到扩展市场。</summary>
    <title>2023 保姆级 VsCode 插件发布教程</title>
    <updated>2026-09-18T08:15:28.636Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="React Native" scheme="https://blog.zisheng.pro/tags/React-Native/"/>
    <category term="前端工程化" scheme="https://blog.zisheng.pro/tags/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/"/>
    <category term="掘金" scheme="https://blog.zisheng.pro/tags/%E6%8E%98%E9%87%91/"/>
    <category term="前端组件化" scheme="https://blog.zisheng.pro/tags/%E5%89%8D%E7%AB%AF%E7%BB%84%E4%BB%B6%E5%8C%96/"/>
    <category term="前端架构" scheme="https://blog.zisheng.pro/tags/%E5%89%8D%E7%AB%AF%E6%9E%B6%E6%9E%84/"/>
    <content>
      <![CDATA[<blockquote><p>本文为掘金社区首发签约文章，未获授权禁止转载</p></blockquote><p>紫升有一个朋友小黑最近在面试时被问到如何设计一个前端组件库。没啥经验的小黑回答了业务提取封装成库以及基于 antd 结合业务二次封装。最后小黑被 HR 以灵力不够挂掉了。其实这个问题考察的并不是假大空的概念，而是有关开发者仓库管理、组件设计、单元测试、持续集成、协作管理等等能力。那么为了赋能小黑完美回答这个问题呢，我决定带领小黑一步一步建设一个 React Native 组件库。</p><p>这是一篇干货比较多的组件库搭建实战教程，不仅有通用的代码规范、提交规范、文档维护、单元测试、GitHub Action 配置的讲解，还涉及基于 lerna 的多包管理架构、React Native 图标库建设、React Native 组件库开发调试、按需加载原理及实现。工程化的思想是通用的，所以无论是你用的框架是什么，本文都值得一读。</p><blockquote><p>如果电脑前的掘友也对组件库开发感兴趣，不妨先给个点赞，再持续关注紫升和小黑的组件库开发之旅。PS：配合<a href="https://github.com/youngjuning/vant-react-native">仓库</a>和<a href="https://vant-react-native.js.org/">组件库文档</a>阅读本文效果更佳喲！</p></blockquote><h2 id="站在-Vant-Design-的肩膀上"><a href="#站在-Vant-Design-的肩膀上" class="headerlink" title="站在 Vant Design 的肩膀上"></a>站在 Vant Design 的肩膀上</h2><p>维护开发一个组件库无疑是需要投入很多时间和精力的，Flag 立了倒，倒了又立。可谓万事开头难，首先我们要有自知之明，在没有设计师和业余开发的情况下，我选择了给现有 UI Design 实现 React Native 版本的方式开启组件库开发之旅。在调研了 <a href="https://github.com/youzan/vant">vant</a>、<a href="https://github.com/NSFI/fishd-mobile/">fishd-mobile</a> 和 <a href="https://github.com/ant-design/ant-design-mobile">antd-mobile</a> 后我选择了 vant。这是几个仓库的现状对比：</p><table><thead><tr><th>组件库</th><th>团队</th><th>Github Star</th><th>Npm 周下载量</th><th>维护度</th></tr></thead><tbody><tr><td>vant</td><td>有赞</td><td>17.7K</td><td>27,789</td><td>维高度高，流行度也高</td></tr><tr><td>antd-mobile</td><td>Ant Design Team</td><td>8.9K</td><td>31,470</td><td>几乎不维护，据说蚂蚁内部也不用了</td></tr><tr><td>fishd-mobile</td><td>网易云商前端</td><td>29</td><td>22</td><td>看起来是个 KPI 项目无疑了</td></tr></tbody></table><p>确定了旅程的方向，就是给我们的组件库起一个合适的名字和口号，用前端工程师的方式表述就是 <code>package.json</code> 的 <code>name</code> 和 <code>description</code> 字段：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// package.json</span></span><br><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;vant-react-native&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;Lightweight React Native UI Components inspired on Vant&quot;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><blockquote><p>由于我们的组件库定位是 vant 的 RN 版，参照 lottie-react-native、styled-react-native、jpush-react-native 的命名方式我们将组件库命名为 vant-react-native，同时也是希望组件库完成时能获得 vant 官方的支持。</p></blockquote><h2 id="基于-Lerna-的多包管理架构"><a href="#基于-Lerna-的多包管理架构" class="headerlink" title="基于 Lerna 的多包管理架构"></a>基于 Lerna 的多包管理架构</h2><p>Lerna 是一个管理工具，用于管理包含多个软件包（package）的 JavaScript 项目。由 Lerna 管理的仓库我们一般称之为单体仓库（monorepo）。基于 Lerna 的多包管理架构的优点在于：</p><ul><li>组件级别解耦，独立版本控制，每个组件都有版本记录可追溯</li><li>组件单独发布，支持灰度、版本回滚以及平滑升降级</li><li>按需引用，用户安装具体某个组件包，无需配置即可实现按需加载的效果。</li><li>关注点分离，降低大型复杂度、组件之间依赖清晰且可控制</li><li>单一职责原则，降低开源基友的参与和贡献难度</li></ul><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">.</span><br><span class="line">└── packages</span><br><span class="line">    ├── button <span class="comment"># @vant-react-native/button</span></span><br><span class="line">    └── icons <span class="comment"># @vant-react-native/icon</span></span><br></pre></td></tr></table></figure><h3 id="初始化-lerna-项目"><a href="#初始化-lerna-项目" class="headerlink" title="初始化 lerna 项目"></a>初始化 lerna 项目</h3><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ <span class="built_in">mkdir</span> vant-react-native &amp;&amp; lerna init --independent</span><br></pre></td></tr></table></figure><h3 id="yarn-workspaces"><a href="#yarn-workspaces" class="headerlink" title="yarn workspaces"></a>yarn workspaces</h3><p>使用 <a href="https://yarnpkg.com/lang/zh-Hans/docs/workspaces/">yarn workspaces</a> 结合 Lerna <code>useWorkspaces</code> 可以实现 <a href="https://github.com/lerna/lerna/blob/main/doc/hoist.md">Lerna Hoisting</a>。这并不是多此一举，这可以让你在统一的地方（根目录）管理依赖，这即节省时间又节省空间。</p><p>配置 <code>lerna.json</code>:</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  ...</span><br><span class="line">  <span class="attr">&quot;npmClient&quot;</span><span class="punctuation">:</span> <span class="string">&quot;yarn&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;useWorkspaces&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><br></pre></td></tr></table></figure><p>托管给 yarn wrokspace 之后，lerna 的 <code>packages</code> 将会被顶级 <code>package.json</code> 的 <code>workspaces</code> 覆盖:</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;private&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><br><span class="line">  <span class="attr">&quot;workspaces&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">    <span class="string">&quot;packages/*&quot;</span></span><br><span class="line">  <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="lerna-publish-config"><a href="#lerna-publish-config" class="headerlink" title="lerna publish config"></a>lerna publish config</h3><p>如果你不想在所有 <code>package.json</code> 文件中单独明确设置你的注册表配置，例如使用私有注册表时，设置 <code>command.publish.registry</code> 很有用。配置 <code>ignoreChanges</code> 则是为了避免不必要的版本升级。</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">&quot;ignoreChanges&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">  <span class="string">&quot;ignored-file&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="string">&quot;**/__tests__/**&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="string">&quot;**/*.md&quot;</span></span><br><span class="line"><span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line"><span class="attr">&quot;command&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;publish&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;registry&quot;</span><span class="punctuation">:</span> <span class="string">&quot;https://registry.npmjs.org&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><blockquote><p>除此之外，如果你的包名是带 scope 的，需要在那个包的 <code>package.json</code> 中设置 <code>publishConfig.access</code> 为 <code>&quot;public&quot;</code>。</p></blockquote><h3 id="lerna-version-config"><a href="#lerna-version-config" class="headerlink" title="lerna version config"></a>lerna version config</h3><p>当配置 <code>conventionalCommits</code> 为 <code>true</code> 后，lerna 版本将使用 <a href="https://conventionalcommits.org/">Conventional Commits Specification</a> 来确定版本升级并 <a href="https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-cli">生成 CHANGELOG.md 文件</a>。</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">&quot;command&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;version&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;conventionalCommits&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;message&quot;</span><span class="punctuation">:</span> <span class="string">&quot;chore(release): publish&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><p>规范化 <code>git commit</code> 对于提高 <code>git log</code> 可读性、可控的版本控制和 changelog 生成都有着重要的作用。紫升之前在 <a href="https://juejin.cn/post/6877462747631026190">一文搞定规范化Git Commit</a> 中详细讲述了 Conventional Commits 的概念以及 commitizen、cz-customizable、@commitlint&#x2F;cli、yorkie 和 commitlint-config-cz 等工具的配置。</p><p>由于配置繁琐，我在 <a href="https://github.com/youngjuning/cli#%E4%B8%80%E9%94%AE%E9%85%8D%E7%BD%AE-conventional-commit">@youngjuning&#x2F;cli</a> 中添加了 <code>init-commit</code> 命令一键配置 conventional commit。可以打开这个 <a href="https://github.com/youngjuning/vant-react-native/commit/d49f4203eacd37d3908306d309783ed5c11fb565">commit</a> 查看配置信息。</p><blockquote><p>注意：husky 高版本用法不向后兼容，我在这个 <a href="https://github.com/youngjuning/vant-react-native/commit/174c7fd883dd5dc7a0c4f5d8fcb25ed605680b12">commit</a> 中用尤大的 yorkie 代替了 husky。</p></blockquote><h2 id="代码规范化"><a href="#代码规范化" class="headerlink" title="代码规范化"></a>代码规范化</h2><p>代码规范化的重要性不言而喻，代码规范化涉及的工具有 editorconfig、eslint、prettier 等，在 <a href="https://juejin.cn/post/6854573217320075278">装它|再也不用操心ESLint配置</a> 一文中我介绍了如何一步一步建设属于自己的 eslint config 插件并产出了 <a href="https://github.com/youngjuning/eslint-config">@youngjuning&#x2F;eslint-config</a> 和 <a href="https://github.com/youngjuning/prettier-config">@youngjuning&#x2F;prettier-config</a>。</p><p>vant-react-native 暂时使用 @youngjuning&#x2F;eslint-config、@youngjuning&#x2F;prettier-config 约束项目代码规范。相关配置如下文。</p><h3 id="eslint"><a href="#eslint" class="headerlink" title="eslint"></a>eslint</h3><p>首先安装 react-native 所需的插件。</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">yarn add -D eslint-plugin-react \</span><br><span class="line">  eslint-plugin-react-hooks \</span><br><span class="line">  eslint-plugin-jsx-a11y \</span><br><span class="line">  eslint-plugin-import \</span><br><span class="line">  eslint-plugin-react-native</span><br></pre></td></tr></table></figure><p>然后配置 <code>.eslintrc.js</code></p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// .eslintrc.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;@youngjuning/eslint-config/react-native&#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 js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><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> = <span class="built_in">require</span>(<span class="string">&#x27;@youngjuning/prettier-config&#x27;</span>);</span><br></pre></td></tr></table></figure><blockquote><p>@youngjuning&#x2F;eslint-config 计划也用 lerna 管理，产出 @youngjuning&#x2F;eslint-config-react、@youngjuning&#x2F;eslint-config-react-native、@youngjuning&#x2F;eslint-config-vue 让开发者无需过多配置开箱即用。</p></blockquote><h3 id="editorconfig"><a href="#editorconfig" class="headerlink" title="editorconfig"></a>editorconfig</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"># .editorconfig</span><br><span class="line"># EditorConfig is awesome: http://EditorConfig.org</span><br><span class="line"></span><br><span class="line"># top-most EditorConfig file</span><br><span class="line">root = true</span><br><span class="line"></span><br><span class="line"># Unix-style newlines with a newline ending every file</span><br><span class="line">[*]</span><br><span class="line">indent_style = space</span><br><span class="line">indent_size = 2</span><br><span class="line">end_of_line = lf</span><br><span class="line">charset = utf-8</span><br><span class="line">trim_trailing_whitespace = true</span><br><span class="line">insert_final_newline = true</span><br><span class="line"></span><br><span class="line">[*.md]</span><br><span class="line">trim_trailing_whitespace = false</span><br><span class="line"></span><br><span class="line">[*.gradle]</span><br><span class="line">indent_size = 4</span><br><span class="line"></span><br><span class="line">[BUCK]</span><br><span class="line">indent_size = 4</span><br></pre></td></tr></table></figure><h3 id="yorkie-lint-staged"><a href="#yorkie-lint-staged" class="headerlink" title="yorkie &amp; lint-staged"></a>yorkie &amp; lint-staged</h3><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ yarn add -D yorkie lint-staged</span><br></pre></td></tr></table></figure><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;gitHooks&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;commit-msg&quot;</span><span class="punctuation">:</span> <span class="string">&quot;commitlint -e -V&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;pre-commit&quot;</span><span class="punctuation">:</span> <span class="string">&quot;lint-staged&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;lint-staged&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;**/*.&#123;js,jsx,ts,tsx&#125;&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">      <span class="string">&quot;eslint --fix&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="string">&quot;git add .&quot;</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><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><h2 id="第一个组件从-Icon-开始"><a href="#第一个组件从-Icon-开始" class="headerlink" title="第一个组件从 Icon 开始"></a>第一个组件从 Icon 开始</h2><p>一个成熟的组件库都会拥有自己的一套 Icon，Icon 一般由设计师通过 Sketch 设计，然后导出 svg 文件。</p><p>ant-design-icons 的 svg 文件是 <a href="https://github.com/ant-design/ant-design-icons/tree/master/packages/icons-svg/svg/filled">保存在本地</a>，然后通过脚本生成 <a href="https://github.com/ant-design/ant-design-icons/tree/master/packages/icons-react/src/icons">react 组件</a>、<a href="https://github.com/ant-design/ant-design-icons/tree/master/packages/icons-vue">vue 组件</a> 和 <a href="https://github.com/ant-design/ant-design-icons/tree/master/packages/icons-react-native">icons-react-native</a> 等组件，由于支持的框架比较完备我们无需自己实现，RN 我们直接使用 <a href="https://github.com/ant-design/ant-design-icons/tree/master/packages/icons-react-native">icons-react-native</a>。</p><p>vant 以及 fishd-mobile 则是通过 Iconfont 维护 svg 文件，然后通过设置 <code>@font-face</code> 的方式实现 Icon 组件，如图所示：</p><p><img src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9b3f6212f0124f4fab8b70dfeeb40c25~tplv-k3u1fbpfcp-watermark.image" alt="image.png"></p><p>有了 ttf 文件，我们可以像 @ant-design&#x2F;icons-react-native 一样基于 ttf 文件使用脚本生成 Icon 组件，但是使用 ttf 字体有一个弊端，就是每次更新图标，都要相应的更新 ttf 文件，然后再次打包发布 APP。而且 ttf 不支持多种色彩的图标，导致所有图标都是单色。如果你是借助 react-native-vector-icons，该库内置了 10 多套 ttf 文件，合起来有 2M 左右；你可能用不到它们，但是它们仍然会被打包进你的 APP 里，这也是我认为 react-native-elements 这个库外强中干的一大原因。</p><p>那么只有 Iconfont 链接我们如何实现 vant-icons 的 React Native 版本呢？这里紫升没有自己写脚本，而是使用了一款叫 react-native-iconfont-cli 的工具，<a href="https://github.com/fwh1990">fwh1990</a> 大佬针对以上痛点用纯 Javascript 实现 iconfont 到 React 组件的转换操作，不需要依赖 ttf 字体文件，不需要手动下载图标到本地。</p><h3 id="创建-lerna-子包"><a href="#创建-lerna-子包" class="headerlink" title="创建 lerna 子包"></a>创建 lerna 子包</h3><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 创建主包，主包用来统一导出所有的组件</span></span><br><span class="line">$ lerna create vant-react-native -y</span><br><span class="line"><span class="comment"># 创建 icons 包，我们的第一个组件！</span></span><br><span class="line">$ lerna create @vant-react-native/icons -y</span><br></pre></td></tr></table></figure><p>我们的目录结构看起来是这样的：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">.</span><br><span class="line">└── packages</span><br><span class="line">    ├── icons</span><br><span class="line">    │   ├── README.md</span><br><span class="line">    │   └── package.json</span><br><span class="line">    └── vant-react-native</span><br><span class="line">        ├── README.md</span><br><span class="line">        └── package.json</span><br></pre></td></tr></table></figure><h3 id="生成-icons"><a href="#生成-icons" class="headerlink" title="生成 icons"></a>生成 icons</h3><h4 id="安装插件"><a href="#安装插件" class="headerlink" title="安装插件"></a>安装插件</h4><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">yarn workspace @vant-react-native/icons add -D react-native-svg react-native-iconfont-cli</span><br></pre></td></tr></table></figure><h4 id="生成配置文件"><a href="#生成配置文件" class="headerlink" title="生成配置文件"></a>生成配置文件</h4><p>我们在 <code>packages/icons</code> 目录下使用 <code>npx iconfont-init</code> 命令会生成 <code>iconfont.json</code> 文件，自定义后内容如下：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;symbol_url&quot;</span><span class="punctuation">:</span> <span class="string">&quot;https://at.alicdn.com/t/font_2553510_7cds497uxwn.js&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;use_typescript&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">false</span></span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;save_dir&quot;</span><span class="punctuation">:</span> <span class="string">&quot;./lib&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;trim_icon_prefix&quot;</span><span class="punctuation">:</span> <span class="string">&quot;van-icon&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;default_icon_size&quot;</span><span class="punctuation">:</span> <span class="number">18</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><h4 id="生成-React-Native-标准组件"><a href="#生成-React-Native-标准组件" class="headerlink" title="生成 React Native 标准组件"></a>生成 React Native 标准组件</h4><p>执行 <code>npx iconfont-rn</code> 命令即可生成标准 React Native 组件。由于图标文件比较多，我们不将图标产物加入 git 管理。所以我们需要在 npm 发布前执行构建命令：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;build&quot;</span><span class="punctuation">:</span> <span class="string">&quot;npx iconfont-rn&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;prepublishOnly&quot;</span><span class="punctuation">:</span> <span class="string">&quot;yarn build&quot;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><h3 id="配置-react-native-vant"><a href="#配置-react-native-vant" class="headerlink" title="配置 react-native-vant"></a>配置 react-native-vant</h3><p>我们前面提到 <code>packages/vant-react-native</code> 是主包的目录，我们需要将 <code>@vant-react-native/icons</code> 包添加到主包的依赖中并导出。</p><h4 id="添加依赖"><a href="#添加依赖" class="headerlink" title="添加依赖"></a>添加依赖</h4><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ lerna add @vant-react-native/icons --scope vant-react-native</span><br></pre></td></tr></table></figure><h4 id="导出-Icon-组件"><a href="#导出-Icon-组件" class="headerlink" title="导出 Icon 组件"></a>导出 Icon 组件</h4><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// packages/vant-react-native/src/index.ts</span></span><br><span class="line"><span class="keyword">export</span> &#123; <span class="keyword">default</span> <span class="keyword">as</span> <span class="title class_">Icon</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;@vant-react-native/icons&#x27;</span>;</span><br><span class="line"><span class="keyword">export</span> * <span class="keyword">from</span> <span class="string">&#x27;@vant-react-native/icons&#x27;</span>;</span><br></pre></td></tr></table></figure><h4 id="tsconfig-配置"><a href="#tsconfig-配置" class="headerlink" title="tsconfig 配置"></a>tsconfig 配置</h4><p>对与每个子包我们期望使用一样的配置，所以我们会先在整个项目的根目录新建 <a href="https://github.com/youngjuning/vant-react-native/blob/main/tsconfig.base.json">tsconfig. base.json</a>，在子包继承即可。</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;extends&quot;</span><span class="punctuation">:</span> <span class="string">&quot;../../tsconfig.base.json&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;compilerOptions&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;outDir&quot;</span><span class="punctuation">:</span> <span class="string">&quot;lib&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;include&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span><span class="string">&quot;src/**/*&quot;</span><span class="punctuation">]</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><h4 id="配置发布脚本"><a href="#配置发布脚本" class="headerlink" title="配置发布脚本"></a>配置发布脚本</h4><p>和 <code>@vant-react-native/icons</code> 子包一样，我们需要添加 <code>build</code> 和 <code>prepublishOnly</code> 脚本：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;build&quot;</span><span class="punctuation">:</span> <span class="string">&quot;tsc&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;prepublishOnly&quot;</span><span class="punctuation">:</span> <span class="string">&quot;yarn build&quot;</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><p>第一次发布的话，注意使用的是 <code>lerna publish 0.0.1</code>，因为 lerna 的发布命令没有第一次发布这个参数，所以需要显示指定初始版本。或者可以将初始版本设置为 <code>0.0.0</code> 然后执行 <code>lerna publish</code>。</p><blockquote><p>小技巧：如果发布后想查看包内容，可以通过 <a href="https://www.jsdelivr.com/">jsdelivr</a> 查看。比如刚发布的 <a href="https://cdn.jsdelivr.net/npm/vant-react-native/">vant-react-native</a> 和 <a href="https://cdn.jsdelivr.net/npm/@vant-react-native/icons/">@vant-react-native&#x2F;icons</a></p></blockquote><h2 id="开发调试"><a href="#开发调试" class="headerlink" title="开发调试"></a>开发调试</h2><p>一个完善且体验良好的调试流程不仅能够满足在开发阶段验证组件是否符合预期，还可以降低开源社区基友的参与难度。React Native 组件库的调试和其他技术栈流程大体没有区别，只不过因为 <a href="https://github.com/facebook/metro/issues/1">Metro 不支持软连接</a> 以及 vant-react-native 是基于 lerna 的单体仓库项目，我们的配置会有不同。</p><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f3856a691a73409c8533fa9dd1966357~tplv-k3u1fbpfcp-watermark.image" alt="image.png"></p><h3 id="初始化-React-Native-App"><a href="#初始化-React-Native-App" class="headerlink" title="初始化 React Native App"></a>初始化 React Native App</h3><p>由于是 React Native 项目，我们需要初始化一个 React Native 项目。首先找一个地方使用 <code>react-native init vantapp --template react-native-template-typescript</code> 创建一个新的 React Native App。然后将生成的 App 与我们的主项目合并。合并后的项目结构如下：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><span class="line">.</span><br><span class="line">├── App.tsx</span><br><span class="line">├── __tests__</span><br><span class="line">│   └── App-test.tsx</span><br><span class="line">├── android</span><br><span class="line">│   ├── app</span><br><span class="line">│   ├── build.gradle</span><br><span class="line">│   ├── gradle</span><br><span class="line">│   ├── gradle.properties</span><br><span class="line">│   ├── gradlew</span><br><span class="line">│   ├── gradlew.bat</span><br><span class="line">│   └── settings.gradle</span><br><span class="line">├── app.json</span><br><span class="line">├── babel.config.js</span><br><span class="line">├── commitlint.config.js</span><br><span class="line">├── index.js</span><br><span class="line">├── ios</span><br><span class="line">│   ├── Podfile</span><br><span class="line">│   ├── Podfile.lock</span><br><span class="line">│   ├── Pods</span><br><span class="line">│   ├── vantapp</span><br><span class="line">│   ├── vantapp.xcodeproj</span><br><span class="line">│   ├── vantapp.xcworkspace</span><br><span class="line">│   └── vantappTests</span><br><span class="line">├── lerna.json</span><br><span class="line">├── metro.config.js</span><br><span class="line">├── package.json</span><br><span class="line">├── packages</span><br><span class="line">│   ├── icons</span><br><span class="line">│   └── vant-react-native</span><br><span class="line">├── tsconfig.base.json</span><br><span class="line">├── tsconfig.json</span><br><span class="line">└── yarn.lock</span><br></pre></td></tr></table></figure><p>主要冲突的是 Prettier、eslint 等工具的配置，合并没那么难。在运行项目之前，我们一般需要编译项目。我们可以借助 <code>lerna run build</code> 命令批量运行子包里的 <code>build</code> npm script。</p><blockquote><p>注意📢：由于子包之间有依赖关系，不要使用 <code>--parallel</code> 参数并行执行打包脚本。</p></blockquote><p>现在我们编写一个九宫格 Demo 验证一下：</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// App.tsx</span></span><br><span class="line"><span class="keyword">import</span> <span class="title class_">React</span>, &#123; <span class="title class_">Component</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;react&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">View</span>, <span class="title class_">Text</span>, <span class="title class_">SafeAreaView</span>, <span class="title class_">ScrollView</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;react-native&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">Icon</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;vant-react-native&#x27;</span>;</span><br><span class="line"><span class="comment">// 我们也可以只安装 @vant-react-native/icons 包</span></span><br><span class="line"><span class="comment">// import &#123; VanIconAdd &#125; from &#x27;@vant-react-native/icons&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> <span class="title class_">IconNameType</span> = <span class="title class_">React</span>.<span class="property">ComponentProps</span>&lt;<span class="keyword">typeof</span> <span class="title class_">Icon</span>&gt;[<span class="string">&#x27;name&#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">class</span> <span class="title class_">App</span> <span class="keyword">extends</span> <span class="title class_ inherited__">Component</span> &#123;</span><br><span class="line">  <span class="title function_">render</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">SafeAreaView</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">ScrollView</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">          <span class="tag">&lt;<span class="name">Text</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">textAlign:</span> &#x27;<span class="attr">center</span>&#x27;, <span class="attr">paddingVertical:</span> <span class="attr">20</span>, <span class="attr">fontSize:</span> <span class="attr">25</span>, <span class="attr">color:</span> &#x27;#<span class="attr">007fff</span>&#x27; &#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">            vant-react-native</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> <span class="attr">style</span>=<span class="string">&#123;&#123;</span> <span class="attr">flexWrap:</span> &#x27;<span class="attr">wrap</span>&#x27;, <span class="attr">flexDirection:</span> &#x27;<span class="attr">row</span>&#x27; &#125;&#125;&gt;</span></span></span><br><span class="line"><span class="language-xml">            &#123;data.map((item, index) =&gt; &#123;</span></span><br><span class="line"><span class="language-xml">              const lastLineLength = data.length % 4 || 4;</span></span><br><span class="line"><span class="language-xml">              return (</span></span><br><span class="line"><span class="language-xml">                <span class="tag">&lt;<span class="name">View</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">                  <span class="attr">key</span>=<span class="string">&#123;item&#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></span></span><br><span class="line"><span class="tag"><span class="language-xml">                    <span class="attr">width:</span> &#x27;<span class="attr">25</span>%&#x27;,</span></span></span><br><span class="line"><span class="tag"><span class="language-xml">                    <span class="attr">marginBottom:</span> <span class="attr">index</span> &lt; <span class="attr">data.length</span> <span class="attr">-</span> <span class="attr">lastLineLength</span> ? <span class="attr">40</span> <span class="attr">:</span> <span class="attr">0</span>,</span></span></span><br><span class="line"><span class="tag"><span class="language-xml">                    <span class="attr">alignItems:</span> &#x27;<span class="attr">center</span>&#x27;,</span></span></span><br><span class="line"><span class="tag"><span class="language-xml">                  &#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">                  <span class="tag">&lt;<span class="name">Icon</span> <span class="attr">name</span>=<span class="string">&#123;item&#125;</span> <span class="attr">size</span>=<span class="string">&#123;40&#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;&#123;</span> <span class="attr">color:</span> &#x27;#<span class="attr">646566</span>&#x27;, <span class="attr">marginTop:</span> <span class="attr">10</span> &#125;&#125;&gt;</span>&#123;item&#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></span><br><span class="line"><span class="language-xml">            &#125;)&#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">ScrollView</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;/<span class="name">SafeAreaView</span>&gt;</span></span></span><br><span class="line">    );</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> <span class="attr">data</span>: <span class="title class_">IconNameType</span>[] = [<span class="string">&#x27;location-o&#x27;</span>, <span class="string">&#x27;like-o&#x27;</span>, <span class="string">&#x27;star-o&#x27;</span>, <span class="string">&#x27;phone-o&#x27;</span>, <span class="string">&#x27;setting-o&#x27;</span>, <span class="string">&#x27;fire-o&#x27;</span>, <span class="string">&#x27;coupon-o&#x27;</span>, <span class="string">&#x27;cart-o&#x27;</span>, <span class="string">&#x27;shopping-cart-o&#x27;</span>, <span class="string">&#x27;cart-circle-o&#x27;</span>, <span class="string">&#x27;friends-o&#x27;</span>, <span class="string">&#x27;comment-o&#x27;</span>, <span class="string">&#x27;gem-o&#x27;</span>, <span class="string">&#x27;gift-o&#x27;</span>, <span class="string">&#x27;point-gift-o&#x27;</span>, <span class="string">&#x27;send-gift-o&#x27;</span>, <span class="string">&#x27;service-o&#x27;</span>, <span class="string">&#x27;bag-o&#x27;</span>, <span class="string">&#x27;todo-list-o&#x27;</span>, <span class="string">&#x27;balance-list-o&#x27;</span>, <span class="string">&#x27;close&#x27;</span>, <span class="string">&#x27;clock-o&#x27;</span>, <span class="string">&#x27;question-o&#x27;</span>, <span class="string">&#x27;passed&#x27;</span>];</span><br></pre></td></tr></table></figure><p>然后执行 <code>yarn ios</code> 查看实际效果（之后我们就可以执行 <code>yarn start --reset-cache</code> 快速开始调试）：</p><p><img src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/3d2828ea83804d3bb53a0563eaf429bd~tplv-k3u1fbpfcp-watermark.image" alt="image.png"></p><p>上面的示例代码中我们可以看到我们直接使用了 <code>import { Icon } from &#39;vant-react-native&#39;;</code> 而不是相对路径引用 packages 下的模块。可是我们的项目并没与安装这个依赖，编译器是怎么找到的呢？这里也没有什么银弹，这是因为 lerna 会把子包软链接到 node_modules 中，我们可以使用 <code>ls -al</code> 发现看到包的实际指向：</p><p><img src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/7857ecd166a74331befe791dc677257b~tplv-k3u1fbpfcp-watermark.image" alt="image.png"></p><p>我们也可以在类型提示中看到实际指向的是 packages 下的文件：</p><p><img src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ba82260550904406b4224734761c2f5e~tplv-k3u1fbpfcp-watermark.image" alt="image.png"></p><blockquote><p>注意📢：<a href="https://github.com/facebook/metro/issues/1">Metro 不支持符号链接</a> 指的是软连接的目录不在项目根目录下，这里我们软连接指向的位置还在根目录下，所以可以正确工作✅。这个特性保证了调试与生产开发的一致性和便利性。</p></blockquote><h3 id="实时编译"><a href="#实时编译" class="headerlink" title="实时编译"></a>实时编译</h3><p>现在我们的调试流程是：</p><ol><li>修改代码</li><li>执行 <code>lerna run build</code> 编译每个子包</li><li>执行 <code>yarn ios</code> 调试项目</li><li>修改代码</li><li>执行 <code>lerna run build</code> 重新编译</li><li>执行 <code>yarn start --reset-cache</code> 运行项目</li><li>循环 4、5、6。</li></ol><p>尽管 React Native 有 Fast Refresh 功能，但是由于我们的代码是需要编译的，所以我们需要重复编译运行的动作。</p><p>任何重复的工作都可以用脚本代替。首先我们需要给每个子包添加实时编译的 script，像 rollup、babel、webpack、typescript 都有参数可以实现实时编译：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><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;tsc -w&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;tsc&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;prepublishOnly&quot;</span><span class="punctuation">:</span> <span class="string">&quot;yarn build&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></pre></td></tr></table></figure><p>而我们的 @vant-react-native&#x2F;icons 包使用的 <code>npx iconfont</code> 没有实时编译选项，经过调研，我引入了 <a href="https://www.npmjs.com/package/onchange">onchange</a> 这个库可以基于 glob 模式监听文件改动后执行一个命令：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><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;onchange -i &#x27;iconfont.json&#x27; -- yarn build&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>然后我们需要使用 <code>lerna run dev --parallel</code> 批量执行实时编译脚本，这里加 <code>--parallel</code> 是因为子包如果是实时编译，进程会卡住。为了补救，我们不得不预先编译 <code>@vant-react-native/icons</code> 包，然后因为同样的原因我引入了 <code>npm-run-all</code> 来并行执行 <code>lerna run dev</code> 和 <code>react-native start</code>，完整脚本如下：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;predev&quot;</span><span class="punctuation">:</span> <span class="string">&quot;lerna run build --scope @vant-react-native/icons&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;dev&quot;</span><span class="punctuation">:</span> <span class="string">&quot;lerna run dev --parallel&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;react-native start&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;debug&quot;</span><span class="punctuation">:</span> <span class="string">&quot;run-p dev start&quot;</span><span class="punctuation">,</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><blockquote><p>小黑：“紫升哥哥，我之前为了使用 react-native-elements 的其中几个组件而引入了整个组件库。因为这个组件库依赖了 react-native-vector-icons 导致 bundle 包变大。如果我就是想用整套 vant-react-native，如何解决这个问题呢？”</p></blockquote><p>众所周知，React Native 的打包工具 Metro <a href="https://github.com/facebook/metro/issues/227#issuecomment-583358386">不支持 tree-shaking</a>。解决这个问题的方式其实很简单，机智的你可能知道配合 <a href="https://www.npmjs.com/package/babel-plugin-import">babel-plugin-import</a> 是可以实现按需加载的需求的。但由于我们是多包管理架构，需要针对多包的架构设计一个方案。</p><h3 id="react-naitve-bundle-包"><a href="#react-naitve-bundle-包" class="headerlink" title="react-naitve bundle 包"></a>react-naitve bundle 包</h3><p>为了比对优化前后包大小，我们需要使用 <code>react-native bundle</code> 命令看一下纯 JS 包的大小，我们来简单看下这个命令：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">react-native bundle --platform ios --entry-file index.js --bundle-output ./bundle/ios/index.ios.jsbundle --assets-dest ./bundle/ios --dev <span class="literal">false</span> --reset-cache</span><br></pre></td></tr></table></figure><ul><li><code>--entry</code>：入口 js 文件</li><li><code>--bundle-output</code>：生成的 bundle 文件路径</li><li><code>--platform</code>：平台</li><li><code>--assets-dest</code>：图片资源的输出目录</li><li><code>--dev</code>：是否为开发版本，打正式版的安装包时我们将其赋值为 false</li><li><code>--reset-cache</code>：重置缓存，避免打包使用旧的缓存</li></ul><h3 id="按需加载原理"><a href="#按需加载原理" class="headerlink" title="按需加载原理"></a>按需加载原理</h3><p>前面我们提到 <code>packages/vant-react-native</code> 只有一个文件 <code>src/index.ts</code> 用来导出所有子包，现在我们添加一个新的包 Button，看上去就是这样：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> &#123; <span class="keyword">default</span> <span class="keyword">as</span> <span class="title class_">Icon</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;@vant-react-native/icons&#x27;</span>;</span><br><span class="line"><span class="keyword">export</span> * <span class="keyword">from</span> <span class="string">&#x27;@vant-react-native/icons&#x27;</span>;</span><br><span class="line"><span class="keyword">export</span> &#123; <span class="keyword">default</span> <span class="keyword">as</span> <span class="title class_">Button</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;@vant-react-native/icons&#x27;</span>;</span><br></pre></td></tr></table></figure><p>这种导出方式，用户只能通过 <code>import Button from &#39;@vant-react-native/button&#39;;</code> 或 <code>import Button from &#39;vant-react-native/lib/button&#39;;</code> 的方式手动实现按需加载，这不仅不方便开发者使用，从打包产物来说也增加了很多字节。那么问题来了，怎么样的组织形式才能满足按需加载呢？答案就在 babel-plugin-import 插件的文档中：</p><p><img src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/3d8bd23d28b0413b8b3da8119d32dd35~tplv-k3u1fbpfcp-watermark.image" alt="image.png"></p><p>从图中我们看出 babel-plugin-import 插件是在编译阶段将引用指向了模块所在文件夹。用户使用时安装插件并做如下配置就完成了按需加载。</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">&quot;plugins&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">  <span class="punctuation">[</span><span class="string">&quot;import&quot;</span><span class="punctuation">,</span> <span class="punctuation">&#123;</span> libraryName<span class="punctuation">:</span> <span class="string">&quot;antd&quot;</span><span class="punctuation">,</span> style<span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span> <span class="punctuation">&#125;</span><span class="punctuation">]</span></span><br><span class="line"><span class="punctuation">]</span></span><br></pre></td></tr></table></figure><p>依然没有银弹，插件做的工作只是代替了你的右手。知道了原理我们就可以按照文档要求的格式重新组织我们的 vant-react-native 包：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line">.</span><br><span class="line">├── CHANGELOG.md</span><br><span class="line">├── lib                    <span class="comment"># 上传到 NPM 的编译产物</span></span><br><span class="line">│   ├── button             <span class="comment"># 符合 babel-plugin-import 的默认配置要求</span></span><br><span class="line">│   │   ├── index.d.ts</span><br><span class="line">│   │   └── index.js</span><br><span class="line">│   ├── icon</span><br><span class="line">│   │   ├── index.d.ts</span><br><span class="line">│   │   └── index.js</span><br><span class="line">│   ├── index.d.ts</span><br><span class="line">│   └── index.js          <span class="comment"># export * from &#x27;./button&#x27;;</span></span><br><span class="line">├── package.json</span><br><span class="line">├── src                   <span class="comment"># 源码目录</span></span><br><span class="line">│   ├── button</span><br><span class="line">│   │   └── index.ts</span><br><span class="line">│   ├── icon</span><br><span class="line">│   │   └── index.ts</span><br><span class="line">│   └── index.ts</span><br><span class="line">└── tsconfig.json         <span class="comment"># 编译配置，将 ts 文件编译到 lib 文件夹下</span></span><br></pre></td></tr></table></figure><p><strong>vant-react-native&#x2F;src&#x2F;button&#x2F;index.ts：</strong></p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">Button</span> <span class="keyword">from</span> <span class="string">&#x27;@vant-react-native/button&#x27;</span>;</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="title class_">Button</span>;</span><br><span class="line"><span class="keyword">export</span> &#123; <span class="title class_">Button</span> &#125;;</span><br></pre></td></tr></table></figure><p><strong>vant-react-native&#x2F;src&#x2F;icon&#x2F;index.ts：</strong></p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">Icon</span> <span class="keyword">from</span> <span class="string">&#x27;@vant-react-native/icons&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="title class_">Icon</span>;</span><br><span class="line"><span class="keyword">export</span> &#123; <span class="title class_">Icon</span> &#125;;</span><br><span class="line"><span class="keyword">export</span> * <span class="keyword">from</span> <span class="string">&#x27;@vant-react-native/icons&#x27;</span>;</span><br></pre></td></tr></table></figure><p><strong>vant-react-native&#x2F;src&#x2F;index.ts：</strong></p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> * <span class="keyword">from</span> <span class="string">&#x27;./icon&#x27;</span>;</span><br><span class="line"><span class="keyword">export</span> * <span class="keyword">from</span> <span class="string">&#x27;./button&#x27;</span>;</span><br></pre></td></tr></table></figure><p>然后项目中修改 babel.config.js：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="variable language_">module</span>.<span class="property">exports</span> = &#123;</span><br><span class="line">  <span class="attr">presets</span>: [<span class="string">&#x27;module:metro-react-native-babel-preset&#x27;</span>],</span><br><span class="line">  <span class="attr">plugins</span>: [</span><br><span class="line">    [<span class="string">&quot;import&quot;</span>, &#123;<span class="attr">libraryName</span>: <span class="string">&#x27;vant-react-native&#x27;</span>&#125;]</span><br><span class="line">  ],</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><h3 id="编写-Babel-插件？"><a href="#编写-Babel-插件？" class="headerlink" title="编写 Babel 插件？"></a>编写 Babel 插件？</h3><p>虽然通过修改主包的导出方式可以完成需求，但是却极大地增加了项目本身的复杂度。前面我们已经知道 babel-plugin-import 的原理是转换引用路径。那么我们是不是可以通过插件动态把 <code>import {Button} from &#39;vant-react-native&#39;</code> 转成 <code>import Button from &#39;@vant-react-native/button&#39;</code> 呢？答案是肯定的，下面是我基于 babel-plugin-import 的 <code>customName</code> 配置编写了一套配置并封装在 babel-plugin-import-vant 包中：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> camelCase <span class="keyword">from</span> <span class="string">&#x27;camelcase&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> (): <span class="built_in">any</span>[] =&gt; [</span><br><span class="line">  [</span><br><span class="line">    <span class="string">&#x27;import&#x27;</span>,</span><br><span class="line">    &#123;</span><br><span class="line">      <span class="attr">libraryName</span>: <span class="string">&#x27;vant-react-native&#x27;</span>,</span><br><span class="line">      <span class="attr">customName</span>: <span class="function">(<span class="params">name: <span class="built_in">string</span></span>) =&gt;</span> &#123;</span><br><span class="line">        <span class="keyword">if</span> (name === <span class="string">&#x27;icon&#x27;</span>) &#123;</span><br><span class="line">          <span class="keyword">return</span> <span class="string">&#x27;@vant-react-native/icons&#x27;</span>;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">if</span> (name.<span class="title function_">match</span>(<span class="regexp">/^van-icon-/</span>)) &#123;</span><br><span class="line">          <span class="keyword">return</span> <span class="string">`@vant-react-native/icons/lib/<span class="subst">$&#123;camelCase(name, &#123; pascalCase: <span class="literal">true</span> &#125;)&#125;</span>`</span>;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> <span class="string">`@vant-react-native/<span class="subst">$&#123;name&#125;</span>`</span>;</span><br><span class="line">      &#125;,</span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="string">&#x27;vant-react-native&#x27;</span>,</span><br><span class="line">  ],</span><br><span class="line">  [</span><br><span class="line">    <span class="string">&#x27;import&#x27;</span>,</span><br><span class="line">    &#123;</span><br><span class="line">      <span class="attr">libraryName</span>: <span class="string">&#x27;@vant-react-native/icons&#x27;</span>,</span><br><span class="line">      <span class="attr">customName</span>: <span class="function">(<span class="params">name: <span class="built_in">string</span></span>) =&gt;</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="string">`@vant-react-native/icons/lib/<span class="subst">$&#123;camelCase(name, &#123; pascalCase: <span class="literal">true</span> &#125;)&#125;</span>`</span>;</span><br><span class="line">      &#125;,</span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="string">&#x27;@vant-react-native/icons&#x27;</span>,</span><br><span class="line">  ],</span><br><span class="line">];</span><br></pre></td></tr></table></figure><p>在项目的 <code>babel.config.js</code> 配置中添加 <code>plugins: [...require(&#39;babel-plugin-import-vant&#39;).default()]</code> 即可实现按需加载。</p><p>还有可以优化的地方吗？机智的你可能又发现我只是通过函数导出了一个配置而已，并不是真正的插件，所以未来我会定制一个 vant-react-native 自己的按需加载 babel 插件。</p><blockquote><p><code>name.match(/^van-icon-/)</code> 这个判断条件是因为 <code>@vant-react-native/icons</code> 包除了包含一个默认导出的 Icon 组件，还导出了很多单个图标组件，为了进一步减小打包体积，我们对这个子包也进行了按需加载处理。</p><p><strong>我们已经知道按需加载的原理是没有中间商赚差价直接和卖家谈</strong>，所以后面我们遇见类似的需求通过转换返回卖家地址即可。不需要破坏性地改项目结构。</p></blockquote><h3 id="成果展示"><a href="#成果展示" class="headerlink" title="成果展示"></a>成果展示</h3><table><thead><tr><th>初始包大小</th><th>未配置按需加载（引入 Button）</th><th>按需加载（引入 Button）</th><th>按需加载（引入 Icon）</th><th>按需加载（引入 VanIconAdd）</th></tr></thead><tbody><tr><td>723KB</td><td>1.8M</td><td>725KB</td><td>1.8M</td><td>1.22M</td></tr></tbody></table><p>之所以 Icon 包会大，是因为 react-native-svg 这个库大，所以不建议直接使用 Icon 组件，而是使用 VanIconAdd、VanIconEye 这种单独的图标组件，少了 593KB 还是挺香的。</p><h2 id="组件库文档"><a href="#组件库文档" class="headerlink" title="组件库文档"></a>组件库文档</h2><p>组件库文档比较重要的是有可以交互的 Demo 演示，我是 Dumi 的资深用户，借助 dumi-theme-mobile 和 <a href="https://github.com/youngjuning/umi-plugin-react-native">umi-plugin-react-native</a> 我们可以很好地满足 React Native 组件库文档的搭建。</p><h3 id="集成-Dumi-到项目中"><a href="#集成-Dumi-到项目中" class="headerlink" title="集成 Dumi 到项目中"></a>集成 Dumi 到项目中</h3><p><strong>安装依赖：</strong></p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ yarn add dumi dumi-theme-mobile umi-plugin-react-native -D</span><br></pre></td></tr></table></figure><p><strong>配置文件：</strong></p><p>在项目根目录添加 <code>.umirc.ts</code></p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; defineConfig, <span class="title class_">IConfig</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;dumi&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="title function_">defineConfig</span>(&#123;</span><br><span class="line">  <span class="attr">title</span>: <span class="string">&#x27;vant-react-native&#x27;</span>,</span><br><span class="line">  <span class="attr">mode</span>: <span class="string">&#x27;site&#x27;</span>,</span><br><span class="line">  <span class="attr">logo</span>: <span class="string">&#x27;https://img01.yzcdn.cn/vant/logo.png&#x27;</span>,</span><br><span class="line">  <span class="attr">favicon</span>: <span class="string">&#x27;https://img01.yzcdn.cn/vant/logo.png&#x27;</span>,</span><br><span class="line">  <span class="attr">resolve</span>: &#123;</span><br><span class="line">    <span class="attr">includes</span>: [<span class="string">&#x27;docs&#x27;</span>, <span class="string">&#x27;packages/button&#x27;</span>, <span class="string">&#x27;packages/icons&#x27;</span>],</span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="comment">// more config: https://d.umijs.org/config</span></span><br><span class="line">&#125; <span class="keyword">as</span> <span class="title class_">IConfig</span>);</span><br></pre></td></tr></table></figure><p>值得一提的是，Dumi 是支持 Lerna 仓库的，它默认会以 <code>packages/[包名]/src</code> 为基础路径搜寻所有子包的 Markdown 文档并生成路由。通过 <code>resolve.includes</code> 可以配置 dumi 嗅探的文档目录，dumi 会尝试在配置的目录中递归寻找 markdown 文件。</p><p><strong>添加 NPM 脚本：</strong></p><blockquote><p>注意📢：由于实际依赖的是 packages 下的包，我们必须先编译所有的包，否则部署的时候会报 <code>This dependency was not found:</code> 的错误。</p></blockquote><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><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;start:dumi&quot;</span><span class="punctuation">:</span> <span class="string">&quot;dumi dev&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;build:dumi&quot;</span><span class="punctuation">:</span> <span class="string">&quot;lerna run build &amp;&amp; dumi build&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><strong>忽略文件（.gitignore）：</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"># umi</span><br><span class="line">.umi</span><br><span class="line">.umi-production</span><br><span class="line">.env.local</span><br><span class="line">dist/</span><br></pre></td></tr></table></figure><h3 id="部署到-GitHub-Pages"><a href="#部署到-GitHub-Pages" class="headerlink" title="部署到 GitHub Pages"></a>部署到 GitHub Pages</h3><p>在根目录新建 <code>.github/workflows/gh-pages</code>：</p><figure class="highlight yml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">name:</span> <span class="string">github</span> <span class="string">pages</span></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><br><span class="line">      <span class="bullet">-</span> <span class="string">main</span> <span class="comment"># default branch</span></span><br><span class="line"><span class="attr">jobs:</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-18.04</span></span><br><span class="line">    <span class="attr">steps:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">uses:</span> <span class="string">actions/checkout@v2</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">run:</span> <span class="string">yarn</span> <span class="string">install</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">run:</span> <span class="string">yarn</span> <span class="string">build:dumi</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Deploy</span></span><br><span class="line">        <span class="attr">uses:</span> <span class="string">peaceiris/actions-gh-pages@v3</span></span><br><span class="line">        <span class="attr">with:</span></span><br><span class="line">          <span class="attr">github_token:</span> <span class="string">$&#123;&#123;</span> <span class="string">secrets.GITHUB_TOKEN</span> <span class="string">&#125;&#125;</span></span><br><span class="line">          <span class="attr">publish_dir:</span> <span class="string">./dist</span></span><br></pre></td></tr></table></figure><h3 id="预览"><a href="#预览" class="headerlink" title="预览"></a>预览</h3><p>现在我们可以访问 <a href="https://blog.zisheng.pro/vant-react-native/">https://blog.zisheng.pro/vant-react-native/</a> 查看效果了：</p><p><img src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9faf0889b6d64238922bb02c51cf02ff~tplv-k3u1fbpfcp-watermark.image" alt="image.png"></p><p><img src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a11e9ce535c54f8bb038599533f0ef9a~tplv-k3u1fbpfcp-watermark.image" alt="image.png"></p><p><img src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/8bed86245d8f485894b0c5e3d9767e9c~tplv-k3u1fbpfcp-watermark.image" alt="image.png"></p><h3 id="配置优化"><a href="#配置优化" class="headerlink" title="配置优化"></a>配置优化</h3><p>现在基于 dumi 的文档站点只是初始化，很多配置（.umirc.ts）可以优化，比如：</p><ol><li>基于 jsdelivr 配置 CDN 加速</li></ol><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> isProd = process.<span class="property">env</span>.<span class="property">NODE_ENV</span> === <span class="string">&#x27;production&#x27;</span>;</span><br><span class="line">...</span><br><span class="line"><span class="attr">publicPath</span>: isProd ? <span class="string">&#x27;https://cdn.jsdelivr.net/gh/youngjuning/vant-react-native@gh-pages/&#x27;</span>: <span class="string">&#x27;/&#x27;</span>,</span><br></pre></td></tr></table></figure><ol start="2"><li>增量发布和避免浏览器加载缓存</li></ol><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">hash</span>: <span class="literal">true</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ol start="3"><li><a href="https://www.umeng.com/web">友盟网站统计</a></li></ol><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">scripts</span>: [<span class="string">&#x27;https://s9.cnzz.com/z_stat.php?id=1280093214&amp;web_id=1280093214&#x27;</span>],</span><br><span class="line">  <span class="attr">styles</span>: [<span class="string">&#x27;a[title=站长统计] &#123; display: none; &#125;&#x27;</span>],</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ol start="4"><li>配置 <code>exportStatic: {}</code> 将所有路由输出为 HTML 目录结构，以免刷新页面时 404。</li></ol><h3 id="Pull-Request-预发预览"><a href="#Pull-Request-预发预览" class="headerlink" title="Pull Request 预发预览"></a>Pull Request 预发预览</h3><p>考虑到后期社区会贡献代码和文档。在 pr 合进主分支之前，我们需要预览文档或组件。满足这一需求的是一个叫 surge.sh 的静态托管服务，surge 支持在命令行通过简单的命令免费发布 HTML、CSS 和 JS 文件到 web。</p><h4 id="申请-Surge-Token"><a href="#申请-Surge-Token" class="headerlink" title="申请 Surge Token"></a>申请 Surge Token</h4><p><strong>安装 surge cli：</strong></p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm install --global surge</span><br></pre></td></tr></table></figure><p><strong>注册 surge 账号：</strong></p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">suerge login</span><br></pre></td></tr></table></figure><p><strong>获取 token：</strong></p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">suerge token</span><br></pre></td></tr></table></figure><h4 id="配置-CI"><a href="#配置-CI" class="headerlink" title="配置 CI"></a>配置 CI</h4><p>由于 GitHub 的安全问题，surge-preview Action 插件无法使用，我们参考 dumi 官方的配置自定义了 CI，首先我们拷贝下图中的三个文件到项目中。</p><p><img src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9e3d3e61ac1c4019ac393b2d1b2c5719~tplv-k3u1fbpfcp-watermark.image" alt="image.png"></p><p>然后修改 <code>preview-build.yml</code> 中的 <code>build step</code>：</p><figure class="highlight diff"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="deletion">- NODE_OPTIONS=&#x27;--max-old-space-size=4096&#x27; yarn build</span></span><br><span class="line"><span class="addition">+ NODE_OPTIONS=&#x27;--max-old-space-size=4096&#x27; PREVIEW_PR=true yarn build:dumi</span></span><br></pre></td></tr></table></figure><p>添加环境变量 <code>PREVIEW_PR=true</code> 是为了让 dumi 打包时识别出不是生产环境打包，<code>.umirc.ts</code> 需要相应修改为：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> isProd =</span><br><span class="line">  process.<span class="property">env</span>.<span class="property">NODE_ENV</span> === <span class="string">&#x27;production&#x27;</span> &amp;&amp; process.<span class="property">env</span>.<span class="property">PREVIEW_PR</span> !== <span class="string">&quot;true&quot;</span>;</span><br><span class="line">...</span><br><span class="line"><span class="attr">publicPath</span>: isProd ? <span class="string">&#x27;https://cdn.jsdelivr.net/gh/youngjuning/vant-react-native@gh-pages/&#x27;</span>: <span class="string">&#x27;/&#x27;</span>,</span><br><span class="line">...</span><br></pre></td></tr></table></figure><p>再然后，修改 <code>preview-deploy.yml</code> 文件中的部署域名 <code>dumi-preview</code> 为 <code>vant-react-native-preview</code>。</p><p>最后我们把前面获取的 Surge Token 添加到仓库的 Secrets 即可。</p><h4 id="成果展示-1"><a href="#成果展示-1" class="headerlink" title="成果展示"></a>成果展示</h4><p><strong>正在部署 PR 预览状态：</strong></p><p><img src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/193ac9e16cca408383ca7ae2ec503613~tplv-k3u1fbpfcp-watermark.image" alt="image.png"></p><p><strong>部署成功状态：</strong></p><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/1659de97aafd4f87a93993c35ef93195~tplv-k3u1fbpfcp-watermark.image" alt="image.png"></p><p>访问 <a href="https://vant-react-native-preview-pr-1.surge.sh/">https://vant-react-native-preview-pr-1.surge.sh/</a> 即可验证文档的正确性✅。</p><h2 id="单元测试"><a href="#单元测试" class="headerlink" title="单元测试"></a>单元测试</h2><p>我在 <a href="https://juejin.cn/post/6937983077209604127">使用 Jest 和 Enzyme 进行 React Native 单元测试|技术点评</a> 一文中曾提交单元测试和文档一样，是保障程序最小单元质量的重要一环。诚然一个成熟的组件库是必然有单元测试的身影。本章就不展开讲单元测试了，主要讲 vant-react-native 是如何配置单元测试的。</p><h3 id="安装依赖"><a href="#安装依赖" class="headerlink" title="安装依赖"></a>安装依赖</h3><p>jest、babel-jest、@types&#x2F;jest 这些依赖都已经安装了，我们需要安装的是 enzyme 这个基于 jest 的单元测试框架。</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ yarn add enzyme jest-enzyme enzyme-adapter-react-16 enzyme-to-json @types/enzyme react-native-mock-render -DW</span><br></pre></td></tr></table></figure><blockquote><p>Enzyme 是用于 React 的 JavaScript 测试实用程序，可以更轻松地测试 React 组件的输出。您还可以根据给定的输出进行操作，遍历并以某种方式模拟运行时。</p></blockquote><h3 id="配置"><a href="#配置" class="headerlink" title="配置"></a>配置</h3><p><strong>jest.config.js：</strong></p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="variable language_">module</span>.<span class="property">exports</span> = &#123;</span><br><span class="line">  <span class="attr">preset</span>: <span class="string">&#x27;react-native&#x27;</span>,</span><br><span class="line">  <span class="attr">verbose</span>: <span class="literal">true</span>,</span><br><span class="line">  <span class="attr">collectCoverage</span>: <span class="literal">true</span>, <span class="comment">// 生成测试覆盖率报告</span></span><br><span class="line">  <span class="attr">moduleNameMapper</span>: &#123;</span><br><span class="line">    <span class="comment">// for https://github.com/facebook/jest/issues/919</span></span><br><span class="line">    <span class="string">&#x27;^image![a-zA-Z0-9$_-]+$&#x27;</span>: <span class="string">&#x27;GlobalImageStub&#x27;</span>,</span><br><span class="line">    <span class="string">&#x27;^[@./a-zA-Z0-9$_-]+\\.(png|gif)$&#x27;</span>: <span class="string">&#x27;RelativeImageStub&#x27;</span>,</span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="attr">setupFilesAfterEnv</span>: [<span class="string">&#x27;&lt;rootDir&gt;/jest.setup.js&#x27;</span>], <span class="comment">// 使用 Jest 运行安装文件以配置 Enzyme 和适配器（如下文jest.setup.js中所示），之前是setupTestFrameworkScriptFile，也可以使用setupFiles</span></span><br><span class="line">  <span class="attr">snapshotSerializers</span>: [<span class="string">&#x27;enzyme-to-json/serializer&#x27;</span>], <span class="comment">// 推荐使用序列化程序使用 enzyme-to-json，它的安装和使用非常简单，并允许您编写简洁的快照测试。</span></span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p><strong>jest.setup.js：</strong></p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="string">&#x27;react-native&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> <span class="title class_">Enzyme</span> <span class="keyword">from</span> <span class="string">&#x27;enzyme&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> <span class="title class_">Adapter</span> <span class="keyword">from</span> <span class="string">&#x27;enzyme-adapter-react-16&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="title class_">Enzyme</span>.<span class="title function_">configure</span>(&#123; <span class="attr">adapter</span>: <span class="keyword">new</span> <span class="title class_">Adapter</span>() &#125;);</span><br></pre></td></tr></table></figure><p><strong>一个简单的示例：</strong></p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// packages/button/__test__/index.tsx</span></span><br><span class="line"><span class="keyword">import</span> <span class="title class_">React</span> <span class="keyword">from</span> <span class="string">&#x27;react&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; shallow &#125; <span class="keyword">from</span> <span class="string">&#x27;enzyme&#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;../src/index&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">setup</span>(<span class="params">props = &#123;&#125;</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> wrapper = <span class="title function_">shallow</span>(<span class="language-xml"><span class="tag">&lt;<span class="name">Button</span> /&gt;</span></span>);</span><br><span class="line">  <span class="keyword">const</span> instance = wrapper.<span class="title function_">instance</span>();</span><br><span class="line">  <span class="keyword">return</span> &#123; wrapper, instance &#125;;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="title function_">describe</span>(<span class="string">&#x27;Button Component&#x27;</span>, <span class="function">() =&gt;</span> &#123;</span><br><span class="line">  <span class="title function_">it</span>(<span class="string">&#x27;renders correctly&#x27;</span>, <span class="function">() =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">const</span> &#123; wrapper &#125; = <span class="title function_">setup</span>();</span><br><span class="line">    <span class="title function_">expect</span>(wrapper).<span class="title function_">toMatchSnapshot</span>();</span><br><span class="line">  &#125;);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p>执行 <code>jest</code> 命令后可以查看覆盖率如下：</p><p><img src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/358737fa32f549f4a9b06a9bcf8903d4~tplv-k3u1fbpfcp-watermark.image" alt="image.png"></p><h2 id="写给勇士"><a href="#写给勇士" class="headerlink" title="写给勇士"></a>写给勇士</h2><p>能写长文的不算勇士，能坚持看到这里的才是勇士。紫升在此感谢您的阅读。然而组件库工程化这只是一个起点，如果本文反响好，组件库具体组件的设计实现、完整的 React Native 单元测试教程等等紫升会在后续的文章中展开讲。</p><h2 id="推荐的-UI-库"><a href="#推荐的-UI-库" class="headerlink" title="推荐的 UI 库"></a>推荐的 UI 库</h2><p>当然了，vant-react-native 并不是你唯一的选择，下面的几个 UI 库都是很优秀的项目。在实现 vant-react-native 时我也多少借鉴了前人优秀的设计。</p><ul><li><a href="https://github.com/ant-design/ant-design-mobile-rn">antd-mobile-rn</a></li><li><a href="https://github.com/react-native-elements/react-native-elements">react-native-elements</a></li><li><a href="https://github.com/akveo/react-native-ui-kitten">react-native-ui-kitten</a></li><li><a href="https://github.com/wix/react-native-ui-lib">react-native-ui-lib</a></li><li><a href="https://zarm.design/">Zarm</a></li></ul>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/eae5334982a6/</id>
    <link href="https://blog.zisheng.pro/posts/eae5334982a6/"/>
    <published>2021-07-11T17:01:01.000Z</published>
    <summary>这是一篇干货比较多的组件库搭建实战教程，不仅有通用的代码规范、提交规范、文档维护、单元测试、GitHub Action 配置的讲解，还涉及基于 lerna 的多包管理架构、React Native 图标库建设、React Native 组件库开发调试、按需加载原理及实现。</summary>
    <title>每个前端都值得拥有自己的组件库，就像每个夏天都拥有西瓜🍉</title>
    <updated>2026-09-18T08:15:28.640Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="掘金翻译计划" scheme="https://blog.zisheng.pro/categories/%E6%8E%98%E9%87%91%E7%BF%BB%E8%AF%91%E8%AE%A1%E5%88%92/"/>
    <category term="JavaScript" scheme="https://blog.zisheng.pro/tags/JavaScript/"/>
    <category term="掘金翻译计划" scheme="https://blog.zisheng.pro/tags/%E6%8E%98%E9%87%91%E7%BF%BB%E8%AF%91%E8%AE%A1%E5%88%92/"/>
    <category term="TC39" scheme="https://blog.zisheng.pro/tags/TC39/"/>
    <content>
      <![CDATA[<blockquote><ul><li>原文地址：<a href="https://medium.com/@jorydotcom/revised-patterns-for-participation-in-standards-committees-dae82d93954e">Revised Patterns for Participation in Standards Committees</a></li><li>原文作者：<a href="https://medium.com/@jorydotcom">Jory Burson</a></li><li>译文出自：<a href="https://github.com/xitu/gold-miner">掘金翻译计划</a></li><li>本文永久链接：<a href="https://github.com/xitu/gold-miner/blob/master/article/ECMA-TC39/Revised-Patterns-for-Participation-in-Standards-Committees.md">https://github.com/xitu/gold-miner/blob/master/article/ECMA-TC39/Revised-Patterns-for-Participation-in-Standards-Committees.md</a></li><li>译者：<a href="https://github.com/youngjuning">紫升</a></li><li>校对者：<a href="https://github.com/KimYangOfCat">KimYangOfCat</a>，<a href="https://github.com/qq1120637483">qq1120637483</a></li></ul></blockquote><p><img src="https://miro.medium.com/max/1020/1*EnBeLnKvRHmiHvbivS4jzg.jpeg" alt="紫升"></p><p><small><em>图片来自民族学局提交给史密森学会秘书的年度报告（1881）</em></small></p><p>我最近重新阅读了 <a href="https://twitter.com/awbjs">艾伦·维尔福斯·布洛克</a> 的论文 <a href="http://pl.csie.ntut.edu.tw/asianplop2016/proceedings/A14.pdf">编程语言的标准化和参与模式</a>，同时为即将到来的讲座做准备。艾伦在编程语言标准开发、研究和文档方面有20多年的经验，这对 JavaScript 社区来说是非常有价值且不能被低估的。这篇论文为那些刚开始从事标准制定工作的人提供了学术性的指导方针和模式；为了使其更具有可操作性，并为我们的社区提供现代化的语言，我在下面更新了他的指导方针，以及一些改善大家参与标准委员会体验的有用建议。</p><p>技术标准委员会的性质决定了用户、实施者和供应商都在挤在同一个房间里，他们的需求或利益往往是相互冲突的。<a href="https://twitter.com/slightlylate">亚历克斯·罗素</a> 在他的 <a href="https://infrequently.org/2018/06/effective-standards-work-part-1-the-lay-of-the-land/">有效的标准工作</a> 系列文章中对网络驱动的标准组织中的这些紧张关系做了很好的说明。重要的是我们要明白，最终大家都在努力做正确的事——挑战在于就什么是正确的事达成共识，并以一种包容和尊重的方式去执行。这些指导方针是关于鼓励社会活动，促进专业（和个人回报）的标准制定过程。</p><h1 id="首先寻求理解…"><a href="#首先寻求理解…" class="headerlink" title="首先寻求理解…"></a>首先寻求理解…</h1><p><strong>做一个积极的倾听者</strong>：艾伦的指导意见是，只有当你确定你有有价值的东西需要说的时候才可以发言。我完全同意，对于一个新人来说，在第一次会议上应该专注于倾听和观察。然而，我认为有必要对这一准则进行修订，如果你有不明白的地方，绝对可以提出问题并得到解释（当然是在适当的时候）。为了提出问题，你必须真正倾听别人说的话——无论是技术上的还是背景上的，这也是快速融入小组的好方法。</p><p><strong>做好研究工作</strong>：这不仅适用于你可能有兴趣提出的具体建议和想法，而且也适用于整个团体的组织、个人和历史。但有时候知易行难，反馈或电子邮件中的文档很难再找到，或者对某件事情有真正了解的人可能已经离开了。Ecma TC39（以及大多数 W3C 工作组）已经通过 GitHub 将大量的背景资料放在网上，并且正在努力为社区呈现更多的组织历史。如果你偶然发现某个领域的历史不为人知，这可能是一个很好的机会，你可以通过填补空白做出有意义的贡献。</p><p><strong>认识你的团队</strong>：艾伦建议了解其他玩家，但我更愿意把它看作是认识和了解你的新团队的其他人。所有的技术都是其社会环境的产物，所以你越是了解这个社会环境并能对其做出积极的贡献，就越能帮助你的事业。如果你不认识小组中的其他人，请发送电子邮件介绍自己并分享你的想法。邀请某个组员吃顿烧烤或喝杯星巴克。和你的新伙伴一起参加或组织会后活动。了解他们来自哪里，哪些问题对他们来说是最重要的。当面建立这些联系更容易——我强烈建议，如果条件允许，你应该每年都参加一次或多次会议。</p><h1 id="…然后被理解"><a href="#…然后被理解" class="headerlink" title="…然后被理解"></a>…然后被理解</h1><p><strong>明确你的目标</strong>：如果你想实现一个新的功能，请准备好你要解决的明确的问题和任何相关的用例。如果你的解决方案的形式足够好，也要记得为它写测试。请记住，根据语言在其生命周期中的位置，实际问题要比理论问题更有说服力。</p><p>正如艾伦所指出的，“语言标准的制定不是一种绿地开发活动……语言标准委员会的存在是为了解决语言现状所产生的问题”。因此，如果你的目标是以某种方式发展一种语言——比如说，这样它就可以在区块链上实现——你就必须要有耐心。反之，如果你已经达到了你的目标，就可以继续做其他事情了。要有目的性。</p><blockquote><p>译者注：绿地项目在软件开发领域，一项绿地工程可能是在全新的环境内开发一个新系统，不需要考虑与其他系统，尤其是与老旧系统（Legacy system）的集成问题。这里作者指的是语言标准的制定是有历史包袱的。</p></blockquote><p><strong>对变化持开放的态度</strong>：标准工作尽管其名声在外，但这并不是关于竞争性的争论（我喜欢说，TC39 不是关于“竞争性的 JavaScripting”）。小组中的任何两个人都可能以不同的方式解决同一个问题，但是并不会自动抉择哪个人是正确的。在制定让你的功能得到实施的策略时，艾伦建议寻找盟友、选择你的战斗并有一个备用方案。与他人合作交流想法，可以提升你对问题和语言的理解，并帮助你发展对其他组织关心的问题的敏感性（从而可能影响你的提案的成功机会）。这也有助于你避免在特定背景下不重要的争论和辩论。另一个重要的策略是：对自己的错误持开放态度，并愿意说出来。毕竟，大多数想法和设计都不好，而我们最终接受的大多数想法和设计在开始时也是不好的。<sup>1</sup>营造一个可以安全地提出、讨论、推进或撤回想法的环境，对于健康的技术对话至关重要——这需要所有各方都愿意改变他们的想法。</p><p><strong>成为贡献者</strong>：艾伦和亚历克斯都认为，从历史上看，团体越老，新人和新想法就越难进入该团体。幸运的是，在这些团体中，有一个明确的向新的想法和工作方式开放的文化转变。</p><blockquote><p>有很多方法可以开始为标准工作做出贡献，而不需要写技术提案。事实上，这往往是最重要也是最缺乏服务的领域!</p></blockquote><p>你如何为一个标准工作做出贡献，最终应该是你的优势、目标和团体的需求的中一个因素。这里有一些方法可以让你成为一个伟大的贡献者，提高你的可信度，而不需要写新的提案。</p><ul><li>会议笔记记录志愿者。</li><li>组织或共同组织社会活动。</li><li>帮助策划会议。</li><li>为委员会的其他工作做出贡献，如编写文件。</li><li>阅读提案并提供关键的反馈或使用案例。</li><li>帮助编辑或支持其他提案，或为功能和提案编写测试。</li><li>帮助识别现有技术和被忽略的声音——是否有可以借鉴的例子，或者可以咨询的人，以帮助推动一些事情的发展？</li></ul><p><strong>力求达成共识</strong>：大多数网络标准委员会使用共识模式进行决策，如何衡量共识，各小组会有所不同，但一般意味着小组的大多数<sup>2</sup>支持该决定，其余人愿意接受该决定。分歧和辩论是可以的，也是需要的，但必须是专业和尊重的。在某些时候，作为代表，你必须决定你是否应该积极反对一项决定，但这应该是一种罕见的情况。并非每项提案都会被纳入最终规范——事实上，大多数都不会，这是件好事。</p><p>无论你是第一次参与标准制定并希望找到自己的立足点，还是你是现有的参与者并希望提高你目前工作的成功率，上述准则都适用。任何组织、委员会或运营联合开源软件项目的团队都可以直接从这些指南中受益，或者进行修改，使其更适合你的团队。我们的目标是快乐地制定标准!</p><ul><li><sup>1</sup>： 亚历克斯·罗素的 <a href="https://infrequently.org/2018/06/effective-standards-work-part-2-threading-the-needle/">穿针引线</a></li><li><sup>2</sup>：大多数的定义因组织而异。它通常需要三分之二或更多的投票来通过一个问题。</li></ul><blockquote><p>如果发现译文存在错误或其他需要改进的地方，欢迎到 <a href="https://github.com/xitu/gold-miner">掘金翻译计划</a> 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 <strong>本文永久链接</strong> 即为本文在 GitHub 上的 MarkDown 链接。</p></blockquote><hr><blockquote><p><a href="https://github.com/xitu/gold-miner">掘金翻译计划</a> 是一个翻译优质互联网技术文章的社区，文章来源为 <a href="https://juejin.im/">掘金</a> 上的英文分享文章。内容覆盖 <a href="https://github.com/xitu/gold-miner#android">Android</a>、<a href="https://github.com/xitu/gold-miner#ios">iOS</a>、<a href="https://github.com/xitu/gold-miner#%E5%89%8D%E7%AB%AF">前端</a>、<a href="https://github.com/xitu/gold-miner#%E5%90%8E%E7%AB%AF">后端</a>、<a href="https://github.com/xitu/gold-miner#%E5%8C%BA%E5%9D%97%E9%93%BE">区块链</a>、<a href="https://github.com/xitu/gold-miner#%E4%BA%A7%E5%93%81">产品</a>、<a href="https://github.com/xitu/gold-miner#%E8%AE%BE%E8%AE%A1">设计</a>、<a href="https://github.com/xitu/gold-miner#%E4%BA%BA%E5%B7%A5%E6%99%BA%E8%83%BD">人工智能</a>等领域，想要查看更多优质译文请持续关注 <a href="https://github.com/xitu/gold-miner">掘金翻译计划</a>、<a href="http://weibo.com/juejinfanyi">官方微博</a>、<a href="https://zhuanlan.zhihu.com/juejinfanyi">知乎专栏</a>。</p></blockquote>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/b64419077480/</id>
    <link href="https://blog.zisheng.pro/posts/b64419077480/"/>
    <published>2021-06-08T08:35:10.000Z</published>
    <summary>这篇论文为那些刚开始从事标准制定工作的人提供了学术性的指导方针和模式；为了使其更具有可操作性，并为我们的社区提供现代化的语言，我在下面更新了他的指导方针，以及一些改善大家参与标准委员会体验的有用建议。</summary>
    <title>修订后的参与 TC39 标准委员会的模式</title>
    <updated>2026-09-18T08:15:28.636Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="漫谈" scheme="https://blog.zisheng.pro/categories/%E6%BC%AB%E8%B0%88/"/>
    <category term="掘金" scheme="https://blog.zisheng.pro/tags/%E6%8E%98%E9%87%91/"/>
    <category term="写作" scheme="https://blog.zisheng.pro/tags/%E5%86%99%E4%BD%9C/"/>
    <category term="个人成长" scheme="https://blog.zisheng.pro/tags/%E4%B8%AA%E4%BA%BA%E6%88%90%E9%95%BF/"/>
    <category term="个人博客" scheme="https://blog.zisheng.pro/tags/%E4%B8%AA%E4%BA%BA%E5%8D%9A%E5%AE%A2/"/>
    <content>
      <![CDATA[<p>小时候长大想做的职业，我的小作文写的是作家。但是很可惜被迫选择了理科。然后专业更是选择了计算机。这一切看似和写作一点关系都没有。实际上是，大学的时候经理了手撸博客、用 wordpress 搭博客、用 hexo 搭静态博客的历程。慢慢地将自己的业务兴趣和专业联系在了一起，假装自己没有背弃理想。</p><p>第一次在社区发文并获得影响是为了推广 wx-promise-pro：</p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/2109e0efc60e4ccda679b599b0298235~tplv-k3u1fbpfcp-zoom-1.image" style="zoom: 67%;" /><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e34344c7ef6847c2a3597ac75df1220e~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/4718dc1b6e574a9e9e92f3c8e91037b6~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><h3 id="大道至简"><a href="#大道至简" class="headerlink" title="大道至简"></a>大道至简</h3><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a95ad345f27a4a4b8fb446670ad006e3~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><h3 id="巧用-issues"><a href="#巧用-issues" class="headerlink" title="巧用 issues"></a>巧用 issues</h3><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0b26b3a1f44b4e50a6e3ac9b53d39606~tplv-k3u1fbpfcp-zoom-1.image" style="zoom: 33%;" /><h2 id="如何坚持写作"><a href="#如何坚持写作" class="headerlink" title="如何坚持写作"></a>如何坚持写作</h2><h3 id="输入决定产出"><a href="#输入决定产出" class="headerlink" title="输入决定产出"></a>输入决定产出</h3><p>长期的输出是伴随着长期的输入的，一般有两个方向可以选择，一是横向地学习并记录 📝 学习过程，二是纵向对自己擅长的领域进行深挖，最终形成系列。</p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/952e21273503469ea3e72fd65389a204~tplv-k3u1fbpfcp-zoom-1.image" style="zoom:50%;" /><h3 id="参与活动"><a href="#参与活动" class="headerlink" title="参与活动"></a>参与活动</h3><p>其实各个内容平台都有推广活动，而且对入门和大佬都有相应的活动可以参与，我便是在参与了几次掘金的活动之后获得了了流量扶持以及结识平台运营。</p><p>一年的积累下来，集齐了两套完整的周边，以及各种单品。并在今年三月、四月获得了月榜的证书。</p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/8251ea3c43f74027a3c215c3f321db3f~tplv-k3u1fbpfcp-zoom-1.image" style="zoom: 50%;" /><h2 id="收益"><a href="#收益" class="headerlink" title="收益"></a>收益</h2><h3 id="影响力："><a href="#影响力：" class="headerlink" title="影响力："></a>影响力：</h3><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/8c7dd5af95d74bba8381bef755a77527~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><h3 id="与行业大佬交流的机会："><a href="#与行业大佬交流的机会：" class="headerlink" title="与行业大佬交流的机会："></a>与行业大佬交流的机会：</h3><img src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/8907918504174d9094119d0f707886a4~tplv-k3u1fbpfcp-watermark.image" style="zoom: 50%;" /><ul><li><a href="https://juejin.cn/post/6946764691725254669">闲鱼宗心大佬聊跨端职业发展</a></li></ul><h3 id="认识一帮前端大佬："><a href="#认识一帮前端大佬：" class="headerlink" title="认识一帮前端大佬："></a>认识一帮前端大佬：</h3><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a78af0e6d7554e04ae35f5e3c5b1d8db~tplv-k3u1fbpfcp-zoom-1.image" style="zoom:50%;" /><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/1f95c125c85e438ba8654d1d9b947228~tplv-k3u1fbpfcp-zoom-1.image" style="zoom: 20%;" /><ul><li><a href="https://blog.zisheng.pro/friends">紫升友人帐</a></li></ul>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/5d19fd0218f8/</id>
    <link href="https://blog.zisheng.pro/posts/5d19fd0218f8/"/>
    <published>2021-06-04T08:31:11.000Z</published>
    <summary>回顾从搭建博客到持续技术写作的路径，讨论输入、系列化输出和社区参与如何积累个人影响力，为技术人长期写作提供一套可执行的起点。</summary>
    <title>少壮不努力，老大写博客</title>
    <updated>2026-09-18T08:15:28.640Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="React Native" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/React-Native/"/>
    <category term="Flutter" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/Flutter/"/>
    <category term="Flutter" scheme="https://blog.zisheng.pro/tags/Flutter/"/>
    <category term="React Native" scheme="https://blog.zisheng.pro/tags/React-Native/"/>
    <category term="跨端" scheme="https://blog.zisheng.pro/tags/%E8%B7%A8%E7%AB%AF/"/>
    <category term="跨平台" scheme="https://blog.zisheng.pro/tags/%E8%B7%A8%E5%B9%B3%E5%8F%B0/"/>
    <content>
      <![CDATA[<blockquote><p>原文链接：<a href="https://www.thedroidsonroids.com/blog/flutter-vs-react-native-what-to-choose-in-2021">https://www.thedroidsonroids.com/blog/flutter-vs-react-native-what-to-choose-in-2021</a><br>原作者：the droids on roids<br>校对：<a href="https://juejin.cn/user/606586150596360">Alex</a></p></blockquote><h2 id="译者序"><a href="#译者序" class="headerlink" title="译者序"></a>译者序</h2><p>从 Flutter 诞生之日起，React Native 就一直被拿来与之作比较，从谷歌超过 700 万的相关搜索结果中可以一瞥这场跨端王位之争的激烈程度。本文不像我翻译的标题一样在捧一踩一，而是较客观地对两者进行评价并给出实际需求中如何抉择。</p><h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>你想更迅速地开发一款移动应用吗？你想通过跨平台技术减少开发成本吗？你还在为不知道选择哪一种技术焦虑吗？</p><p>本文正是解决你的焦虑的，我们比较了跨端方案的佼佼者：Flutter 和 React Native，并从 App 所有者的角度出发帮助你判断哪一款框架更适合你的 App 的需求。</p><h2 id="为什么我们写这篇文章"><a href="#为什么我们写这篇文章" class="headerlink" title="为什么我们写这篇文章"></a>为什么我们写这篇文章</h2><p>跨平台解决方案越来越受欢迎。Flutter 和 React Native 是两个行业领先的参与者，但是在 <a href="https://www.thedroidsonroids.com/about-us">Droids On Roids</a>，在为合作伙伴构建商业应用时，我们会根据具体需求来选择最合适的方案：可能是原生、Flutter 或者 React Native。</p><p>尽管如此，企业主总是希望知道几个关键问题的答案:</p><ul><li>哪一个框架最适合我的项目？</li><li>哪一种方案可以让我的应用以最快的速度面市？</li><li>能保证我的 App 稳定且有很好地用户体验吗？</li><li>如果 <a href="https://www.thedroidsonroids.com/services/product-design-ux-and-ui-for-mobile-and-web-app">完美设计</a> 是我的第一原则我该原则哪一种解决方案？</li></ul><p><strong>我们写这篇文章是为了总结与合作伙伴分享的答案，以便你也可以从中受益。</strong></p><p>我们希望这篇文章能帮助你在为你的 App 选择跨端方案时做出明智且合理的决策。</p><h3 id="作者介绍"><a href="#作者介绍" class="headerlink" title="作者介绍"></a>作者介绍</h3><p>深度对比这两种技术，需要对 Flutter 和 React Native 都有一定水平和经验的大佬才能胜任。</p><p>因此，我们要求两名 Droids On Roids 开发人员来执行此任务：</p><ul><li>Bartosz – Web 和 React Native 开发者</li><li>Damian – iOS 和 Flutter 开发者 (<strong><a href="https://github.com/DroidsOnRoids/flutter-roadmap">Roadmap for Flutter Developers in 2021</a></strong> 的作者—欢迎 PR)</li></ul><p>这两位大佬日常工作便是使用 Flutter 和 React Native 来为客户构建商业 App。因此，他们的分享不仅包括理论，还包括多年的开发经验。</p><p>我们还针对社区中的热门文章验证了我们的理解，并参考了这些资源来解答问题。 你可以在本文的末尾找到完整的参考文献列表。</p><h2 id="Flutter-vs-React-Native-概览"><a href="#Flutter-vs-React-Native-概览" class="headerlink" title="Flutter vs React Native: 概览"></a>Flutter vs React Native: 概览</h2><table><thead><tr><th align="left"></th><th align="left">Flutter</th><th>React Native</th></tr></thead><tbody><tr><td align="left">简介</td><td align="left">Flutter 是一个由谷歌开发的开源移动应用软件开发工具包，用于为 Android、iOS、Windows、Mac、Linux、Google Fuchsia 开发应用。</td><td>React Native 基于 React.js，目的是让开发者可以利用 JavaScript 和 React.js 的声明式编程模式开发出在多平台上运作的程式</td></tr><tr><td align="left">正式发布</td><td align="left">2018 年 12 月，Google I&#x2F;O 大会</td><td>2015 年 3 月，F8 大会</td></tr><tr><td align="left">厂商</td><td align="left">谷歌</td><td>脸书</td></tr><tr><td align="left">是否免费开源</td><td align="left">是</td><td>是</td></tr><tr><td align="left">编程语言</td><td align="left">Dart</td><td>JavaScript</td></tr><tr><td align="left">人气</td><td align="left"><a href="https://github.com/flutter/flutter">Github</a> 11.5 万 Star (2021 年 3 月)</td><td><a href="https://github.com/facebook/react-native">Github</a> 9.3 万 Star (2021 年 3 月)</td></tr><tr><td align="left">热加载（Hot Reload）</td><td align="left">是</td><td>是</td></tr><tr><td align="left">原生性能</td><td align="left">优秀</td><td>优秀</td></tr><tr><td align="left">UI</td><td align="left">Flutter 应用程序在最新的操作系统上看起来与旧版本一样好。由于它们只有一个代码库，因此这些应用在 <a href="https://www.thedroidsonroids.com/services/ios-mobile-app-development">iOS</a> 和 <a href="https%EF%BC%9A//www.thedroidsonroids">Android</a> 上的外观和行为类似，但是由于 Material Design 和 Cupertino widgets 的帮助，他们还可以模仿平台本身的设计。这是怎么做到的呢？Flutter 包含两组符合特定设计语言的 Widgets：Material Design Widgets 实现了同名 Google 的设计语言；Cupertino widgets 模拟了苹果公司 iOS 的设计。这意味着你的 Flutter 应用在每个平台上的外观和行为都将自然地模仿它们的原生组件。</td><td>应用组件看起来和原生组件一样（比如，一个 Button 在 iOS 设备上看起来和 iOS 原生 Button 一样，Android 设备上也是如此）。React Native 在后台使用了原生组件这一事实印证了这一点，在任何操作系统 UI 更新之后，你的应用程序组件也将相应地立即升级。也就是说，这可能会破坏应用的用户界面，但这种情况很少发生。如果你希望你的应用在各个平台上看起来几乎相同，在较旧版本的操作系统下也相同（正如 Flutter 实现的那样），你需要使用第三方组件库（比如 <a href="https://reactnativepaper.com/">react-native-paper</a>）。它们将使你能够使用 Material Design 组件代替原生组件。</td></tr><tr><td align="left">一码多端</td><td align="left">最近 Flutter 2 正式版发布了，作为 Flutter 的重大升级，使用 Flutter 2 开发者可以用相同的代码，把使用 Flutter 开发的应用发布到五个操作系统：<code>iOS</code>，<code>Android</code>，<code>Windows</code>，<code>macOS</code> 和 <code>Linux</code>； 以及运行到 <code>Chrome</code> 、 <code>Firefox</code>， <code>Safari</code> 或 <code>Edge</code>等浏览器的 Web 版本上，Flutter 甚至还可以嵌入到车机、电视和智能家电中。</td><td>iOS、Android — 但是有第三方库可以让你使用一套代码构建 iOS、Android、web 和 Windows10 应用。你还可以在移动端、桌面端和 Web 应用中提取共享代码到单独的存储库； 将其视为一个单独的项目； 然后以与另一个依赖项相同的方式注入它。这使开发人员可以专注于为特定平台编写代码，而不必考虑与另一个依赖项的兼容性。</td></tr><tr><td align="left">知名案例</td><td align="left">阿里巴巴的闲鱼、Hamilton Musical 的 Hamilton App、Google Ads App</td><td>Instagram, Facebook, Facebook Ads, Skype, Tesla</td></tr><tr><td align="left">开发周期</td><td align="left">通常比原生开发要快得多。</td><td>可能与 Flutter 的开发速度一样快。但是 React Native 使用桥接和原生组件，因此可能需要对每个平台进行单独的优化-基于 widget 的 Flutter 则没有这个问题。 它可能会使使用 React Native 进行 App 开发周期更长</td></tr><tr><td align="left">竞争优势</td><td align="left">得益于丰富的 widgets 让 Flutter 开发的界面很漂亮；快速增长的社区和知名度；Flutter 团队的精心提供的出色文档（这使得使用 Flutter 进行开发变得容易）;改进 Flutter for Web，为一码多端的跨平台开发提供了潜力；难以缩短开发周期</td><td>稳定性（已面市 5 年以上）；许多使用 React Native 的成功案例以及著名的市场参与者；成熟、庞大的社区；易于学习的技术；大量的教程和库，使你可以快速轻松地进行开发；可以轻松地在 Web 应用程序和桌面应用程序之间重用代码。</td></tr><tr><td align="left">什么时候不是好的选择</td><td align="left">如果你的应用程序需要支持 3D Touch（目前，Flutter 不支持 3D，但在 Flutter 团队的长期路线图中有此功能）应用程序的设计是特定于平台的；你的应用需要与操作系统进行多次交互或需要稀有的鲜为人知的原生库；你需要一个简约的 UI，但需要大量使用手机硬件（例如，播放音乐或仅拍照的应用程序）；你想要创建一个即时应用程序（小型应用程序），那么最好选择原生应用程序开发。有关这些情况的更多信息，请点击 <a href="https://www.thedroidsonroids.com/blog/flutter-in-mobile-app-development-pros-and-cons-for-app-owners#notgoodchoice">此处&gt;&gt;</a></td><td>如果你的应用程序需要在后台处理不太常见的任务或超特定任务（例如计算）；你需要通过蓝牙进行自定义通信（使用 React Native 实施可能会很棘手）； 如果你想要同时开发 iOS 和 Android 应用并且掌握 JavaScript 那么你可以选择 React Native，但是如果你只想开发 Android 应用，则最好让原生团队去构建。如果符合以上的则更适合选择原生应用</td></tr></tbody></table><h2 id="介绍-Flutter-和-React-Native"><a href="#介绍-Flutter-和-React-Native" class="headerlink" title="介绍 Flutter 和 React Native"></a>介绍 Flutter 和 React Native</h2><p>首先，让我们介绍 Flutter 和 React Native 的基本（但必不可少的）细节。</p><h3 id="Flutter-是什么？"><a href="#Flutter-是什么？" class="headerlink" title="Flutter 是什么？"></a>Flutter 是什么？</h3><p>Flutter 是一个可移植的 UI 工具包。 换句话说，这是一个全面的应用软件开发套件（SDK），其中包含 widgets 和工具。</p><h3 id="Flutter-应用场景"><a href="#Flutter-应用场景" class="headerlink" title="Flutter 应用场景"></a>Flutter 应用场景</h3><p><strong>Flutter 为跨平台开发赋能。</strong></p><p>它为开发人员提供了一种简便的方法来为移动应用（iOS、Android）、<a href="https://flutter.cn/web">Web</a> 和桌面端构建和部署具有视觉吸引力的原生编译的应用程序：一套代码运行在多端，即所谓的一次编写处处运行。</p><p><strong>请注意:</strong></p><ul><li><p><strong>在使用 Flutter 开发桌面应用程序时，API 仍处于开发阶段</strong>。Google 正在努力扩展 Flutter，以支持桌面端作为目标环境，从而允许开发人员使用该技术创建 macOS，Windows 和 Linux 应用程序。</p><blockquote><p>Flutter 2 已经全平台 SDK 稳定，详情查看 <a href="https://juejin.cn/post/6935520179262586917">Flutter 2.0 发布 | 针对 Web，移动端和桌面端构建的下一代 Flutter</a></p></blockquote></li><li><p>Flutter for Web 是 Flutter 的代码兼容实现，允许您将用 Dart 编写的现有 Flutter 代码编译为可嵌入到浏览器中并部署到任何 Web 服务器的客户端。 您可以使用 Flutter 的所有功能，并且不需要浏览器插件</p></li></ul><p>关于 Flutter 的更多信息：</p><ul><li>免费且开源</li><li>它基于 <a href="https://dart.cn/">Dart</a>——一种快速的，面向对象的编程语言。<br>Dart 相对较新，并且易于学习-特别是对于经验丰富的熟悉 Java 和 C# 的开发人员</li><li>该架构基于非常流行的反应式编程（它遵循与 React 相同的风格）</li><li>它提供了自己的控件，这些控件是从自己的高性能渲染引擎中提取的，这些控件快速，有吸引力且可自定义</li><li>多亏了控件的使用经验，Flutter 应用程序的外观和感觉都很棒（尽管你仍然可以创建自己的 <a href="https://www.thedroidsonroids.com/services/product-design-ux-and-ui-for-mobile-and-web-app">自定义应用程序设计</a>，使用遵循特定平台准则的易于使用的 UI 组件）</li></ul><h3 id="谁创建了-Flutter-项目"><a href="#谁创建了-Flutter-项目" class="headerlink" title="谁创建了 Flutter 项目"></a>谁创建了 Flutter 项目</h3><p>一个来自 <strong>Google</strong> 的团队创建了 Flutter 项目。</p><p>但是作为一个开源项目，Google 和 Flutter 社区都在为 Flutter 的开发贡献力量。</p><h3 id="Flutter-有多成熟？"><a href="#Flutter-有多成熟？" class="headerlink" title="Flutter 有多成熟？"></a>Flutter 有多成熟？</h3><p>Flutter 简明历史：</p><ul><li><strong>2018 年 2 月，世界移动通信大会 – Flutter 发布了第一个 beta 版本</strong></li><li>2018 年 4 月，Google I&#x2F;O 大会 – Flutter beta 2 发布</li><li>2018 年 5 月，Google I&#x2F;O – Flutter beta 3 发布<ul><li>Flutter 进入 <strong>GitHub Top 100</strong></li></ul></li><li>2018 年 6 月 – Flutter Preview 1 发布</li><li>2018 年 9 月 – Flutter Preview 2 发布</li><li>2018 年 11 月 - Dart 2.1 发布</li><li>2018 年 12 月，Google I&#x2F;O – Flutter 1.0 发布<ul><li>自 2018 年 12 月以来，Flutter 被认为是稳定且可投入生产的技术里程碑</li></ul></li><li>2019 年 2 月，移动世界大会– Flutter 1.2 发布</li><li>2019 年 5 月，世界移动通信大会<ul><li>Flutter 1.5 发布</li><li><a href="https://flutter.dev/web">Flutter for Web</a> 预览版发布</li><li>Dart 2.3 发布</li></ul></li><li>2019 年 7 月, <a href="https://medium.com/flutter/announcing-flutter-1-7-9cab4f34eacf">Flutter 1.7 发布</a></li><li>2019 年九月, <a href="https://medium.com/flutterpub/whats-new-in-flutter-1-9-26b3c518dd0">Flutter 1.9 发布</a></li></ul><p>如你所见，Google 以快速的步伐从 Flutter beta 版本过渡到 Flutter 1.0 稳定版。</p><p>更妙的是，自 Flutter 1.0 发布以来，Flutter 团队并没有因此而感到骄傲。他们积极致力于使工具包更强大，更灵活，并优先考虑增强稳定性、性能和质量。</p><p>此外，Flutter 1.5 包含数百项响应开发人员反馈的更改。</p><p>Flutter 现在是 GitHub 上排名前 20 位的活跃仓库之一（截至 2019 年 12 月 11 日排名第 16 位），这证明了开发人员社区正在使用它，并继续为其改进做出贡献。</p><p>总而言之，Flutter 仍然是一个新兴的技术。</p><p>但是，考虑到 Flutter 不断改进的步伐以及它的爆炸性普及，我们可以充满信心地说，它随着年龄的增长已经非常稳定和成熟。Fluter 2.0 发布之后更是如此。</p><p>从发展速度来看，我们预计 Flutter for Web 也将在 12 个月内稳定下来。（截止发稿已经稳定了）</p><h3 id="哪些流行的应用在使用-Flutter？"><a href="#哪些流行的应用在使用-Flutter？" class="headerlink" title="哪些流行的应用在使用 Flutter？"></a>哪些流行的应用在使用 Flutter？</h3><ul><li>阿里巴巴的闲鱼 App – 阿里巴巴是全球最大的电子商务公司之一</li><li>Hamilton App – 百老汇音乐剧的官方应用程序</li><li>Google Ads app</li><li>Reflectly</li><li>京东金融 App – 奶茶妹妹老公的公司</li><li>Topline app by Abbey Road Studios</li></ul><h3 id="React-Native-是什么"><a href="#React-Native-是什么" class="headerlink" title="React Native 是什么?"></a>React Native 是什么?</h3><p>React Native 是使用 JavaScript 的开源移动应用程序框架。</p><h3 id="React-Native-应用场景"><a href="#React-Native-应用场景" class="headerlink" title="React Native 应用场景?"></a>React Native 应用场景?</h3><p>React Native 适用于以下场景：</p><ul><li><strong>跨平台开发</strong></li><li>使用 JavaScript 开发移动应用</li><li>一套代码开发 Android 和 iOS 应用</li><li>使用和 React 相同的设计</li></ul><p><strong>请注意：</strong></p><ul><li>使用 React Native <strong>创建的应用程序不是移动 Web 应用程序</strong>。 React Native 使用与常规 iOS 和 Android 应用相同的基本 UI 构建块：这意味着，你不需要使用 Java、Kotlin 或者 Swift 构建应用，而是使用 JavaScript 和 React 将相同的代码放在一起。</li><li>React Native 使用类似于 Flutter 中的控件的组件进行开发。</li></ul><p>要使用 React Native 开发<strong>Web 和桌面应用程序</strong>，最好使用外部库。</p><h3 id="谁创建了-React-Native-项目"><a href="#谁创建了-React-Native-项目" class="headerlink" title="谁创建了 React Native 项目?"></a>谁创建了 React Native 项目?</h3><p><strong>Facebook</strong> 创建了 React Native</p><h3 id="React-Native-成熟度"><a href="#React-Native-成熟度" class="headerlink" title="React Native 成熟度?"></a>React Native 成熟度?</h3><p>React Native 简明历史:</p><ul><li>2013 年夏季，Facebook 黑客马拉松 – React Native 作为 Facebook 内部项目开始</li><li>2015 年 1 月，React.js 会议– React Native 1 发布</li><li>2015 年 3 月，F8 大会 – React Native 正式启动<ul><li>Facebook 宣布 React Native 为 “开放使用并在 Github 上可用”</li></ul></li></ul><p>综上所述，可以肯定有两件事：React Native 比 Flutte r 年长，并且拥有更大的社区。 更不用说 Facebook 团队有足够的时间稳定 API 并专注于解决所有潜在问题：</p><ul><li><a href="https://github.com/react-native-community/discussions-and-proposals/issues/6">Lean Core </a>– 通过将可选组件&#x2F;功能移动到单独的存储库中来减小应用程序的大小（以在需要时添加到应用程序中）</li><li><a href="https://github.com/react-native-community/discussions-and-proposals/issues/40">TurboModules</a> – 用于改进原生模块的处理</li><li><a href="https://github.com/react-native-community/discussions-and-proposals/issues/4">React Native Fabric</a> – 重构 UI 层</li></ul><h3 id="哪些流行的应用在使用-React-Native"><a href="#哪些流行的应用在使用-React-Native" class="headerlink" title="哪些流行的应用在使用 React Native?"></a>哪些流行的应用在使用 React Native?</h3><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/fe8a0e49580c488e96dedfa7b6cf484c~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><p>如果你想查看其他使用 React Native 制作的应用示例，请查看 <a href="https://facebook.github.io/react-native/showcase.html">official React Native showcase</a>.</p><h2 id="Flutter-–-优势和劣势"><a href="#Flutter-–-优势和劣势" class="headerlink" title="Flutter – 优势和劣势"></a>Flutter – 优势和劣势</h2><p>在本节中，我们简要讨论 Flutter 的主要优缺点。</p><p>如果您想了解更多信息，请查看 <a href="https://www.thedroidsonroids.com/blog/flutter-in-mobile-app-development-pros-and-cons-for-app-owners">这篇文章</a> Flutter 对应用程序所有者的利弊。</p><h3 id="✅-Flutter-的优势"><a href="#✅-Flutter-的优势" class="headerlink" title="✅ Flutter 的优势"></a>✅ Flutter 的优势</h3><h4 id="1-热重载-快速开发"><a href="#1-热重载-快速开发" class="headerlink" title="1. 热重载 &#x3D; 快速开发"></a>1. 热重载 &#x3D; 快速开发</h4><p>从开发人员的角度来看，Flutter 提供了更多动态的，更快的应用程序开发。 这是 Flutter 最为人称道的事情之一，每一家顶级移动应用开发公司都对此表示赞赏</p><p>开发人员可以即时更改代码库，并立即将其反映在应用程序中。 这就是所谓的 <a href="https://flutter.cn/docs/development/tools/hot-reload">热重载</a> 功能，响应更改通常是秒级甚至毫秒级。</p><p>该功能可帮助团队立即添加功能，修复错误并尝试新的想法。 另外，在开发人员与设计人员的协作中，<strong>热加载非常方便</strong>。</p><h4 id="2-一次编写-多处运行"><a href="#2-一次编写-多处运行" class="headerlink" title="2. 一次编写, 多处运行"></a>2. 一次编写, 多处运行</h4><p>使用 Flutter，开发人员仅可以为两个应用程序编写一个代码库——覆盖 iOS 和 Android 平台。</p><p>Flutter 具有平台无关性，因为它具有自己的 widgets 和设计，这意味着你可以在两个平台上拥有完全相同的应用程序（如果你希望区分自己的应用程序，这同样容易实现）。</p><p>Google 目前正在开发 Flutter for Web，您可以将其作为预览版进行查看。 一旦上线，单个代码库将覆盖 Android，iOS 和 Web 平台。</p><h4 id="3-最多减少-50％-的测试"><a href="#3-最多减少-50％-的测试" class="headerlink" title="3. 最多减少 50％ 的测试"></a>3. 最多减少 50％ 的测试</h4><p>假设你在两个平台上都有相同的应用程序，则质量保证流程会更快更少的测试。</p><p>我们编写的自动化测试大约减少 50％，因为我们可以创建相同的测试以在两个平台上运行，从而减少了对 QA 团队的需求。</p><p>也就是说，你仍然必须在与原生编程类似的级别上运行手动测试，因为你的质量检查专家必须手动检查每个平台上的两个应用程序。</p><h4 id="4-更快的应用"><a href="#4-更快的应用" class="headerlink" title="4. 更快的应用"></a>4. 更快的应用</h4><p><strong>Flutter 应用程序运行流畅，快速滚动时不会卡顿。</strong> 为什么？</p><p>Flutter 使用 <a href="https://skia.org/">Skia 图形库</a>。 因此，每次视图更改时，UI 都会重新绘制。</p><p>大部分工作在 GPU 上完成； 这就是 Flutter UI 流畅且可提供 60fps（每秒帧数）的原因。</p><p>但是在开发过程中必须小心，以免重绘视图中数据未更改的那些元素。</p><p>重绘整个视图而不是仅重绘那些已更改的元素会影响应用程序的性能和速度，尤其是在需要经常重载视图的情况下，例如在秒表应用程序中。</p><h4 id="5-用户会喜欢的设计"><a href="#5-用户会喜欢的设计" class="headerlink" title="5. 用户会喜欢的设计"></a>5. 用户会喜欢的设计</h4><p>Flutter 不依赖原生系统组件。 相反，Flutter 拥有自己的一组自定义 widgets，这些 widgets 由框架的图形引擎呈现和管理。 用户将看到与典型的原生应用程序不同的 UI 组件，但这不一定是不利的。</p><p>Flutter 应用程序具有特别友好的用户界面：Flutter 与 React Native 相比具有至关重要的优势，源于对视觉细节的超级专注。 Flutter 的创建是为了使你可以轻松创建自己的窗口 widgets，或简单地自定义现有窗口 widgets。</p><p>随意浏览 <a href="https://flutter.cn/widgets/">核心 Widget 目录</a>; 或者，单击链接以查看 <a href="https://flutter.cn/docs/development/ui/widgets/material">Material Design widgets</a> 和 <a href="https://flutter.cn/docs/development/ui/widgets/cupertino">Cupertino widgets</a> 的示例。</p><h4 id="6-一样的-UI-甚至在旧机型上"><a href="#6-一样的-UI-甚至在旧机型上" class="headerlink" title="6. 一样的 UI, 甚至在旧机型上"></a>6. 一样的 UI, 甚至在旧机型上</h4><p>甚至新的应用在旧的 iOS 或 Android 系统上看起来也一样，因此你不必担心支持旧的设备。</p><table><thead><tr><th align="center">Android 5.1.1</th><th align="center">Android 8.1.0</th></tr></thead><tbody><tr><td align="center"><img src="https://media.giphy.com/media/8mdR9KZHBVue6J8ItN/giphy.gif" alt="紫升"></td><td align="center"><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/53ae3b6533ea4548bd447cbb59feaa72~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></td></tr></tbody></table><h4 id="7-完美适用于-MVP"><a href="#7-完美适用于-MVP" class="headerlink" title="7. 完美适用于 MVP"></a>7. 完美适用于 MVP</h4><p>如果你需要为应用程序构建 MVP（Minimum Viable Product，最小可行产品）（例如，作为潜在投资者的展示），则 Flutter 是理想的选择，尤其是在时间短的情况下。 另请阅读：</p><ul><li><a href="https://www.thedroidsonroids.com/blog/how-to-build-minimum-viable-product-mvp-business-plan">How to Build a Minimum Viable Product – MVP Guide for App Owners</a></li><li><a href="https://www.thedroidsonroids.com/blog/mobile-app-development-cost-in-2021">App development cost in 2021</a></li></ul><p>查看 <a href="https://play.google.com/store/apps/details?id=io.flutter.demo.gallery">Flutter Gallery 应用程序</a>，其中包含 Flutter 的核心功能，小部件和小插图的演示。</p><table><thead><tr><th align="center">Flutter Galary</th><th align="center">Flutter Galary</th><th align="center">Flutter Galary</th></tr></thead><tbody><tr><td align="center"><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/7dc504630daa48f99d68f68a33d40ea0~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></td><td align="center"><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c80f1894916144fdb8a3cdbd15131ed9~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></td><td align="center"><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/2b2171b61cee4213bed667635194a633~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></td></tr></tbody></table><h3 id="🔻-Flutter-的劣势"><a href="#🔻-Flutter-的劣势" class="headerlink" title="🔻 Flutter 的劣势"></a>🔻 Flutter 的劣势</h3><h4 id="1-开发者社区的规模"><a href="#1-开发者社区的规模" class="headerlink" title="1. 开发者社区的规模"></a>1. 开发者社区的规模</h4><p>大多数人会告诉你：React Native 相对 Flutter 的一个关键优势是其更加成熟，经验更丰富的开发者社区。<br>此外，就编程语言而言，至少在目前，Dart 还没有像 JavaScript 那样得到广泛使用。</p><p>实际上，要想以 “哥哥” 相称，Flutter 有很多工作要做，这是可以理解的。<br>社区需要时间来教育其受众并获得更多的经验，这对于任何新的年轻的工具都是必经之路。</p><p>但是，我们并不认为这是一个明显的劣势。<br>同样值得注意的是，Flutter 社区正在成倍增长。</p><p>另外，开发套件包也是令人兴奋的。</p><p><strong>请注意：</strong></p><ul><li><p><a href="https://insights.stackoverflow.com/trends?tags=flutter">Stack Overflow Trends</a> 标签趋势表明开发人员对 Flutter 的兴趣大大增加</p></li><li><p>在 Udemy 上，有 50 多门 <a href="https://www.udemy.com/topic/google-flutter/">Flutter 课程</a>（约 213,000 个课程的参与者）</p><p>还有 35 项 <a href="https://www.udemy.com/topic/dart-programming-language/">Dart 课程</a>（约有 187,500 门课程的参与者（2019 年 6 月 25 日））</p><p><a href="https://www.udemy.com/topic/react-native/">React Native</a> 的数字大致相同——共有 597,000 门课程的 287,000 名学生</p></li><li><p>Flutter 有 11.5 万 Github stars；React Native 有 9.3 万</p></li></ul><p><strong>综上所述，Flutter 目前确实有一个较小的，经验不足的社区，而 Dart 比 JavaScript 更具优势。</strong></p><p><strong>尽管如此，目前的趋势表明，Flutter 将在这方面赶上竞争对手。</strong></p><h4 id="2-库和支持-令人印象深刻，但仍然没有像原生开发一样丰富"><a href="#2-库和支持-令人印象深刻，但仍然没有像原生开发一样丰富" class="headerlink" title="2. 库和支持 - 令人印象深刻，但仍然没有像原生开发一样丰富"></a>2. 库和支持 - 令人印象深刻，但仍然没有像原生开发一样丰富</h4><p>Google 对 Flutter 的支持令人印象深刻，但 Flutter 还是一个相当新的事物。<br>这意味着你无法始终在现有库中找到所需的功能，因此开发人员可能需要自己构建自定义功能，这需要时间。</p><h4 id="3-持续集成支持"><a href="#3-持续集成支持" class="headerlink" title="3. 持续集成支持"></a>3. 持续集成支持</h4><p>在撰写本文时，Flutter 缺少对 Travis 或 Jenkins 等 CI 平台的支持。<br>因此，要实现自动构建，测试和部署，你的开发人员需要使用和维护自定义脚本，<br>例如 <a href="https://github.com/yjbanov/flutter_travis_sample/blob/master/.travis.yml">这个</a>。</p><p>值得注意的是：</p><ul><li>Flutter Live 2018 宣布了适用于 Flutter 应用程序的新 CI&#x2F;CD 系统——<a href="https://codemagic.io/start/">Codemagic</a></li><li>2019 年 1 月，<a href="https://blog.bitrise.io/flutter-ci-on-bitrise-v1">Bitrise 发布了功能齐全的 Flutter CI</a></li></ul><h4 id="4-平台风险"><a href="#4-平台风险" class="headerlink" title="4. 平台风险"></a>4. 平台风险</h4><p>即使 Flutter 是开源的，但如果 Google 决定取消对该项目的支持，那将是一场灾难。</p><p>也就是说，自 Google 团队发布 Flutter 的 Beta 版以来，它一直在加大对 Flutter 的支持，<br>正如 Flutter 在 19 届 Google IO 期间的杰出作用以及最近的 Flutter Live 活动所表明的那样。</p><p>很难想像没有 Google 的 Flutter 会走向何方。</p><h4 id="5-应用大小"><a href="#5-应用大小" class="headerlink" title="5. 应用大小"></a>5. 应用大小</h4><p>用 Flutter 编写的应用程序比原生应用程序大。<br>请查看文章 <a href="https://android.jlelse.eu/comparing-apk-sizes-a0eb37bb36f">比较 APK 大小</a>。<br>但是，Flutter 团队正在研究 <a href="https://github.com/flutter/flutter/issues/16833#issuecomment-410103493">减小应用程序的大小</a>。</p><h2 id="React-Native-–-优势和劣势"><a href="#React-Native-–-优势和劣势" class="headerlink" title="React Native – 优势和劣势"></a>React Native – 优势和劣势</h2><h3 id="✅-React-Native-优势"><a href="#✅-React-Native-优势" class="headerlink" title="✅ React Native 优势"></a>✅ React Native 优势</h3><h4 id="1-热刷新-快速开发"><a href="#1-热刷新-快速开发" class="headerlink" title="1. 热刷新 &#x3D; 快速开发"></a>1. 热刷新 &#x3D; 快速开发</h4><p>本质上与 Flutter 具有相同的功能。</p><p>热重载允许开发人员将新代码直接注入正在运行的应用程序中，从而加快了开发过程。<br>因此，开发人员可以立即看到更改，而无需重建应用程序。</p><p>热重装还保留了应用程序的状态，避免了在完全重装期间丢失它的风险（在基于状态的框架中是一项重要优势）—— 加快了移动应用程序的开发过程。</p><p>为了改善热重载的开发人员体验，React Native 团队在 0.61 版本中包括一项称为快速刷新的新功能，<br>该功能将实时重载和热重载相结合。<br>与以前的版本相比，它对错别字和错误的处理更具弹性。<br>您可以在 <a href="https://facebook.github.io/react-native/blog/2019/09/18/version-0.61#fast-refresh">此处</a> 阅读更多有关快速刷新的信息。</p><h4 id="2-一处编写-处处运行"><a href="#2-一处编写-处处运行" class="headerlink" title="2. 一处编写, 处处运行"></a>2. 一处编写, 处处运行</h4><ul><li><p>与 Flutter 完全相同：编写单个代码库为 2 个应用程序提供动力 —— 涵盖 Android 和 iOS 平台。</p><p>更好的是，JavaScript 通过与 <a href="https://www.thedroidsonroids.com/services/web-app-development">Web 应用程序</a><br>共享代码来编写跨平台应用程序时，你也可以一臂之力。这是通过创建可编译为目标平台的抽象组件来完成的。</p></li></ul><p>参见下面的示例库，这些库可让你在 iOS 和 Android 以外的平台（包括 Web 和桌面应用程序）上同时创建代码：</p><ul><li><a href="https://github.com/necolas/react-native-web">React Native for Web</a> – 支持 Android，iOS 和 Web（Twitter 使用它来创建 Twitter Lite）</li><li><a href="https://github.com/microsoft/reactxp">ReactXp</a> – 由 Skype 团队开发，以支持 Android，iOS 和 Web；另外，可在 Windows 10（UWP）上使用</li><li><a href="https://github.com/microsoft/react-native-windows">react-native-windows</a> – 由 Microsoft 团队开发，支持 Windows 10 支持的所有设备（PC，平板电脑，2 合 1 电脑），Xbox，混合现实设备等）</li></ul><p><strong>顺便一提</strong></p><p>Bartosz（我们的 React Native 开发人员）对事物的看法与大多数事物有所不同，因此更喜欢使用替代方法。</p><p>如果将 Web 桌面应用程序，移动 Web 应用程序和原生应用程序进行比较，则可以假定它们共享相同的业务逻辑，但它们可能需要满足不同用户需求的特定 UI。</p><p>所以，为什么不呢？</p><ol><li>将共享代码提取到单独的存储库中；</li><li>将其视为一个单独的项目；</li><li>是否以与其他依赖项相同的方式注入代码？</li></ol><p>通过这种方式，开发人员可以专注于将应用程序编写到实际平台上，而不必考虑跨平台兼容性。</p><p>观看 <a href="https://www.youtube.com/watch?v=IA_M2ESWoXw">Ben Ellerby 的演示文稿</a> 了解该方法，如果你喜欢在平台上创建抽象的想法，请查看 <a href="https://www.youtube.com/watch?v=ryMvNklnDjU">Radek Pietruszewski 的演示文稿</a>。</p><h4 id="3-它使用广泛流行的语言-——-JavaScript"><a href="#3-它使用广泛流行的语言-——-JavaScript" class="headerlink" title="3. 它使用广泛流行的语言 —— JavaScript"></a>3. 它使用广泛流行的语言 —— JavaScript</h4><p>React Native 使用 JavaScript：许多开发人员都熟悉的一种编程语言（而 Dart 仍然不那么广为人知或使用）。而且，如果你是喜欢静态类型的编程语言的开发人员，则甚至可以使用 <a href="https://www.typescriptlang.org/">TypeScript</a> —— 一个 JavaScript 超集。</p><h4 id="4-开发人员的选择自由"><a href="#4-开发人员的选择自由" class="headerlink" title="4. 开发人员的选择自由"></a>4. 开发人员的选择自由</h4><p>React Native 使开发人员可以构建跨平台应用程序。不做更多事情。</p><p>优点是 React Native 允许开发人员精确地决定他们要使用什么解决方案； 两者都是根据项目的要求以及开发人员的偏好进行的。</p><p>假设，如果开发人员需要决定如何处理全局状态（如何存储和管理应用程序中许多组件中使用的数据），请选择路由器库，或者在 JavaScript 和 TypeScript 之间进行选择——他们可以决定是否使用自定义 UI 库，或者自己编写。</p><h4 id="5-相对成熟度"><a href="#5-相对成熟度" class="headerlink" title="5. 相对成熟度"></a>5. 相对成熟度</h4><p>官方的 React Native 版本是 5 年前发布的，因此 Facebook 团队有足够的时间来稳定 API，以及专注于解决问题和解决问题。</p><p>现在，他们正在努力进行一些令人兴奋的改进——例如减小应用程序的大小。</p><h4 id="6-一个活跃而广阔的社区"><a href="#6-一个活跃而广阔的社区" class="headerlink" title="6. 一个活跃而广阔的社区"></a>6. 一个活跃而广阔的社区</h4><p>React Native 有一个庞大的开发者社区。不仅如此，还有 <strong>无数的教程，库和 UI 框架</strong>，它们使学习该技术变得容易，并且可以快速，轻松地进行开发。</p><p>而且，如果你比较专注于收集有关特定技术的文章，工具和材料的存储库，则 React Native 的位置要比 Xamarin，Flutter 或 Ionic 更好（来源：<a href="https://github.com/Solido/awesome-flutter">Awesome-Flutter</a>，<a href="https://github.com/jondot/awesome-react-native">Awesome-ReactNative</a>，<a href="https://github.com/Alexintosh/Awesome-Ionic">Awesome-Ionic</a>，<a href="https://github.com/XamSome/awesome-xamarin">Awesome-Xamarin</a>。</p><p>而且，React Native 是 **“ React 家族” ** 的一部分。</p><ul><li>还值得引起你注意还有 <a href="https://expo.io/">Expo</a> —— 一种简化了对原生 API 的访问的 React 构造框架，该框架具有针对典型移动功能（例如，推送通知）的现成解决方案。</li><li>值得一提的第二个库是 <a href="https://aws-amplify.github.io/">AWS Amplify</a>：该解决方案通过涵盖身份验证，存储，推送通知和分析来简化与 AWS 功能的集成。</li></ul><h4 id="7-易于学习"><a href="#7-易于学习" class="headerlink" title="7. 易于学习"></a>7. 易于学习</h4><p>我们列表中的这一优势是针对 React 开发人员的。<br><strong>如果您具有 Web 开发背景并且已经使用了流行的 React 解决方案，则可以轻松使用 React Native</strong> ，<br>而无需学习新的库。 您可以使用相同的库，工具和模式。</p><h4 id="8-最多减少-50％-的测试"><a href="#8-最多减少-50％-的测试" class="headerlink" title="8. 最多减少 50％ 的测试"></a>8. 最多减少 50％ 的测试</h4><p>设你在两个平台上都有相同的应用程序，则质量保证流程会更快更少的测试。</p><p>我们编写的自动化测试大约减少 50％，因为我们可以创建相同的测试以在两个平台上运行，从而减少了对 QA 团队的需求。</p><p>也就是说，你仍然必须在与原生编程类似的级别上运行手动测试，因为你的质量检查专家必须手动检查每个平台上的两个应用程序。</p><h3 id="🔻-React-Native-劣势"><a href="#🔻-React-Native-劣势" class="headerlink" title="🔻 React Native 劣势"></a>🔻 React Native 劣势</h3><h5 id="1-只是接近原生而已"><a href="#1-只是接近原生而已" class="headerlink" title="1. 只是接近原生而已"></a>1. 只是接近原生而已</h5><p>和其他跨平台解决方案一样，不管是性能还是用户体验都只是接近原生而已。</p><p>但是，与 Flutter 相比，使用 React Native 更容易获得自然的感觉。 如果你希望 Flutter 应用程序具有原生组件，则需要进行其他工作。</p><h4 id="2-开箱即用的组件少"><a href="#2-开箱即用的组件少" class="headerlink" title="2. 开箱即用的组件少"></a>2. 开箱即用的组件少</h4><p><strong>React Native 开箱只支持基础组件</strong> (许多都是针对特定平台开箱即用，例如按钮，加载指示器或滑块).</p><p>外部存储库包含许多其他 React Native 组件。 开发人员可以在项目中使用它们，但这需要额外的精力和时间。</p><p>另一方面，Flutter 旨在支持开箱即用的 Material Design，因此该框架支持更多的 widget 来节省开发者时间。<br>使用 Flutter 的开发人员可以使用易于定制且跨平台一致的预制窗口 widget 来创建大多数视图。</p><h4 id="3-开发人员的选择自由"><a href="#3-开发人员的选择自由" class="headerlink" title="3. 开发人员的选择自由"></a>3. 开发人员的选择自由</h4><p>既是诅咒又是祝福。</p><p>开发人员创建新项目后，他们需要确定要使用哪个导航包以及哪个全局状态管理。</p><p>了解每个解决方案的细微差别，最终决定要用于项目的最佳解决方案，可能需要花费大量时间。</p><h4 id="4-很多被遗弃的包"><a href="#4-很多被遗弃的包" class="headerlink" title="4. 很多被遗弃的包"></a>4. 很多被遗弃的包</h4><p>React Native 拥有大量的库。不幸的是，它们太多要么质量低下，要么完全被遗弃。</p><p>Dan Abramov 建议检查存储库中的问题数量和提交频率，以防止使用废弃的程序包。</p><p>更多信息请查看 <a href="https://github.com/react-native-community/discussions-and-proposals/issues/104">summary of his discussion of “What do you dislike about React Native?”</a></p><h4 id="5-脆弱的用户界面"><a href="#5-脆弱的用户界面" class="headerlink" title="5. 脆弱的用户界面"></a>5. 脆弱的用户界面</h4><p>React Native 在后台使用原生组件这一事实应该使你充满信心，<strong>每次 OS UI 更新后，你的应用程序组件也将立即升级</strong>。</p><p>也就是说 <strong>这可能会破坏应用的用户界面，但这种情况很少发生。</strong></p><p>更糟糕的是，如果更新会导致 Native Components API 发生某些更改，则更新会变得更加危险。 好消息是这种事件很少发生。</p><p>而就 Flutter 而言（由于框架自行重新创建原生组件的方式），应用程序 UI 更加稳定。</p><h4 id="6-应用尺寸大于原生"><a href="#6-应用尺寸大于原生" class="headerlink" title="6. 应用尺寸大于原生"></a>6. 应用尺寸大于原生</h4><p>用 React Native 编写的应用程序必须能够运行 Javascript 代码（JavaScript 虚拟机）。<br>Android 默认情况下不具有此功能，这意味着应用程序需要包含一个支持 JavaScript 代码的库，从而导致应用程序比其原生 Android 同类产品更大。</p><p>使用 React Native 制作的 <a href="https://www.thedroidsonroids.com/services/ios-mobile-app-development">iOS 应用</a> 并没有这个问题，但是它们通常比原生的更大。 好消息是正如我们之前提到的，React Native 团队正在 <a href="https://github.com/react-native-community/discussions-and-proposals/issues/6">减小应用程序的大小</a>。</p><p>阅读有关 <a href="https://www.thedroidsonroids.com/blog/react-native-pros-and-cons">2021 年为应用程序所有者进行 React Native 开发的利弊</a><br>的更多信息。</p><h2 id="预测未来：Flutter＆React-Native"><a href="#预测未来：Flutter＆React-Native" class="headerlink" title="预测未来：Flutter＆React Native"></a>预测未来：Flutter＆React Native</h2><p><strong>越来越多的公司被 Flutter 所吸引。</strong> 毕竟，随着 Google 不断完善其工具，我们正在见证 Flutter SDK 的每月改进。 <strong>另外，社区始终是乐于助人和热情的</strong>。 而且，我们可以期待 Flutter 很快将使我们不仅可以创建移动应用程序，还可以创建用于 Web 和桌面的应用程序。</p><p>鉴于像阿里巴巴这样的领先公司已经在使用 Flutter，它的未来看起来很有希望。</p><p>至于 <strong>React Native</strong>，不错，Facebook 当前正在致力于 <strong>该技术的大规模重新架构</strong>。</p><p>团队正在尽最大努力改善对 React Native 用户和更广泛社区的支持。<br>有了这个，社区现在可以通过使用 <a href="https://github.com/react-native-community/discussions-and-proposals/issues/">专用 GitHub 存储库</a> 关注进度。</p><p>此类体系结构改进的实际结果是：</p><ul><li><p><a href="https://engineering.fb.com/android/hermes/">Hermes</a> ：针对移动应用程序优化的开源 JavaScript 引擎，可缩短交互时间，并降低应用程序大小和内存利用率</p></li><li><p>0.60 版本中的重大更改（<a href="https://facebook.github.io/react-native/blog/2019/07/03/version-60%EF%BC%89">https://facebook.github.io/react-native/blog/2019/07/03/version-60）</a> - 默认情况下，使用最受欢迎的依赖项管理器 CocoaPods 可以更轻松地管理 iOS 依赖项， - 你可以将 React Native 迁移到 AndroidX， - 你可以在精益核心流程中提取可选功能。</p></li></ul><p><strong>创建一个开放的环境来讨论 React Native 是重要的一步。</strong> 这既是不断改进的标志，又是该技术光明前景的信号。</p><p>由于 React Native 在市场上具有如此稳定的地位并处于持续发展的轨道上，我们不太可能很快看到该工具包被遗弃。 阅读有关 React Native 的 <a href="https://facebook.github.io/react-native/blog/2018/11/01/oss-roadmap">长期愿景</a>。</p><p>尽管如此，Flutter 还是 React Native 的强大竞争对手。</p><p>另请阅读：<a href="https://www.thedroidsonroids.com/blog/benefits-outsourcing-software-development">外包软件开发 - 应用程序所有者的收益和风险</a></p>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/eee38519bec9/</id>
    <link href="https://blog.zisheng.pro/posts/eee38519bec9/"/>
    <published>2021-04-19T09:50:36.000Z</published>
    <summary>从 Flutter 诞生之日起，React Native 就一直被拿来与之作比较，从谷歌超过 700 万的相关搜索结果中可以一瞥这场跨端王位之争的激烈程度。本文不像我翻译的标题一样在捧一踩一，而是较客观地对两者进行评价并给出实际需求中如何抉择。</summary>
    <title>2021 Flutter 和 React Native 如何选</title>
    <updated>2026-09-18T08:15:28.629Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="加密货币" scheme="https://blog.zisheng.pro/categories/%E5%8A%A0%E5%AF%86%E8%B4%A7%E5%B8%81/"/>
    <category term="区块链" scheme="https://blog.zisheng.pro/categories/%E5%8A%A0%E5%AF%86%E8%B4%A7%E5%B8%81/%E5%8C%BA%E5%9D%97%E9%93%BE/"/>
    <category term="MetaMask" scheme="https://blog.zisheng.pro/tags/MetaMask/"/>
    <category term="BSC" scheme="https://blog.zisheng.pro/tags/BSC/"/>
    <category term="区块链" scheme="https://blog.zisheng.pro/tags/%E5%8C%BA%E5%9D%97%E9%93%BE/"/>
    <category term="钱包" scheme="https://blog.zisheng.pro/tags/%E9%92%B1%E5%8C%85/"/>
    <category term="小狐狸" scheme="https://blog.zisheng.pro/tags/%E5%B0%8F%E7%8B%90%E7%8B%B8/"/>
    <content>
      <![CDATA[<h2 id="下载"><a href="#下载" class="headerlink" title="下载"></a>下载</h2><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images/202111232204640.png" alt="小狐狸钱包（MetaMask）教程"></p><h2 id="开始"><a href="#开始" class="headerlink" title="开始"></a>开始</h2><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images/202111232202734.png" alt="小狐狸钱包（MetaMask）教程"></p><h2 id="创建钱包"><a href="#创建钱包" class="headerlink" title="创建钱包"></a>创建钱包</h2><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images/202111232204544.png" alt="创建小狐狸钱包"></p><h2 id="同意"><a href="#同意" class="headerlink" title="同意"></a>同意</h2><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images/202111232205121.png" alt="小狐狸钱包（MetaMask）教程"></p><h2 id="创建密码"><a href="#创建密码" class="headerlink" title="创建密码"></a>创建密码</h2><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images/202111232206560.png" alt="创建小狐狸钱包密码"></p><p><img src="https://cdn.jsdelivr.net/gh/youngjuning/images/202111232206713.png" alt="创建小狐狸钱包密码"></p><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ul><li><a href="https://metamask.io/">metamask</a></li><li><a href="https://academy.binance.com/zh/articles/an-introduction-to-binance-smart-chain-bsc">币安智能链 (Binance Smart Chain，简称 BSC) </a></li></ul>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/77349c019445/</id>
    <link href="https://blog.zisheng.pro/posts/77349c019445/"/>
    <published>2020-11-23T13:58:00.000Z</published>
    <summary>本文主要讲解了小狐狸钱包教程，具有详细的操作步骤，对你有一定的参考价值，需要的朋友可以参考下。</summary>
    <title>小狐狸钱包（MetaMask）教程</title>
    <updated>2026-09-18T08:15:28.640Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="工具" scheme="https://blog.zisheng.pro/categories/%E5%B7%A5%E5%85%B7/"/>
    <category term="git" scheme="https://blog.zisheng.pro/tags/git/"/>
    <category term="git-filter-repo" scheme="https://blog.zisheng.pro/tags/git-filter-repo/"/>
    <category term="git-filter-branch" scheme="https://blog.zisheng.pro/tags/git-filter-branch/"/>
    <category term="git-filter" scheme="https://blog.zisheng.pro/tags/git-filter/"/>
    <content>
      <![CDATA[<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>对于程序员来说，公司就像是工地，代码就是我们的钢筋水泥。那在工地什么最重要呢？没错就是安全生产。本文要讨论的是开发中的合规安全。GitHub 作为当代开源社区的基础设施，不仅是个人开发者在上面维护项目，很多企业也会将内部优秀项目选择开源反馈社区和扩大社区影响力。然后，就出现了公司拖欠工资，员工怒将内部代码开源、员工将敏感信息上传。有些信息虽然看起来不怎么重要，但是如果被不怀好意的黑客盯上，就没有一篇雪花是干净的。</p><p>那如果安全隐患已经存在，我们该如何补救呢？答案是使用 git-filter 能力对 git 记录进行合并、删除等操作。git-filter-repo 正是这类工具的集大成者。下面就让我们一起学习下这款强大的 git log 操作工具吧。</p><h2 id="git-filter-branch"><a href="#git-filter-branch" class="headerlink" title="git-filter-branch"></a>git-filter-branch</h2><p>git 自带的有一个 git-filter-branch 工具，我第一次使用是为了删除单个文件及其相关的 commit 记录。命令如下：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">git filter-branch --force --index-filter <span class="string">&#x27;git rm --cached --ignore-unmatch CHANGELOG.md&#x27;</span> --prune-empty --tag-name-filter <span class="built_in">cat</span> -- --all</span><br></pre></td></tr></table></figure><p>执行后，会收到如下的警告：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">WARNING: git-filter-branch has a glut of gotchas generating mangled <span class="built_in">history</span></span><br><span class="line">         rewrites.  Hit Ctrl-C before proceeding to abort, <span class="keyword">then</span> use an</span><br><span class="line">         alternative filtering tool such as <span class="string">&#x27;git filter-repo&#x27;</span></span><br><span class="line">         (https://github.com/newren/git-filter-repo/) instead.  See the</span><br><span class="line">         filter-branch manual page <span class="keyword">for</span> more details; to squelch this warning,</span><br><span class="line">         <span class="built_in">set</span> FILTER_BRANCH_SQUELCH_WARNING=1.</span><br></pre></td></tr></table></figure><p>翻译过来如下：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">警告：翻译过来如下：git-filter-branch有大量的问题，会产生错误的历史记录重写。</span><br><span class="line">        在继续进行之前按 Ctrl-C 中止，然后用一个替代的过滤工具，</span><br><span class="line">        如 https://github.com/newren/git-filter-repo/ 来代替。</span><br><span class="line">        参见 filter-branch 手册页了解更多细节。</span><br><span class="line">        要消除这个警告。请设置 FILTER_BRANCH_SQUELCH_WARNING=1。</span><br></pre></td></tr></table></figure><ul><li><a href="https://github.com/newren/git-filter-repo">git-filter-repo</a></li><li><a href="https://blog.csdn.net/q258523454/article/details/83899911">Github: 单独删除某个文件的所有历史记录</a></li><li><a href="https://serverfault.com/questions/1018302/warning-git-filter-branch-has-a-glut-of-gotchas-generating-mangled-history-rewr">WARNING: git-filter-branch has a glut of gotchas generating mangled history rewrites</a></li><li><a href="https://nyakku.moe/posts/2020/06/12/use-git-filter-repo-clean-git-history.html">使用 git-filter-repo 清理 git 历史记录</a></li><li><a href="https://www.vicw.com/groups/code_monkey/topics/362">使用git filter-repo清理git commit历史中的大文件</a></li></ul>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/6ead325700ca/</id>
    <link href="https://blog.zisheng.pro/posts/6ead325700ca/"/>
    <published>2020-11-15T11:55:00.000Z</published>
    <summary>对于程序员来说，公司就像是工地，代码就是我们的钢筋水泥。那在工地什么最重要呢？没错就是安全生产。本文要讨论的是开发中的合规安全。</summary>
    <title>使用 git-filter-branch 删除单个文件及其相关的 commit 信息</title>
    <updated>2026-09-18T08:15:28.636Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="漫谈" scheme="https://blog.zisheng.pro/categories/%E6%BC%AB%E8%B0%88/"/>
    <category term="年中总结" scheme="https://blog.zisheng.pro/tags/%E5%B9%B4%E4%B8%AD%E6%80%BB%E7%BB%93/"/>
    <category term="程序员" scheme="https://blog.zisheng.pro/tags/%E7%A8%8B%E5%BA%8F%E5%91%98/"/>
    <category term="杭州" scheme="https://blog.zisheng.pro/tags/%E6%9D%AD%E5%B7%9E/"/>
    <category term="紫升的博客" scheme="https://blog.zisheng.pro/tags/%E7%B4%AB%E5%8D%87%E7%9A%84%E5%8D%9A%E5%AE%A2/"/>
    <category term="紫升" scheme="https://blog.zisheng.pro/tags/%E7%B4%AB%E5%8D%87/"/>
    <category term="前端开发" scheme="https://blog.zisheng.pro/tags/%E5%89%8D%E7%AB%AF%E5%BC%80%E5%8F%91/"/>
    <category term="跨端开发" scheme="https://blog.zisheng.pro/tags/%E8%B7%A8%E7%AB%AF%E5%BC%80%E5%8F%91/"/>
    <content>
      <![CDATA[<h2 id="上半年总结"><a href="#上半年总结" class="headerlink" title="上半年总结"></a>上半年总结</h2><p>2020 的上半年注定是不平凡的一年，不管什么谈话基本都可以从疫情聊起：</p><ul><li>朋友A：你们公司复工了吗？还在家办公吗？</li><li>朋友B：你们工资及时发了吗？公司不会倒闭了吧？</li><li>某亲戚：今年这疫情弄得，办个婚礼都没人去，好多都推了。</li><li>面试官：你为什么从之前公司离职？<ul><li>我：教育行业，因为疫情拖欠了俩月工资</li></ul></li></ul><p>总的来说，疫情对我的直接影响还挺大。工作上来说，如果不是被拖欠工资，大概率不会这么快离职；生活上来说，订婚从2月份推迟到了4月底才办…</p><h2 id="工作"><a href="#工作" class="headerlink" title="工作"></a>工作</h2><p>上家公司是从 2018年3月干到2020年5月的，整整两年，写过小程序，负责过公众号、参与过基于element 和 vue 的后台程序开发、基于React Native开发过业务，搞过React Native 项目的架构。离职时，职位停留在了移动端开发负责人，实现了月薪过万的小目标。</p><p>离职原因一方面是因为薪资拖欠，另一方面是在公司也摸到了天花板。第二方面是最让我难受的，这天花板离我的心理预期太远。之前之所以一直拖着没跳槽是信了leader的核心人员以后不会亏待。长期的境况与心理预期不符不管是对公司还是对自己都是不负责任的，再加上疫情拖欠薪资，直接造成了房贷的还款困难（年前刚开始自己承担全部贷款），我最终下定决心离开工作了两年的公司。</p><p>再来说一下新工作吧，本人找工作的base是郑州，一个互联网三线城市。刚开始主动打招呼的和海投回应的都很多，9成都是 Vue 开发，甚至需要维护 jquery 项目。我心想还是主动投投 React Native 岗位吧，现实再次打了我的脸，郑州这边 RN 岗位两个手数的过来，并且范围都是 8-10K，甚至 4-8K。抱着快要放弃的心态，再次缩小范围投递了几家 React 技术栈的公司，最终实现了试用期工资高于上家的目标。</p><h2 id="生活"><a href="#生活" class="headerlink" title="生活"></a>生活</h2><p>在 4月初与小女友定亲啦，当时的沸点也得到了掘友的祝福，证明了掘金是一个有爱的有温度的技术社区。</p><h2 id="学习"><a href="#学习" class="headerlink" title="学习"></a>学习</h2><ol><li>看完了一个关于 Linux 的视频教程，并写下了<a href="https://juejin.im/post/5e81e2db518825737b4ad911">写给前端工程师的Linux实战教程【持续更新】</a>。</li><li>看完了《Docker从入门到实践》，并写下<a href="https://juejin.im/post/5e839f2851882573ab44f1b4">一个前端工程师的Docker学习笔记【持续更新】</a>，感谢掘金编写的推荐，获得了不少流量扶持。</li><li>工作需要，总结了React Native 开发经验并输出了<a href="https://juejin.im/post/5e535e0df265da576b566397">React Native 项目架构面面观【持续更新】</a>、<a href="https://juejin.im/post/5e50f3ace51d4526ef5f8d56">react-navigation 5.x 最佳实践</a> 等多篇相关博文，这几篇也成为了几十篇文章中阅读量和点赞最多的。果然我还是个只能靠实力的蓝孩子。这个系列还有更多的存货没有放出来。待我新工作转正继续输出。</li><li>新公司入职有两个月了，独立负责了两个不大的中台项目，技术栈是基于 umijs 的，而 umijs 基于 dva、antd、react-router，而 dva 又基于 redux。两个项目下来，我的水平停留在了熟练基于 umijs 开发业务。为后续研究源码和性能优化打下了基础。这方面的经验我写了一篇<a href="https://juejin.im/post/5ef1dd62f265da02981f0266">UmiJS开发技巧【持续更新】</a> 做了总结。</li></ol><h2 id="社区"><a href="#社区" class="headerlink" title="社区"></a>社区</h2><p>我现在混得社区比较少，一个是GitHub，一个是掘金。短期内也没计划再混其他的社区（不想精力分散太多）。</p><h3 id="掘金"><a href="#掘金" class="headerlink" title="掘金"></a>掘金</h3><p>从2月份到现在共发布了27篇博客，ps:之前分享的文章的流量看的我心疼（qaq）。</p><p>很多朋友都是通过<a href="https://juejin.im/post/5e535e0df265da576b566397">React Native 项目架构面面观【持续更新】</a>、<a href="https://juejin.im/post/5e50f3ace51d4526ef5f8d56">react-navigation 5.x 最佳实践</a> 这两篇文章认识我的，从推送来看，都是在点赞这两篇文章之后关注我。后期我也会继续输出跨平台相关的技术分享。</p><h3 id="GitHub"><a href="#GitHub" class="headerlink" title="GitHub"></a>GitHub</h3><p>假期的时候，我更新了<a href="https://github.com/youngjuning/wx-promise-pro">wx-promise-pro</a>，支持了TS，并优化了内部实现。更新之后到现在累计增加100star。这是一个微信小程序的增强插件，年前已被 <a href="https://hellogithub.com/periodical/volume/47/#wx-promise-pro">HelloGitHub 47期</a> 收录，有 20+ 的 github 库在使用，欢迎尝试。</p>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/1e22179cfc15/</id>
    <link href="https://blog.zisheng.pro/posts/1e22179cfc15/"/>
    <published>2020-07-15T21:06:51.000Z</published>
    <summary>怕什么真理无穷，进一寸有一寸的欢喜 | 掘金征文 上半年总结 2020 的上半年注定是不平凡的一年，不管什么谈话基本都可以从疫情聊起：  朋友A：你们公司复工了吗？还在家办公吗？ 朋友B：你们工资及时发了吗？公司不会倒闭了吧？ 某亲戚：今年这疫情弄得，办个婚礼都没人去，好多都推了。 面试官：你为什么从之前公司离职？  我：教育行业，因为疫情拖欠了俩月工资    总的来说，疫情对我的直接影响还挺大。...</summary>
    <title>2020 年中总结</title>
    <updated>2026-09-18T08:15:28.629Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="vant" scheme="https://blog.zisheng.pro/tags/vant/"/>
    <category term="微精通" scheme="https://blog.zisheng.pro/tags/%E5%BE%AE%E7%B2%BE%E9%80%9A/"/>
    <category term="vue" scheme="https://blog.zisheng.pro/tags/vue/"/>
    <content>
      <![CDATA[<p>精通任何技能都是需要时间的，但是往往工作中是不会给你这个时间的，尤其是开发工作。新技术层出不穷，每个项目都可能技术栈不一样。</p><p>无法改变环境，那么就只能改变我们自身。如果让公司等你精通再开发，那你只能被淘汰。但如果毫无准备进入开发，项目质量又无从谈起，而且项目也可能失控。而微精通就是框定一个最小范围，快速熟悉完成任务所涉及的内容。今天我就拿 Vue 移动端开发做一个实验。</p><h2 id="项目初始化"><a href="#项目初始化" class="headerlink" title="项目初始化"></a>项目初始化</h2><h3 id="创建项目"><a href="#创建项目" class="headerlink" title="创建项目"></a>创建项目</h3><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 安装 Vue Cli</span></span><br><span class="line">npm install -g @vue/cli</span><br><span class="line"></span><br><span class="line"><span class="comment"># 创建一个项目</span></span><br><span class="line">vue create zhiliao-vant</span><br></pre></td></tr></table></figure><h3 id="配置Prettier"><a href="#配置Prettier" class="headerlink" title="配置Prettier"></a>配置Prettier</h3><p>1、安装依赖</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ yarn add prettier eslint-plugin-prettier eslint-config-prettier -D</span><br></pre></td></tr></table></figure><ul><li>prettier: Prettier CLI</li><li>eslint-plugin-prettier: 以 ESLint 插件的形式运行 prettier</li><li>eslint-config-prettier: 关闭所有不必要或可能与 prettier 的规则冲突的 ESLint 规则。一定要放到最后。</li></ul><p>2、配置  <code>.eslintrc.js</code>:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="variable language_">module</span>.<span class="property">exports</span> = &#123;</span><br><span class="line">  <span class="attr">root</span>: <span class="literal">true</span>,</span><br><span class="line">  <span class="attr">env</span>: &#123;</span><br><span class="line">    <span class="attr">node</span>: <span class="literal">true</span>,</span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="attr">extends</span>: [</span><br><span class="line">    <span class="string">&#x27;plugin:vue/essential&#x27;</span>,</span><br><span class="line">    <span class="string">&#x27;@vue/airbnb&#x27;</span>,</span><br><span class="line">    <span class="string">&#x27;plugin:prettier/recommended&#x27;</span>,</span><br><span class="line">    <span class="string">&quot;prettier/vue&quot;</span></span><br><span class="line">  ],</span><br><span class="line">  <span class="attr">parserOptions</span>: &#123;</span><br><span class="line">    <span class="attr">parser</span>: <span class="string">&#x27;babel-eslint&#x27;</span>,</span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="attr">rules</span>: &#123;</span><br><span class="line">    <span class="string">&#x27;no-console&#x27;</span>: process.<span class="property">env</span>.<span class="property">NODE_ENV</span> === <span class="string">&#x27;production&#x27;</span> ? <span class="string">&#x27;warn&#x27;</span> : <span class="string">&#x27;off&#x27;</span>,</span><br><span class="line">    <span class="string">&#x27;no-debugger&#x27;</span>: process.<span class="property">env</span>.<span class="property">NODE_ENV</span> === <span class="string">&#x27;production&#x27;</span> ? <span class="string">&#x27;warn&#x27;</span> : <span class="string">&#x27;off&#x27;</span>,</span><br><span class="line">    <span class="comment">// add your rules</span></span><br><span class="line">  &#125;,</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>3、新建 <code>/.prettierrc.js</code>，并写入如下配置</p><blockquote><p>注意: 要不要加分号的原则是<strong>领导为大，喜好为小</strong></p></blockquote><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br></pre></td><td class="code"><pre><span class="line"><span class="variable language_">module</span>.<span class="property">exports</span> = &#123;</span><br><span class="line">  <span class="attr">printWidth</span>: <span class="number">120</span>, <span class="comment">// 一行的字符数，如果超过会进行换行，默认为80</span></span><br><span class="line">  <span class="attr">tabWidth</span>: <span class="number">2</span>, <span class="comment">// tab缩进大小,默认为2</span></span><br><span class="line">  <span class="attr">useTabs</span>: <span class="literal">false</span>, <span class="comment">// 使用tab缩进，默认false</span></span><br><span class="line">  <span class="attr">semi</span>: <span class="literal">false</span>, <span class="comment">// 使用分号, 默认true</span></span><br><span class="line">  <span class="comment">/**</span></span><br><span class="line"><span class="comment">   * 行尾逗号,默认none,可选 none|es5|all</span></span><br><span class="line"><span class="comment">   * es5 包括es5中的数组、对象</span></span><br><span class="line"><span class="comment">   * all 包括函数对象等所有可选</span></span><br><span class="line"><span class="comment">   */</span></span><br><span class="line">  <span class="attr">trailingComma</span>: <span class="string">&#x27;es5&#x27;</span>,</span><br><span class="line">  <span class="attr">singleQuote</span>: <span class="literal">true</span>, <span class="comment">// 使用单引号, 默认false(在jsx中配置无效, 默认都是双引号)</span></span><br><span class="line">  <span class="comment">/**</span></span><br><span class="line"><span class="comment">   * 对象中的空格 默认true</span></span><br><span class="line"><span class="comment">   * true: &#123; foo: bar &#125;</span></span><br><span class="line"><span class="comment">   * false: &#123;foo: bar&#125;</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">/**</span></span><br><span class="line"><span class="comment">   * JSX标签闭合位置 默认false</span></span><br><span class="line"><span class="comment">   * false:</span></span><br><span class="line"><span class="comment">   * &lt;div</span></span><br><span class="line"><span class="comment">   *  className=&quot;&quot;</span></span><br><span class="line"><span class="comment">   *  style=&#123;&#123;&#125;&#125;</span></span><br><span class="line"><span class="comment">   * &gt;</span></span><br><span class="line"><span class="comment">   * true:</span></span><br><span class="line"><span class="comment">   * &lt;div</span></span><br><span class="line"><span class="comment">   *  className=&quot;&quot;</span></span><br><span class="line"><span class="comment">   * style=&#123;&#123;&#125;&#125; &gt;</span></span><br><span class="line"><span class="comment">   */</span></span><br><span class="line">  <span class="attr">jsxBracketSameLine</span>: <span class="literal">false</span>,</span><br><span class="line">  <span class="comment">/**</span></span><br><span class="line"><span class="comment">   * 箭头函数参数括号 默认avoid 可选 avoid| always</span></span><br><span class="line"><span class="comment">   * avoid 能省略括号的时候就省略 例如x =&gt; x</span></span><br><span class="line"><span class="comment">   * always 总是有括号</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="attr">vueIndentScriptAndStyle</span>: <span class="literal">false</span>, <span class="comment">// vue 文件 script 和 style 标签缩进，默认false</span></span><br><span class="line">  <span class="attr">endOfLine</span>: <span class="string">&#x27;lf&#x27;</span>, <span class="comment">// 强制使用 unix 风格的换行符</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="vscode-配置"><a href="#vscode-配置" class="headerlink" title="vscode 配置"></a>vscode 配置</h3><p>新建 <code>/.vscode/settings.json</code> 并写入以下配置：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;editor.formatOnSave&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">false</span></span><span class="punctuation">,</span> <span class="comment">// 关闭保存时自动格式化，防止与 eslint 冲突</span></span><br><span class="line">  <span class="attr">&quot;files.eol&quot;</span><span class="punctuation">:</span> <span class="string">&quot;\n&quot;</span><span class="punctuation">,</span> <span class="comment">// 统一默认行尾字符为 LF</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.eslint&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span> <span class="comment">// 保存时自动修复</span></span><br><span class="line">  <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="comment">// 关闭 vetur 验证功能</span></span><br><span class="line">  <span class="attr">&quot;vetur.validation.script&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">false</span></span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;vetur.validation.style&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">false</span></span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;vetur.validation.template&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">false</span></span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;vetur.format.enable&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">false</span></span><span class="punctuation">,</span></span><br><span class="line">  <span class="comment">// 禁用 prettier 插件，避免与 eslint 冲突，建议删除 vscode-prettier</span></span><br><span class="line">  <span class="attr">&quot;prettier.disableLanguages&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">    <span class="string">&quot;javascript&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="string">&quot;javascriptreact&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="string">&quot;typescript&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="string">&quot;typescriptreact&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="string">&quot;vue&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="string">&quot;json&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="string">&quot;jsonc&quot;</span></span><br><span class="line">  <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><blockquote><p>注意：如果是团队协作的项目，请删除 <code>.gitignire</code> 中的 <code>.vscode</code> ，将配置加入到代码库。</p></blockquote><h3 id="Format初始代码"><a href="#Format初始代码" class="headerlink" title="Format初始代码"></a>Format初始代码</h3><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ yarn lint</span><br></pre></td></tr></table></figure><h2 id="vue-cli"><a href="#vue-cli" class="headerlink" title="@vue&#x2F;cli"></a>@vue&#x2F;cli</h2><h3 id="vuerc"><a href="#vuerc" class="headerlink" title="~&#x2F;.vuerc"></a>~&#x2F;.vuerc</h3><p>被保存的 preset 将会存在用户的 home 目录下一个名为 <code>.vuerc</code> 的 JSON 文件里。如果你想要修改被保存的 <code>preset/</code> 选项，可以编辑这个文件。</p><p>在项目创建的过程中，你也会被提示选择喜欢的包管理器或使用<a href="https://npm.taobao.org/">淘宝 npm 镜像源</a>以更快地安装依赖。这些选择也将会存入 <code>~/.vuerc</code>。下面是我的配置：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;useTaobaoRegistry&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">false</span></span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;packageManager&quot;</span><span class="punctuation">:</span> <span class="string">&quot;yarn&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;presets&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</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;useConfigFiles&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;plugins&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">        <span class="attr">&quot;@vue/cli-plugin-babel&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span><span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;@vue/cli-plugin-router&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">          <span class="attr">&quot;historyMode&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">false</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;@vue/cli-plugin-vuex&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span><span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;@vue/cli-plugin-eslint&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">          <span class="attr">&quot;config&quot;</span><span class="punctuation">:</span> <span class="string">&quot;airbnb&quot;</span><span class="punctuation">,</span></span><br><span class="line">          <span class="attr">&quot;lintOn&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">            <span class="string">&quot;save&quot;</span><span class="punctuation">,</span></span><br><span class="line">            <span class="string">&quot;commit&quot;</span></span><br><span class="line">          <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 class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;cssPreprocessor&quot;</span><span class="punctuation">:</span> <span class="string">&quot;less&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">&#125;</span></span><br></pre></td></tr></table></figure><h3 id="git-hooks"><a href="#git-hooks" class="headerlink" title="git hooks"></a>git hooks</h3><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;gitHooks&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;pre-commit&quot;</span><span class="punctuation">:</span> <span class="string">&quot;lint-staged&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;lint-staged&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;*.&#123;js,jsx,vue&#125;&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">      <span class="string">&quot;vue-cli-service lint&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="string">&quot;git add&quot;</span></span><br><span class="line">    <span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;*.&#123;md,json&#125;&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">      <span class="string">&quot;prettier --write&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="string">&quot;git add&quot;</span></span><br><span class="line">    <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><h3 id="常见问题"><a href="#常见问题" class="headerlink" title="常见问题"></a>常见问题</h3><h4 id="vue-cli3出现Invalid-Host-header的解决方案"><a href="#vue-cli3出现Invalid-Host-header的解决方案" class="headerlink" title="vue-cli3出现Invalid Host header的解决方案"></a>vue-cli3出现Invalid Host header的解决方案</h4><blockquote><p>参考: <a href="https://blog.csdn.net/guzhao593/article/details/85918869">vue-cli3出现Invalid Host header的解决方案</a></p></blockquote><p><strong>产生原因</strong></p><p>新版的 <code>webpack-dev-server</code> 增加了安全验证，默认检查<code>hostname</code>，如果<code>hostname</code>不是配置内的，将中断访问。</p><p><strong>解决方案</strong></p><p>对<code>vue.config.js</code>进行如下配置：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="variable language_">module</span>.<span class="property">exports</span> = &#123;</span><br><span class="line">  <span class="attr">devServer</span>: &#123;</span><br><span class="line">    <span class="attr">disableHostCheck</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><h2 id="Vant-UI"><a href="#Vant-UI" class="headerlink" title="Vant UI"></a>Vant UI</h2><h3 id="安装依赖"><a href="#安装依赖" class="headerlink" title="安装依赖"></a>安装依赖</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ yarn add vant</span><br></pre></td></tr></table></figure><h3 id="按需引入组件"><a href="#按需引入组件" class="headerlink" title="按需引入组件"></a>按需引入组件</h3><p>安装 <code>babel-import-plugin</code></p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ yarn add babel-plugin-import -D</span><br></pre></td></tr></table></figure><p>对于使用 babel7 的用户，可以在 <code>babel.config.js</code> 中配置</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="variable language_">module</span>.<span class="property">exports</span> = &#123;</span><br><span class="line">  <span class="attr">presets</span>: [<span class="string">&#x27;@vue/cli-plugin-babel/preset&#x27;</span>],</span><br><span class="line">  <span class="attr">plugins</span>: [</span><br><span class="line">    [</span><br><span class="line">      <span class="string">&#x27;import&#x27;</span>,</span><br><span class="line">      &#123;</span><br><span class="line">        <span class="attr">libraryName</span>: <span class="string">&#x27;vant&#x27;</span>,</span><br><span class="line">        <span class="attr">libraryDirectory</span>: <span class="string">&#x27;es&#x27;</span>,</span><br><span class="line">        <span class="attr">style</span>: <span class="literal">true</span>,</span><br><span class="line">      &#125;,</span><br><span class="line">      <span class="string">&#x27;vant&#x27;</span>,</span><br><span class="line">    ],</span><br><span class="line">  ],</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>接着你可以在代码中直接引入 Vant 组件：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><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;vant&#x27;</span>;</span><br><span class="line"><span class="title class_">Vue</span>.<span class="title function_">use</span>(<span class="title class_">Button</span>);</span><br></pre></td></tr></table></figure><h3 id="配置基于-Rem-的适配方案"><a href="#配置基于-Rem-的适配方案" class="headerlink" title="配置基于 Rem 的适配方案"></a>配置基于 Rem 的适配方案</h3><p>Vant 中的样式默认使用<code>px</code>作为单位，如果需要使用<code>rem</code>单位，推荐使用以下两个工具：</p><ul><li><a href="https://github.com/cuth/postcss-pxtorem">postcss-pxtorem</a> 是一款 postcss 插件，用于将单位转化为 rem</li><li><a href="https://github.com/amfe/lib-flexible">lib-flexible</a> 用于设置 rem 基准值</li></ul><p>1、安装依赖：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">$ yarn add amfe-flexible</span><br><span class="line">$ yarn add postcss-pxtorem -D</span><br></pre></td></tr></table></figure><p>2、在根目录新建 <code>postcss.config.js</code>，并写入以下配置：</p><blockquote><p>参考: <a href="https://github.com/youzan/vant/issues/1181">设计稿是750px，根元素应该设置75，但是vant转换后好小，要改成35才行</a>、<a href="https://www.cnblogs.com/yimei/p/11319657.html">使用vue vantUi框架 根字体是37.5 和默认根字体75不一致，导致页面组件样式变小</a></p></blockquote><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="variable language_">module</span>.<span class="property">exports</span> = <span class="function">(<span class="params">&#123; file &#125;</span>) =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">const</span> designWidth = file.<span class="property">dirname</span>.<span class="title function_">includes</span>(<span class="string">&#x27;node_modules/vant&#x27;</span>) ? <span class="number">37.5</span> : <span class="number">75</span></span><br><span class="line">  <span class="keyword">return</span> &#123;</span><br><span class="line">    <span class="attr">plugins</span>: &#123;</span><br><span class="line">      <span class="attr">autoprefixer</span>: &#123;</span><br><span class="line">        <span class="attr">overrideBrowserslist</span>: [<span class="string">&#x27;Android 4.1&#x27;</span>, <span class="string">&#x27;iOS 7.1&#x27;</span>, <span class="string">&#x27;Chrome &gt; 31&#x27;</span>, <span class="string">&#x27;ff &gt; 31&#x27;</span>, <span class="string">&#x27;ie &gt;= 8&#x27;</span>],</span><br><span class="line">      &#125;,</span><br><span class="line">      <span class="string">&#x27;postcss-pxtorem&#x27;</span>: &#123;</span><br><span class="line">        <span class="attr">rootValue</span>: designWidth,</span><br><span class="line">        <span class="attr">propList</span>: [<span class="string">&#x27;*&#x27;</span>, <span class="string">&#x27;!border&#x27;</span>],</span><br><span class="line">        <span class="attr">selectorBlackList</span>: [<span class="string">&#x27;.ignore&#x27;</span>, <span class="string">&#x27;.hairlines&#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></pre></td></tr></table></figure><blockquote><p>注意: 你可以使用 <code>Px</code> 或 <code>PX</code> 来让 <code>postcss-pxtorem</code> 忽略转换，而且这样浏览器也能识别。</p></blockquote><p>3、在 <code>src/main.js</code> 中引入 <code>amfe-flexible</code>：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">...</span><br><span class="line">import &#x27;amfe-flexible&#x27;</span><br><span class="line">...</span><br></pre></td></tr></table></figure><h3 id="底部安全区适配"><a href="#底部安全区适配" class="headerlink" title="底部安全区适配"></a>底部安全区适配</h3><p>iPhone X 等机型底部存在底部指示条，指示条的操作区域与页面底部存在重合，容易导致用户误操作，因此我们需要针对这些机型进行底部安全区适配。Vant 中部分组件提供了<code>safe-area-inset-bottom</code>属性，设置该属性后，即可在对应的机型上开启适配，如下示例：</p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">&lt;!-- 在 head 标签中添加 meta 标签，并设置 viewport-fit=cover 值 --&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">meta</span></span></span><br><span class="line"><span class="tag">  <span class="attr">name</span>=<span class="string">&quot;viewport&quot;</span></span></span><br><span class="line"><span class="tag">  <span class="attr">content</span>=<span class="string">&quot;width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no, viewport-fit=cover&quot;</span></span></span><br><span class="line"><span class="tag">/&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="comment">&lt;!-- 开启 safe-area-inset-bottom 属性 --&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">van-number-keyboard</span> <span class="attr">safe-area-inset-bottom</span> /&gt;</span></span><br></pre></td></tr></table></figure><h3 id="配置自定义主题色方案"><a href="#配置自定义主题色方案" class="headerlink" title="配置自定义主题色方案"></a>配置自定义主题色方案</h3><h4 id="1、按需引入样式"><a href="#1、按需引入样式" class="headerlink" title="1、按需引入样式"></a>1、按需引入样式</h4><p>在 <code>babel.config.js</code> 中配置按需引入样式源文件:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="variable language_">module</span>.<span class="property">exports</span> = &#123;</span><br><span class="line">  <span class="attr">plugins</span>: [</span><br><span class="line">    [</span><br><span class="line">      <span class="string">&#x27;import&#x27;</span>,</span><br><span class="line">      &#123;</span><br><span class="line">        <span class="attr">libraryName</span>: <span class="string">&#x27;vant&#x27;</span>,</span><br><span class="line">        <span class="attr">libraryDirectory</span>: <span class="string">&#x27;es&#x27;</span>,</span><br><span class="line">        <span class="comment">// 指定样式路径</span></span><br><span class="line">        <span class="attr">style</span>: <span class="function">(<span class="params">name</span>) =&gt;</span> <span class="string">`<span class="subst">$&#123;name&#125;</span>/style/less`</span>,</span><br><span class="line">      &#125;,</span><br><span class="line">      <span class="string">&#x27;vant&#x27;</span>,</span><br><span class="line">    ],</span><br><span class="line">  ],</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 shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">yarn add less less-loader</span></span><br></pre></td></tr></table></figure><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// vue.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">css</span>: &#123;</span><br><span class="line">    <span class="attr">loaderOptions</span>: &#123;</span><br><span class="line">      <span class="attr">less</span>: &#123;</span><br><span class="line">        <span class="attr">modifyVars</span>: &#123;</span><br><span class="line">          <span class="comment">// 直接覆盖变量</span></span><br><span class="line">          <span class="string">&#x27;text-color&#x27;</span>: <span class="string">&#x27;#111&#x27;</span>,</span><br><span class="line">          <span class="string">&#x27;border-color&#x27;</span>: <span class="string">&#x27;#eee&#x27;</span>,</span><br><span class="line">          <span class="comment">// 或者可以通过 less 文件覆盖（文件路径为绝对路径）</span></span><br><span class="line">          <span class="attr">hack</span>: <span class="string">&#x27;true; @import &quot;your-less-file-path.less&quot;;&#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><blockquote><p>Vant 使用了 <a href="http://lesscss.org/">Less</a> 对样式进行预处理，并内置了一些样式变量，下面是一些基本的样式变量，所有可用的颜色变量请参考 <a href="https://github.com/youzan/vant/blob/dev/src/style/var.less">配置文件</a>。</p></blockquote><h3 id="配置基于-Viewport-的适配方案（推荐）"><a href="#配置基于-Viewport-的适配方案（推荐）" class="headerlink" title="配置基于 Viewport 的适配方案（推荐）"></a>配置基于 Viewport 的适配方案（推荐）</h3><p>该方案和<strong>配置基于 Rem 的适配方案</strong>是互斥的，请二选一。</p><blockquote><p>参考: <a href="https://my.oschina.net/u/4382386/blog/4290707">移动端布局之postcss-px-to-viewport（兼容vant）</a>、<a href="https://www.cnblogs.com/cnloop/p/9697229.html">vue —— 利用 viewport 进行适配</a></p></blockquote><p>1、安装 postcss-px-to-viewport</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">yarn add postcss-px-to-viewport -D</span></span><br></pre></td></tr></table></figure><p>2、配置<code> postcss.config.js</code> 文件</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="variable language_">module</span>.<span class="property">exports</span> = <span class="function">(<span class="params">&#123; file &#125;</span>) =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">const</span> designWidth = file.<span class="property">dirname</span>.<span class="title function_">includes</span>(<span class="string">&#x27;node_modules/vant&#x27;</span>) ? <span class="number">375</span> : <span class="number">750</span>;</span><br><span class="line">  <span class="keyword">return</span> &#123;</span><br><span class="line">    <span class="attr">plugins</span>: &#123;</span><br><span class="line">      <span class="attr">autoprefixer</span>: &#123;</span><br><span class="line">        <span class="comment">// 用来给不同的浏览器自动添加相应前缀，如-webkit-，-moz-等等</span></span><br><span class="line">        <span class="attr">overrideBrowserslist</span>: [</span><br><span class="line">          <span class="string">&#x27;Android 4.1&#x27;</span>,</span><br><span class="line">          <span class="string">&#x27;iOS 7.1&#x27;</span>,</span><br><span class="line">          <span class="string">&#x27;Chrome &gt; 31&#x27;</span>,</span><br><span class="line">          <span class="string">&#x27;ff &gt; 31&#x27;</span>,</span><br><span class="line">          <span class="string">&#x27;ie &gt;= 8&#x27;</span>,</span><br><span class="line">        ],</span><br><span class="line">      &#125;,</span><br><span class="line">      <span class="string">&quot;postcss-px-to-viewport&quot;</span>: &#123;</span><br><span class="line">      <span class="attr">unitToConvert</span>: <span class="string">&quot;px&quot;</span>, <span class="comment">// 要转化的单位</span></span><br><span class="line">        <span class="attr">viewportWidth</span>: designWidth, <span class="comment">// UI设计稿的宽度</span></span><br><span class="line">        <span class="attr">unitPrecision</span>: <span class="number">6</span>, <span class="comment">// 转换后的精度，即小数点位数</span></span><br><span class="line">        <span class="attr">propList</span>: [<span class="string">&quot;*&quot;</span>,<span class="string">&quot;!border&quot;</span>], <span class="comment">// 指定转换的css属性的单位，*代表全部css属性的单位都进行转换</span></span><br><span class="line">        <span class="attr">viewportUnit</span>: <span class="string">&quot;vw&quot;</span>, <span class="comment">// 指定需要转换成的视窗单位，默认vw</span></span><br><span class="line">        <span class="attr">fontViewportUnit</span>: <span class="string">&quot;vw&quot;</span>, <span class="comment">// 指定字体需要转换成的视窗单位，默认vw</span></span><br><span class="line">        <span class="attr">selectorBlackList</span>: [<span class="string">&#x27;.ignore&#x27;</span>, <span class="string">&#x27;.hairlines&#x27;</span>], <span class="comment">// 指定不转换为视窗单位的类名</span></span><br><span class="line">        <span class="attr">minPixelValue</span>: <span class="number">1</span>, <span class="comment">// 默认值1，小于或等于1px则不进行转换</span></span><br><span class="line">        <span class="attr">landscape</span>: <span class="literal">false</span> <span class="comment">// 是否处理横屏情况</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><ul><li><code>propList</code>: 当有些属性的单位我们不希望转换的时候，可以添加在数组后面，并在前面加上<code>!</code>号，如<code>propList: [&quot;*&quot;,&quot;!border&quot;]</code>,这表示：所有css属性的属性的单位都进行转化，除了<code>border</code>的</li><li><code>selectorBlackList</code>：转换的黑名单，在黑名单里面的我们可以写入字符串，只要类名包含有这个字符串，就不会被匹配。比如<code>selectorBlackList: [&#39;wrap&#39;]</code>,它表示形如<code>wrap</code>,<code>my-wrap</code>,<code>wrapper</code>这样的类名的单位，都不会被转换</li></ul><h2 id="vue"><a href="#vue" class="headerlink" title="vue"></a>vue</h2><h3 id="vue中style-scope深度访问新方式-v-deep"><a href="#vue中style-scope深度访问新方式-v-deep" class="headerlink" title="vue中style scope深度访问新方式(::v-deep)"></a>vue中style scope深度访问新方式(<code>::v-deep</code>)</h3><blockquote><p>参考： <a href="https://segmentfault.com/a/1190000021576348">vue中style scope深度访问新方式(::v-deep)</a></p></blockquote><p>由于使用 scoped 后，父组件的样式将不会渗透到子组件中。官方引入了 <a href="https://vue-loader.vuejs.org/zh/guide/scoped-css.html#%E6%B7%B1%E5%BA%A6%E4%BD%9C%E7%94%A8%E9%80%89%E6%8B%A9%E5%99%A8">深度作用选择器</a>，来解决这个问题。记得之前使用的是 <code>/deep/</code>，据说这个属性有兼容问题，现在引入了新方式：<code>::v-deep</code>:</p><figure class="highlight css"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="selector-id">#editDoctorAdvice</span> &#123;</span><br><span class="line">  <span class="selector-class">.topSearch</span> &#123;</span><br><span class="line">    <span class="attribute">float</span>: left;</span><br><span class="line">    <span class="attribute">margin-right</span>: <span class="number">10px</span>;</span><br><span class="line">  &#125;</span><br><span class="line">  &amp;::v-deep .el-input__inner &#123;</span><br><span class="line">    <span class="attribute">padding-right</span>: <span class="number">6px</span>;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="selector-class">.dateTimeClass</span> &#123;</span><br><span class="line">    <span class="attribute">width</span>: <span class="number">150px</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><ul><li><a href="https://juejin.im/post/5c9f22876fb9a05e425556ed">Vue入门指南(快速上手vue)</a></li><li><a href="https://juejin.im/post/5a0c191f6fb9a04514639419">vue快速入门的三个小实例</a></li><li><a href="https://github.com/chimurai/http-proxy-middleware">http-proxy-middleware</a></li></ul>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/4617061a3c2c/</id>
    <link href="https://blog.zisheng.pro/posts/4617061a3c2c/"/>
    <published>2020-07-13T07:37:03.000Z</published>
    <summary>精通任何技能都是需要时间的，但是往往工作中是不会给你这个时间的，尤其是开发工作。新技术层出不穷，每个项目都可能技术栈不一样。</summary>
    <title>微精通之 Vue 移动端开发</title>
    <updated>2026-09-18T08:15:28.640Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="vant" scheme="https://blog.zisheng.pro/tags/vant/"/>
    <category term="wml" scheme="https://blog.zisheng.pro/tags/wml/"/>
    <category term="模块调试" scheme="https://blog.zisheng.pro/tags/%E6%A8%A1%E5%9D%97%E8%B0%83%E8%AF%95/"/>
    <category term="组件调试" scheme="https://blog.zisheng.pro/tags/%E7%BB%84%E4%BB%B6%E8%B0%83%E8%AF%95/"/>
    <category term="前端组件化" scheme="https://blog.zisheng.pro/tags/%E5%89%8D%E7%AB%AF%E7%BB%84%E4%BB%B6%E5%8C%96/"/>
    <content>
      <![CDATA[<p>前端模块开发已经成为了开发标准。模块开发带来的一大便利就是让我们可以跨页面甚至跨应用复用代码。当我们跨应用复用模块时，就需要借助 npm 把模块发布出去供各个系统调用。今天介绍的 wml 是一个进行模块调试的利器。</p><h2 id="wml-是什么"><a href="#wml-是什么" class="headerlink" title="wml 是什么"></a>wml 是什么</h2><p>wml 侦听某个文件夹中的更改（使用<a href="https://facebook.github.io/watchman/">Watchman</a>），然后将更改的文件复制到另一个文件夹中。</p><p>wml 是一个 CLI 工具，其工作方式与ln -s非常相似。首先，使用 <code>wml add</code> 命令设置链接，然后运行wml服务（<code>wml start</code>）开始监听。仅此而已！</p><h2 id="为什么使用-wml"><a href="#为什么使用-wml" class="headerlink" title="为什么使用 wml"></a>为什么使用 wml</h2><p>让我们面对现实吧，有时符号链接还不够。Github中有超过 <a href="https://github.com/search?utf8=%E2%9C%93&q=support+for+symlinks&type=Issues">1万个</a> 关于 <strong>“support for symlinks”</strong>。</p><p>到目前为止，我遇到的两个例子是: React Native 的包管理器<a href="https://github.com/facebook/react-native/issues/637">缺失对 symlinks 的支持</a> 和Webpack的 <a href="http://webpack.github.io/docs/troubleshooting.html#npm-linked-modules-doesn-t-find-their-dependencies">inability to find linked modules dependencies</a>。很多人的办法就是直接修改 <strong>node_modules</strong> 下的文件夹，但是这会造成很多问题：</p><ol><li>当同时有两个项目依赖你的模块时，就搞砸了</li><li>当不小心执行了 <code>npm install</code>，就又搞砸了</li><li>总之就是感觉这样不对</li></ol><p>wml 使用 Facebook 的超快 Watchman 来监视源文件夹中的更改，并将它们（并且仅复制）复制到目标文件夹中。</p><h2 id="安装-watchman"><a href="#安装-watchman" class="headerlink" title="安装 watchman"></a>安装 watchman</h2><ul><li><a href="https://facebook.github.io/watchman/docs/install">watchmamn Installation</a></li><li><a href="https://stackoverflow.com/questions/38451764/how-to-install-watchman-on-windows-win10">How to install Watchman on Windows (win10)?</a></li></ul><p>windows 下为了让 <code>wml start</code> 能够工作需要执行以下命令：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># npmGlobalPrefix 可以通过 `npm prefix -g` 获取</span></span><br><span class="line">$ watchman watch <span class="variable">$&#123;npmGlobalPrefix&#125;</span>\node_modules\wml\src</span><br><span class="line"><span class="comment"># 一定要重启电脑让配置生效！</span></span><br></pre></td></tr></table></figure><h2 id="安装"><a href="#安装" class="headerlink" title="安装"></a>安装</h2><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ npm install -g wml</span><br></pre></td></tr></table></figure><h3 id="wml-命令"><a href="#wml-命令" class="headerlink" title="wml 命令"></a>wml 命令</h3><ul><li><code>wml add|a &lt;src&gt; &lt;dest&gt;</code>: 添加链接</li><li><code>wml start|s</code>: 开启 wml</li><li><code>wml list|ls</code>: 显示所有的链接</li><li><code>wml rm &lt;linkId&gt;</code>: 删除一个链接，传递 <code>all</code> 可以删除所有链接</li><li><code>wml enable|e</code>: 启用一个链接，传递 <code>all</code> 一键启用所有链接</li><li><code>wml disable|d</code>: 禁用一个链接，传递 <code>all</code> 一键禁用所有链接</li><li><code>watchman watch-del-all</code>: 修改 <code>.watchmanconfig</code> 之后需要清空 watchman 缓存</li></ul><h2 id="最佳实践"><a href="#最佳实践" class="headerlink" title="最佳实践"></a>最佳实践</h2><h3 id="npm-scripts"><a href="#npm-scripts" class="headerlink" title="npm scripts"></a>npm scripts</h3><p>这是我的习惯，写脚本能提高工作效率：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><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;wml start&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="comment">// yarn wml:add &lt;dest&gt;</span></span><br><span class="line">    <span class="attr">&quot;wml:add&quot;</span><span class="punctuation">:</span> <span class="string">&quot;wml add ./&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="comment">// 修改 `.watchmanconfig` 之后需要清空 watchman 缓存</span></span><br><span class="line">    <span class="attr">&quot;wml:clean&quot;</span><span class="punctuation">:</span><span class="string">&quot;watchman watch-del-all&quot;</span><span class="punctuation">,</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></pre></td></tr></table></figure><h3 id="demo-在模块中"><a href="#demo-在模块中" class="headerlink" title="demo 在模块中"></a>demo 在模块中</h3><p>你可能为了演示你的模块，而把演示项目放在模块项目中，这也很常见的。我们需要做一下配置：</p><p>1、修改 package.json</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><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;zhiliao&quot;</span><span class="punctuation">,</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;wml start&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;wml:add&quot;</span><span class="punctuation">:</span> <span class="string">&quot;wml add ./ ./Example/node_modules/zhiliao/&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="comment">// 修改 `.watchmanconfig` 之后需要清空 watchman 缓存</span></span><br><span class="line">    <span class="attr">&quot;wml:clean&quot;</span><span class="punctuation">:</span><span class="string">&quot;watchman watch-del-all&quot;</span><span class="punctuation">,</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></pre></td></tr></table></figure><p>2、配置 .watchmanconfig 文件把 Example 文件夹忽略掉</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;ignore_dirs&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">    <span class="string">&quot;.git&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="string">&quot;node_modules&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="string">&quot;yarn-error.log&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="string">&quot;yarn.lock&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="string">&quot;README.md&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="string">&quot;Example&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>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/c31611afec6b/</id>
    <link href="https://blog.zisheng.pro/posts/c31611afec6b/"/>
    <published>2020-07-10T08:15:07.000Z</published>
    <summary>前端模块开发已经成为了开发标准。模块开发带来的一大便利就是让我们可以跨页面甚至跨应用复用代码。当我们跨应用复用模块时，就需要借助 npm 把模块发布出去供各个系统调用。今天介绍的 wml 是一个进行模块调试的利器。</summary>
    <title>使用 wml 进行模块调试</title>
    <updated>2026-09-18T08:15:28.636Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="React" scheme="https://blog.zisheng.pro/tags/React/"/>
    <category term="UmiJS" scheme="https://blog.zisheng.pro/tags/UmiJS/"/>
    <content>
      <![CDATA[<p>鉴于 UmiJS 是重度的封装了很多工具和功能，对于刚接触的人形成了一层技术壁垒（大佬请忽略这句话）。所以我总结了使用UmiJS 开发中遇到的坑和技巧。</p><h2 id="本地开发-umi-dev-时关闭-mock"><a href="#本地开发-umi-dev-时关闭-mock" class="headerlink" title="本地开发 umi dev 时关闭 mock"></a>本地开发 umi dev 时关闭 mock</h2><blockquote><p>参考: <a href="https://github.com/umijs/umi/issues/486">希望本地开发 umi dev 时可以关闭 mock</a></p></blockquote><p>方案一（推荐）：在 npm scripts 中加入以下指令：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><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;MOCK=none umi dev&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>.env</code> 文件里 设置 <code>MOCK=none</code> 也可以关闭</p><h2 id="jsx无法转到定义处"><a href="#jsx无法转到定义处" class="headerlink" title="jsx无法转到定义处"></a>jsx无法转到定义处</h2><p>在 <code>jsconfig.json</code> 文件中进行如下配置：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;compilerOptions&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;jsx&quot;</span><span class="punctuation">:</span> <span class="string">&quot;react&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="在umi中如何访问静态资源"><a href="#在umi中如何访问静态资源" class="headerlink" title="在umi中如何访问静态资源"></a>在umi中如何访问静态资源</h2><p>在umi框架中，图片等静态资源主要放到三个地方：</p><ol><li>在 <code>/public</code> 目录下，一般放共享资源。</li><li>在 <code>/src/assets/</code> 目录下，一般放全局静态资源。</li><li>在 <code>/src/pages/</code> 里的各个页面目录下，放在这里的好处是更符合组件化开发的思想，便于拷贝复用。</li></ol><p>由于静态资源会受 <code>context.config.publicPath</code> 的影响，所以在 <code>document.ejs</code> 中应该这样引入比较安全：</p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">script</span> <span class="attr">type</span>=<span class="string">&quot;text/javascript&quot;</span> <span class="attr">src</span>=<span class="string">&quot;&lt;%= context.config.publicPath %&gt;ol.js&quot;</span> /&gt;</span></span><br></pre></td></tr></table></figure><h2 id="如何访问静态图片"><a href="#如何访问静态图片" class="headerlink" title="如何访问静态图片"></a>如何访问静态图片</h2><p>1、如果在&#x2F;public目录下的静态图片，可以直接输入绝对路径，假设&#x2F;public&#x2F;yay.jpg,访问方式如下：</p><p><code>&lt;img src=&quot;/yay.jpg&quot; /&gt;</code></p><p>注意：以上必须构建后在dist中才能看到。</p><p>2、在 <code>/src/assets</code> 和 <code>/src/pages/</code> 目录下的图片，不能通过输入绝对路径访问，必须先 <code>import</code> 导入，才能访问。或者 <code>require</code> 导入。比如 <code>/src/assets/yay.jpg</code> 需：</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> yayImg <span class="keyword">from</span> <span class="string">&#x27;/src/assets/yay.jpg&#x27;</span>;</span><br><span class="line"><span class="language-xml"><span class="tag">&lt;<span class="name">img</span> <span class="attr">src</span>=<span class="string">&#123;yayImg&#125;</span> /&gt;</span></span></span><br><span class="line"></span><br><span class="line"><span class="language-xml"><span class="tag">&lt;<span class="name">img</span> <span class="attr">src</span>=<span class="string">&#123;require(</span>&#x27;/<span class="attr">src</span>/<span class="attr">assets</span>/<span class="attr">yay.jpg</span>&#x27;)&#125;</span></span></span><br></pre></td></tr></table></figure><p>为什么会这样呢？主要是因为构建时，<code>/public</code> 目录下的文件会原样复制到 <code>/dist/</code> 目录下，而 <code>/src/assets/</code> 和 <code>/src/pages/</code> 目录下的文件会被改名并复制到 <code>/dist/</code> 下。</p><h2 id="react-router三种传参方式"><a href="#react-router三种传参方式" class="headerlink" title="react-router三种传参方式"></a>react-router三种传参方式</h2><blockquote><p>参考: <a href="https://www.kelede.win/posts/react-router%E4%B8%89%E7%A7%8D%E4%BC%A0%E5%8F%82%E6%96%B9%E5%BC%8F/">react-router三种传参方式</a></p></blockquote><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">Component</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;react&#x27;</span></span><br><span class="line"><span class="keyword">import</span> router <span class="keyword">from</span> <span class="string">&#x27;umi/router&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">RouterDemo</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> <span class="title function_">onOk</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">    router.<span class="title function_">push</span>(&#123;</span><br><span class="line">      <span class="attr">pathname</span>: <span class="string">&#x27;test/router&#x27;</span>,</span><br><span class="line">      <span class="comment">// 点击之后，页面会跳转且地址上会跟上query的参数，?id=1&amp;code=123</span></span><br><span class="line">      <span class="comment">// 获取方式传值内容的方式: this.props.location.query</span></span><br><span class="line">      <span class="attr">query</span>: &#123;</span><br><span class="line">        <span class="attr">id</span>: <span class="string">&#x27;1&#x27;</span>,</span><br><span class="line">        <span class="attr">code</span>: <span class="string">&#x27;123&#x27;</span>,</span><br><span class="line">      &#125;,</span><br><span class="line">      <span class="comment">// 点击之后，页面会跳转</span></span><br><span class="line">      <span class="comment">// 通过this.props.location.params可以获得params的值</span></span><br><span class="line">      <span class="comment">// params可以为其他名字，如text、nihao等，不一定是params</span></span><br><span class="line">      <span class="comment">// 刷新页面后，params的值会丢失。</span></span><br><span class="line">      <span class="attr">params</span>: &#123;</span><br><span class="line">        <span class="attr">d</span>: <span class="string">&#x27;1&#x27;</span>,</span><br><span class="line">        <span class="attr">code</span>: <span class="string">&#x27;123&#x27;</span>,</span><br><span class="line">      &#125;,</span><br><span class="line">      <span class="comment">// 使用state传值和params传值一样，都不会再url中显示出来</span></span><br><span class="line">      <span class="comment">// state传值与params传值的区别是state传值刷新页面值还在，而使用params传值刷新后值不没有了。</span></span><br><span class="line">      <span class="attr">state</span>: &#123;</span><br><span class="line">        <span class="attr">d</span>: <span class="string">&#x27;1&#x27;</span>,</span><br><span class="line">        <span class="attr">code</span>: <span class="string">&#x27;123&#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">  <span class="keyword">return</span> <span class="language-xml"><span class="tag">&lt;<span class="name">div</span> <span class="attr">onClick</span>=<span class="string">&#123;this.onOk&#125;</span>&gt;</span>点击测试<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><blockquote><p>参考: <a href="https://v2.umijs.org/zh/guide/html-template.html#%E9%85%8D%E7%BD%AE%E6%A8%A1%E6%9D%BF">HTML 配置模板</a></p></blockquote><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">&lt;!-- 图片在 /public 下 --&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">link</span> <span class="attr">rel</span>=<span class="string">&quot;icon&quot;</span> <span class="attr">type</span>=<span class="string">&quot;image/x-icon&quot;</span> <span class="attr">href</span>=<span class="string">&quot;&lt;%= context.publicPath %&gt;favicon.png&quot;</span> /&gt;</span></span><br><span class="line"><span class="comment">&lt;!-- 图片在 /src/assets/ 下--&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">link</span> <span class="attr">rel</span>=<span class="string">&quot;icon&quot;</span> <span class="attr">type</span>=<span class="string">&quot;image/x-icon&quot;</span> <span class="attr">href</span>=<span class="string">&quot;&lt;%= context.publicPath %&gt;static/favicon.png&quot;</span> /&gt;</span></span><br></pre></td></tr></table></figure><h2 id="支持-ie11"><a href="#支持-ie11" class="headerlink" title="支持 ie11"></a>支持 ie11</h2><blockquote><p>参考: <a href="https://github.com/umijs/umi/issues/1394">ie11兼容问题</a></p></blockquote><p>配置浏览器最低版本，会自动引入 polyfill 和做语法转换，配置的 targets 会和合并到默认值，所以不需要重复配置:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// umi默认兼容最低浏览器版本</span></span><br><span class="line"><span class="comment">// Default: &#123; chrome: 49, firefox: 45, safari: 10, edge: 13, ios: 10 &#125;</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> &#123;</span><br><span class="line">  <span class="attr">targets</span>: &#123;</span><br><span class="line">    <span class="attr">ie</span>: <span class="number">11</span>,</span><br><span class="line">  &#125;,</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><h2 id="编译-node-modules-下的包"><a href="#编译-node-modules-下的包" class="headerlink" title="编译 node_modules 下的包"></a>编译 node_modules 下的包</h2><h3 id="UmiJS-2-x"><a href="#UmiJS-2-x" class="headerlink" title="UmiJS 2.x"></a>UmiJS 2.x</h3><blockquote><p>参考: <a href="https://github.com/umijs/umi/issues/2117#issuecomment-539982434">How to configure extraBabelIncludes</a></p></blockquote><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> path = <span class="built_in">require</span>(<span class="string">&#x27;path&#x27;</span>);</span><br><span class="line">&#123;</span><br><span class="line">  <span class="attr">extraBabelIncludes</span>: [path.<span class="title function_">resolve</span>(__dirname, <span class="string">&#x27;node_modules/&lt;package_name&gt;&#x27;</span>)],</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="UmiJS-3-1"><a href="#UmiJS-3-1" class="headerlink" title="UmiJS 3.1+"></a>UmiJS 3.1+</h3><blockquote><p>参考: <a href="https://umijs.org/zh-CN/config#nodemodulestransform-31">nodeModulesTransform</a>、<a href="https://umijs.org/zh-CN/guide/boost-compile-speed">如何做编译提速</a></p></blockquote><p>UmiJS 3 删除了 <code>extraBabelIncludes</code> 和 <code>es5ImcompatibleVersions</code>，<code>node_modules</code> 也走 babel 编译后就没有意义了，无需配置</p><p>UmiJS 3 默认编译 <code>node_modules</code> 下的文件，带来一些收益的同时，也增加了额外的编译时间。如果不希望 <code>node_modules</code> 下的文件走 babel 编译，可通过以下配置减少 40% 到 60% 的编译时间。</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> &#123;</span><br><span class="line">  <span class="attr">nodeModulesTransform</span>: &#123;</span><br><span class="line">    <span class="attr">type</span>: <span class="string">&#x27;none&#x27;</span>,</span><br><span class="line">    <span class="attr">exclude</span>: [], <span class="comment">// 忽略的依赖库，包名，暂不支持绝对路径；可通过 exclude 配置添加额外需要编译的</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="call"><a href="#call" class="headerlink" title="call"></a>call</h3><blockquote><p>参考: <a href="https://github.com/dvajs/dva/issues/756#issuecomment-317770608">求教多个异步的请求问题？</a>、<a href="https://redux-saga-in-chinese.js.org/docs/advanced/RunningTasksInParallel.html">同时执行多个任务</a></p></blockquote><p><code>yield</code> 指令可以很简单的将异步控制流以同步的写法表现出来，但与此同时我们将也会需要同时执行多个任务，我们不能直接这样写：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 错误写法，effects 将按照顺序执行</span></span><br><span class="line"><span class="keyword">const</span> users = <span class="keyword">yield</span> <span class="title function_">call</span>(fetch, <span class="string">&#x27;/users&#x27;</span>)</span><br><span class="line"><span class="keyword">const</span> repos = <span class="keyword">yield</span> <span class="title function_">call</span>(fetch, <span class="string">&#x27;/repos&#x27;</span>)</span><br></pre></td></tr></table></figure><p>由于第二个 effect 将会在第一个 call 执行完毕才开始。所以我们需要这样写：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 正确写法, effects 将会同步执行</span></span><br><span class="line">*<span class="title function_">effects</span>(<span class="params">&#123;&#125;, &#123; all, call &#125;</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> [users, repos] = <span class="keyword">yield</span> <span class="title function_">all</span>([</span><br><span class="line">    <span class="title function_">call</span>(fetch, <span class="string">&#x27;/users&#x27;</span>),</span><br><span class="line">    <span class="title function_">call</span>(fetch, <span class="string">&#x27;/repos&#x27;</span>)</span><br><span class="line">  ])</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>当我们需要 <code>yield</code> 一个包含 effects 的数组， generator 会被阻塞直到所有的 effects 都执行完毕，或者当一个 effect 被拒绝 （就像 <code>Promise.all</code> 的行为）。</p><h3 id="put"><a href="#put" class="headerlink" title="put"></a>put</h3><blockquote><p>参考: <a href="https://github.com/dvajs/dva/issues/2094">yield all中放put而出现的问题</a></p></blockquote><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">*<span class="title function_">effects</span>(<span class="params">&#123;&#125;, &#123; all, call &#125;</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> [users, repos] = <span class="keyword">yield</span> <span class="title function_">all</span>([</span><br><span class="line">    <span class="keyword">yield</span> <span class="title function_">put</span>(&#123; <span class="attr">type</span>: <span class="string">&#x27;getUsers&#x27;</span> &#125;),</span><br><span class="line">    <span class="keyword">yield</span> <span class="title function_">put</span>(&#123; <span class="attr">type</span>: <span class="string">&#x27;getRepos&#x27;</span> &#125;)</span><br><span class="line">  ])</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>或者使用 <code>put.resolve</code>:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">*<span class="title function_">effects</span>(<span class="params">&#123;&#125;, &#123; all, call &#125;</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> [users, repos] = <span class="keyword">yield</span> <span class="title function_">all</span>([</span><br><span class="line">    put.<span class="title function_">resolve</span>(&#123; <span class="attr">type</span>: <span class="string">&#x27;getUsers&#x27;</span> &#125;),</span><br><span class="line">    put.<span class="title function_">resolve</span>(&#123; <span class="attr">type</span>: <span class="string">&#x27;getRepos&#x27;</span> &#125;)</span><br><span class="line">  ])</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="局部覆盖antd-样式"><a href="#局部覆盖antd-样式" class="headerlink" title="局部覆盖antd 样式"></a>局部覆盖antd 样式</h2><p>由于业务的个性化需求，我们经常会遇到需要覆盖组件样式的情况，这里举个简单的例子。</p><p>antd Select 在多选状态下，默认会展示所有选中项，这里我们给它加一个限制高度，超过此高度就出滚动条。</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">&lt;<span class="title class_">Select</span></span><br><span class="line">  mode=<span class="string">&quot;multiple&quot;</span></span><br><span class="line">  style=&#123;&#123; <span class="attr">width</span>: <span class="number">300</span> &#125;&#125;</span><br><span class="line">  placeholder=<span class="string">&quot;Please select&quot;</span></span><br><span class="line">  className=&#123;styles.<span class="property">customSelect</span>&#125;</span><br><span class="line">&gt;</span><br><span class="line">  &#123;children&#125;</span><br><span class="line">&lt;/<span class="title class_">Select</span>&gt;</span><br></pre></td></tr></table></figure><figure class="highlight css"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="selector-class">.customSelect</span> &#123;</span><br><span class="line">  :global &#123;</span><br><span class="line">    <span class="selector-class">.ant-select-selection</span> &#123;</span><br><span class="line">      <span class="attribute">max-height</span>: <span class="number">51px</span>;</span><br><span class="line">      <span class="attribute">overflow</span>: auto;</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><ul><li>引入的 antd 组件类名没有被 CSS Modules 转化，所以被覆盖的类名 <code>.ant-select-selection</code> 必须放到 <code>:global</code> 中。</li><li>因为覆盖是全局性的。为了防止对其他 Select 组件造成影响，所以需要包裹额外的 className 限制样式的生效范围。</li></ul><h2 id="优化包大小"><a href="#优化包大小" class="headerlink" title="优化包大小"></a>优化包大小</h2><blockquote><p>参考: <a href="https://juejin.im/post/5ef8581ce51d4534c14d9a5f">H5 分包实现首屏加载时间优化</a>、<a href="https://juejin.im/post/5c08fe7d6fb9a04a0d56a702">webapck4 玄妙的 SplitChunks Plugin</a>、<a href="https://github.com/umijs/umi/issues/3535#issuecomment-617590579">请问如何单独打包组件</a></p></blockquote><h3 id="UmiJS-2-x-1"><a href="#UmiJS-2-x-1" class="headerlink" title="UmiJS 2.x"></a>UmiJS 2.x</h3><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="comment">// 忽略 moment 的 locale 文件，用于减少尺寸。</span></span><br><span class="line">  <span class="comment">// https://v2.umijs.org/zh/config/#ignoremomentlocale</span></span><br><span class="line">  <span class="attr">ignoreMomentLocale</span>: <span class="literal">true</span>,</span><br><span class="line">  <span class="comment">// 配置是否开启 treeShaking，默认关闭。</span></span><br><span class="line">  <span class="comment">// https://v2.umijs.org/zh/config/#treeshaking</span></span><br><span class="line">  <span class="attr">treeShaking</span>: <span class="literal">true</span>,</span><br><span class="line">  <span class="comment">// 通过 [webpack-chain](https://github.com/mozilla-neutrino/webpack-chain) 的 API 扩展或修改 webpack 配置。</span></span><br><span class="line">  <span class="comment">// https://v2.umijs.org/zh/config/#chainwebpack</span></span><br><span class="line">  <span class="title function_">chainWebpack</span>(<span class="params">config</span>) &#123;</span><br><span class="line">    config.<span class="property">optimization</span>.<span class="title function_">splitChunks</span>(&#123;</span><br><span class="line">      <span class="attr">chunks</span>: <span class="string">&#x27;all&#x27;</span>,</span><br><span class="line">      <span class="attr">automaticNameDelimiter</span>: <span class="string">&#x27;～&#x27;</span>,</span><br><span class="line">      <span class="attr">name</span>: <span class="literal">true</span>,</span><br><span class="line">      <span class="attr">minSize</span>: <span class="number">30000</span>,</span><br><span class="line">      <span class="attr">minChunks</span>: <span class="number">1</span>,</span><br><span class="line">      <span class="attr">cacheGroups</span>: &#123;</span><br><span class="line">        <span class="attr">echarts</span>: &#123;</span><br><span class="line">          <span class="attr">name</span>: <span class="string">&#x27;echarts&#x27;</span>,</span><br><span class="line">          <span class="attr">test</span>: <span class="regexp">/[\\/]node_modules[\\/](echarts)[\\/]/</span>,</span><br><span class="line">          <span class="attr">priority</span>: -<span class="number">9</span>,</span><br><span class="line">          <span class="attr">enforce</span>: <span class="literal">true</span>,</span><br><span class="line">        &#125;,</span><br><span class="line">        <span class="attr">antd</span>: &#123;</span><br><span class="line">          <span class="attr">name</span>: <span class="string">&#x27;antd&#x27;</span>,</span><br><span class="line">          <span class="attr">test</span>: <span class="regexp">/[\\/]node_modules[\\/](@ant-design|antd|antd-mobile)[\\/]/</span>,</span><br><span class="line">          <span class="attr">priority</span>: -<span class="number">10</span>,</span><br><span class="line">          <span class="attr">enforce</span>: <span class="literal">true</span>,</span><br><span class="line">        &#125;,</span><br><span class="line">        <span class="attr">vendors</span>: &#123;</span><br><span class="line">          <span class="attr">name</span>: <span class="string">&#x27;vendors&#x27;</span>,</span><br><span class="line">          <span class="attr">test</span>: <span class="regexp">/[\\/]node_modules[\\/]/</span>,</span><br><span class="line">          <span class="attr">priority</span>: -<span class="number">11</span>,</span><br><span class="line">          <span class="attr">enforce</span>: <span class="literal">true</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">  <span class="attr">plugins</span>: [</span><br><span class="line">    [</span><br><span class="line">      <span class="comment">// 这是官方封装的一个插件集，包含 18 个常用的进阶功能。</span></span><br><span class="line">      <span class="comment">// https://v2.umijs.org/zh/plugin/umi-plugin-react.html#%E5%AE%89%E8%A3%85</span></span><br><span class="line">      <span class="string">&#x27;umi-plugin-react&#x27;</span>,</span><br><span class="line">      &#123;</span><br><span class="line">        <span class="comment">// 默认是 [&#x27;umi&#x27;]，可修改，比如做了 vendors 依赖提取之后，会需要在 umi.js 之前加载 vendors.js</span></span><br><span class="line">        <span class="comment">// https://v2.umijs.org/zh/plugin/umi-plugin-react.html#chunks</span></span><br><span class="line">        <span class="attr">chunks</span>: [<span class="string">&#x27;vendors&#x27;</span>, <span class="string">&#x27;antd&#x27;</span>, <span class="string">&#x27;echarts&#x27;</span>, <span class="string">&#x27;umi&#x27;</span>],</span><br><span class="line">      &#125;,</span><br><span class="line">    ],</span><br><span class="line">  ],</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="UmiJS-3-x"><a href="#UmiJS-3-x" class="headerlink" title="UmiJS 3.x"></a>UmiJS 3.x</h3><blockquote><p>参考 <a href="https://umijs.org/zh-CN/docs/upgrade-to-umi-3#%E5%8D%87%E7%BA%A7-umi-plugin-react-%E4%B8%BA-umijspreset-react">升级 umi-plugin-react 为 @umijs&#x2F;preset-react</a></p></blockquote><p>由于 Umi 3 的配置方式是拍平的方式，还需要修改配置:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="comment">// 忽略 moment 的 locale 文件，用于减少尺寸。</span></span><br><span class="line">  <span class="comment">// https://v2.umijs.org/zh/config/#ignoremomentlocale</span></span><br><span class="line">  <span class="attr">ignoreMomentLocale</span>: <span class="literal">true</span>,</span><br><span class="line">  <span class="comment">// 配置是否开启 treeShaking，默认关闭。</span></span><br><span class="line">  <span class="comment">// https://v2.umijs.org/zh/config/#treeshaking</span></span><br><span class="line">  <span class="attr">treeShaking</span>: <span class="literal">true</span>,</span><br><span class="line">  <span class="comment">// 通过 [webpack-chain](https://github.com/mozilla-neutrino/webpack-chain) 的 API 扩展或修改 webpack 配置。</span></span><br><span class="line">  <span class="comment">// https://v2.umijs.org/zh/config/#chainwebpack</span></span><br><span class="line">  <span class="title function_">chainWebpack</span>(<span class="params">config</span>) &#123;</span><br><span class="line">    config.<span class="property">optimization</span>.<span class="title function_">splitChunks</span>(&#123;</span><br><span class="line">      <span class="attr">chunks</span>: <span class="string">&#x27;all&#x27;</span>,</span><br><span class="line">      <span class="attr">automaticNameDelimiter</span>: <span class="string">&#x27;～&#x27;</span>,</span><br><span class="line">      <span class="attr">name</span>: <span class="literal">true</span>,</span><br><span class="line">      <span class="attr">minSize</span>: <span class="number">30000</span>,</span><br><span class="line">      <span class="attr">minChunks</span>: <span class="number">1</span>,</span><br><span class="line">      <span class="attr">cacheGroups</span>: &#123;</span><br><span class="line">        <span class="attr">echarts</span>: &#123;</span><br><span class="line">          <span class="attr">name</span>: <span class="string">&#x27;echarts&#x27;</span>,</span><br><span class="line">          <span class="attr">test</span>: <span class="regexp">/[\\/]node_modules[\\/](echarts)[\\/]/</span>,</span><br><span class="line">          <span class="attr">priority</span>: -<span class="number">9</span>,</span><br><span class="line">          <span class="attr">enforce</span>: <span class="literal">true</span>,</span><br><span class="line">        &#125;,</span><br><span class="line">        <span class="attr">antd</span>: &#123;</span><br><span class="line">          <span class="attr">name</span>: <span class="string">&#x27;antd&#x27;</span>,</span><br><span class="line">          <span class="attr">test</span>: <span class="regexp">/[\\/]node_modules[\\/](@ant-design|antd|antd-mobile)[\\/]/</span>,</span><br><span class="line">          <span class="attr">priority</span>: -<span class="number">10</span>,</span><br><span class="line">          <span class="attr">enforce</span>: <span class="literal">true</span>,</span><br><span class="line">        &#125;,</span><br><span class="line">        <span class="attr">vendors</span>: &#123;</span><br><span class="line">          <span class="attr">name</span>: <span class="string">&#x27;vendors&#x27;</span>,</span><br><span class="line">          <span class="attr">test</span>: <span class="regexp">/[\\/]node_modules[\\/]/</span>,</span><br><span class="line">          <span class="attr">priority</span>: -<span class="number">11</span>,</span><br><span class="line">          <span class="attr">enforce</span>: <span class="literal">true</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">  <span class="comment">// https://umijs.org/zh-CN/config#chunks</span></span><br><span class="line">  <span class="attr">chunks</span>: [<span class="string">&#x27;vendors&#x27;</span>, <span class="string">&#x27;antd&#x27;</span>, <span class="string">&#x27;echarts&#x27;</span>, <span class="string">&#x27;umi&#x27;</span>],</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="momentjs"><a href="#momentjs" class="headerlink" title="momentjs"></a>momentjs</h2><h3 id="使用中文配置"><a href="#使用中文配置" class="headerlink" title="使用中文配置"></a>使用中文配置</h3><blockquote><p>参考: <a href="https://blog.csdn.net/hu_lanlan/article/details/91043696">antd design国际化配置为中文时，日期组件中月与星期显示为英文，其他显示为中文</a></p></blockquote><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">LocaleProvider</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;antd&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> zh_CN <span class="keyword">from</span> <span class="string">&#x27;antd/lib/locale-provider/zh_CN&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> moment <span class="keyword">from</span> <span class="string">&#x27;moment&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> <span class="string">&#x27;moment/locale/zh-cn&#x27;</span>;</span><br><span class="line"></span><br><span class="line">moment.<span class="title function_">locale</span>(<span class="string">&#x27;zh-cn&#x27;</span>);</span><br><span class="line">...</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">LocaleProvider</span> <span class="attr">locale</span>=<span class="string">&#123;zh_CN&#125;</span>&gt;</span><span class="tag">&lt;<span class="name">App</span> /&gt;</span><span class="tag">&lt;/<span class="name">LocaleProvider</span>&gt;</span></span>;</span><br></pre></td></tr></table></figure><h3 id="替换-momentjs"><a href="#替换-momentjs" class="headerlink" title="替换 momentjs"></a>替换 momentjs</h3><blockquote><p>参考: <a href="https://github.com/ant-design/antd-dayjs-webpack-plugin">antd-dayjs-webpack-plugin</a>、<a href="https://ant.design/docs/react/replace-moment-cn">替换 Moment.js</a>、<a href="https://zhuanlan.zhihu.com/p/136510620">基于umi、antd的前端工程优化实践</a></p></blockquote><p>请先删除 <code>ignoreMomentLocale: true</code> 配置再进行以下操作：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">yarn add antd-dayjs-webpack-plugin -D</span><br></pre></td></tr></table></figure><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> &#123;</span><br><span class="line">  <span class="title function_">chainWebpack</span>(<span class="params">config</span>) &#123;</span><br><span class="line">    <span class="comment">// antd moment -&gt; dayjs</span></span><br><span class="line">    <span class="comment">// 如果在 Ant Design 3.x 的项目中使用本插件，需要传入以下配置，指定 preset。</span></span><br><span class="line">    config.<span class="title function_">plugin</span>(<span class="string">&#x27;moment2dayjs&#x27;</span>).<span class="title function_">use</span>(<span class="string">&#x27;antd-dayjs-webpack-plugin&#x27;</span>, [</span><br><span class="line">      &#123;</span><br><span class="line">        <span class="attr">preset</span>: <span class="string">&#x27;antdv3&#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><p>如果项目中需要使用中文语言，还要引入dayjs的中文语言包并与antd的ConfigProvider配合服用。</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 设置dayjs中文</span></span><br><span class="line"><span class="keyword">import</span> dayjs <span class="keyword">from</span> <span class="string">&#x27;dayjs&#x27;</span></span><br><span class="line"><span class="keyword">import</span> <span class="string">&#x27;dayjs/locale/zh-cn&#x27;</span></span><br><span class="line">dayjs.<span class="title function_">locale</span>(<span class="string">&#x27;zh-cn&#x27;</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">ConfigProvider</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;antd&#x27;</span></span><br><span class="line"><span class="keyword">import</span> zhCN <span class="keyword">from</span> <span class="string">&#x27;antd/lib/locale-provider/zh_CN&#x27;</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> (&#123;children&#125;) =&gt; <span class="language-xml"><span class="tag">&lt;<span class="name">ConfigProvider</span> <span class="attr">locale</span>=<span class="string">&#123;zhCN&#125;</span>&gt;</span>&#123;children&#125;<span class="tag">&lt;/<span class="name">ConfigProvider</span>&gt;</span></span></span><br></pre></td></tr></table></figure><p>通过上述配置后，使用DatePicker组件拿到的日期与之前一致，但可以直接使用dayjs的API操作日期，moment不复存在。最终dayjs打包体积为14.64KB，减小了330KB之多。</p><p>注：目前<a href="mailto:&#100;&#x61;&#121;&#106;&#115;&#64;&#49;&#46;&#x38;&#46;&#50;&#x30;">dayjs@1.8.20</a>后有个bug会导致替换后WeekPicker显示不正常，1.8.21版本之后已修复。</p><h2 id="dva-loading-使用"><a href="#dva-loading-使用" class="headerlink" title="dva-loading 使用"></a>dva-loading 使用</h2><blockquote><p>参考: <a href="https://www.jianshu.com/p/61fe7a57fad4">dva-loading 实践用法</a></p></blockquote><p>loading 分为四种使用情况，下面依次用代码展示：</p><p><strong>1、全局</strong></p><p>监听的是应用中所有 effect 是否执行完毕，若执行完毕。loading 的值就变为 <code>false</code>。</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">React</span> <span class="keyword">from</span> <span class="string">&#x27;react&#x27;</span></span><br><span class="line"><span class="keyword">const</span> &#123;useSelector,useDispatch&#125; = <span class="string">&#x27;dva&#x27;</span></span><br><span class="line"><span class="keyword">import</span> &#123;<span class="title class_">Spin</span>&#125; <span class="keyword">from</span> <span class="string">&#x27;antd&#x27;</span></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">DemoPage</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> &#123;loading&#125; = <span class="title function_">useSelector</span>(<span class="function"><span class="params">stores</span> =&gt;</span> (&#123;</span><br><span class="line">    <span class="attr">loading</span>: stores.<span class="property">loading</span></span><br><span class="line">  &#125;))</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">Spin</span> <span class="attr">spinning</span>=<span class="string">&#123;loading.global&#125;/</span>&gt;</span></span></span><br><span class="line">  )</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>2、model</strong></p><p>监听某个模块的所有 effect 是否执行完毕，若执行完毕。loading 的值就变为 <code>false</code>。</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">React</span> <span class="keyword">from</span> <span class="string">&#x27;react&#x27;</span></span><br><span class="line"><span class="keyword">const</span> &#123;useSelector,useDispatch&#125; = <span class="string">&#x27;dva&#x27;</span></span><br><span class="line"><span class="keyword">import</span> &#123;<span class="title class_">Spin</span>&#125; <span class="keyword">from</span> <span class="string">&#x27;antd&#x27;</span></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">DemoPage</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> &#123;loading,demoModel&#125; = <span class="title function_">useSelector</span>(<span class="function"><span class="params">stores</span> =&gt;</span> (&#123;</span><br><span class="line">    <span class="attr">loading</span>: stores.<span class="property">loading</span>,</span><br><span class="line">    <span class="attr">demoModel</span>: stores.<span class="property">loading</span>,</span><br><span class="line">  &#125;))</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">Spin</span> <span class="attr">spinning</span>=<span class="string">&#123;loading.models.demoModel&#125;/</span>&gt;</span></span></span><br><span class="line">  )</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>3、effect:</strong></p><p>监听某个 effect 是否执行完毕，若执行完毕。loading 的值就变为 <code>false</code>。</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">React</span> <span class="keyword">from</span> <span class="string">&#x27;react&#x27;</span></span><br><span class="line"><span class="keyword">const</span> &#123;useSelector,useDispatch&#125; = <span class="string">&#x27;dva&#x27;</span></span><br><span class="line"><span class="keyword">import</span> &#123;<span class="title class_">Spin</span>&#125; <span class="keyword">from</span> <span class="string">&#x27;antd&#x27;</span></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">DemoPage</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> &#123;loading,demoModel&#125; = <span class="title function_">useSelector</span>(<span class="function"><span class="params">stores</span> =&gt;</span> (&#123;</span><br><span class="line">    <span class="attr">loading</span>: stores.<span class="property">loading</span>,</span><br><span class="line">    <span class="attr">demoModel</span>: stores.<span class="property">loading</span>,</span><br><span class="line">  &#125;))</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">Spin</span> <span class="attr">spinning</span>=<span class="string">&#123;loading.effects[</span>&#x27;<span class="attr">demoModel</span>/<span class="attr">effect1</span>&#x27;]/&gt;</span></span></span><br><span class="line">  )</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>4、effects</strong></p><p>如果想监听某个 model 中的某几个 effect，可以使用 <code>||</code> 连接，当全部执行完毕时，返回的是 <code>undefined</code>，所以必须在末尾拼接 <code>|| false</code>：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">React</span> <span class="keyword">from</span> <span class="string">&#x27;react&#x27;</span></span><br><span class="line"><span class="keyword">const</span> &#123;useSelector,useDispatch&#125; = <span class="string">&#x27;dva&#x27;</span></span><br><span class="line"><span class="keyword">import</span> &#123;<span class="title class_">Spin</span>&#125; <span class="keyword">from</span> <span class="string">&#x27;antd&#x27;</span></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">DemoPage</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> &#123;loading,demoModel&#125; = <span class="title function_">useSelector</span>(<span class="function"><span class="params">stores</span> =&gt;</span> (&#123;</span><br><span class="line">    <span class="attr">loading</span>: stores.<span class="property">loading</span>,</span><br><span class="line">    <span class="attr">demoModel</span>: stores.<span class="property">loading</span>,</span><br><span class="line">  &#125;))</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">Spin</span> <span class="attr">spinning</span>=<span class="string">&#123;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">      <span class="attr">loading.effects</span>[&#x27;<span class="attr">demoModel</span>/<span class="attr">effect1</span>&#x27;] ||</span></span></span><br><span class="line"><span class="tag"><span class="language-xml">      <span class="attr">loading.effects</span>[&#x27;<span class="attr">demoModel</span>/<span class="attr">effect3</span>&#x27;] ||</span></span></span><br><span class="line"><span class="tag"><span class="language-xml">      <span class="attr">loading.effects</span>[&#x27;<span class="attr">demoModel</span>/<span class="attr">effect4</span>&#x27;] || <span class="attr">false</span></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">    /&gt;</span></span></span><br><span class="line">  )</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="关闭-Umi-UI"><a href="#关闭-Umi-UI" class="headerlink" title="关闭 Umi UI"></a>关闭 Umi UI</h2><p>umi 项目默认启动 <code>umi ui</code>，会出现一个mini图标气泡浮在右下角，关闭有两种方式，一种是直接用样式 <code>display none</code>。另一种是在启动时加上 <code>UMI_UI=none</code> 环境变量。</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><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;start&quot;</span><span class="punctuation">:</span> <span class="string">&quot;UMI_UI=none umi dev&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;dev&quot;</span><span class="punctuation">:</span> <span class="string">&quot;MOCK=none UMI_UI=none umi dev&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>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/f477c87db8e2/</id>
    <link href="https://blog.zisheng.pro/posts/f477c87db8e2/"/>
    <published>2020-06-08T02:17:55.000Z</published>
    <summary>鉴于 UmiJS 是重度的封装了很多工具和功能，对于刚接触的人形成了一层技术壁垒（大佬请忽略这句话）。所以我总结了使用UmiJS 开发中遇到的坑和技巧。</summary>
    <title>UmiJS开发技巧</title>
    <updated>2026-09-18T08:15:28.632Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="React" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/React/"/>
    <category term="React" scheme="https://blog.zisheng.pro/tags/React/"/>
    <category term="Ant Design" scheme="https://blog.zisheng.pro/tags/Ant-Design/"/>
    <category term="Form 表单" scheme="https://blog.zisheng.pro/tags/Form-%E8%A1%A8%E5%8D%95/"/>
    <category term="Input" scheme="https://blog.zisheng.pro/tags/Input/"/>
    <category term="Table 表格" scheme="https://blog.zisheng.pro/tags/Table-%E8%A1%A8%E6%A0%BC/"/>
    <content>
      <![CDATA[<h2 id="Form-表单"><a href="#Form-表单" class="headerlink" title="Form 表单"></a>Form 表单</h2><h3 id="让-Input-组件自动获取焦点"><a href="#让-Input-组件自动获取焦点" class="headerlink" title="让 Input 组件自动获取焦点"></a>让 Input 组件自动获取焦点</h3><p>添加 <code>autoFocus</code> 属性：</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&lt;<span class="title class_">Input</span> autoFocus /&gt;</span><br></pre></td></tr></table></figure><ul><li><a href="https://github.com/ant-design/ant-design/issues/2952">https://github.com/ant-design/ant-design/issues/2952</a></li><li><a href="https://stackoverflow.com/questions/28889826/how-to-set-focus-on-an-input-field-after-rendering">https://stackoverflow.com/questions/28889826/how-to-set-focus-on-an-input-field-after-rendering</a></li></ul><h2 id="Table-表格"><a href="#Table-表格" class="headerlink" title="Table 表格"></a>Table 表格</h2><h3 id="table组件多选时，会选中下一页的表格同行数据的解决办法"><a href="#table组件多选时，会选中下一页的表格同行数据的解决办法" class="headerlink" title="table组件多选时，会选中下一页的表格同行数据的解决办法"></a>table组件多选时，会选中下一页的表格同行数据的解决办法</h3><blockquote><p>解决办法来自：<a href="https://blog.csdn.net/weixin_41606276/article/details/99312155">https://blog.csdn.net/weixin_41606276/article/details/99312155</a></p></blockquote><p>类似选中某页的几行数据，在删除等操作后其他页面的表格或者删除后本表格的同行页为选中状态。这是因为没有区分每页表格的<code>id</code>。解决办法是给 Table 加上 <code>rowKey</code></p><h3 id="defaultExpandAllRows-true-刷新后不展开问题"><a href="#defaultExpandAllRows-true-刷新后不展开问题" class="headerlink" title="defaultExpandAllRows&#x3D;{true} 刷新后不展开问题"></a>defaultExpandAllRows&#x3D;{true} 刷新后不展开问题</h3><blockquote><p>解决办法来自：<a href="https://github.com/ant-design/ant-design/issues/4145#issuecomment-271230317">https://github.com/ant-design/ant-design/issues/4145#issuecomment-271230317</a></p></blockquote><p>借助了key改变自动变成新的component，虽然可以解决默认问题。。但总觉得不是key正确的用法</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> [data,setData] = <span class="title function_">useState</span>([])</span><br><span class="line"><span class="keyword">const</span> [expandedRowKeys, setExpandedRowKeys] = <span class="title function_">useState</span>([]);</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="title function_">setExpandedRowKeys</span>(listTableData.<span class="property">list</span>.<span class="title function_">map</span>(<span class="function"><span class="params">item</span> =&gt;</span> item.<span class="property">id</span>));</span><br><span class="line">&#125;, [listTableData]);</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">onExpand</span> = (<span class="params">expanded, record</span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">if</span> (expanded) &#123;</span><br><span class="line">    <span class="title function_">setExpandedRowKeys</span>([...expandedRowKeys, record.<span class="property">tableCode</span>]);</span><br><span class="line">  &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    <span class="title function_">setExpandedRowKeys</span>(expandedRowKeys.<span class="title function_">filter</span>(<span class="function"><span class="params">item</span> =&gt;</span> item !== record.<span class="property">tableCode</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="title function_">useEffect</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">  <span class="title function_">setData</span>([&#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">&lt;<span class="title class_">Table</span></span><br><span class="line">  dataSource=&#123;data&#125;</span><br><span class="line">  rowKey=<span class="string">&quot;id&quot;</span></span><br><span class="line">  expandedRowKeys=&#123;expandedRowKeys&#125;</span><br><span class="line">  onExpand=&#123;onExpand&#125;</span><br><span class="line">&gt;</span><br></pre></td></tr></table></figure><h3 id="表格的column筛选排序状态重置问题"><a href="#表格的column筛选排序状态重置问题" class="headerlink" title="表格的column筛选排序状态重置问题"></a>表格的column筛选排序状态重置问题</h3><blockquote><p>解决办法来自：<a href="https://github.com/ant-design/ant-design-pro/issues/3695#issuecomment-642460173">https://github.com/ant-design/ant-design-pro/issues/3695#issuecomment-642460173</a></p></blockquote><p>表格设置了排序以及筛选之后, 当我们从外部点击按钮刷新data时,数据列表实际并没有经过筛选排序。解决办法如下：</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> [sortedInfo, setSortedInfo] = <span class="title function_">useState</span>(&#123;&#125;);</span><br><span class="line"><span class="comment">// 分页、排序、筛选变化时触发</span></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">handleTableChange</span> = <span class="keyword">async</span> (<span class="params">pagination, filters, sorter</span>) =&gt; &#123;</span><br><span class="line">  <span class="title function_">setSortedInfo</span>(sorter);</span><br><span class="line">&#125;;</span><br><span class="line">...</span><br><span class="line">&lt;<span class="title class_">Table</span> onChange=&#123;handleTableChange&#125;&gt;</span><br><span class="line">  ...</span><br><span class="line">  &lt;<span class="title class_">Column</span></span><br><span class="line">    title=<span class="string">&quot;查看次数&quot;</span></span><br><span class="line">    dataIndex=<span class="string">&quot;queryCount&quot;</span></span><br><span class="line">    key=<span class="string">&quot;queryCount&quot;</span></span><br><span class="line">    sorter</span><br><span class="line">    sortOrder=&#123;sortedInfo.<span class="property">columnKey</span> === <span class="string">&#x27;queryCount&#x27;</span> &amp;&amp; sortedInfo.<span class="property">order</span>&#125;</span><br><span class="line">  /&gt;</span><br><span class="line">  ...</span><br><span class="line">&lt;/<span class="title class_">Table</span>&gt;</span><br></pre></td></tr></table></figure><h3 id="Table-Row-Hover-时不改变背景"><a href="#Table-Row-Hover-时不改变背景" class="headerlink" title="Table Row Hover 时不改变背景"></a>Table Row Hover 时不改变背景</h3><figure class="highlight css"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="selector-class">.ant-table-thead</span>&gt;<span class="selector-tag">tr</span><span class="selector-class">.ant-table-row-hover</span><span class="selector-pseudo">:not</span>(<span class="selector-class">.ant-table-expanded-row</span>)&gt;<span class="selector-tag">td</span>,</span><br><span class="line"><span class="selector-class">.ant-table-tbody</span>&gt;<span class="selector-tag">tr</span><span class="selector-class">.ant-table-row-hover</span><span class="selector-pseudo">:not</span>(<span class="selector-class">.ant-table-expanded-row</span>)&gt;<span class="selector-tag">td</span>,</span><br><span class="line"><span class="selector-class">.ant-table-thead</span>&gt;<span class="selector-tag">tr</span><span class="selector-pseudo">:hover</span><span class="selector-pseudo">:not</span>(<span class="selector-class">.ant-table-expanded-row</span>)&gt;<span class="selector-tag">td</span>,</span><br><span class="line"><span class="selector-class">.ant-table-tbody</span>&gt;<span class="selector-tag">tr</span><span class="selector-pseudo">:hover</span><span class="selector-pseudo">:not</span>(<span class="selector-class">.ant-table-expanded-row</span>)&gt;<span class="selector-tag">td</span> &#123;</span><br><span class="line">  <span class="attribute">background</span>: unset; //Change the existing <span class="attribute">color</span> <span class="selector-tag">to</span> `unset`</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="Tooltip"><a href="#Tooltip" class="headerlink" title="Tooltip"></a>Tooltip</h2><h3 id="如何更改-ant-tooltip-组件的文本颜色和背景颜色"><a href="#如何更改-ant-tooltip-组件的文本颜色和背景颜色" class="headerlink" title="如何更改 ant-tooltip 组件的文本颜色和背景颜色"></a>如何更改 ant-tooltip 组件的文本颜色和背景颜色</h3><blockquote><p>解决办法来自: <a href="https://www.coder.work/article/3437880">https://www.coder.work/article/3437880</a></p></blockquote><figure class="highlight css"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">:global &#123;</span><br><span class="line">  <span class="selector-class">.ant-tooltip-inner</span> &#123;</span><br><span class="line">    <span class="attribute">color</span>: <span class="number">#ffffff</span>;</span><br><span class="line">    <span class="attribute">background-color</span>: <span class="built_in">rgba</span>(<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0.6</span>);</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="selector-class">.ant-tooltip-arrow</span><span class="selector-pseudo">::before</span> &#123;</span><br><span class="line">    <span class="attribute">background-color</span>: <span class="built_in">rgba</span>(<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0.6</span>);</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="selector-class">.ant-tooltip-placement-bottom</span> <span class="selector-class">.ant-tooltip-arrow</span>, <span class="selector-class">.ant-tooltip-placement-bottomLeft</span> <span class="selector-class">.ant-tooltip-arrow</span>, <span class="selector-class">.ant-tooltip-placement-bottomRight</span> <span class="selector-class">.ant-tooltip-arrow</span> &#123;</span><br><span class="line">    <span class="attribute">border-bottom-color</span>: <span class="number">#000000</span>;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="Icon"><a href="#Icon" class="headerlink" title="Icon"></a>Icon</h2><h3 id="自定义-svg-图标"><a href="#自定义-svg-图标" class="headerlink" title="自定义 svg 图标"></a>自定义 svg 图标</h3><blockquote><p>解决办法参考自: <a href="https://blog.csdn.net/c5211314963/article/details/103371845">https://blog.csdn.net/c5211314963/article/details/103371845</a></p></blockquote><p>如果你们的 UED 同学就是传给你一个 svg 图片，你可以按照下面的方式进行。</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">React</span> <span class="keyword">from</span> <span class="string">&#x27;react&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">Icon</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;antd&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">PointIconSvg</span> = (<span class="params">&#123; size = <span class="number">1</span>, color = <span class="string">&#x27;#1a37ea&#x27;</span> &#125;</span>) =&gt; &#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">svg</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">      <span class="attr">width</span>=<span class="string">&#123;</span>`$&#123;<span class="attr">10</span> * <span class="attr">size</span>&#125;<span class="attr">px</span>`&#125;</span></span></span><br><span class="line"><span class="tag"><span class="language-xml">      <span class="attr">height</span>=<span class="string">&#123;</span>`$&#123;<span class="attr">13</span> * <span class="attr">size</span>&#125;<span class="attr">px</span>`&#125;</span></span></span><br><span class="line"><span class="tag"><span class="language-xml">      <span class="attr">viewBox</span>=<span class="string">&quot;0 0 10 13&quot;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">      <span class="attr">version</span>=<span class="string">&quot;1.1&quot;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">      <span class="attr">xmlns</span>=<span class="string">&quot;http://www.w3.org/2000/svg&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;&#123;</span> <span class="attr">display:</span> &#x27;<span class="attr">block</span>&#x27; &#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">      <span class="tag">&lt;<span class="name">g</span> <span class="attr">id</span>=<span class="string">&quot;污染源档案&quot;</span> <span class="attr">stroke</span>=<span class="string">&quot;none&quot;</span> <span class="attr">strokeWidth</span>=<span class="string">&quot;1&quot;</span> <span class="attr">fill</span>=<span class="string">&quot;none&quot;</span> <span class="attr">fillRule</span>=<span class="string">&quot;evenodd&quot;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">g</span> <span class="attr">id</span>=<span class="string">&quot;6企业详情-详细信息-环境管理-2&quot;</span> <span class="attr">transform</span>=<span class="string">&quot;translate(-698.000000, -746.000000)&quot;</span> <span class="attr">fill</span>=<span class="string">&#123;color&#125;</span> <span class="attr">fillRule</span>=<span class="string">&quot;nonzero&quot;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">          <span class="tag">&lt;<span class="name">g</span> <span class="attr">id</span>=<span class="string">&quot;基本信息&quot;</span> <span class="attr">transform</span>=<span class="string">&quot;translate(24.000000, 335.000000)&quot;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">            <span class="tag">&lt;<span class="name">g</span> <span class="attr">id</span>=<span class="string">&quot;编组-7&quot;</span> <span class="attr">transform</span>=<span class="string">&quot;translate(30.000000, 120.000000)&quot;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">              <span class="tag">&lt;<span class="name">g</span> <span class="attr">id</span>=<span class="string">&quot;地图&quot;</span> <span class="attr">transform</span>=<span class="string">&quot;translate(0.000000, 273.000000)&quot;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">                <span class="tag">&lt;<span class="name">g</span> <span class="attr">id</span>=<span class="string">&quot;编组&quot;</span> <span class="attr">transform</span>=<span class="string">&quot;translate(644.000000, 16.000000)&quot;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">                  <span class="tag">&lt;<span class="name">g</span> <span class="attr">id</span>=<span class="string">&quot;编组-4&quot;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">                    <span class="tag">&lt;<span class="name">path</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">                      <span class="attr">d</span>=<span class="string">&quot;M4.99877841,2 C2.24285365,2 0,4.29103476 0,7.10616241 C0,9.82395853 4.48082093,14.6506047 4.67139018,14.8552505 C4.75690203,14.9475907 4.87661862,15 5.0012216,15 C5.0036648,15 5.00855119,15 5.01099438,15 C5.13804056,15 5.26020034,14.9401037 5.343269,14.8402764 L6.89469827,13.0259167 C8.95675544,10.4478787 10,8.4563256 10,7.10865809 C10,4.29103476 7.75714636,2 4.99877841,2 Z M4.99877841,9.42714533 C3.74297582,9.42714533 2.7266064,8.38894222 2.7266064,7.10616241 C2.7266064,5.82338261 3.74297582,4.78517949 4.99877841,4.78517949 C6.254581,4.78517949 7.27095042,5.82338261 7.27095042,7.10616241 C7.27095042,8.38894222 6.254581,9.42714533 4.99877841,9.42714533 Z&quot;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">                      <span class="attr">id</span>=<span class="string">&quot;形状&quot;</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">g</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">                <span class="tag">&lt;/<span class="name">g</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">              <span class="tag">&lt;/<span class="name">g</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">            <span class="tag">&lt;/<span class="name">g</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">          <span class="tag">&lt;/<span class="name">g</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;/<span class="name">g</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;/<span class="name">g</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">svg</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> props =&gt; <span class="language-xml"><span class="tag">&lt;<span class="name">Icon</span> <span class="attr">component</span>=<span class="string">&#123;()</span> =&gt;</span> <span class="tag">&lt;<span class="name">PointIconSvg</span> &#123;<span class="attr">...props</span>&#125; /&gt;</span>&#125; /&gt;</span>;</span><br></pre></td></tr></table></figure><h2 id="Button"><a href="#Button" class="headerlink" title="Button"></a>Button</h2><h3 id="点击动画边框-css-清除"><a href="#点击动画边框-css-清除" class="headerlink" title="点击动画边框 css 清除"></a>点击动画边框 css 清除</h3><figure class="highlight css"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">:global&#123;</span><br><span class="line">  <span class="selector-class">.ant-click-animating-without-extra-node</span><span class="selector-pseudo">:after</span> &#123;</span><br><span class="line">    <span class="attribute">border</span>: <span class="number">0</span> none;</span><br><span class="line">    <span class="attribute">opacity</span>: <span class="number">0</span>;</span><br><span class="line">    <span class="attribute">animation</span>:none <span class="number">0</span> ease <span class="number">0</span> <span class="number">1</span> normal;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/681c13911f61/</id>
    <link href="https://blog.zisheng.pro/posts/681c13911f61/"/>
    <published>2020-06-05T08:59:43.000Z</published>
    <summary>本文介绍了一些在开发中常用的 Antd 技巧</summary>
    <title>Antd 开发技巧</title>
    <updated>2026-09-18T08:15:28.632Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="echarts" scheme="https://blog.zisheng.pro/tags/echarts/"/>
    <category term="可视化" scheme="https://blog.zisheng.pro/tags/%E5%8F%AF%E8%A7%86%E5%8C%96/"/>
    <category term="开发技巧" scheme="https://blog.zisheng.pro/tags/%E5%BC%80%E5%8F%91%E6%8A%80%E5%B7%A7/"/>
    <content>
      <![CDATA[<h2 id="柱状图颜色渐变"><a href="#柱状图颜色渐变" class="headerlink" title="柱状图颜色渐变"></a>柱状图颜色渐变</h2><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> echarts <span class="keyword">from</span> <span class="string">&#x27;echarts&#x27;</span></span><br><span class="line">...</span><br><span class="line"><span class="attr">series</span>: [</span><br><span class="line">  &#123;</span><br><span class="line">    <span class="attr">type</span>: <span class="string">&#x27;bar&#x27;</span>,</span><br><span class="line">    <span class="comment">// 图形样式</span></span><br><span class="line">    <span class="attr">itemStyle</span>: &#123;</span><br><span class="line">      <span class="attr">color</span>: <span class="keyword">new</span> echarts.<span class="property">graphic</span>.<span class="title class_">LinearGradient</span>(<span class="number">0</span>, <span class="number">0</span>, <span class="number">0</span>, <span class="number">1</span>, [</span><br><span class="line">        &#123; <span class="attr">offset</span>: <span class="number">0</span>, <span class="attr">color</span>: <span class="string">&#x27;#178ffc&#x27;</span> &#125;,</span><br><span class="line">        &#123; <span class="attr">offset</span>: <span class="number">1</span>, <span class="attr">color</span>: <span class="string">&#x27;#ffffff&#x27;</span> &#125;,</span><br><span class="line">      ]),</span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="attr">emphasis</span>: &#123;</span><br><span class="line">      <span class="attr">color</span>: <span class="keyword">new</span> echarts.<span class="property">graphic</span>.<span class="title class_">LinearGradient</span>(<span class="number">0</span>, <span class="number">0</span>, <span class="number">0</span>, <span class="number">1</span>, [</span><br><span class="line">        &#123;<span class="attr">offset</span>: <span class="number">0</span>, <span class="attr">color</span>: <span class="string">&#x27;#178ffc&#x27;</span>&#125;,</span><br><span class="line">        &#123;<span class="attr">offset</span>: <span class="number">1</span>, <span class="attr">color</span>: <span class="string">&#x27;#ffffff&#x27;</span>&#125;</span><br><span class="line">      ])</span><br><span class="line">    &#125;</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><p>echarts中，横轴数据如果非常多，会自动隐藏一部分数据，我们可以通过属性 <code>interval</code> 来进行调整。</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">xAxis</span>: &#123;</span><br><span class="line">  <span class="attr">type</span>: <span class="string">&#x27;category&#x27;</span>,</span><br><span class="line">  <span class="attr">data</span>: labels,</span><br><span class="line">  <span class="attr">axisLabel</span>: &#123;</span><br><span class="line">    <span class="attr">interval</span>: <span class="number">0</span>,</span><br><span class="line">  &#125;,</span><br><span class="line">&#125;,</span><br></pre></td></tr></table></figure>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/95a45272613e/</id>
    <link href="https://blog.zisheng.pro/posts/95a45272613e/"/>
    <published>2020-06-05T06:05:24.000Z</published>
    <summary>这里记录了一些实用的 Echarts 开发技巧</summary>
    <title>Echarts 开发技巧</title>
    <updated>2026-09-18T08:15:28.632Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="CSS3" scheme="https://blog.zisheng.pro/tags/CSS3/"/>
    <category term="flex" scheme="https://blog.zisheng.pro/tags/flex/"/>
    <category term="lineHeight" scheme="https://blog.zisheng.pro/tags/lineHeight/"/>
    <category term="textAlign" scheme="https://blog.zisheng.pro/tags/textAlign/"/>
    <category term="height" scheme="https://blog.zisheng.pro/tags/height/"/>
    <content>
      <![CDATA[<p>居中的效果是Web开发中常见的需求，几乎和阴影、圆角可以并列为产品三大法宝，本文的目的是捋顺开发中常用的居中计较，方便记忆和日后查阅。为了方便演示，本文所有 demo 使用 React 的 CSS-in-JS 语法完成。</p><h2 id="单行文本"><a href="#单行文本" class="headerlink" title="单行文本"></a>单行文本</h2><h3 id="height、lineHeight、textAlign"><a href="#height、lineHeight、textAlign" class="headerlink" title="height、lineHeight、textAlign"></a>height、lineHeight、textAlign</h3><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">&lt;div</span><br><span class="line">  style=&#123;&#123;</span><br><span class="line">    <span class="attr">boxSizing</span>: <span class="string">&#x27;border-box&#x27;</span>,</span><br><span class="line">    <span class="attr">height</span>: <span class="string">&#x27;100px&#x27;</span>,</span><br><span class="line">    <span class="attr">width</span>: <span class="string">&#x27;300px&#x27;</span>,</span><br><span class="line">    <span class="attr">background</span>: <span class="string">&#x27;purple&#x27;</span>,</span><br><span class="line">    <span class="attr">color</span>: <span class="string">&#x27;#fff&#x27;</span>,</span><br><span class="line">    <span class="attr">textAlign</span>: <span class="string">&#x27;center&#x27;</span>,</span><br><span class="line">    <span class="attr">lineHeight</span>: <span class="string">&#x27;98px&#x27;</span>,</span><br><span class="line">    <span class="attr">border</span>: <span class="string">&#x27;1px solid red&#x27;</span>,</span><br><span class="line">  &#125;&#125;</span><br><span class="line">&gt;</span><br><span class="line">  单行文本</span><br><span class="line">&lt;/div&gt;</span><br></pre></td></tr></table></figure><h3 id="Flex"><a href="#Flex" class="headerlink" title="Flex"></a>Flex</h3><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">&lt;div</span><br><span class="line">  style=&#123;&#123;</span><br><span class="line">    <span class="attr">height</span>: <span class="string">&#x27;100px&#x27;</span>,</span><br><span class="line">    <span class="attr">width</span>: <span class="string">&#x27;300px&#x27;</span>,</span><br><span class="line">    <span class="attr">background</span>: <span class="string">&#x27;purple&#x27;</span>,</span><br><span class="line">    <span class="attr">color</span>: <span class="string">&#x27;#fff&#x27;</span>,</span><br><span class="line">    <span class="attr">display</span>: <span class="string">&#x27;flex&#x27;</span>,</span><br><span class="line">    <span class="attr">justifyContent</span>: <span class="string">&#x27;center&#x27;</span>,</span><br><span class="line">    <span class="attr">alignItems</span>: <span class="string">&#x27;center&#x27;</span>,</span><br><span class="line">  &#125;&#125;</span><br><span class="line">&gt;</span><br><span class="line">  单行文本</span><br><span class="line">&lt;/div&gt;</span><br></pre></td></tr></table></figure><h2 id="多行文本居中"><a href="#多行文本居中" class="headerlink" title="多行文本居中"></a>多行文本居中</h2><h3 id="table-cell、vertical-align"><a href="#table-cell、vertical-align" class="headerlink" title="table-cell、vertical-align"></a>table-cell、vertical-align</h3><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">&lt;div</span><br><span class="line">  style=&#123;&#123;</span><br><span class="line">    <span class="attr">boxSizing</span>: <span class="string">&#x27;border-box&#x27;</span>,</span><br><span class="line">    <span class="attr">height</span>: <span class="string">&#x27;100px&#x27;</span>,</span><br><span class="line">    <span class="attr">width</span>: <span class="string">&#x27;300px&#x27;</span>,</span><br><span class="line">    <span class="attr">background</span>: <span class="string">&#x27;purple&#x27;</span>,</span><br><span class="line">    <span class="attr">color</span>: <span class="string">&#x27;#fff&#x27;</span>,</span><br><span class="line">    <span class="attr">display</span>: <span class="string">&#x27;table-cell&#x27;</span>,</span><br><span class="line">    <span class="attr">verticalAlign</span>: <span class="string">&#x27;middle&#x27;</span>,</span><br><span class="line">  &#125;&#125;</span><br><span class="line">&gt;</span><br><span class="line">  多行文本多行文本多行文本多行文本多行文本多行文本多行文本多行文本多行文本多行文本多行文本多行文本多行文本多行文本多行文本多行文本多行文本多行文本</span><br><span class="line">&lt;/div&gt;</span><br></pre></td></tr></table></figure><h3 id="Flex-1"><a href="#Flex-1" class="headerlink" title="Flex"></a>Flex</h3><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">&lt;div</span><br><span class="line">  style=&#123;&#123;</span><br><span class="line">    <span class="attr">height</span>: <span class="string">&#x27;100px&#x27;</span>,</span><br><span class="line">    <span class="attr">width</span>: <span class="string">&#x27;300px&#x27;</span>,</span><br><span class="line">    <span class="attr">background</span>: <span class="string">&#x27;purple&#x27;</span>,</span><br><span class="line">    <span class="attr">color</span>: <span class="string">&#x27;#fff&#x27;</span>,</span><br><span class="line">    <span class="attr">display</span>: <span class="string">&#x27;flex&#x27;</span>,</span><br><span class="line">    <span class="attr">justifyContent</span>: <span class="string">&#x27;center&#x27;</span>,</span><br><span class="line">    <span class="attr">alignItems</span>: <span class="string">&#x27;center&#x27;</span>,</span><br><span class="line">  &#125;&#125;</span><br><span class="line">&gt;</span><br><span class="line">  多行文本多行文本多行文本多行文本多行文本多行文本多行文本多行文本多行文本多行文本多行文本多行文本多行文本多行文本多行文本多行文本多行文本多行文本</span><br><span class="line">&lt;/div&gt;</span><br></pre></td></tr></table></figure><h2 id="图片居中"><a href="#图片居中" class="headerlink" title="图片居中"></a>图片居中</h2><h3 id="table-cell、verticalAlign、textAlign"><a href="#table-cell、verticalAlign、textAlign" class="headerlink" title="table-cell、verticalAlign、textAlign"></a>table-cell、verticalAlign、textAlign</h3><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">&lt;div</span><br><span class="line">  style=&#123;&#123;</span><br><span class="line">    <span class="attr">height</span>: <span class="string">&#x27;100px&#x27;</span>,</span><br><span class="line">    <span class="attr">width</span>: <span class="string">&#x27;300px&#x27;</span>,</span><br><span class="line">    <span class="attr">background</span>: <span class="string">&#x27;purple&#x27;</span>,</span><br><span class="line">    <span class="attr">display</span>: <span class="string">&#x27;table-cell&#x27;</span>,</span><br><span class="line">    <span class="attr">verticalAlign</span>: <span class="string">&#x27;middle&#x27;</span>,</span><br><span class="line">    <span class="attr">textAlign</span>: <span class="string">&#x27;center&#x27;</span>,</span><br><span class="line">  &#125;&#125;</span><br><span class="line">&gt;</span><br><span class="line">  <span class="language-xml"><span class="tag">&lt;<span class="name">img</span> <span class="attr">src</span>=<span class="string">&quot;http://iph.href.lu/50x50&quot;</span> <span class="attr">alt</span>=<span class="string">&quot;&quot;</span> /&gt;</span></span></span><br><span class="line">&lt;/div&gt;</span><br></pre></td></tr></table></figure><h3 id="Flex-2"><a href="#Flex-2" class="headerlink" title="Flex"></a>Flex</h3><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">&lt;div</span><br><span class="line">  style=&#123;&#123;</span><br><span class="line">    <span class="attr">height</span>: <span class="string">&#x27;100px&#x27;</span>,</span><br><span class="line">    <span class="attr">width</span>: <span class="string">&#x27;300px&#x27;</span>,</span><br><span class="line">    <span class="attr">background</span>: <span class="string">&#x27;purple&#x27;</span>,</span><br><span class="line">    <span class="attr">display</span>: <span class="string">&#x27;flex&#x27;</span>,</span><br><span class="line">    <span class="attr">justifyContent</span>: <span class="string">&#x27;center&#x27;</span>,</span><br><span class="line">    <span class="attr">alignItems</span>: <span class="string">&#x27;center&#x27;</span>,</span><br><span class="line">  &#125;&#125;</span><br><span class="line">&gt;</span><br><span class="line">  <span class="language-xml"><span class="tag">&lt;<span class="name">img</span> <span class="attr">src</span>=<span class="string">&quot;http://iph.href.lu/50x50&quot;</span> <span class="attr">alt</span>=<span class="string">&quot;&quot;</span> /&gt;</span></span></span><br><span class="line">&lt;/div&gt;</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><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line">&lt;div</span><br><span class="line">  style=&#123;&#123;</span><br><span class="line">    <span class="attr">height</span>: <span class="string">&#x27;100px&#x27;</span>,</span><br><span class="line">    <span class="attr">width</span>: <span class="string">&#x27;300px&#x27;</span>,</span><br><span class="line">    <span class="attr">background</span>: <span class="string">&#x27;purple&#x27;</span>,</span><br><span class="line">    <span class="attr">position</span>: <span class="string">&#x27;relative&#x27;</span>,</span><br><span class="line">  &#125;&#125;</span><br><span class="line">&gt;</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">style</span>=<span class="string">&#123;&#123;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">      <span class="attr">background:</span> &#x27;#<span class="attr">fff</span>&#x27;,</span></span></span><br><span class="line"><span class="tag"><span class="language-xml">      <span class="attr">width:</span> &#x27;<span class="attr">50px</span>&#x27;,</span></span></span><br><span class="line"><span class="tag"><span class="language-xml">      <span class="attr">height:</span> &#x27;<span class="attr">50px</span>&#x27;,</span></span></span><br><span class="line"><span class="tag"><span class="language-xml">      <span class="attr">position:</span> &#x27;<span class="attr">absolute</span>&#x27;,</span></span></span><br><span class="line"><span class="tag"><span class="language-xml">      <span class="attr">left:</span> &#x27;<span class="attr">50</span>%&#x27;,</span></span></span><br><span class="line"><span class="tag"><span class="language-xml">      <span class="attr">top:</span> &#x27;<span class="attr">50</span>%&#x27;,</span></span></span><br><span class="line"><span class="tag"><span class="language-xml">      <span class="attr">transform:</span> &#x27;<span class="attr">translate</span>(<span class="attr">-50</span>%,<span class="attr">-50</span>%)&#x27;,</span></span></span><br><span class="line"><span class="tag"><span class="language-xml">    &#125;&#125;</span></span></span><br><span class="line"><span class="tag"><span class="language-xml">  /&gt;</span></span></span><br><span class="line">&lt;/div&gt;</span><br></pre></td></tr></table></figure><h3 id="Flex-3"><a href="#Flex-3" class="headerlink" title="Flex"></a>Flex</h3><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line">&lt;div</span><br><span class="line">  style=&#123;&#123;</span><br><span class="line">    <span class="attr">height</span>: <span class="string">&#x27;100px&#x27;</span>,</span><br><span class="line">    <span class="attr">width</span>: <span class="string">&#x27;300px&#x27;</span>,</span><br><span class="line">    <span class="attr">background</span>: <span class="string">&#x27;purple&#x27;</span>,</span><br><span class="line">    <span class="attr">display</span>: <span class="string">&#x27;flex&#x27;</span>,</span><br><span class="line">    <span class="attr">justifyContent</span>: <span class="string">&#x27;center&#x27;</span>,</span><br><span class="line">    <span class="attr">alignItems</span>: <span class="string">&#x27;center&#x27;</span>,</span><br><span class="line">  &#125;&#125;</span><br><span class="line">&gt;</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">style</span>=<span class="string">&#123;&#123;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">      <span class="attr">background:</span> &#x27;#<span class="attr">fff</span>&#x27;,</span></span></span><br><span class="line"><span class="tag"><span class="language-xml">      <span class="attr">width:</span> &#x27;<span class="attr">50px</span>&#x27;,</span></span></span><br><span class="line"><span class="tag"><span class="language-xml">      <span class="attr">height:</span> &#x27;<span class="attr">50px</span>&#x27;,</span></span></span><br><span class="line"><span class="tag"><span class="language-xml">    &#125;&#125;</span></span></span><br><span class="line"><span class="tag"><span class="language-xml">  /&gt;</span></span></span><br><span class="line">&lt;/div&gt;</span><br></pre></td></tr></table></figure>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/97c353d732f9/</id>
    <link href="https://blog.zisheng.pro/posts/97c353d732f9/"/>
    <published>2020-06-03T05:35:13.000Z</published>
    <summary>居中的效果是Web开发中常见的需求，几乎和阴影、圆角可以并列为产品三大法宝，本文的目的是捋顺开发中常用的居中计较，方便记忆和日后查阅。为了方便演示，本文所有 demo 使用 React 的 CSS-in-JS 语法完成。</summary>
    <title>CSS 中的居中操作</title>
    <updated>2026-09-18T08:15:28.629Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="React Native" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/React-Native/"/>
    <category term="React Native" scheme="https://blog.zisheng.pro/tags/React-Native/"/>
    <content>
      <![CDATA[<h2 id="code-push-server"><a href="#code-push-server" class="headerlink" title="code-push-server"></a>code-push-server</h2><h2 id="code-push-cli"><a href="#code-push-cli" class="headerlink" title="code-push-cli"></a>code-push-cli</h2><p>由于 <code>appcenter-cli</code> 只支持微软官方的 CodePush 服务，因此我们只好继续使用 code-push-cli。</p><h3 id="登录"><a href="#登录" class="headerlink" title="登录"></a>登录</h3><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">code-push login http://192.168.2.119:3000/</span></span><br></pre></td></tr></table></figure><p>这时候，浏览器会弹出页面让你填写密码，以此获取 <code>accessKey</code>，将获取的<code>accessKey</code>填回命令行，即可登录成功，记住这个 <code>accessKey</code>，下次即可通过以下命令直接登录：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">code-push login http://192.168.2.119:3000/ --accessKey 8CB9b37oz6CuHyW2vwsDaXut6biy4ksvOXqog</span></span><br></pre></td></tr></table></figure><h3 id="添加应用"><a href="#添加应用" class="headerlink" title="添加应用"></a>添加应用</h3><blockquote><p><code>code-push app add &lt;appName&gt; &lt;os&gt; &lt;platform&gt;</code></p></blockquote><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">code-push app add ios_myapp ios react-native</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">code-push app add android_myapp Android react-native</span></span><br></pre></td></tr></table></figure><blockquote><p>注意：如果你的应用分为 Android 和 ios 版，那么注册应用的时候需要注册两个 App 获取两套 <code>deployment key</code>，可以通过 <code>code-push deployment ls &lt;appName&gt; -k</code> 获取部署的密钥</p></blockquote><h3 id="操作应用"><a href="#操作应用" class="headerlink" title="操作应用"></a>操作应用</h3><ul><li>移除应用：<code>code-push app remove|rm &lt;appName&gt;</code></li><li>重命名应用：<code>code-push app rename &lt;currentAppName&gt; &lt;newAppName&gt;</code></li><li>应用列表：<code>code-push app list|ls</code></li></ul><h3 id="添加部署"><a href="#添加部署" class="headerlink" title="添加部署"></a>添加部署</h3><p>添加应用后，它默认包含两个部署环境：<code>Staging</code> 和 <code>Production</code> 。这让你可以理解发布更新到一个内部的环境，你可以在推送到终端用户之前彻底的测试每个更新。这个工作流是至关重要的，以确保你的版本准备好给大众，而且这是一个在Web上实践很久的惯例。</p><p>如果你的App有 <code>Staging</code> 和 <code>Production</code> 环境其实已经满足了你的需求，然后你不需要做任何事情。不过，如果你需要 <code>alpha</code>，<code>dev</code>等部署环境，那你可以简单的使用如下命令创建：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">code-push deployment add &lt;appName&gt; &lt;deploymentName&gt;</span></span><br></pre></td></tr></table></figure><h3 id="操作部署"><a href="#操作部署" class="headerlink" title="操作部署"></a>操作部署</h3><ul><li><code>code-push deployment clear &lt;appName&gt; &lt;deploymentName&gt;</code>： 清除部署历史</li><li><code>code-push deployment remove|rm &lt;appName&gt; &lt;deploymentName&gt;</code>：删除一个部署</li><li><code>code-push deployment rename &lt;appName&gt; &lt;currentDeploymentName&gt; &lt;newDeploymentName&gt;</code>：重命名部署</li><li><code>code-push deployment list|ls &lt;appName&gt; [options]</code>：部署列表<ul><li><code>--format</code> ：Output format to display your deployments with (<code>&quot;json&quot;</code>or <code>&quot;table&quot;</code>) [字符串] [默认值: <code>&quot;table&quot;</code>]</li><li><code>--displayKeys</code>,<code> -k</code>： Specifies whether to display the deployment keys [布尔] [默认值: <code>false</code>]</li></ul></li><li><code>code-push deployment history|h &lt;appName&gt; &lt;deploymentName&gt; [options]</code>：查看发布的历史记录<ul><li><code>--format</code>：Output format to display the release history with (<code>&quot;json&quot;</code> or <code>&quot;table&quot;</code>) [字符串] [默认值: <code>&quot;table&quot;</code>]</li><li><code>--displayAuthor</code>,<code> -a</code>：Specifies whether to display the release author [布尔] [默认值: <code>false</code>]</li></ul></li></ul><h3 id="发布部署"><a href="#发布部署" class="headerlink" title="发布部署"></a>发布部署</h3><ul><li><code>code-push release-react &lt;appName&gt; &lt;platform&gt; [options]</code>: 发布新的热更新版本<ul><li><code>code-push release-react android_myapp android --gradleFile=android/app/build.gradle</code></li><li><code>code-push release-react ios_myapp ios --plistFile=ios/betty/Info.plist</code></li></ul></li></ul><p>其中参数 <code>-t</code> 为二进制(<code>.ipa</code>与<code>apk</code>)安装包的的版本，建议动态读；<code>--dev</code> 为是否启用开发者模式(默认为<code>false</code>)；<code>–d</code> 是要发布更新的环境分 <code>Production</code> 与 <code>Staging</code> (默认为 <code>Staging</code>)；<code>--des</code> 为更新说明；<code>–m</code> 是强制更新。</p><h2 id="react-native-code-push"><a href="#react-native-code-push" class="headerlink" title="react-native-code-push"></a>react-native-code-push</h2><h3 id="安装组件"><a href="#安装组件" class="headerlink" title="安装组件"></a>安装组件</h3><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">yarn add react-native-code-push</span></span><br></pre></td></tr></table></figure><h3 id="iOS设置"><a href="#iOS设置" class="headerlink" title="iOS设置"></a>iOS设置</h3><p>获得 CodePush 插件后，必须将其集成到 React Native 应用程序的 Xcode 项目中并正确配置。</p><ol><li><p>运行<code>cd ios &amp;&amp; pod install &amp;&amp; cd ..</code>以安装所有必需的CocoaPods依赖项。</p></li><li><p>打开<code>AppDelegate.m</code>文件，并为CodePush标头添加导入语句：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">#import &lt;CodePush/CodePush.h&gt;</span><br></pre></td></tr></table></figure></li><li><p>查找以下代码行，该代码为生产版本的网桥设置源URL：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">return [[NSBundle mainBundle] URLForResource:@&quot;main&quot; withExtension:@&quot;jsbundle&quot;];</span><br></pre></td></tr></table></figure></li><li><p>用以下行替换它：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">return [CodePush bundleURL];</span><br></pre></td></tr></table></figure><p>此更改将你的应用配置为始终加载应用JS捆绑包的最新版本。在首次启动时，这将与使用该应用程序编译的文件相对应。但是，在通过CodePush推送更新后，这将返回最近安装的更新的位置。</p><p>通常，您只想使用 CodePush 来解决发行版本中的 JS 包位置，因此，我们建议使用<code>DEBUG</code>预处理器宏在是否使用打包程序服务器和 CodePush 之间进行动态切换。这样可以更轻松地确保您在生产中获得所需的正确行为，同时仍可以在调试时使用Chrome开发工具，实时重新加载等。</p><p>你的<code>sourceURLForBridge</code>方法应如下所示：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge</span><br><span class="line">&#123;</span><br><span class="line">  #if DEBUG</span><br><span class="line">    return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@&quot;index&quot; fallbackResource:nil];</span><br><span class="line">  #else</span><br><span class="line">    return [CodePush bundleURL];</span><br><span class="line">  #endif</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></li><li><p>将部署密钥添加到<code>Info.plist</code>：</p><p>为了让 CodePush 运行时知道应该针对哪个部署查询更新，请打开你的应用的 <code>Info.plist</code> 文件，并添加一个名为<code>CodePushDeploymentKey</code>的新条目，其值是你要配置的应用的 <code>Staging Deployment Key</code>。</p><p>你可以通过 <code>code-push deployment ls &lt;appName&gt; -k</code> 来检索这个值（该 <code>-k</code> 标志是必需的，因为默认情况下不会显示键），然后复制相对应的 <code>Deployment Key</code> 即可。</p><p><img src="https://user-gold-cdn.xitu.io/2020/2/27/170823d9cfe4d5ba?w=1764&h=414&f=png&s=250595" alt="紫升"></p><p>为了有效利用与 CodePush 应用程序一起创建的 <code>Staging</code> 和 <code>Production</code> 部署，请在实际将你的应用程序对 CodePush 的使用移入生产环境之前，进行<strong>多部署测试</strong>的配置。</p></li></ol><h4 id="多部署测试"><a href="#多部署测试" class="headerlink" title="多部署测试"></a>多部署测试</h4><blockquote><p>该部分适用于 Xcode 11</p></blockquote><p>Xcode 允许你为每个<strong>配置</strong> (如 <code>debug</code>, <code>release</code>) 自定义构建设置，然后可以将其引用为 <code>Info.plist</code> 文件中的键值（如 <code>CodePushDeploymentKey</code> 设置）。此机制是您可以轻松地进行构建配置以生成二进制文件，这些二进制文件被配置为与不同的 CodePush 部署同步。</p><p>要进行设置，请执行以下步骤：</p><ol><li><p>打开您的 Xcode 项目，然后在 <code>Project navigator</code> 窗口中选择您的项目</p></li><li><p>确保已选择 <code>PROJECT</code> 节点，而不是 <code>TARGETS</code></p></li><li><p>选择 <code>Info</code> 标签</p></li><li><p>点击 <code>+</code> 的内部按钮<code>Duplicate &quot;Release&quot; Configuration</code><br> <img src="https://user-gold-cdn.xitu.io/2020/3/9/170bb342a289a24a?w=1026&h=366&f=png&s=63534" alt="紫升"></p></li><li><p>将新配置命名为 <code>Staging</code>（或您喜欢的任何名称）</p></li><li><p>选择 <code>Build Settings</code> 选项卡</p></li><li><p>单击工具栏上的 <code>+</code> 按钮，创建一个名为  <code>CONFIGURATION_BUILD_DIR</code> 的 <code>User-Defined Setting</code>, 使用相同的 <code>per-configuration</code> 配置。</p>  <figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$</span><span class="language-bash">(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)`为`$(BUILD_DIR)/Release$(EFFECTIVE_PLATFORM_NAME)`</span></span><br></pre></td></tr></table></figure></li></ol><p><img src="https://user-gold-cdn.xitu.io/2020/3/9/170bb34307cf8b7f?w=1058&h=316&f=png&s=99721" alt="紫升"></p><blockquote><p>注意：每次创建这个 Xcode 都会崩溃，只能先把值写入之后，在 <code>project.pbxproj</code> 中新建。</p></blockquote><ol start="8"><li><p>点击工具栏的 <code>+</code>  并选择 <code>Add User-Defined Setting</code></p><p><img src="https://user-gold-cdn.xitu.io/2020/3/9/170bb342a1fca283?w=1308&h=228&f=png&s=62560" alt="紫升"></p></li><li><p>将此新设置命名为<code>CodePushDeploymentKey</code>，展开它，然后为 <code>Staging </code>配置指定您的 <code>Staging</code> 部署密钥，为 <code>Release</code> 配置指定您的 <code>Production</code> 部署密钥。</p><p><img src="https://user-gold-cdn.xitu.io/2020/3/9/170bb342d671ab43?w=890&h=172&f=png&s=67687" alt="紫升"></p><blockquote><p>提醒一下，您可以通过<code>appcenter codepush deployment list -a &lt;ownerName&gt;/&lt;appName&gt; --displayKeys</code>从终端运行来检索这些键。</p></blockquote></li><li><p>打开项目的 <code>Info.plist</code> 文件，然后将<code>CodePushDeploymentKey</code>条目的值更改为<code>$(CODEPUSH_KEY)</code></p><p><img src="https://user-gold-cdn.xitu.io/2020/3/9/170bb342f42a6b4e?w=890&h=110&f=png&s=29574" alt="紫升"></p></li></ol><p>就是这样了，现在当你运行或构建你的App，你的 <code>Staging</code> 包将自动同步你的 <code>Staging</code> 部署，你的 <code>Release</code> 包将自动同步你的 <code>Production</code> 部署。</p><h3 id="Android设置"><a href="#Android设置" class="headerlink" title="Android设置"></a>Android设置</h3><p>为了将CodePush集成到您的Android项目中，请执行以下步骤：</p><ol><li><p>在<code>android/app/build.gradle</code>文件中，将文件<code>codepush.gradle</code>添加为下面的其他构建任务定义：</p><figure class="highlight groovy"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">...</span><br><span class="line">apply <span class="attr">from:</span> <span class="string">&quot;../../node_modules/react-native-code-push/android/codepush.gradle&quot;</span></span><br><span class="line">...</span><br></pre></td></tr></table></figure></li><li><p><code>MainApplication.java</code>通过以下更改更新文件以使用 CodePush：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">...</span><br><span class="line"><span class="comment">// 1. 导入插件的类</span></span><br><span class="line"><span class="keyword">import</span> com.microsoft.codepush.react.CodePush;</span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">MainApplication</span> <span class="keyword">extends</span> <span class="title class_">Application</span> <span class="keyword">implements</span> <span class="title class_">ReactApplication</span> &#123;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> <span class="type">ReactNativeHost</span> <span class="variable">mReactNativeHost</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">ReactNativeHost</span>(<span class="built_in">this</span>) &#123;</span><br><span class="line">        ...</span><br><span class="line">        <span class="comment">// 2. 重写 getJSBundleFile 方法，每次 app 启动的时候让 CodePush 运行时决定从哪里加载 JS bundle</span></span><br><span class="line">        <span class="meta">@Override</span></span><br><span class="line">        <span class="keyword">protected</span> String <span class="title function_">getJSBundleFile</span><span class="params">()</span> &#123;</span><br><span class="line">            <span class="keyword">return</span> CodePush.getJSBundleFile();</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></li><li><p>将部署密钥添加到<code>strings.xml</code>：</p><p>为了让 CodePush 运行时知道它应该查询哪些部署更新，请打开您的应用程序的 <code>string.xml</code> 文件，并添加一个名为 <code>CodePushDeploymentKey</code> 的新字符串，它的值是应用的 <code>Staging</code> 部署。你可以通过 <code>appcenter deployment list &lt;ownerName&gt;/&lt;appName&gt; -k</code> 获取该值。</p><p><img src="https://user-gold-cdn.xitu.io/2020/2/27/170823d9cfe4d5ba?w=1764&h=414&f=png&s=250595" alt="紫升"></p><p>您<code>strings.xml</code>应该看起来像这样：</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">resources</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">string</span> <span class="attr">name</span>=<span class="string">&quot;app_name&quot;</span>&gt;</span>AppName<span class="tag">&lt;/<span class="name">string</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">string</span> <span class="attr">moduleConfig</span>=<span class="string">&quot;true&quot;</span> <span class="attr">name</span>=<span class="string">&quot;CodePushDeploymentKey&quot;</span>&gt;</span>DeploymentKey<span class="tag">&lt;/<span class="name">string</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">resources</span>&gt;</span></span><br></pre></td></tr></table></figure><p>为了有效利用与CodePush应用程序一起创建的<code>Staging</code>和<code>Production</code>部署，请在实际将您的应用程序对CodePush的使用移入生产环境之前，请参考下面的<strong>多部署测试</strong>。</p></li></ol><h4 id="多部署测试-1"><a href="#多部署测试-1" class="headerlink" title="多部署测试"></a>多部署测试</h4><figure class="highlight groovy"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line">android &#123;</span><br><span class="line">  ...</span><br><span class="line">  buildTypes &#123;</span><br><span class="line">    debug &#123;</span><br><span class="line">      signingConfig signingConfigs.debug</span><br><span class="line">      <span class="comment">// Note: CodePush updates should not be tested in Debug mode as they are overriden by the RN packager. However, because CodePush checks for updates in all modes, we must supply a key.</span></span><br><span class="line">      resValue <span class="string">&quot;string&quot;</span>, <span class="string">&quot;CodePushDeploymentKey&quot;</span>, <span class="string">&#x27;&quot;&quot;&#x27;</span></span><br><span class="line">    &#125;</span><br><span class="line">    release &#123;</span><br><span class="line">      <span class="comment">// Caution! In production, you need to generate your own keystore file.</span></span><br><span class="line">      <span class="comment">// see https://facebook.github.io/react-native/docs/signed-apk-android.</span></span><br><span class="line">      signingConfig signingConfigs.release</span><br><span class="line">      minifyEnabled enableProguardInReleaseBuilds</span><br><span class="line">      proguardFiles getDefaultProguardFile(<span class="string">&quot;proguard-android.txt&quot;</span>), <span class="string">&quot;proguard-rules.pro&quot;</span></span><br><span class="line">      resValue <span class="string">&quot;string&quot;</span>, <span class="string">&quot;CodePushDeploymentKey&quot;</span>, <span class="string">&#x27;&quot;&quot;&#x27;</span></span><br><span class="line">    &#125;</span><br><span class="line">    releaseStaging.initWith(release)</span><br><span class="line">    releaseStaging &#123;</span><br><span class="line">        resValue <span class="string">&quot;string&quot;</span>, <span class="string">&quot;CodePushDeploymentKey&quot;</span>, <span class="string">&#x27;&quot;&quot;&#x27;</span></span><br><span class="line">        <span class="comment">// Note: It is a good idea to provide matchingFallbacks for the new buildType you create to prevent build issues</span></span><br><span class="line">        <span class="comment">// Add the following line if not already there</span></span><br><span class="line">        matchingFallbacks = [<span class="string">&#x27;release&#x27;</span>]</span><br><span class="line">    &#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><h2 id="动态部署分配"><a href="#动态部署分配" class="headerlink" title="动态部署分配"></a>动态部署分配</h2><p>上一节说明了如何利用多个<code>CodePush</code>部署，以便在更新发布给用户之前，有效地测试您的更新内容。 但是，由于该工作流静态地将部署分配嵌入到实际二进制文件中，因此<code>临时构建</code>和<code>生产构建</code>只会同步该部署的更新内容。</p><p>在许多情况下，这是足够的，因为您只希望您的团队，客户，利益相关者等与您的预生产版本同步，因此，他们只需要知道如何与该版本同步构建。</p><p>但是，如果你希望能够进行 <code>A / B</code> 测试，或者为某些用户提供应用程序的早期访问权限，那么能够在运行时将特定用户（或受众）动态地置于特定部署中将非常有用。</p><p>为了实现此工作流程，你需要做的就是指定调用该<code>codePush</code>方法时希望当前用户与之同步的部署密钥。指定后，此密钥将覆盖应用程序的 <code>Info.plist</code>（iOS）或 <code>MainActivity.java</code>（Android）文件中提供的“默认”密钥。这允许您生成临时或生产构建，也可以根据需要动态“重定向”。</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Imagine that &quot;userProfile&quot; is a prop that this component received</span></span><br><span class="line"><span class="comment">// which includes the deployment key that the current user should use.</span></span><br><span class="line">codePush.<span class="title function_">sync</span>(&#123; <span class="attr">deploymentKey</span>: userProfile.<span class="property">CODEPUSH_KEY</span> &#125;);</span><br></pre></td></tr></table></figure><p>有了这样的变化后，现在只需选择应用程序如何为当前用户配置正确的部署密钥。 在实践中，通常有两种解决方案：</p><ol><li>将更改部署的功能开放给用户。例如，您的设置页面可能会有一个切换按钮以启用“测试版”的访问权限。 如果您不在乎预生产更新的内容被得知，并且您的某些用户可能希望根据自己的意愿选择使用最新（并且可能有错误）的更新（有点像Chrome渠道）。 但是，此解决方案将决策权交给您的用户，这无法帮助您透明地执行 <code>A / B</code> 测试。</li><li>使用额外的元数据注释用户的服务器端配置文件，标明与其同步的部署。 默认情况下，您的应用只能使用二进制嵌入密钥，但在用户通过身份验证后，您的服务器可以选择将其“重定向”到其他部署，这样您就可以根据需要逐步将某些用户或组放置在不同的部署中。您甚至可以选择将服务器响应存储在本地存储中，以使其成为新的默认值。 如何将密钥与用户的配置文件一起存储完全取决于您的身份验证解决方案（例如 <code>Auth0</code>，<code>Firebase</code>，自定义<code>DB</code> + <code>REST API</code>），但这通常非常简单。</li></ol><blockquote><p>注意：如果需要，您还可以实施混合解决方案，允许最终用户在不同部署之间切换，同时还允许您的服务器覆盖该决策。 这样，您就拥有了“部署解决方案”的层次结构，可确保您的应用程序能够自行更新，用户可以通过获得最新内容的访问权限来获得最新体验，但您也有能力根据需要对用户进行 <code>A / B</code> 测试。</p></blockquote><p>由于我们建议将<code>Staging</code>部署用于更新的预发布测试（如上一节中所述），因此使用该部署对用户执行 <code>A / B</code> 测试并不一定有意义，与此相反，你应该允许早期访问（如上面选项1中所述）。因此，我们建议充分利用自定义应用程序部署，以便您可以按用户需求对用户进行细分。例如，您可以创建长期甚至是一次性的部署，向其发布应用程序的变体，然后将某些用户放入其中，以查看其参与度。</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">// <span class="comment">#1) Create your new deployment to hold releases of a specific app variant</span></span><br><span class="line">appcenter codepush deployment add -a &lt;ownerName&gt;/&lt;appName&gt; test-variant-one</span><br><span class="line"></span><br><span class="line">// <span class="comment">#2) Target any new releases at that custom deployment</span></span><br><span class="line">appcenter codepush release-react -a &lt;ownerName&gt;/&lt;appName&gt; -d test-variant-one</span><br></pre></td></tr></table></figure><blockquote><p>注意：从一个部署“切换”到另一个部署的用户数，被纳入到部署中的“安装度量”中报告的总用户数。例如，如果您的<code>Production</code> 部署当前报告的用户总数为1，但您将该用户动态切换为 <code>Staging</code> 部署，则 <code>Production</code> 部署将报告 0个总用户，而 <code>Staging</code> 部署将报告1（刚刚切换的用户）。 即使在使用基于运行时的部署重定向解决方案的情况下，这种行为可以让你准确地跟踪您的版本使用情况。</p></blockquote><h2 id="最佳实践"><a href="#最佳实践" class="headerlink" title="最佳实践"></a>最佳实践</h2><blockquote><p>源码：<a href="https://github.com/youngjuning/AppCenterCodePushDemo">https://github.com/youngjuning/AppCenterCodePushDemo</a></p></blockquote><h3 id="App-js"><a href="#App-js" class="headerlink" title="App.js"></a>App.js</h3><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">React</span>, &#123;useEffect&#125; <span class="keyword">from</span> <span class="string">&#x27;react&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123;<span class="title class_">View</span>, <span class="title class_">StyleSheet</span>&#125; <span class="keyword">from</span> <span class="string">&#x27;react-native&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> codePush <span class="keyword">from</span> <span class="string">&#x27;react-native-code-push&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> <span class="title class_">AwesomeButton</span> <span class="keyword">from</span> <span class="string">&#x27;react-native-really-awesome-button&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123;codePushSync, checkForUpdate&#125; <span class="keyword">from</span> <span class="string">&#x27;./CodePushUtils&#x27;</span>;</span><br><span class="line"><span class="keyword">const</span> <span class="title function_">App</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> <span class="title function_">getUpdateMetadata</span> = <span class="keyword">async</span> (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">    <span class="keyword">const</span> running = <span class="keyword">await</span> codePush.<span class="title function_">getUpdateMetadata</span>(</span><br><span class="line">      codePush.<span class="property">UpdateState</span>.<span class="property">RUNNING</span>,</span><br><span class="line">    );</span><br><span class="line">    <span class="keyword">const</span> pending = <span class="keyword">await</span> codePush.<span class="title function_">getUpdateMetadata</span>(</span><br><span class="line">      codePush.<span class="property">UpdateState</span>.<span class="property">PENDING</span>,</span><br><span class="line">    );</span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">&#x27;[CodePush] running&#x27;</span>, running);</span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">&#x27;[CodePush] pending&#x27;</span>, pending);</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="title function_">codePushSync</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">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">AwesomeButton</span> <span class="attr">onPress</span>=<span class="string">&#123;checkForUpdate&#125;</span> <span class="attr">style</span>=<span class="string">&#123;&#123;marginBottom:</span> <span class="attr">10</span>&#125;&#125;&gt;</span></span></span><br><span class="line"><span class="language-xml">        Check For Update!</span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;/<span class="name">AwesomeButton</span>&gt;</span></span></span><br><span class="line"><span class="language-xml"><span class="tag">&lt;<span class="name">AwesomeButton</span> <span class="attr">onPress</span>=<span class="string">&#123;()</span> =&gt;</span> codePush.clearUpdates()&#125; style=&#123;&#123;marginBottom: 10&#125;&#125;&gt;</span></span><br><span class="line"><span class="language-xml">        Clear Updates!</span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;/<span class="name">AwesomeButton</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">AwesomeButton</span> <span class="attr">onPress</span>=<span class="string">&#123;getUpdateMetadata&#125;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">        getUpdateMetadata!</span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;/<span class="name">AwesomeButton</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">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">    <span class="attr">alignItems</span>: <span class="string">&#x27;center&#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="title class_">App</span>;</span><br></pre></td></tr></table></figure><h3 id="CodePushUtils-js"><a href="#CodePushUtils-js" class="headerlink" title="CodePushUtils.js"></a>CodePushUtils.js</h3><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123;<span class="title class_">AppState</span>, <span class="title class_">Platform</span>, <span class="title class_">Alert</span>&#125; <span class="keyword">from</span> <span class="string">&#x27;react-native&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> codePush <span class="keyword">from</span> <span class="string">&#x27;react-native-code-push&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> configReader <span class="keyword">from</span> <span class="string">&#x27;react-native-config-reader&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="title class_">CodePushDeploymentKey</span> = &#123;</span><br><span class="line">  <span class="attr">ios</span>: &#123;</span><br><span class="line">    <span class="attr">debug</span>: <span class="string">&#x27;&#x27;</span>,</span><br><span class="line">    <span class="attr">staging</span>: <span class="string">&#x27;944zuIiRSds-ZZY6AQF82aRl0b1vUL_mMxiie&#x27;</span>,</span><br><span class="line">    <span class="attr">release</span>: <span class="string">&#x27;yyJfk2vtpLUUlOCg3FnvCcky9o4U1lEWR1UJV&#x27;</span>,</span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="attr">android</span>: &#123;</span><br><span class="line">    <span class="attr">debug</span>: <span class="string">&#x27;&#x27;</span>,</span><br><span class="line">    <span class="attr">releasestaging</span>: <span class="string">&#x27;tOncLvKACzzSkUaML9tCOUfPZxHVnobfaNIUe&#x27;</span>,</span><br><span class="line">    <span class="attr">release</span>: <span class="string">&#x27;Gtc4iXTPn24yu6CBrbl_V2GTy21xtdQyfm6x1&#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">const</span> <span class="title function_">getDeploymentKey</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> buildType = configReader.<span class="property">BUILD_TYPE</span>.<span class="title function_">toLowerCase</span>();</span><br><span class="line">  <span class="keyword">const</span> deploymentKey = <span class="title class_">CodePushDeploymentKey</span>[<span class="title class_">Platform</span>.<span class="property">OS</span>][buildType];</span><br><span class="line">  <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">&#x27;[CodePushUtils]&#x27;</span>, deploymentKey);</span><br><span class="line">  <span class="keyword">return</span> deploymentKey;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">codePushStatusDidChange</span> = <span class="keyword">async</span> syncStatus =&gt; &#123;</span><br><span class="line">  <span class="keyword">switch</span> (syncStatus) &#123;</span><br><span class="line">    <span class="keyword">case</span> codePush.<span class="property">SyncStatus</span>.<span class="property">CHECKING_FOR_UPDATE</span>:</span><br><span class="line">      <span class="comment">// 0 - 正在查询CodePush服务器以进行更新。</span></span><br><span class="line">      <span class="variable language_">console</span>.<span class="title function_">info</span>(<span class="string">&#x27;[CodePush] Checking for update.&#x27;</span>);</span><br><span class="line">      <span class="keyword">break</span>;</span><br><span class="line">    <span class="keyword">case</span> codePush.<span class="property">SyncStatus</span>.<span class="property">AWAITING_USER_ACTION</span>:</span><br><span class="line">      <span class="comment">// 1 - 有可用的更新，并且向最终用户显示了一个确认对话框。（仅在updateDialog使用时适用）</span></span><br><span class="line">      <span class="variable language_">console</span>.<span class="title function_">info</span>(<span class="string">&#x27;[CodePush] Awaiting user action.&#x27;</span>);</span><br><span class="line">      <span class="keyword">break</span>;</span><br><span class="line">    <span class="keyword">case</span> codePush.<span class="property">SyncStatus</span>.<span class="property">DOWNLOADING_PACKAGE</span>:</span><br><span class="line">      <span class="comment">// 2 - 正在从CodePush服务器下载可用更新。</span></span><br><span class="line">      <span class="variable language_">console</span>.<span class="title function_">info</span>(<span class="string">&#x27;[CodePush] Downloading package.&#x27;</span>);</span><br><span class="line">      <span class="keyword">break</span>;</span><br><span class="line">    <span class="keyword">case</span> codePush.<span class="property">SyncStatus</span>.<span class="property">INSTALLING_UPDATE</span>:</span><br><span class="line">      <span class="comment">// 3 - 已下载一个可用的更新，并将其安装。</span></span><br><span class="line">      <span class="variable language_">console</span>.<span class="title function_">info</span>(<span class="string">&#x27;[CodePush] Installing update.&#x27;</span>);</span><br><span class="line">      <span class="keyword">break</span>;</span><br><span class="line">    <span class="keyword">case</span> codePush.<span class="property">SyncStatus</span>.<span class="property">UP_TO_DATE</span>:</span><br><span class="line">      <span class="comment">// 4 - 应用程序已配置的部署完全最新。</span></span><br><span class="line">      <span class="variable language_">console</span>.<span class="title function_">info</span>(<span class="string">&#x27;[CodePush] App is up to date.&#x27;</span>);</span><br><span class="line">      <span class="keyword">break</span>;</span><br><span class="line">    <span class="keyword">case</span> codePush.<span class="property">SyncStatus</span>.<span class="property">UPDATE_IGNORED</span>:</span><br><span class="line">      <span class="comment">// 5 该应用程序具有可选更新，最终用户选择忽略该更新。（仅在updateDialog使用时适用）</span></span><br><span class="line">      <span class="variable language_">console</span>.<span class="title function_">info</span>(<span class="string">&#x27;[CodePush] User cancelled the update.&#x27;</span>);</span><br><span class="line">      <span class="keyword">break</span>;</span><br><span class="line">    <span class="keyword">case</span> codePush.<span class="property">SyncStatus</span>.<span class="property">UPDATE_INSTALLED</span>:</span><br><span class="line">      <span class="comment">// 6 - 安装了一个可用的更新，它将根据 SyncOptions 中的 InstallMode指定在 syncStatusChangedCallback 函数返回后立即或在下次应用恢复/重新启动时立即运行。</span></span><br><span class="line"></span><br><span class="line">      <span class="comment">// 由于安装了CodePush更新，暂时禁止任何程序性重启。这是高级API，当您应用中的组件（例如，入职流程）需要确保在其生命周期内不会出现最终用户中断时，此功能非常有用。</span></span><br><span class="line">      <span class="comment">// codePush.disallowRestart();</span></span><br><span class="line">      <span class="variable language_">console</span>.<span class="title function_">info</span>(<span class="string">&#x27;[CodePush] Installed update.&#x27;</span>);</span><br><span class="line">      <span class="keyword">break</span>;</span><br><span class="line">    <span class="keyword">case</span> codePush.<span class="property">SyncStatus</span>.<span class="property">SYNC_IN_PROGRESS</span>:</span><br><span class="line">      <span class="comment">// 7 - 正在执行的 sync 操作</span></span><br><span class="line">      <span class="variable language_">console</span>.<span class="title function_">info</span>(<span class="string">&#x27;[CodePush] Sync already in progress.&#x27;</span>);</span><br><span class="line">      <span class="keyword">break</span>;</span><br><span class="line">    <span class="keyword">case</span> codePush.<span class="property">SyncStatus</span>.<span class="property">UNKNOWN_ERROR</span>:</span><br><span class="line">      <span class="comment">// -1 - 同步操作遇到未知错误。</span></span><br><span class="line">      <span class="variable language_">console</span>.<span class="title function_">info</span>(<span class="string">&#x27;[CodePush] An unknown error occurred.&#x27;</span>);</span><br><span class="line">      <span class="keyword">break</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> <span class="title function_">codePushDownloadDidProgress</span> = progress =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> curPercent = (</span><br><span class="line">    (progress.<span class="property">receivedBytes</span> / progress.<span class="property">totalBytes</span>) *</span><br><span class="line">    <span class="number">100</span></span><br><span class="line">  ).<span class="title function_">toFixed</span>(<span class="number">0</span>);</span><br><span class="line">  <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">&#x27;[CodePushUtils] Downloading Progress&#x27;</span>, <span class="string">`<span class="subst">$&#123;curPercent&#125;</span>%`</span>);</span><br><span class="line">  <span class="comment">// console.log(`$&#123;progress.receivedBytes&#125; of $&#123;progress.totalBytes&#125; received.`);</span></span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">syncImmediate</span> = <span class="keyword">async</span> (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> deploymentKey = <span class="title function_">getDeploymentKey</span>();</span><br><span class="line">  codePush.<span class="title function_">sync</span>(</span><br><span class="line">    &#123;</span><br><span class="line">      <span class="attr">updateDialog</span>: &#123;</span><br><span class="line">        <span class="comment">// 是否显示更新描述</span></span><br><span class="line">        <span class="attr">appendReleaseDescription</span>: <span class="literal">true</span>,</span><br><span class="line">        <span class="comment">// 更新描述的前缀。 默认为&quot;Description&quot;</span></span><br><span class="line">        <span class="attr">descriptionPrefix</span>: <span class="string">&#x27;\n\n更新内容：\n&#x27;</span>,</span><br><span class="line">        <span class="comment">// 强制更新按钮文字，默认为continue</span></span><br><span class="line">        <span class="attr">mandatoryContinueButtonLabel</span>: <span class="string">&#x27;立即更新&#x27;</span>,</span><br><span class="line">        <span class="comment">// 强制更新时的信息. 默认为&quot;An update is available that must be installed.&quot;</span></span><br><span class="line">        <span class="attr">mandatoryUpdateMessage</span>: <span class="string">&#x27;必须更新后才能使用&#x27;</span>,</span><br><span class="line">        <span class="comment">// 非强制更新时，按钮文字,默认为&quot;ignore&quot;</span></span><br><span class="line">        <span class="attr">optionalIgnoreButtonLabel</span>: <span class="string">&#x27;稍后&#x27;</span>,</span><br><span class="line">        <span class="comment">// 非强制更新时，确认按钮文字. 默认为&quot;Install&quot;</span></span><br><span class="line">        <span class="attr">optionalInstallButtonLabel</span>: <span class="string">&#x27;后台更新&#x27;</span>,</span><br><span class="line">        <span class="comment">// 非强制更新时，检查到更新的消息文本</span></span><br><span class="line">        <span class="attr">optionalUpdateMessage</span>: <span class="string">&#x27;有新版本了，是否更新？&#x27;</span>,</span><br><span class="line">        <span class="comment">// Alert窗口的标题</span></span><br><span class="line">        <span class="attr">title</span>: <span class="string">&#x27;更新&#x27;</span>,</span><br><span class="line">      &#125;,</span><br><span class="line">      deploymentKey,</span><br><span class="line">      <span class="attr">installMode</span>: codePush.<span class="property">InstallMode</span>.<span class="property">IMMEDIATE</span>,</span><br><span class="line">    &#125;,</span><br><span class="line">    codePushStatusDidChange,</span><br><span class="line">    codePushDownloadDidProgress,</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">const</span> <span class="title function_">checkForUpdate</span> = <span class="keyword">async</span> (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> deploymentKey = <span class="title function_">getDeploymentKey</span>();</span><br><span class="line">  <span class="keyword">const</span> update = <span class="keyword">await</span> codePush.<span class="title function_">checkForUpdate</span>(deploymentKey);</span><br><span class="line">  <span class="keyword">if</span> (!update) &#123;</span><br><span class="line">    <span class="title class_">Alert</span>.<span class="title function_">alert</span>(<span class="string">&#x27;提示&#x27;</span>, <span class="string">&#x27;已是最新版本&#x27;</span>);</span><br><span class="line">  &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    <span class="title function_">syncImmediate</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">const</span> <span class="title function_">codePushSync</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">  <span class="title function_">syncImmediate</span>();</span><br><span class="line">  <span class="title class_">AppState</span>.<span class="title function_">addEventListener</span>(<span class="string">&#x27;change&#x27;</span>, <span class="function"><span class="params">newState</span> =&gt;</span> &#123;</span><br><span class="line">    newState === <span class="string">&#x27;active&#x27;</span> &amp;&amp; <span class="title function_">syncImmediate</span>();</span><br><span class="line">  &#125;);</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><h3 id="Npm-Scripts"><a href="#Npm-Scripts" class="headerlink" title="Npm Scripts"></a>Npm Scripts</h3><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><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><br><span class="line">    <span class="attr">&quot;gradle:clean&quot;</span><span class="punctuation">:</span> <span class="string">&quot;cd android &amp;&amp; ./gradlew clean&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;an:release&quot;</span><span class="punctuation">:</span> <span class="string">&quot;yarn gradle:clean &amp;&amp; cd android &amp;&amp; ./gradlew app:assembleRelease&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;an:installRelease&quot;</span><span class="punctuation">:</span> <span class="string">&quot;yarn gradle:clean &amp;&amp; cd android &amp;&amp; ./gradlew app:installRelease&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;an:staging&quot;</span><span class="punctuation">:</span> <span class="string">&quot;yarn gradle:clean &amp;&amp; cd android &amp;&amp; ./gradlew app:assembleReleaseStaging&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;an:installStaging&quot;</span><span class="punctuation">:</span> <span class="string">&quot;yarn gradle:clean &amp;&amp; cd android &amp;&amp; ./gradlew app:installReleaseStaging&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;displayKeys&quot;</span><span class="punctuation">:</span> <span class="string">&quot;yarn disPlayIosKeys &amp;&amp; yarn disPlayAndroidKeys&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;disPlayIosKeys&quot;</span><span class="punctuation">:</span> <span class="string">&quot;appcenter codepush deployment list --app youngjuning/AppCenterCodePushDemo-iOS --displayKeys&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;disPlayAndroidKeys&quot;</span><span class="punctuation">:</span> <span class="string">&quot;appcenter codepush deployment list --app youngjuning/AppCenterCodePushDemo-Android --displayKeys&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;release-react&quot;</span><span class="punctuation">:</span> <span class="string">&quot;yarn release-react-ios &amp;&amp; yarn release-react-android&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;release-react-ios&quot;</span><span class="punctuation">:</span> <span class="string">&quot;appcenter codepush release-react --app youngjuning/AppCenterCodePushDemo-iOS&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;release-react-android&quot;</span><span class="punctuation">:</span> <span class="string">&quot;appcenter codepush release-react --app youngjuning/AppCenterCodePushDemo-Android&quot;</span></span><br><span class="line">    ...</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></pre></td></tr></table></figure>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/2233b15714d5/</id>
    <link href="https://blog.zisheng.pro/posts/2233b15714d5/"/>
    <published>2020-05-30T13:59:19.000Z</published>
    <summary>本文记录了一些 React Native 热更新的搭建过程</summary>
    <title>React Native 热更新不求人</title>
    <updated>2026-09-18T08:15:28.632Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="运维" scheme="https://blog.zisheng.pro/categories/%E8%BF%90%E7%BB%B4/"/>
    <category term="Android" scheme="https://blog.zisheng.pro/tags/Android/"/>
    <category term="Jenkins" scheme="https://blog.zisheng.pro/tags/Jenkins/"/>
    <category term="docker" scheme="https://blog.zisheng.pro/tags/docker/"/>
    <content>
      <![CDATA[<p>Docker 安装配置请参考我的另一篇文章: <a href="https://juejin.im/post/5e839f2851882573ab44f1b4#heading-11">https://juejin.im/post/5e839f2851882573ab44f1b4#heading-11</a></p><h2 id="安装Jenkins"><a href="#安装Jenkins" class="headerlink" title="安装Jenkins"></a>安装Jenkins</h2><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">docker run -d \</span></span><br><span class="line"><span class="language-bash"> -u root \</span></span><br><span class="line"><span class="language-bash"> -p 8080:8080 -p 50000:50000 \</span></span><br><span class="line"><span class="language-bash"> -v jenkins_home:/var/jenkins_home \</span></span><br><span class="line"><span class="language-bash"> -e PLUGINS_FORCE_UPGRADE=<span class="literal">true</span> \</span></span><br><span class="line"><span class="language-bash"> -e TRY_UPGRADE_IF_NO_MARKER=<span class="literal">true</span> \</span></span><br><span class="line"><span class="language-bash"> -e JAVA_OPTS=-Duser.timezone=Asia/Shanghai \</span></span><br><span class="line"><span class="language-bash"> --name jenkins \</span></span><br><span class="line"><span class="language-bash"> --restart always \</span></span><br><span class="line"><span class="language-bash"> jenkins/jenkins</span></span><br></pre></td></tr></table></figure><p>需要更多定制参数你可以参考 <a href="https://github.com/jenkinsci/docker">https://github.com/jenkinsci/docker</a></p><h2 id="Nginx-反向代理"><a href="#Nginx-反向代理" class="headerlink" title="Nginx 反向代理"></a>Nginx 反向代理</h2><p>我没有域名，所以我的配置是基于端口的，你可以查看<a href="https://github.com/jenkinsci/docker/blob/master/README.md">官方文档</a> 获取更多方案：</p><figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">server</span> &#123;</span><br><span class="line"><span class="attribute">listen</span> <span class="number">8882</span>;</span><br><span class="line"><span class="attribute">server_name</span> _;</span><br><span class="line"></span><br><span class="line"><span class="section">location</span> / &#123;</span><br><span class="line"><span class="attribute">proxy_set_header</span>        Host <span class="variable">$host</span>:<span class="variable">$server_port</span>;</span><br><span class="line"><span class="attribute">proxy_set_header</span>        X-Real-IP <span class="variable">$remote_addr</span>;</span><br><span class="line"><span class="attribute">proxy_set_header</span>        X-Forwarded-For <span class="variable">$proxy_add_x_forwarded_for</span>;</span><br><span class="line"><span class="attribute">proxy_set_header</span>        X-Forwarded-Proto <span class="variable">$scheme</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment"># Fix the &quot;It appears that your reverse proxy set up is broken&quot; error.</span></span><br><span class="line"><span class="attribute">proxy_pass</span>          http://127.0.0.1:8080;</span><br><span class="line"><span class="attribute">proxy_read_timeout</span>  <span class="number">90</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment"># Required for new HTTP-based CLI</span></span><br><span class="line"><span class="attribute">proxy_http_version</span> <span class="number">1</span>.<span class="number">1</span>;</span><br><span class="line"><span class="attribute">proxy_request_buffering</span> <span class="literal">off</span>;</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="初始化jenkins"><a href="#初始化jenkins" class="headerlink" title="初始化jenkins"></a>初始化jenkins</h2><p>1、执行<code>cat /var/lib/docker/volumes/jenkins_home/_data/secrets/initialAdminPassword</code>，获取<strong>管理员密码</strong></p><p>2、启动完jenkins后通过浏览器输入地址<code>http://部署jenkins主机IP:端口</code></p><img src="https://i.loli.net/2020/04/10/YtRrWTZVQc7Mma3.png" style="zoom:75%;" /><p>3、选择安装插件方式，这里我是默认第一个：</p><img src="https://i.loli.net/2020/04/10/IBlrbMFusXmdeA5.png" style="zoom:75%;" /><p>4、进入插件安装界面，连网等待插件安装：</p><img src="https://i.loli.net/2020/04/10/gimNZtUID5YjK9f.png" style="zoom:75%;" /><p>5、安装完插件后，进入创建管理员界面：</p><img src="https://i.loli.net/2020/04/10/SxvtAW3McBCqTEX.png" style="zoom:75%;" /><h2 id="基于角色的认证策略"><a href="#基于角色的认证策略" class="headerlink" title="基于角色的认证策略"></a>基于角色的认证策略</h2><ol><li><p>请确保在 <strong>系统管理</strong> -&gt; <strong>插件管理</strong> 中安装了<strong>Role-based Authorization Strategy</strong>  插件！！</p></li><li><p>在 <strong>系统管理</strong> -&gt; <strong>安全</strong> -&gt; <strong>全局安全设置</strong> 中的 <strong>授权策略</strong> 配置项中选择 <strong>Role-Based Strategy</strong></p><p><img src="https://i.loli.net/2020/05/09/FiR59LEtqwCbDIh.png" alt="紫升"></p></li><li><p>然后就可以在 <code>Manage and Assign Roles</code> 中管理角色</p></li></ol><h2 id="全局工具配置"><a href="#全局工具配置" class="headerlink" title="全局工具配置"></a>全局工具配置</h2><h3 id="JDK"><a href="#JDK" class="headerlink" title="JDK"></a>JDK</h3><blockquote><p>自动安装请先到Oracle注册账号</p></blockquote><p><img src="https://i.loli.net/2020/05/09/jbzQ15qthxmTu9Z.png" alt="紫升"></p><h3 id="Git"><a href="#Git" class="headerlink" title="Git"></a>Git</h3><blockquote><p>需要先执行 <code>apt install git</code> 安装git，然后通过 <code>which git</code> 查看 git 安装位置！</p></blockquote><p><img src="https://i.loli.net/2020/05/09/lLSYIN5FARQ38uH.png" alt="紫升"></p><h3 id="NodeJS"><a href="#NodeJS" class="headerlink" title="NodeJS"></a>NodeJS</h3><blockquote><p>需要先需要安装 <a href="https://plugins.jenkins.io/nodejs">NodeJS Plugin</a> 插件</p></blockquote><p><img src="https://i.loli.net/2020/05/09/B9R7pgIXd5OSCbq.png" alt="紫升"></p><h2 id="Android-打包环境"><a href="#Android-打包环境" class="headerlink" title="Android 打包环境"></a>Android 打包环境</h2><h3 id="安装-android-sdk、sdkmagager"><a href="#安装-android-sdk、sdkmagager" class="headerlink" title="安装 android-sdk、sdkmagager"></a>安装 android-sdk、sdkmagager</h3><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">apt update | apt install android-sdk</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">apt install snapd</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">snap install androidsdk</span></span><br></pre></td></tr></table></figure><h3 id="复制android-sdk到-jenkins-volume"><a href="#复制android-sdk到-jenkins-volume" class="headerlink" title="复制android-sdk到 jenkins volume"></a>复制android-sdk到 jenkins volume</h3><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash"><span class="built_in">cp</span> -R /usr/lib/android-sdk/ /var/lib/docker/volume/jenkins_home/_data/</span></span><br></pre></td></tr></table></figure><p>android-sdk 的安装位置可以通过 <code>adb --version</code> 查看：</p><img src="https://user-gold-cdn.xitu.io/2020/5/9/171f7fae7cf4ff7a?w=864&h=230&f=png&s=176444" alt="" style="zoom: 75%;" /><h3 id="设置环境变量"><a href="#设置环境变量" class="headerlink" title="设置环境变量"></a>设置环境变量</h3><p>将下面的的配置加入到 <code>~/.bash_profile</code> 或 <code>~/.zshrc</code> 中，并执行 <code>srouce ~/.bash_profile</code> 或 <code>srouce ~/.zshrc</code>：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">export ANDROID_HOME=/var/lib/docker/volumes/jenkins_home/_data/android-sdk</span><br><span class="line">export PATH=$PATH:$ANDROID_HOME/tools</span><br><span class="line">export PATH=$PATH:$ANDROID_HOME/tools/bin</span><br><span class="line">export PATH=$PATH:$ANDROID_HOME/platform-tools</span><br><span class="line">export PATH=$PATH:$ANDROID_HOME/emulator</span><br></pre></td></tr></table></figure><h3 id="accepted-licences"><a href="#accepted-licences" class="headerlink" title="accepted licences"></a>accepted licences</h3><blockquote><p>如果不授权会报错： Failed to install the following Android SDK packages as some licences have not been accepted.</p></blockquote><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash"><span class="built_in">yes</span> | sdkmanager --sdk_root=<span class="variable">$&#123;ANDROID_HOME&#125;</span> --licenses</span></span><br></pre></td></tr></table></figure><blockquote><p>如果不指定 <code>--sdk_root=${ANDROID_HOME}</code> 会报错：<a href="https://stackoverflow.com/questions/60730615/sdkmanager-warning-could-not-create-setting-java-lang-illegalargumentexcepti">SDKmanager : Warning: Could not create setting. java.lang.IllegalArgumentException</a></p></blockquote><h3 id="设置-Jenkins-ANDROID-HOME"><a href="#设置-Jenkins-ANDROID-HOME" class="headerlink" title="设置 Jenkins ANDROID_HOME"></a>设置 Jenkins ANDROID_HOME</h3><p>进入 <strong>系统管理</strong> -&gt; <strong>系统配置</strong> -&gt; <strong>全局属性</strong> 新增环境变量 <code>ANDROID_HOME</code>：</p><p><img src="https://i.loli.net/2020/05/09/jWANFZh8xCD1Sgm.png" alt="紫升"></p><h2 id="GitHub-Plugin"><a href="#GitHub-Plugin" class="headerlink" title="GitHub Plugin"></a>GitHub Plugin</h2><p>这个插件的作用是集成 GitHub 到 Jenkins</p><h3 id="获取-GitHub-的-Personal-access-token"><a href="#获取-GitHub-的-Personal-access-token" class="headerlink" title="获取 GitHub 的 Personal access token"></a>获取 GitHub 的 Personal access token</h3><blockquote><p><strong>GitHub 主页</strong> -&gt; <strong>Settings</strong> -&gt; <strong>Developer settings</strong> -&gt; <strong>Personal access tokens</strong></p></blockquote><p><img src="https://i.loli.net/2020/05/09/qTLBC359Z2rNday.png" alt="紫升"></p><h3 id="添加Github服务器"><a href="#添加Github服务器" class="headerlink" title="添加Github服务器"></a>添加Github服务器</h3><blockquote><p><strong>系统管理</strong> -&gt; <strong>系统设置</strong> -&gt; <strong>GitHub</strong> -&gt; <strong>添加Github服务器</strong></p></blockquote><p>这里的名称随便填，API URL 填写 <code>https://api.github.com/</code>：</p><p><img src="https://i.loli.net/2020/05/09/1FLEfTHhpexyXQW.png" alt="紫升"></p><p>凭证类型选 <code>Secret text</code>：</p><p><img src="https://i.loli.net/2020/05/09/3Oypr4xniKZamG9.png" alt="紫升"></p><p>在凭据选上刚刚你添加的，勾上管理 Hook，点击“连接测试”，成功之后如下所示：</p><p><img src="https://i.loli.net/2020/05/09/pBWKZMfq5IkGvyz.png" alt="紫升"></p><h3 id="配置-GitHub-的-webhook-地址"><a href="#配置-GitHub-的-webhook-地址" class="headerlink" title="配置 GitHub 的 webhook 地址"></a>配置 GitHub 的 webhook 地址</h3><p>webhook 是通知 Jenkins 时的请求地址，用来填写到 GitHub 上，这样 GitHub 就能通过该地址通知到 Jenkins</p><p>假设Jenkins所在服务器的地址是：192.168.0.1，端口为8080，那么webhook地址就是 <code>http://192.168.0.1:8080/github-webhook/</code></p><p><img src="https://i.loli.net/2020/05/09/dU7AeiX13mWHqTp.png" alt="image-20200509175614040"></p><p>在 Payload URL 位置填入 webhook 地址，再点击底部的 Add webhook 按钮，这样就完成 webhook 配置了，今后当前工程有代码提交，GitHub 就会向此 webhook 地址发请求，通知 Jenkins 构建：</p><p><img src="https://i.loli.net/2020/05/09/BqoXjUQsile3K7A.png" alt="紫升"></p><h3 id="任务配置"><a href="#任务配置" class="headerlink" title="任务配置"></a>任务配置</h3><ul><li><p>构造触发器选择：GitHub hook trigger for GITScm polling</p><p><img src="https://i.loli.net/2020/05/09/EF5DYKJHgOPayxA.png" alt="紫升"></p></li><li><p>构建环境和绑定</p><p>如下图所示，勾选 Use secret text(s) or file(s)，下面的”凭据”选择我们之前配置过的凭证</p><p><img src="https://i.loli.net/2020/05/09/2noTEULmhAzd7Dg.png" alt="紫升"></p></li></ul><h2 id="常见问题"><a href="#常见问题" class="headerlink" title="常见问题"></a>常见问题</h2><h3 id="修改默认时区为上海时区"><a href="#修改默认时区为上海时区" class="headerlink" title="修改默认时区为上海时区"></a>修改默认时区为上海时区</h3><p>在 <strong>系统管理</strong> -&gt; <strong>脚本命令行</strong> 里运行 <code>System.setProperty(&#39;org.apache.commons.jelly.tags.fmt.timeZone&#39;, &#39;Asia/Shanghai&#39;)</code></p>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/f0d2184d4dfe/</id>
    <link href="https://blog.zisheng.pro/posts/f0d2184d4dfe/"/>
    <published>2020-05-08T10:16:43.000Z</published>
    <summary>本文介绍了如何通过 Docker 安装配置 Jenkins</summary>
    <title>使用 Docker 安装配置 Jenkins</title>
    <updated>2026-09-18T08:15:28.636Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="工具" scheme="https://blog.zisheng.pro/categories/%E5%B7%A5%E5%85%B7/"/>
    <category term="Typora" scheme="https://blog.zisheng.pro/tags/Typora/"/>
    <category term="markdown" scheme="https://blog.zisheng.pro/tags/markdown/"/>
    <category term="图床" scheme="https://blog.zisheng.pro/tags/%E5%9B%BE%E5%BA%8A/"/>
    <content>
      <![CDATA[<h2 id="下载-PicGo-Core"><a href="#下载-PicGo-Core" class="headerlink" title="下载 PicGo-Core"></a>下载 PicGo-Core</h2><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">$ npm install picgo -g</span><br><span class="line"><span class="comment"># or</span></span><br><span class="line">$ yarn global add picgo</span><br></pre></td></tr></table></figure><h2 id="获取路径"><a href="#获取路径" class="headerlink" title="获取路径"></a>获取路径</h2><ul><li>node安装路径（<code>which node</code>）：<code>/usr/local/bin/node</code></li><li>picgo安装路径（<code>which picgo</code>）：<code>/usr/local/bin/picgo</code></li></ul><h3 id="Imgae-Upldoad-Setting"><a href="#Imgae-Upldoad-Setting" class="headerlink" title="Imgae Upldoad Setting"></a>Imgae Upldoad Setting</h3><blockquote><p>打开 Typora -&gt; 偏好设置 -&gt; 图像：</p></blockquote><p>上传服务选择“Custom Command”，自定义命令格式是 “[your node path] [your picgo-core path] upload”，比如我的是 <code>/usr/local/bin/node /usr/local/bin/picgo upload </code></p><p><img src="https://i.loli.net/2020/04/10/aCFG5ZW2xuR6qls.png" alt="紫升"></p><h2 id="配置-sm-ms-api-token"><a href="#配置-sm-ms-api-token" class="headerlink" title="配置 sm.ms api token"></a>配置 sm.ms api token</h2><p>picgo 的默认配置文件为<code>~/.picgo/config.json</code>。其中<code>~</code>为用户目录。不同系统的用户目录不太一样。</p><p>linux 和 macOS 均为<code>~/.picgo/config.json</code>。</p><p>windows 则为<code>C:\Users\你的用户名/.picgo\config.json</code>。</p><p>配置文件需要至少有如下的配置项：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;picBed&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;uploader&quot;</span><span class="punctuation">:</span> <span class="string">&quot;smms&quot;</span><span class="punctuation">,</span> <span class="comment">// 代表当前的默认上传图床为 SM.MS,</span></span><br><span class="line">    <span class="attr">&quot;smms&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">      <span class="attr">&quot;token&quot;</span><span class="punctuation">:</span> <span class="string">&quot;&quot;</span> <span class="comment">// 从https://sm.ms/home/apitoken获取的token</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="attr">&quot;picgoPlugins&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span><span class="punctuation">&#125;</span> <span class="comment">// 为插件预留</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><p>点击<strong>验证图片上传选项</strong> 按钮</p><p><img src="https://i.loli.net/2020/04/10/LYNhz8rACdORjP7.png" alt="image-20200410180446932"></p><h2 id="参考链接"><a href="#参考链接" class="headerlink" title="参考链接"></a>参考链接</h2><ul><li><a href="https://picgo.github.io/PicGo-Core-Doc/zh/guide/config.html#%E9%BB%98%E8%AE%A4%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6">Picgo-Core</a></li><li><a href="https://support.typora.io/Upload-Image/">Typora Upload Image</a></li></ul>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/cc88fa106758/</id>
    <link href="https://blog.zisheng.pro/posts/cc88fa106758/"/>
    <published>2020-04-10T09:57:41.000Z</published>
    <summary>本文介绍了如何用 Typora 基于 PicGo-Core 实现粘贴上传图片</summary>
    <title>Typora 基于 PicGo-Core 实现粘贴上传图片</title>
    <updated>2026-09-18T08:15:28.632Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="运维" scheme="https://blog.zisheng.pro/categories/%E8%BF%90%E7%BB%B4/"/>
    <category term="Docker" scheme="https://blog.zisheng.pro/tags/Docker/"/>
    <category term="笔记" scheme="https://blog.zisheng.pro/tags/%E7%AC%94%E8%AE%B0/"/>
    <category term="紫升的笔记" scheme="https://blog.zisheng.pro/tags/%E7%B4%AB%E5%8D%87%E7%9A%84%E7%AC%94%E8%AE%B0/"/>
    <category term="DevOps" scheme="https://blog.zisheng.pro/tags/DevOps/"/>
    <category term="Container" scheme="https://blog.zisheng.pro/tags/Container/"/>
    <category term="容器" scheme="https://blog.zisheng.pro/tags/%E5%AE%B9%E5%99%A8/"/>
    <category term="daemon" scheme="https://blog.zisheng.pro/tags/daemon/"/>
    <category term="守护进程" scheme="https://blog.zisheng.pro/tags/%E5%AE%88%E6%8A%A4%E8%BF%9B%E7%A8%8B/"/>
    <category term="qshell" scheme="https://blog.zisheng.pro/tags/qshell/"/>
    <category term="crontab" scheme="https://blog.zisheng.pro/tags/crontab/"/>
    <content>
      <![CDATA[<p>Docker 是个划时代的开源项目，它彻底释放了计算虚拟化的威力，极大提高了应用的维护效率，降低了云计算应用开发的成本！使用 Docker，可以让应用的部署、测试和分发都变得前所未有的高效和轻松！</p><p>无论是应用开发者、运维人员、还是其他信息技术从业人员，都有必要认识和掌握 Docker，节约有限的生命。</p><p>本文是笔者以一个前端工程师的视角学习 Docker 过程中的笔记，如果对您有所帮助，荣幸之至。</p><h2 id="Docker-是什么"><a href="#Docker-是什么" class="headerlink" title="Docker 是什么"></a>Docker 是什么</h2><p><strong>Docker</strong> 使用 <code>Google</code> 公司推出的 <a href="https://golang.org/">Go 语言</a> 进行开发实现，基于 <code>Linux</code> 内核的 <a href="https://zh.wikipedia.org/wiki/Cgroups">cgroup</a>，<a href="https://en.wikipedia.org/wiki/Linux_namespaces">namespace</a>，以及 <a href="https://docs.docker.com/storage/storagedriver/overlayfs-driver/">OverlayFS</a> 类的 <a href="https://en.wikipedia.org/wiki/Union_mount">Union FS</a> 等技术，对进程进行封装隔离，属于 <a href="https://en.wikipedia.org/wiki/Operating-system-level_virtualization">操作系统层面的虚拟化技术</a>。由于隔离的进程独立于宿主和其它的隔离的进程，因此也称其为容器。最初实现是基于 <a href="https://linuxcontainers.org/lxc/introduction/">LXC</a>，从 0.7 版本以后开始去除 <code>LXC</code>，转而使用自行开发的 <a href="https://github.com/docker/libcontainer">libcontainer</a>，从 1.11 开始，则进一步演进为使用 <a href="https://github.com/opencontainers/runc">runC</a> 和 <a href="https://github.com/containerd/containerd">containerd</a>。</p><h2 id="概念"><a href="#概念" class="headerlink" title="概念"></a>概念</h2><h3 id="DevOps"><a href="#DevOps" class="headerlink" title="DevOps"></a><strong>DevOps</strong></h3><p>DevOps（<strong>Dev</strong>elopment和<strong>Op</strong>erations的组合词）是一种重视软件开发人员（Dev）和IT运维技术人员（Ops）之间沟通合作的文化、运动或惯例。透过自动化“软件交付”和“架构变更”的流程，来使得构建、测试、发布软件能够更加地快捷、频繁和可靠。</p><p>DevOps 的引入能对产品交付、<a href="https://zh.wikipedia.org/wiki/%E6%B5%8B%E8%AF%95">测试</a>、功能开发和<a href="https://zh.wikipedia.org/wiki/%E8%BB%9F%E9%AB%94%E7%B6%AD%E8%AD%B7">维护</a>（包括曾经罕见但如今已屡见不鲜的“<a href="https://zh.wikipedia.org/wiki/Hot_fix">热补丁</a>”）起到意义深远的影响。在缺乏 DevOps 能力的组织中，开发与运营之间存在着信息“鸿沟”。例如运营人员要求更好的可靠性和安全性，开发人员则希望<a href="https://zh.wikipedia.org/wiki/%E5%9F%BA%E7%A1%80%E8%AE%BE%E6%96%BD">基础设施</a>响应更快，而业务用户的需求则是更快地将更多的特性发布给最终用户使用。这种信息鸿沟就是最常出问题的地方。</p><h3 id="容器"><a href="#容器" class="headerlink" title="容器"></a>容器</h3><p>容器有效地将由单个操作系统管理的资源划分到孤立的组中，以更好地在孤立的组之间平衡有冲突的资源使用需求。与虚拟化相比，这样既不需要指令级模拟，也不需要即时编译。容器可以在核心 CPU 本地运行指令，而不需要任何专门的解释机制。此外，也避免了准虚拟化（para-virtualization）和系统调用替换中的复杂性。</p><h3 id="虚拟化"><a href="#虚拟化" class="headerlink" title="虚拟化"></a>虚拟化</h3><p>在计算机技术中，虚拟化是一种资源管理技术，是将计算机中的各种实体资源，如服务器、网络、内存及存储等，予以抽象、转换后呈现出来，打破实体结构间的不可切割的障碍，使用户可以用比原来的组态更好的方式来应用这些资源。</p><h3 id="Docker-与虚拟机比较"><a href="#Docker-与虚拟机比较" class="headerlink" title="Docker 与虚拟机比较"></a>Docker 与虚拟机比较</h3><table><thead><tr><th>特性</th><th>容器</th><th>虚拟机</th></tr></thead><tbody><tr><td>启动</td><td>秒级</td><td>分钟级</td></tr><tr><td>硬盘使用</td><td>一般为 <code>MB</code></td><td>一般为 <code>GB</code></td></tr><tr><td>性能</td><td>接近原生</td><td>弱于</td></tr><tr><td>系统支持量</td><td>单机支持上千个容器</td><td>一般几十个</td></tr></tbody></table><p>如下图，虚拟机是在硬件层面实现虚拟化，需要额外的虚拟机管理应用和虚拟机操作系统层。Docker容器是在操作系统层面上实现虚拟化，直接复用本地主机的操作系统，因此更加轻量级。</p><p><img src="https://i.loli.net/2020/03/31/mZiyHL2kGAgrMFx.png" alt="Docker 与虚拟机比较"></p><h3 id="Docker核心概念"><a href="#Docker核心概念" class="headerlink" title="Docker核心概念"></a>Docker核心概念</h3><h4 id="镜像（Image）"><a href="#镜像（Image）" class="headerlink" title="镜像（Image）"></a>镜像（Image）</h4><p>我们都知道，操作系统分为内核和用户空间。对于 Linux 而言，内核启动后，会挂载 <code>root</code> 文件系统为其提供用户空间支持。而 Docker 镜像（Image），就相当于是一个 <code>root</code> 文件系统。比如官方镜像 <code>ubuntu:18.04</code> 就包含了完整的一套 Ubuntu 18.04 最小系统的 <code>root</code> 文件系统。</p><p>Docker 镜像是一个特殊的文件系统，除了提供容器运行时所需的程序、库、资源、配置等文件外，还包含了一些为运行时准备的一些配置参数（如匿名卷、环境变量、用户等）。镜像不包含任何动态数据，其内容在构建之后也不会被改变。</p><p><strong>分层存储</strong></p><p>因为镜像包含操作系统完整的 <code>root</code> 文件系统，其体积往往是庞大的，因此在 Docker 设计时，就充分利用 <a href="https://en.wikipedia.org/wiki/Union_mount">Union FS</a> 的技术，将其设计为分层存储的架构。所以严格来说，镜像并非是像一个 ISO 那样的打包文件，镜像只是一个虚拟的概念，其实际体现并非由一个文件组成，而是由一组文件系统组成，或者说，由多层文件系统联合组成。</p><p>镜像构建时，会一层层构建，前一层是后一层的基础。每一层构建完就不会再发生改变，后一层上的任何改变只发生在自己这一层。比如，删除前一层文件的操作，实际不是真的删除前一层的文件，而是仅在当前层标记为该文件已删除。在最终容器运行的时候，虽然不会看到这个文件，但是实际上该文件会一直跟随镜像。因此，在构建镜像的时候，需要额外小心，每一层尽量只包含该层需要添加的东西，任何额外的东西应该在该层构建结束前清理掉。</p><p>分层存储的特征还使得镜像的复用、定制变的更为容易。甚至可以用之前构建好的镜像作为基础层，然后进一步添加新的层，以定制自己所需的内容，构建新的镜像。</p><h4 id="容器（Container）"><a href="#容器（Container）" class="headerlink" title="容器（Container）"></a>容器（Container）</h4><p>镜像（<code>Image</code>）和容器（<code>Container</code>）的关系，就像是面向对象程序设计中的 <code>类</code> 和 <code>实例</code> 一样，镜像是静态的定义，容器是镜像运行时的实体。容器可以被创建、启动、停止、删除、暂停等。</p><p>容器的实质是进程，但与直接在宿主执行的进程不同，容器进程运行于属于自己的独立的 <a href="https://en.wikipedia.org/wiki/Linux_namespaces">命名空间</a>。因此容器可以拥有自己的 <code>root</code> 文件系统、自己的网络配置、自己的进程空间，甚至自己的用户 ID 空间。容器内的进程是运行在一个隔离的环境里，使用起来，就好像是在一个独立于宿主的系统下操作一样。这种特性使得容器封装的应用比直接在宿主运行更加安全。也因为这种隔离的特性，很多人初学 Docker 时常常会混淆容器和虚拟机。</p><p>前面讲过镜像使用的是分层存储，容器也是如此。每一个容器运行时，是以镜像为基础层，在其上创建一个当前容器的存储层，我们可以称这个为容器运行时读写而准备的存储层为 <strong>容器存储层</strong>。</p><p>容器存储层的生存周期和容器一样，容器消亡时，容器存储层也随之消亡。因此，任何保存于容器存储层的信息都会随容器删除而丢失。</p><p>按照 Docker 最佳实践的要求，容器不应该向其存储层内写入任何数据，容器存储层要保持无状态化。所有的文件写入操作，都应该使用 <a href="https://vuepress.mirror.docker-practice.com/data_management/volume.html">数据卷（Volume）</a>、或者绑定宿主目录，在这些位置的读写会跳过容器存储层，直接对宿主（或网络存储）发生读写，其性能和稳定性更高。</p><p>数据卷的生存周期独立于容器，容器消亡，数据卷不会消亡。因此，使用数据卷后，容器删除或者重新运行之后，数据却不会丢失。</p><h4 id="仓库注册服务器（Registry）"><a href="#仓库注册服务器（Registry）" class="headerlink" title="仓库注册服务器（Registry）"></a>仓库注册服务器（Registry）</h4><p>一个 <strong>Docker Registry</strong> 中可以包含多个 <strong>仓库</strong>（<code>Repository</code>）；每个仓库可以包含多个 <strong>标签</strong>（<code>Tag</code>）；每个标签对应一个镜像。</p><p>通常，一个仓库会包含同一个软件不同版本的镜像，而标签就常用于对应该软件的各个版本。我们可以通过 <code>&lt;仓库名&gt;:&lt;标签&gt;</code> 的格式来指定具体是这个软件哪个版本的镜像。如果不给出标签，将以 <code>latest</code> 作为默认标签。</p><p>以 <a href="https://hub.docker.com/_/ubuntu">Ubuntu 镜像</a> 为例，<code>ubuntu</code> 是仓库的名字，其内包含有不同的版本标签，如，<code>16.04</code>, <code>18.04</code>。我们可以通过 <code>ubuntu:16.04</code>，或者 <code>ubuntu:18.04</code> 来具体指定所需哪个版本的镜像。如果忽略了标签，比如 <code>ubuntu</code>，那将视为 <code>ubuntu:latest</code>。</p><p>仓库名经常以 <em>两段式路径</em> 形式出现，比如 <code>jwilder/nginx-proxy</code>，前者往往意味着 Docker Registry 多用户环境下的用户名，后者则往往是对应的软件名。但这并非绝对，取决于所使用的具体 Docker Registry 的软件或服务。</p><p><strong>公有 Docker Registry：</strong></p><ul><li><a href="https://hub.docker.com/">Docker Hub</a></li><li><a href="https://c.163.com/hub#/m/library/">网易云镜像服务</a></li><li><a href="https://hub.daocloud.io/">DaoCloud 镜像市场</a></li><li><a href="https://cr.console.aliyun.com/">aliyun镜像库</a></li></ul><p><strong>私有 Docker Registry：</strong></p><ul><li><a href="https://vuepress.mirror.docker-practice.com/repository/nexus3_registry.html">Sonatype Nexus</a></li><li><a href="https://github.com/goharbor/harbor">Harbor</a></li></ul><h3 id="守护进程-daemon"><a href="#守护进程-daemon" class="headerlink" title="守护进程 daemon"></a>守护进程 daemon</h3><p>在一个多任务的电脑操作系统中，守护进程（daemon）是一种在后台执行的电脑程序。此类程序会被以进程的形式初始化。守护进程程序的名称通常以字母”d“结尾：例如，<code>syslogd</code> 就是指管理系统日志的守护进程。</p><p>通常，守护进程没有任何存在的父进程（即PPID&#x3D;1），且在 UNIX 系统进程层级中直接位于 init 之下。守护进程程序通常通过如下方法是自己成为守护进程：对一个子进程进行 fork，然后使其父进程立即终止，使得这个子进程能在 init 下运行。这种方法通常被称为”脱壳“。</p><p>系统通常在启动时一同引导守护进程。守护进程为对网络请求，硬件活动等进行响应，或其他通过某些任务对其他应用程序的请求进行回应提供支持。守护进程也能够对硬件进行配置（如某些Linux系统上的devfsd），运行计划任务（例如cron），以及运行其他任务。</p><p>在 DOS 环境中，此类应用程序被称为驻留程序（TSR）。在 Windows 系统中，由称为 Windows服务的应用程序来履行守护进程的职责。</p><p>在原本的 Mac OS 系统中，此类应用程序被称为”extensions“。而作为 Unux-like 的 Mac OS X 有守护进程。</p><h2 id="安装配置"><a href="#安装配置" class="headerlink" title="安装配置"></a>安装配置</h2><ul><li>操作系统：Linux ubuntu18 4.15.0-91-generic，通过 <code>uname -a</code> 查看</li><li>docker-ce镜像：<a href="https://developer.aliyun.com/mirror/docker-ce?spm=a2c6h.13651102.0.0.52471b11cIp2pH">https://developer.aliyun.com/mirror/docker-ce?spm=a2c6h.13651102.0.0.52471b11cIp2pH</a></li></ul><h3 id="卸载旧版本"><a href="#卸载旧版本" class="headerlink" title="卸载旧版本"></a>卸载旧版本</h3><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">apt remove docker docker-engine docker.io containerd runc</span></span><br></pre></td></tr></table></figure><h3 id="通过软件包安装"><a href="#通过软件包安装" class="headerlink" title="通过软件包安装"></a>通过软件包安装</h3><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_"># </span><span class="language-bash">step 1: 安装必要的一些系统工具</span></span><br><span class="line">sudo apt-get update</span><br><span class="line">sudo apt-get -y install apt-transport-https ca-certificates curl gnupg-agent pass software-properties-common</span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">step 2: 安装GPG证书</span></span><br><span class="line">curl -fsSL https://mirrors.aliyun.com/docker-ce/linux/ubuntu/gpg | sudo apt-key add -</span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">Step 3: 写入软件源信息</span></span><br><span class="line">sudo add-apt-repository &quot;deb [arch=amd64] https://mirrors.aliyun.com/docker-ce/linux/ubuntu $(lsb_release -cs) stable&quot;</span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">Step 4: 更新并安装Docker-CE</span></span><br><span class="line">sudo apt-get -y update</span><br><span class="line">sudo apt-get -y install docker-ce</span><br></pre></td></tr></table></figure><h3 id="通过脚本安装"><a href="#通过脚本安装" class="headerlink" title="通过脚本安装"></a>通过脚本安装</h3><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">curl -fsSL https://get.docker.com -o get-docker.sh</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">sudo sh get-docker.sh --mirror Aliyun</span></span><br></pre></td></tr></table></figure><p>安装成功后，会自动启动 Docker 服务。用户可以使用 <code>systemctl is-enabled docker</code> 来确认 Docker 服务是否是开机自启动。</p><h3 id="可选配置"><a href="#可选配置" class="headerlink" title="可选配置"></a>可选配置</h3><p><strong>解决 <code>WARNING: Your kernel does not support cgroup swap limit capabilities</code>：</strong></p><ol><li><p>编辑 <code>/etc/default/grub</code> 文件</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">nano /etc/default/grub</span></span><br></pre></td></tr></table></figure></li><li><p>找到 <code>GRUB_CMDLINE_LINUX=</code> 配置项，并追加 <code>cgroup_enable=memory swapaccount=1</code>。</p></li><li><p>保存文件后执行一下命令：<code>sudo update-grub</code></p></li><li><p>重启服务器：<code>reboot</code></p></li></ol><h3 id="测试-Docker-是否安装正确"><a href="#测试-Docker-是否安装正确" class="headerlink" title="测试 Docker 是否安装正确"></a>测试 Docker 是否安装正确</h3><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">docker run hello-world</span></span><br></pre></td></tr></table></figure><p>执行以上命令，若能正常输出以下信息，则说明安装成功。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line">Unable to find image &#x27;hello-world:latest&#x27; locally</span><br><span class="line">latest: Pulling from library/hello-world</span><br><span class="line">1b930d010525: Pull complete</span><br><span class="line">Digest: sha256:f9dfddf63636d84ef479d645ab5885156ae030f611a56f3a7ac7f2fdd86d7e4e</span><br><span class="line">Status: Downloaded newer image for hello-world:latest</span><br><span class="line"></span><br><span class="line">Hello from Docker!</span><br><span class="line">This message shows that your installation appears to be working correctly.</span><br><span class="line"></span><br><span class="line">To generate this message, Docker took the following steps:</span><br><span class="line"> 1. The Docker client contacted the Docker daemon.</span><br><span class="line"> 2. The Docker daemon pulled the &quot;hello-world&quot; image from the Docker Hub.</span><br><span class="line">    (amd64)</span><br><span class="line"> 3. The Docker daemon created a new container from that image which runs the</span><br><span class="line">    executable that produces the output you are currently reading.</span><br><span class="line"> 4. The Docker daemon streamed that output to the Docker client, which sent it</span><br><span class="line">    to your terminal.</span><br><span class="line"></span><br><span class="line">To try something more ambitious, you can run an Ubuntu container with:</span><br><span class="line"> $ docker run -it ubuntu bash</span><br><span class="line"></span><br><span class="line">Share images, automate workflows, and more with a free Docker ID:</span><br><span class="line"> https://hub.docker.com/</span><br><span class="line"></span><br><span class="line">For more examples and ideas, visit:</span><br><span class="line"> https://docs.docker.com/get-started/</span><br></pre></td></tr></table></figure><h3 id="Docker-Deamon-配置"><a href="#Docker-Deamon-配置" class="headerlink" title="Docker Deamon 配置"></a>Docker Deamon 配置</h3><p>执行 <code>nano /etc/docker/daemon.json</code> 中写入如下内容：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;ip&quot;</span><span class="punctuation">:</span> <span class="string">&quot;127.0.0.1&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;experimental&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">false</span></span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;registry-mirrors&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">    <span class="string">&quot;https://registry.docker-cn.com&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="string">&quot;https://mirror.ccs.tencentyun.com&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="string">&quot;http://docker.mirrors.ustc.edu.cn&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="string">&quot;http://hub-mirror.c.163.com&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><p>重新启动服务:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">systemctl daemon-reload</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">systemctl restart docker.service</span></span><br></pre></td></tr></table></figure><h2 id="使用-Docker-镜像"><a href="#使用-Docker-镜像" class="headerlink" title="使用 Docker 镜像"></a>使用 Docker 镜像</h2><h3 id="获取镜像"><a href="#获取镜像" class="headerlink" title="获取镜像"></a>获取镜像</h3><blockquote><p><code>docker pull [选项] [Docker Registry 地址[:端口号]/][用户名]&lt;仓库名&gt;[:TAG]</code></p></blockquote><ul><li>默认选项<ul><li><code>-a</code>，<code>--all-tags=true|false</code>：是否获取仓库中的所有镜像，默认为否</li><li><code>--disable-content-trust</code>：取消镜像的内容校验，默认为真</li></ul></li><li>默认 Docker Registry：<code>registry.hub.docker.com</code></li><li>默认用户名：<code>library</code>，也就是官方镜像</li><li>默认TAG：<code>latest</code></li></ul><h3 id="查看镜像信息"><a href="#查看镜像信息" class="headerlink" title="查看镜像信息"></a>查看镜像信息</h3><h4 id="列出本地主机上已有镜像"><a href="#列出本地主机上已有镜像" class="headerlink" title="列出本地主机上已有镜像"></a>列出本地主机上已有镜像</h4><blockquote><p><code>docker image ls</code> | <code>docker images</code></p></blockquote><p>镜像的大小信息只是表示了该镜像的逻辑体积大小，实际上由于相同的镜像层本地只会存储一份，物理上占用的存储空间会小于各镜像逻辑体积之和。</p><h4 id="使用-tag-命令添加镜像标签"><a href="#使用-tag-命令添加镜像标签" class="headerlink" title="使用 tag 命令添加镜像标签"></a>使用 tag 命令添加镜像标签</h4><blockquote><p><code>docker tag ubuntu:latest myubuntu:latest</code></p></blockquote><p>为了方便在后续工作中使用特定镜像，还可以使用 <code>docker tag</code> 命令来为本地镜像任意添加新的标签。</p><h4 id="使用-inspect-命令查看详细信息"><a href="#使用-inspect-命令查看详细信息" class="headerlink" title="使用 inspect 命令查看详细信息"></a>使用 inspect 命令查看详细信息</h4><blockquote><p><code>docker inspect &lt;仓库&gt;</code></p></blockquote><p>使用 <code>docker inspect</code> 命令可以获取该镜像的详细信息，包括制作者、适应架构、各层的数字摘要等。</p><h4 id="使用-history-命令查看镜像历史"><a href="#使用-history-命令查看镜像历史" class="headerlink" title="使用 history 命令查看镜像历史"></a>使用 history 命令查看镜像历史</h4><blockquote><p><code>docker history &lt;REPOSITORY&gt;[:TAG]</code> 或 <code>docker history &lt;IMAGE ID&gt;</code></p></blockquote><p>注意，过长的命令会被自动截断了，可以使用 <code>--no-trunc</code> 选项来输出完整命令。</p><h4 id="删除镜像"><a href="#删除镜像" class="headerlink" title="删除镜像"></a>删除镜像</h4><ol><li><p>使用标签删除镜像</p><blockquote><p><code>docker rmi &lt;IMAGE&gt; [IMAGE...]</code> 或 <code>docker image rm &lt;IMAGE&gt; [IMAGE...]</code></p></blockquote></li><li><p>使用镜像 ID 来删除镜像</p><blockquote><p><code>docker rmi &lt;IMAGE ID&gt;</code></p></blockquote><p>当使用 <code>docker rmi</code> 命令，并且后面跟上镜像的 ID（也可以是能进行区分的部分 ID 串前缀）时，会先尝试删除所有指向该镜像的标签，然后删除该镜像文件本身。</p><blockquote><p>注意，当有基于该镜像创建的容器时，镜像文件默认是无法被删除的。我们可以使用 <code>docker ps -a</code> 命令可以查看本机上存在的所有容器。</p><p>最佳实践：先用 <code>docker rm &lt;Container ID&gt;</code> 删除依赖该镜像的所有容易，然后执行 <code>docker rmi &lt;IMAGE ID&gt;</code> 再来删除镜像。</p></blockquote></li></ol><h4 id="清理镜像"><a href="#清理镜像" class="headerlink" title="清理镜像"></a>清理镜像</h4><blockquote><p><code>docker image prune [options]</code></p><ul><li><code>-a</code>，<code>--all</code>：删除所有无用镜像，不光是临时镜像</li><li><code>-f</code>,<code>--force</code>：强制删除镜像，而不进行提示确认</li></ul></blockquote><p>使用 Docker 一段时间后，系统中可能会遗留一些临时的镜像文件，以及没有使用的镜像，可以通过 <code>docker image prune</code> 命令来进行清理。</p><p>我们可以结合 crontab 来定时清理，执行 <code>crontab -e</code>，写入一下配置：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"># 一定要记得在后面按 Enter 输入换行符，否则不会生效的</span><br><span class="line">59 23 * * * docker image prune -f</span><br><span class="line"></span><br></pre></td></tr></table></figure><h3 id="创建镜像"><a href="#创建镜像" class="headerlink" title="创建镜像"></a>创建镜像</h3><h4 id="1-基于已有容器创建"><a href="#1-基于已有容器创建" class="headerlink" title="1. 基于已有容器创建"></a>1. 基于已有容器创建</h4><blockquote><p><code>docker commit [OPTIONS] &lt;CONTAINER&gt; &lt;REPOSITORY&gt;[:TAG]</code></p><ul><li><code>-a</code>，<code>--author=</code>：作者信息</li><li><code>-m</code>，<code>--message=&quot;&quot;</code>：提交信息</li><li><code>-p</code>，<code>--pause=true</code>：提交时暂停容器执行</li></ul></blockquote><p>首先，启动一个 alpine 镜像，并在其中进行安装 nano 的操作，然后发布一个新的镜像：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">docker run -it alpine bash</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">docker commit -m <span class="string">&quot;install nano&quot;</span> -a <span class="string">&quot;紫升&quot;</span> ff3034d2ffa7 my-alpine:0.1</span></span><br></pre></td></tr></table></figure><h4 id="2-基于-Dockerfile-创建"><a href="#2-基于-Dockerfile-创建" class="headerlink" title="2. 基于 Dockerfile 创建"></a>2. 基于 Dockerfile 创建</h4><blockquote><p><code>docker build -t &lt;IMAGE NAME&gt; &lt;上下文路径/URL/-&gt;</code></p></blockquote><p>通过 Dockerfile 创建是最常见的方式。Dockerfile 是一个文本文件，利用指定的指令描述基于某个父镜像创建新镜像的过程。</p><p>下面给出 Dockerfile 的一个简单示例，基于 alpine 镜像安装 node 环境，构成一个新的 <code>youngjuning/alpine</code> 镜像：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">FROM alpine</span><br><span class="line"></span><br><span class="line">LABEL version=&quot;1.0&quot; maintainer=&quot;youngjuning&lt;youngjuning@aliyun.com&gt;&quot;</span><br><span class="line"></span><br><span class="line">RUN sed -i &#x27;s/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g&#x27; /etc/apk/repositories</span><br></pre></td></tr></table></figure><p>构建：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">docker build -t youngjuning/alpine:latest .</span></span><br></pre></td></tr></table></figure><h3 id="存储镜像"><a href="#存储镜像" class="headerlink" title="存储镜像"></a>存储镜像</h3><p>如果要导出镜像到本地文件，可以使用 <code>docker save</code> 命令。该命令支持 <code>-o &lt;string&gt;</code> 或 <code>--output &lt;string&gt;</code> 参数，导出镜像到指定的文件中。</p><p>例如，导出本地 alpine 镜像为文件 alpine.tar，如下所示：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">docker save -o alpine.tar alpine</span></span><br></pre></td></tr></table></figure><p>之后，用户就可以通过复制 alpine.tar 文件将该镜像分享给他人。</p><h3 id="载入镜像"><a href="#载入镜像" class="headerlink" title="载入镜像"></a>载入镜像</h3><p>可以使用 <code>docker load</code> 将导出的 tar 文件再导入到本地镜像库。支持 <code>-i &lt;string&gt;</code> 或 <code>-input &lt;string&gt;</code> 选项，从指定文件中读入镜像内容。</p><p>例如，从文件 alpine.tar 导入镜像到本地镜像列表，如下所示：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">docker load -i alpine.tar</span></span><br></pre></td></tr></table></figure><h3 id="上传镜像"><a href="#上传镜像" class="headerlink" title="上传镜像"></a>上传镜像</h3><blockquote><p><code>docker push [选项] [Docker Registry 地址[:端口号]/][用户名]&lt;仓库名&gt;[:TAG]</code></p></blockquote><p><strong>发布新版本流程：</strong></p><ul><li>发布 latest 版本：<code>docker push youngjuning/alpine:latest</code></li><li>添加新标签：<code>docker tag youngjuning/alpine:latest youngjuning/alpine:1.0.0</code></li><li>发布 1.0.0 版本：<code>docker push youngjuning/alpine:1.0.0</code></li></ul><blockquote><p>可以查看 [<a href="https://hub.docker.com/r/youngjuning/alpine]">https://hub.docker.com/r/youngjuning/alpine]</a> 项目查看我发布的基于aliyun镜像的 Aplpine Docker Image</p></blockquote><h2 id="操作-Docker-容器"><a href="#操作-Docker-容器" class="headerlink" title="操作 Docker 容器"></a>操作 Docker 容器</h2><ul><li>Docker 容器是镜像的一个运行实例。</li><li>Docker 容器是独立运行的一个（或一组）应用，以及它们必需的运行环境</li></ul><h3 id="启动容器"><a href="#启动容器" class="headerlink" title="启动容器"></a>启动容器</h3><h4 id="1-新建并启动"><a href="#1-新建并启动" class="headerlink" title="1. 新建并启动"></a>1. 新建并启动</h4><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">docker run -it ubuntu:18.04 /bin/bash</span></span><br></pre></td></tr></table></figure><p>其中, <code>-t</code> 选项让 Docker 分配一个伪终端（pseudo-tty）并绑定到容器的标准输入上，<code>-i</code> 则让容器的标准输入保持打开。</p><p>当利用 <code>docker run</code> 来创建容器时，Docker 在后台运行的标准操作包括：</p><ol><li>检查本地是否存在指定的镜像，不存在就从公有仓库下载</li><li>利用镜像创建并启动一个容器</li><li>分配一个文件系统，并在只读的镜像层外面挂在一层可读写层</li><li>从宿主主机配置的网桥接口中桥接一个虚拟接口到容器中去</li><li>从地址池配置一个ip地址给容器</li><li>执行用户指定的应用程序</li><li>执行完毕后容器被终止</li></ol><p><strong>一些常用选项：</strong></p><ul><li><code>-d</code>，<code>--detach=true|false</code>：是否在后台运行容器，默认为<code>false</code></li><li><code>-i</code>，<code>--interactive=true|false</code>：保持标准输入打开，默认为 <code>false</code></li><li><code>-p</code>，<code>--publish=[]</code>：指定如何映射到本地主机端口，例如 <code>-p 9000:9000</code></li><li><code>--restart=&quot;no&quot;</code>：容器的重启策略，包括 <code>no</code>、<code>on-failure[:max-retry]</code>、<code>always</code>、<code>unless-stopped</code> 等</li><li><code>--rm=true|false</code>：容器退出后是否自动删除，不能跟 <code>-d</code> 同时使用</li><li><code>-t</code>，<code>--tty=true|false</code>：是否分配一个伪终端，默认为 <code>false</code></li><li><code>-v [HOST-DIR:]&lt;CONTAINER-DIR&gt;[:OPTIONS]</code>，<code>--volume=[HOST-DIR:]&lt;CONTAINER-DIR&gt;[:OPTIONS]</code>：挂在主机上的文件卷到容器内</li><li><code>--name=&quot;&quot;</code>：指定容器的别名</li></ul><h4 id="2-启动已终止容器"><a href="#2-启动已终止容器" class="headerlink" title="2. 启动已终止容器"></a>2. 启动已终止容器</h4><p>可以利用 <code>docker start &lt;CONTAINER ID&gt;</code> 命令，直接将一个已经终止的容器启动运行。</p><h4 id="3-查看容器输出"><a href="#3-查看容器输出" class="headerlink" title="3. 查看容器输出"></a>3. 查看容器输出</h4><p>要获取容器的输出信息，可以通过 <code>docker &lt;CONTAINER ID&gt; logs</code> 命令。</p><h3 id="终止容器"><a href="#终止容器" class="headerlink" title="终止容器"></a>终止容器</h3><p>可以使用 <code>docker stop &lt;CONTAINER ID&gt;</code> 来终止一个运行中的容器。</p><p>处于终止状态的容器，可以通过 <code>docker container start</code> 命令来重新启动。</p><p>此外，<code>docker container restart</code> 命令会将一个运行态的容器终止，然后再重新启动它。</p><h3 id="exec进入容器"><a href="#exec进入容器" class="headerlink" title="exec进入容器"></a><code>exec</code>进入容器</h3><p>在使用 <code>-d</code> 参数时，容器启动后会进入后台。</p><p>某些时候需要进入容器进行操作，推荐大家使用 <code>docker exec</code> 命令：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">docker run -dit alpine</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">docker ps</span></span><br><span class="line">CONTAINER ID        IMAGE                 COMMAND             CREATED             STATUS              PORTS                      NAMES</span><br><span class="line">3d95dabef801        alpine                &quot;/bin/sh&quot;           21 seconds ago      Up 19 seconds                                  recursing_aryabhata</span><br></pre></td></tr></table></figure><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">docker <span class="built_in">exec</span> -it &lt;CONTAINER ID&gt;</span></span><br></pre></td></tr></table></figure><p>如果从这个 stdin 中 exit，不会导致容器的停止。</p><h3 id="删除容器"><a href="#删除容器" class="headerlink" title="删除容器"></a>删除容器</h3><p>可以使用 <code>docker container rm</code> 来删除一个处于终止状态的容器。例如</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">docker <span class="built_in">rm</span>  &lt;CONTAINER ID&gt;</span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">删除运行中的容器，并删除容器挂载的数据卷</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">docker <span class="built_in">rm</span> -vf</span></span><br></pre></td></tr></table></figure><p>如果要删除一个运行中的容器，可以添加 <code>-f</code> 参数。Docker 会发送 <code>SIGKILL</code> 信号给容器。</p><h3 id="清理所有处于终止状态的容器"><a href="#清理所有处于终止状态的容器" class="headerlink" title="清理所有处于终止状态的容器"></a>清理所有处于终止状态的容器</h3><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">docker container prune</span></span><br></pre></td></tr></table></figure><h3 id="导出和导入容器"><a href="#导出和导入容器" class="headerlink" title="导出和导入容器"></a>导出和导入容器</h3><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">docker <span class="built_in">export</span> 7691a814370e &gt; ubuntu.tar</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash"><span class="built_in">cat</span> ubuntu.tar | docker import - <span class="built_in">test</span>/ubuntu:v1.0</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">docker import http://example.com/exampleimage.tgz example/imagerepo</span></span><br></pre></td></tr></table></figure><h3 id="查看容器"><a href="#查看容器" class="headerlink" title="查看容器"></a>查看容器</h3><h4 id="1-查看容器详情"><a href="#1-查看容器详情" class="headerlink" title="1. 查看容器详情"></a>1. 查看容器详情</h4><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">docker inspect [OPTIONS] &lt;CONTAINER ID&gt;</span></span><br></pre></td></tr></table></figure><h4 id="2-查看容器内进程"><a href="#2-查看容器内进程" class="headerlink" title="2. 查看容器内进程"></a>2. 查看容器内进程</h4><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">docker top [OPTIONS] &lt;CONTAINER ID&gt;</span></span><br></pre></td></tr></table></figure><h4 id="3-查看统计信息"><a href="#3-查看统计信息" class="headerlink" title="3. 查看统计信息"></a>3. 查看统计信息</h4><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">docker stats [OPTIONS] &lt;CONTAINER ID&gt;</span></span><br></pre></td></tr></table></figure><h3 id="更新配置"><a href="#更新配置" class="headerlink" title="更新配置"></a>更新配置</h3><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">docker update --restart=always &lt;CONTAINER ID&gt;</span></span><br></pre></td></tr></table></figure><h3 id="重命名容器"><a href="#重命名容器" class="headerlink" title="重命名容器"></a>重命名容器</h3><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">docker rename &lt;old name&gt; &lt;new name&gt;</span></span><br></pre></td></tr></table></figure><h3 id="查看容器日志"><a href="#查看容器日志" class="headerlink" title="查看容器日志"></a>查看容器日志</h3><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">docker logs -f &lt;CONTAINER ID&gt;</span></span><br></pre></td></tr></table></figure><h3 id="Portainer-容器管理工具"><a href="#Portainer-容器管理工具" class="headerlink" title="Portainer 容器管理工具"></a>Portainer 容器管理工具</h3><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">docker volume create portainer_data</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">docker run -d -p 9000:9000 \</span></span><br><span class="line"><span class="language-bash">-v /var/run/docker.sock:/var/run/docker.sock \</span></span><br><span class="line"><span class="language-bash">-v portainer_data:/data \</span></span><br><span class="line"><span class="language-bash">--name portainer \</span></span><br><span class="line"><span class="language-bash">--restart always \</span></span><br><span class="line"><span class="language-bash">portainer/portainer</span></span><br></pre></td></tr></table></figure><p>配置 <code>/etc/nginx/sites-enabled/dafulat</code> 文件：</p><figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">upstream</span> portainer &#123;</span><br><span class="line">    <span class="attribute">server</span> <span class="number">127.0.0.1:9000</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="section">server</span> &#123;</span><br><span class="line">  <span class="attribute">listen</span> <span class="number">80</span>;</span><br><span class="line"></span><br><span class="line">  <span class="section">location</span> /portainer/ &#123;</span><br><span class="line">      <span class="attribute">proxy_http_version</span> <span class="number">1</span>.<span class="number">1</span>;</span><br><span class="line">      <span class="attribute">proxy_set_header</span> Connection <span class="string">&quot;&quot;</span>;</span><br><span class="line">      <span class="attribute">proxy_pass</span> http://portainer/;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="section">location</span> /portainer/ws/ &#123;</span><br><span class="line">      <span class="attribute">proxy_set_header</span> Upgrade <span class="variable">$http_upgrade</span>;</span><br><span class="line">      <span class="attribute">proxy_set_header</span> Connection <span class="string">&quot;upgrade&quot;</span>;</span><br><span class="line">      <span class="attribute">proxy_http_version</span> <span class="number">1</span>.<span class="number">1</span>;</span><br><span class="line">      <span class="attribute">proxy_pass</span> http://portainer/ws/;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="Docker-数据持久化"><a href="#Docker-数据持久化" class="headerlink" title="Docker 数据持久化"></a>Docker 数据持久化</h2><p><code>数据卷</code> 是一个可供一个或多个容器使用的特殊目录，它绕过 UFS，可以提供很多有用的特性：</p><ul><li><code>数据卷</code> 可以在容器之间共享和重用</li><li>对 <code>数据卷</code> 的修改会立马生效</li><li>对 <code>数据卷</code> 的更新，不会影响镜像</li><li><code>数据卷</code> 默认会一直存在，即使容器被删除</li></ul><h3 id="1-创建数据卷"><a href="#1-创建数据卷" class="headerlink" title="1. 创建数据卷"></a>1. 创建数据卷</h3><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">docker volume create my-vol</span></span><br></pre></td></tr></table></figure><p>除了 <code>create</code> 子命令外，docker volume 还支持 <code>inspect</code>(查看详细信息)、<code>ls</code>（列出已有数据卷）、<code>prune</code>（清理无用数据卷）、<code>rm</code>（删除数据卷）</p><h3 id="2-绑定数据卷"><a href="#2-绑定数据卷" class="headerlink" title="2. 绑定数据卷"></a>2. 绑定数据卷</h3><h4 id="mount"><a href="#mount" class="headerlink" title="--mount"></a><code>--mount</code></h4><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">$ docker run -d -P \</span><br><span class="line">    --name web \</span><br><span class="line">    --mount <span class="built_in">source</span>=my-vol,target=/webapp \</span><br><span class="line">    training/webapp \</span><br><span class="line">    python app.py</span><br></pre></td></tr></table></figure><h4 id="v，-volume"><a href="#v，-volume" class="headerlink" title="-v，--volume"></a><code>-v</code>，<code>--volume</code></h4><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">docker run -d -P \</span></span><br><span class="line"><span class="language-bash">    --name web \</span></span><br><span class="line"><span class="language-bash">    -v my-vol:/webapp \</span></span><br><span class="line"><span class="language-bash">    training/webapp \</span></span><br><span class="line"><span class="language-bash">    python app.py</span></span><br></pre></td></tr></table></figure><blockquote><p>source 也可以是绝对路径的任意系统位置。</p></blockquote><blockquote><p>如果直接挂载一个文件到容器，使用文件编辑工具，包括 vi 或者 <code>sed --in-place</code> 的时候，可能会造成文件 inode 的改变，从 Docker 1.1 起，这会导致报错误信息。所以推荐的方式是直接挂载文件所在的目录到容器内。</p></blockquote><h2 id="Dockerfile"><a href="#Dockerfile" class="headerlink" title="Dockerfile"></a>Dockerfile</h2><blockquote><p>详细指令详解请查看：<a href="https://vuepress.mirror.docker-practice.com/image/dockerfile/">Dockerfile 指令详解</a></p></blockquote><p><img src="https://i.loli.net/2020/04/11/hmlMV4QA2opON9j.png" alt="紫升"></p><h2 id="应用安装"><a href="#应用安装" class="headerlink" title="应用安装"></a>应用安装</h2><h2 id="GitLab-及其官方镜像"><a href="#GitLab-及其官方镜像" class="headerlink" title="GitLab 及其官方镜像"></a>GitLab 及其官方镜像</h2><blockquote><p>特别耗CPU，我的服务器太辣鸡带不动！！！</p></blockquote><h3 id="docker-compose-yml"><a href="#docker-compose-yml" class="headerlink" title="docker-compose.yml"></a>docker-compose.yml</h3><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">web:</span></span><br><span class="line">  <span class="attr">image:</span> <span class="string">&#x27;gitlab/gitlab-ce:latest&#x27;</span></span><br><span class="line">  <span class="attr">restart:</span> <span class="string">always</span></span><br><span class="line">  <span class="attr">hostname:</span> <span class="string">&#x27;gitlab.yangjunning.pro&#x27;</span></span><br><span class="line">  <span class="attr">environment:</span></span><br><span class="line">    <span class="attr">GITLAB_OMNIBUS_CONFIG:</span> <span class="string">|</span></span><br><span class="line"><span class="string">      external_url &#x27;http://gitlab.yangjunning.pro:8929&#x27;</span></span><br><span class="line"><span class="string">      gitlab_rails[&#x27;gitlab_shell_ssh_port&#x27;] = 2224</span></span><br><span class="line"><span class="string"></span>  <span class="attr">ports:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">&#x27;8929:8929&#x27;</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">&#x27;2224:22&#x27;</span></span><br><span class="line">  <span class="attr">volumes:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">&#x27;gitlab_config:/etc/gitlab&#x27;</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">&#x27;gitlab_logs:/var/log/gitlab&#x27;</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">&#x27;gitlab_data:/var/opt/gitlab&#x27;</span></span><br></pre></td></tr></table></figure><h3 id="运行容器"><a href="#运行容器" class="headerlink" title="运行容器"></a>运行容器</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ docker-compose up -d</span><br></pre></td></tr></table></figure><h3 id="更新-gitlab"><a href="#更新-gitlab" class="headerlink" title="更新 gitlab"></a>更新 gitlab</h3><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">docker-compose pull</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">docker-compose up -d</span></span><br></pre></td></tr></table></figure><h2 id="Docker-相关的定时任务"><a href="#Docker-相关的定时任务" class="headerlink" title="Docker 相关的定时任务"></a>Docker 相关的定时任务</h2><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"># crontab -e</span><br><span class="line"># 每天凌晨强制删除无用镜像，不光是临时镜像；每天凌晨清理无用的数据卷</span><br><span class="line">00 00 * * * docker image prune -af &amp;&amp; docker volume prune -f &amp;&amp; rsync -arv /var/lib/docker/volumes /backups/docker</span><br></pre></td></tr></table></figure><blockquote><p>qshell 同步文件到七牛云的配置请参考<a href="https://juejin.im/post/5e81e2db518825737b4ad911#heading-59">备份到七牛云</a></p></blockquote>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/8e0831eec81f/</id>
    <link href="https://blog.zisheng.pro/posts/8e0831eec81f/"/>
    <published>2020-03-30T16:23:31.000Z</published>
    <summary>Docker 是个划时代的开源项目，它彻底释放了计算虚拟化的威力，极大提高了应用的维护效率，降低了云计算应用开发的成本！使用 Docker，可以让应用的部署、测试和分发都变得前所未有的高效和轻松！</summary>
    <title>一个前端工程师的 Docker 学习笔记</title>
    <updated>2026-09-18T08:15:28.636Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="运维" scheme="https://blog.zisheng.pro/categories/%E8%BF%90%E7%BB%B4/"/>
    <category term="Linux" scheme="https://blog.zisheng.pro/tags/Linux/"/>
    <category term="Ubuntu" scheme="https://blog.zisheng.pro/tags/Ubuntu/"/>
    <category term="阿里云" scheme="https://blog.zisheng.pro/tags/%E9%98%BF%E9%87%8C%E4%BA%91/"/>
    <category term="腾讯云" scheme="https://blog.zisheng.pro/tags/%E8%85%BE%E8%AE%AF%E4%BA%91/"/>
    <category term="华为云" scheme="https://blog.zisheng.pro/tags/%E5%8D%8E%E4%B8%BA%E4%BA%91/"/>
    <category term="GNU" scheme="https://blog.zisheng.pro/tags/GNU/"/>
    <category term="ssh" scheme="https://blog.zisheng.pro/tags/ssh/"/>
    <content>
      <![CDATA[<p>笔者使用的是 AliYun 服务器 ECS，镜像选择的是 Ubuntu，Ubuntu的详细版本信息是：<code>Ubuntu 18.04.4 LTS (GNU/Linux 4.15.0-91-generic x86_64)</code>，如何购买云服务器不在本文讨论范围内，如果只是学习，那你完全可以使用 virtualbox 安装 Ubuntu。</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_"># </span><span class="language-bash">连接服务器</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">ssh &lt;username&gt;@&lt;hostname&gt;</span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">更新系统源</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">apt update</span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">升级系统源</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">apt upgrade</span></span><br></pre></td></tr></table></figure><h2 id="必备环境及应用"><a href="#必备环境及应用" class="headerlink" title="必备环境及应用"></a>必备环境及应用</h2><h3 id="Git"><a href="#Git" class="headerlink" title="Git"></a>Git</h3><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_"># </span><span class="language-bash">安装 git</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">apt install git</span></span><br></pre></td></tr></table></figure><h4 id="命令行配置"><a href="#命令行配置" class="headerlink" title="命令行配置"></a>命令行配置</h4><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 1、初始化设置</span></span><br><span class="line">$ git config --global user.name <span class="string">&#x27;youngjuning&#x27;</span></span><br><span class="line">$ git config --global user.email <span class="string">&#x27;youngjuning@aliyun.com&#x27;</span></span><br><span class="line"><span class="comment"># 2、将 `color.ui` 设置为 `auto` 可以让命令的输出拥有更高的可读性。</span></span><br><span class="line">$ git config --global color.ui auto</span><br><span class="line"><span class="comment"># 3、git 记住用户名和密码</span></span><br><span class="line">$ git config --global credential.helper store</span><br><span class="line"><span class="comment"># 4、core.autocrlf</span></span><br><span class="line">$ git config --global core.autocrlf input</span><br></pre></td></tr></table></figure><blockquote><p>Linux或Mac系统使用LF作为行结束符，因此你不想 Git 在签出文件时进行自动的转换；当一个以 <code>CRLF</code> 为行结束符的文件不小心被引入时你肯定想进行修正，把 <code>core.autocrlf</code> 设置成 <code>input</code> 来告诉 Git 在提交时把 <code>CRLF</code> 转换成 <code>LF</code>，签出时不转换：<br>这样会在 Windows 系统上的签出文件中保留 <code>CRLF</code>，会在 Mac 和 Linux 系统上，包括仓库中保留 <code>LF</code>。</p></blockquote><h3 id="Java"><a href="#Java" class="headerlink" title="Java"></a>Java</h3><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_"># </span><span class="language-bash">安装 jre、jdk</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">apt install openjdk-8-jre-headless openjdk-8-jdk-headless</span></span><br></pre></td></tr></table></figure><h3 id="Node"><a href="#Node" class="headerlink" title="Node"></a>Node</h3><p>Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行时。我们使用 <code>n</code> 模块来维护 Node 的版本：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">apt install nodejs npm build-essential</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">sudo npm install -g npm n</span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">安装最新版</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">sudo n latest</span></span><br><span class="line"><span class="meta prompt_">#</span><span class="language-bash"><span class="comment"># 其他命令##</span></span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">安装指定版本</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">sudo n 10.16.0</span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">安装最新的稳定版</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">sudo n lts</span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">删除指定版本</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">n <span class="built_in">rm</span> 12.10.0</span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">除去当前版本以外的所有缓存版本</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">n prune</span></span><br></pre></td></tr></table></figure><h3 id="oh-my-zsh"><a href="#oh-my-zsh" class="headerlink" title="oh-my-zsh"></a>oh-my-zsh</h3><blockquote><p>修改 <code>~/.zshrc</code> 之后都需要执行 <code>source ~/.zshrc</code> 命令使之立即生效</p><p><code>echo $SHELL</code> 可以查看当前 Shell</p></blockquote><ul><li><code>apt install zsh</code></li><li><code>git clone https://github.com/robbyrussell/oh-my-zsh.git ~/.oh-my-zsh</code></li><li><code>cp ~/.oh-my-zsh/templates/zshrc.zsh-template ~/.zshrc</code></li><li><code>nano ~/.zshrc</code></li><li>找到 <code>ZSH_THEME=&quot;robbyrussell&quot;</code> ，把 <code>robbyrussell</code> 替换为 <code>ys</code></li><li>修改默认Shell：<code>chsh -s /bin/zsh</code></li><li>命令高亮<ul><li><code>git clone https://github.com/zsh-users/zsh-syntax-highlighting.git &quot;$HOME/.zsh-syntax-highlighting&quot; --depth 1</code></li><li><code>echo &quot;source $HOME/.zsh-syntax-highlighting/zsh-syntax-highlighting.zsh&quot; &gt;&gt; &quot;$HOME/.zshrc&quot;</code></li></ul></li><li>重新打开命令行</li><li><code>echo &quot;export EDITOR=nano&quot; &gt;&gt; ~/.zshrc</code></li></ul><h3 id="vim"><a href="#vim" class="headerlink" title="vim"></a>vim</h3><blockquote><p>建议使用：<a href="https://github.com/amix/vimrc">https://github.com/amix/vimrc</a></p></blockquote><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">git <span class="built_in">clone</span> --depth=1 https://github.com/amix/vimrc.git ~/.vim_runtime</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">sh ~/.vim_runtime/install_awesome_vimrc.sh</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash"><span class="built_in">echo</span> <span class="string">&quot;set number&quot;</span> &gt;&gt; ~/.vimrc</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash"><span class="built_in">echo</span> <span class="string">&quot;set showcmd&quot;</span> &gt;&gt; ~/.vimrc</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash"><span class="built_in">source</span> ~/.vimrc</span></span><br></pre></td></tr></table></figure><h2 id="目录"><a href="#目录" class="headerlink" title="目录"></a>目录</h2><ul><li><code>/</code><ul><li><code>bin</code>：包含了会被所有用户使用的可执行程序</li><li><code>boot</code>：包含与 Linux 启动密切相关的文件</li><li><code>dev</code>：包含外设。它里面的子目录，每一个对应一个外设</li><li><code>etc</code>：包含系统的配置文件</li><li><code>home</code>：用户的私人目录</li><li><code>lib</code>：包含被程序所调用的库文件，例如<code>.so</code>结尾的文件</li><li><code>media</code>：可移动的外设（USB盘，SD卡，DVD，光盘，等等）插入电脑时</li><li><code>mnt</code>：用于临时挂载一些装置</li><li><code>opt</code>：可选的应用软件包，用于安装多数第三方软件和插件</li><li><code>root</code>：超级用户 root 的家目录</li><li><code>sbin</code>：用于包含系统级的重要可执行程序</li><li><code>srv</code>：包含一些网络服务启动之后所需要取用的数据</li><li><code>tmp</code>：普通用户和程序存放临时文件的地方</li><li><code>usr</code>：Unix Software Resource，安装了大部分用户要调用的程序</li><li><code>var</code>：通常包含程序的数据，比如 log（日志）文件</li></ul></li></ul><h2 id="常用命令"><a href="#常用命令" class="headerlink" title="常用命令"></a>常用命令</h2><blockquote><p>Linux命令搜索引擎：<a href="https://git.io/linux">https://git.io/linux</a></p></blockquote><ul><li><code>reboot</code>：重新启动正在运行的Linux操作系统</li><li><code>halt</code>：关闭系统</li><li><code>shutdown</code>：用来系统关机命令。shutdown指令可以关闭所有程序，并依用户的需要，进行重新开机或关机的动作。</li><li><code>poweroff</code>：直接运行即可关机</li><li><code>history</code>：用于显示历史命令</li><li><code>whice</code>：查找并显示给定命令的绝对路径</li><li><code>pwd</code>：显示当前工作目录。（Print Working Directory）</li><li><code>source</code>：在当前Shell环境中从指定文件读取和执行命令，让文件修改立即生效。</li><li><code>updatedb</code>：创建或更新<code>locate</code>命令所必需的数据库文件</li><li><code>uname -a</code>：显示全部Linux系统信息</li><li><code>cat /proc/version</code>：显示Linux信息</li><li><code>cat /etc/lsb-release</code>：查看发行版信息</li><li><code>echo $SHELL</code>：当前的shell</li><li><code>service &lt;service&gt; force-reload|reload|restart|start|status|stop</code></li><li><code>systemctl start|stop|restart|status|reload</code>：<ul><li><code>enable</code>：开机自动启动服务</li><li><code>disable</code>：开机不自动启动服务</li><li><code>is-enabled</code>：查看服务是否开机自动启动</li><li><code>list-unit-files --type=service</code>：查看各个级别下服务的启动和禁用情况</li></ul></li></ul><h2 id="文件管理"><a href="#文件管理" class="headerlink" title="文件管理"></a>文件管理</h2><h3 id="文件目录操作"><a href="#文件目录操作" class="headerlink" title="文件目录操作"></a>文件目录操作</h3><ul><li><p><code>ls</code>：显示目录内容列表</p><ul><li><code>-a</code>：列出所有文件，包括以 “.” 开头的隐含文件。</li><li><code>-l</code>：除每个文件名外，增加显示文件类型、权限、硬链接数、所有者名、组名、大小（byte）及时间信息（如未指明是其它时间即指修改时间），可以用 <code>ll</code> 代替</li></ul></li><li><p><code>du</code>：显示每个文件和目录的磁盘使用空间（Disk Usage）</p><ul><li><code>-a</code>或<code>-all</code>：显示目录中个别文件的大小。</li><li><code>-h</code>或<code>--human-readable</code>：以K，M，G为单位，提高信息的可读性。</li><li><code>-s</code>或<code>--summarize</code>：仅显示总计，只列出最后加总的值。</li></ul></li><li><p><code>cat -N</code>：连接多个文件并打印到标准输出。(concatenate)</p></li><li><p><code>less -N</code></p><ul><li>空格键：前进一页（一个屏幕）</li><li>b键：后退一页（一个屏幕）</li><li>d键：前进半页（半个屏幕）</li><li>u键：后退半页（半个屏幕）</li><li>回车键&#x2F;e键&#x2F;下箭头：前进一行</li><li>y键&#x2F;上箭头：后退一行</li><li>q键：停止读取文件，中止 less 命令</li><li>&#x3D;键：显示你在文件中的什么位置</li><li>&#x2F;键：进入搜索模式<ul><li>n键：跳转到下一个匹配项</li><li>shift+n：跳转到上一个匹配项</li></ul></li></ul></li><li><p><code>head</code>：显示文件的开头部分</p></li><li><p><code>tail</code>：在屏幕上显示指定文件的末尾若干行</p><ul><li><code>-f</code>：显示文件最新追加的内容。</li><li><code>-s&lt;秒数&gt;</code>：与<code>-f</code>选项连用，指定监视文件变化时间隔的秒数，默认1秒</li></ul></li><li><p><code>touch</code>：创建新的空文件</p></li><li><p><code>mkdir</code>：用来创建目录</p><ul><li><code>-p</code>或<code>--parents</code>：若所要建立目录的上层目录目前尚未建立，则会一并建立上层目录</li></ul></li><li><p><code>cp</code>：将源文件或目录复制到目标文件或目录中</p><ul><li><code>-r</code>或<code>-R</code>：递归处理，将指定目录下的所有文件与子目录一并处理，recursive</li></ul></li><li><p><code>mv</code>：用来对文件或目录重新命名</p></li><li><p><code>rm</code>：用于删除给定的文件和目录</p><ul><li><code>-r</code>或<code>-R</code>：递归处理，将指定目录下的所有文件与子目录一并处理，recursive</li><li><code>-f</code>：强制删除文件或目录</li></ul></li><li><p><code>ln</code>：用来为文件创建链接</p><blockquote><ul><li>硬链接缺陷：一般情况下，只能创建指向文件的硬链接，不能创建指向目录的</li><li>硬链接指的是使用了同一个<code>inode</code>号，但是文件名不一样</li><li><code>ls -i</code>：显示<code>inode</code>号</li></ul></blockquote><ul><li><code>-s</code>, <code>--symbolic</code>：对源文件建立符号链接，而非硬链接</li></ul></li><li><p><code>locate</code>：比 <code>find</code> 好用的文件查找工具，需要配合 <code>updatedb</code> 使用</p></li><li><p><code>find</code>：在指定目录下查找文件</p><blockquote><p><code>find [何处] &lt;何物&gt; [做什么]</code></p></blockquote><ul><li><p><code>-name &lt;范本样式&gt;</code>：指定字符串作为寻找文件或目录的范本样式</p></li><li><p><code>-size &lt;文件大小&gt;</code>：查找符合指定的文件大小的文件</p></li><li><p><code>-type &lt;文件类型&gt;</code>：只寻找符合指定的文件类型的文件；</p><ul><li><strong>f</strong> 普通文件</li><li><strong>l</strong> 符号连接</li><li><strong>d</strong> 目录</li><li><strong>c</strong> 字符设备</li><li><strong>b</strong> 块设备</li><li><strong>s</strong> 套接字</li><li><strong>p</strong> Fifo</li></ul></li><li><p><code>-atime &lt;24小时数&gt;</code>：查找在指定时间曾被存取过的文件或目录，单位以24小时计算；</p></li><li><p><code>-exec &lt;执行指令&gt;</code>：假设find指令的回传值为True，就执行该指令</p><blockquote><p><code>find . -name *.txt -exec chmod 600 {} \;</code></p><p>可以换成<code>-ok</code>,ok 会让你确认操作</p></blockquote></li></ul></li></ul><h3 id="权限管理"><a href="#权限管理" class="headerlink" title="权限管理"></a>权限管理</h3><ul><li><p><code>sudo su</code>：切换 root 身份，substitute do</p></li><li><p><code>useradd -m</code>：创建的新的系统用户自动创建用户的家目录</p></li><li><p><code>passwd</code>：用于让用户可以更改自己的密码</p></li><li><p>&#96;userdel -r：用于删除给定的用户以及与用户相关的文件</p></li><li><p><code>users</code>：显示当前登录系统的所有用户</p></li><li><p><code>usermod</code>：用于修改用户的基本信息</p><ul><li><code>-l</code>：对用户重命名。<code>/home</code> 中的用户家目录名不改变，需要手动修改</li><li><code>-g</code>：修改用户所在群组</li><li><code>-G</code>：将用户添加到多个群组，多个群组用<code>,</code>分割</li><li><code>-aG</code> 或 <code>-ag</code>：不离开原来的群组进入新的群组</li></ul></li><li><p><code>groupadd</code>：用于创建一个新的工作组</p></li><li><p><code>groups</code>：用来打印指定用户所属的工作组</p></li><li><p><code>chown</code>：用来变更文件或目录的拥有者或所属群组</p><blockquote><p><code>chown &lt;username&gt;[:gruop] &lt;file|dir&gt;</code></p></blockquote><ul><li><code>-R</code>：递归处理，将指定目录下的所有文件及子目录一并处理</li></ul></li><li><p><code>chgrp</code>：用来变更文件或目录的所属群组</p></li><li><p><code>chmod</code>：用来变更文件或目录的权限</p><blockquote><img src="https://i.loli.net/2020/03/28/fiXQkrtNn8yZLWY.png" style="zoom:50%;" /><p>一共有10位，第<strong>1</strong>位代表文件类型，第<strong>2-4</strong>位表示所有者的读、写和执行权限，第<strong>5-7</strong>位表示同组用户的读、写和执行权限，第<strong>8-10</strong>位表示其他用户的读、写和执行权限。如果没有权限用<code>-</code>占位。如果开启 SELinux，则第<strong>11</strong>位会多一个<code>.</code>。</p><ul><li>文件类型<ul><li><code>d</code>：英语 directory 的缩写，表示<strong>目录</strong></li><li><code>l</code>：英文 link 的缩写，表示<strong>链接</strong></li><li><code>-</code>：文件</li></ul></li><li><code>r</code>：英语 read 的缩写，表示<strong>读</strong></li><li><code>w</code>：英文 write 的缩写，表示<strong>写</strong></li><li><code>x</code>：英语 execute 的缩写，表示<strong>执行&#x2F;运行</strong>。可以运行这个文件</li><li><code>.</code>：SELinux 的安全标签，如果第11位有<code>.</code>，表示启用了 SELinux</li></ul></blockquote><ul><li><p>用数字来分配权限</p><ul><li>没有权限：0</li><li>执行权限：1</li><li>写权限：2</li><li>写执行权限：3</li><li>读权限：4</li><li>读执行权限：5</li><li>读写权限：6</li><li>读写执行权限：7</li></ul></li><li><p>用字母来分配权限</p><ul><li>u：user 的缩写，是英语用户的意思。表示所有者</li><li>g：group的缩写，是英语群组的意思。表示群组用户</li><li>o：other的缩写，是英语其他的意思。表示其他用户</li><li>a：all的缩写，是英语所有的意思。表示所有用户</li><li>+：加号，表示添加权限</li><li>-：减号，表示去除权限</li><li>&#x3D;：等号，表示分配权限</li></ul><blockquote><p><code>chmod u+rx file</code>：文件 file 的所有者增加读和运行的权限</p><p><code>chmod g+r file</code>：文件 file 的群组其他用户增加读的权限</p><p><code>chmod o-r file</code>：文件 file 的其他用户移除读的权限</p><p><code>chmod g+r o-r file</code>：文件 file 的群组其他用户增加读的权限，其他用户移除读的权限</p><p><code>chmod go-r file</code>：文件 file 的群组其他用户和其他用户均移除读的权限</p><p><code>chmod +x file</code>：文件 file 的所有用户增加运行的权限</p><p><code>chmod u=rwx,g=r,0=- file</code>：文件 file 的所有者分配读，写和执行的权限；群组其他用户分配读的权限，不能写或执行；其他用户没有任何权限</p></blockquote></li></ul></li></ul><h2 id="正则表达式和数据操作"><a href="#正则表达式和数据操作" class="headerlink" title="正则表达式和数据操作"></a>正则表达式和数据操作</h2><h3 id="grep-筛选数据"><a href="#grep-筛选数据" class="headerlink" title="grep: 筛选数据"></a>grep: 筛选数据</h3><p><strong>grep</strong> （global search regular expression(RE) and print out the line，全面搜索正则表达式并把行打印出来）是一种强大的文本搜索工具，它能使用正则表达式搜索文本，并把匹配的行打印出来。用于过滤&#x2F;搜索的特定字符。可使用正则表达式能多种命令配合使用，使用上十分灵活。</p><ul><li><code>-i</code> <code>--ignore-case</code>：忽略字符大小写的差别。</li><li><code>-n</code>  <code>--line-number</code>：在显示符合范本样式的那一列之前，标示出该列的编号。</li><li><code>-v</code> <code>--revert-match</code>：反转查找。</li><li><code>-R/-r</code>  <code>--recursive</code>：在所有子目录和子文件中查找，可以用  <code>rgrep</code> 替代</li><li><code>-E</code> <code>--extended-regexp</code>：将范本样式为延伸的普通表示法来使用，意味着使用能使用扩展正则表达式。可以用 <code>egrep</code> 代替<ul><li><code>grep -E ^p /etc/profile</code></li><li><code>grep -E [at] /etc/profile</code></li><li><code>grep -E [0-4] /etc/profile</code></li><li><code>grep -E [a-zA-Z] /etc/profile</code></li></ul></li></ul><h4 id="规则表达式"><a href="#规则表达式" class="headerlink" title="规则表达式"></a>规则表达式</h4><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">^    # 锚定行的开始 如：&#x27;^grep&#x27;匹配所有以grep开头的行。</span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">   <span class="comment"># 锚定行的结束 如：&#x27;grep$&#x27; 匹配所有以grep结尾的行。</span></span></span><br><span class="line">.    # 匹配一个非换行符的字符 如：&#x27;gr.p&#x27;匹配gr后接一个任意字符，然后是p。</span><br><span class="line">*    # 匹配零个或多个先前字符 如：&#x27;*grep&#x27;匹配所有一个或多个空格后紧跟grep的行。</span><br><span class="line">.*   # 一起用代表任意字符。</span><br><span class="line">[]   # 匹配一个指定范围内的字符，如&#x27;[Gg]rep&#x27;匹配Grep和grep。</span><br><span class="line">[^]  # 匹配一个不在指定范围内的字符，如：&#x27;[^A-FH-Z]rep&#x27;匹配不包含A-R和T-Z的一个字母开头，紧跟rep的行。</span><br><span class="line">\(..\)  # 标记匹配字符，如&#x27;\(love\)&#x27;，love被标记为1。</span><br><span class="line">\&lt;      # 锚定单词的开始，如:&#x27;\&lt;grep&#x27;匹配包含以grep开头的单词的行。</span><br><span class="line">\&gt;      # 锚定单词的结束，如&#x27;grep\&gt;&#x27;匹配包含以grep结尾的单词的行。</span><br><span class="line">x\&#123;m\&#125;  # 重复字符x，m次，如：&#x27;0\&#123;5\&#125;&#x27;匹配包含5个o的行。</span><br><span class="line">x\&#123;m,\&#125;   # 重复字符x,至少m次，如：&#x27;o\&#123;5,\&#125;&#x27;匹配至少有5个o的行。</span><br><span class="line">x\&#123;m,n\&#125;  # 重复字符x，至少m次，不多于n次，如：&#x27;o\&#123;5,10\&#125;&#x27;匹配5--10个o的行。</span><br><span class="line">\w    # 匹配文字和数字字符，也就是[A-Za-z0-9]，如：&#x27;G\w*p&#x27;匹配以G后跟零个或多个文字或数字字符，然后是p。</span><br><span class="line">\W    # \w的反置形式，匹配一个或多个非单词字符，如点号句号等。</span><br><span class="line">\b    # 单词锁定符，如: &#x27;\bgrep\b&#x27;只匹配grep。</span><br></pre></td></tr></table></figure><h3 id="sort-文件排序"><a href="#sort-文件排序" class="headerlink" title="sort 文件排序"></a>sort 文件排序</h3><p>对文本文件中所有行进行排序。</p><ul><li><code>-o</code>, <code>--output=FILE</code>：将结果写入FILE而不是标准输出。<code>sort -o name.txt name_sorted.txt</code></li><li><code>-r</code>, <code>--reverse</code>：将结果倒序排列。</li><li><code>-R</code>：<code>--random-sort</code> 随机排序，但分组相同的行。</li><li><code>-n</code>, <code>--numeric-sort</code>：根据数字排序。</li></ul><h4 id="主要用途"><a href="#主要用途" class="headerlink" title="主要用途"></a>主要用途</h4><ul><li>将所有输入文件的内容排序后并输出。</li><li>当没有文件或文件为<code>-</code>时，读取标准输入。</li></ul><h3 id="wc-文件统计"><a href="#wc-文件统计" class="headerlink" title="wc 文件统计"></a>wc 文件统计</h3><p>统计文件的行数、字数、字节数</p><blockquote><p><strong>wc命令</strong> 统计指定文件中的字节数、字数、行数，并将统计结果显示输出。利用wc指令我们可以计算文件的Byte数、字数或是列数，若不指定文件名称，或是所给予的文件名为“-”，则wc指令会从标准输入设备读取数据。wc同时也给出所指定文件的总统计数。</p></blockquote><ul><li><code>-c</code>： 统计字节数，或<code>--bytes</code>或<code>--chars</code>：只显示Bytes数</li><li><code>-l</code>：统计行数，或<code>--lines</code>：只显示列数</li><li><code>-m</code>：统计字符数。这个标志不能与 <code>-c</code> 标志一起使用</li><li><code>-w</code>：统计字数，或<code>--words</code>：只显示字数。一个字被定义为由空白、跳格或换行字符分隔的字符串</li><li><code>-L</code>：打印最长行的长度</li></ul><h3 id="uniq-删除文件中的重复内容"><a href="#uniq-删除文件中的重复内容" class="headerlink" title="uniq: 删除文件中的重复内容"></a>uniq: 删除文件中的重复内容</h3><p>显示或忽略重复的行。<code>uniq</code> 命令有点”呆“，只能将连续的重复行变为一行</p><ul><li><code>-c</code>, <code>--count</code>：在每行开头增加重复次数</li><li><code>-d</code>, <code>--repeated</code>：只显示重复行的值</li></ul><h4 id="主要用途-1"><a href="#主要用途-1" class="headerlink" title="主要用途"></a>主要用途</h4><ul><li>将输入文件（或标准输入）中邻近的重复行写入到输出文件（或标准输出）中。</li><li>当没有选项时，邻近的重复行将合并为一个。</li></ul><h3 id="cut-剪切文件的一部分内容"><a href="#cut-剪切文件的一部分内容" class="headerlink" title="cut: 剪切文件的一部分内容"></a>cut: 剪切文件的一部分内容</h3><p>连接文件并打印到标准输出设备上</p><ul><li><code>-c</code>：仅显示行中指定范围的字符，<code>cut -c 2-4 file</code></li><li><code>-d</code>：指定字段的分隔符，默认的字段分隔符为“TAB”。</li><li></li></ul><h2 id="输出重定向"><a href="#输出重定向" class="headerlink" title="输出重定向"></a>输出重定向</h2><blockquote><p>黑洞文件 <code>/dev/null</code>，此文件具有唯一的属性，它总是空的。它能使发送到 <code>/dev/null</code> 的任何数据作废</p></blockquote><h3 id="重定向到文件"><a href="#重定向到文件" class="headerlink" title="&gt; 重定向到文件"></a><code>&gt;</code> 重定向到文件</h3><ul><li>如果此文件不存在，则新建一个文件</li><li>如果此文件已经存在，那就会把文件内容覆盖掉，而且不会征求用户同意</li></ul><h3 id="重定向到文件末尾"><a href="#重定向到文件末尾" class="headerlink" title="&gt;&gt; 重定向到文件末尾"></a><code>&gt;&gt;</code> 重定向到文件末尾</h3><p><code>&gt;&gt;</code>的作用与<code>&gt;</code>是类似的，不过它不会像<code>&gt;</code>那么危险。它会将重定向的内容写入到文件末尾，起到追加的作用。如果文件不存在，则创建文件</p><h3 id="2-、2-重定向到标准错误输出"><a href="#2-、2-重定向到标准错误输出" class="headerlink" title="2&gt; 、2&gt;&gt;重定向到标准错误输出"></a><code>2&gt;</code> 、<code>2&gt;&gt;</code>重定向到标准错误输出</h3><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash"><span class="built_in">cat</span> not_exist_file.csv &gt; results.txt 2&gt; errors.log</span></span><br></pre></td></tr></table></figure><h3 id="2-1-组合符合"><a href="#2-1-组合符合" class="headerlink" title="2&gt;&amp;1 组合符合"></a><code>2&gt;&amp;1</code> 组合符合</h3><ul><li>将标准错误输出重定向到与标准输出相同的地方</li><li>覆盖：<code>cat not_exist_file_csv &gt; results.txt 2&gt;&amp;1</code></li><li>末尾追加：<code>cat not_exist_file_csv &gt;&gt; results.txt 2&gt;&amp;1</code></li></ul><h3 id="stdin、stdout、stderr"><a href="#stdin、stdout、stderr" class="headerlink" title="stdin、stdout、stderr"></a>stdin、stdout、stderr</h3><ul><li>从键盘向终端输入数据，这是标准输入，也就是 stdin</li><li>终端接收键盘输入的命令，会产生两种输出<ul><li>标准输出：stdout。指终端输出的信息（不包括错误信息）</li><li>标准错误输出：stderr。指终端输出的错误信息</li></ul></li></ul><h2 id="输入重定向"><a href="#输入重定向" class="headerlink" title="输入重定向"></a>输入重定向</h2><h3 id=""><a href="#" class="headerlink" title="&lt; 从文件中读取"></a><code>&lt;</code> 从文件中读取</h3><ul><li><p><code>cat notes.csv</code></p><ul><li><code>cat</code> 命令接受的输入是 notes.csv 这个文件名</li><li>那么它要先打开 notes.csv 文件</li><li>然后打印出文件内容</li></ul></li><li><p><code>cat &lt; notes.csv</code></p><ul><li><code>cat</code> 命令接受的输入直接是 notes.csv 这个文件的内容</li><li><code>cat</code> 命令只负责将其内容打印</li><li>打开文件并将文件内容传递给 <code>cat</code> 命令的工作则交给终端完成</li></ul></li></ul><h3 id="-1"><a href="#-1" class="headerlink" title="&lt;&lt; 从键盘读取"></a><code>&lt;&lt;</code> 从键盘读取</h3><ul><li><code>&lt;&lt;</code> 符号的作用是将键盘的输入重定向为某个命令的输入</li><li><code>sort -n &lt;&lt; END</code></li></ul><h2 id="管道符号"><a href="#管道符号" class="headerlink" title="| 管道符号"></a><code>|</code> 管道符号</h2><p>将一个命令的输出作为另一个命令的输入</p><ul><li>传说中的 ”管道符号“：<code>|</code></li><li><code>|</code>符号既然被称为”管道符“，其作用就是”建立命令管道“</li><li>管道也算是重定向流的一种</li><li><code>cut -d , -f 1 notes.csv | sort &gt; sorted_names.txt</code></li><li><code>du -h | sort -n | head</code></li><li><code>sudo grep log -Ir /var/log | cut -d : -f 1 | sort | uniq</code><ul><li><code>-I</code>：排除二进制文件</li><li><code>-r</code>：用于递归遍历</li></ul></li></ul><h2 id="进程和系统监测"><a href="#进程和系统监测" class="headerlink" title="进程和系统监测"></a>进程和系统监测</h2><ul><li><p><code>w</code>：显示目前登入系统的用户信息</p></li><li><p><code>uptime</code>：查看Linux系统负载信息</p><blockquote><p>能够打印系统总共运行了多长时间和系统的平均负载。uptime命令可以显示的信息显示依次为：现在时间、系统已经运行了多长时间、目前有多少登陆用户、系统在过去的1分钟、5分钟和15分钟内的平均负载。</p></blockquote></li><li><p><code>tload</code>：显示系统负载状况</p><blockquote><p>以图形化的方式输出当前系统的平均负载到指定的终端。假设不给予终端机编号，则会在执行tload指令的终端机显示负载情形</p></blockquote></li><li><p><code>who</code>：显示目前登录系统的用户信息</p><blockquote><p>显示目前登录系统的用户信息。执行who命令可得知目前有那些用户登入系统，单独执行who命令会列出登入帐号，使用的终端机，登入时间以及从何处登入或正在使用哪个X显示器。</p></blockquote></li><li><p><code>ps</code>：报告当前系统的进程状态 process status</p><ul><li>输出<ul><li>UID：运行进程的用户</li><li>PID：进程号，process identifier，每个进程有唯一的进程号</li><li>PPID：程序的父进程号，parent process ID</li><li>TTY：进程运行所在的终端</li><li>TIME：进程运行的时间</li><li>CMD：产生这个进程的程序名</li></ul></li><li>实践<ul><li><code>-ef</code>：列出所有用户在所有终端的所有进程，可以配合管道方便查看 <code>ps -ef | less</code></li><li><code>-efH</code>：按照乔木状列出进程</li><li><code>-u</code>：列出此用户运行的进程</li><li><code>-aux</code>：通过CPU和内存使用来过滤进程<ul><li><code>ps -aux --sort -pcpu | less</code>：根据CPU使用率降序排列</li><li><code>ps -aux --sort -pmem | less</code>：根据内存使用率降序排列</li></ul></li><li><code>ps -axjf</code> ，<code>pstree</code>：以树形结构显示进程</li></ul></li></ul><blockquote><p>用于报告当前系统的进程状态。可以搭配<code>kill</code>指令随时中断、删除不必要的程序。ps命令是最基本同时也是非常强大的进程查看命令，使用该命令可以确定有哪些进程正在运行和运行的状态、进程是否结束、进程有没有僵死、哪些进程占用了过多的资源等等，总之大部分信息都是可以通过执行该命令得到的。</p></blockquote></li><li><p><code>top</code>：显示或管理执行中的程序</p><ul><li>q键：退出</li><li>h键：显示帮助文档</li><li>B键：加粗某些信息</li><li>f&#x2F;F键：在进程列表中添加或删除某些列</li><li>u键：依照用户来过滤显示</li><li>k键：结束某个进程</li><li>s键：改变刷新页面的时间，默认地页面每个3秒刷新一次</li></ul></li><li><p><code>kill</code>：根据进程号结束一个进程</p><ul><li><code>-9</code>：强制结束进程</li></ul></li><li><p><code>killall</code>：根据进程名结束所有进程，可以结合<code>find</code></p></li></ul><h3 id="glances"><a href="#glances" class="headerlink" title="glances"></a>glances</h3><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">apt install glances -y</span></span><br></pre></td></tr></table></figure><img src="https://i.loli.net/2020/03/29/mUqi9MRt6vT2HIK.png" style="zoom:25%;" /><h3 id="htop"><a href="#htop" class="headerlink" title="htop"></a>htop</h3><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">apt install htop -y</span></span><br></pre></td></tr></table></figure><img src="https://i.loli.net/2020/03/29/jZbnwPI3BKq4lES.png" style="zoom:25%;" /><h2 id="管理前后台进程"><a href="#管理前后台进程" class="headerlink" title="管理前后台进程"></a>管理前后台进程</h2><img src="https://i.loli.net/2020/03/29/Y15XE8zCdVKDrJP.png" style="zoom:80%;" /><h3 id="符号：后台运行进程"><a href="#符号：后台运行进程" class="headerlink" title="&amp; 符号：后台运行进程"></a><code>&amp;</code> 符号：后台运行进程</h3><ul><li><code>cp file.txt file-copy.txt &amp;</code></li><li><code>find / -name &quot;*log&quot; &gt; output_find 2&gt;&amp;1 &amp;</code></li></ul><h3 id="nohup-使进程与终端分离"><a href="#nohup-使进程与终端分离" class="headerlink" title="nohup: 使进程与终端分离"></a><code>nohup</code>: 使进程与终端分离</h3><p>无论是否将 nohup 命令的输出重定向到终端，输出都将附加到当前目录的 nohup.out 文件中。hangup，挂起</p><p>如果当前目录的 nohup.out 文件不可写，输出重定向到<code>$HOME/nohup.out</code>文件中。如果没有文件能创建或打开以用于追加，那么 command 参数指定的命令不可调用。如果标准错误是一个终端，那么把指定的命令写给标准错误的所有输出作为标准输出重定向到相同的文件描述符。</p><h3 id="Ctrl-Z、bg-进程转为后台运行"><a href="#Ctrl-Z、bg-进程转为后台运行" class="headerlink" title="Ctrl + Z、bg 进程转为后台运行"></a>Ctrl + Z、bg 进程转为后台运行</h3><h3 id="jobs：显示后台进程状态"><a href="#jobs：显示后台进程状态" class="headerlink" title="jobs：显示后台进程状态"></a>jobs：显示后台进程状态</h3><h3 id="fg：使进程转为前台运行"><a href="#fg：使进程转为前台运行" class="headerlink" title="fg：使进程转为前台运行"></a>fg：使进程转为前台运行</h3><h2 id="文件的解压和压缩"><a href="#文件的解压和压缩" class="headerlink" title="文件的解压和压缩"></a>文件的解压和压缩</h2><ul><li><code>tar</code>：Linux下的归档使用工具，用来打包和备份<ul><li><code>-cvf</code>：创建一个 tar 归档，<code>tar -cvf a.tar a/</code><ul><li>c：create 的缩写，表示创建</li><li>v：verbose 的缩写，表示冗余。会显示操作细节</li><li>f：file 的缩写，表示文件，指定归档文件</li></ul></li><li><code>-tf</code>：显示归档里的内容，并不解开归</li><li><code>-xvf</code>：解开归档<ul><li>x：extract 的缩写，表示”提取，取出“</li></ul></li><li><code>-zcvf</code>：归档，然后用 gzip 来压缩归档</li><li><code>-zxvf</code>：解gzip压缩，然后解开归档</li><li><code>-jcvf</code>：归档，然后用 bzip2 来压缩归档</li><li><code>-jxvf</code>：解bzip2压缩，然后解开归档</li></ul></li><li><code>zcat</code>、<code>zmore</code>、<code>zless</code></li><li><code>bzcat</code>、<code>bzmore</code>、<code>bzless</code></li><li><code>unzip</code>、<code>unrar</code>：<code>apt install zip</code></li></ul><h2 id="定时和延时执行"><a href="#定时和延时执行" class="headerlink" title="定时和延时执行"></a>定时和延时执行</h2><h3 id="date：显示或设置系统时间与日期"><a href="#date：显示或设置系统时间与日期" class="headerlink" title="date：显示或设置系统时间与日期"></a>date：显示或设置系统时间与日期</h3><ul><li><code>date +&quot;%H&quot;</code>：11</li><li><code>date +&quot;%H:%M:%S&quot;</code>：11:15:51</li><li><code>date +&quot;%Y-%m-%d&quot;</code>：2020-03-29</li><li><code>date +&quot;%Y-%m-%d %H:%M:%S&quot;</code>：2020-03-29 11:15:51</li><li>修改系统时间：<code>date 10121430</code></li></ul><h3 id="at：在指定时间执行一个任务"><a href="#at：在指定时间执行一个任务" class="headerlink" title="at：在指定时间执行一个任务"></a>at：在指定时间执行一个任务</h3><p><strong>at命令</strong> 用于在指定时间执行命令。<code>at</code>允许使用一套相当复杂的指定时间的方法。它能够接受在当天的<code>hh:mm（小时:分钟）</code>式的时间指定。假如该时间已过去，那么就放在第二天执行。当然也能够使用<code>midnight</code>（深夜），<code>noon</code>（中午），<code>teatime</code>（饮茶时间，一般是下午4点）等比较模糊的词语来指定时间。用户还能够采用12小时计时制，即在时间后面加上AM（上午）或PM（下午）来说明是上午还是下午。 也能够指定命令执行的具体日期，指定格式为<code>month day（月 日）</code>或 <code>mm/dd/yy（月/日/年）</code>或<code>dd.mm.yy（日.月.年）</code>。指定的日期必须跟在指定时间的后面。</p><p>上面介绍的都是绝对计时法，其实还能够使用相对计时法，这对于安排不久就要执行的命令是很有好处的。指定格式为：<code>now + count time-units</code>，<code>now</code>就是当前时间，time-units 是时间单位，这里能够是<code>minutes</code>（分钟）、<code>hours</code>（小时）、<code>days</code>（天）、<code>weeks</code>（星期）。<code>count</code>是时间的数量，究竟是几天，还是几小时，等等。 更有一种计时方法就是直接使用<code>today</code>（今天）、<code>tomorrow</code>（明天）来指定完成命令的时间。</p><ul><li><code>at 22:10</code>，会提示让输入命令，以<code>&lt;EOT&gt;</code>结束输入</li><li><code>at 22:10 tomorrow</code>：明天 22:10 执行</li><li><code>at 22:10 03/30/2020</code>：在 2020年3月20号22:10执行</li><li><code>at now +10 minutes</code>：10分钟之后执行</li></ul><h3 id="atq：列出当前用户的at任务列表"><a href="#atq：列出当前用户的at任务列表" class="headerlink" title="atq：列出当前用户的at任务列表"></a>atq：列出当前用户的at任务列表</h3><ul><li>q是英语 queue 的首字母，表示“队列”</li></ul><p><strong>atq命令</strong> 显示系统中待执行的任务列表，也就是列出当前用户的at任务列表。</p><h3 id="atrm：删除待执行任务队列中的指定任务"><a href="#atrm：删除待执行任务队列中的指定任务" class="headerlink" title="atrm：删除待执行任务队列中的指定任务"></a>atrm：删除待执行任务队列中的指定任务</h3><p><strong>atrm命令</strong> 用于删除待执行任务队列中的指定任务。</p><h3 id="sleep：将目前动作延迟一段时间"><a href="#sleep：将目前动作延迟一段时间" class="headerlink" title="sleep：将目前动作延迟一段时间"></a>sleep：将目前动作延迟一段时间</h3><ul><li><code>touch file.txt;sleep 10;rm file.txt</code></li></ul><h3 id="、-、"><a href="#、-、" class="headerlink" title="&amp;&amp;、||、;"></a><code>&amp;&amp;</code>、<code>||</code>、<code>;</code></h3><ul><li><code>&amp;&amp;</code>：<code>&amp;&amp;</code> 号前的命令执行成功，才会执行后面的命令</li><li><code>||</code>：<code>||</code> 号前的命令执行失败，才会执行后面的命令</li><li><code>;</code>：不论分号前的命令执行成功与否，都执行分号后的命令</li></ul><h3 id="crontab-提交和管理用户的需要周期性执行的任务"><a href="#crontab-提交和管理用户的需要周期性执行的任务" class="headerlink" title="crontab 提交和管理用户的需要周期性执行的任务"></a>crontab 提交和管理用户的需要周期性执行的任务</h3><p><strong>crontab命令</strong> 被用来提交和管理用户的需要周期性执行的任务，与windows下的计划任务类似，当安装完成操作系统后，默认会安装此服务工具，并且会自动启动crond进程，crond进程每分钟会定期检查是否有要执行的任务，如果有要执行的任务，则自动执行该任务。</p><ul><li><code>-e</code>：编辑该用户的计时器设置；</li><li><code> -l</code>：列出该用户的计时器设置；</li><li><code> -r</code>：删除该用户的计时器设置；</li><li><code>-u &lt;用户名称&gt;</code>：指定要设定计时器的用户名称。</li></ul><blockquote><p> m h dom mon dow  command</p><ul><li><p>顺序口诀：分 时 日 月 周</p></li><li><p>m：minute 的缩写，表示”分钟“（0~59）</p></li><li><p>h：hour 的缩写，表示“小时”(0~23)</p></li><li><p>dom：day of month 的缩写，表示“一个月的哪一天”（0~31）</p></li><li><p>mon：month 的缩写，表示”月份“（1~12）</p></li><li><p>dow：day of week 的缩写，表示“星期几”(0~6,星期日是0)</p></li><li><p>command：英语“命令”的意思，表示需要定时执行的命令</p></li></ul></blockquote><p><strong>特殊字符：</strong></p><ul><li>星号（*）：代表所有可能的值，例如month字段如果是星号，则表示在满足其它字段的制约条件后每月都执行该命令操作。</li><li>逗号（,）：可以用逗号隔开的值指定一个列表范围，例如，“1,2,5,7,8,9”</li><li>中杠（-）：可以用整数之间的中杠表示一个整数范围，例如“2-6”表示“2,3,4,5,6”</li><li>正斜线（&#x2F;）：可以用正斜线指定时间的间隔频率，例如“0-23&#x2F;2”表示每两小时执行一次。同时正斜线可以和星号一起使用，例如*&#x2F;10，如果用在minute字段，表示每十分钟执行一次。</li></ul><h4 id="实例"><a href="#实例" class="headerlink" title="实例"></a>实例</h4><blockquote><p>一定要换行，不换行不会生效的<br>重启 cron 服务：<code>systemctl cron.service restart</code></p></blockquote><p><strong>每1分钟执行一次command</strong></p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">* * * * * command</span><br><span class="line"></span><br></pre></td></tr></table></figure><p><strong>每小时的第3和第15分钟执行</strong></p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">3,15 * * * * command</span><br><span class="line"></span><br></pre></td></tr></table></figure><p><strong>在上午8点到11点的第3和第15分钟执行</strong></p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">3,15 8-11 * * * command</span><br><span class="line"></span><br></pre></td></tr></table></figure><p><strong>每隔两天的上午8点到11点的第3和第15分钟执行</strong></p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">3,15 8-11 */2 * * command</span><br><span class="line"></span><br></pre></td></tr></table></figure><p><strong>每个星期一的上午8点到11点的第3和第15分钟执行</strong></p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">3,15 8-11 * * 1 command</span><br><span class="line"></span><br></pre></td></tr></table></figure><p><strong>每晚的21:30重启smb</strong></p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">30 21 * * * /etc/init.d/smb restart</span><br><span class="line"></span><br></pre></td></tr></table></figure><p><strong>每月1、10、22日的4 : 45重启smb</strong></p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">45 4 1,10,22 * * /etc/init.d/smb restart</span><br><span class="line"></span><br></pre></td></tr></table></figure><p><strong>每周六、周日的1:10重启smb</strong></p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">10 1 * * 6,0 /etc/init.d/smb restart</span><br><span class="line"></span><br></pre></td></tr></table></figure><p><strong>每天18 : 00至23 : 00之间每隔30分钟重启smb</strong></p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">*/30 18-23 * * * /etc/init.d/smb restart</span><br><span class="line"></span><br></pre></td></tr></table></figure><p><strong>每星期六的晚上11:00 pm重启smb</strong></p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">0 23 * * 6 /etc/init.d/smb restart</span><br><span class="line"></span><br></pre></td></tr></table></figure><p><strong>每一小时重启smb</strong></p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">* */1 * * * /etc/init.d/smb restart</span><br><span class="line"></span><br></pre></td></tr></table></figure><p><strong>晚上11点到早上7点之间，每隔一小时重启smb</strong></p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">* 23-7/1 * * * /etc/init.d/smb restart</span><br><span class="line"></span><br></pre></td></tr></table></figure><p><strong>每月的4号与每周一到周三的11点重启smb</strong></p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">0 11 4 * mon-wed /etc/init.d/smb restart</span><br><span class="line"></span><br></pre></td></tr></table></figure><p><strong>一月一号的4点重启smb</strong></p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">0 4 1 jan * /etc/init.d/smb restart</span><br><span class="line"></span><br></pre></td></tr></table></figure><p><strong>每小时执行<code>/etc/cron.hourly</code>目录内的脚本</strong></p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">01 * * * * root run-parts /etc/cron.hourly</span><br><span class="line"></span><br></pre></td></tr></table></figure><h2 id="SSH"><a href="#SSH" class="headerlink" title="SSH"></a>SSH</h2><ul><li>全局 SSH 客户端的配置：<code>/etc/ssh/ssh_config</code></li><li>用户 SSH 客户端的配置：<code>~/.ssh/config</code><ul><li>如果 <code>~/.ssh</code> 不存在，执行 <code>ssh localhost</code></li><li><code>nano config</code> 进行编辑</li></ul></li><li>SSH 服务端的配置：<code>/etc/ssh/sshd_config</code></li></ul><h3 id="服务端-config-文件的常用配置参数"><a href="#服务端-config-文件的常用配置参数" class="headerlink" title="服务端 config 文件的常用配置参数"></a>服务端 config 文件的常用配置参数</h3><ul><li>Port：sshd 服务端口号（默认是22）</li><li>PermitRootLogin：是否允许以 root 用户身份登录（默认是可以）</li><li>PasswordAuthentication：是否允许密码验证登录（默认是可以）</li><li>PubkeyAuthentication：是否允许公钥验证登录（默认是可以）</li><li>PermitEmptyPasswords：是否允许空密码登录（不安全。默认不可以）</li></ul><h3 id="客户端-config-文件的配置"><a href="#客户端-config-文件的配置" class="headerlink" title="客户端 config 文件的配置"></a>客户端 config 文件的配置</h3><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">nano ~/.ssh/config</span></span><br></pre></td></tr></table></figure><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">Host aliyun</span><br><span class="line">  HostName 47.98.152.68</span><br><span class="line">  Port 22</span><br><span class="line">  User root</span><br></pre></td></tr></table></figure><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">ssh aliyun</span></span><br></pre></td></tr></table></figure><h3 id="配置免密码登录"><a href="#配置免密码登录" class="headerlink" title="配置免密码登录"></a>配置免密码登录</h3><ol><li><p>在客户机中生成密钥对（公钥和私钥）</p><blockquote><p><code>ssh-keygen -t rsa -C &quot;youngjuning@aliyun.com&quot;</code></p></blockquote><ul><li><p>在 <code>~/.ssh/</code> 目录下，会新生成两个文件</p><ul><li>id_rsa：私钥</li><li>id_rsa.pub：公钥</li></ul></li></ul></li><li><p><code>ssh-copy-id</code>：把本地的 ssh 公钥文件安装到远程主机对应的账户下</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">ssh-copy-id -i ~/.ssh/id_rsa.pub root@47.98.152.68</span></span><br></pre></td></tr></table></figure><blockquote><p><strong>ssh-copy-id命令</strong> 可以把本地主机的公钥复制到远程主机的<code>authorized_keys</code>文件上，<code>ssh-copy-id</code> 命令也会给远程主机的用户主目录（home）和<code>~/.ssh</code>, 和<code>~/.ssh/authorized_keys</code>设置合适的权限。</p><ul><li><code>-i</code>：指定公钥文件</li></ul></blockquote></li></ol><p><strong>设置免密码后仍想使用密码登录：</strong></p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">ssh -o PreferredAuthentications=password -o PubkeyAuthentication=no root@47.98.152.68</span></span><br></pre></td></tr></table></figure><p><strong>测试Github是否正确配置免密码登录：</strong></p><p><code>ssh -T git@github.com</code></p><h2 id="Linux-网络"><a href="#Linux-网络" class="headerlink" title="Linux 网络"></a>Linux 网络</h2><h3 id="wget：Linux系统下载文件工具"><a href="#wget：Linux系统下载文件工具" class="headerlink" title="wget：Linux系统下载文件工具"></a>wget：Linux系统下载文件工具</h3><p><strong>wget命令</strong> 用来从指定的URL下载文件。wget非常稳定，它在带宽很窄的情况下和不稳定网络中有很强的适应性，如果是由于网络的原因下载失败，wget会不断的尝试，直到整个文件下载完毕。如果是服务器打断下载过程，它会再次联到服务器上从停止的地方继续下载。这对从那些限定了链接时间的服务器上下载大文件非常有用。</p><p>wget支持HTTP，HTTPS和FTP协议，可以使用HTTP代理。所谓的自动下载是指，wget可以在用户退出系统的之后在后台执行。这意味这你可以登录系统，启动一个wget下载任务，然后退出系统，wget将在后台执行直到任务完成，相对于其它大部分浏览器在下载大量数据时需要用户一直的参与，这省去了极大的麻烦。</p><p>用于从网络上下载资源，没有指定目录，下载资源回默认为当前目录。wget虽然功能强大，但是使用起来还是比较简单：</p><ol><li><strong>支持断点下传功能</strong> 这一点，也是网络蚂蚁和FlashGet当年最大的卖点，现在，Wget也可以使用此功能，那些网络不是太好的用户可以放心了；</li><li><strong>同时支持FTP和HTTP下载方式</strong> 尽管现在大部分软件可以使用HTTP方式下载，但是，有些时候，仍然需要使用FTP方式下载软件；</li><li><strong>支持代理服务器</strong> 对安全强度很高的系统而言，一般不会将自己的系统直接暴露在互联网上，所以，支持代理是下载软件必须有的功能；</li><li><strong>设置方便简单</strong> 可能，习惯图形界面的用户已经不是太习惯命令行了，但是，命令行在设置上其实有更多的优点，最少，鼠标可以少点很多次，也不要担心是否错点鼠标；</li><li><strong>程序小，完全免费</strong> 程序小可以考虑不计，因为现在的硬盘实在太大了；完全免费就不得不考虑了，即使网络上有很多所谓的免费软件，但是，这些软件的广告却不是我们喜欢的。</li></ol><h3 id="scp：加密的方式在本地主机和远程主机之间复制文件"><a href="#scp：加密的方式在本地主机和远程主机之间复制文件" class="headerlink" title="scp：加密的方式在本地主机和远程主机之间复制文件"></a>scp：加密的方式在本地主机和远程主机之间复制文件</h3><p><strong>scp命令</strong> 用于在Linux下进行远程拷贝文件的命令，和它类似的命令有cp，不过cp只是在本机进行拷贝不能跨服务器，而且scp传输是加密的。可能会稍微影响一下速度。当你服务器硬盘变为只读read only system时，用scp可以帮你把文件移出来。另外，scp还非常不占资源，不会提高多少系统负荷，在这一点上，rsync就远远不及它了。虽然 rsync比scp会快一点，但当小文件众多的情况下，rsync会导致硬盘I&#x2F;O非常高，而scp基本不影响系统正常使用。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ scp file.txt root@192.168.2.195</span><br></pre></td></tr></table></figure><h3 id="netstat-ntulp-grep-9001"><a href="#netstat-ntulp-grep-9001" class="headerlink" title="netstat -ntulp | grep 9001"></a>netstat -ntulp | grep 9001</h3><p>查看端口占用情况</p><h3 id="rsync：远程数据同步工具"><a href="#rsync：远程数据同步工具" class="headerlink" title="rsync：远程数据同步工具"></a>rsync：远程数据同步工具</h3><p><strong>rsync命令</strong> 是一个远程数据同步工具，可通过LAN&#x2F;WAN快速同步多台主机间的文件。rsync使用所谓的“rsync算法”来使本地和远程两个主机之间的文件达到同步，这个算法只传送两个文件的不同部分，而不是每次都整份传送，因此速度相当快。 rsync是一个功能非常强大的工具，其命令也有很多功能特色选项，我们下面就对它的选项一一进行分析说明。</p><ul><li>默认地，rsync 在同步时并不会删除目标目录的文件。例如，你的源目录（被同步目录）中删除了一个文件，但是用 rsync 同步时，它并不会删除同步目录中的相同文件。<code>rsync -arv --delete</code> 可以删除</li></ul><p><strong>用 rsync 备份到同一台电脑的其他目录：</strong></p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">rsync -arv Images/ backups</span></span><br></pre></td></tr></table></figure><ul><li><code>-a</code>：保留文件的所有信息，包括权限，修改日期，等等。a 是 archive 的缩写，是<strong>归档</strong>的意思</li><li><code>-r</code>：递归调用。表示子目录的所有文件也都包括。r 是 recursive 的缩写，是<strong>递归</strong>的意思</li><li><code>-v</code> ：冗余模式。输出详细操作信息。v 是 verbose 的缩写，是<strong>冗余的</strong>意思</li></ul><p><strong>用 rsync 备份到同步本地文件到服务器：</strong></p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">rsync -arvz --progress --delete ~/Desktop/blog root@47.98.152.68:/home/yangjunning/</span></span><br></pre></td></tr></table></figure><h2 id="备份到七牛云"><a href="#备份到七牛云" class="headerlink" title="备份到七牛云"></a>备份到七牛云</h2><h3 id="安装命令行工具-qshell"><a href="#安装命令行工具-qshell" class="headerlink" title="安装命令行工具(qshell)"></a>安装命令行工具(qshell)</h3><ol><li>进入家目录：<code>cd ~</code></li><li>下载压缩包：<code>wget http://devtools.qiniu.com/qshell-linux-x86-v2.4.1.zip</code></li><li>解压压缩包：<code>unzip ~/qshell-linux-x86-v2.4.1.zip</code></li><li>任何位置运行：<code>mv ~/qshell-linux-x86-v2.4.1 /usr/local/bin/qshell</code></li><li>权限：<code>chmod +x /usr/local/bin/qshell</code></li><li>删除压缩包：<code>rm -rf ~/qshell-linux-x86-v2.4.1.zip</code></li></ol><h3 id="密钥设置"><a href="#密钥设置" class="headerlink" title="密钥设置"></a>密钥设置</h3><p>需要鉴权的命令都需要依赖七牛账号下的 <code>AccessKey</code> 和 <code>SecretKey</code>。所以这类命令运行之前，需要使用 <code>account</code> 命令来设置下 <code>AccessKey</code> ，<code>SecretKey</code> 。</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ qshell account -- ak sk name</span><br></pre></td></tr></table></figure><blockquote><p>注意：<code>ak</code>、<code>sk</code> 在七牛云<strong>控制台</strong> -&gt; <strong>个人中心</strong> - &gt; <strong>密钥管理</strong>内。</p></blockquote><p>可以连续使用 <code>qshell account</code> 添加账号<code>ak</code>, <code>sk</code>, <code>name</code>信息，qshell会保存这些账号的信息， 可以使用<code>qshell user</code>命令列举账号信息，在各个账号之间切换, 删除账号等</p><h3 id="账户管理"><a href="#账户管理" class="headerlink" title="账户管理"></a>账户管理</h3><p>使用qshell user子命令可以用来管理记录的多账户信息。</p><ol><li><code>qshell user ls</code>可以列举账户下所有的账户信息</li><li><code>qshell user cu</code> 可以用来切换账户</li><li><code>qshell user cu</code> 不携带的话会切换到最近的上个账户；比如我在A账户做完操作后，使用<code>qshell user cu B</code>到了B 账户，那么使用<code>qshell user cu</code>可以切回到A账户</li></ol><h3 id="qupload2"><a href="#qupload2" class="headerlink" title="qupload2"></a>qupload2</h3><p>同步数据到七牛空间， 带同步进度信息，和数据上传完整性检查（命令式），详情请查看<a href="http://github.com/qiniu/qshell/blob/master/docs/qupload2.md">文档</a></p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ qshell qupload2 --src-dir=/root --bucket=aliyun-server-backup</span><br></pre></td></tr></table></figure>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/ef588191ce47/</id>
    <link href="https://blog.zisheng.pro/posts/ef588191ce47/"/>
    <published>2020-03-28T12:39:42.000Z</published>
    <summary>本文主要讲解了写给前端的 Linux 实战教程，简单易懂，又有实战操作，对你有一定的参考价值，需要的朋友可以学习下。</summary>
    <title>写给前端的 Linux 实战教程</title>
    <updated>2026-09-18T08:15:28.636Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="前端工程化" scheme="https://blog.zisheng.pro/tags/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/"/>
    <category term="规范" scheme="https://blog.zisheng.pro/tags/%E8%A7%84%E8%8C%83/"/>
    <category term="前端规范化" scheme="https://blog.zisheng.pro/tags/%E5%89%8D%E7%AB%AF%E8%A7%84%E8%8C%83%E5%8C%96/"/>
    <category term="eslint" scheme="https://blog.zisheng.pro/tags/eslint/"/>
    <category term="editorconfig" scheme="https://blog.zisheng.pro/tags/editorconfig/"/>
    <category term="npm script" scheme="https://blog.zisheng.pro/tags/npm-script/"/>
    <category term="husky" scheme="https://blog.zisheng.pro/tags/husky/"/>
    <category term="lint-staged" scheme="https://blog.zisheng.pro/tags/lint-staged/"/>
    <category term="prettier" scheme="https://blog.zisheng.pro/tags/prettier/"/>
    <category term="standard-version" scheme="https://blog.zisheng.pro/tags/standard-version/"/>
    <category term="Conventional Commits" scheme="https://blog.zisheng.pro/tags/Conventional-Commits/"/>
    <content>
      <![CDATA[<p>本文以 React Native 项目为示例介绍了前端规范化开发涉及的工具及知识，但是所涉及内容具有通用性，建议读者点赞收藏(^▽^)。</p><blockquote><p>另外安利一下 <a href="https://github.com/sigmayun/react-native-template-typescript">@sigmayun&#x2F;react-native-template-typescript</a>，这是一个基于 typescript 的 react native 开箱即用的模板。本文所涉及的大部分内容都集成到了该模板中。</p><ul><li><a href="https://github.com/sishuguojixuefu/eslint-config">sishuguojixuefu&#x2F;eslint-config</a>：ESLint config for sishuguojixuefu</li><li><a href="https://github.com/youngjuning/conventional-commits-demo">conventional-commits-demo</a>：本文实践的配套代码库</li></ul></blockquote><h2 id="gitconfig"><a href="#gitconfig" class="headerlink" title="gitconfig"></a>gitconfig</h2><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 初始化设置</span></span><br><span class="line">$ git config --global user.name <span class="string">&#x27;your_name&#x27;</span></span><br><span class="line">$ git config --global user.email <span class="string">&#x27;your_email@aliyun.com&#x27;</span></span><br><span class="line"><span class="comment"># 提高命令输出的可读性</span></span><br><span class="line">$ git config --global color.ui auto</span><br><span class="line"><span class="comment"># git 记住用户名和密码</span></span><br><span class="line">$ git config --global credential.helper store</span><br><span class="line"><span class="comment"># core.autocrlf</span></span><br><span class="line">$ git config --global core.autocrlf input</span><br></pre></td></tr></table></figure><p>以上配置内容可以在 <code>~/.gitconfig</code> 文件中找到。</p><h2 id="npm-scripts"><a href="#npm-scripts" class="headerlink" title="npm scripts"></a>npm scripts</h2><p><code>package.json</code> 文件的 scripts 字段是定义可执行脚本用的，可以利用它来实现简单的工作流。如果不是大型的项目，不建议使用 gulp，npm scripts 一样可以完成工作流设计的任务。下面是 React Native 项目默认的 scripts，如今我们可以把使用和扩展 scripts 看做是前端基本功。</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><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;android&quot;</span><span class="punctuation">:</span> <span class="string">&quot;react-native run-android&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;ios&quot;</span><span class="punctuation">:</span> <span class="string">&quot;react-native run-ios&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;react-native start&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;test&quot;</span><span class="punctuation">:</span> <span class="string">&quot;jest&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;eslint .&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><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"># EditorConfig is awesome: http://EditorConfig.org</span><br><span class="line"></span><br><span class="line"># top-most EditorConfig file</span><br><span class="line">root = true</span><br><span class="line"></span><br><span class="line"># Unix-style newlines with a newline ending every file</span><br><span class="line">[*]</span><br><span class="line">indent_style = space</span><br><span class="line">indent_size = 2</span><br><span class="line">end_of_line = lf</span><br><span class="line">charset = utf-8</span><br><span class="line">trim_trailing_whitespace = true</span><br><span class="line">insert_final_newline = true</span><br><span class="line"></span><br><span class="line">[*.md]</span><br><span class="line">trim_trailing_whitespace = false</span><br><span class="line"></span><br><span class="line">[*.gradle]</span><br><span class="line">indent_size = 4</span><br><span class="line"></span><br><span class="line">[BUCK]</span><br><span class="line">indent_size = 4</span><br></pre></td></tr></table></figure><h2 id="使用ESLint规范代码"><a href="#使用ESLint规范代码" class="headerlink" title="使用ESLint规范代码"></a>使用ESLint规范代码</h2><blockquote><p>作者自己团队的 ESLint 方案是基于 eslint-config-airbnb 扩展的，有兴趣可以体验一下 <a href="https://github.com/sishuguojixuefu/eslint-config">@sishuguojixuefu&#x2F;eslint-config</a></p></blockquote><h3 id="安装依赖"><a href="#安装依赖" class="headerlink" title="安装依赖"></a>安装依赖</h3><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 安装 eslint-config-airbnb 及其 peerDependencies</span></span><br><span class="line">$ yarn global add install-peerdeps</span><br><span class="line">$ install-peerdeps -D eslint-config-airbnb</span><br><span class="line"><span class="comment"># 支持 react-native</span></span><br><span class="line">$ yarn add -D eslint-plugin-react-native</span><br><span class="line"><span class="comment"># 支持 typescript</span></span><br><span class="line">$ yarn add -D @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint-import-resolver-typescript typescript</span><br><span class="line"><span class="comment"># 支持 prettier</span></span><br><span class="line">$ yarn add -D prettier eslint-plugin-prettier eslint-config-prettier</span><br><span class="line"><span class="comment"># 其他</span></span><br><span class="line">$ yarn add -D eslint-plugin-promise eslint-plugin-you-dont-need-lodash-underscore eslint-plugin-you-dont-need-momentjs</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="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">   <span class="attr">&quot;eslintConfig&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;extends&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">      <span class="string">&quot;airbnb&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="string">&quot;airbnb/hooks&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="string">&quot;plugin:react-native/all&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="string">&quot;plugin:@typescript-eslint/recommended&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="string">&quot;plugin:promise/recommended&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="string">&quot;plugin:you-dont-need-lodash-underscore/compatible&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="string">&quot;plugin:you-dont-need-momentjs/recommended&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="string">&quot;plugin:prettier/recommended&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="string">&quot;prettier&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="string">&quot;prettier/react&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="string">&quot;prettier/@typescript-eslint&quot;</span></span><br><span class="line">    <span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;env&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">      <span class="attr">&quot;react-native/react-native&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;parser&quot;</span><span class="punctuation">:</span> <span class="string">&quot;@typescript-eslint/parser&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;parserOptions&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">      <span class="attr">&quot;ecmaFeatures&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">        <span class="attr">&quot;jsx&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><br><span class="line">    <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;settings&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">      <span class="attr">&quot;import/extensions&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span><span class="string">&quot;.js&quot;</span><span class="punctuation">,</span> <span class="string">&quot;.ts&quot;</span><span class="punctuation">,</span> <span class="string">&quot;.tsx&quot;</span><span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;import/resolver&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</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;alwaysTryTypes&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><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><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><h3 id="prettierrc-js"><a href="#prettierrc-js" class="headerlink" title=".prettierrc.js"></a>.prettierrc.js</h3><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br></pre></td><td class="code"><pre><span class="line"><span class="variable language_">module</span>.<span class="property">exports</span> = &#123;</span><br><span class="line">  <span class="attr">printWidth</span>: <span class="number">150</span>, <span class="comment">// 一行的字符数，如果超过会进行换行，默认为80</span></span><br><span class="line">  <span class="attr">tabWidth</span>: <span class="number">2</span>, <span class="comment">// tab缩进大小,默认为2</span></span><br><span class="line">  <span class="attr">useTabs</span>: <span class="literal">false</span>, <span class="comment">// 使用tab缩进，默认false</span></span><br><span class="line">  <span class="attr">semi</span>: <span class="literal">false</span>, <span class="comment">// 使用分号, 默认true</span></span><br><span class="line">  <span class="comment">/**</span></span><br><span class="line"><span class="comment">   * 行尾逗号,默认none,可选 none|es5|all</span></span><br><span class="line"><span class="comment">   * es5 包括es5中的数组、对象</span></span><br><span class="line"><span class="comment">   * all 包括函数对象等所有可选</span></span><br><span class="line"><span class="comment">   */</span></span><br><span class="line">  <span class="attr">trailingComma</span>: <span class="string">&#x27;es5&#x27;</span>,</span><br><span class="line">  <span class="attr">singleQuote</span>: <span class="literal">true</span>, <span class="comment">// 使用单引号, 默认false(在jsx中配置无效, 默认都是双引号)</span></span><br><span class="line">  <span class="comment">/**</span></span><br><span class="line"><span class="comment">   * 对象中的空格 默认true</span></span><br><span class="line"><span class="comment">   * true: &#123; foo: bar &#125;</span></span><br><span class="line"><span class="comment">   * false: &#123;foo: bar&#125;</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">/**</span></span><br><span class="line"><span class="comment">   * JSX标签闭合位置 默认false</span></span><br><span class="line"><span class="comment">   * false:</span></span><br><span class="line"><span class="comment">   * &lt;div</span></span><br><span class="line"><span class="comment">   *  className=&quot;&quot;</span></span><br><span class="line"><span class="comment">   *  style=&#123;&#123;&#125;&#125;</span></span><br><span class="line"><span class="comment">   * &gt;</span></span><br><span class="line"><span class="comment">   * true:</span></span><br><span class="line"><span class="comment">   * &lt;div</span></span><br><span class="line"><span class="comment">   *  className=&quot;&quot;</span></span><br><span class="line"><span class="comment">   * style=&#123;&#123;&#125;&#125; &gt;</span></span><br><span class="line"><span class="comment">   */</span></span><br><span class="line">  <span class="attr">jsxBracketSameLine</span>: <span class="literal">false</span>,</span><br><span class="line">  <span class="comment">/**</span></span><br><span class="line"><span class="comment">   * 箭头函数参数括号 默认avoid 可选 avoid| always</span></span><br><span class="line"><span class="comment">   * avoid 能省略括号的时候就省略 例如x =&gt; x</span></span><br><span class="line"><span class="comment">   * always 总是有括号</span></span><br><span class="line"><span class="comment">   */</span></span><br><span class="line">  <span class="attr">arrowParens</span>: <span class="string">&#x27;avoid&#x27;</span>,</span><br><span class="line">  <span class="attr">vueIndentScriptAndStyle</span>: <span class="literal">false</span>, <span class="comment">// vue 文件 script 和 style 标签缩进，默认false</span></span><br><span class="line">  <span class="attr">endOfLine</span>: <span class="string">&#x27;lf&#x27;</span>, <span class="comment">// 强制使用 unix 风格的换行符</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="插件介绍"><a href="#插件介绍" class="headerlink" title="插件介绍"></a>插件介绍</h3><ul><li>eslint-config-airbnb: 该软件包提供 Airbnb 的 .eslintrc 作为可扩展的共享配置</li><li>eslint-plugin-react-native: 针对 ESLint 的 React Native 特定 linting 规则</li><li>@typescript-eslint&#x2F;parser: 将 TypeScript 转换为 ESTree，使 eslint 可以识别</li><li>@typescript-eslint&#x2F;eslint-plugin: 一个包含一堆特定于 TypeScript 的 ESLint 规则的插件</li><li>eslint-import-resolver-typescript: 给 eslint-plugin-import 添加 typescript 支持的插件</li><li>eslint-plugin-prettier: 以 ESLint 插件的形式运行 prettier</li><li>eslint-config-prettier: 关闭所有不必要或可能与 prettier 的规则冲突的 ESLint 规则。一定要放到最后。</li></ul><h3 id="vscode-eslint-plugin-config"><a href="#vscode-eslint-plugin-config" class="headerlink" title="vscode eslint plugin config"></a>vscode eslint plugin config</h3><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><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.eslint&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;eslint.lintTask.enable&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;eslint.packageManager&quot;</span><span class="punctuation">:</span> <span class="string">&quot;yarn&quot;</span><span class="punctuation">,</span></span><br><span class="line"><span class="attr">&quot;eslint.alwaysShowStatus&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;eslint.validate&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">  <span class="string">&quot;javascript&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="string">&quot;javascriptreact&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="string">&quot;typescript&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="string">&quot;typescriptreact&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="string">&quot;vue&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="string">&quot;html&quot;</span></span><br><span class="line"><span class="punctuation">]</span></span><br></pre></td></tr></table></figure><h2 id="用-husky-和-lint-staged-构建超溜的代码检查工作流"><a href="#用-husky-和-lint-staged-构建超溜的代码检查工作流" class="headerlink" title="用 husky 和 lint-staged 构建超溜的代码检查工作流"></a>用 husky 和 lint-staged 构建超溜的代码检查工作流</h2><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ yarn add -D husky</span><br></pre></td></tr></table></figure><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;husky&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;hooks&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">      <span class="attr">&quot;pre-commit&quot;</span><span class="punctuation">:</span> <span class="string">&quot;tsc --noEmit &amp;&amp; lint-staged&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="attr">&quot;lint-staged&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;**/*.&#123;js,jsx,ts,tsx&#125;&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">      <span class="string">&quot;eslint --fix&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="string">&quot;git add&quot;</span></span><br><span class="line">    <span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;**/*.&#123;md,json&#125;&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">      <span class="string">&quot;prettier --write&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="string">&quot;git add&quot;</span></span><br><span class="line">    <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><h2 id="Conventional-Commits-约定式提交规范"><a href="#Conventional-Commits-约定式提交规范" class="headerlink" title="Conventional Commits 约定式提交规范"></a>Conventional Commits 约定式提交规范</h2><p><a href="https://www.conventionalcommits.org/zh-hans/v1.0.0-beta.4/">Conventional Commits</a> 是一种用于给提交信息增加人机可读含义的规范。约定式提交规范是一种基于消息的轻量级约定。它提供了一组用于创建清晰的提交历史的简单规则；这使得编写基于规范的自动化工具变得更容易。这个约定与 <a href="http://semver.org/">SemVer</a> 相吻合，在提交信息中描述新特性、bug 修复和破坏性变更。</p><p>提交说明的结构如下所示：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">&lt;类型&gt;([可选的作用域]): &lt;描述&gt;</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><h3 id="类型"><a href="#类型" class="headerlink" title="类型"></a>类型</h3><ul><li><code>build:</code>: 影响构建系统或外部依赖关系的更改（示例范围：gulp、broccoli、NPM）。</li><li><code>chore:</code>:  其他不修改<code>src</code>或<code>test</code>文件。</li><li><code>ci:</code>: 更改持续集成文件和脚本（示例范围：Travis、Circle、BrowserStack、SauceLabs）。</li><li><code>docs:</code>: 只是更改文档。</li><li><code>feat:</code>:  类型为 <code>feat</code> 的提交表示在代码库中新增了一个功能（这和语义化版本中的 <a href="https://semver.org/lang/zh-CN/#%E6%91%98%E8%A6%81"><code>MINOR</code></a> 相对应）。</li><li><code>fix:</code>：类型为 <code>fix</code> 的 提交表示在代码库中修复了一个 bug （这和语义化版本中的 <a href="https://semver.org/lang/zh-CN/#%E6%91%98%E8%A6%81"><code>PATCH</code></a> 相对应）。</li><li><code>improvement:</code>: 用于对当前实现进行改进而没有添加新功能或修复错误的提交。</li><li><code>perf:</code>: 改进性能的代码更改。</li><li><code>refactor:</code>: 代码重构，既不修复错误也不添加功能。</li><li><code>revert:</code>: commit 回退。</li><li><code>style:</code>: 不影响代码含义的变化（空白、格式化、缺少分号等）。</li><li><code>test:</code>: 添加确实测试或更正现有的测试。</li></ul><h3 id="范围"><a href="#范围" class="headerlink" title="范围"></a>范围</h3><p>可以为提交类型添加一个围在圆括号内的作用域，以为其提供额外的上下文信息。例如 <code>feat(parser): adds ability to parse arrays.</code>。</p><h3 id="BREAKING-CHANGE"><a href="#BREAKING-CHANGE" class="headerlink" title="BREAKING CHANGE"></a>BREAKING CHANGE</h3><p>在可选的正文或脚注的起始位置带有 <code>BREAKING CHANGE:</code> 的提交，表示引入了破坏性 API 变更（这和语义化版本中的 <a href="https://semver.org/lang/zh-CN/#%E6%91%98%E8%A6%81"><code>MAJOR</code></a> 相对应）。 破坏性变更可以是任意 <em>类型</em> 提交的一部分。</p><h3 id="示例"><a href="#示例" class="headerlink" title="示例"></a>示例</h3><h4 id="包含了描述以及正文内有破坏性变更的提交说明"><a href="#包含了描述以及正文内有破坏性变更的提交说明" class="headerlink" title="包含了描述以及正文内有破坏性变更的提交说明"></a>包含了描述以及正文内有破坏性变更的提交说明</h4><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">feat: allow provided config object to extend other configs</span><br><span class="line"></span><br><span class="line">BREAKING CHANGE: `extends` key in config file is now used for extending other config files</span><br></pre></td></tr></table></figure><h4 id="包含了可选的-字符以提醒注意破坏性变更的提交说明"><a href="#包含了可选的-字符以提醒注意破坏性变更的提交说明" class="headerlink" title="包含了可选的 ! 字符以提醒注意破坏性变更的提交说明"></a>包含了可选的 <code>!</code> 字符以提醒注意破坏性变更的提交说明</h4><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">chore!: drop Node 6 from testing matrix</span><br><span class="line"></span><br><span class="line">BREAKING CHANGE: dropping Node 6 which hits end of life in April</span><br></pre></td></tr></table></figure><h4 id="不包含正文的提交说明"><a href="#不包含正文的提交说明" class="headerlink" title="不包含正文的提交说明"></a>不包含正文的提交说明</h4><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docs: correct spelling of CHANGELOG</span><br></pre></td></tr></table></figure><h4 id="包含作用域的提交说明"><a href="#包含作用域的提交说明" class="headerlink" title="包含作用域的提交说明"></a>包含作用域的提交说明</h4><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">feat(lang): add polish language</span><br></pre></td></tr></table></figure><h4 id="为-fix-编写的提交说明，包含（可选的）-issue-编号"><a href="#为-fix-编写的提交说明，包含（可选的）-issue-编号" class="headerlink" title="为 fix 编写的提交说明，包含（可选的） issue 编号"></a>为 fix 编写的提交说明，包含（可选的） issue 编号</h4><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">fix: correct minor typos in code</span><br><span class="line"></span><br><span class="line">see the issue for details on the typos fixed</span><br><span class="line"></span><br><span class="line">closes issue #12</span><br></pre></td></tr></table></figure><h3 id="约定式提交规范"><a href="#约定式提交规范" class="headerlink" title="约定式提交规范"></a>约定式提交规范</h3><ol><li>每个提交都<strong>必须</strong>使用类型字段前缀，它由一个名词组成，诸如<code>feat</code>或<code>fix</code>，其后接一个<strong>可选的</strong>作用域字段，以及一个<strong>必要的</strong>冒号（英文半角）和空格。</li><li>当一个提交为应用或类库实现了新特性时，<strong>必须</strong>使用<code>feat</code>类型。</li><li>当一个提交为应用修复 bug 时，<strong>必须</strong>使用<code>fix</code>类型。</li><li>作用域字段可以跟随在类型字段后面。作用有<strong>必须</strong>是一个描述某部分代码的名词，并用圆括号包围，例如：<code>fix(parser): </code></li><li>描述字段<strong>必须</strong>紧接在类型&#x2F;作用域前缀的空格之后。描述指的是对代码变更的简短总结，例如：<code>fix:array parsing issue when multiplejspaces were contained in string</code>。</li><li>在简短描述之后，<strong>可以</strong>编写更长的提交正文，为代码变更提供额外的上下文信息。正文<strong>必须</strong>起始于描述字段结束的一个空行后。</li><li>在正文结束的一个空行之后，<strong>可以</strong>编写一行或或多行脚注。脚注<strong>必须</strong>包含关于提交的元信息，例如：关联的合并请求、Reviewer、破坏性变更、每条元信息一行。</li><li>破坏性变更<strong>必须</strong>标示在正文区域最开始处，或脚注区域中某一行的开始。一个破坏性变更<strong>必须</strong>包含大写的文本<code>BREAKING CHANGE</code>，后面紧跟冒号和空格。</li><li>在<code>BREAKING CHANGE:</code>之后<strong>必须</strong>提供描述，以描述对 API 的变更。例如：<code>BREAKING CHANGE: environment variables now take precedence over config files</code>。</li><li>在提交说明中，<strong>可以</strong>使用<code>feat</code>和<code>fix</code>之外的类型。</li><li>工具的实现<strong>必须不</strong>区分大小写地解析构成约定式提交的信息单元，只有<code>BREAKING CHANGE</code>  <strong>必须</strong>是大写的。</li><li><strong>可以</strong>在类型&#x2F;作用域前缀之后，<code>:</code>之前，附加<code>!</code>字符，以进一步提醒注意破坏性变更。当有<code>!</code>前缀时，正文或脚注内必须包含<code>BREAKING CHANGE: description</code></li></ol><h3 id="为什么使用约定式提交"><a href="#为什么使用约定式提交" class="headerlink" title="为什么使用约定式提交"></a>为什么使用约定式提交</h3><ul><li>自动化生产 CHANGELOG。</li><li>基于提交的类型，自动决定语义化的版本变更。</li><li>向同事、公众与其他利益关系者传达变化的性质。</li><li>触发构建和部署流程。</li><li>让人们探索一个更加结构化的提交历史，以便降低对你的项目作出贡献的难度。</li></ul><h2 id="commitlint"><a href="#commitlint" class="headerlink" title="commitlint"></a>commitlint</h2><p>commitlint检查您的提交消息是否符合<a href="https://conventionalcommits.org/">conventional commit format</a>。</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">yarn add -D @commitlint/cli @commitlint/config-conventional @commitlint/prompt-cli</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">yarn add -D husky lint-staged</span></span><br></pre></td></tr></table></figure><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// package.json</span></span><br><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;commit&quot;</span><span class="punctuation">:</span> <span class="string">&quot;commit&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;commitlint&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;extends&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">      <span class="string">&quot;@commitlint/config-conventional&quot;</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><span class="line">  <span class="attr">&quot;husky&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;hooks&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">      <span class="attr">&quot;commit-msg&quot;</span><span class="punctuation">:</span> <span class="string">&quot;commitlint -E HUSKY_GIT_PARAMS&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">&#125;</span></span><br></pre></td></tr></table></figure><h2 id="standard-version"><a href="#standard-version" class="headerlink" title="standard-version"></a>standard-version</h2><p><a href="https://link.zhihu.com/?target=https://github.com/conventional-changelog/standard-version">standard-version</a> 是一款遵循<a href="https://link.zhihu.com/?target=https://semver.org/">语义化版本（ semver）</a>和 <a href="https://link.zhihu.com/?target=https://conventionalcommits.org/">commit message 标准规范</a> 的版本和 changlog 自动化工具。通常情况线下，我们会在 master 分支进行如下的版本发布操作：</p><ol><li><code>git pull origin master</code></li><li>根据 <code>package.json</code> 中的 <code>version</code> 更新版本号，更新 CHANGELOG</li><li><code>git add .</code></li><li><code>git commit</code></li><li><code>git tag</code> 打版本操作</li><li><code>git push --follow-tags origin master &amp;&amp; npm publish</code>：push 版本 tag 和 master 分支到仓库并发布</li></ol><p>其中 <strong>2，3，4，5</strong> 是 standard-version 工具会自动完成的工作，配合本地的 shell 脚本，则可以自动完成一系列版本发布的工作了。</p><h3 id="安装-使用"><a href="#安装-使用" class="headerlink" title="安装 &amp; 使用"></a>安装 &amp; 使用</h3><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">yarn add -D standard-version</span></span><br></pre></td></tr></table></figure><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// package.json</span></span><br><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;release&quot;</span><span class="punctuation">:</span> <span class="string">&quot;standard-version&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><ul><li>First Release：<code>yarn release --first-release</code></li><li>Cutting Release：<code>yarn release</code></li><li>Release as a Pre-Release：<code>yarn release --prerelease</code> or <code>yarn release --prerelease alpha</code></li><li>Release as a Target Type Imperatively (<code>npm version</code>-like)：<code>yarn release --release-as minor</code> or <code>yarn release --release-as 1.1.0</code>，可以合并 <code>--prerelease</code>以此方便发布实验性特性</li><li>Prevent Git Hooks：<code>yarn release --no-verify</code></li></ul><h2 id="资源参考"><a href="#资源参考" class="headerlink" title="资源参考"></a>资源参考</h2><ul><li><a href="%5Bhttps://git-scm.com/book/zh/v2/%E8%87%AA%E5%AE%9A%E4%B9%89-Git-Git-%E9%92%A9%E5%AD%90%5D(https://git-scm.com/book/zh/v2/%E8%87%AA%E5%AE%9A%E4%B9%89-Git-Git-%E9%92%A9%E5%AD%90)">Git 钩子</a></li><li><a href="https://juejin.im/post/5ce607946fb9a07ef5620e5b">前端自动化工作流中的hooks</a></li><li><a href="https://juejin.im/post/5bd2debfe51d457abc710b57">规范你的 commit message 并且根据 commit 自动生成 CHANGELOG.md</a></li><li><a href="https://zhuanlan.zhihu.com/p/51894196">git commit 、CHANGELOG 和版本发布的标准自动化</a></li><li><a href="https://juejin.im/post/5afc5242f265da0b7f44bee4">优雅的提交你的 Git Commit Message</a></li><li><a href="https://www.conventionalcommits.org/zh-hans/v1.0.0-beta.4/">Conventional Commits</a>：约定式提交，一种用于给提交信息增加人机可读含义的规范</li><li><a href="https://github.com/conventional-changelog/standard-version">standard-version</a>：<a href="https://link.zhihu.com/?target=https://github.com/conventional-changelog/standard-version">standard-version</a> 是一款遵循<a href="https://link.zhihu.com/?target=https://semver.org/">语义化版本（ semver）</a>和 <a href="https://link.zhihu.com/?target=https://conventionalcommits.org/">commit message 标准规范</a> 的版本和 changlog 自动化工具</li><li><a href="https://github.com/conventional-changelog/commitlint">commitlint</a>：检查您的提交消息是否符合<a href="https://conventionalcommits.org/">conventional commit format</a></li><li><a href="https://editorconfig.org/">EditorConfig</a>：EditorConfig有助于维护跨多个编辑器和IDE从事同一项目的多个开发人员的一致编码风格。</li><li><a href="https://eslint.org/">ESLint</a>：查找并修复JavaScript代码中的问题</li><li><a href="https://prettier.io/">Prettier</a>：更漂亮、固执的代码格式化</li></ul>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/c26ea6288229/</id>
    <link href="https://blog.zisheng.pro/posts/c26ea6288229/"/>
    <published>2020-03-24T14:53:11.000Z</published>
    <summary>本文以 React Native 项目为示例介绍了前端规范化开发涉及的工具及知识，但是所涉及内容具有通用性，建议读者点赞收藏(^▽^)。</summary>
    <title>前端规范化开发实践</title>
    <updated>2026-09-18T08:15:28.636Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="React Native" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/React-Native/"/>
    <category term="React Native" scheme="https://blog.zisheng.pro/tags/React-Native/"/>
    <category term="amap" scheme="https://blog.zisheng.pro/tags/amap/"/>
    <category term="高德地图" scheme="https://blog.zisheng.pro/tags/%E9%AB%98%E5%BE%B7%E5%9C%B0%E5%9B%BE/"/>
    <category term="定位" scheme="https://blog.zisheng.pro/tags/%E5%AE%9A%E4%BD%8D/"/>
    <category term="react-native-amap-geolocation" scheme="https://blog.zisheng.pro/tags/react-native-amap-geolocation/"/>
    <category term="react-native" scheme="https://blog.zisheng.pro/tags/react-native/"/>
    <content>
      <![CDATA[<p>react-native-amap-geolocation 这款插件接入了高德地图定位模块。支持 Android + iOS，提供尽可能完善的原生接口， 同时提供符合 Web 标准的 Geolocation API。</p><blockquote><p>本文基于 react-native 0.60 以上版本！</p></blockquote><h2 id="获取高德-App-Key"><a href="#获取高德-App-Key" class="headerlink" title="获取高德 App Key"></a>获取高德 App Key</h2><p>为了使用高德 SDK，你需要准备高德 App Key，获取方法参考高德地图 SDK 官方文档：</p><ul><li><p><a href="https://lbs.amap.com/api/android-location-sdk/guide/create-project/get-key">获取 Android App Key</a></p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;an:key-debug&quot;</span><span class="punctuation">:</span> <span class="string">&quot;keytool -list -v -keystore ./android/app/debug.keystore&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;an:key-release&quot;</span><span class="punctuation">:</span> <span class="string">&quot;keytool -v -list -keystore ./android/app/release.keystore&quot;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure></li><li><p><a href="https://lbs.amap.com/api/ios-location-sdk/guide/create-project/get-key">获取 iOS App Key</a></p><blockquote><p>ios 只需要 <code>Bundle Identifier</code> 即可</p></blockquote></li></ul><h2 id="安装"><a href="#安装" class="headerlink" title="安装"></a>安装</h2><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ yarn add react-native-amap-geolocation</span><br></pre></td></tr></table></figure><h2 id="ios-配置"><a href="#ios-配置" class="headerlink" title="ios 配置"></a>ios 配置</h2><h3 id="下载和安装高德IOS-SDK"><a href="#下载和安装高德IOS-SDK" class="headerlink" title="下载和安装高德IOS SDK"></a>下载和安装高德IOS SDK</h3><ol><li>下载<a href="https://links.jianshu.com/go?to=https://a.amap.com/lbs/static/zip/AMap_iOS_Foundation_Lib_V1.4.3.zip">基础SDK(含IDFA)</a></li><li>下载<a href="https://links.jianshu.com/go?to=https://a.amap.com/lbs/static/zip/AMap_iOS_Loc_Lib_V2.6.2.zip">定位SDK</a></li><li>将下载的 <code>AMapFoundationKit.framework</code> 和 <code>AMapLocationKit.framework</code> 以及 项目自带的<code>ExternalAccessory.framework</code> 添加到 <code>Build Phases</code> ➜ <code>Link Binary With Libraries</code>。</li></ol><h3 id="配置权限"><a href="#配置权限" class="headerlink" title="配置权限"></a>配置权限</h3><ol><li><p>添加以下权限到 <code>Info.plist</code> 文件中：</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">key</span>&gt;</span>NSLocationWhenInUseUsageDescription<span class="tag">&lt;/<span class="name">key</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">string</span>&gt;</span><span class="tag">&lt;/<span class="name">string</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">key</span>&gt;</span>NSLocationAlwaysUsageDescription<span class="tag">&lt;/<span class="name">key</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">string</span>&gt;</span><span class="tag">&lt;/<span class="name">string</span>&gt;</span></span><br></pre></td></tr></table></figure></li><li><p>ATS设置:  <code>Target</code>➜ <code>Info</code> ➜ <code>App Transport Security Setting</code> 展开后的 <code>Allow Arbitrary Loads</code> 设置为 <code>YES</code></p></li></ol><p><strong>如果要支持后台持续定位，还需要配置以下：</strong></p><ol><li><p>打开xcode，点击项目</p></li><li><p>选择 <code>Signing&amp;Capabilities</code>,把 <code>Background Modes</code> 打开为 <code>ON</code>，然后打勾 <code>Location updates</code></p><img src="https://i.loli.net/2020/03/18/ljTXqS9AboycPNk.png" style="zoom: 67%;" /></li></ol><h2 id="Android"><a href="#Android" class="headerlink" title="Android"></a>Android</h2><p>react-native 为 0.6x 的话，<code>yarn add react-native-amap-geolocation</code> 就完事了，一步到位不需要过多配置。</p><h2 id="基本用法"><a href="#基本用法" class="headerlink" title="基本用法"></a>基本用法</h2><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">PermissionsAndroid</span> &#125; <span class="keyword">from</span> <span class="string">&quot;react-native&quot;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; init, <span class="title class_">Geolocation</span> &#125; <span class="keyword">from</span> <span class="string">&quot;react-native-amap-geolocation&quot;</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 对于 Android 需要自行根据需要申请权限</span></span><br><span class="line"><span class="keyword">await</span> <span class="title class_">PermissionsAndroid</span>.<span class="title function_">request</span>(<span class="title class_">PermissionsAndroid</span>.<span class="property">PERMISSIONS</span>.<span class="property">ACCESS_COARSE_LOCATION</span>);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 使用自己申请的高德 App Key 进行初始化</span></span><br><span class="line"><span class="keyword">await</span> <span class="title function_">init</span>(&#123;</span><br><span class="line">  <span class="attr">ios</span>: <span class="string">&quot;9bd6c82e77583020a73ef1af59d0c759&quot;</span>,</span><br><span class="line">  <span class="attr">android</span>: <span class="string">&quot;043b24fe18785f33c491705ffe5b6935&quot;</span></span><br><span class="line">&#125;);</span><br><span class="line"></span><br><span class="line"><span class="title class_">Geolocation</span>.<span class="title function_">getCurrentPosition</span>(<span class="function">(<span class="params">&#123; coords &#125;</span>) =&gt;</span> &#123;</span><br><span class="line">  <span class="variable language_">console</span>.<span class="title function_">log</span>(coords);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><h2 id="更多用法"><a href="#更多用法" class="headerlink" title="更多用法"></a>更多用法</h2><p>该项目除了提供符合 Web 标准的 Geolocation API，同时为了最大程度的发挥高德定位 SDK 的功能， 会尽可能提供与原生 SDK 一致的接口封装。由于 iOS 和 Android SDK 提供的接口并不一致， 于是最终实现的接口大部分是并不通用的。这在接口文档或文档注释有注明， 比如 <code>@platform android</code> 表示该接口仅用于 Android。</p><p>以下是一些常用接口的用法说明以及示例代码，更多接口的具体用法请参考<a href="https://qiuxiang.github.io/react-native-amap-geolocation/#/">接口文档</a>。</p><h2 id="直接使用原生接口"><a href="#直接使用原生接口" class="headerlink" title="直接使用原生接口"></a>直接使用原生接口</h2><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; init, addLocationListener, start, stop &#125; <span class="keyword">from</span> <span class="string">&quot;react-native-amap-geolocation&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_">addLocationListener</span>(<span class="function"><span class="params">location</span> =&gt;</span> <span class="variable language_">console</span>.<span class="title function_">log</span>(location));</span><br><span class="line"></span><br><span class="line"><span class="comment">// 开始连续定位</span></span><br><span class="line"><span class="title function_">start</span>();</span><br><span class="line"></span><br><span class="line"><span class="comment">// 在不需要的时候停止定位</span></span><br><span class="line"><span class="title function_">stop</span>();</span><br></pre></td></tr></table></figure><h2 id="逆地理编码"><a href="#逆地理编码" class="headerlink" title="逆地理编码"></a>逆地理编码</h2><p>Android 默认返回逆地理编码，而 iOS 需要手动设置。</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; setLocatingWithReGeocode, setNeedAddress &#125; <span class="keyword">from</span> <span class="string">&quot;react-native-amap-geolocation&quot;</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// android</span></span><br><span class="line"><span class="title function_">setNeedAddress</span>(<span class="literal">true</span>);</span><br><span class="line"></span><br><span class="line"><span class="comment">// ios</span></span><br><span class="line"><span class="title function_">setLocatingWithReGeocode</span>(<span class="literal">true</span>);</span><br></pre></td></tr></table></figure><h2 id="定位回调频率限制"><a href="#定位回调频率限制" class="headerlink" title="定位回调频率限制"></a>定位回调频率限制</h2><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; <span class="built_in">setInterval</span>, setDistanceFilter &#125; <span class="keyword">from</span> <span class="string">&quot;react-native-amap-geolocation&quot;</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// android，5 秒请求一次定位</span></span><br><span class="line"><span class="built_in">setInterval</span>(<span class="number">5000</span>);</span><br><span class="line"></span><br><span class="line"><span class="comment">// ios，设备移动超过 10 米才会更新位置信息</span></span><br><span class="line"><span class="title function_">setDistanceFilter</span>(<span class="number">10</span>);</span><br></pre></td></tr></table></figure><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ul><li><a href="https://lbs.amap.com/api">高德地图API</a></li><li><a href="https://www.jianshu.com/p/f87316635ff6">react-native-amap-geolocation安装与使用</a></li><li><a href="https://qiuxiang.github.io/react-native-amap-geolocation/">react-native-amap-geolocation 使用指南</a></li><li>[<a href="https://qiuxiang.github.io/react-native-amap-geolocation/api/index.html">react-native-amap-geolocation 接口文档</a></li></ul>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/6ab9356b577c/</id>
    <link href="https://blog.zisheng.pro/posts/6ab9356b577c/"/>
    <published>2020-03-17T08:05:44.000Z</published>
    <summary>react-native-amap-geolocation 这款插件接入了高德地图定位模块。支持 Android + iOS，提供尽可能完善的原生接口， 同时提供符合 Web 标准的 Geolocation API。</summary>
    <title>使用 react-native-amap-geolocation 接入高德地图定位</title>
    <updated>2026-09-18T08:15:28.636Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="漫谈" scheme="https://blog.zisheng.pro/categories/%E6%BC%AB%E8%B0%88/"/>
    <category term="程序员" scheme="https://blog.zisheng.pro/tags/%E7%A8%8B%E5%BA%8F%E5%91%98/"/>
    <category term="macOS" scheme="https://blog.zisheng.pro/tags/macOS/"/>
    <category term="开发环境" scheme="https://blog.zisheng.pro/tags/%E5%BC%80%E5%8F%91%E7%8E%AF%E5%A2%83/"/>
    <category term="Mac 环境" scheme="https://blog.zisheng.pro/tags/Mac-%E7%8E%AF%E5%A2%83/"/>
    <category term="程序员 Mac 环境" scheme="https://blog.zisheng.pro/tags/%E7%A8%8B%E5%BA%8F%E5%91%98-Mac-%E7%8E%AF%E5%A2%83/"/>
    <content>
      <![CDATA[<h2 id="Xcode-Command-Line-Tools"><a href="#Xcode-Command-Line-Tools" class="headerlink" title="Xcode Command Line Tools"></a>Xcode Command Line Tools</h2><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ xcode-select --install</span><br></pre></td></tr></table></figure><h2 id="Homebrew"><a href="#Homebrew" class="headerlink" title="Homebrew"></a>Homebrew</h2><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">/bin/zsh -c <span class="string">&quot;<span class="subst">$(curl -fsSL https://gitee.com/cunkai/HomebrewCN/raw/master/Homebrew.sh)</span>&quot;</span></span><br></pre></td></tr></table></figure><h2 id="Git"><a href="#Git" class="headerlink" title="Git"></a>Git</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">git config --global user.name <span class="string">&#x27;紫升&#x27;</span></span><br><span class="line">git config --global user.email <span class="string">&#x27;luozhu2021@gmail.com&#x27;</span></span><br><span class="line">git config --global color.ui auto</span><br><span class="line">git config --global core.ignorecase <span class="literal">false</span></span><br><span class="line">git config --global core.autocrlf input</span><br><span class="line">git config --global pager.branch <span class="literal">false</span></span><br></pre></td></tr></table></figure><h2 id="Node"><a href="#Node" class="headerlink" title="Node"></a>Node</h2><p>Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行时。我们使用 <code>n</code> 模块来维护 Node 的版本：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">brew install n</span><br></pre></td></tr></table></figure><h3 id="安装指定版本"><a href="#安装指定版本" class="headerlink" title="安装指定版本"></a>安装指定版本</h3><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 安装最新的稳定版</span></span><br><span class="line">sudo n lts</span><br><span class="line"><span class="comment"># 安装最新版</span></span><br><span class="line">sudo n latest</span><br><span class="line"><span class="comment"># 安装指定版本</span></span><br><span class="line">sudo n 10.16.0</span><br></pre></td></tr></table></figure><h3 id="删除命令"><a href="#删除命令" class="headerlink" title="删除命令"></a>删除命令</h3><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 删除指定版本</span></span><br><span class="line">n <span class="built_in">rm</span> 12.10.0</span><br><span class="line"><span class="comment"># 除去当前版本以外的所有缓存版本</span></span><br><span class="line">n prune</span><br></pre></td></tr></table></figure><h3 id="安装-yarn-和-pnpm"><a href="#安装-yarn-和-pnpm" class="headerlink" title="安装 yarn 和 pnpm"></a>安装 yarn 和 pnpm</h3><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">brew install yarn pnpm -g</span><br></pre></td></tr></table></figure><h3 id="更新-npm"><a href="#更新-npm" class="headerlink" title="更新 npm"></a>更新 npm</h3><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sudo npm install -g npm</span><br></pre></td></tr></table></figure><h3 id="nrm-管理-registry"><a href="#nrm-管理-registry" class="headerlink" title="nrm 管理 registry"></a>nrm 管理 registry</h3><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">sudo npm install -g nrm</span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">列出可用的代理</span></span><br><span class="line">nrm ls</span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">添加私有代理</span></span><br><span class="line">nrm add &lt;name&gt; &lt;url&gt; [home]</span><br></pre></td></tr></table></figure><h3 id="安装-npkill"><a href="#安装-npkill" class="headerlink" title="安装 npkill"></a>安装 npkill</h3><p>轻松查找并删除旧的和沉重的 <code>node_modules</code> 文件夹</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">npm install -g npkill</span><br><span class="line"><span class="comment"># 在要搜索的根目录执行，比如 cd Desktop</span></span><br><span class="line">npkill</span><br></pre></td></tr></table></figure><h3 id="清理所有-node-进程"><a href="#清理所有-node-进程" class="headerlink" title="清理所有 node 进程"></a>清理所有 node 进程</h3><p>node 进程异常退出不会杀死 node 进程，久而久之 CPU 就负担不了了，这个时候 pkill 命令就可以代替活动监视器。</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">$ pkill node</span><br><span class="line">$ pkill esbuild</span><br></pre></td></tr></table></figure><h3 id="权限不足"><a href="#权限不足" class="headerlink" title="权限不足"></a>权限不足</h3><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">sudo <span class="built_in">chmod</span> -R 777 /usr/local/lib/node_modules</span><br><span class="line">sudo <span class="built_in">chmod</span> -R 777 /usr/local/bin/</span><br><span class="line">sudo <span class="built_in">chmod</span> -R 777 ~/.npm</span><br></pre></td></tr></table></figure><h2 id="VS-Code"><a href="#VS-Code" class="headerlink" title="VS Code"></a>VS Code</h2><ul><li>下载 <a href="https://code.visualstudio.com/">Visual Studio Code</a></li><li>安装 <a href="https://marketplace.visualstudio.com/items?itemName=youngjuning.vscode-luozhu-pack">vscode-luozhu-pack</a> 扩展包插件</li></ul><h2 id="iTerm2"><a href="#iTerm2" class="headerlink" title="iTerm2"></a>iTerm2</h2><p>点击 <a href="https://iterm2.com/downloads/stable/latest">https://iterm2.com/downloads/stable/latest</a> 下载最新版 iTerm2。</p><h2 id="ohmyzsh"><a href="#ohmyzsh" class="headerlink" title="ohmyzsh"></a>ohmyzsh</h2><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 切换 shell 为 zsh</span></span><br><span class="line">chsh -s /bin/zsh</span><br><span class="line"><span class="comment"># 通过 curl 安装 ohmyzsh</span></span><br><span class="line">sh -c <span class="string">&quot;<span class="subst">$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)</span>&quot;</span></span><br></pre></td></tr></table></figure><h3 id="插件"><a href="#插件" class="headerlink" title="插件"></a>插件</h3><ul><li><a href="https://github.com/zsh-users/zsh-autosuggestions/blob/master/INSTALL.md#oh-my-zsh">zsh-autosuggestions</a></li><li><a href="https://github.com/zsh-users/zsh-syntax-highlighting/blob/master/INSTALL.md">zsh-syntax-highlighting</a></li></ul><h3 id="主题"><a href="#主题" class="headerlink" title="主题"></a>主题</h3><p>ohmyzsh 主题我只用 ys。</p><h2 id="SwitchHosts"><a href="#SwitchHosts" class="headerlink" title="SwitchHosts"></a>SwitchHosts</h2><p>快速切换 hosts，点击 <a href="https://github.com/oldj/SwitchHosts/releases">https://github.com/oldj/SwitchHosts/releases</a> 去下载。</p><h2 id="Docker"><a href="#Docker" class="headerlink" title="Docker"></a>Docker</h2><blockquote><p>参考 <a href="https://zhuanlan.zhihu.com/p/1899756565264827657">Mac安装Docker（Orbstack）并使用Portainer管理</a></p></blockquote><ul><li><a href="https://orbstack.dev/download">Orbstack</a>：Docker Desktop 替代方案，实现光速开发。</li><li><a href="https://www.portainer.cn/">Portainer</a>：Docker 可视化管理面板</li></ul><h2 id="提效工具"><a href="#提效工具" class="headerlink" title="提效工具"></a>提效工具</h2><ul><li><a href="https://www.alfredapp.com/">Alfred</a>：Mac 上最强大的效率工具</li><li><a href="https://monica.im/">Monica</a>：人工智能助手</li><li><a href="https://getkap.co/">Kap</a>：Mac 上轻量 GIF 录屏小工具</li><li><a href="https://zh.snipaste.com/">Snipaste</a>：截图+贴图</li><li><a href="https://github.com/siddharthvaddem/openscreen">openscreen</a>：Screen Studio 的理想替代方案。</li><li><a href="https://www.u-tools.cn/index.html">uTools</a>：AI 时代的轻工具平台</li></ul><h2 id="v8"><a href="#v8" class="headerlink" title="v8"></a>v8</h2><blockquote><p>不要使用 <code>brew install v8</code>，因为可用命令是不完整的。</p></blockquote><h3 id="预备条件"><a href="#预备条件" class="headerlink" title="预备条件"></a>预备条件</h3><ul><li>Install Xcode (Avaliable on the Mac App Store)</li><li>Install Xcode Command Line Tools (Preferences &gt; Downloads)</li><li>Install <a href="https://www.chromium.org/developers/how-tos/install-depot-tools">depot_tools</a><ul><li><code>cd ~ &amp;&amp; git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git</code></li><li><code>sudo nano ~/.zshrc</code>(zsh) or <code>sudo nano ~/.bash_profile</code>(bash)</li><li>Add <code>export PATH=~/depot_tools:&quot;$PATH&quot;</code> (it’s important that depot_tools comes first here)</li><li><code>source ~/.zshrc</code></li><li>From the directory you want to install V8 into, run <code>gclient</code></li></ul></li></ul><h3 id="Build-V8"><a href="#Build-V8" class="headerlink" title="Build V8"></a>Build V8</h3><ul><li><code>fetch v8</code></li><li><code>cd ~/v8</code></li><li><code>gclient sync</code></li><li><code>tools/dev/v8gen.py x64.optdebug</code></li><li><code>ninja -C out.gn/x64.optdebug</code> (prepare for lots of fan noise)</li></ul><p>I’d also recommend adding these to your <code>.zshrc</code>:</p><ul><li><code>sudo nano ~/.zshrc</code></li><li>Add <code>alias d8=~/v8/out.gn/x64.optdebug/d8</code></li><li>Add <code>alias tick-processor=~/v8/tools/mac-tick-processor</code></li><li>Add <code>export D8_PATH=&quot;~/v8/out.gn/x64.optdebug&quot;</code></li><li><code>source ~/.zshrc</code></li></ul><blockquote><p>使用 Demo 请参考 <a href="https://gist.github.com/kevincennis/0cd2138c78a07412ef21#d8-shell-examples">d8-shell-examples</a></p></blockquote><h2 id="MySql"><a href="#MySql" class="headerlink" title="MySql"></a>MySql</h2><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">brew install mysql</span><br></pre></td></tr></table></figure><h3 id="修改配置文件"><a href="#修改配置文件" class="headerlink" title="修改配置文件"></a>修改配置文件</h3><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">code /usr/local/etc/my.cnf</span><br></pre></td></tr></table></figure><h3 id="启动服务"><a href="#启动服务" class="headerlink" title="启动服务"></a>启动服务</h3><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">brew services start mysql</span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">重启</span></span><br><span class="line">brew services restart mysql</span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">停止</span></span><br><span class="line">brew services stop mysql</span><br></pre></td></tr></table></figure><blockquote><p>Or, if you don’t want&#x2F;need a background service you can just run: <code>mysql.server start</code></p></blockquote><h3 id="安全设置"><a href="#安全设置" class="headerlink" title="安全设置"></a>安全设置</h3><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">mysql_secure_installation</span><br></pre></td></tr></table></figure><ol><li>建立密码验证插件</li></ol><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">Securing the MySQL server deployment.</span><br><span class="line"></span><br><span class="line">Connecting to MySQL using a blank password.</span><br><span class="line"></span><br><span class="line">VALIDATE PASSWORD PLUGIN can be used to test passwords and improve security. It checks the strength of password and allows the users to set only those passwords which are secure enough. Would you like to setup VALIDATE PASSWORD plugin?</span><br><span class="line"></span><br><span class="line">Press y|Y for Yes, any other key for No: y</span><br></pre></td></tr></table></figure><ol start="2"><li>选择密码规则</li></ol><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">There are three levels of password validation policy:</span><br><span class="line"></span><br><span class="line">LOW    Length &gt;= 8</span><br><span class="line"><span class="meta prompt_">#</span><span class="language-bash">长度大于等于8</span></span><br><span class="line">MEDIUM Length &gt;= 8, numeric, mixed case, and special characters</span><br><span class="line"><span class="meta prompt_">#</span><span class="language-bash">长度大于等于8，数字、大小写字母、特殊符号</span></span><br><span class="line">STRONG Length &gt;= 8, numeric, mixed case, special characters and dictionary file</span><br><span class="line"><span class="meta prompt_">#</span><span class="language-bash">长度大于等于8，数字、大小写字母、特殊符号和字典文件（慎选！）</span></span><br><span class="line"></span><br><span class="line">Please enter 0 = LOW, 1 = MEDIUM and 2 = STRONG: 1</span><br><span class="line">Please set the password for root here.</span><br><span class="line"></span><br><span class="line">New password: （输入你的密码）</span><br><span class="line">Re-enter new password: （再次输入你的密码）</span><br></pre></td></tr></table></figure><ol start="3"><li>创建符合规则的新密码</li></ol><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">Estimated strength of the password: 50 #密码强度</span><br><span class="line">Do you wish to continue with the password provided?(Press y|Y for Yes, any other key for No) : y</span><br></pre></td></tr></table></figure><ol start="4"><li>删除匿名用户</li></ol><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">By default, a MySQL installation has an anonymous user, allowing anyone to log into MySQL without having to have a user account created for them. This is intended only for testing, and to make the installation go a bit smoother.</span><br><span class="line">You should remove them before moving into a production environment.</span><br><span class="line"></span><br><span class="line">Remove anonymous users? (Press y|Y for Yes, any other key for No) : y</span><br><span class="line">Success.</span><br></pre></td></tr></table></figure><ol start="5"><li>禁止远程登录</li></ol><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">Normally, root should only be allowed to connect from &#x27;localhost&#x27;. This ensures that someone cannot guess at the root password from the network.</span><br><span class="line"></span><br><span class="line">Disallow root login remotely? (Press y|Y for Yes, any other key for No) : y</span><br><span class="line">Success.</span><br></pre></td></tr></table></figure><ol start="6"><li>删除测试数据表</li></ol><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">By default, MySQL comes with a database named &#x27;test&#x27; that anyone can access. This is also intended only for testing, and should be removed before moving into a production environment.</span><br><span class="line"></span><br><span class="line">Remove test database and access to it? (Press y|Y for Yes, any other key for No) : y</span><br><span class="line">- Dropping test database...</span><br><span class="line">Success.</span><br><span class="line"></span><br><span class="line">- Removing privileges on test database...</span><br><span class="line">Success.</span><br></pre></td></tr></table></figure><ol start="7"><li>Done</li></ol><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">Reloading the privilege tables will ensure that all changes made so far will take effect immediately.</span><br><span class="line"></span><br><span class="line">Reload privilege tables now? (Press y|Y for Yes, any other key for No) : y</span><br><span class="line"><span class="meta prompt_">#</span><span class="language-bash">是否重新加载权限表</span></span><br><span class="line">Success.</span><br><span class="line"></span><br><span class="line">All done!</span><br></pre></td></tr></table></figure><p><strong>Your password does not satisfy the current policy requirements：</strong></p><p>如果你在选择密码规则的时候不小心选择了 2，也就是数字、大小写字母、特殊符号和字典文件的组合。这时你会发现 <code>mysql_secure_installation</code>不会再给你机会重新设置了。手动微笑，mmp。方法还是有的：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">SHOW VARIABLES LIKE &#x27;validate_password%&#x27;;</span><br></pre></td></tr></table></figure><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/68541c4633b944d69276a3da8f453406~tplv-k3u1fbpfcp-zoom-1.image" alt="紫升"></p><p>使用命令 <code>mysql -u root</code> 登陆，执行：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">set global validate_password.policy=0;</span><br><span class="line">set global validate_password.length=4;</span><br><span class="line"><span class="meta prompt_">#</span><span class="language-bash">将密码规则设置为LOW，就可以使用纯数字纯字母密码</span></span><br></pre></td></tr></table></figure><h3 id="登陆"><a href="#登陆" class="headerlink" title="登陆"></a>登陆</h3><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">mysql -u root -p</span></span><br></pre></td></tr></table></figure><h2 id="mongodb"><a href="#mongodb" class="headerlink" title="mongodb"></a>mongodb</h2><blockquote><p>参考: <a href="https://www.runoob.com/mongodb/mongodb-osx-install.html">Mac OSX 平台安装 MongoDB</a>、<a href="https://www.32e.top/system/mac/article-87.html">Mac 下安装 MongoDB 数据库-启动-停止-开启验证-登陆</a>、<a href="https://uizph.com/article/5db177e4a9f13d7f535810c5">【环境搭建：二】Mac 安装、配置 MongoDB</a>、<a href="https://www.jianshu.com/p/f5afc6488f9e">MongoDB 的用户创建更新及删除</a>、<a href="https://cloud.tencent.com/developer/article/1446551">MongoDB 用户名密码登录 认证登陆</a></p></blockquote><h3 id="下载安装"><a href="#下载安装" class="headerlink" title="下载安装"></a>下载安装</h3><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">$ brew install mongodb/brew/mongodb-community</span><br><span class="line">$ mongod -version</span><br></pre></td></tr></table></figure><h3 id="配置"><a href="#配置" class="headerlink" title="配置"></a>配置</h3><p><strong>启动 mongo</strong>：</p><ol><li>新建 dbpath</li></ol><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">$ sudo <span class="built_in">mkdir</span> ~/data/db</span><br><span class="line">$ sudo <span class="built_in">mkdir</span> ~/data/log</span><br></pre></td></tr></table></figure><ol start="2"><li>启动</li></ol><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ sudo mongod --dbpath ~/data/db --fork --logpath ~/data/log/mongo.log</span><br></pre></td></tr></table></figure><blockquote><p>注意：Mac OS 10.15.1 版本之后， <code>/data/db</code> 文件夹消失了，重新创建文件夹提示 <code>mkdir: /data/db: Read-only file system</code>，解决办法也可以是 <code>sudo mkdir ~/data/db &amp;&amp; sudo mongodb --dbpath ~/data/db</code></p></blockquote><p><strong>设置验证和用户名密码</strong>：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line">$ mongo</span><br><span class="line"><span class="comment"># 创建超级管理员</span></span><br><span class="line">&gt; db.createUser(&#123; user: <span class="string">&quot;root&quot;</span> , <span class="built_in">pwd</span>: <span class="string">&quot;123456&quot;</span>, roles: [<span class="string">&quot;root&quot;</span>]&#125;);</span><br><span class="line">Successfully added user: &#123;</span><br><span class="line">   <span class="string">&quot;user&quot;</span> : <span class="string">&quot;root&quot;</span>,</span><br><span class="line">   <span class="string">&quot;roles&quot;</span> : [<span class="string">&quot;root&quot;</span>]</span><br><span class="line">&#125;</span><br><span class="line"><span class="comment"># 尝试使用上面创建的用户信息进行连接。</span></span><br><span class="line">&gt; db.auth(<span class="string">&quot;root&quot;</span>,<span class="string">&quot;123456&quot;</span>)</span><br><span class="line">1</span><br><span class="line"><span class="comment"># 创建一个名为 admin，密码为 123456 的用户。</span></span><br><span class="line">&gt; db.createUser(&#123; user: <span class="string">&quot;admin&quot;</span>, <span class="built_in">pwd</span>: <span class="string">&quot;123456&quot;</span>, roles:[<span class="string">&quot;userAdminAnyDatabase&quot;</span>, <span class="string">&quot;dbAdminAnyDatabase&quot;</span>, <span class="string">&quot;readWriteAnyDatabase&quot;</span>]&#125;);</span><br><span class="line">Successfully added user: &#123;</span><br><span class="line">   <span class="string">&quot;user&quot;</span>: <span class="string">&quot;admin&quot;</span>,</span><br><span class="line">   <span class="string">&quot;roles&quot;</span>: [</span><br><span class="line">   &#123;</span><br><span class="line">      <span class="string">&quot;role&quot;</span>: <span class="string">&quot;userAdminAnyDatabase&quot;</span>,</span><br><span class="line">      <span class="string">&quot;db&quot;</span>: <span class="string">&quot;admin&quot;</span></span><br><span class="line">   &#125;</span><br><span class="line">  ]</span><br><span class="line">&#125;</span><br><span class="line"><span class="comment"># 尝试使用上面创建的用户信息进行连接。</span></span><br><span class="line">&gt; db.auth(<span class="string">&quot;admin&quot;</span>,<span class="string">&quot;123456&quot;</span>)</span><br><span class="line">1</span><br></pre></td></tr></table></figure><p><strong>开启验证模式登录</strong>：</p><p>开启 mongod 时，指定 <code>--auth</code> 参数即可以验证模式打开：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ sudo mongod --dbpath ~/data/db --fork --logpath ~/data/log/mongo.log --auth</span><br></pre></td></tr></table></figure><p><strong>登录时验证</strong>：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">mongo 127.0.0.1:27017/admin -u admin -p 123456</span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">等价于</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">mongo --port 27017 -u <span class="string">&quot;adminUser&quot;</span> -p <span class="string">&quot;adminPass&quot;</span> --authenticationDatabase <span class="string">&quot;admin&quot;</span></span></span><br></pre></td></tr></table></figure><p><strong>登录后验证</strong>：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">mongo</span></span><br><span class="line"><span class="meta prompt_">&gt; </span><span class="language-bash">use admin</span></span><br><span class="line"><span class="meta prompt_">&gt; </span><span class="language-bash">ab.auth(<span class="string">&quot;admin&quot;</span>,<span class="string">&quot;123456&quot;</span>)</span></span><br></pre></td></tr></table></figure><h3 id="退出-mongodb"><a href="#退出-mongodb" class="headerlink" title="退出 mongodb"></a>退出 mongodb</h3><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 先停止 mongod 服务</span></span><br><span class="line">$ use admin;</span><br><span class="line">$ db.shutdownServer();</span><br><span class="line"></span><br><span class="line"><span class="comment"># 然后退出 mongo</span></span><br><span class="line">$ <span class="built_in">exit</span>;</span><br></pre></td></tr></table></figure><h2 id="更多"><a href="#更多" class="headerlink" title="更多"></a>更多</h2><h3 id="natapp"><a href="#natapp" class="headerlink" title="natapp"></a>natapp</h3><p>开启您的内网穿透之旅,调试微信的利器，请至<a href="https://natapp.cn/">官网</a>下载。</p><h3 id="设置-SSH-Key"><a href="#设置-SSH-Key" class="headerlink" title="设置 SSH Key"></a>设置 SSH Key</h3><p>在用户主目录下，看看有没有 <code>.ssh</code> 目录，如果有，再看看这个目录下有没有 <code>id_rsa</code> 和 <code>id_rsa.pub</code> 这两个文件，如果已经有了，可直接跳到下一步。如果没有，打开 Shell（Windows 下打开 Git Bash），创建 SSH Key：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">$ ssh-keygen -t rsa -C <span class="string">&quot;luozhu2021@gmail.com&quot;</span></span><br><span class="line"><span class="comment"># 添加到系统 keychain</span></span><br><span class="line">ssh-add --apple-use-keychain ~/.ssh/id_rsa</span><br><span class="line"><span class="comment"># 添加 public key 到 github</span></span><br><span class="line">gh auth login</span><br><span class="line">gh ssh-key add ~/.ssh/id_rsa.pub -t id_rsa</span><br></pre></td></tr></table></figure><h3 id="vim"><a href="#vim" class="headerlink" title="vim"></a>vim</h3><blockquote><p>建议使用：<a href="https://github.com/amix/vimrc">https://github.com/amix/vimrc</a></p></blockquote><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">git <span class="built_in">clone</span> --depth=1 https://github.com/amix/vimrc.git ~/.vim_runtime</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">sh ~/.vim_runtime/install_awesome_vimrc.sh</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash"><span class="built_in">echo</span> <span class="string">&quot;set number&quot;</span> &gt;&gt; ~/.vimrc</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash"><span class="built_in">echo</span> <span class="string">&quot;set showcmd&quot;</span> &gt;&gt; ~/.vimrc</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash"><span class="built_in">source</span> ~/.vimrc</span></span><br></pre></td></tr></table></figure><blockquote><p>本文首发于「<a href="https://blog.zisheng.pro/">紫升的博客</a>」，同步于「<a href="https://juejin.cn/user/325111174662855">掘金专栏</a>」。</p></blockquote>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/da1fab34e822/</id>
    <link href="https://blog.zisheng.pro/posts/da1fab34e822/"/>
    <published>2020-03-04T17:06:42.000Z</published>
    <summary>本文介绍了程序员常用的 Mac 环境配置，作为记录分享给大家。</summary>
    <title>程序员的 Mac 开发环境</title>
    <updated>2026-09-18T08:15:28.640Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="React" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/React/"/>
    <category term="Ant Design" scheme="https://blog.zisheng.pro/tags/Ant-Design/"/>
    <category term="UI 框架" scheme="https://blog.zisheng.pro/tags/UI-%E6%A1%86%E6%9E%B6/"/>
    <category term="babel-plugin-import" scheme="https://blog.zisheng.pro/tags/babel-plugin-import/"/>
    <category term="babel" scheme="https://blog.zisheng.pro/tags/babel/"/>
    <content>
      <![CDATA[<p><code>antd</code> 是基于 Ant Design 设计体系的 React UI 组件库，主要用于研发企业级中后台产品。</p><blockquote><p>文章可参考源码：<a href="https://github.com/youngjuning/antd-with-ts-demo">antd-with-ts-demo</a></p></blockquote><h2 id="Ant-Design-设计体系"><a href="#Ant-Design-设计体系" class="headerlink" title="Ant Design 设计体系"></a>Ant Design 设计体系</h2><p>以笔者的经验来看，Ant Design 设计体系下的产品设计理念、使用方式、底层技术、周边工具都保持着高度一致，工具不是越多越好，有一套好用顺手的就行，UI 框架千千万，你不可能都学一遍。Ant Design 无疑能够减少你的学习成本。</p><ul><li>设计<ul><li>设计价值观</li><li>全局样式</li><li>设计模式</li></ul></li><li>组件库<ul><li><a href="https://ant.design/docs/react/introduce-cn">Ant Design of React</a>: 基于 Ant Design 设计体系的 React UI 组件库，主要用于研发企业级中后台产品。</li><li><a href="https://mobile.ant.design/docs/react/introduce-cn">Ant Design Mobile of React</a>: <code>antd-mobile</code> 是 Ant Design 的移动规范的 React 实现，服务于蚂蚁及口碑无线业务。</li><li><a href="https://rn.mobile.ant.design/docs/react/introduce-cn">Ant Design Mobile RN of React</a>: <code>@ant-design/react-native</code> 是 <a href="http://ant.design/">Ant Design</a> 的移动规范的 React 实现，服务于蚂蚁及口碑无线业务。</li><li><a href="https://ng.ant.design/docs/introduce/zh">Ant Design of Angular</a>: 这里是 Ant Design 的 Angular 实现，开发和服务于企业级后台产品。</li><li><a href="https://ng.mobile.ant.design/#/docs/introduce/zh">Ant Design Mobile of Angular</a>: 这里是 <strong>Ant Design</strong> 移动规范的 <strong>Angular</strong> 实现，服务于阿里巴巴集团数据无线业务。</li><li><a href="https://www.antdv.com/docs/vue/introduce-cn/">Ant Design of Vue</a>: 这里是 Ant Design 的 Vue 实现，开发和服务于企业级后台产品。</li></ul></li><li><a href="https://ant.design/components/icon-cn/">Icons</a>: 一整套优质的图标集</li><li><a href="https://antv.vision/zh">AntV</a>: AntV 是蚂蚁金服全新一代数据可视化解决方案，致力于提供一套简单方便、专业可靠、无限可能的数据可视化最佳实践。</li><li><a href="https://pro.ant.design/index-cn">Ant Design Pro</a>: 开箱即用的中台前端&#x2F;设计解决方案<ul><li><a href="http://dvajs.com/">dva</a>: 一个基于 Redux 的 轻量级数据流方案，概念来自 elm，支持 side effects、热替换、动态加载、react-native、SSR 等，已在生产环境广泛应用。</li><li><a href="http://umijs.org/">umi</a> : 一个可插拔的企业级 react 应用框架。umi 以路由为基础的，支持<a href="https://umijs.org/zh/guide/router.html">类 next.js 的约定式路由</a>，以及各种进阶的路由功能，并以此进行功能扩展，比如<a href="https://umijs.org/zh/plugin/umi-plugin-react.html#dynamicimport">支持路由级的按需加载</a>。然后配以完善的<a href="https://umijs.org/zh/plugin/">插件体系</a>，覆盖从源码到构建产物的每个生命周期，支持各种功能扩展和业务需求，同时提供 <a href="https://umijs.org/zh/guide/umi-ui.html">Umi UI</a> 通过可视化辅助编程（VAP）提高开发体验和研发效率。</li></ul></li></ul><p>从上面的体系中可以看出，Ant Design of React 可以说是整个 Ant Design 设计体系的核心产品，想要学习 Ant Design Pro，首先就要先熟悉 Ant Design of React。</p><h2 id="流行趋势"><a href="#流行趋势" class="headerlink" title="流行趋势"></a>流行趋势</h2><h3 id="npm-下载量"><a href="#npm-下载量" class="headerlink" title="npm 下载量"></a>npm 下载量</h3><p>如果拿 antd 和 element-ui、iview 这些老牌 Vue.js UI 框架对比，遥遥领先啊有没有：</p><p><img src="https://user-gold-cdn.xitu.io/2020/3/4/170a4939e1919973?w=1103&h=458&f=png&s=59582" alt="紫升"></p><p>如果拿 ant-design-vue 来和 element-ui、iview这些老牌 vue UI框架对比，也是很有竞争力的：</p><p><img src="https://user-gold-cdn.xitu.io/2020/3/4/170a4939e21c45c6?w=1115&h=454&f=png&s=76675" alt="紫升"></p><h3 id="GitHub-Star"><a href="#GitHub-Star" class="headerlink" title="GitHub Star"></a>GitHub Star</h3><p><img src="https://user-gold-cdn.xitu.io/2020/3/4/170a4939e23e1d0b?w=1119&h=207&f=png&s=36028" alt="紫升"></p><h2 id="特性"><a href="#特性" class="headerlink" title="特性"></a>特性</h2><ul><li>🌈 提炼自企业级中后台产品的交互语言和视觉风格。</li><li>📦 开箱即用的高质量 React 组件。</li><li>🛡 使用 TypeScript 开发，提供完整的类型定义文件。</li><li>⚙️ 全链路开发和设计工具体系。</li><li>🌍 数十个国际化语言支持。</li><li>🎨 深入每个细节的主题定制能力。</li></ul><h2 id="支持环境"><a href="#支持环境" class="headerlink" title="支持环境"></a>支持环境</h2><ul><li>现代浏览器和 IE11 及以上（需要 <a href="https://ant.design/docs/react/getting-started-cn#%E5%85%BC%E5%AE%B9%E6%80%A7">polyfills</a>）。</li><li>支持服务端渲染。<ul><li><a href="https://nextjs.frontendx.cn/">Next.js</a>: <strong>Next.js</strong> 是一个轻量级的 React 服务端渲染应用框架。</li></ul></li><li><a href="https://electronjs.org/">Electron</a>：使用 JavaScript，HTML 和 CSS 构建跨平台的桌面应用程序<ul><li><a href="https://github.com/electron-react-boilerplate/electron-react-boilerplate">electron-react-boilerplate</a>: 可扩展的跨平台应用程序的基础</li></ul></li></ul><h2 id="安装"><a href="#安装" class="headerlink" title="安装"></a>安装</h2><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">yarn add antd</span></span><br></pre></td></tr></table></figure><h2 id="按需加载"><a href="#按需加载" class="headerlink" title="按需加载"></a>按需加载</h2><p>Antd 系列的 UI 组件库都需要引入 <a href="https://github.com/ant-design/babel-plugin-import">babel-plugin-import</a> 库来实现懒加载</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// .babelrc or babel-loader option</span></span><br><span class="line">&#123;</span><br><span class="line">  <span class="string">&quot;plugins&quot;</span>: [</span><br><span class="line">    [<span class="string">&quot;import&quot;</span>, &#123;</span><br><span class="line">      <span class="string">&quot;libraryName&quot;</span>: <span class="string">&quot;antd&quot;</span>,</span><br><span class="line">      <span class="string">&quot;libraryDirectory&quot;</span>: <span class="string">&quot;es&quot;</span>,</span><br><span class="line">      <span class="string">&quot;style&quot;</span>: <span class="string">&quot;css&quot;</span> <span class="comment">// `style: true` 会加载 less 文件</span></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>然后只需从 antd 引入模块即可，无需单独引入样式。等同于下面手动引入的方式。</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// babel-plugin-import 会帮助你加载 JS 和 CSS</span></span><br><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">DatePicker</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;antd&#x27;</span></span><br></pre></td></tr></table></figure><h2 id="快速上手"><a href="#快速上手" class="headerlink" title="快速上手"></a>快速上手</h2><blockquote><p>在开始之前，推荐先学习 <a href="http://reactjs.org/">React</a> 和 <a href="http://babeljs.io/docs/learn-es2015/">ES2015</a>，并正确安装和配置了 <a href="https://nodejs.org/">Node.js</a> v8 或以上。官方指南假设你已了解关于 HTML、CSS 和 JavaScript 的中级知识，并且已经完全掌握了 React 全家桶的正确开发方式。如果你刚开始学习前端或者 React，将 UI 框架作为你的第一步可能不是最好的主意。</p></blockquote><h3 id="1-创建一个-codesanbox"><a href="#1-创建一个-codesanbox" class="headerlink" title="1. 创建一个 codesanbox"></a>1. 创建一个 codesanbox</h3><p>访问 <a href="http://u.ant.design/codesandbox-repro">http://u.ant.design/codesandbox-repro</a> 创建一个 codesandbox 的在线示例，别忘了保存以创建一个新的实例。</p><h3 id="2-使用组件"><a href="#2-使用组件" class="headerlink" title="2. 使用组件"></a>2. 使用组件</h3><p>直接用下面的代码替换 <code>index.js</code> 的内容，用 React 的方式直接使用 antd 组件。</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">React</span> <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_">ReactDOM</span> <span class="keyword">from</span> <span class="string">&#x27;react-dom&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">ConfigProvider</span>, <span class="title class_">DatePicker</span>, message &#125; <span class="keyword">from</span> <span class="string">&#x27;antd&#x27;</span>;</span><br><span class="line"><span class="comment">// 由于 antd 组件的默认文案是英文，所以需要修改为中文</span></span><br><span class="line"><span class="keyword">import</span> zhCN <span class="keyword">from</span> <span class="string">&#x27;antd/es/locale/zh_CN&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> moment <span class="keyword">from</span> <span class="string">&#x27;moment&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> <span class="string">&#x27;moment/locale/zh-cn&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> <span class="string">&#x27;antd/dist/antd.css&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> <span class="string">&#x27;./index.css&#x27;</span>;</span><br><span class="line"></span><br><span class="line">moment.<span class="title function_">locale</span>(<span class="string">&#x27;zh-cn&#x27;</span>);</span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">App</span> <span class="keyword">extends</span> <span class="title class_ inherited__">React.Component</span> &#123;</span><br><span class="line">  state = &#123;</span><br><span class="line">    <span class="attr">date</span>: <span class="literal">null</span>,</span><br><span class="line">  &#125;;</span><br><span class="line"></span><br><span class="line">  handleChange = <span class="function"><span class="params">date</span> =&gt;</span> &#123;</span><br><span class="line">    message.<span class="title function_">info</span>(<span class="string">`您选择的日期是: <span class="subst">$&#123;date ? date.format(<span class="string">&#x27;YYYY-MM-DD&#x27;</span>) : <span class="string">&#x27;未选择&#x27;</span>&#125;</span>`</span>);</span><br><span class="line">    <span class="variable language_">this</span>.<span class="title function_">setState</span>(&#123; date &#125;);</span><br><span class="line">  &#125;;</span><br><span class="line">  <span class="title function_">render</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="keyword">const</span> &#123; date &#125; = <span class="variable language_">this</span>.<span class="property">state</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">ConfigProvider</span> <span class="attr">locale</span>=<span class="string">&#123;zhCN&#125;</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">width:</span> <span class="attr">400</span>, <span class="attr">margin:</span> &#x27;<span class="attr">100px</span> <span class="attr">auto</span>&#x27; &#125;&#125;&gt;</span></span></span><br><span class="line"><span class="language-xml">          <span class="tag">&lt;<span class="name">DatePicker</span> <span class="attr">onChange</span>=<span class="string">&#123;this.handleChange&#125;</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">marginTop:</span> <span class="attr">20</span> &#125;&#125;&gt;</span></span></span><br><span class="line"><span class="language-xml">            当前日期：&#123;date ? date.format(&#x27;YYYY-MM-DD&#x27;) : &#x27;未选择&#x27;&#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 class="language-xml">      <span class="tag">&lt;/<span class="name">ConfigProvider</span>&gt;</span></span></span><br><span class="line">    );</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="title class_">ReactDOM</span>.<span class="title function_">render</span>(<span class="language-xml"><span class="tag">&lt;<span class="name">App</span> /&gt;</span></span>, <span class="variable language_">document</span>.<span class="title function_">getElementById</span>(<span class="string">&#x27;root&#x27;</span>));</span><br></pre></td></tr></table></figure><h3 id="3-探索更多组件用法"><a href="#3-探索更多组件用法" class="headerlink" title="3. 探索更多组件用法"></a>3. 探索更多组件用法</h3><p>你可以在左侧菜单查看组件列表，比如 <a href="https://ant.design/components/alert-cn/">Alert</a> 组件，组件文档中提供了各类演示，最下方有组件 API 文档可以查阅。在代码演示部分找到第一个例子，点击右下角的图标展开代码。</p><p>然后依照演示代码的写法，在之前的 codesandbox 里修改 <code>index.js</code>，首先在 <code>import</code> 内引入 Alert 组件：</p><figure class="highlight diff"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="deletion">- import &#123; ConfigProvider, DatePicker, message &#125; from &#x27;antd&#x27;;</span></span><br><span class="line"><span class="addition">+ import &#123; ConfigProvider, DatePicker, message, Alert &#125; from &#x27;antd&#x27;;</span></span><br></pre></td></tr></table></figure><p>然后在 <code>render</code> 内添加相应的 jsx 代码：</p><figure class="highlight diff"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">  &lt;DatePicker onChange=&#123;value =&gt; this.handleChange(value)&#125; /&gt;</span><br><span class="line">  &lt;div style=&#123;&#123; marginTop: 20 &#125;&#125;&gt;</span><br><span class="line"><span class="deletion">-   当前日期：&#123;date ? date.format(&#x27;YYYY-MM-DD&#x27;) : &#x27;未选择&#x27;&#125;</span></span><br><span class="line"><span class="addition">+   &lt;Alert message=&#123;`当前日期：$&#123;date ? date.format(&#x27;YYYY-MM-DD&#x27;) : &#x27;未选择&#x27;&#125;`&#125; type=&quot;success&quot; /&gt;</span></span><br><span class="line">  &lt;/div&gt;</span><br></pre></td></tr></table></figure><p>好的，现在你已经会使用基本的 antd 组件了，你可以在这个例子中继续探索其他组件的用法。如果你遇到组件的 bug，也推荐建一个可重现的 codesandbox 来报告 bug。</p><h3 id="4-下一步"><a href="#4-下一步" class="headerlink" title="4. 下一步#"></a>4. 下一步<a href="https://ant.design/docs/react/getting-started-cn#4.-%E4%B8%8B%E4%B8%80%E6%AD%A5">#</a></h3><p>实际项目开发中，你会需要构建、调试、代理、打包部署等一系列工程化的需求。您可以阅读后面的文档或者使用以下脚手架和范例：</p><ul><li><a href="http://pro.ant.design/">Ant Design Pro</a></li><li><a href="https://github.com/zuiidea/antd-admin">antd-admin</a></li><li><a href="https://github.com/d2-projects/d2-admin">d2-admin</a></li><li>更多脚手架可以查看 <a href="http://scaffold.ant.design/">脚手架市场</a></li></ul><h2 id="使用-Day-js-替换-momentjs-优化打包大小"><a href="#使用-Day-js-替换-momentjs-优化打包大小" class="headerlink" title="使用 Day.js 替换 momentjs 优化打包大小"></a>使用 Day.js 替换 momentjs 优化打包大小</h2><p>你可以使用 <a href="https://github.com/ant-design/antd-dayjs-webpack-plugin">antd-dayjs-webpack-plugin</a> 插件用 Day.js 替换 momentjs 来大幅减小打包大小。这需要更新 webpack 的配置文件如下：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// webpack-config.js</span></span><br><span class="line"><span class="keyword">import</span> <span class="title class_">AntdDayjsWebpackPlugin</span> <span class="keyword">from</span> <span class="string">&#x27;antd-dayjs-webpack-plugin&#x27;</span>;</span><br><span class="line"></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">// ...</span></span><br><span class="line">  <span class="attr">plugins</span>: [<span class="keyword">new</span> <span class="title class_">AntdDayjsWebpackPlugin</span>()],</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><h2 id="在-TypeScript-中使用"><a href="#在-TypeScript-中使用" class="headerlink" title="在 TypeScript 中使用"></a>在 TypeScript 中使用</h2><p>使用 <code>create-react-app</code> 一步步地创建一个 TypeScript 项目，并引入 antd。</p><h3 id="安装和初始化"><a href="#安装和初始化" class="headerlink" title="安装和初始化"></a>安装和初始化</h3><p>创建 <a href="https://github.com/facebook/create-react-app/tree/master/packages/cra-template-typescript">cra-template-typescript</a> 项目：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">npx create-react-app my-app --template typescript</span></span><br></pre></td></tr></table></figure><p>然后我们进入项目并启动。</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash"><span class="built_in">cd</span> antd-demo-ts</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">yarn start</span></span><br></pre></td></tr></table></figure><p>此时浏览器会访问 <a href="http://localhost:3000/">http://localhost:3000/</a> ，看到 <code>Welcome to React</code> 的界面就算成功了。</p><h3 id="引入-antd"><a href="#引入-antd" class="headerlink" title="引入 antd"></a>引入 antd</h3><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">yarn add antd</span></span><br></pre></td></tr></table></figure><h3 id="自定义-create-react-app-配置"><a href="#自定义-create-react-app-配置" class="headerlink" title="自定义 create-react-app 配置"></a>自定义 create-react-app 配置</h3><p>我们需要对 create-react-app 的默认配置进行自定义，这里我们使用 <a href="https://github.com/timarney/react-app-rewired">react-app-rewired</a> （一个对 create-react-app 进行自定义配置的社区解决方案）。</p><p>引入 react-app-rewired 并修改 package.json 里的启动配置。由于新的 <a href="https://github.com/timarney/react-app-rewired#alternatives">react-app-rewired@2.x</a> 版本的关系，你还需要安装 <a href="https://github.com/arackaf/customize-cra">customize-cra</a>。</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">yarn add react-app-rewired customize-cra -D</span></span><br></pre></td></tr></table></figure><figure class="highlight diff"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">/* package.json */</span><br><span class="line">&quot;scripts&quot;: &#123;</span><br><span class="line"><span class="deletion">-   &quot;start&quot;: &quot;react-scripts start&quot;,</span></span><br><span class="line"><span class="addition">+   &quot;start&quot;: &quot;react-app-rewired start&quot;,</span></span><br><span class="line"><span class="deletion">-   &quot;build&quot;: &quot;react-scripts build&quot;,</span></span><br><span class="line"><span class="addition">+   &quot;build&quot;: &quot;react-app-rewired build&quot;,</span></span><br><span class="line"><span class="deletion">-   &quot;test&quot;: &quot;react-scripts test&quot;,</span></span><br><span class="line"><span class="addition">+   &quot;test&quot;: &quot;react-app-rewired test&quot;,</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在项目根目录创建一个 <code>config-overrides.js</code> 用于修改默认配置:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="variable language_">module</span>.<span class="property">exports</span> = <span class="keyword">function</span> <span class="title function_">override</span>(<span class="params">config, env</span>) &#123;</span><br><span class="line">  <span class="comment">// do stuff with the webpack config...</span></span><br><span class="line">  <span class="keyword">return</span> config;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><h3 id="使用-babel-plugin-import"><a href="#使用-babel-plugin-import" class="headerlink" title="使用 babel-plugin-import"></a>使用 babel-plugin-import</h3><p><a href="https://github.com/ant-design/babel-plugin-import">babel-plugin-import</a> 是一个用于按需加载组件代码和样式的 babel 插件（<a href="https://ant.design/docs/react/getting-started-cn#%E6%8C%89%E9%9C%80%E5%8A%A0%E8%BD%BD">原理</a>），现在我们尝试安装它并修改 <code>config-overrides.js</code> 文件。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ yarn add babel-plugin-import -D</span><br></pre></td></tr></table></figure><p>替换 <code>config-overrides.js</code> 文件内容：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> &#123; override, fixBabelImports &#125; = <span class="built_in">require</span>(<span class="string">&#x27;customize-cra&#x27;</span>);</span><br><span class="line"></span><br><span class="line"><span class="variable language_">module</span>.<span class="property">exports</span> = <span class="title function_">override</span>(</span><br><span class="line">  <span class="title function_">fixBabelImports</span>(<span class="string">&#x27;import&#x27;</span>, &#123;</span><br><span class="line">    <span class="attr">libraryName</span>: <span class="string">&#x27;antd&#x27;</span>,</span><br><span class="line">    <span class="attr">libraryDirectory</span>: <span class="string">&#x27;es&#x27;</span>,</span><br><span class="line">    <span class="attr">style</span>: <span class="string">&#x27;css&#x27;</span>, <span class="comment">// `style: true` 会加载 less 文件</span></span><br><span class="line">  &#125;),</span><br><span class="line">);</span><br></pre></td></tr></table></figure><h3 id="使用-antd"><a href="#使用-antd" class="headerlink" title="使用 antd"></a>使用 antd</h3><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// src/App.tsxe</span></span><br><span class="line"><span class="keyword">import</span> <span class="title class_">React</span> <span class="keyword">from</span> <span class="string">&#x27;react&#x27;</span>;</span><br><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;antd&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> <span class="string">&#x27;./App.css&#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">return</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;App&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;primary&quot;</span>&gt;</span>Button<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><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="title class_">App</span>;</span><br></pre></td></tr></table></figure><p>运行 <code>yarn start</code> 访问页面，antd 组件的 js 和 css 代码都会按需加载，你在控制台也不会看到这样的<a href="https://zos.alipayobjects.com/rmsportal/vgcHJRVZFmPjAawwVoXK.png">警告信息</a>。关于按需加载的原理和其他方式可以阅读<a href="https://ant.design/docs/react/getting-started-cn#%E6%8C%89%E9%9C%80%E5%8A%A0%E8%BD%BD">这里</a>。</p><h3 id="自定义主题"><a href="#自定义主题" class="headerlink" title="自定义主题"></a>自定义主题</h3><p>按照 <a href="https://ant.design/docs/react/customize-theme-cn">配置主题</a> 的要求，自定义主题需要用到 less 变量覆盖功能。我们可以引入 <code>customize-cra</code> 中提供的 less 相关的函数 <a href="https://github.com/arackaf/customize-cra#addlessloaderloaderoptions">addLessLoader</a> 来帮助加载 less 样式，同时修改 <code>config-overrides.js</code> 文件如下。</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">yarn add less less-loader -D</span></span><br></pre></td></tr></table></figure><figure class="highlight diff"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="deletion">- const &#123; override, fixBabelImports &#125; = require(&#x27;customize-cra&#x27;);</span></span><br><span class="line"><span class="addition">+ const &#123; override, fixBabelImports, addLessLoader &#125; = require(&#x27;customize-cra&#x27;);</span></span><br><span class="line"></span><br><span class="line">module.exports = override(</span><br><span class="line">  fixBabelImports(&#x27;import&#x27;, &#123;</span><br><span class="line">    libraryName: &#x27;antd&#x27;,</span><br><span class="line">    libraryDirectory: &#x27;es&#x27;,</span><br><span class="line"><span class="deletion">-   style: &#x27;css&#x27;,</span></span><br><span class="line"><span class="addition">+   style: true,</span></span><br><span class="line">  &#125;),</span><br><span class="line"><span class="addition">+ addLessLoader(&#123;</span></span><br><span class="line"><span class="addition">+   javascriptEnabled: true,</span></span><br><span class="line"><span class="addition">+   modifyVars: &#123; &#x27;@primary-color&#x27;: &#x27;#1DA57A&#x27; &#125;,</span></span><br><span class="line"><span class="addition">+ &#125;),</span></span><br><span class="line">);</span><br></pre></td></tr></table></figure><p>这里利用了 <a href="https://github.com/webpack/less-loader#less-options">less-loader</a> 的 <code>modifyVars</code> 来进行主题配置，变量和其他配置方式可以参考 <a href="https://ant.design/docs/react/customize-theme-cn">配置主题</a> 文档。</p><p>修改后重启 <code>yarn start</code>，如果看到一个绿色的按钮就说明配置成功了。</p><h3 id="使用-Day-js-替换-momentjs-优化打包大小-1"><a href="#使用-Day-js-替换-momentjs-优化打包大小-1" class="headerlink" title="使用 Day.js 替换 momentjs 优化打包大小"></a>使用 Day.js 替换 momentjs 优化打包大小</h3><figure class="highlight diff"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="addition">+ const AntdDayjsWebpackPlugin = require(&#x27;antd-dayjs-webpack-plugin&#x27;);</span></span><br><span class="line"><span class="deletion">- const &#123; override, fixBabelImports, addLessLoader &#125; = require(&#x27;customize-cra&#x27;);</span></span><br><span class="line"><span class="addition">+ const &#123; override, fixBabelImports, addLessLoader, addWebpackPlugin &#125; = require(&#x27;customize-cra&#x27;);</span></span><br><span class="line"></span><br><span class="line">module.exports = override(</span><br><span class="line">  fixBabelImports(&#x27;import&#x27;, &#123;</span><br><span class="line">    libraryName: &#x27;antd&#x27;,</span><br><span class="line">    libraryDirectory: &#x27;es&#x27;,</span><br><span class="line">    style: true,</span><br><span class="line">  &#125;),</span><br><span class="line">  addLessLoader(&#123;</span><br><span class="line">    javascriptEnabled: true,</span><br><span class="line">    modifyVars: &#123; &#x27;@primary-color&#x27;: &#x27;#1DA57A&#x27; &#125;,</span><br><span class="line">  &#125;),</span><br><span class="line"><span class="addition">+  addWebpackPlugin(new AntdDayjsWebpackPlugin()),</span></span><br><span class="line">);</span><br><span class="line"></span><br></pre></td></tr></table></figure><h3 id="decorators"><a href="#decorators" class="headerlink" title="decorators"></a>decorators</h3><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">yarn add -D @babel/plugin-proposal-decorators</span></span><br></pre></td></tr></table></figure><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> &#123; addDecoratorsLegacy &#125; = <span class="built_in">require</span>(<span class="string">&#x27;customize-cra&#x27;</span>);</span><br><span class="line"><span class="variable language_">module</span>.<span class="property">exports</span> = <span class="title function_">override</span>(</span><br><span class="line">...</span><br><span class="line">  <span class="title function_">addDecoratorsLegacy</span>(),</span><br><span class="line">  ...</span><br><span class="line">);</span><br></pre></td></tr></table></figure><h3 id="配置-Babel-插件"><a href="#配置-Babel-插件" class="headerlink" title="配置 Babel 插件"></a>配置 Babel 插件</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">module.exports = override(</span><br><span class="line">  ...,</span><br><span class="line">  ...addBabelPresets(</span><br><span class="line">    [</span><br><span class="line">      &quot;@babel/preset-env&quot;,</span><br><span class="line">      &#123;</span><br><span class="line">        targets: &#123;</span><br><span class="line">          browsers: [&quot;&gt; 1%&quot;, &quot;last 2 versions&quot;],</span><br><span class="line">          ie: 9</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><br><span class="line">);</span><br></pre></td></tr></table></figure><h3 id="允许使用-babelrc-js-文件进行Babel配置。"><a href="#允许使用-babelrc-js-文件进行Babel配置。" class="headerlink" title="允许使用 .babelrc.js 文件进行Babel配置。"></a>允许使用 .babelrc.js 文件进行Babel配置。</h3><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// config-overrides.js</span></span><br><span class="line"><span class="keyword">const</span> &#123; useBabelRc &#125; = <span class="built_in">require</span>(<span class="string">&#x27;customize-cra&#x27;</span>)</span><br><span class="line"></span><br><span class="line"><span class="variable language_">module</span>.<span class="property">exports</span> = <span class="title function_">override</span>(</span><br><span class="line">  ...</span><br><span class="line">  <span class="comment">// 允许使用 .babelrc.js 文件进行Babel配置。</span></span><br><span class="line">  <span class="title function_">useBabelRc</span>()</span><br><span class="line">  ...</span><br><span class="line">);</span><br><span class="line"></span><br></pre></td></tr></table></figure><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">yarn add @babel/preset-env -D</span></span><br></pre></td></tr></table></figure><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// .babelrc.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">presets</span>: [</span><br><span class="line">    [</span><br><span class="line">      <span class="string">&quot;@babel/preset-env&quot;</span>, <span class="comment">//兼容ie9</span></span><br><span class="line">      &#123;</span><br><span class="line">        <span class="attr">targets</span>: &#123;</span><br><span class="line">          <span class="attr">ie</span>: <span class="string">&quot;9&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><br><span class="line">  <span class="attr">plugins</span>: [<span class="string">&quot;@babel/plugin-proposal-decorators&quot;</span>] <span class="comment">// 可以用来替换 addDecoratorsLegacy</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/4f0002889f48/</id>
    <link href="https://blog.zisheng.pro/posts/4f0002889f48/"/>
    <published>2020-03-03T20:17:10.000Z</published>
    <summary>以笔者的经验来看，Ant Design 设计体系下的产品设计理念、使用方式、底层技术、周边工具都保持着高度一致，工具不是越多越好，有一套好用顺手的就行，UI 框架千千万，你不可能都学一遍。Ant Design 无疑能够减少你的学习成本。</summary>
    <title>Antd 快速上手教程</title>
    <updated>2026-09-18T08:15:28.632Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="Node.js" scheme="https://blog.zisheng.pro/tags/Node-js/"/>
    <category term="Nest.js" scheme="https://blog.zisheng.pro/tags/Nest-js/"/>
    <category term="server" scheme="https://blog.zisheng.pro/tags/server/"/>
    <content>
      <![CDATA[<h2 id="Nest-js-是什么？"><a href="#Nest-js-是什么？" class="headerlink" title="Nest.js 是什么？"></a>Nest.js 是什么？</h2><p>​Nest 是一个用于构建高效，可扩展的 <a href="http://nodejs.cn/">Node.js</a> 服务器端应用程序的框架。它使用渐进式 JavaScript，内置并完全支持 <a href="https://www.tslang.cn/">TypeScript</a>（但仍然允许开发人员使用纯 JavaScript 编写代码）并结合了 <code>OOP</code>（面向对象编程），<code>FP</code>（函数式编程）和<code> FRP</code>（函数式响应编程）的元素。</p><p>​在底层，Nest使用强大的 HTTP Server 框架，如 Express（默认）和 Fastify。Nest 在这些框架之上提供了一定程度的抽象，同时也将其 API 直接暴露给开发人员。这样可以轻松使用每个平台的无数第三方模块。</p><h2 id="Nest-js-解决了什么问题？"><a href="#Nest-js-解决了什么问题？" class="headerlink" title="Nest.js 解决了什么问题？"></a>Nest.js 解决了什么问题？</h2><p>​近年来，感谢 Node.js，JavaScript 已成为前端和后端应用程序的网络“通用语言”。这产生了令人敬畏的项目，如 <a href="https://angular.cn/">Angular</a>，React 和 Vue，它们提高了开发人员的工作效率，并能够构建快速，可测试和可扩展的前端应用程序。然而，虽然 Node（和服务器端 JavaScript ）存在大量优秀的库，帮助器和工具，但它们都没有有效地解决主要问题 - 架构。</p><p>​Nest 提供了一个开箱即用的应用程序架构，允许开发人员和团队创建高度可测试，可扩展，松散耦合且易于维护的应用程序。</p><h2 id="流行趋势"><a href="#流行趋势" class="headerlink" title="流行趋势"></a>流行趋势</h2><h3 id="npm下载量"><a href="#npm下载量" class="headerlink" title="npm下载量"></a>npm下载量</h3><img src="https://i.loli.net/2020/03/02/yFtbpaICvz9mlEJ.png" style="zoom:60%;" /><h3 id="star趋势"><a href="#star趋势" class="headerlink" title="star趋势"></a>star趋势</h3><img src="https://i.loli.net/2020/03/02/eC8Nitx2Y4QcKwL.png" style="zoom:60%;" /><h2 id="建立新项目"><a href="#建立新项目" class="headerlink" title="建立新项目"></a>建立新项目</h2><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">npm i -g @nestjs/cli</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">nest new MyNest</span></span><br></pre></td></tr></table></figure><img src="https://i.loli.net/2020/03/02/vyuW5OdrCiSHNPe.png" style="zoom: 67%;" /><h2 id="核心文件"><a href="#核心文件" class="headerlink" title="核心文件"></a>核心文件</h2><p>将创建 <code>project</code> 目录， 安装node模块和一些其他样板文件，并将创建一个 <code>src</code> 目录，目录中包含几个核心文件。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">src</span><br><span class="line">├── app.controller.ts // 带有单个路由的基本控制器示例。</span><br><span class="line">├── app.module.ts // 应用程序的根模块。</span><br><span class="line">└── main.ts // 应用程序入口文件。它使用 NestFactory 用来创建 Nest 应用实例。</span><br></pre></td></tr></table></figure><p><code>main.ts</code> 包含一个异步函数，它负责<strong>引导</strong>我们的应用程序：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">NestFactory</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;@nestjs/core&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">ApplicationModule</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;./app.module&#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_">bootstrap</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> app = <span class="keyword">await</span> <span class="title class_">NestFactory</span>.<span class="title function_">create</span>(<span class="title class_">ApplicationModule</span>);</span><br><span class="line">  <span class="keyword">await</span> app.<span class="title function_">listen</span>(<span class="number">3000</span>);</span><br><span class="line">&#125;</span><br><span class="line"><span class="title function_">bootstrap</span>();</span><br></pre></td></tr></table></figure><p>要创建一个 Nest 应用程序，我们使用了 <code>NestFactory</code> 核心类。<code>NestFactory</code> 暴露了一些静态方法用于创建应用实例。<code>create()</code> 方法返回一个实现 <code>INestApplication</code> 接口的对象，并提供一组可用的方法。在上面的 <code>main.ts</code> 示例中，我们只是启动了 HTTP 服务器，它允许应用程序等待入站 HTTP 请求。</p><h2 id="运行应用程序"><a href="#运行应用程序" class="headerlink" title="运行应用程序"></a>运行应用程序</h2><p>安装过程完成后，您可以在系统命令提示符下运行以下命令，以启动应用程序监听入站 HTTP 请求：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">npm run start</span></span><br></pre></td></tr></table></figure><p>此命令在 <code>src</code> 目录中的 <code>main.ts</code> 文件中定义的端口上启动 HTTP 服务器。在应用程序运行时, 打开浏览器并访问 <code>http://localhost:3000/</code>。 你应该看到 <code>Hello world!</code> 信息。</p>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/f9e39267a7ed/</id>
    <link href="https://blog.zisheng.pro/posts/f9e39267a7ed/"/>
    <published>2020-03-01T20:05:36.000Z</published>
    <summary>从 Nest CLI 创建项目开始，梳理 Nest.js 的模块、控制器和应用启动结构，帮助熟悉 TypeScript 后端框架的基础组织方式。</summary>
    <title>Nest.js 快速上手</title>
    <updated>2026-09-18T08:15:28.629Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="React Native" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/React-Native/"/>
    <category term="React Native" scheme="https://blog.zisheng.pro/tags/React-Native/"/>
    <category term="掘金" scheme="https://blog.zisheng.pro/tags/%E6%8E%98%E9%87%91/"/>
    <content>
      <![CDATA[<ol><li><p>下载<a href="http://hao.ziticq.com/">免费商用字体</a></p><blockquote><p>法棍体：链接:<a href="https://pan.baidu.com/s/1M-CDEqsJ06qj-42uRyF0WA">https://pan.baidu.com/s/1M-CDEqsJ06qj-42uRyF0WA</a>  密码:83er</p></blockquote></li><li><p>使用 FontLab 查看字体的 <code>fontFamily</code></p><blockquote><p>FontLab 破解版 Mac：</p><p>链接:<a href="https://pan.baidu.com/s/1Wb3xgB9mJqF4YGX0xXddMg">https://pan.baidu.com/s/1Wb3xgB9mJqF4YGX0xXddMg</a>  密码:1cfb</p></blockquote><img src="https://i.loli.net/2020/03/01/m9A5CK2cZMYswBF.png"  style="zoom:40%;" /></li><li><p>将字体放到放在项目根目录：</p></li><li><p>在根目录新建 <code>react-naitve.config.js</code>:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="variable language_">module</span>.<span class="property">exports</span> = &#123;</span><br><span class="line">  <span class="attr">project</span>: &#123;</span><br><span class="line">    <span class="attr">ios</span>: &#123;&#125;,</span><br><span class="line">    <span class="attr">android</span>: &#123;&#125;,</span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="attr">assets</span>: [<span class="string">&#x27;./fonts&#x27;</span>],</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></li><li><p>在根目录执行 <code>react-native link</code> 命令</p></li><li><p>使用字体：</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 全局</span></span><br><span class="line"><span class="keyword">const</span> <span class="title class_">TextRender</span> = <span class="title class_">Text</span>.<span class="property">render</span></span><br><span class="line"><span class="title class_">Text</span>.<span class="property">render</span> = <span class="function">(<span class="params">...args</span>) =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">const</span> originText = <span class="title class_">TextRender</span>.<span class="title function_">apply</span>(<span class="variable language_">this</span>, args)</span><br><span class="line">  <span class="keyword">const</span> &#123; style &#125; = originText.<span class="property">props</span></span><br><span class="line">  <span class="keyword">return</span> <span class="title class_">React</span>.<span class="title function_">cloneElement</span>(originText, &#123;</span><br><span class="line">    <span class="attr">allowFontScaling</span>: <span class="literal">false</span>,</span><br><span class="line">    <span class="attr">style</span>: [&#123; <span class="attr">fontFamily</span>: <span class="string">&#x27;Baguette&#x27;</span> &#125;, style],</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">&lt;<span class="title class_">Text</span> style=&#123;&#123; <span class="attr">fontSize</span>: <span class="number">50</span>, <span class="attr">fontFamily</span>: <span class="string">&#x27;Baguette&#x27;</span> &#125;&#125;&gt; 法棍体🥖 &lt;/<span class="title class_">Text</span>&gt;</span><br></pre></td></tr></table></figure></li><li><p>效果：</p><img src="https://i.loli.net/2020/03/01/6YZlpi3ar87LKQd.jpg" alt="WechatIMG1166.jpeg" style="zoom:50%;" /></li></ol><h2 id="附录"><a href="#附录" class="headerlink" title="附录"></a>附录</h2><ul><li><a href="https://github.com/react-native-training/react-native-fonts">React Native 中开箱即用的字体</a></li></ul>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/78713277e253/</id>
    <link href="https://blog.zisheng.pro/posts/78713277e253/"/>
    <published>2020-02-29T08:52:39.000Z</published>
    <summary>本文主要讲解了如何在 React Native 中使用自定义字体，以及如何在 iOS 和 Android 中使用自定义字体。</summary>
    <title>玩转 React Native 自定义字体</title>
    <updated>2026-09-18T08:15:28.640Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="React Native" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/React-Native/"/>
    <category term="React Native" scheme="https://blog.zisheng.pro/tags/React-Native/"/>
    <category term="架构" scheme="https://blog.zisheng.pro/tags/%E6%9E%B6%E6%9E%84/"/>
    <category term="最佳实践" scheme="https://blog.zisheng.pro/tags/%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5/"/>
    <content>
      <![CDATA[<p><img src="https://i.loli.net/2020/02/25/dLMXty7iYKnVk35.png" alt="React Native 集成 CodePush 指南"></p><p>目前现存的热更新方案有腾讯的 <a href="https://bugly.qq.com/v2/products/upgrade">Bugly 应用升级</a>、React Native 中文网的 <a href="https://update.reactnative.cn/home">Pushy</a>、微软的 <a href="https://bre.is/r3Y9hJvB">CodePush</a> 和用来搭建私服的 <a href="https://github.com/lisong/code-push-server">code-push-server</a>。</p><p>本文分享的是基于微软 AppCenter 的 CodePush 服务实现热更新，这个比较有代表性，也方便各位读者大大实践。当然鉴于国内的网络环境，后期会发布一篇如何基于 <code>code-push-server</code> 实现热更新功能。</p><h2 id="环境"><a href="#环境" class="headerlink" title="环境"></a>环境</h2><ul><li>Xcode：Version 11.3.1 (11C504)</li><li>react-native：0.61.5</li><li>react-native-code-push: 6.1.0</li><li>appcenter-cli：2.3.3</li></ul><h2 id="CodePush介绍"><a href="#CodePush介绍" class="headerlink" title="CodePush介绍"></a>CodePush介绍</h2><p>CodePush 是一个 App Center 云服务，使 Apache Cordova 和 React Native 开发人员可以将移动应用程序更新直接部署到其用户的设备上。它充当中央存储库的角色，开发人员可以将某些更新（例如JS，HTML，CSS和图像更改）发布到该存储库，并且应用程序可以（使用提供的客户端SDK）从中查询更新。这使你可以与最终用户建立更具确定性和直接的参与度模型，同时解决错误和&#x2F;或添加一些小的功能，这些功能不需要你重建二进制文件和&#x2F;或通过任何公共应用商店重新分发二进制文件。默认情况下，在 App Center 上创建的所有 React Native 应用程序都启用了 CodePush。</p><blockquote><p>注意：对于Android设备，CodePush仅在兼容TLS 1.2的设备上运行</p></blockquote><h3 id="1-安装-App-Center-CLI"><a href="#1-安装-App-Center-CLI" class="headerlink" title="1.安装 App Center CLI"></a>1.安装 App Center CLI</h3><p>你可以使用 App Center CLI 管理 CodePush 的大多数功能。要安装 CLI，请打开终端窗口或命令提示符并执行以下命令：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm install -g appcenter-cli</span><br></pre></td></tr></table></figure><p>成功安装 App Center CLI 后，执行<code>appcenter login</code>命令为你的 App Center 帐户详细信息配置 CLI：</p><p><img src="https://i.loli.net/2020/02/26/TwJyWsBOK5trHmj.png" alt="React Native 集成 CodePush 指南"></p><h3 id="2-应用管理"><a href="#2-应用管理" class="headerlink" title="2.应用管理"></a>2.应用管理</h3><p>部署更新之前，必须使用以下命令使用 App Center 创建应用：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">appcenter apps create -d &lt;appDisplayName&gt; -o &lt;operatingSystem&gt; -p &lt;platform&gt;</span><br></pre></td></tr></table></figure><p>如果您的应用同时针对 Android 和 iOS，我们强烈建议您使用 CodePush 创建单独的应用。每个平台一个。这样，您可以分别管理和发布更新，从长远来看，这会使事情变得更简单。大多数人只是在应用名称后缀<code>-Android</code>和<code>-iOS</code>。例如：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">appcenter apps create -d MyApp-Android -o Android -p React-Native</span><br><span class="line">appcenter apps create -d MyApp-iOS -o iOS -p React-Native</span><br></pre></td></tr></table></figure><blockquote><p>注意：在 Android 和 iOS 上使用相同的应用程序可能会导致安装异常，因为为 iOS 生成的 CodePush 更新包将具有与为 Android 生成的更新不同的内容。</p></blockquote><blockquote><p>通过 <code>appcenter apps list</code> 可以查看所有的应用。</p></blockquote><blockquote><p>在App中心CLI的一个重要的新功能是设置一个应用程序的能力<strong>当前应用程序</strong>使用<code>appcenter apps set-current &lt;ownerName&gt;/&lt;appName&gt;</code>。通过将一个应用程序设置为当前应用程序，您无需<code>-a</code>在其他CLI命令中使用该标志。例如，<code>appcenter codepush deployment list -a &lt;ownerName&gt;/&lt;appName&gt;</code>可以将命令缩短<code>appcenter codepush deployment list</code>为设置当前应用程序的时间。您可以使用来检查哪个应用程序被设置为您帐户的当前应用程序<code>appcenter apps get-current</code>。设置当前应用程序可以缩短大多数CLI命令的键入时间。</p></blockquote><p>使用 <code>code-push-cli</code>，应用程序会自动进行两次部署（<code>Staging</code>和 <code>Production</code>）。在 App Center 中，你必须使用以下命令自行创建它们：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">appcenter codepush deployment add -a &lt;ownerName&gt;/&lt;appName&gt; Staging</span><br><span class="line">appcenter codepush deployment add -a &lt;ownerName&gt;/&lt;appName&gt; Production</span><br></pre></td></tr></table></figure><p>创建部署后，您可以使用来访问两个部署的部署密钥<code>appcenter codepush deployment list --displayKeys</code>，您可以开始通过它们各自的SDK（用于<a href="https://docs.microsoft.com/en-us/appcenter/distribution/codepush/cordova">Cordova</a>和<a href="https://docs.microsoft.com/en-us/appcenter/distribution/codepush/react-native">React Native的</a>详细信息）来配置移动客户端。</p><h3 id="3-修改versionNam"><a href="#3-修改versionNam" class="headerlink" title="3.修改versionNam"></a>3.修改versionNam</h3><p>在 <code> android/app/build.gradle</code> 中有个 <code>android.defaultConfig.versionName</code> 属性（在 <code>ios/**/Info.plist</code> 是 <code>&lt;key&gt;CFBundleShortVersionString&lt;/key&gt;</code> 属性 ）；我们需要把应用版本改成 <code>1.0.0</code>（默认<code>1.0</code>，但是 <code>codepush</code> 需要三位数）</p><h3 id="3-发布应用更新"><a href="#3-发布应用更新" class="headerlink" title="3.发布应用更新"></a>3.发布应用更新</h3><p>更改应用程序的代码或资产后，请按照以下说明使用 App Center CLI 将更新发布到App Center。</p><p>执行 App Center CLI <code>release-react </code> 命令以捆绑应用程序的代码和资产文件，然后将它们作为新版本发布到 App Center 服务器。例如：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">appcenter codepush release-react -a &lt;ownerName&gt;/&lt;appName&gt; -d Staging -t 1.0.0 -m  --development false --description &lt;description&gt;</span><br></pre></td></tr></table></figure><ul><li><code>[-a|--app &lt;ownerName&gt;/&lt;appName&gt;]</code>:  指定应用</li><li><code>[-d|--deployment-name &lt;deploymentName&gt;</code>]:  此参数指定要将更新发布到的部署。它默认为<code>Staging</code>，但是当您准备部署到<code>Production</code>或您自己的自定义部署之一时，只需显式设置此参数即可。</li><li><code>[-t|--target-binary-version &lt;targetBinaryVersion&gt;]</code>:  指定要更新的应用的原生版本</li><li><code>[-m|--mandatory]</code>:  是强制更新，默认 <code>false</code></li><li><code>[--development]</code>: 此参数指定是否生成未缩小的开发JS包。如果未指定，则默认为<code>false</code>禁用警告并缩小包的位置。</li><li><code>[--description &lt;description&gt;</code>]:  此参数为部署提供了可选的“更改日志”。该值将往返传送给客户端，以便在检测到更新时，您的应用可以选择将其显示给最终用户（例如，通过“新功能”对话框）。该字符串接受诸如<code>\n</code>和的控制字符，<code>\t</code>因此您可以在描述中包括空格格式，以提高可读性。</li></ul><blockquote><p>CodePush客户端支持差异更新，因此，即使您在每次更新中释放JS捆绑包和资产，最终用户也只会实际下载他们需要的文件。该服务会自动处理此问题，因此您可以专注于创建出色的应用程序，而我们会担心优化最终用户的下载。</p></blockquote><h2 id="React-Native-Client-SDK"><a href="#React-Native-Client-SDK" class="headerlink" title="React Native Client SDK"></a>React Native Client SDK</h2><p>该插件为 CodePush 服务提供了客户端集成，使你可以轻松地向你的 React Native 应用添加动态更新体验。</p><blockquote><p>注意：以下配置均基于 react-native 0.60 版本。</p></blockquote><h3 id="它是如何工作的？"><a href="#它是如何工作的？" class="headerlink" title="它是如何工作的？"></a>它是如何工作的？</h3><p>React Native 应用程序由 JavaScript 文件和任何相关的图片组成，它们由打包程序 <a href="https://facebook.github.io/metro/">metro</a> 捆绑在一起, 并作为特定于平台的二进制文件（<code>.ipa</code> 或 <code>.apk</code> 文件）的一部分进行分发。发行该应用程序时，更新 JavaScript 代码（例如进行错误修复，添加新功能）或更新图片资源要求你重新编译并重新分发整个二进制文件，其中包括与商店相关的所有时间。</p><p>通过使你的 JavaScript 和图片资源与您发布到 CodePush 服务器的更新同步，CodePush 插件可帮助你立即在最终用户面前获得产品改进。</p><p>为了确保您的最终用户始终拥有你的应用程序的正常运行版本，CodePush 插件会维护先前更新的副本，因此，如果您不小心推送了包含崩溃的更新，它可以自动回滚。这样，你可以放心，新发现的发行版不会导致用户被阻塞。</p><blockquote><p>注意：任何涉及本机代码的产品更改（例如，修改<code>AppDelegate.m</code>&#x2F;<code>MainActivity.java</code>、添加 <code>ttf</code> 或添加原生插件）都无法通过 CodePush 分发，因此必须通过相应的商店进行更新。</p></blockquote><h3 id="支持的React-Native平台"><a href="#支持的React-Native平台" class="headerlink" title="支持的React Native平台"></a>支持的React Native平台</h3><ul><li>iOS（7以上）</li><li>Android（4.1以上）</li><li>Windows（UWP）</li></ul><h3 id="安装-react-native-code-push"><a href="#安装-react-native-code-push" class="headerlink" title="安装 react-native-code-push"></a>安装 react-native-code-push</h3><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">yarn add react-native-code-push</span><br></pre></td></tr></table></figure><p>与其他所有React Native插件一样，iOS 和 Android 的集成体验也有所不同，因此请根据您的应用目标平台执行以下设置步骤。请注意，如果您同时针对两个平台，建议为每个平台创建单独的 CodePush 应用程序。</p><blockquote><p>本指南假定您已使用该<code>react-native init</code>命令初始化React Native项目。</p></blockquote><h3 id="iOS设置"><a href="#iOS设置" class="headerlink" title="iOS设置"></a>iOS设置</h3><p>获得 CodePush 插件后，必须将其集成到 React Native 应用程序的 Xcode 项目中并正确配置。</p><ol><li><p>运行<code>cd ios &amp;&amp; pod install &amp;&amp; cd ..</code>以安装所有必需的CocoaPods依赖项。</p></li><li><p>打开<code>AppDelegate.m</code>文件，并为CodePush标头添加导入语句：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">#import &lt;CodePush/CodePush.h&gt;</span><br></pre></td></tr></table></figure></li><li><p>查找以下代码行，该代码为生产版本的网桥设置源URL：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">return [[NSBundle mainBundle] URLForResource:@&quot;main&quot; withExtension:@&quot;jsbundle&quot;];</span><br></pre></td></tr></table></figure></li><li><p>用以下行替换它：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">return [CodePush bundleURL];</span><br></pre></td></tr></table></figure><p>此更改将你的应用配置为始终加载应用JS捆绑包的最新版本。在首次启动时，这将与使用该应用程序编译的文件相对应。但是，在通过CodePush推送更新后，这将返回最近安装的更新的位置。</p><p>通常，您只想使用 CodePush 来解决发行版本中的 JS 包位置，因此，我们建议使用<code>DEBUG</code>预处理器宏在是否使用打包程序服务器和 CodePush 之间进行动态切换。这样可以更轻松地确保您在生产中获得所需的正确行为，同时仍可以在调试时使用Chrome开发工具，实时重新加载等。</p><p>你的<code>sourceURLForBridge</code>方法应如下所示：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge</span><br><span class="line">&#123;</span><br><span class="line">  #if DEBUG</span><br><span class="line">    return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@&quot;index&quot; fallbackResource:nil];</span><br><span class="line">  #else</span><br><span class="line">    return [CodePush bundleURL];</span><br><span class="line">  #endif</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></li><li><p>将部署密钥添加到<code>Info.plist</code>：</p><p>为了让 CodePush 运行时知道应该针对哪个部署查询更新，请打开你的应用的 <code>Info.plist</code> 文件，并添加一个名为<code>CodePushDeploymentKey</code>的新条目，其值是你要配置的应用的 <code>Staging Deployment Key</code>。</p><p>你可以通过 <code>appcenter codepush deployment list -k</code> 来检索这个值（该 <code>-k</code> 标志是必需的，因为默认情况下不会显示键），然后复制相对应的 <code>Deployment Key</code> 即可。</p><p><img src="https://cloud.githubusercontent.com/assets/116461/11601733/13011d5e-9a8a-11e5-9ce2-b100498ffb34.png" alt="React Native 集成 CodePush 指南"></p><p>为了有效利用与 CodePush 应用程序一起创建的 <code>Staging</code> 和 <code>Production</code> 部署，请在实际将你的应用程序对 CodePush 的使用移入生产环境之前，进行<a href="#%E5%A4%9A%E9%83%A8%E7%BD%B2%E6%B5%8B%E8%AF%95">多部署测试</a>的配置。</p><blockquote><p>如果您需要动态使用其他部署，还可以使用<a href="#%E5%8A%A8%E6%80%81%E9%83%A8%E7%BD%B2%E5%88%86%E9%85%8D">动态部署分配</a>在JS代码中覆盖部署密钥</p></blockquote></li></ol><h3 id="Android设置"><a href="#Android设置" class="headerlink" title="Android设置"></a>Android设置</h3><p>为了将CodePush集成到您的Android项目中，请执行以下步骤：</p><ol><li><p>在<code>android/app/build.gradle</code>文件中，将文件<code>codepush.gradle</code>添加为下面的其他构建任务定义：</p><figure class="highlight groovy"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">...</span><br><span class="line">apply <span class="attr">from:</span> <span class="string">&quot;../../node_modules/react-native-code-push/android/codepush.gradle&quot;</span></span><br><span class="line">...</span><br></pre></td></tr></table></figure></li><li><p><code>MainApplication.java</code>通过以下更改更新文件以使用 CodePush：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">...</span><br><span class="line"><span class="comment">// 1. 导入插件的类</span></span><br><span class="line"><span class="keyword">import</span> com.microsoft.codepush.react.CodePush;</span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">MainApplication</span> <span class="keyword">extends</span> <span class="title class_">Application</span> <span class="keyword">implements</span> <span class="title class_">ReactApplication</span> &#123;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> <span class="type">ReactNativeHost</span> <span class="variable">mReactNativeHost</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">ReactNativeHost</span>(<span class="built_in">this</span>) &#123;</span><br><span class="line">        ...</span><br><span class="line">        <span class="comment">// 2. 重写 getJSBundleFile 方法，每次 app 启动的时候让 CodePush 运行时决定从哪里加载 JS bundle</span></span><br><span class="line">        <span class="meta">@Override</span></span><br><span class="line">        <span class="keyword">protected</span> String <span class="title function_">getJSBundleFile</span><span class="params">()</span> &#123;</span><br><span class="line">            <span class="keyword">return</span> CodePush.getJSBundleFile();</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></li><li><p>将部署密钥添加到<code>strings.xml</code>：</p><p>为了让 CodePush 运行时知道它应该查询哪些部署更新，请打开您的应用程序的 <code>string.xml</code> 文件，并添加一个名为 <code>CodePushDeploymentKey</code> 的新字符串，它的值是应用的 <code>Staging</code> 部署。你可以通过 <code>appcenter deployment list &lt;ownerName&gt;/&lt;appName&gt; -k</code> 获取该值。</p><p><img src="https://cloud.githubusercontent.com/assets/116461/11601733/13011d5e-9a8a-11e5-9ce2-b100498ffb34.png" alt="React Native 集成 CodePush 指南"></p><p>您<code>strings.xml</code>应该看起来像这样：</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">resources</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">string</span> <span class="attr">name</span>=<span class="string">&quot;app_name&quot;</span>&gt;</span>AppName<span class="tag">&lt;/<span class="name">string</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">string</span> <span class="attr">moduleConfig</span>=<span class="string">&quot;true&quot;</span> <span class="attr">name</span>=<span class="string">&quot;CodePushDeploymentKey&quot;</span>&gt;</span>DeploymentKey<span class="tag">&lt;/<span class="name">string</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">resources</span>&gt;</span></span><br></pre></td></tr></table></figure><p>为了有效利用与CodePush应用程序一起创建的<code>Staging</code>和<code>Production</code>部署，请在实际将您的应用程序对CodePush的使用移入生产环境之前，请参考下面的<a href="#%E5%A4%9A%E9%83%A8%E7%BD%B2%E6%B5%8B%E8%AF%95">多部署测试</a>文档。</p><blockquote><p>如果您需要动态使用其他部署，还可以使用<a href="#%E5%8A%A8%E6%80%81%E9%83%A8%E7%BD%B2%E5%88%86%E9%85%8D">动态部署分配</a>在JS代码中覆盖部署密钥</p></blockquote></li></ol><h3 id="使用插件"><a href="#使用插件" class="headerlink" title="使用插件"></a>使用插件</h3><p>下载并链接了 CodePush 插件，并且为你的应用程序询问 CodePush 从何处获取正确的 JS bundle 包后，剩下的唯一一件事就是向你的应用程序添加必要的代码，以控制以下策略：</p><ol><li>什么时候（多久）检查一次更新？（例如，应用程序启动，在设置页面中单击按钮或按固定时间间隔定期进行）</li><li>当有可用更新时，如何将其呈现给最终用户？</li></ol><p>最简单的方式是 <code>CodePush-ify</code> 应用程序的根组件。为此，您可以选择一下两个选项之一：</p><ul><li><p>选项1：将您的根组件与 codePush 高阶组件包装在一起：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> codePush <span class="keyword">from</span> <span class="string">&#x27;react-native-code-push&#x27;</span></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">App</span> = (<span class="params"></span>) =&gt; &#123;&#125;</span><br><span class="line"><span class="title class_">App</span> = <span class="title function_">codePush</span>(<span class="title class_">App</span>)</span><br></pre></td></tr></table></figure></li><li><p>选项2：使用 <a href="https://github.com/wycats/javascript-decorators">ES7装饰器 </a>语法：</p><blockquote><p>Babel 6.x 尚不支持装饰器。您可能需要通过安装和使用 <a href="https://github.com/skevy/babel-preset-react-native-stage-0#babel-preset-react-native-stage-0">babel-preset-react-native-stage-0</a> 来启用装饰器。</p></blockquote><blockquote><p>Babel 7.x 支持装饰器语法。你可以使用 <a href="https://babeljs.io/docs/en/next/babel-plugin-proposal-decorators.html">@babel&#x2F;plugin-proposal-decorators</a> 来启用装饰器。</p></blockquote><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> codePush <span class="keyword">from</span> <span class="string">&quot;react-native-code-push&quot;</span></span><br><span class="line"></span><br><span class="line">@codePush</span><br><span class="line"><span class="keyword">class</span> <span class="title class_">MyApp</span> <span class="keyword">extends</span> <span class="title class_ inherited__">Component</span> &#123;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></li></ul><p>默认情况下，CodePush 将在每次启动应用程序时检查更新。如果有可用更新，它将在下一次重新启动应用程序时（由最终用户或操作系统明确显示）以静默方式下载并安装，从而确保最终用户获得最少的侵入性体验。如果必须使用可用的更新，则将立即安装该更新，以确保最终用户尽快获得它。</p><p>如果您希望应用程序更快地发现更新，则还可以选择每次应用程序从后台恢复时与 CodePush 服务器同步。</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> codePushOptions = &#123; <span class="attr">checkFrequency</span>: codePush.<span class="property">CheckFrequency</span>.<span class="property">ON_APP_RESUME</span> &#125;</span><br><span class="line"><span class="keyword">const</span> <span class="title function_">App</span> = (<span class="params"></span>) =&gt; &#123;&#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_">codePush</span>(codePushOptios)(<span class="title class_">App</span>)</span><br></pre></td></tr></table></figure><p>另外，如果您想对检查发生的时间进行细粒度的控制（例如按按钮或定时器间隔），则可以使用 <a href="https://bre.is/dPuwHWre">CodePush.sync()</a> 随时进行调用，还可以通过 <code>SyncOptions</code> 通过 <code>CheckFrequency.MANUAL</code> 来关闭 CodePush 的自动检查功能:</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">React</span> <span class="keyword">from</span> <span class="string">&#x27;react&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123;<span class="title class_">View</span>, <span class="title class_">StyleSheet</span>&#125; <span class="keyword">from</span> <span class="string">&#x27;react-native&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> codePush <span class="keyword">from</span> <span class="string">&#x27;react-native-code-push&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> <span class="title class_">AwesomeButton</span> <span class="keyword">from</span> <span class="string">&#x27;react-native-really-awesome-button&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> codePushOptions = &#123; <span class="attr">checkFrequency</span>: codePush.<span class="property">CheckFrequency</span>.<span class="property">MANUAL</span> &#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">App</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> <span class="title function_">checkForUpdate</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">    codePush.<span class="title function_">sync</span>(&#123;</span><br><span class="line">      <span class="attr">updateDialog</span>: <span class="literal">true</span>,</span><br><span class="line">      <span class="attr">installMode</span>: codePush.<span class="property">InstallMode</span>.<span class="property">IMMEDIATE</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> <span class="title function_">clear</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">    codePush.<span class="title function_">clearUpdates</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">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">AwesomeButton</span> <span class="attr">type</span>=<span class="string">&quot;secondary&quot;</span> <span class="attr">onPress</span>=<span class="string">&#123;checkForUpdate&#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">AwesomeButton</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">AwesomeButton</span> <span class="attr">type</span>=<span class="string">&quot;secondary&quot;</span> <span class="attr">onPress</span>=<span class="string">&#123;clear&#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">AwesomeButton</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">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">    <span class="attr">alignItems</span>: <span class="string">&#x27;center&#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="comment">// 注意：这是可选的，完全可以不使用 codePush 这里包装</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="title function_">codePush</span>(codePushOptions)(<span class="title class_">App</span>);</span><br></pre></td></tr></table></figure><p>如果你想要显示一个更新确认弹窗（一个主动安装）。配置何时安装可用更新（例如强制立即重启）或以任何其他方式自定义更新体验，请参阅 <a href="https://docs.microsoft.com/en-us/appcenter/distribution/codepush/react-native#api-reference">codepush</a> API参考以获取有关一下信息：如何调整此默认行为。</p><h3 id="应用商店规则"><a href="#应用商店规则" class="headerlink" title="应用商店规则"></a>应用商店规则</h3><ul><li><p>苹果App允许使用热更新<a href="https://developer.apple.com/programs/ios/information/iOS_Program_Information_4_3_15.pdf">Apple’s developer agreement</a>, 为了不影响用户体验，规定必须使用静默更新。</p></li><li><p>Google Play不能使用静默更新，必须弹框告知用户App有更新。</p></li><li><p>中国的android市场必须采用静默更新（如果弹框提示，App会被“请上传最新版本的二进制应用包”原因驳回）。</p></li></ul><h2 id="多部署测试"><a href="#多部署测试" class="headerlink" title="多部署测试"></a>多部署测试</h2><p>在入门文档中，我们说明了如何使用特定的部署密钥配置 CodePush 插件。但是，为了有效地测试发型版，至关重要的是，在首次创建 CodePush 应用程序（或你可能已经创建的任何自定义部署）时，利用我们建议进行的 <code>Staging</code> 和 <code>Production</code> 部署。</p><blockquote><p>我们的客户端回滚功能可以帮助您在安装导致崩溃的版本后解除对用户的阻止，服务器端的回滚（例如<code>appcenter codepush rollback</code>）使您可以防止其他用户在发现错误的版本后再安装它。但是，如果可以从一开始就防止广泛发布错误更新，那显然更好。</p></blockquote><p>利用<code>Staging</code>和<code>Production</code>部署，您可以实现类似于以下的工作流程（随意定制！）：</p><ol><li><code>Staging</code> 使用 <code>appcenter codepush release-react</code> 命令将 CodePush 更新发布到您的部署中（如果你需要更多的控制权可以使用 <code>appcenter codepush release</code> ）</li><li>构建应用程序的 staging<code>/</code>beta&#96; 版本，从服务器同步更新，并验证其是否按预期工作</li><li>使用以下命令将测试的发行版从 <code>Staging</code> 升级到 <code>Prouction</code>: <code>appcenter codepush promote -a &lt;ownerName&gt;/&lt;appName&gt; -s Staging -d Production</code></li><li>构建应用程序的 <code>production</code>&#x2F;<code>release</code>，从服务其同步更新并验证其是否按预期工作</li></ol><blockquote><p>如果您想采取更为谨慎的方法，甚至可以选择在“＃3”中执行 <strong>分阶段推出</strong>，这使您可以减轻更新带来的额外潜在风险（例如，＃2中的测试是否接触了所有可能的设备），仅使一定比例的用户可以使用生产更新（例如<code>code-push promote -a / -s Staging -d Production -r 20%</code>）。然后，在等待了一段合理的时间以查看是否有崩溃报告或客户反馈后，您可以通过运行将其扩展到整个受众<code>appcenter codepush patch -a / Production -r 100%</code>。</p></blockquote><h3 id="安卓系统"><a href="#安卓系统" class="headerlink" title="安卓系统"></a>安卓系统</h3><p>在<a href="https://google.github.io/android-gradle-dsl/current/index.html">Android Gradle plugin </a>允许您定义自定义配置设置，每个“构建类型”（如调试，发布）。此机制使您可以轻松地使用 CodePush 部署密钥配置调试版本，而发行版本也可以配置为使用 CodePush 生产部署密钥。</p><p>提醒一下，您可以通过<code>appcenter codepush deployment list  -k</code>从终端运行来检索这些键。</p><p>要进行设置，请执行以下步骤：</p><ol><li><p>打开项目的应用程序级别<code>build.gradle</code>文件（例如标准 React Native 项目中的 <code>android/app/build.gradle</code>）</p></li><li><p>查找此<code>android { buildTypes {} }</code>部分，并<code>resValue</code>为您<code>debug</code>和<code>release</code>构建类型定义条目，分别引用您的密钥<code>Staging</code>和<code>Production</code>部署密钥。</p> <figure class="highlight groovy"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line">android &#123;</span><br><span class="line">  ...</span><br><span class="line">  buildTypes &#123;</span><br><span class="line">    debug &#123;</span><br><span class="line">      signingConfig signingConfigs.debug</span><br><span class="line">      <span class="comment">// Note: CodePush updates should not be tested in Debug mode as they are overriden by the RN packager. However, because CodePush checks for updates in all modes, we must supply a key.</span></span><br><span class="line">      resValue <span class="string">&quot;string&quot;</span>, <span class="string">&quot;CodePushDeploymentKey&quot;</span>, <span class="string">&#x27;&quot;&quot;&#x27;</span></span><br><span class="line">    &#125;</span><br><span class="line">    release &#123;</span><br><span class="line">      <span class="comment">// Caution! In production, you need to generate your own keystore file.</span></span><br><span class="line">      <span class="comment">// see https://facebook.github.io/react-native/docs/signed-apk-android.</span></span><br><span class="line">      signingConfig signingConfigs.release</span><br><span class="line">      minifyEnabled enableProguardInReleaseBuilds</span><br><span class="line">      proguardFiles getDefaultProguardFile(<span class="string">&quot;proguard-android.txt&quot;</span>), <span class="string">&quot;proguard-rules.pro&quot;</span></span><br><span class="line">      resValue <span class="string">&quot;string&quot;</span>, <span class="string">&quot;CodePushDeploymentKey&quot;</span>, <span class="string">&#x27;&quot;&quot;&#x27;</span></span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// <span class="doctag">NOTE:</span> The naming convention for releaseStaging is significant due to http://t.cn/EAnyAzi</span></span><br><span class="line">    releaseStaging.initWith(release)</span><br><span class="line">    releaseStaging &#123;</span><br><span class="line">        resValue <span class="string">&quot;string&quot;</span>, <span class="string">&quot;CodePushDeploymentKey&quot;</span>, <span class="string">&#x27;&quot;&quot;&#x27;</span></span><br><span class="line">        <span class="comment">// Note: It is a good idea to provide matchingFallbacks for the new buildType you create to prevent build issues（http://t.cn/EAex4XH）</span></span><br><span class="line">        <span class="comment">// Add the following line if not already there</span></span><br><span class="line">        matchingFallbacks = [<span class="string">&#x27;release&#x27;</span>]</span><br><span class="line">    &#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></li></ol><blockquote><p>如果要在构建过程中配置部署密钥，请记住从<code>strings.xml</code>中删除密钥。</p></blockquote><blockquote><p><code>releaseStaging</code>由于<a href="https://github.com/facebook/react-native/blob/e083f9a139b3f8c5552528f8f8018529ef3193b9/react.gradle#L79">此行</a>，的命名约定，这不能改。</p></blockquote><h3 id="iOS"><a href="#iOS" class="headerlink" title="iOS"></a>iOS</h3><blockquote><p>该部分适用于 Xcode 11</p></blockquote><p>Xcode 允许你为每个<strong>配置</strong> (如 <code>debug</code>, <code>release</code>) 自定义构建设置，然后可以将其引用为 <code>Info.plist</code> 文件中的键值（如 <code>CodePushDeploymentKey</code> 设置）。此机制是您可以轻松地进行构建配置以生成二进制文件，这些二进制文件被配置为与不同的 CodePush 部署同步。</p><p>要进行设置，请执行以下步骤：</p><ol><li><p>打开您的 Xcode 项目，然后在 <code>Project navigator</code> 窗口中选择您的项目</p></li><li><p>确保已选择 <code>PROJECT</code> 节点，而不是 <code>TARGETS</code></p></li><li><p>选择 <code>Info</code> 标签</p></li><li><p>点击 <code>+</code> 的内部按钮<code>Duplicate &quot;Release&quot; Configuration</code><br>  <img src="https://docs.microsoft.com/en-us/appcenter/distribution/codepush/images/rn-ios-8.png" alt="React Native 集成 CodePush 指南"></p></li><li><p>将新配置命名为 <code>Staging</code>（或您喜欢的任何名称）</p></li><li><p>选择 <code>Build Settings</code> 选项卡</p></li><li><p>单击工具栏上的 <code>+</code> 按钮，创建一个名为  <code>CONFIGURATION_BUILD_DIR</code> 的 <code>User-Defined Setting</code>, 使用相同的 <code>per-configuration</code> 配置。</p></li></ol><p>  <img src="https://i.stack.imgur.com/Sodu3.png" alt="React Native 集成 CodePush 指南"></p><blockquote><p>注意：每次创建这个 Xcode 都会崩溃，只能先把值写入之后，在 <code>project.pbxproj</code> 中新建。</p></blockquote><ol start="8"><li><p>点击工具栏的 <code>+</code>  并选择 <code>Add User-Defined Setting</code></p><p><img src="https://docs.microsoft.com/en-us/appcenter/distribution/codepush/images/rn-ios-10.png" alt="React Native 集成 CodePush 指南"></p></li><li><p>将此新设置命名为<code>CodePushDeploymentKey</code>，展开它，然后为 <code>Staging </code>配置指定您的 <code>Staging</code> 部署密钥，为 <code>Release</code> 配置指定您的 <code>Production</code> 部署密钥。</p><p><img src="https://docs.microsoft.com/en-us/appcenter/distribution/codepush/images/rn-ios-11.png" alt="React Native 集成 CodePush 指南"></p><blockquote><p>提醒一下，您可以通过<code>appcenter codepush deployment list -a &lt;ownerName&gt;/&lt;appName&gt; --displayKeys</code>从终端运行来检索这些键。</p></blockquote></li><li><p>打开项目的 <code>Info.plist</code> 文件，然后将<code>CodePushDeploymentKey</code>条目的值更改为<code>$(CODEPUSH_KEY)</code></p><p><img src="https://docs.microsoft.com/en-us/appcenter/distribution/codepush/images/rn-ios-12.png" alt="React Native 集成 CodePush 指南"></p></li></ol><p>就是这样了，现在当你运行或构建你的App，你的 <code>Staging</code> 包将自动同步你的 <code>Staging</code> 部署，你的 <code>Release</code> 包将自动同步你的 <code>Production</code> 部署。</p><blockquote><p>注意：如果你遇到 <code>ld: library not found for ...</code> 错误信息，请看一下这个 <a href="https://github.com/Microsoft/react-native-code-push/issues/426">issuse</a></p></blockquote><p>此外，如果你想给他们不同的名称和&#x2F;或图标，你可以修改<code>Product Bundle Identifier</code>，<code>Product Name</code>以及<code>Asset Catalog App Icon Set Name</code></p><h2 id="动态部署分配"><a href="#动态部署分配" class="headerlink" title="动态部署分配"></a>动态部署分配</h2><p>上一节说明了如何利用多个<code>CodePush</code>部署，以便在更新发布给用户之前，有效地测试您的更新内容。 但是，由于该工作流静态地将部署分配嵌入到实际二进制文件中，因此<code>临时构建</code>和<code>生产构建</code>只会同步该部署的更新内容。</p><p>在许多情况下，这是足够的，因为您只希望您的团队，客户，利益相关者等与您的预生产版本同步，因此，他们只需要知道如何与该版本同步构建。</p><p>但是，如果你希望能够进行 <code>A / B</code> 测试，或者为某些用户提供应用程序的早期访问权限，那么能够在运行时将特定用户（或受众）动态地置于特定部署中将非常有用。</p><p>为了实现此工作流程，你需要做的就是指定调用该<code>codePush</code>方法时希望当前用户与之同步的部署密钥。指定后，此密钥将覆盖应用程序的 <code>Info.plist</code>（iOS）或 <code>MainActivity.java</code>（Android）文件中提供的“默认”密钥。这允许您生成临时或生产构建，也可以根据需要动态“重定向”。</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Imagine that &quot;userProfile&quot; is a prop that this component received</span></span><br><span class="line"><span class="comment">// which includes the deployment key that the current user should use.</span></span><br><span class="line">codePush.<span class="title function_">sync</span>(&#123; <span class="attr">deploymentKey</span>: userProfile.<span class="property">CODEPUSH_KEY</span> &#125;);</span><br></pre></td></tr></table></figure><p>有了这样的变化后，现在只需选择应用程序如何为当前用户配置正确的部署密钥。 在实践中，通常有两种解决方案：</p><ol><li>将更改部署的功能开放给用户。例如，您的设置页面可能会有一个切换按钮以启用“测试版”的访问权限。 如果您不在乎预生产更新的内容被得知，并且您的某些用户可能希望根据自己的意愿选择使用最新（并且可能有错误）的更新（有点像Chrome渠道）。 但是，此解决方案将决策权交给您的用户，这无法帮助您透明地执行 <code>A / B</code> 测试。</li><li>使用额外的元数据注释用户的服务器端配置文件，标明与其同步的部署。 默认情况下，您的应用只能使用二进制嵌入密钥，但在用户通过身份验证后，您的服务器可以选择将其“重定向”到其他部署，这样您就可以根据需要逐步将某些用户或组放置在不同的部署中。您甚至可以选择将服务器响应存储在本地存储中，以使其成为新的默认值。 如何将密钥与用户的配置文件一起存储完全取决于您的身份验证解决方案（例如 <code>Auth0</code>，<code>Firebase</code>，自定义<code>DB</code> + <code>REST API</code>），但这通常非常简单。</li></ol><blockquote><p>注意：如果需要，您还可以实施混合解决方案，允许最终用户在不同部署之间切换，同时还允许您的服务器覆盖该决策。 这样，您就拥有了“部署解决方案”的层次结构，可确保您的应用程序能够自行更新，用户可以通过获得最新内容的访问权限来获得最新体验，但您也有能力根据需要对用户进行 <code>A / B</code> 测试。</p></blockquote><p>由于我们建议将<code>Staging</code>部署用于更新的预发布测试（如上一节中所述），因此使用该部署对用户执行 <code>A / B</code> 测试并不一定有意义，与此相反，你应该允许早期访问（如上面选项1中所述）。因此，我们建议充分利用自定义应用程序部署，以便您可以按用户需求对用户进行细分。例如，您可以创建长期甚至是一次性的部署，向其发布应用程序的变体，然后将某些用户放入其中，以查看其参与度。</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">// <span class="comment">#1) Create your new deployment to hold releases of a specific app variant</span></span><br><span class="line">appcenter codepush deployment add -a &lt;ownerName&gt;/&lt;appName&gt; test-variant-one</span><br><span class="line"></span><br><span class="line">// <span class="comment">#2) Target any new releases at that custom deployment</span></span><br><span class="line">appcenter codepush release-react -a &lt;ownerName&gt;/&lt;appName&gt; -d test-variant-one</span><br></pre></td></tr></table></figure><blockquote><p>注意：从一个部署“切换”到另一个部署的用户数，被纳入到部署中的“安装度量”中报告的总用户数。例如，如果您的<code>Production</code> 部署当前报告的用户总数为1，但您将该用户动态切换为 <code>Staging</code> 部署，则 <code>Production</code> 部署将报告 0个总用户，而 <code>Staging</code> 部署将报告1（刚刚切换的用户）。 即使在使用基于运行时的部署重定向解决方案的情况下，这种行为可以让你准确地跟踪您的版本使用情况。</p></blockquote><h2 id="最佳实践"><a href="#最佳实践" class="headerlink" title="最佳实践"></a>最佳实践</h2><blockquote><p>源码：<a href="https://github.com/youngjuning/AppCenterCodePushDemo">https://github.com/youngjuning/AppCenterCodePushDemo</a></p></blockquote><h3 id="App-js"><a href="#App-js" class="headerlink" title="App.js"></a>App.js</h3><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">React</span>, &#123;useEffect&#125; <span class="keyword">from</span> <span class="string">&#x27;react&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123;<span class="title class_">View</span>, <span class="title class_">StyleSheet</span>&#125; <span class="keyword">from</span> <span class="string">&#x27;react-native&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> codePush <span class="keyword">from</span> <span class="string">&#x27;react-native-code-push&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> <span class="title class_">AwesomeButton</span> <span class="keyword">from</span> <span class="string">&#x27;react-native-really-awesome-button&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123;codePushSync, checkForUpdate&#125; <span class="keyword">from</span> <span class="string">&#x27;./CodePushUtils&#x27;</span>;</span><br><span class="line"><span class="keyword">const</span> <span class="title function_">App</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> <span class="title function_">getUpdateMetadata</span> = <span class="keyword">async</span> (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">    <span class="keyword">const</span> running = <span class="keyword">await</span> codePush.<span class="title function_">getUpdateMetadata</span>(</span><br><span class="line">      codePush.<span class="property">UpdateState</span>.<span class="property">RUNNING</span>,</span><br><span class="line">    );</span><br><span class="line">    <span class="keyword">const</span> pending = <span class="keyword">await</span> codePush.<span class="title function_">getUpdateMetadata</span>(</span><br><span class="line">      codePush.<span class="property">UpdateState</span>.<span class="property">PENDING</span>,</span><br><span class="line">    );</span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">&#x27;[CodePush] running&#x27;</span>, running);</span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">&#x27;[CodePush] pending&#x27;</span>, pending);</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="title function_">codePushSync</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">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">AwesomeButton</span> <span class="attr">onPress</span>=<span class="string">&#123;checkForUpdate&#125;</span> <span class="attr">style</span>=<span class="string">&#123;&#123;marginBottom:</span> <span class="attr">10</span>&#125;&#125;&gt;</span></span></span><br><span class="line"><span class="language-xml">        Check For Update!</span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;/<span class="name">AwesomeButton</span>&gt;</span></span></span><br><span class="line"><span class="language-xml"><span class="tag">&lt;<span class="name">AwesomeButton</span> <span class="attr">onPress</span>=<span class="string">&#123;()</span> =&gt;</span> codePush.clearUpdates()&#125; style=&#123;&#123;marginBottom: 10&#125;&#125;&gt;</span></span><br><span class="line"><span class="language-xml">        Clear Updates!</span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;/<span class="name">AwesomeButton</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">AwesomeButton</span> <span class="attr">onPress</span>=<span class="string">&#123;getUpdateMetadata&#125;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">        getUpdateMetadata!</span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;/<span class="name">AwesomeButton</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">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">    <span class="attr">alignItems</span>: <span class="string">&#x27;center&#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="title class_">App</span>;</span><br></pre></td></tr></table></figure><h3 id="CodePushUtils-js"><a href="#CodePushUtils-js" class="headerlink" title="CodePushUtils.js"></a>CodePushUtils.js</h3><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123;<span class="title class_">AppState</span>, <span class="title class_">Platform</span>, <span class="title class_">Alert</span>&#125; <span class="keyword">from</span> <span class="string">&#x27;react-native&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> codePush <span class="keyword">from</span> <span class="string">&#x27;react-native-code-push&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> configReader <span class="keyword">from</span> <span class="string">&#x27;react-native-config-reader&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="title class_">CodePushDeploymentKey</span> = &#123;</span><br><span class="line">  <span class="attr">ios</span>: &#123;</span><br><span class="line">    <span class="attr">debug</span>: <span class="string">&#x27;&#x27;</span>,</span><br><span class="line">    <span class="attr">staging</span>: <span class="string">&#x27;944zuIiRSds-ZZY6AQF82aRl0b1vUL_mMxiie&#x27;</span>,</span><br><span class="line">    <span class="attr">release</span>: <span class="string">&#x27;yyJfk2vtpLUUlOCg3FnvCcky9o4U1lEWR1UJV&#x27;</span>,</span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="attr">android</span>: &#123;</span><br><span class="line">    <span class="attr">debug</span>: <span class="string">&#x27;&#x27;</span>,</span><br><span class="line">    <span class="attr">releasestaging</span>: <span class="string">&#x27;tOncLvKACzzSkUaML9tCOUfPZxHVnobfaNIUe&#x27;</span>,</span><br><span class="line">    <span class="attr">release</span>: <span class="string">&#x27;Gtc4iXTPn24yu6CBrbl_V2GTy21xtdQyfm6x1&#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">const</span> <span class="title function_">getDeploymentKey</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> buildType = configReader.<span class="property">BUILD_TYPE</span>.<span class="title function_">toLowerCase</span>();</span><br><span class="line">  <span class="keyword">const</span> deploymentKey = <span class="title class_">CodePushDeploymentKey</span>[<span class="title class_">Platform</span>.<span class="property">OS</span>][buildType];</span><br><span class="line">  <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">&#x27;[CodePushUtils]&#x27;</span>, deploymentKey);</span><br><span class="line">  <span class="keyword">return</span> deploymentKey;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">codePushStatusDidChange</span> = <span class="keyword">async</span> syncStatus =&gt; &#123;</span><br><span class="line">  <span class="keyword">switch</span> (syncStatus) &#123;</span><br><span class="line">    <span class="keyword">case</span> codePush.<span class="property">SyncStatus</span>.<span class="property">CHECKING_FOR_UPDATE</span>:</span><br><span class="line">      <span class="comment">// 0 - 正在查询CodePush服务器以进行更新。</span></span><br><span class="line">      <span class="variable language_">console</span>.<span class="title function_">info</span>(<span class="string">&#x27;[CodePush] Checking for update.&#x27;</span>);</span><br><span class="line">      <span class="keyword">break</span>;</span><br><span class="line">    <span class="keyword">case</span> codePush.<span class="property">SyncStatus</span>.<span class="property">AWAITING_USER_ACTION</span>:</span><br><span class="line">      <span class="comment">// 1 - 有可用的更新，并且向最终用户显示了一个确认对话框。（仅在updateDialog使用时适用）</span></span><br><span class="line">      <span class="variable language_">console</span>.<span class="title function_">info</span>(<span class="string">&#x27;[CodePush] Awaiting user action.&#x27;</span>);</span><br><span class="line">      <span class="keyword">break</span>;</span><br><span class="line">    <span class="keyword">case</span> codePush.<span class="property">SyncStatus</span>.<span class="property">DOWNLOADING_PACKAGE</span>:</span><br><span class="line">      <span class="comment">// 2 - 正在从CodePush服务器下载可用更新。</span></span><br><span class="line">      <span class="variable language_">console</span>.<span class="title function_">info</span>(<span class="string">&#x27;[CodePush] Downloading package.&#x27;</span>);</span><br><span class="line">      <span class="keyword">break</span>;</span><br><span class="line">    <span class="keyword">case</span> codePush.<span class="property">SyncStatus</span>.<span class="property">INSTALLING_UPDATE</span>:</span><br><span class="line">      <span class="comment">// 3 - 已下载一个可用的更新，并将其安装。</span></span><br><span class="line">      <span class="variable language_">console</span>.<span class="title function_">info</span>(<span class="string">&#x27;[CodePush] Installing update.&#x27;</span>);</span><br><span class="line">      <span class="keyword">break</span>;</span><br><span class="line">    <span class="keyword">case</span> codePush.<span class="property">SyncStatus</span>.<span class="property">UP_TO_DATE</span>:</span><br><span class="line">      <span class="comment">// 4 - 应用程序已配置的部署完全最新。</span></span><br><span class="line">      <span class="variable language_">console</span>.<span class="title function_">info</span>(<span class="string">&#x27;[CodePush] App is up to date.&#x27;</span>);</span><br><span class="line">      <span class="keyword">break</span>;</span><br><span class="line">    <span class="keyword">case</span> codePush.<span class="property">SyncStatus</span>.<span class="property">UPDATE_IGNORED</span>:</span><br><span class="line">      <span class="comment">// 5 该应用程序具有可选更新，最终用户选择忽略该更新。（仅在updateDialog使用时适用）</span></span><br><span class="line">      <span class="variable language_">console</span>.<span class="title function_">info</span>(<span class="string">&#x27;[CodePush] User cancelled the update.&#x27;</span>);</span><br><span class="line">      <span class="keyword">break</span>;</span><br><span class="line">    <span class="keyword">case</span> codePush.<span class="property">SyncStatus</span>.<span class="property">UPDATE_INSTALLED</span>:</span><br><span class="line">      <span class="comment">// 6 - 安装了一个可用的更新，它将根据 SyncOptions 中的 InstallMode指定在 syncStatusChangedCallback 函数返回后立即或在下次应用恢复/重新启动时立即运行。</span></span><br><span class="line">      <span class="variable language_">console</span>.<span class="title function_">info</span>(<span class="string">&#x27;[CodePush] Installed update.&#x27;</span>);</span><br><span class="line">      <span class="keyword">break</span>;</span><br><span class="line">    <span class="keyword">case</span> codePush.<span class="property">SyncStatus</span>.<span class="property">SYNC_IN_PROGRESS</span>:</span><br><span class="line">      <span class="comment">// 7 - 正在执行的 sync 操作</span></span><br><span class="line">      <span class="variable language_">console</span>.<span class="title function_">info</span>(<span class="string">&#x27;[CodePush] Sync already in progress.&#x27;</span>);</span><br><span class="line">      <span class="keyword">break</span>;</span><br><span class="line">    <span class="keyword">case</span> codePush.<span class="property">SyncStatus</span>.<span class="property">UNKNOWN_ERROR</span>:</span><br><span class="line">      <span class="comment">// -1 - 同步操作遇到未知错误。</span></span><br><span class="line">      <span class="variable language_">console</span>.<span class="title function_">info</span>(<span class="string">&#x27;[CodePush] An unknown error occurred.&#x27;</span>);</span><br><span class="line">      <span class="keyword">break</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> <span class="title function_">codePushDownloadDidProgress</span> = progress =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> curPercent = (</span><br><span class="line">    (progress.<span class="property">receivedBytes</span> / progress.<span class="property">totalBytes</span>) *</span><br><span class="line">    <span class="number">100</span></span><br><span class="line">  ).<span class="title function_">toFixed</span>(<span class="number">0</span>);</span><br><span class="line">  <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">&#x27;[CodePushUtils] Downloading Progress&#x27;</span>, <span class="string">`<span class="subst">$&#123;curPercent&#125;</span>%`</span>);</span><br><span class="line">  <span class="comment">// console.log(`$&#123;progress.receivedBytes&#125; of $&#123;progress.totalBytes&#125; received.`);</span></span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">syncImmediate</span> = <span class="keyword">async</span> (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> deploymentKey = <span class="title function_">getDeploymentKey</span>();</span><br><span class="line">  codePush.<span class="title function_">sync</span>(</span><br><span class="line">    &#123;</span><br><span class="line">      <span class="attr">updateDialog</span>: &#123;</span><br><span class="line">        <span class="comment">// 是否显示更新描述</span></span><br><span class="line">        <span class="attr">appendReleaseDescription</span>: <span class="literal">true</span>,</span><br><span class="line">        <span class="comment">// 更新描述的前缀。 默认为&quot;Description&quot;</span></span><br><span class="line">        <span class="attr">descriptionPrefix</span>: <span class="string">&#x27;\n\n更新内容：\n&#x27;</span>,</span><br><span class="line">        <span class="comment">// 强制更新按钮文字，默认为continue</span></span><br><span class="line">        <span class="attr">mandatoryContinueButtonLabel</span>: <span class="string">&#x27;立即更新&#x27;</span>,</span><br><span class="line">        <span class="comment">// 强制更新时的信息. 默认为&quot;An update is available that must be installed.&quot;</span></span><br><span class="line">        <span class="attr">mandatoryUpdateMessage</span>: <span class="string">&#x27;必须更新后才能使用&#x27;</span>,</span><br><span class="line">        <span class="comment">// 非强制更新时，按钮文字,默认为&quot;ignore&quot;</span></span><br><span class="line">        <span class="attr">optionalIgnoreButtonLabel</span>: <span class="string">&#x27;稍后&#x27;</span>,</span><br><span class="line">        <span class="comment">// 非强制更新时，确认按钮文字. 默认为&quot;Install&quot;</span></span><br><span class="line">        <span class="attr">optionalInstallButtonLabel</span>: <span class="string">&#x27;后台更新&#x27;</span>,</span><br><span class="line">        <span class="comment">// 非强制更新时，检查到更新的消息文本</span></span><br><span class="line">        <span class="attr">optionalUpdateMessage</span>: <span class="string">&#x27;有新版本了，是否更新？&#x27;</span>,</span><br><span class="line">        <span class="comment">// Alert窗口的标题</span></span><br><span class="line">        <span class="attr">title</span>: <span class="string">&#x27;更新&#x27;</span>,</span><br><span class="line">      &#125;,</span><br><span class="line">      deploymentKey,</span><br><span class="line">      <span class="attr">installMode</span>: codePush.<span class="property">InstallMode</span>.<span class="property">IMMEDIATE</span>,</span><br><span class="line">    &#125;,</span><br><span class="line">    codePushStatusDidChange,</span><br><span class="line">    codePushDownloadDidProgress,</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">const</span> <span class="title function_">checkForUpdate</span> = <span class="keyword">async</span> (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> deploymentKey = <span class="title function_">getDeploymentKey</span>();</span><br><span class="line">  <span class="keyword">const</span> update = <span class="keyword">await</span> codePush.<span class="title function_">checkForUpdate</span>(deploymentKey);</span><br><span class="line">  <span class="keyword">if</span> (!update) &#123;</span><br><span class="line">    <span class="title class_">Alert</span>.<span class="title function_">alert</span>(<span class="string">&#x27;提示&#x27;</span>, <span class="string">&#x27;已是最新版本&#x27;</span>);</span><br><span class="line">  &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    <span class="title function_">syncImmediate</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">const</span> <span class="title function_">codePushSync</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">  <span class="title class_">AppState</span>.<span class="title function_">addEventListener</span>(<span class="string">&#x27;change&#x27;</span>, <span class="function"><span class="params">newState</span> =&gt;</span> &#123;</span><br><span class="line">    newState === <span class="string">&#x27;active&#x27;</span> &amp;&amp; <span class="title function_">syncImmediate</span>();</span><br><span class="line">  &#125;);</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><h3 id="Npm-Scripts"><a href="#Npm-Scripts" class="headerlink" title="Npm Scripts"></a>Npm Scripts</h3><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><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><br><span class="line">    <span class="attr">&quot;gradle:clean&quot;</span><span class="punctuation">:</span> <span class="string">&quot;cd android &amp;&amp; ./gradlew clean&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;an:release&quot;</span><span class="punctuation">:</span> <span class="string">&quot;yarn gradle:clean &amp;&amp; cd android &amp;&amp; ./gradlew app:assembleRelease&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;an:installRelease&quot;</span><span class="punctuation">:</span> <span class="string">&quot;yarn gradle:clean &amp;&amp; cd android &amp;&amp; ./gradlew app:installRelease&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;an:staging&quot;</span><span class="punctuation">:</span> <span class="string">&quot;yarn gradle:clean &amp;&amp; cd android &amp;&amp; ./gradlew app:assembleReleaseStaging&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;an:installStaging&quot;</span><span class="punctuation">:</span> <span class="string">&quot;yarn gradle:clean &amp;&amp; cd android &amp;&amp; ./gradlew app:installReleaseStaging&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;displayKeys&quot;</span><span class="punctuation">:</span> <span class="string">&quot;yarn disPlayIosKeys &amp;&amp; yarn disPlayAndroidKeys&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;disPlayIosKeys&quot;</span><span class="punctuation">:</span> <span class="string">&quot;appcenter codepush deployment list --app youngjuning/AppCenterCodePushDemo-iOS --displayKeys&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;disPlayAndroidKeys&quot;</span><span class="punctuation">:</span> <span class="string">&quot;appcenter codepush deployment list --app youngjuning/AppCenterCodePushDemo-Android --displayKeys&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;release-react&quot;</span><span class="punctuation">:</span> <span class="string">&quot;yarn release-react-ios &amp;&amp; yarn release-react-android&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;release-react-ios&quot;</span><span class="punctuation">:</span> <span class="string">&quot;appcenter codepush release-react --app youngjuning/AppCenterCodePushDemo-iOS&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;release-react-android&quot;</span><span class="punctuation">:</span> <span class="string">&quot;appcenter codepush release-react --app youngjuning/AppCenterCodePushDemo-Android&quot;</span></span><br><span class="line">    ...</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></pre></td></tr></table></figure><h2 id="扩展"><a href="#扩展" class="headerlink" title="扩展"></a>扩展</h2><h3 id="CodePush-什么情况下不会立即重启应用"><a href="#CodePush-什么情况下不会立即重启应用" class="headerlink" title="CodePush 什么情况下不会立即重启应用"></a>CodePush 什么情况下不会立即重启应用</h3><ol><li>自上一次<code>disallowRestart</code>被调用，没有新的更新。</li><li>有更新，但<code>installMode</code>为<code>InstallMode.ON_NEXT_RESTART</code>的情况下。</li><li>有更新，但<code>installMode</code>为<code>InstallMode.ON_NEXT_RESUME</code>，并且程序一直处于前台，并没有从后台切换到前台的情况下。</li><li>自从上次<code>disallowRestart</code>被调用，没有再调用<code>restartApp</code>。</li></ol><h3 id="TypeSctipt"><a href="#TypeSctipt" class="headerlink" title="TypeSctipt"></a>TypeSctipt</h3><p>如果使用 TypeScript，再浏览一次文档的API部分之后，就可以依靠类型系统的提示来工作啦： <a href="https://github.com/microsoft/react-native-code-push/blob/master/typings/react-native-code-push.d.ts">react-native-code-push.d.ts</a></p><h3 id="iOS-添加-BUILD-Type"><a href="#iOS-添加-BUILD-Type" class="headerlink" title="iOS 添加 BUILD_Type"></a>iOS 添加 BUILD_Type</h3><p>在 <code>Info.plist</code> 中添加 <code>BUILD_TYPE</code>，取值为 <code>$(CONFIGURATION)</code></p><h3 id="react-native-bundle"><a href="#react-native-bundle" class="headerlink" title="react-native bundle"></a>react-native bundle</h3><p>生成  <code>bundle</code> 命名：<code>react-native bundle --platform</code> 平台 <code> --entry-file</code>启动文件 <code>--bundle-output</code> 打包js输出文件 <code> --assets-dest</code>  资源输出目录 <code> --dev</code>  是否调试：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ react-native bundle --platform android --entry-file index.js --bundle-output ./bundle/android/main.jsbundle --assets-dest ./bundle/android --dev <span class="literal">false</span></span><br></pre></td></tr></table></figure><h2 id="联系作者"><a href="#联系作者" class="headerlink" title="联系作者"></a>联系作者</h2><table><thead><tr><th align="center">作者微信</th><th align="center">知识星球</th><th align="center">赞赏作者</th></tr></thead><tbody><tr><td align="center"><img src="https://user-gold-cdn.xitu.io/2020/2/24/17074acbb24c7412?w=200&h=200&f=jpeg&s=17183" style="width:200px"/></td><td align="center"><img src="https://user-gold-cdn.xitu.io/2020/2/24/17074acbb26af8e1?w=200&h=200&f=png&s=39093" style="width:200px"/></td><td align="center"><img src="https://user-gold-cdn.xitu.io/2020/2/24/17074acbb338c643?w=698&h=700&f=png&s=315492" style="width:200px"/></td></tr></tbody></table><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ul><li><a href="https://docs.microsoft.com/en-us/appcenter/distribution/codepush/">Appcenter CodePush</a></li><li><a href="https://bre.is/DxWyD6wP">React-Native应用部署-热更新-CodePush最新集成总结</a></li><li><a href="https://www.jianshu.com/p/6a5e00d22723">CodePush热更新详细接入教程</a></li><li><a href="http://techblog.sishuxuefu.com/atricle.html?5beaa7e59f5454007039e01c">react-native热更新</a></li></ul>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/2167a27028fb/</id>
    <link href="https://blog.zisheng.pro/posts/2167a27028fb/"/>
    <published>2020-02-25T09:08:43.000Z</published>
    <summary>本文分享的是基于微软 AppCenter 的 CodePush 服务实现热更新，这个比较有代表性，也方便各位读者大大实践。当然鉴于国内的网络环境，后期会发布一篇如何基于 code-push-server 实现热更新功能。</summary>
    <title>React Native 集成 CodePush 指南</title>
    <updated>2026-09-18T08:15:28.632Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="React Native" scheme="https://blog.zisheng.pro/tags/React-Native/"/>
    <category term="metro" scheme="https://blog.zisheng.pro/tags/metro/"/>
    <content>
      <![CDATA[<p>当我们执行 <code>react-native start</code> 命令时，React Native 会启动一个默认端口号为 <code>8081</code>的本地服务，该 <code>8081</code> 的服务就是React Native项目的一个本地 Node 服务器，用于提供 JSBundle 包和一些静态资源。这一切都是 React Native 帮开发者配置好的，一切都是那么简便。</p><p>本文旨在告诉大家现在我们只需要简单地对 Metro 进行配置即可修改默认的版本号。</p><p>我翻阅了一下网上的方案，都太麻烦了，想着这功能官方不该提供可配置的吗？于是翻阅了官方的脚手架工具<a href="https://github.com/react-native-community/cli">@react-native-community&#x2F;cli</a>。打开<a href="https://bre.is/hDtBSjob">loadMetroConfig.ts</a> 文件我找到了以下代码：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">server</span>: &#123;</span><br><span class="line">  <span class="attr">port</span>: <span class="title class_">Number</span>(process.<span class="property">env</span>.<span class="property">RCT_METRO_PORT</span>) || <span class="number">8081</span>,</span><br><span class="line">&#125;,</span><br></pre></td></tr></table></figure><p>当我看到 <code>RCT_METRO_PORT</code> 这个属性，顿感这玩意和项目根目录的 <code>metro.config.js</code> 有关系。于是照猫画虎，在这个文件中添加了一下配置：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="variable language_">module</span>.<span class="property">exports</span> = &#123;</span><br><span class="line">  ...</span><br><span class="line">  <span class="attr">server</span>: &#123;</span><br><span class="line">    <span class="attr">port</span>: <span class="number">8082</span>,</span><br><span class="line">  &#125;,</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>抱着试一试的心态，运行了 <code>react-native run-android</code>,运行成功：</p><img src="https://i.loli.net/2020/02/25/Zcs2rIDk5F9hoUS.png" style="zoom:60%;" /><p>知其然，知其所以然，这个为什么可行呢？打开 metro 的官方文档，我找到了下面的内容：</p><img src="https://i.loli.net/2020/02/25/KDgRdMbToZLB1Vs.png" style="zoom:67%;" /><p>现在 metro 已经接手了所有 React Native 的打包任务，后期遇到任何打包相关的问题，都可以先阅读一下 Metro 的文档进行解决。顺便吐槽一下，这么基础的设施，竟然没有中文翻译的文档。</p>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/8c4c7f4e5f09/</id>
    <link href="https://blog.zisheng.pro/posts/8c4c7f4e5f09/"/>
    <published>2020-02-25T08:02:10.000Z</published>
    <summary>本文旨在告诉大家现在我们只需要简单地对 Metro 进行配置即可修改默认的版本号。</summary>
    <title>利用 Metro 配置 React Native 端口</title>
    <updated>2026-09-18T08:15:28.636Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="React Native" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/React-Native/"/>
    <category term="React Native" scheme="https://blog.zisheng.pro/tags/React-Native/"/>
    <category term="架构" scheme="https://blog.zisheng.pro/tags/%E6%9E%B6%E6%9E%84/"/>
    <category term="最佳实践" scheme="https://blog.zisheng.pro/tags/%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5/"/>
    <content>
      <![CDATA[<h2 id="sigmayun-react-native-template-typescript"><a href="#sigmayun-react-native-template-typescript" class="headerlink" title="@sigmayun&#x2F;react-native-template-typescript"></a>@sigmayun&#x2F;react-native-template-typescript</h2><p><a href="https://github.com/sigmayun/react-native-template-typescript">@sigmayun&#x2F;react-native-template-typescript</a>是基于 <a href="https://github.com/react-native-community/react-native-template-typescript">react-native-community&#x2F;react-native-template-typescript</a> 二次开发的模板脚手架。本文的架构方案融进了该模板。你甚至不需要阅读本文的内容即可使用本文中的最佳实践！！！</p><h2 id="全局变量"><a href="#全局变量" class="headerlink" title="全局变量"></a>全局变量</h2><p>既然都引入 TypeScript 了，就不用再担心给全局对象添加属性不小心污染它了，但是需要我们进行一些配置。</p><p>1、在项目根目录新建 <code>types</code>,并新建 <code>global.d.ts</code> 文件名，添加一下内容：</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">declare</span> <span class="keyword">const</span> <span class="attr">global</span>: &#123;</span><br><span class="line">  <span class="title class_">HermesInternal</span>: <span class="literal">null</span> | &#123;&#125;</span><br><span class="line">  ...</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>2、为了防止和 <code>@types/node</code> 的 <code>Global</code> 声明冲突，需要把 <code>tsconfig.json</code> 中的 <code>skipLibCheck</code> 的值设置为 <code>true</code></p><blockquote><p>该部分示例代码涉及 <code>global.ts</code>、<code>types/global.d.ts</code>、<code>tsconfig.json</code>，请到 <a href="https://github.com/sigmayun/react-native-template-typescript">https://github.com/sigmayun/react-native-template-typescript</a> 查看。</p></blockquote><h2 id="编码规范"><a href="#编码规范" class="headerlink" title="编码规范"></a>编码规范</h2><ul><li><a href="https://github.com/sishuguojixuefu/eslint-config">@sishuguojixuefu&#x2F;eslint-config</a>：大而全的 ESlint 配置，支持 vue、jsx、js、ts、tsx、html 的 ESLint 配置插件，基于 airbnb，支持Prettier</li><li>husky + lint-staged: 禁止 <code>commit</code> 不符合规范的代码</li></ul><blockquote><p>该部分示例代码：<a href="https://bre.is/3GD7cHKn">https://bre.is/3GD7cHKn</a></p></blockquote><h2 id="Npm-Scripts"><a href="#Npm-Scripts" class="headerlink" title="Npm Scripts"></a>Npm Scripts</h2><p>善用 NPM Scripts 能帮助改善工作流程！！！</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><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;postinstall&quot;</span><span class="punctuation">:</span> <span class="string">&quot;npx jetifier&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;android&quot;</span><span class="punctuation">:</span> <span class="string">&quot;react-native run-android&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;ios&quot;</span><span class="punctuation">:</span> <span class="string">&quot;react-native run-ios&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;react-native start&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;test&quot;</span><span class="punctuation">:</span> <span class="string">&quot;jest&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;eslint . --ext .js,.jsx,.ts,.tsx&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;pod&quot;</span><span class="punctuation">:</span> <span class="string">&quot;cd ios &amp;&amp; pod install&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;gradle:clean&quot;</span><span class="punctuation">:</span> <span class="string">&quot;cd android &amp;&amp; ./gradlew clean&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;an:release&quot;</span><span class="punctuation">:</span> <span class="string">&quot;yarn gradle:clean &amp;&amp; cd android &amp;&amp; ./gradlew app:assembleRelease&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;an:installRelease&quot;</span><span class="punctuation">:</span> <span class="string">&quot;yarn gradle:clean &amp;&amp; cd android &amp;&amp; ./gradlew app:installRelease&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;an:releaseStaging&quot;</span><span class="punctuation">:</span> <span class="string">&quot;yarn gradle:clean &amp;&amp; cd android &amp;&amp; ./gradlew app:assembleReleaseStaging&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;an:installReleaseStaging&quot;</span><span class="punctuation">:</span> <span class="string">&quot;yarn gradle:clean &amp;&amp; cd android &amp;&amp; ./gradlew app:installReleaseStaging&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;an:genkeypair&quot;</span><span class="punctuation">:</span> <span class="string">&quot;keytool -genkeypair -v -keystore release.keystore -alias my-key-alias -keyalg RSA -keysize 2048 -validity 10000&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;an:key-debug&quot;</span><span class="punctuation">:</span> <span class="string">&quot;keytool -list -v -keystore ./android/app/debug.keystore&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;an:key-release&quot;</span><span class="punctuation">:</span> <span class="string">&quot;keytool -v -list -keystore ./android/app/release.keystore&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;icon&quot;</span><span class="punctuation">:</span> <span class="string">&quot;npx iconfont-rn&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><blockquote><p>该部分示例代码：<a href="https://bre.is/LC6uu8kc">https://bre.is/LC6uu8kc</a></p></blockquote><h2 id="网络编程"><a href="#网络编程" class="headerlink" title="网络编程"></a>网络编程</h2><ul><li><a href="https://www.kancloud.cn/yunye/axios/234845">axios</a>: Axios 是一个基于 promise 的 HTTP 库，可以用在浏览器和 node.js 中。</li><li><a href="https://www.npmjs.com/package/qs">qs</a>: A querystring parsing and stringifying library with some added security.</li><li><a href="https://www.npmjs.com/package/clean-deep">clean-deep</a>: 从对象中递归删除空对象、空数组、空字符串、<code>null</code>和 <code>value</code>值。不改变原始数据。</li></ul><blockquote><p>该部分示例代码：<a href="https://bre.is/39khhJtn">https://bre.is/39khhJtn</a></p></blockquote><h2 id="路由管理"><a href="#路由管理" class="headerlink" title="路由管理"></a>路由管理</h2><ul><li>React Navigation: 这个模块是官方推荐的导航组件，升级到5之后不仅支持了 <code>hooks</code>，更是支持了动态路由。</li></ul><blockquote><p>该部分示例代码：<a href="https://bre.is/pzA7JArY">https://bre.is/pzA7JArY</a></p><p>相关博客：<a href="https://bre.is/pzA7JArY">https://bre.is/pzA7JArY</a></p></blockquote><h2 id="状态管理"><a href="#状态管理" class="headerlink" title="状态管理"></a>状态管理</h2><ul><li><code>mobx</code> + <code>mobx-react</code>: 相对于 redux 来说更简单易用，还在犹豫的同学，可以读一下有赞的一篇文章 <a href="https://tech.youzan.com/mobx_vs_redux/">我为什么从Redux迁移到了Mobx</a></li></ul><h2 id="本地存储"><a href="#本地存储" class="headerlink" title="本地存储"></a>本地存储</h2><ul><li><a href="https://www.npmjs.com/package/react-native-simple-store">react-native-simple-storage</a>:  AsyncStorage 的一个简单包装器</li><li><a href="https://github.com/pubkey/rxdb">rxdb</a>:  JavaScript 即时数据库，如果有复杂的缓存业务可以考虑使用</li><li><a href="https://github.com/Nozbe/WatermelonDB">watermelonDB</a>: 一个专为 React、React Native 服务的响应式数据库框架</li></ul><h2 id="启动屏"><a href="#启动屏" class="headerlink" title="启动屏"></a>启动屏</h2><ul><li><a href="https://github.com/crazycodeboy/react-native-splash-screen">react-native-splash-screen</a></li></ul><blockquote><p>启动屏的配置涉及到不少原生知识，请阅读 <a href="https://bre.is/CRC2SkqP">https://bre.is/CRC2SkqP</a> 进行配置。</p></blockquote><h2 id="热更新"><a href="#热更新" class="headerlink" title="热更新"></a>热更新</h2><ul><li><a href="https://update.reactnative.cn/home">react-native-pushy</a>: ReactNative中文网推出的代码热更新服务，免费，但是限制多，不推荐使用</li><li><a href="https://github.com/microsoft/react-native-code-push">react-native-code-push</a> + <a href="https://appcenter.ms/">AppCenter</a>：完全免费，国内速度可能慢，适合个人开发者【推荐】</li><li><a href="https://github.com/microsoft/react-native-code-push">react-native-code-push</a> + <a href="https://github.com/lisong/code-push-server">code-push-server</a>：适合公司自建热更新服务器</li></ul><blockquote><p>新版热更新部分我还没写好博客，可以先参考我之前写的： <a href="http://techblog.sishuxuefu.com/atricle.html?5beaa7e59f5454007039e01c%EF%BC%8C%E6%88%91%E4%BC%9A%E5%9C%A8%E7%9C%8B%E5%AE%8C%E6%96%B0%E6%96%87%E6%A1%A3%E4%B9%8B%E5%90%8E%E6%8A%8A%E8%BF%99%E9%83%A8%E5%88%86%E9%85%8D%E7%BD%AE%E4%B9%9F%E5%86%85%E7%BD%AE%E5%88%B0%E8%84%9A%E6%89%8B%E6%9E%B6%E4%B8%AD%EF%BC%8C%E6%AF%95%E7%AB%9F%E8%BF%99%E4%B8%AA%E6%9C%8D%E5%8A%A1%E4%B9%9F%E6%98%AF">http://techblog.sishuxuefu.com/atricle.html?5beaa7e59f5454007039e01c，我会在看完新文档之后把这部分配置也内置到脚手架中，毕竟这个服务也是</a> React Native 的标配。</p></blockquote><h2 id="UI框架"><a href="#UI框架" class="headerlink" title="UI框架"></a>UI框架</h2><ul><li><a href="https://rn.mobile.ant.design/index-cn">@ant-design&#x2F;react-native</a>:  Ant Design 出品的一个基于 React Native 的 UI 组件库</li><li><a href="https://github.com/Meituan-Dianping/beeshell">beeshell</a>: 美团 React Native 组件库</li><li><a href="https://react-native-elements.github.io/react-native-elements/">react-native-elements</a>: 跨平台React Native UI工具包</li><li><a href="https://github.com/wix/react-native-ui-lib">react-native-ui-lib</a>: 没有体验过，看起来很强大</li></ul><blockquote><p>其他组件收藏请查看：<a href="https://bre.is/jWUGvPrK">https://bre.is/jWUGvPrK</a></p></blockquote><h2 id="列表"><a href="#列表" class="headerlink" title="列表"></a>列表</h2><ul><li><a href="https://bre.is/yqJ2EEdL">mobx+react-native-largelist 实现分页功能</a>: 推荐使用 <a href="https://github.com/bolan9999/react-native-largelist">react-native-largelist-v3</a>，支持下拉刷新、上拉加载更多、分组列表、表格和瀑布流</li><li>自行封装 <code>FlatList</code>、<code>SectionList</code>: 并不复杂，建议还是要回手写的，react-native-largelist-v3 不一定能满足所有场景</li></ul><h2 id="图标"><a href="#图标" class="headerlink" title="图标"></a>图标</h2><ul><li><a href="https://www.npmjs.com/package/react-native-iconfont-cli">react-native-iconfont-cli</a>: 【推荐】用纯JS把图标转换成RN组件，不依赖字体，支持多色彩，支持热更新</li><li><a href="https://bre.is/dfCDM2Kw">@ant-design&#x2F;icons-react-native</a>: Ant Design Icons for React Native</li><li><a href="https://github.com/oblador/react-native-vector-icons">react-native-vector-icons</a>: Perfect for buttons, logos and nav&#x2F;tab bars. Easy to extend, style and integrate into your project.</li></ul><h2 id="字体"><a href="#字体" class="headerlink" title="字体"></a>字体</h2><ul><li><a href="https://github.com/react-native-training/react-native-fonts">react-native-fonts</a>: 在React Native项目中开箱即用的可用字体，这个库只是统计了一下 React Native 中可用的字体而已</li><li><a href="https://juejin.im/entry/59c74c91f265da065c5e9282">React Native 配置自定义字体</a></li><li><a href="https://bre.is/N2NqkX4N">React Native Custom Fonts</a></li><li><a href="https://github.com/heyman333/react-native-responsive-fontSize">react-native-responsive-fontSize</a>: 响应式fontSize基于React Native中设备的屏幕尺寸</li><li><a href="https://bre.is/WTxnrfPV">React Native 配置字体大小不随字体设置变化</a></li><li><a href="https://juejin.im/post/5ce66c26e51d4555fd20a2a0">React Native字体问题解决方案指北</a></li><li><a href="https://www.npmjs.com/package/react-native-responsive-fontsize">react-native-responsive-fontsize</a></li></ul><h2 id="工具"><a href="#工具" class="headerlink" title="工具"></a>工具</h2><ul><li><a href="https://ethercreative.github.io/react-native-shadow-generator/">react-native-shadow-generator</a>: 在线生成 React Native 阴影代码</li><li><a href="https://github.com/JonnyBurger/npx-visualize-bundle">visualize-bundle</a>: <code>npx visualize-bundle</code> 允许你一键检查你的 React Native bundle 包 并且找到大的依赖</li><li><a href="https://icon.wuruihong.com/">图标工厂</a>: 一键生成所有尺寸的应用图标&#x2F;启动图</li></ul><h2 id="React-Hooks"><a href="#React-Hooks" class="headerlink" title="React Hooks"></a>React Hooks</h2><p>这个新语法是2019年的明星，React Navigation、Mobx这些常用库都内置支持了，还没有学习的同学快去补课吧！！！</p><h3 id="推荐阅读"><a href="#推荐阅读" class="headerlink" title="推荐阅读"></a>推荐阅读</h3><blockquote><p>建议先读官方文档！！！</p></blockquote><ul><li><a href="https://zh-hans.reactjs.org/docs/hooks-intro.html">官方文档</a></li><li><a href="https://juejin.im/post/5be3ea136fb9a049f9121014">30分钟精通React Hooks</a></li><li><a href="https://juejin.im/post/5dbbdbd5f265da4d4b5fe57d">React Hooks 详解 【近 1W 字】+ 项目实战</a></li><li><a href="https://juejin.im/post/5d985deae51d4577f9285c2f">呕心沥血，一文看懂 react hooks</a></li><li><a href="https://zhuanlan.zhihu.com/p/86211675">React Hooks 带来的困扰与思考</a></li></ul><h3 id="如何兼容类组件"><a href="#如何兼容类组件" class="headerlink" title="如何兼容类组件"></a>如何兼容类组件</h3><p>考虑到对于不适应 Hooks 的但是业务又很紧急的场景，我们可以在类组件之上封装一层来支持 React Navigation 的 Hooks 组件，之所以这么做，起因是因为 React Navigation 5 中我们只能通过 <code>useHeaderHeight()</code> 方法获取标题栏高度。</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Albums</span> <span class="keyword">extends</span> <span class="title class_ inherited__">React.Component</span> &#123;</span><br><span class="line">  <span class="title function_">render</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">ScrollView</span> <span class="attr">ref</span>=<span class="string">&#123;this.props.scrollRef&#125;</span>&gt;</span><span class="tag">&lt;<span class="name">Text</span>&gt;</span>&#123;this.props.headerHeight&#125;<span class="tag">&lt;/<span class="name">Text</span>&gt;</span><span class="tag">&lt;/<span class="name">ScrollView</span>&gt;</span></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="keyword">export</span> <span class="keyword">default</span> <span class="keyword">function</span>(<span class="params">props</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> ref = <span class="title class_">React</span>.<span class="title function_">useRef</span>(<span class="literal">null</span>);</span><br><span class="line">  <span class="keyword">const</span> headerHeight = <span class="title function_">useHeaderHeight</span>();</span><br><span class="line">  <span class="title function_">useScrollToTop</span>(ref);</span><br><span class="line">  <span class="keyword">return</span> <span class="language-xml"><span class="tag">&lt;<span class="name">Albums</span> &#123;<span class="attr">...props</span>&#125; <span class="attr">scrollRef</span>=<span class="string">&#123;ref&#125;</span> <span class="attr">headerHeight</span>=<span class="string">&#123;headerHeight&#125;/</span>&gt;</span></span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="Metro"><a href="#Metro" class="headerlink" title="Metro"></a>Metro</h2><ul><li><a href="https://juejin.im/post/5e54d910f265da571671090d">利用Metro配置React Native端口</a></li></ul><h2 id="第三放服务"><a href="#第三放服务" class="headerlink" title="第三放服务"></a>第三放服务</h2><blockquote><p>这里尽量为大家推荐了免费的服务，当然选择自行搭建服务也是一个很棒的选择！！</p></blockquote><h3 id="Sentry"><a href="#Sentry" class="headerlink" title="Sentry"></a><a href="https://sentry.io/welcome/">Sentry</a></h3><blockquote><p>Sentry 胜在可以把服务搭建到自己的服务器上，并且官方提供了<a href="https://github.com/getsentry/sentry-react-native">sentry-react-native</a>插件来帮助 React Native 工程师快速集成。如果有条件，选择 Sentry 恐怕是目前行业的最佳选择。</p></blockquote><p>Sentry提供自托管和基于云的错误监控，可帮助所有软件团队实时发现分类和确定错误的优先级。已经有超过五万家公司的100万名开发人员使用，Sentry可以更快地提供更好的软件。你不加入他们吗？</p><h3 id="腾讯-Bugly"><a href="#腾讯-Bugly" class="headerlink" title="腾讯 Bugly"></a><a href="https://bugly.qq.com/v2/">腾讯 Bugly</a></h3><p>腾讯Bugly，为移动开发者提供专业的异常上报和运营统计，帮助开发者快速发现并解决异常，同时掌握产品运营动态，及时跟进用户反馈。免费提供异常上报、运营统计、应用升级</p><h3 id="推送"><a href="#推送" class="headerlink" title="推送"></a>推送</h3><h4 id="信鸽-腾讯移动推送"><a href="#信鸽-腾讯移动推送" class="headerlink" title="信鸽 | 腾讯移动推送"></a><a href="https://xg.qq.com/">信鸽 | 腾讯移动推送</a></h4><p>为开发者提供免费、快速、简单的推送服务。QQ登录即可快速注册，为APP接入SDK后马上获得无限量应用推送能力，<br>有效提升用户留存率、活跃度，开发者的不二选择！</p><h4 id="小米消息推送服务"><a href="#小米消息推送服务" class="headerlink" title="小米消息推送服务"></a><a href="https://dev.mi.com/console/appservice/push.html">小米消息推送服务</a></h4><ul><li>MIUI上系统级通道</li><li>iOS&#x2F;Android全平台支持</li><li>免费 稳定 安全 高效</li></ul><h4 id="极光推送"><a href="#极光推送" class="headerlink" title="极光推送"></a><a href="https://www.jiguang.cn/push">极光推送</a></h4><p>极光推送服务，可以免费使用，但限制较多。官方SDK, React Native 集成容易</p><h4 id="U-Push"><a href="#U-Push" class="headerlink" title="U-Push"></a><a href="https://www.umeng.com/push">U-Push</a></h4><p>和极光属于一类，限制级免费，友盟的产品胜在生态，公司产品考虑，后期用户上来之后方便扩展。</p><h3 id="社会化分享"><a href="#社会化分享" class="headerlink" title="社会化分享"></a>社会化分享</h3><h4 id="ShareSDK"><a href="#ShareSDK" class="headerlink" title="ShareSDK"></a><a href="http://www.mob.com/mobService/sharesdk">ShareSDK</a></h4><p>为开发者提供40+主流平台的分享与授权等社会化功能，效果稳定，完整清晰统计分享数据</p><ul><li>一键分享</li><li>第三方登录</li><li>闭环分享</li><li>短链转换</li><li>数据统计</li><li>新浪微博独家LinkCard</li></ul><h4 id="jshare"><a href="#jshare" class="headerlink" title="jshare"></a><a href="https://github.com/jpush/jshare-react-native">jshare</a></h4><ul><li>一键分享</li><li>第三放登录</li><li>社会化统计</li><li>官方 React Native SDK 集成方便</li></ul><h3 id="U-Share"><a href="#U-Share" class="headerlink" title="U-Share"></a><a href="https://bre.is/LM3eaCsL">U-Share</a></h3><ul><li>全面覆盖国内外社交平台</li><li>集成成本低、速度快</li><li>自由定制分享界面</li><li>权威、实时的大数据分析</li></ul><h3 id="其他"><a href="#其他" class="headerlink" title="其他"></a>其他</h3><ul><li><a href="https://github.com/yorkie/react-native-wechat">react-native-wechat</a>: 🚀 <strong>WeChat login</strong>, <strong>share</strong>, <strong>favorite</strong> and <strong>payment</strong> for React-Native on iOS and Android platforms</li><li><a href="https://github.com/0x5e/react-native-alipay">@0x5e&#x2F;react-native-alipay</a>: Alipay SDK for React Native. Support mobile webpage url payment. Support RN &gt;&#x3D; 0.47.</li></ul><h2 id="测试"><a href="#测试" class="headerlink" title="测试"></a>测试</h2><blockquote><p>这部分作者的理解仅限于 Jest，有大佬可以联系我补充！！！</p></blockquote><ul><li><a href="https://jestjs.io/zh-Hans/">Jest</a>: Jest是一个令人愉快的 JavaScript 测试框架，专注于简洁明快。React Native 已经集成好了，直接使用即可。</li></ul><h2 id="自动化运维"><a href="#自动化运维" class="headerlink" title="自动化运维"></a>自动化运维</h2><blockquote><p>这部分作者接触不深，只知道 Jekens + GitLab 的方案</p></blockquote><h3 id="shell-文件的坑"><a href="#shell-文件的坑" class="headerlink" title="shell 文件的坑"></a>shell 文件的坑</h3><p>为了安全性 shell 文件默认都是不可执行的，当然也包括 <code>android/gradlew</code> 这个用来打包的脚本文件，这会给持续集成带来麻烦：运维同学默认是执行不了我们的打包命令的。解决办法很简单：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">git update-index --add --<span class="built_in">chmod</span>=+x android/gradlew</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><p>set-version.sh:</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#!/bin/bash</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># current_git_branch_latest_id=`git rev-parse HEAD`</span></span><br><span class="line">current_git_branch_latest_short_id=`git rev-parse --short HEAD`</span><br><span class="line">current_os=`<span class="built_in">uname</span> -s`</span><br><span class="line"></span><br><span class="line"><span class="comment"># echo current git branch latest commit id=$current_git_branch_latest_id</span></span><br><span class="line"><span class="built_in">echo</span> current git branch latest commit short <span class="built_in">id</span>=<span class="variable">$current_git_branch_latest_short_id</span></span><br><span class="line"><span class="built_in">echo</span> current os=<span class="variable">$current_os</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> [ <span class="string">&quot;<span class="variable">$current_os</span>&quot;</span> == <span class="string">&quot;Darwin&quot;</span> ]</span><br><span class="line">  <span class="keyword">then</span></span><br><span class="line">  sed -i <span class="string">&#x27;&#x27;</span> <span class="string">&#x27;s/&quot;version&quot;.*/&quot;version&quot;: &quot;1.0.0-&#x27;</span><span class="variable">$current_git_branch_latest_short_id</span><span class="string">&#x27;&quot;,/g&#x27;</span> package.json</span><br><span class="line"><span class="keyword">else</span></span><br><span class="line">  <span class="built_in">echo</span> windows</span><br><span class="line">  sed -i <span class="string">&#x27;s/&quot;version&quot;.*/&quot;version&quot;: &quot;&#x27;</span><span class="variable">$current_git_branch_latest_short_id</span><span class="string">&#x27;&quot;,/g&#x27;</span> package.json</span><br><span class="line"><span class="keyword">fi</span></span><br></pre></td></tr></table></figure><p>package.json:</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="comment">// 依赖 husky</span></span><br><span class="line">  <span class="attr">&quot;husky&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;hooks&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">      <span class="attr">&quot;pre-commit&quot;</span><span class="punctuation">:</span> <span class="string">&quot;lint-staged&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;post-commit&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">        <span class="string">&quot;./scripts/set-version.sh&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="string">&quot;git add .&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="string">&quot;git commit -m bump version&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="string">&quot;git push&quot;</span></span><br><span class="line">      <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 class="punctuation">,</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><ul><li><a href="https://github.com/youngjuning/blog/issues/7">React Native 解决Image 圆角在安卓上面没效果</a></li><li><a href="https://github.com/youngjuning/blog/issues/6">React Native 处理Android系统上文字偏下的问题</a></li><li><a href="https://github.com/youngjuning/blog/issues/5">React Native 自定义 TextInput 高度的问题</a></li><li><a href="https://github.com/youngjuning/blog/issues/32">[Android] Using TextInput inside ViewPagerAndroid causes context menu (copy&#x2F;paste) in some cases to not display</a></li></ul><h2 id="VsCode-插件推荐"><a href="#VsCode-插件推荐" class="headerlink" title="VsCode 插件推荐"></a>VsCode 插件推荐</h2><ul><li>React Native TypeScript Snippets: 本人开发，主要服务于本文的架构，帮助开发者快速搭建页面和组件，凡是重复的工作都可以靠工具来解决，一个不够，就来俩！！！</li><li>ES7 React&#x2F;Redux&#x2F;GraphQL&#x2F;React-Native Snippets</li></ul><h2 id="性能优化"><a href="#性能优化" class="headerlink" title="性能优化"></a>性能优化</h2><h3 id="自动-remove-console-语句"><a href="#自动-remove-console-语句" class="headerlink" title="自动 remove console 语句"></a>自动 remove console 语句</h3><p>准确地说，是在正式环境下删除 <code>console</code> 语句，配置方式请参考 <a href="https://bre.is/2CTUZyUj">自动 remove console 语句</a></p><h3 id="推荐阅读-1"><a href="#推荐阅读-1" class="headerlink" title="推荐阅读"></a>推荐阅读</h3><ul><li><a href="https://juejin.im/post/5e1676e16fb9a04847095b12">React Native 性能优化指南【全网最全，值得收藏】</a></li></ul><h2 id="应用内测"><a href="#应用内测" class="headerlink" title="应用内测"></a>应用内测</h2><ul><li><a href="https://fir.im/">fir</a>: 免费应用内测托管平台</li><li><a href="https://www.pgyer.com/">蒲公英</a>: 免费的苹果ios应用app内测分发托管、android安卓app内测分发托管、ios企业签名、ios专属签名、ios超级签名、ios企业账号</li></ul><h2 id="安卓必知必会"><a href="#安卓必知必会" class="headerlink" title="安卓必知必会"></a>安卓必知必会</h2><h3 id="配置应用名"><a href="#配置应用名" class="headerlink" title="配置应用名"></a>配置应用名</h3><p>很简单,我们直接打开 <code>android/app/src/main/res/values/strings.xml</code>，即可看到配置中的 <code>app_name</code>，修改为你想要的即可。</p><blockquote><p>你可以在初始化项目的时候指定应用的名字，像这样：<code>npx react-native init MyApp --title 掘金</code></p></blockquote><h3 id="配置图标"><a href="#配置图标" class="headerlink" title="配置图标"></a>配置图标</h3><p>1、使用<a href="https://icon.wuruihong.com/">图标工厂</a>、<a href="https://www.npmjs.com/package/react-native-svg-app-icon">react-native-svg-app-icon</a> 或者让设计师给图片</p><p>2、在 <code>android\app\src\main\res\mipmap-xxxxxx</code> 中直接覆盖图标就可以，注意图标的大小。</p><h3 id="打包-APK"><a href="#打包-APK" class="headerlink" title="打包 APK"></a>打包 APK</h3><p>1、在项目根目录执行 <code>keytool -genkeypair -v -keystore release.keystore -alias my-key-alias -keyalg RSA -keysize 2048 -validity 10000</code> 生成密钥文件 <code>release.keystore</code></p><p>2、把 <code>release.keystore</code> 文件放到你工程中的 <code>android/app</code> 文件夹下。</p><p>3、配置 <code>android/app/build.gradle</code></p><figure class="highlight groovy"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line">android&#123;</span><br><span class="line">    ...</span><br><span class="line">    signingConfigs &#123;</span><br><span class="line">        release &#123;</span><br><span class="line">            storeFile file(<span class="string">&quot;release.keystore&quot;</span>)</span><br><span class="line">            storePassword <span class="string">&quot;****&quot;</span></span><br><span class="line">            keyAlias <span class="string">&quot;my-key-alias&quot;</span></span><br><span class="line">            keyPassword <span class="string">&quot;****&quot;</span></span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    ...</span><br><span class="line">    buildTypes &#123;</span><br><span class="line">        release &#123;</span><br><span class="line">            ...</span><br><span class="line">            signingConfig signingConfigs.release</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></pre></td></tr></table></figure><h3 id="打包优化"><a href="#打包优化" class="headerlink" title="打包优化"></a>打包优化</h3><h4 id="去除无用的语言资源"><a href="#去除无用的语言资源" class="headerlink" title="去除无用的语言资源"></a>去除无用的语言资源</h4><p>通过配置 <code>android/defaultConfig/resConfigs</code> 可以选择只打包哪几种语言，进而去掉各种 <code>aar</code> 包中全世界的语言，尤其是 <code>support</code> 包中的。</p><p>选择保留什么语言要根据产品的用户和市场来定，如果只选择默认英语和中文语言，配置如下：</p><figure class="highlight groovy"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">defaultConfig &#123;</span><br><span class="line">resConfigs <span class="string">&quot;en&quot;</span>,<span class="string">&quot;zh&quot;</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="配置-PackagingOptions"><a href="#配置-PackagingOptions" class="headerlink" title="配置 PackagingOptions"></a>配置 PackagingOptions</h4><p>打开 <code>android/app/build.gradle</code> 文件，添加如下配置：</p><figure class="highlight groovy"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">packagingOptions &#123;</span><br><span class="line">    exclude <span class="string">&#x27;META-INF/android_release.kotlin_module&#x27;</span></span><br><span class="line">    exclude <span class="string">&#x27;META-INF/DEPENDENCIES&#x27;</span></span><br><span class="line">    exclude <span class="string">&#x27;META-INF/LICENSE&#x27;</span></span><br><span class="line">    exclude <span class="string">&#x27;META-INF/LICENSE.txt&#x27;</span></span><br><span class="line">    exclude <span class="string">&#x27;META-INF/license.txt&#x27;</span></span><br><span class="line">    exclude <span class="string">&#x27;META-INF/NOTICE&#x27;</span></span><br><span class="line">    exclude <span class="string">&#x27;META-INF/NOTICE.txt&#x27;</span></span><br><span class="line">    exclude <span class="string">&#x27;META-INF/notice.txt&#x27;</span></span><br><span class="line">    exclude <span class="string">&#x27;META-INF/ASL2.0&#x27;</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li><code>pickFirsts</code>: 当出现重复文件，会使用第一个匹配的文件打包进入 apk</li><li><code>merges</code>: 当出现重复文件，合并重复的文件打入 apk</li><li><code>excludes</code>: 打包的时候排除匹配的文件</li></ul><h4 id="配置-splits"><a href="#配置-splits" class="headerlink" title="配置 splits"></a>配置 splits</h4><blockquote><p>查看手机 CPU 信息：<code>adb shell</code> -&gt; <code>cd /proc</code> -&gt; <code>cat cpuinfo</code></p></blockquote><p>默认情况下，生成的 <code>APK</code> 会同时包含针对于 <code>x86</code> 和 <code>ARMv7a</code> 两种 <code>CPU</code> 架构的原生代码。这样可以让我们更方便的向其他人分享这个 <code>APK</code>，因为它几乎可以运行在所有的 Android 设备上。但是，这会导致所有设备上都有一些根本不会运行的代码，白白占据了空间。目前安卓设备绝大多数是 <code>ARM</code> 架构，因此对于大部分应用来说可以考虑去掉 <code>x86</code> 架构的支持。</p><p>你可以在 <code>android/app/build.gradle</code> 中修改如下代码：</p><figure class="highlight diff"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="deletion">- include &quot;armeabi-v7a&quot;, &quot;x86&quot;, &quot;arm64-v8a&quot;, &quot;x86_64&quot;</span></span><br><span class="line"><span class="addition">+ include &quot;armeabi-v7a&quot;, &quot;arm64-v8a&quot;</span></span><br><span class="line"><span class="deletion">- def versionCodes = [&quot;armeabi-v7a&quot;:1, &quot;x86&quot;:2, &quot;arm64-v8a&quot;: 3, &quot;x86_64&quot;: 4]</span></span><br><span class="line"><span class="addition">+ def versionCodes = [&quot;armeabi-v7a&quot;:1, &quot;arm64-v8a&quot;: 2]</span></span><br></pre></td></tr></table></figure><h3 id="gradle-编译速度优化配置"><a href="#gradle-编译速度优化配置" class="headerlink" title="gradle 编译速度优化配置"></a>gradle 编译速度优化配置</h3><p>在 <code>android\gradle.properties</code> 中加入以下配置：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"># 让gradle使用单独的守护进程</span><br><span class="line">org.gradle.daemon=true</span><br><span class="line"># 让gradle并行编译</span><br><span class="line">org.gradle.parallel=true</span><br><span class="line"># 让gradle在需要的时候才配置</span><br><span class="line">org.gradle.configureondemand=true</span><br><span class="line"># 增加gradle运行的java虚拟机的大小</span><br><span class="line">org.gradle.jvmargs=-Xmx4096m -XX:MaxPermSize=4096m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8</span><br></pre></td></tr></table></figure><h3 id="BuildConfig"><a href="#BuildConfig" class="headerlink" title="BuildConfig"></a>BuildConfig</h3><blockquote><p>在 react-native 中，我们可以借助 react-native-config-reader 来方便地读取这些属性</p></blockquote><p><code>BuildConfig</code> 是程序编译后，根据 <code>buildType</code> 生成在 <code>app\build\generated\source\buildConfig\debug(release)\</code> 包名下的一个 java 文件。默认有一下属性：</p><ul><li><code>DEBUG</code>：是否是调试版本</li><li><code>APPLICATION_ID</code>：当前应用的包名</li><li><code>FLAVOR</code>：产品（渠道包的名称）</li><li><code>BUILD_TYPE</code>：当前的编译类型(<code>release</code>&#x2F;<code>debug</code>)</li><li><code>VERSION_CODE</code>：版本号(数字)</li><li><code>VERSION_NAME</code>：版本号</li></ul><h4 id="自定义-BuildConfig"><a href="#自定义-BuildConfig" class="headerlink" title="自定义 BuildConfig"></a>自定义 BuildConfig</h4><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">defaultConfig &#123;</span><br><span class="line">  <span class="comment">// 三个参数: 1.要定义的常量的类型 2.该常量的命名 3.该常量的值</span></span><br><span class="line">  <span class="comment">// APP_NAME，对应 ios 的 CFBundleDisplayName</span></span><br><span class="line">  buildConfigField <span class="string">&quot;String&quot;</span>, <span class="string">&quot;APP_NAME&quot;</span>, <span class="string">&#x27;&quot;我是谁&quot;&#x27;</span></span><br><span class="line">  <span class="comment">// BUILD_TIME</span></span><br><span class="line">  buildConfigField <span class="string">&quot;String&quot;</span>, <span class="string">&quot;BUILD_TIME&quot;</span>, <span class="string">&#x27;&quot;&#x27;</span> + <span class="keyword">new</span> <span class="title class_">Date</span>().format(<span class="string">&quot;yyyy-MM-dd HH:mm:ss&quot;</span>, TimeZone.getTimeZone(<span class="string">&quot;Asia/Shanghai&quot;</span>)) + <span class="string">&#x27;&quot;&#x27;</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="在子模块中取主项目的-BuildConfig"><a href="#在子模块中取主项目的-BuildConfig" class="headerlink" title="在子模块中取主项目的 BuildConfig"></a>在子模块中取主项目的 BuildConfig</h4><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line">...</span><br><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> Object <span class="title function_">getBuildConfigValue</span><span class="params">(Context context, String fieldName)</span> &#123;</span><br><span class="line">  <span class="keyword">try</span> &#123;</span><br><span class="line">    Class&lt;?&gt; clazz = Class.forName(context.getPackageName() + <span class="string">&quot;.BuildConfig&quot;</span>);</span><br><span class="line">    <span class="type">Field</span> <span class="variable">field</span> <span class="operator">=</span> clazz.getField(fieldName);</span><br><span class="line">    <span class="keyword">return</span> field.get(<span class="literal">null</span>);</span><br><span class="line">  &#125; <span class="keyword">catch</span> (ClassNotFoundException e) &#123;</span><br><span class="line">    e.printStackTrace();</span><br><span class="line">  &#125; <span class="keyword">catch</span> (NoSuchFieldException e) &#123;</span><br><span class="line">    e.printStackTrace();</span><br><span class="line">  &#125; <span class="keyword">catch</span> (IllegalAccessException e) &#123;</span><br><span class="line">    e.printStackTrace();</span><br><span class="line">  &#125;</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="type">String</span> <span class="variable">versionName</span> <span class="operator">=</span> (String)getBuildConfigValue(activity, <span class="string">&quot;VERSION_NAME&quot;</span>))</span><br></pre></td></tr></table></figure><h3 id="解决在-Android-P-上的提醒弹窗-（Detected-problems-with-API-compatibility-visit-g-co-dev-appcompat-for-more-info"><a href="#解决在-Android-P-上的提醒弹窗-（Detected-problems-with-API-compatibility-visit-g-co-dev-appcompat-for-more-info" class="headerlink" title="解决在 Android P 上的提醒弹窗 （Detected problems with API compatibility(visit g.co&#x2F;dev&#x2F;appcompat for more info)"></a>解决在 Android P 上的提醒弹窗 （Detected problems with API compatibility(visit g.co&#x2F;dev&#x2F;appcompat for more info)</h3><p>在 <code>MainActivity.java</code> 中添加 closeAndroidPDialog 方法并在 <code>onCreate</code> 方法中调用</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> android.os.Bundle;</span><br><span class="line"><span class="keyword">import</span> java.lang.reflect.Constructor;</span><br><span class="line"><span class="keyword">import</span> java.lang.reflect.Field;</span><br><span class="line"><span class="keyword">import</span> java.lang.reflect.Method;</span><br><span class="line">...</span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">protected</span> <span class="keyword">void</span> <span class="title function_">onCreate</span><span class="params">(Bundle savedInstanceState)</span> &#123;</span><br><span class="line">  closeAndroidPDialog(); <span class="comment">// here</span></span><br><span class="line">  <span class="built_in">super</span>.onCreate(savedInstanceState);</span><br><span class="line">&#125;</span><br><span class="line">...</span><br><span class="line"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title function_">closeAndroidPDialog</span><span class="params">()</span>&#123;</span><br><span class="line">  <span class="keyword">try</span> &#123;</span><br><span class="line">    <span class="type">Class</span> <span class="variable">aClass</span> <span class="operator">=</span> Class.forName(<span class="string">&quot;android.content.pm.PackageParser$Package&quot;</span>);</span><br><span class="line">    <span class="type">Constructor</span> <span class="variable">declaredConstructor</span> <span class="operator">=</span> aClass.getDeclaredConstructor(String.class);</span><br><span class="line">    declaredConstructor.setAccessible(<span class="literal">true</span>);</span><br><span class="line">  &#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">    e.printStackTrace();</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">try</span> &#123;</span><br><span class="line">    <span class="type">Class</span> <span class="variable">cls</span> <span class="operator">=</span> Class.forName(<span class="string">&quot;android.app.ActivityThread&quot;</span>);</span><br><span class="line">    <span class="type">Method</span> <span class="variable">declaredMethod</span> <span class="operator">=</span> cls.getDeclaredMethod(<span class="string">&quot;currentActivityThread&quot;</span>);</span><br><span class="line">    declaredMethod.setAccessible(<span class="literal">true</span>);</span><br><span class="line">    <span class="type">Object</span> <span class="variable">activityThread</span> <span class="operator">=</span> declaredMethod.invoke(<span class="literal">null</span>);</span><br><span class="line">    <span class="type">Field</span> <span class="variable">mHiddenApiWarningShown</span> <span class="operator">=</span> cls.getDeclaredField(<span class="string">&quot;mHiddenApiWarningShown&quot;</span>);</span><br><span class="line">    mHiddenApiWarningShown.setAccessible(<span class="literal">true</span>);</span><br><span class="line">    mHiddenApiWarningShown.setBoolean(activityThread, <span class="literal">true</span>);</span><br><span class="line">  &#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">    e.printStackTrace();</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>配置 <code>android/app/build.gradle</code>:</p><figure class="highlight groovy"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">...</span><br><span class="line"><span class="keyword">def</span> releaseTime() &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">new</span> Date().format(<span class="string">&quot;yyyyMMdd-HHmmss&quot;</span>, TimeZone.getTimeZone(<span class="string">&quot;GMT+08:00&quot;</span>))</span><br><span class="line">&#125;</span><br><span class="line">...</span><br><span class="line"><span class="attr">android:</span> &#123;</span><br><span class="line">    applicationVariants.all &#123; variant -&gt;</span><br><span class="line">        ...</span><br><span class="line">    variant.outputs.all &#123;</span><br><span class="line">            <span class="comment">// the apk name is e.g. galaxy_v1.0.1_2018-11-1_debug.apk</span></span><br><span class="line">           outputFileName = <span class="string">&quot;galaxy_v$&#123;defaultConfig.versionName&#125;_$&#123;releaseTime()&#125;_$&#123;variant.buildType.name&#125;.apk&quot;</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="常见报错及解决办法"><a href="#常见报错及解决办法" class="headerlink" title="常见报错及解决办法"></a>常见报错及解决办法</h3><h4 id="Failed-to-read-PNG-signature-file-does-not-start-with-PNG-signature"><a href="#Failed-to-read-PNG-signature-file-does-not-start-with-PNG-signature" class="headerlink" title="Failed to read PNG signature: file does not start with PNG signature"></a>Failed to read PNG signature: file does not start with PNG signature</h4><p>有时从网上下载的 Demo 资源文件不规范，会出现直接将 jpg 文件改为 png 后缀名的情况，gradle 打包检查时报错编译通不过的。我们通过 <code>aaptOptions.cruncherEnabled=false</code> 来禁止 Gradle 检查 png 的合法性：</p><figure class="highlight groovy"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">android &#123;</span><br><span class="line">  aaptOptions &#123;</span><br><span class="line">    cruncherEnabled=<span class="literal">false</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="com-android-build-api-transform-TransformException"><a href="#com-android-build-api-transform-TransformException" class="headerlink" title="com.android.build.api.transform.TransformException"></a><a href="http://t.cn/EZcTDtV">com.android.build.api.transform.TransformException</a></h4><p>在 <code>android\gradle.properties</code> 中加入以下配置：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">dexOptions.javaMaxHeapSize = 2g</span><br></pre></td></tr></table></figure><h4 id="The-number-of-method-references-in-a-dex-file-cannot-exceed-64K"><a href="#The-number-of-method-references-in-a-dex-file-cannot-exceed-64K" class="headerlink" title="The number of method references in a .dex file cannot exceed 64K."></a>The number of method references in a .dex file cannot exceed 64K.</h4><p>随着 Android 平台的持续成长，Android 应用的大小也在增加。当您的应用及其引用的库达到特定大小时，您会遇到构建错误，指明您的应用已达到 Android 应用构建架构的极限。</p><p>解决办法是配置您的应用进行 <code>Dalvik</code> 可执行文件分包，在 <code>android/app/build.gradle</code> 中做下面的配置：</p><figure class="highlight diff"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">defaultConfig &#123;</span><br><span class="line"><span class="addition">+    multiDexEnabled true</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="ios必知必会"><a href="#ios必知必会" class="headerlink" title="ios必知必会"></a>ios必知必会</h2><blockquote><p>这部分目前作者接触不深，有大佬可以联系我补充！！！</p></blockquote><h3 id="镜像"><a href="#镜像" class="headerlink" title="镜像"></a>镜像</h3><blockquote><p><code>pod install</code> 会从 GitHub 拉代码，所以在国内会很慢。年轻人要爱国，自觉不翻墙！！！</p></blockquote><ul><li><a href="https://github.com/youngjuning/blog/issues/10">Homebrew 镜像使用帮助</a></li><li><a href="https://mirror.tuna.tsinghua.edu.cn/help/CocoaPods/">CocoaPods 镜像使用帮助</a></li></ul><h3 id="配置应用名-1"><a href="#配置应用名-1" class="headerlink" title="配置应用名"></a>配置应用名</h3><ol><li>选中工程名称</li><li>找到右侧菜单 <code>Info</code> 选项</li><li>添加 <code>Bundle display name</code> 并把 <code>value</code> 设置成 “应用名称”</li><li>最后重新再 <code>Run</code>一次，即可看到最新的效果</li></ol><blockquote><p>你可以在初始化项目的时候指定应用的名字，像这样：<code>npx react-native init MyApp --title 掘金</code></p></blockquote><h3 id="配置图标-1"><a href="#配置图标-1" class="headerlink" title="配置图标"></a>配置图标</h3><p>1、使用<a href="https://icon.wuruihong.com/">图标工厂</a>、<a href="https://www.npmjs.com/package/react-native-svg-app-icon">react-native-svg-app-icon</a> 或者让设计师给图片<br>2、把准备好的图标拖到图中箭头指向的位置：</p><img src="https://i.loli.net/2020/02/21/Sy3OjWBKbMdYgoH.png" style="zoom:36%;" /><h3 id="获取BUILD-TYPE"><a href="#获取BUILD-TYPE" class="headerlink" title="获取BUILD_TYPE"></a>获取BUILD_TYPE</h3><p>在 <code>Info.plist</code> 中添加 <code>BUILD_TYPE</code>，取值为 <code>$(CONFIGURATION)</code></p><h3 id="获取构建时间"><a href="#获取构建时间" class="headerlink" title="获取构建时间"></a>获取构建时间</h3><p>在 <code>Info.plist</code> 中添加 <code>BUILD_TIME</code>,取值为空，并通过脚本在每次编译的时候对其更新，脚本添加步骤 <code>Target</code>-&gt; <code>Build Phases</code> -&gt; <code>+</code> -&gt; <code>New Run Script Phase</code>, Shell 代码如下：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">#</span><span class="language-bash">!/bin/bash</span></span><br><span class="line">infoplist=&quot;$BUILT_PRODUCTS_DIR/$INFOPLIST_PATH&quot;</span><br><span class="line">builddate=`date +%Y-%m-%d_%H:%M`</span><br><span class="line">if [[ -n &quot;$builddate&quot; ]]; then</span><br><span class="line">/usr/libexec/PlistBuddy -c &quot;Set :BUILD_TIME $builddate&quot; $&#123;infoplist&#125;</span><br><span class="line">fi</span><br></pre></td></tr></table></figure><h3 id="权限申请"><a href="#权限申请" class="headerlink" title="权限申请"></a>权限申请</h3><ul><li><code>Privacy - Camera Usage Description</code></li><li><code>Privacy - Photo Library Usage Description</code></li><li><code>Privacy - Microphone Usage Description</code></li></ul><h2 id="Lean-Core"><a href="#Lean-Core" class="headerlink" title="Lean Core"></a>Lean Core</h2><table><thead><tr><th align="left">COMPONENT</th><th align="left">DEPRECATED?</th><th align="left">NEW HOME</th></tr></thead><tbody><tr><td align="left"><strong>AsyncStorage</strong></td><td align="left">0.59</td><td align="left"><a href="https://github.com/react-native-community/react-native-async-storage">@react-native-community&#x2F;react-native-async-storage</a></td></tr><tr><td align="left"><strong>ImageStore</strong></td><td align="left">0.59</td><td align="left"><a href="https://github.com/expo/expo/tree/master/packages/expo-file-system">expo-file-system</a> or <a href="https://github.com/itinance/react-native-fs">react-native-fs</a></td></tr><tr><td align="left"><strong>MaskedViewIOS</strong></td><td align="left">0.59</td><td align="left"><a href="https://github.com/react-native-community/react-native-masked-view">@react-native-community&#x2F;react-native-masked-view</a></td></tr><tr><td align="left"><strong>NetInfo</strong></td><td align="left">0.59</td><td align="left"><a href="https://github.com/react-native-community/react-native-netinfo">@react-native-community&#x2F;react-native-netinfo</a></td></tr><tr><td align="left"><strong>Slider</strong></td><td align="left">0.59</td><td align="left"><a href="https://github.com/react-native-community/react-native-slider">@react-native-community&#x2F;react-native-slider</a></td></tr><tr><td align="left"><strong>ViewPagerAndroid</strong></td><td align="left">0.59</td><td align="left"><a href="https://github.com/react-native-community/react-native-viewpager">@react-native-community&#x2F;react-native-viewpager</a></td></tr><tr><td align="left"><strong>WebView</strong></td><td align="left">0.60</td><td align="left"><a href="https://github.com/react-native-community/react-native-webview">react-native-webview</a></td></tr><tr><td align="left"><strong>NetInfo</strong></td><td align="left">0.60</td><td align="left"><a href="https://github.com/react-native-community/react-native-netinfo">@react-native-community&#x2F;netinfo</a></td></tr><tr><td align="left"><strong>Geolocation</strong></td><td align="left">0.60</td><td align="left"><a href="https://github.com/react-native-community/react-native-geolocation">@react-native-community&#x2F;geolocation</a></td></tr><tr><td align="left"><strong>Apple TV Support</strong></td><td align="left">0.62</td><td align="left"><a href="https://github.com/react-native-community/react-native-tvos">react-native-community&#x2F;react-native-tvos</a></td></tr></tbody></table><h2 id="联系作者"><a href="#联系作者" class="headerlink" title="联系作者"></a>联系作者</h2><table><thead><tr><th align="center">作者微信</th><th align="center">知识星球</th><th align="center">赞赏作者</th></tr></thead><tbody><tr><td align="center"><img src="https://user-gold-cdn.xitu.io/2020/2/24/17074acbb24c7412?w=200&h=200&f=jpeg&s=17183" style="width:200px"/></td><td align="center"><img src="https://user-gold-cdn.xitu.io/2020/2/24/17074acbb26af8e1?w=200&h=200&f=png&s=39093" style="width:200px"/></td><td align="center"><img src="https://user-gold-cdn.xitu.io/2020/2/24/17074acbb338c643?w=698&h=700&f=png&s=315492" style="width:200px"/></td></tr></tbody></table>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/f5a60d0347b3/</id>
    <link href="https://blog.zisheng.pro/posts/f5a60d0347b3/"/>
    <published>2020-02-23T09:26:09.000Z</published>
    <summary>本文记录了一些 React Native 架构方案的最佳实践</summary>
    <title>React Native 架构面面观</title>
    <updated>2026-09-18T08:15:28.632Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="React Native" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/React-Native/"/>
    <category term="React Native" scheme="https://blog.zisheng.pro/tags/React-Native/"/>
    <category term="启动屏" scheme="https://blog.zisheng.pro/tags/%E5%90%AF%E5%8A%A8%E5%B1%8F/"/>
    <category term="React Native Splash screen" scheme="https://blog.zisheng.pro/tags/React-Native-Splash-screen/"/>
    <category term="React Native 启动白屏" scheme="https://blog.zisheng.pro/tags/React-Native-%E5%90%AF%E5%8A%A8%E7%99%BD%E5%B1%8F/"/>
    <content>
      <![CDATA[<p>如果你有设计师，请让设计师给你要的所有尺寸图，如果没有请自行使用 <a href="https://icon.wuruihong.com/">图标工厂</a> 一键生成所有尺寸的图标&#x2F;启动图。</p><h2 id="安装依赖"><a href="#安装依赖" class="headerlink" title="安装依赖"></a>安装依赖</h2><p>$ yarn add react-native-splash-screen</p><h2 id="Android"><a href="#Android" class="headerlink" title="Android"></a>Android</h2><p>1、通过创建 <code>launch_screen.png</code> 文件并把它们放到 <code>mipmap-</code> 文件夹下来自定义你的启动图。安卓会自动选择合适的分辨率，因此你不是必须为所有手机分辨率提供图片。不过，你可以为以下所有分辨率提供启动图：</p><ul><li><code>mipmap-mdpi</code></li><li><code>mipmap-hdpi</code></li><li><code>mipmap-xhdpi</code></li><li><code>mipmap-xxhdpi</code></li><li><code>mipmap-xxxhdpi</code></li></ul><p>2、更新你的 <code>MainActivity.java</code> 文件如下：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> android.os.Bundle;</span><br><span class="line"><span class="keyword">import</span> org.devio.rn.splashscreen.SplashScreen;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">MainActivity</span> <span class="keyword">extends</span> <span class="title class_">ReactActivity</span> &#123;</span><br><span class="line">   <span class="meta">@Override</span></span><br><span class="line">   <span class="keyword">protected</span> <span class="keyword">void</span> <span class="title function_">onCreate</span><span class="params">(Bundle savedInstanceState)</span> &#123;</span><br><span class="line">     SplashScreen.show(<span class="built_in">this</span>, <span class="literal">true</span>);  <span class="comment">// 添加这一句</span></span><br><span class="line">     <span class="built_in">super</span>.onCreate(savedInstanceState);</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="comment">// ...other code</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>2、创建一个名为 <code>launch_screen.xml</code> 的布局文件来自定义你的启动屏幕。</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">&lt;?xml version=<span class="string">&quot;1.0&quot;</span> encoding=<span class="string">&quot;utf-8&quot;</span>?&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">RelativeLayout</span> <span class="attr">xmlns:android</span>=<span class="string">&quot;http://schemas.android.com/apk/res/android&quot;</span></span></span><br><span class="line"><span class="tag">    <span class="attr">android:orientation</span>=<span class="string">&quot;vertical&quot;</span> <span class="attr">android:layout_width</span>=<span class="string">&quot;match_parent&quot;</span></span></span><br><span class="line"><span class="tag">    <span class="attr">android:layout_height</span>=<span class="string">&quot;match_parent&quot;</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">ImageView</span> <span class="attr">android:layout_width</span>=<span class="string">&quot;match_parent&quot;</span> <span class="attr">android:layout_height</span>=<span class="string">&quot;match_parent&quot;</span> <span class="attr">android:src</span>=<span class="string">&quot;@mipmap/launch_screen&quot;</span> <span class="attr">android:scaleType</span>=<span class="string">&quot;centerCrop&quot;</span> /&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">RelativeLayout</span>&gt;</span></span><br></pre></td></tr></table></figure><p>3、你也可以启用app主题透明选项来解决在APP启动时因主题原因导致的短暂白屏的问题,具体步骤如下:</p><p>打开 <code>android/app/src/main/res/values/styles.xml</code> 文件,添加 <code>&lt;item name=&quot;android:windowIsTranslucent&quot;&gt;true&lt;/item&gt;</code>,如下 :</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">resources</span>&gt;</span></span><br><span class="line">    <span class="comment">&lt;!-- Base application theme. --&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">style</span> <span class="attr">name</span>=<span class="string">&quot;AppTheme&quot;</span> <span class="attr">parent</span>=<span class="string">&quot;Theme.AppCompat.Light.NoActionBar&quot;</span>&gt;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">        <span class="comment">&lt;!-- Customize your theme here. --&gt;</span></span></span><br><span class="line"><span class="language-xml">        <span class="comment">&lt;!--设置透明背景--&gt;</span></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">item</span> <span class="attr">name</span>=<span class="string">&quot;android:windowIsTranslucent&quot;</span>&gt;</span>true<span class="tag">&lt;/<span class="name">item</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">    </span><span class="tag">&lt;/<span class="name">style</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">resources</span>&gt;</span></span><br></pre></td></tr></table></figure><p>4、Add a color called <code>primary_dark</code> in <code>app/src/main/res/values/colors.xml</code></p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">&lt;?xml version=<span class="string">&quot;1.0&quot;</span> encoding=<span class="string">&quot;utf-8&quot;</span>?&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">resources</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">color</span> <span class="attr">name</span>=<span class="string">&quot;primary_dark&quot;</span>&gt;</span>#000000<span class="tag">&lt;/<span class="name">color</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">resources</span>&gt;</span></span><br></pre></td></tr></table></figure><h2 id="iOS"><a href="#iOS" class="headerlink" title="iOS"></a>iOS</h2><h3 id="Xcode-11-配置-LaunchImage"><a href="#Xcode-11-配置-LaunchImage" class="headerlink" title="Xcode 11 配置 LaunchImage"></a>Xcode 11 配置 LaunchImage</h3><p>如果有设计师资源，请 ui 同学提供以下尺寸的图片</p><ul><li>640x960</li><li>640x1136</li><li>750x1334</li><li>828x1792</li><li>1125x2436</li><li>1242x2436</li></ul><p>如果你没有设计师资源，可以使用 <a href="https://icon.wuruihong.com/">图标工厂</a> 自行生成</p><p>1、添加 LaunchImage</p><p><img src="https://i.loli.net/2019/09/16/SpIYPmE7MHTzw6o.png" alt="紫升"></p><p>2、将准备好的图片拖到下图红框的区域</p><p><img src="https://i.loli.net/2019/09/16/bBO1u4PAaWLdMqV.png" alt="紫升"></p><p>3、你在 <code>buildSetting</code> 中搜索 <code>launch</code>,可看到 <code>Asset Catalog Launch Image Set Name</code> 你只要把对应的<code>LaunchImage</code> 名称设置上去就好了:</p><p><img src="https://i.loli.net/2020/02/21/dXqnrGc2sWK4aVP.png" alt="紫升"></p><p>4、清空 Launch Screen File</p><p><img src="https://i.loli.net/2020/02/21/oyFKjUY1RZwtf45.png" alt="紫升"></p><p>5、最后别忘了把 <code>Info.list</code> 的 <code>UILaunchStoryboardName</code> 删除:</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">key</span>&gt;</span>UILaunchStoryboardName<span class="tag">&lt;/<span class="name">key</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">string</span>&gt;</span>LaunchScreen<span class="tag">&lt;/<span class="name">string</span>&gt;</span></span><br></pre></td></tr></table></figure><h3 id="更新-AppDelegate-m"><a href="#更新-AppDelegate-m" class="headerlink" title="更新 AppDelegate.m"></a>更新 <code>AppDelegate.m</code></h3><figure class="highlight obj-c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#import <span class="string">&quot;AppDelegate.h&quot;</span></span></span><br><span class="line"></span><br><span class="line"><span class="meta">#import <span class="string">&lt;React/RCTBundleURLProvider.h&gt;</span></span></span><br><span class="line"><span class="meta">#import <span class="string">&lt;React/RCTRootView.h&gt;</span></span></span><br><span class="line"><span class="meta">#import <span class="string">&quot;RNSplashScreen.h&quot;</span>  <span class="comment">// 添加这一句</span></span></span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">@implementation</span> <span class="title">AppDelegate</span></span></span><br><span class="line"></span><br><span class="line">- (<span class="type">BOOL</span>)application:(<span class="built_in">UIApplication</span> *)application didFinishLaunchingWithOptions:(<span class="built_in">NSDictionary</span> *)launchOptions</span><br><span class="line">&#123;</span><br><span class="line">    <span class="comment">// ...other code</span></span><br><span class="line"></span><br><span class="line">    [RNSplashScreen show];  <span class="comment">// 添加这一句，这一句一定要在最后</span></span><br><span class="line">    <span class="keyword">return</span> <span class="literal">YES</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">@end</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><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">SplashScreen</span> <span class="keyword">from</span> <span class="string">&#x27;react-native-splash-screen&#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">class</span> <span class="title class_">WelcomePage</span> <span class="keyword">extends</span> <span class="title class_ inherited__">Component</span> &#123;</span><br><span class="line">    <span class="title function_">componentDidMount</span>(<span class="params"></span>) &#123;</span><br><span class="line">        <span class="title class_">SplashScreen</span>.<span class="title function_">hide</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><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="title function_">App</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">  <span class="title class_">React</span>.<span class="title function_">useEffect</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">    <span class="title class_">SplashScreen</span>.<span class="title function_">hide</span>()</span><br><span class="line">  &#125;, [])</span><br><span class="line">  <span class="keyword">return</span> (...)</span><br></pre></td></tr></table></figure>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/c948214d88f6/</id>
    <link href="https://blog.zisheng.pro/posts/c948214d88f6/"/>
    <published>2020-02-21T04:41:18.000Z</published>
    <summary>本文介绍了配置 React Native Splash Screen 防止启动白屏</summary>
    <title>配置 React Native Splash Screen 防止启动白屏</title>
    <updated>2026-09-18T08:15:28.640Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="React Native" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/React-Native/"/>
    <category term="React Native" scheme="https://blog.zisheng.pro/tags/React-Native/"/>
    <category term="排障" scheme="https://blog.zisheng.pro/tags/%E6%8E%92%E9%9A%9C/"/>
    <category term="无障碍" scheme="https://blog.zisheng.pro/tags/%E6%97%A0%E9%9A%9C%E7%A2%8D/"/>
    <category term="字体缩放" scheme="https://blog.zisheng.pro/tags/%E5%AD%97%E4%BD%93%E7%BC%A9%E6%94%BE/"/>
    <content>
      <![CDATA[<p>系统字体调大后，React Native 页面出现截断、按钮被顶出屏幕，是很常见的线上问题。最直接的修复是给 <code>Text</code> 或 <code>TextInput</code> 设置 <code>allowFontScaling={false}</code>；但这也会让低视力用户失去系统字号适配。关键不在于「能不能关」，而在于先判断页面是否真的需要固定的视觉尺度。</p><h2 id="一句话总结"><a href="#一句话总结" class="headerlink" title="一句话总结"></a>一句话总结</h2><p>默认尊重系统字体缩放；只有品牌字标、极小的辅助标识等无法随字号变化的元素，才做组件级限制。布局错位应优先通过弹性布局、换行和最小高度修复。</p><h2 id="先确认问题来自字体缩放"><a href="#先确认问题来自字体缩放" class="headerlink" title="先确认问题来自字体缩放"></a>先确认问题来自字体缩放</h2><p>在 iOS 的“辅助功能 → 显示与文字大小 → 更大字体”，或 Android 的“显示 → 字体大小”中调大字号，再检查：</p><ul><li>文本是否被固定高度容器裁切；</li><li>图文横向排列是否缺少 <code>flexShrink</code> 或换行；</li><li>输入框是否只按默认行高计算高度；</li><li>是否把字号、行高和容器高度写死成同一组像素值。</li></ul><p>这一步很重要：如果不在真实的大字体环境复现，给某个组件加 <code>allowFontScaling={false}</code> 往往只是碰巧掩盖了布局问题。</p><h2 id="推荐：只对必要组件限制"><a href="#推荐：只对必要组件限制" class="headerlink" title="推荐：只对必要组件限制"></a>推荐：只对必要组件限制</h2><p>例如品牌 Logo 旁的短标签，设计上不希望它改变尺寸，可以显式关闭：</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; <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">export</span> <span class="keyword">function</span> <span class="title function_">BrandMark</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">Text</span> <span class="attr">allowFontScaling</span>=<span class="string">&#123;false&#125;</span>&gt;</span>ZISHENG<span class="tag">&lt;/<span class="name">Text</span>&gt;</span></span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>对于信息密度较高、但仍希望兼顾可读性的文本，使用上限比完全关闭更合适：</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">&lt;<span class="title class_">Text</span> maxFontSizeMultiplier=&#123;<span class="number">1.3</span>&#125;&gt;</span><br><span class="line">  这段内容会跟随系统字号，但最大放大到 <span class="number">1.3</span> 倍。</span><br><span class="line">&lt;/<span class="title class_">Text</span>&gt;</span><br></pre></td></tr></table></figure><p><code>maxFontSizeMultiplier</code> 适合导航标题、列表副标题等空间受限区域；正文、表单说明和错误提示通常不应设置得过小。</p><h2 id="用布局解决，而不是禁止用户放大"><a href="#用布局解决，而不是禁止用户放大" class="headerlink" title="用布局解决，而不是禁止用户放大"></a>用布局解决，而不是禁止用户放大</h2><p>常见的横向信息行可以这样写：</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">&lt;<span class="title class_">View</span> style=&#123;&#123; <span class="attr">flexDirection</span>: <span class="string">&#x27;row&#x27;</span>, <span class="attr">alignItems</span>: <span class="string">&#x27;center&#x27;</span>, <span class="attr">gap</span>: <span class="number">8</span> &#125;&#125;&gt;</span><br><span class="line">  <span class="language-xml"><span class="tag">&lt;<span class="name">Icon</span> <span class="attr">name</span>=<span class="string">&quot;info&quot;</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;&#123;</span> <span class="attr">flex:</span> <span class="attr">1</span>, <span class="attr">flexShrink:</span> <span class="attr">1</span>, <span class="attr">lineHeight:</span> <span class="attr">22</span> &#125;&#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">&lt;/<span class="title class_">View</span>&gt;</span><br></pre></td></tr></table></figure><p>避免为文本行设置固定 <code>height</code>，并为多行文本留出合理 <code>lineHeight</code>。需要单行省略时，再明确设置 <code>numberOfLines={1}</code>，不要让裁切变成偶然结果。</p><h2 id="不建议的全局写法"><a href="#不建议的全局写法" class="headerlink" title="不建议的全局写法"></a>不建议的全局写法</h2><p>下面的写法确实能快速让全站不再缩放，但会覆盖所有文本，包括正文、表单和错误提示：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 不建议作为默认方案</span></span><br><span class="line"><span class="title class_">Text</span>.<span class="property">defaultProps</span> = &#123; ...<span class="title class_">Text</span>.<span class="property">defaultProps</span>, <span class="attr">allowFontScaling</span>: <span class="literal">false</span> &#125;</span><br><span class="line"><span class="title class_">TextInput</span>.<span class="property">defaultProps</span> = &#123; ...<span class="title class_">TextInput</span>.<span class="property">defaultProps</span>, <span class="attr">allowFontScaling</span>: <span class="literal">false</span> &#125;</span><br></pre></td></tr></table></figure><p>它只适合明确以固定字号为产品要求的封闭场景，例如内部大屏、受控硬件终端。普通 App 应把它视为最后手段，并在设计评审中确认无障碍取舍。</p><h2 id="我的判断框架"><a href="#我的判断框架" class="headerlink" title="我的判断框架"></a>我的判断框架</h2><p>值得上：组件级 <code>maxFontSizeMultiplier</code>、弹性布局与真机大字号回归测试。</p><p>可以再等：为了省事全局关闭 <code>allowFontScaling</code>。它会把短期布局成本转移给用户。</p>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/528f99f5addc/</id>
    <link href="https://blog.zisheng.pro/posts/528f99f5addc/"/>
    <published>2020-02-20T07:38:31.000Z</published>
    <summary>React Native 的 allowFontScaling 能修复字号变大后的布局问题，但全局关闭会伤害无障碍体验。本文给出排查路径、组件级处理方式与适合全局限制的边界。</summary>
    <title>React Native 字体缩放：别为了布局稳定一刀切关掉无障碍</title>
    <updated>2026-09-18T08:15:28.632Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="React Native" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/React-Native/"/>
    <category term="React Native" scheme="https://blog.zisheng.pro/tags/React-Native/"/>
    <category term="安卓" scheme="https://blog.zisheng.pro/tags/%E5%AE%89%E5%8D%93/"/>
    <content>
      <![CDATA[<h2 id="配置应用名"><a href="#配置应用名" class="headerlink" title="配置应用名"></a>配置应用名</h2><p>很简单,我们直接打开 <code>android/app/src/main/res/values/strings.xml</code>，即可看到配置中的 <code>app_name</code>，修改为你想要的即可。</p><h2 id="配置图标"><a href="#配置图标" class="headerlink" title="配置图标"></a>配置图标</h2><p>也很简单，在 <code>android\app\src\main\res\mipmap-xxxxxx</code> 中直接覆盖图标就可以，注意图标的大小。</p><h2 id="打包-APK"><a href="#打包-APK" class="headerlink" title="打包 APK"></a>打包 APK</h2><p>1、在项目根目录执行 <code>keytool -genkeypair -v -keystore release.keystore -alias my-key-alias -keyalg RSA -keysize 2048 -validity 10000</code> 生成密钥文件 <code>release.keystore</code></p><p>2、把 <code>release.keystore</code> 文件放到你工程中的 <code>android/app</code> 文件夹下。</p><p>3、配置 <code>android/app/build.gradle</code></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line">android&#123;</span><br><span class="line">    ...</span><br><span class="line">    signingConfigs &#123;</span><br><span class="line">        release &#123;</span><br><span class="line">            storeFile <span class="title function_">file</span><span class="params">(<span class="string">&quot;release.keystore&quot;</span>)</span></span><br><span class="line">            storePassword <span class="string">&quot;****&quot;</span></span><br><span class="line">            keyAlias <span class="string">&quot;my-key-alias&quot;</span></span><br><span class="line">            keyPassword <span class="string">&quot;****&quot;</span></span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    ...</span><br><span class="line">    buildTypes &#123;</span><br><span class="line">        release &#123;</span><br><span class="line">            ...</span><br><span class="line">            signingConfig signingConfigs.release</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></pre></td></tr></table></figure><h3 id="去除无用的语言资源"><a href="#去除无用的语言资源" class="headerlink" title="去除无用的语言资源"></a>去除无用的语言资源</h3><p>通过配置 <code>android/defaultConfig/resConfigs</code> 可以选择只打包哪几种语言，进而去掉各种 aar 包中全世界的语言，尤其是 support 包中的。</p><p>选择保留什么语言要根据产品的用户和市场来定，如果只选择默认英语和中文语言，配置如下：</p><figure class="highlight diff"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">defaultConfig &#123;</span><br><span class="line"><span class="addition">+    resConfigs &quot;en&quot;,&quot;zh&quot;</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="配置-PackagingOptions"><a href="#配置-PackagingOptions" class="headerlink" title="配置 PackagingOptions"></a>配置 PackagingOptions</h3><p>打开 <code>android/app/build.gradle</code> 文件，添加如下配置：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">packagingOptions &#123;</span><br><span class="line">    exclude &#x27;META-INF/android_release.kotlin_module&#x27;</span><br><span class="line">    exclude &#x27;META-INF/DEPENDENCIES&#x27;</span><br><span class="line">    exclude &#x27;META-INF/LICENSE&#x27;</span><br><span class="line">    exclude &#x27;META-INF/LICENSE.txt&#x27;</span><br><span class="line">    exclude &#x27;META-INF/license.txt&#x27;</span><br><span class="line">    exclude &#x27;META-INF/NOTICE&#x27;</span><br><span class="line">    exclude &#x27;META-INF/NOTICE.txt&#x27;</span><br><span class="line">    exclude &#x27;META-INF/notice.txt&#x27;</span><br><span class="line">    exclude &#x27;META-INF/ASL2.0&#x27;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li>pickFirsts: 当出现重复文件，会使用第一个匹配的文件打包进入 apk</li><li>merges: 当出现重复文件，合并重复的文件打入 apk</li><li>excludes: 打包的时候排除匹配的文件</li></ul><p><strong>参考链接</strong></p><ul><li><a href="http://t.cn/Ewt1xD2">PackagingOptions</a></li><li><a href="http://t.cn/AipuM9Ll">More than one file was found with OS independent path</a></li><li><a href="http://t.cn/AipuMfcH">More than one file was found with OS independent path ‘META-INF&#x2F;LICENSE’</a></li></ul><h3 id="配置-splits"><a href="#配置-splits" class="headerlink" title="配置 splits"></a>配置 splits</h3><blockquote><p>查看手机 CPU 信息：<code>adb shell</code> -&gt; <code>cd /proc</code> -&gt; <code>cat cpuinfo</code></p></blockquote><p>默认情况下，生成的 <code>APK</code> 会同时包含针对于 <code>x86</code> 和 <code>ARMv7a</code> 两种 <code>CPU</code> 架构的原生代码。这样可以让我们更方便的向其他人分享这个 <code>APK</code>，因为它几乎可以运行在所有的 Android 设备上。但是，这会导致所有设备上都有一些根本不会运行的代码，白白占据了空间。目前安卓设备绝大多数是 <code>ARM</code> 架构，因此对于大部分应用来说可以考虑去掉 <code>x86</code> 架构的支持。</p><p>你可以在 <code>android/app/build.gradle</code> 中修改如下代码：</p><figure class="highlight diff"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="deletion">- include &quot;armeabi-v7a&quot;, &quot;x86&quot;, &quot;arm64-v8a&quot;, &quot;x86_64&quot;</span></span><br><span class="line"><span class="addition">+ include &quot;armeabi-v7a&quot;, &quot;arm64-v8a&quot;</span></span><br><span class="line"><span class="deletion">- def versionCodes = [&quot;armeabi-v7a&quot;:1, &quot;x86&quot;:2, &quot;arm64-v8a&quot;: 3, &quot;x86_64&quot;: 4]</span></span><br><span class="line"><span class="addition">+ def versionCodes = [&quot;armeabi-v7a&quot;:1, &quot;arm64-v8a&quot;: 2]</span></span><br></pre></td></tr></table></figure><ul><li><p>abi: Application Binary Interface，针对不同的 <code>CPU</code> 架构生成 <code>APK</code> 以减小 <code>APK</code> 文件的大小</p><ul><li><code>mips/mips64</code>：极少用于手机，出发点是高性能,主要用于路由器、猫。</li><li><code>armeabi</code>：老版本 <code>ARMv5</code>，不支持硬件辅助浮点运算，支持所有的 <code>ARM</code> 设备。</li><li><code>x86</code>&#x2F;<code>x86_64</code>：<code>x86</code> 架构的手机的市场占有率很低，约为 1%左右。而且 <code>x86</code> 架构都包含 <code>ARM</code> 模拟层，兼容 <code>ARM</code> 类型的 <code>ABI</code>。注意，模拟器为 <code>x86</code> 架构。</li><li><code>arm64-v8a</code>：64 位 <code>ARM</code> 架构。可用 32 位模式运行 <code>armeabi-v7a</code> 和 <code>armeabi</code>。（所谓的 <code>ARMv8</code> 架构，就是在 <code>MIPS64</code> 架构上增加了 <code>ARMv7</code> 架构中已经拥有的的 <code>TrustZone</code> 技术、虚拟化技术及 <code>NEON advanced SIMD</code> 技术等特性，研发成的）</li><li><code>armeabi-v7a</code>：主流版本 <code>AMRv7</code>，2011 年 15 月以后的生产的大部分 Android 设备都使用它。</li></ul></li><li><p>density: 针对不同的分辨率生成 <code>APK</code> 以减小 <code>APK</code> 文件的大小</p></li></ul><h3 id="Failed-to-read-PNG-signature-file-does-not-start-with-PNG-signature"><a href="#Failed-to-read-PNG-signature-file-does-not-start-with-PNG-signature" class="headerlink" title="Failed to read PNG signature: file does not start with PNG signature"></a>Failed to read PNG signature: file does not start with PNG signature</h3><p>有时从网上下载的 Demo 资源文件不规范，会出现直接将 jpg 文件改为 png 后缀名的情况，gradle 打包检查时报错编译通不过的。我们通过 <code>aaptOptions.cruncherEnabled=false</code> 来禁止 Gradle 检查 png 的合法性：</p><figure class="highlight diff"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">android &#123;</span><br><span class="line">  aaptOptions &#123;</span><br><span class="line">    cruncherEnabled=false</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="配置-dexOptions-javaMaxHeapSize"><a href="#配置-dexOptions-javaMaxHeapSize" class="headerlink" title="配置 dexOptions.javaMaxHeapSize"></a>配置 dexOptions.javaMaxHeapSize</h3><blockquote><p>android studio 需要较大的内存才能正常编译项目，主要解决这个警告：<a href="http://t.cn/EZcTDtV">com.android.build.api.transform.TransformException</a></p></blockquote><p>在 <code>android\gradle.properties</code> 中加入以下配置：</p><figure class="highlight diff"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="addition">+ dexOptions.javaMaxHeapSize = 2g</span></span><br></pre></td></tr></table></figure><h3 id="gradle-优化配置"><a href="#gradle-优化配置" class="headerlink" title="gradle 优化配置"></a>gradle 优化配置</h3><p>在 <code>android\gradle.properties</code> 中加入以下配置：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"># 让gradle使用单独的守护进程</span><br><span class="line">org.gradle.daemon=true</span><br><span class="line"># 让gradle并行编译</span><br><span class="line">org.gradle.parallel=true</span><br><span class="line"># 让gradle在需要的时候才配置</span><br><span class="line">org.gradle.configureondemand=true</span><br><span class="line"># 增加gradle运行的java虚拟机的大小</span><br><span class="line">org.gradle.jvmargs=-Xmx3072m -XX:MaxPermSize=1024m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8</span><br></pre></td></tr></table></figure><h3 id="配置方法数超过-64K-的应用"><a href="#配置方法数超过-64K-的应用" class="headerlink" title="配置方法数超过 64K 的应用"></a>配置方法数超过 64K 的应用</h3><p>随着 Android 平台的持续成长，Android 应用的大小也在增加。当您的应用及其引用的库达到特定大小时，您会遇到构建错误，指明您的应用已达到 Android 应用构建架构的极限。会报告这一错误：</p><blockquote><p>The number of method references in a .dex file cannot exceed 64K.</p></blockquote><p>解决办法是配置您的应用进行 Dalvik 可执行文件分包，在 <code>android/app/build.gradle</code> 中做下面的配置：</p><figure class="highlight diff"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">defaultConfig &#123;</span><br><span class="line"><span class="addition">+    multiDexEnabled true</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="BuildConfig"><a href="#BuildConfig" class="headerlink" title="BuildConfig"></a>BuildConfig</h2><blockquote><p>在 react-native 中，我们可以借助 react-native-config-reader 来方便地读取这些属性</p></blockquote><p>BuildConfig 是程序编译后，根据 buildType 生成在 <code>app\build\generated\source\buildConfig\debug(release)\</code> 包名下的一个 java 文件。默认有一下属性：</p><ul><li>DEBUG：是否是调试版本</li><li>APPLICATION_ID：当前应用的包名</li><li>FLAVOR：产品（渠道包的名称）</li><li>BUILD_TYPE：当前的编译类型(release&#x2F;debug)</li><li>VERSION_CODE：版本号(数字)</li><li>VERSION_NAME：版本号</li></ul><h3 id="自定义-BuildConfig"><a href="#自定义-BuildConfig" class="headerlink" title="自定义 BuildConfig"></a>自定义 BuildConfig</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">defaultConfig &#123;</span><br><span class="line">  <span class="comment">// 三个参数: 1.要定义的常量的类型 2.该常量的命名 3.该常量的值</span></span><br><span class="line">  <span class="comment">// APP_NAME，对应 ios 的 CFBundleDisplayName</span></span><br><span class="line">  buildConfigField <span class="string">&quot;String&quot;</span>, <span class="string">&quot;APP_NAME&quot;</span>, <span class="string">&#x27;&quot;我是谁&quot;&#x27;</span></span><br><span class="line">  <span class="comment">// BUILD_TIME</span></span><br><span class="line">  buildConfigField <span class="string">&quot;String&quot;</span>, <span class="string">&quot;BUILD_TIME&quot;</span>, <span class="string">&#x27;&quot;&#x27;</span> + <span class="keyword">new</span> <span class="title class_">Date</span>().format(<span class="string">&quot;yyyy-MM-dd HH:mm:ss&quot;</span>, TimeZone.getTimeZone(<span class="string">&quot;Asia/Shanghai&quot;</span>)) + <span class="string">&#x27;&quot;&#x27;</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="在子模块中取主项目的-BuildConfig"><a href="#在子模块中取主项目的-BuildConfig" class="headerlink" title="在子模块中取主项目的 BuildConfig"></a>在子模块中取主项目的 BuildConfig</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line">...</span><br><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> Object <span class="title function_">getBuildConfigValue</span><span class="params">(Context context, String fieldName)</span> &#123;</span><br><span class="line">  <span class="keyword">try</span> &#123;</span><br><span class="line">    Class&lt;?&gt; clazz = Class.forName(context.getPackageName() + <span class="string">&quot;.BuildConfig&quot;</span>);</span><br><span class="line">    <span class="type">Field</span> <span class="variable">field</span> <span class="operator">=</span> clazz.getField(fieldName);</span><br><span class="line">    <span class="keyword">return</span> field.get(<span class="literal">null</span>);</span><br><span class="line">  &#125; <span class="keyword">catch</span> (ClassNotFoundException e) &#123;</span><br><span class="line">    e.printStackTrace();</span><br><span class="line">  &#125; <span class="keyword">catch</span> (NoSuchFieldException e) &#123;</span><br><span class="line">    e.printStackTrace();</span><br><span class="line">  &#125; <span class="keyword">catch</span> (IllegalAccessException e) &#123;</span><br><span class="line">    e.printStackTrace();</span><br><span class="line">  &#125;</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="type">String</span> <span class="variable">versionName</span> <span class="operator">=</span> (String)getBuildConfigValue(activity, <span class="string">&quot;VERSION_NAME&quot;</span>))</span><br></pre></td></tr></table></figure><ul><li><a href="https://www.jianshu.com/p/3d9b23afe514">BuildConfig 与 build.gradle 的关系</a></li><li><a href="https://www.jianshu.com/p/274c9d95cf76">Gradle 之 BuildConfig 自定义常量</a></li></ul><h2 id="解决在-Android-P-上的提醒弹窗-（Detected-problems-with-API-compatibility-visit-g-co-dev-appcompat-for-more-info"><a href="#解决在-Android-P-上的提醒弹窗-（Detected-problems-with-API-compatibility-visit-g-co-dev-appcompat-for-more-info" class="headerlink" title="解决在 Android P 上的提醒弹窗 （Detected problems with API compatibility(visit g.co&#x2F;dev&#x2F;appcompat for more info)"></a>解决在 Android P 上的提醒弹窗 （Detected problems with API compatibility(visit g.co&#x2F;dev&#x2F;appcompat for more info)</h2><p>在 <code>MainActivity.java</code> 中添加 closeAndroidPDialog 方法并在 <code>onCreate</code> 方法中调用</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> android.os.Bundle;</span><br><span class="line"><span class="keyword">import</span> java.lang.reflect.Constructor;</span><br><span class="line"><span class="keyword">import</span> java.lang.reflect.Field;</span><br><span class="line"><span class="keyword">import</span> java.lang.reflect.Method;</span><br><span class="line">...</span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">protected</span> <span class="keyword">void</span> <span class="title function_">onCreate</span><span class="params">(Bundle savedInstanceState)</span> &#123;</span><br><span class="line">  closeAndroidPDialog(); <span class="comment">// here</span></span><br><span class="line">  <span class="built_in">super</span>.onCreate(savedInstanceState);</span><br><span class="line">&#125;</span><br><span class="line">...</span><br><span class="line"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title function_">closeAndroidPDialog</span><span class="params">()</span>&#123;</span><br><span class="line">  <span class="keyword">try</span> &#123;</span><br><span class="line">    <span class="type">Class</span> <span class="variable">aClass</span> <span class="operator">=</span> Class.forName(<span class="string">&quot;android.content.pm.PackageParser$Package&quot;</span>);</span><br><span class="line">    <span class="type">Constructor</span> <span class="variable">declaredConstructor</span> <span class="operator">=</span> aClass.getDeclaredConstructor(String.class);</span><br><span class="line">    declaredConstructor.setAccessible(<span class="literal">true</span>);</span><br><span class="line">  &#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">    e.printStackTrace();</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">try</span> &#123;</span><br><span class="line">    <span class="type">Class</span> <span class="variable">cls</span> <span class="operator">=</span> Class.forName(<span class="string">&quot;android.app.ActivityThread&quot;</span>);</span><br><span class="line">    <span class="type">Method</span> <span class="variable">declaredMethod</span> <span class="operator">=</span> cls.getDeclaredMethod(<span class="string">&quot;currentActivityThread&quot;</span>);</span><br><span class="line">    declaredMethod.setAccessible(<span class="literal">true</span>);</span><br><span class="line">    <span class="type">Object</span> <span class="variable">activityThread</span> <span class="operator">=</span> declaredMethod.invoke(<span class="literal">null</span>);</span><br><span class="line">    <span class="type">Field</span> <span class="variable">mHiddenApiWarningShown</span> <span class="operator">=</span> cls.getDeclaredField(<span class="string">&quot;mHiddenApiWarningShown&quot;</span>);</span><br><span class="line">    mHiddenApiWarningShown.setAccessible(<span class="literal">true</span>);</span><br><span class="line">    mHiddenApiWarningShown.setBoolean(activityThread, <span class="literal">true</span>);</span><br><span class="line">  &#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">    e.printStackTrace();</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><ul><li>mdpi：320x480</li><li>hdpi：480x800</li><li>xhdpi：720x1280</li><li>xxhdpi：1080x1920</li><li>xxxhdpi：2160x3840</li></ul>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/e49e15e51f69/</id>
    <link href="https://blog.zisheng.pro/posts/e49e15e51f69/"/>
    <published>2020-02-20T06:57:55.000Z</published>
    <summary>本文记录了一些 React Native 安卓端必须掌握的知识</summary>
    <title>React Native 安卓必知必会</title>
    <updated>2026-09-18T08:15:28.632Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="JavaScript" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/JavaScript/"/>
    <category term="JavaScript" scheme="https://blog.zisheng.pro/tags/JavaScript/"/>
    <category term="apply" scheme="https://blog.zisheng.pro/tags/apply/"/>
    <category term="call" scheme="https://blog.zisheng.pro/tags/call/"/>
    <category term="bind" scheme="https://blog.zisheng.pro/tags/bind/"/>
    <category term="手写代码" scheme="https://blog.zisheng.pro/tags/%E6%89%8B%E5%86%99%E4%BB%A3%E7%A0%81/"/>
    <category term="Function.prototype.bind" scheme="https://blog.zisheng.pro/tags/Function-prototype-bind/"/>
    <category term="this" scheme="https://blog.zisheng.pro/tags/this/"/>
    <content>
      <![CDATA[<p><code>bind</code> 是在 ES5 加入的语法，具体如何使用 MDN 都有。但有一点需要额外注意一下：<code>bind</code> 会返回一个新的函数。也就是说：它会修改 <code>this</code> 的指向，并返回新的函数体。</p><h2 id="实现思路"><a href="#实现思路" class="headerlink" title="实现思路"></a>实现思路</h2><ol><li>改变 <code>this</code> 指向: 需要使用 <code>apply</code> 或 <code>call</code></li><li>返回新的函数体: 函数作为值返回</li></ol><h2 id="实现"><a href="#实现" class="headerlink" title="实现"></a>实现</h2><p>使用闭包保存 this 指针、上下文和参数，并将上下文和参数使用 apply 改变 this 指向。</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="title class_">Function</span>.<span class="property"><span class="keyword">prototype</span></span>.<span class="property">bind1</span> = <span class="keyword">function</span>(<span class="params">context</span>) &#123;</span><br><span class="line">  <span class="keyword">var</span> that = <span class="variable language_">this</span></span><br><span class="line">  <span class="keyword">return</span> <span class="keyword">function</span>(<span class="params"></span>) &#123;</span><br><span class="line">    that.<span class="title function_">apply</span>(context, <span class="variable language_">arguments</span>)</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>使用方式和原生 <code>bind()</code> 一样：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> person = &#123; <span class="attr">name</span>:<span class="string">&quot;紫升&quot;</span> &#125;</span><br><span class="line"><span class="keyword">function</span> <span class="title function_">sayHi</span>(<span class="params">name</span>) &#123;</span><br><span class="line">  <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="variable language_">this</span>, name)</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">var</span> pSay = sayHi.<span class="title function_">bind1</span>(person)</span><br></pre></td></tr></table></figure>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/ef0fa347e647/</id>
    <link href="https://blog.zisheng.pro/posts/ef0fa347e647/"/>
    <published>2020-02-19T07:28:58.000Z</published>
    <summary>bind 是在 ES5 加入的语法，具体如何使用 MDN 都有。但有一点需要额外注意一下：bind 会返回一个新的函数。也就是说：它会修改 this 的指向，并返回新的函数体。</summary>
    <title>手写 JavaScript bind 函数</title>
    <updated>2026-09-18T08:15:28.640Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="TypeScript" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/TypeScript/"/>
    <category term="TypeScript" scheme="https://blog.zisheng.pro/tags/TypeScript/"/>
    <category term="掘金" scheme="https://blog.zisheng.pro/tags/%E6%8E%98%E9%87%91/"/>
    <category term="interface" scheme="https://blog.zisheng.pro/tags/interface/"/>
    <category term="type" scheme="https://blog.zisheng.pro/tags/type/"/>
    <content>
      <![CDATA[<h2 id="相同点"><a href="#相同点" class="headerlink" title="相同点"></a>相同点</h2><h3 id="都可以描述一个对象或者函数"><a href="#都可以描述一个对象或者函数" class="headerlink" title="都可以描述一个对象或者函数"></a>都可以描述一个对象或者函数</h3><h4 id="interface"><a href="#interface" class="headerlink" title="interface"></a>interface</h4><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">interface</span> <span class="title class_">User</span> &#123;</span><br><span class="line">  <span class="attr">name</span>: <span class="built_in">string</span></span><br><span class="line">  <span class="attr">age</span>: <span class="built_in">number</span></span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">interface</span> <span class="title class_">SetUser</span> &#123;</span><br><span class="line">  (<span class="attr">name</span>: <span class="built_in">string</span>, <span class="attr">age</span>: <span class="built_in">number</span>) : <span class="built_in">void</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="type"><a href="#type" class="headerlink" title="type"></a>type</h4><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> <span class="title class_">User</span> &#123;</span><br><span class="line">  <span class="attr">name</span>: <span class="built_in">string</span></span><br><span class="line">  <span class="attr">age</span>: <span class="built_in">number</span></span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">type</span> <span class="title class_">SetUser</span> = <span class="function">(<span class="params">name:<span class="built_in">string</span>, age: <span class="built_in">number</span></span>) =&gt;</span> <span class="built_in">void</span></span><br></pre></td></tr></table></figure><h3 id="都允许拓展（extends）"><a href="#都允许拓展（extends）" class="headerlink" title="都允许拓展（extends）"></a>都允许拓展（<code>extends</code>）</h3><p><code>interface</code> 和 <code>type</code> 都可以扩展，并且两者并不是相互独立的，也就是说 <code>interface</code> 可以 <code>extends</code> <code>type</code>，<code>type</code> 也可以 <code>extends</code> <code>interface</code>。<strong>虽然效果差不多，但是两者语法不同</strong>。</p><h4 id="interface-extends-type"><a href="#interface-extends-type" class="headerlink" title="interface extends type"></a>interface extends type</h4><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> <span class="title class_">Name</span> = &#123;</span><br><span class="line">  <span class="attr">name</span>: <span class="built_in">string</span>;</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">interface</span> <span class="title class_">User</span> <span class="keyword">extends</span> <span class="title class_">Name</span> &#123;</span><br><span class="line">  <span class="attr">age</span>: <span class="built_in">number</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="type-extends-interface"><a href="#type-extends-interface" class="headerlink" title="type extends interface"></a>type extends interface</h4><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">interface</span> <span class="title class_">Name</span> &#123;</span><br><span class="line">  <span class="attr">name</span>: <span class="built_in">string</span>;</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">type</span> <span class="title class_">User</span> = <span class="title class_">Name</span> &amp; &#123;</span><br><span class="line">  <span class="attr">age</span>: <span class="built_in">number</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="type-可以而-interface-不行"><a href="#type-可以而-interface-不行" class="headerlink" title="type 可以而 interface 不行"></a>type 可以而 interface 不行</h3><h4 id="type-可以声明基本类型别名，联合类型，元组等类型"><a href="#type-可以声明基本类型别名，联合类型，元组等类型" class="headerlink" title="type 可以声明基本类型别名，联合类型，元组等类型"></a>type 可以声明基本类型别名，联合类型，元组等类型</h4><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 基本类型别名</span></span><br><span class="line"><span class="keyword">type</span> <span class="title class_">Name</span> = <span class="built_in">string</span></span><br><span class="line"><span class="comment">// 联合类型</span></span><br><span class="line"><span class="keyword">interface</span> <span class="title class_">Dog</span> &#123;</span><br><span class="line">  <span class="title function_">wong</span>()</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">interface</span> <span class="title class_">Cat</span> &#123;</span><br><span class="line">  <span class="title function_">miao</span>()</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">type</span> <span class="title class_">Pet</span> = <span class="title class_">Dog</span> | <span class="title class_">Cat</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 具体定义数组每个的类型</span></span><br><span class="line"><span class="keyword">type</span> <span class="title class_">PetList</span> = [<span class="title class_">Dog</span>, <span class="title class_">Pet</span>]</span><br></pre></td></tr></table></figure><h4 id="type-语句中还可以使用-typeof-获取实例的类型进行赋值"><a href="#type-语句中还可以使用-typeof-获取实例的类型进行赋值" class="headerlink" title="type 语句中还可以使用 typeof 获取实例的类型进行赋值"></a>type 语句中还可以使用 typeof 获取实例的类型进行赋值</h4><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 当你想获取一个变量的类型时，使用 typeof</span></span><br><span class="line"><span class="keyword">let</span> div = <span class="variable language_">document</span>.<span class="title function_">createElement</span>(<span class="string">&#x27;div&#x27;</span>)</span><br><span class="line"><span class="keyword">type</span> B = <span class="keyword">typeof</span> div</span><br></pre></td></tr></table></figure><h3 id="interface-可以而-type-不行"><a href="#interface-可以而-type-不行" class="headerlink" title="interface 可以而 type 不行"></a>interface 可以而 type 不行</h3><p>interface 能够声明合并</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">interface</span> <span class="title class_">User</span> &#123;</span><br><span class="line">  <span class="attr">name</span>: <span class="built_in">string</span></span><br><span class="line">  <span class="attr">age</span>: <span class="built_in">number</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_">User</span> &#123;</span><br><span class="line">  <span class="attr">sex</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="comment">/*</span></span><br><span class="line"><span class="comment">User 接口为 &#123;</span></span><br><span class="line"><span class="comment">  name: string</span></span><br><span class="line"><span class="comment">  age: number</span></span><br><span class="line"><span class="comment">  sex: string</span></span><br><span class="line"><span class="comment">&#125;</span></span><br><span class="line"><span class="comment">*/</span></span><br></pre></td></tr></table></figure><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>一般来说，如果不清楚什么时候用interface&#x2F;type，能用 interface 实现，就用 interface , 如果不能就用 type 。</p>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/a7d393e0caa2/</id>
    <link href="https://blog.zisheng.pro/posts/a7d393e0caa2/"/>
    <published>2020-02-18T17:26:43.000Z</published>
    <summary>本文介绍了 TypeScript 中 type 和 interface 的区别</summary>
    <title>TypeScript 中 type 和 interface 的区别</title>
    <updated>2026-09-18T08:15:28.632Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="微信小程序" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/%E5%BE%AE%E4%BF%A1%E5%B0%8F%E7%A8%8B%E5%BA%8F/"/>
    <category term="微信小程序" scheme="https://blog.zisheng.pro/tags/%E5%BE%AE%E4%BF%A1%E5%B0%8F%E7%A8%8B%E5%BA%8F/"/>
    <category term="微信小程序异步库" scheme="https://blog.zisheng.pro/tags/%E5%BE%AE%E4%BF%A1%E5%B0%8F%E7%A8%8B%E5%BA%8F%E5%BC%82%E6%AD%A5%E5%BA%93/"/>
    <category term="微信小程序promise" scheme="https://blog.zisheng.pro/tags/%E5%BE%AE%E4%BF%A1%E5%B0%8F%E7%A8%8B%E5%BA%8Fpromise/"/>
    <category term="微信小程序async/await" scheme="https://blog.zisheng.pro/tags/%E5%BE%AE%E4%BF%A1%E5%B0%8F%E7%A8%8B%E5%BA%8Fasync-await/"/>
    <content>
      <![CDATA[<p>✨强大、优雅的小程序异步库🚀 小程序promise</p><p><a href="https://nodei.co/npm/wx-promise-pro/"><img src="https://user-gold-cdn.xitu.io/2020/2/18/1705883821e962d8?w=297&h=45&f=png&s=3701" alt="NPM"></a></p><h2 id="优势"><a href="#优势" class="headerlink" title="优势"></a>优势</h2><ul><li>方便集成：一处引用，处处使用</li><li>把微信小程序所有异步 API promise 化并挂在到<code>wx.pro</code> 对象下</li><li>支持 ES2018 <code>finally</code> 特性</li><li>支持 TypeScript 开发</li></ul><h2 id="安装"><a href="#安装" class="headerlink" title="安装"></a>安装</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">$ npm i wx-promise-pro</span><br><span class="line"><span class="comment"># or</span></span><br><span class="line">$ yarn add wx-promise-pro</span><br></pre></td></tr></table></figure><h2 id="初始化"><a href="#初始化" class="headerlink" title="初始化"></a>初始化</h2><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; promisifyAll, promisify &#125; <span class="keyword">from</span> <span class="string">&#x27;wx-promise-pro&#x27;</span></span><br><span class="line"><span class="comment">// promisify all wx‘s api</span></span><br><span class="line"><span class="title function_">promisifyAll</span>()</span><br><span class="line"><span class="comment">// promisify single api</span></span><br><span class="line"><span class="title function_">promisify</span>(wx.<span class="property">getSystemInfo</span>)().<span class="title function_">then</span>(<span class="variable language_">console</span>.<span class="property">log</span>)</span><br></pre></td></tr></table></figure><h2 id="支持所有的微信小程序异步API"><a href="#支持所有的微信小程序异步API" class="headerlink" title="支持所有的微信小程序异步API"></a>支持所有的微信小程序异步API</h2><p>使用 <code>promisifyAll</code> 开发者无需关心兼容与否，只要是 <code>wx</code> 支持的 api，<code>wx.pro</code> 全部支持。</p><p><strong>示例代码：</strong></p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 演示 wxPromise 的能力</span></span><br><span class="line">wx.<span class="property">pro</span>.<span class="title function_">showLoading</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">mask</span>: <span class="literal">true</span></span><br><span class="line">&#125;)</span><br><span class="line">wx.<span class="property">pro</span>.<span class="title function_">request</span>(&#123;</span><br><span class="line">  <span class="attr">url</span>: <span class="string">&#x27;https://cnodejs.org/api/v1/topics&#x27;</span>,</span><br><span class="line">  <span class="attr">data</span>: &#123;&#125;,</span><br><span class="line">  <span class="attr">method</span>: <span class="string">&#x27;GET&#x27;</span>,</span><br><span class="line">  <span class="attr">header</span>: &#123;<span class="string">&#x27;content-type&#x27;</span>: <span class="string">&#x27;application/json&#x27;</span>&#125;</span><br><span class="line">&#125;).<span class="title function_">then</span>(<span class="function"><span class="params">res</span> =&gt;</span> &#123;</span><br><span class="line">  <span class="variable language_">console</span>.<span class="title function_">log</span>(res)</span><br><span class="line">&#125;).<span class="title function_">catch</span>(<span class="function"><span class="params">err</span> =&gt;</span> &#123;</span><br><span class="line">  <span class="variable language_">console</span>.<span class="title function_">log</span>(err)</span><br><span class="line">&#125;).<span class="title function_">finally</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">  wx.<span class="title function_">hideLoading</span>()</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><h2 id="Contributors"><a href="#Contributors" class="headerlink" title="Contributors"></a>Contributors</h2><p>This project exists thanks to all the people who contribute. <!--[[Contribute](CONTRIBUTING.md)].--><br><a href="https://github.com/youngjuning/wx-promise-pro/graphs/contributors"><img src="https://user-gold-cdn.xitu.io/2020/2/18/17058837fd8af7c9?w=890&h=74&f=svg&s=150945" /></a></p><h2 id="GitHub源码：https-github-com-youngjuning-wx-promise-pro"><a href="#GitHub源码：https-github-com-youngjuning-wx-promise-pro" class="headerlink" title="GitHub源码：https://github.com/youngjuning/wx-promise-pro"></a>GitHub源码：<a href="https://github.com/youngjuning/wx-promise-pro">https://github.com/youngjuning/wx-promise-pro</a></h2>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/2f2bd5ced1a6/</id>
    <link href="https://blog.zisheng.pro/posts/2f2bd5ced1a6/"/>
    <published>2020-02-18T13:57:59.000Z</published>
    <summary>wx-promise-pro 是一个 强大、优雅的小程序异步库🚀 小程序 promise 插件</summary>
    <title>wx-promise-pro</title>
    <updated>2026-09-18T08:15:28.636Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="Node.js" scheme="https://blog.zisheng.pro/tags/Node-js/"/>
    <category term="CLI" scheme="https://blog.zisheng.pro/tags/CLI/"/>
    <category term="掘金" scheme="https://blog.zisheng.pro/tags/%E6%8E%98%E9%87%91/"/>
    <content>
      <![CDATA[<p>Node.js 用途很广，常用来开发服务、桌面应用等被开发者熟知，Node.js 还有另外一个非常实用的场景 - 命令行应用（后文统称 CLI）。</p><blockquote><p>本文可参考的源码：<a href="https://github.com/youngjuning/cli">https://github.com/youngjuning/cli</a></p></blockquote><h2 id="HelloWorld"><a href="#HelloWorld" class="headerlink" title="HelloWorld"></a>HelloWorld</h2><p>创建一个<code>youngjuning</code> 目录，并使用进入该目录:</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ <span class="built_in">mkdir</span> youngjuning &amp;&amp; <span class="built_in">cd</span> youngjuning</span><br></pre></td></tr></table></figure><p>在该目录下新建 <code>bin/cli.js</code>:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#!/usr/bin/env node</span></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">&#x27;Hello World&#x27;</span>)</span><br></pre></td></tr></table></figure><p>使用 <code>package.json</code> 的 <code>bin</code> 项是用来指定各个内部命令对应的可执行文件的位置：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">&quot;bin&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;youngjuning&quot;</span><span class="punctuation">:</span> <span class="string">&quot;./bin/cli.js&quot;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p>在命令行输入 <code>npm link</code> 或 <code>npm install -g</code> 将当前项目安装到全局环境，这样就可以直接使用 <code>youngjuning</code> 来运行文件了。</p><h2 id="commander-js"><a href="#commander-js" class="headerlink" title="commander.js"></a>commander.js</h2><p>Node.js 为我们提供了 <code>process.argv</code> 来读取命令行参数，这里我们对此不作介绍，有兴趣可以自行研究，我选择了 tj 大神的 <a href="https://www.npmjs.com/package/commander">commander</a>：</p><ul><li><code>command</code>: 定义命令行指令，后面可跟上一个 name，用空格隔开，如 <code>.command( &#39;app [name]&#39;)</code></li><li><code>alias</code>: 定义一个更短的命令行指令</li><li><code>description</code>: 描述，它会在 help 里面展示</li><li><code>option</code>: 定义参数。它接受四个参数<ul><li>在第一个参数中，它可输入短名字 <code>-a</code> 和长名字 <code>–app</code>，使用 <code>|</code> 或者 <code>,</code> 分隔，在命令行里使用时，这两个是等价的，区别是后者可以在程序里通过回调获取到</li><li>第二个为描述, 会在 <code>help</code> 信息里展示出来</li><li>第三个参数为回调函数，他接收的参数为一个 <code>string</code>，有时候我们需要一个命令行创建多个模块，就需要一个回调来处理</li><li>第四个参数为默认值</li></ul></li><li><code>action</code>: 注册一个 <code>callback</code> 函数,这里需注意目前回调不支持 let 声明变量</li><li><code>parse</code>: 解析命令行</li></ul><h2 id="inquire-js"><a href="#inquire-js" class="headerlink" title="inquire.js"></a>inquire.js</h2><blockquote><p>交互命令输入插件！</p></blockquote><h3 id="基本语法"><a href="#基本语法" class="headerlink" title="基本语法"></a>基本语法</h3><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> answers = <span class="keyword">await</span> inquirer.<span class="title function_">prompt</span>([</span><br><span class="line">  <span class="comment">/* Pass your questions in here */</span></span><br><span class="line">])</span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(answers)</span><br></pre></td></tr></table></figure><h3 id="参数详解"><a href="#参数详解" class="headerlink" title="参数详解"></a>参数详解</h3><ul><li><code>type</code>: 表示提问的类型，包括：<code>input</code>, <code>confirm</code>, <code>list</code>, <code>rawlist</code>, <code>expand</code>, <code>checkbox</code>, <code>password</code>, <code>editor</code>；</li><li><code>name</code>: 存储当前问题回答的变量；</li><li><code>message</code>: 问题的描述；</li><li><code>default</code>: 默认值；</li><li><code>choices</code>: 列表选项，在某些 type 下可用，并且包含一个分隔符(separator)；</li><li><code>validate</code>: 对用户的回答进行校验；</li><li><code>filter</code>: 对用户的回答进行过滤处理，返回处理后的值；</li><li><code>transformer</code>: 对用户回答的显示效果进行处理(如：修改回答的字体或背景颜色)，但不会影响最终的答案的内容；</li><li><code>when</code>: 根据前面问题的回答，判断当前问题是否需要被回答；</li><li><code>pageSize</code>: 修改某些 <code>type</code> 类型下的渲染行数；</li><li><code>prefix</code>: 修改 <code>message</code> 默认前缀；</li><li><code>suffix</code>: 修改 <code>message</code> 默认后缀。</li></ul><h2 id="实现动态模板"><a href="#实现动态模板" class="headerlink" title="实现动态模板"></a>实现动态模板</h2><ul><li>利用 <code>recursive-readdir</code> 库递归读取项目所有文件</li><li>利用 <code>handlebars</code> 进行模板替换</li><li>利用 <code>fs</code> 模块进行读文件和写文件操作</li></ul><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> fs = <span class="built_in">require</span>(<span class="string">&#x27;fs&#x27;</span>)</span><br><span class="line"><span class="keyword">const</span> handlebars = <span class="built_in">require</span>(<span class="string">&#x27;handlebars&#x27;</span>)</span><br><span class="line"><span class="keyword">const</span> recursive = <span class="built_in">require</span>(<span class="string">&#x27;recursive-readdir&#x27;</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">compile</span> = (<span class="params">meta, file</span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> content = fs.<span class="title function_">readFileSync</span>(file).<span class="title function_">toString</span>()</span><br><span class="line">  <span class="keyword">const</span> result = handlebars.<span class="title function_">compile</span>(content)(meta)</span><br><span class="line">  fs.<span class="title function_">writeFileSync</span>(file, result)</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> = <span class="function">(<span class="params">meta, src</span>) =&gt;</span> &#123;</span><br><span class="line">  <span class="title function_">recursive</span>(src, <span class="function">(<span class="params">err, files</span>) =&gt;</span> &#123;</span><br><span class="line">    files.<span class="title function_">forEach</span>(<span class="function"><span class="params">file</span> =&gt;</span> &#123;</span><br><span class="line">      <span class="title function_">compile</span>(meta, file)</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><ul><li><a href="https://www.npmjs.com/package/chalk">chalk.js</a>: 美化命令行的模块</li><li><a href="https://www.npmjs.com/package/ora">ora</a>: Elegant terminal spinner</li><li><a href="https://www.npmjs.com/package/ncp">ncp</a>: 异步的拷贝文件，包含空文件夹</li><li><a href="https://www.npmjs.com/package/recursive-readdir">recursive-readdir</a>: 递归地列出目录和子目录下的所有文件，不包含目录本身。</li><li><a href="https://github.com/shelljs/shelljs">shelljs</a>: Unix shell commands for Node.js</li><li><a href="https://www.npmjs.com/package/log-symbols">log-symbols</a>: 打印日志的特殊标志</li><li><a href="https://www.npmjs.com/package/download-git-repo">download-git-repo</a>: Download and extract a git repository (GitHub, GitLab, Bitbucket) from node</li><li><a href="https://www.npmjs.com/package/global-prefix">global-prefix</a>: 获取 npm 全局安装的前缀</li><li><a href="https://www.npmjs.com/package/exec-sh">exec-sh</a>: 执行 shell 命令转发所有 stdio 流，比 shelljs exec 命令好用</li><li><a href="https://www.npmjs.com/package/wml">wml</a>: wml 侦听某个文件夹中的更改（使用 watchman），然后将更改的文件复制到另一个文件夹中。</li><li><a href="https://blog.ihaiu.com/cmd-open/">cmd-open</a>: 扩展 Windows 命令 open</li><li><a href="https://github.com/shelljs/shx">shx</a>: shx is a wrapper around ShellJS Unix commands, providing an easy solution for simple Unix-like, cross-platform commands in npm package scripts.</li></ul>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/614cd34a921f/</id>
    <link href="https://blog.zisheng.pro/posts/614cd34a921f/"/>
    <published>2020-02-17T07:23:35.000Z</published>
    <summary>Node.js 用途很广，常用来开发服务、桌面应用等被开发者熟知，Node.js 还有另外一个非常实用的场景 - 命令行应用（后文统称 CLI）。</summary>
    <title>从零开发一个 Node Cli</title>
    <updated>2026-09-18T08:15:28.636Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="React Native" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/React-Native/"/>
    <category term="React Native" scheme="https://blog.zisheng.pro/tags/React-Native/"/>
    <category term="最佳实践" scheme="https://blog.zisheng.pro/tags/%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5/"/>
    <content>
      <![CDATA[<blockquote><p>文章示例源码: <a href="https://github.com/youngjuning/react-navigation-best-practice">https://github.com/youngjuning/react-navigation-best-practice</a></p></blockquote><h2 id="安装依赖"><a href="#安装依赖" class="headerlink" title="安装依赖"></a>安装依赖</h2><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ yarn add @react-navigation/native @react-navigation/stack @react-navigation/bottom-tabs react-native-reanimated react-native-gesture-handler react-native-screens react-native-safe-area-context @react-native-community/masked-view</span><br></pre></td></tr></table></figure><h2 id="配置"><a href="#配置" class="headerlink" title="配置"></a>配置</h2><p>为了完成  <code>react-native-screens</code> 的安装，添加下面两行代码到 <code>android/app/build.gradle</code> 文件的 <code>dependencies</code> 部分中：</p><figure class="highlight groovy"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">implementation <span class="string">&#x27;androidx.appcompat:appcompat:1.1.0-rc01&#x27;</span></span><br><span class="line">implementation <span class="string">&#x27;androidx.swiperefreshlayout:swiperefreshlayout:1.1.0-alpha02&#x27;</span></span><br></pre></td></tr></table></figure><p>为了完成  <code>react-native-gesture-handler</code> 的安装, 在入口文件的顶部添加下面的代码, 比如 <code>index.js</code> 或 <code>App.js</code>:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="string">&#x27;react-native-gesture-handler&#x27;</span>;</span><br></pre></td></tr></table></figure><p>现在，我们需要把整个 App用 <code>NavigationContainer</code>包裹：</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">React</span> <span class="keyword">from</span> <span class="string">&#x27;react&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">NavigationContainer</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;@react-navigation/native&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">App</span> = (<span class="params"></span>) =&gt; &#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">NavigationContainer</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      &#123;/* Rest of your app code */&#125;</span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">NavigationContainer</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 class_">App</span>;</span><br></pre></td></tr></table></figure><h2 id="App-js"><a href="#App-js" class="headerlink" title="App.js"></a>App.js</h2><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br><span class="line">128</span><br><span class="line">129</span><br><span class="line">130</span><br><span class="line">131</span><br><span class="line">132</span><br><span class="line">133</span><br><span class="line">134</span><br><span class="line">135</span><br><span class="line">136</span><br><span class="line">137</span><br><span class="line">138</span><br><span class="line">139</span><br><span class="line">140</span><br><span class="line">141</span><br><span class="line">142</span><br><span class="line">143</span><br><span class="line">144</span><br><span class="line">145</span><br><span class="line">146</span><br><span class="line">147</span><br><span class="line">148</span><br><span class="line">149</span><br><span class="line">150</span><br><span class="line">151</span><br><span class="line">152</span><br><span class="line">153</span><br><span class="line">154</span><br><span class="line">155</span><br><span class="line">156</span><br><span class="line">157</span><br><span class="line">158</span><br><span class="line">159</span><br><span class="line">160</span><br><span class="line">161</span><br><span class="line">162</span><br><span class="line">163</span><br><span class="line">164</span><br><span class="line">165</span><br><span class="line">166</span><br><span class="line">167</span><br><span class="line">168</span><br><span class="line">169</span><br><span class="line">170</span><br><span class="line">171</span><br><span class="line">172</span><br><span class="line">173</span><br><span class="line">174</span><br><span class="line">175</span><br><span class="line">176</span><br><span class="line">177</span><br><span class="line">178</span><br><span class="line">179</span><br><span class="line">180</span><br><span class="line">181</span><br><span class="line">182</span><br><span class="line">183</span><br><span class="line">184</span><br><span class="line">185</span><br><span class="line">186</span><br><span class="line">187</span><br><span class="line">188</span><br><span class="line">189</span><br><span class="line">190</span><br><span class="line">191</span><br><span class="line">192</span><br><span class="line">193</span><br><span class="line">194</span><br><span class="line">195</span><br><span class="line">196</span><br><span class="line">197</span><br><span class="line">198</span><br><span class="line">199</span><br><span class="line">200</span><br><span class="line">201</span><br><span class="line">202</span><br><span class="line">203</span><br><span class="line">204</span><br><span class="line">205</span><br><span class="line">206</span><br><span class="line">207</span><br><span class="line">208</span><br><span class="line">209</span><br><span class="line">210</span><br><span class="line">211</span><br><span class="line">212</span><br><span class="line">213</span><br><span class="line">214</span><br><span class="line">215</span><br><span class="line">216</span><br><span class="line">217</span><br><span class="line">218</span><br><span class="line">219</span><br><span class="line">220</span><br><span class="line">221</span><br><span class="line">222</span><br><span class="line">223</span><br><span class="line">224</span><br><span class="line">225</span><br><span class="line">226</span><br><span class="line">227</span><br><span class="line">228</span><br><span class="line">229</span><br><span class="line">230</span><br><span class="line">231</span><br><span class="line">232</span><br><span class="line">233</span><br><span class="line">234</span><br><span class="line">235</span><br><span class="line">236</span><br><span class="line">237</span><br><span class="line">238</span><br><span class="line">239</span><br><span class="line">240</span><br><span class="line">241</span><br><span class="line">242</span><br><span class="line">243</span><br><span class="line">244</span><br><span class="line">245</span><br><span class="line">246</span><br><span class="line">247</span><br><span class="line">248</span><br><span class="line">249</span><br><span class="line">250</span><br><span class="line">251</span><br><span class="line">252</span><br><span class="line">253</span><br><span class="line">254</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">React</span> <span class="keyword">from</span> <span class="string">&#x27;react&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123;</span><br><span class="line">  <span class="title class_">View</span>,</span><br><span class="line">  <span class="title class_">Text</span>,</span><br><span class="line">  <span class="title class_">StyleSheet</span>,</span><br><span class="line">  <span class="title class_">SafeAreaView</span>,</span><br><span class="line">  <span class="title class_">StatusBar</span>,</span><br><span class="line">  <span class="title class_">BackHandler</span>,</span><br><span class="line">&#125; <span class="keyword">from</span> <span class="string">&#x27;react-native&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123;<span class="title class_">NavigationContainer</span>, useFocusEffect&#125; <span class="keyword">from</span> <span class="string">&#x27;@react-navigation/native&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123;createBottomTabNavigator&#125; <span class="keyword">from</span> <span class="string">&#x27;@react-navigation/bottom-tabs&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123;createStackNavigator, <span class="title class_">HeaderBackButton</span>&#125; <span class="keyword">from</span> <span class="string">&#x27;@react-navigation/stack&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123;<span class="title class_">IconOutline</span>&#125; <span class="keyword">from</span> <span class="string">&#x27;@ant-design/icons-react-native&#x27;</span>;</span><br><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;@ant-design/react-native&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> <span class="title class_">IconWithBadge</span> <span class="keyword">from</span> <span class="string">&#x27;./IconWithBadge&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> <span class="title class_">HeaderButtons</span> <span class="keyword">from</span> <span class="string">&#x27;./HeaderButtons&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> getActiveRouteName <span class="keyword">from</span> <span class="string">&#x27;./getActiveRouteName&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> getScreenOptions <span class="keyword">from</span> <span class="string">&#x27;./getScreenOptions&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123;navigationRef&#125; <span class="keyword">from</span> <span class="string">&#x27;./NavigationService&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">HomeScreen</span> = (<span class="params">&#123;navigation, route&#125;</span>) =&gt; &#123;</span><br><span class="line">  navigation.<span class="title function_">setOptions</span>(&#123;</span><br><span class="line">    <span class="attr">headerLeft</span>: <span class="function"><span class="params">props</span> =&gt;</span> (</span><br><span class="line">      <span class="language-xml"><span class="tag">&lt;<span class="name">HeaderBackButton</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">        &#123;<span class="attr">...props</span>&#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> &#123;</span></span><br><span class="line"><span class="language-xml">          console.log(&#x27;不能再返回了！&#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><br><span class="line">    <span class="attr">headerRight</span>: <span class="function">() =&gt;</span> (</span><br><span class="line">      <span class="language-xml"><span class="tag">&lt;<span class="name">HeaderButtons</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">        &#123;/* title、iconName、onPress、IconComponent、iconSize、color */&#125;</span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">HeaderButtons.Item</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">          <span class="attr">title</span>=<span class="string">&quot;添加&quot;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">          <span class="attr">iconName</span>=<span class="string">&quot;plus&quot;</span></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> console.log(&#x27;点击了添加按钮&#x27;)&#125;</span></span><br><span class="line"><span class="language-xml">          iconSize=&#123;24&#125;</span></span><br><span class="line"><span class="language-xml">          color=&quot;#ffffff&quot;</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">HeaderButtons</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="title function_">useFocusEffect</span>(</span><br><span class="line">    <span class="title class_">React</span>.<span class="title function_">useCallback</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">      <span class="comment">// Do something when the screen is focused</span></span><br><span class="line">      <span class="keyword">return</span> <span class="function">() =&gt;</span> &#123;</span><br><span class="line">        <span class="comment">// Do something when the screen is unfocused</span></span><br><span class="line">        <span class="comment">// Useful for cleanup functions</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> &#123;author&#125; = route.<span class="property">params</span> || &#123;&#125;;</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">StatusBar</span> <span class="attr">barStyle</span>=<span class="string">&quot;dark-content&quot;</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.container&#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>Home Screen<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>&gt;</span>&#123;author&#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">Button</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">          <span class="attr">type</span>=<span class="string">&quot;warning&quot;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">          // <span class="attr">使用</span> <span class="attr">setOptions</span> <span class="attr">更新标题</span></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> navigation.setOptions(&#123;headerTitle: &#x27;Updated!&#x27;&#125;)&#125;&gt;</span></span><br><span class="line"><span class="language-xml">          Update the title</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">Button</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">          <span class="attr">type</span>=<span class="string">&quot;primary&quot;</span></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></span></span><br><span class="line"><span class="language-xml">            // 跳转到指定页面，并传递两个参数</span></span><br><span class="line"><span class="language-xml">            navigation.navigate(&#x27;DetailsScreen&#x27;, &#123;</span></span><br><span class="line"><span class="language-xml">              otherParam: &#x27;anything you want here&#x27;,</span></span><br><span class="line"><span class="language-xml">            &#125;)</span></span><br><span class="line"><span class="language-xml">          &#125;&gt;</span></span><br><span class="line"><span class="language-xml">          Go to DetailsScreen</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">Button</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">          <span class="attr">type</span>=<span class="string">&quot;warning&quot;</span></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> navigation.navigate(&#x27;SafeAreaViewScreen&#x27;)&#125;&gt;</span></span><br><span class="line"><span class="language-xml">          Go SafeAreaViewScreen</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">Button</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">          <span class="attr">type</span>=<span class="string">&quot;primary&quot;</span></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></span></span><br><span class="line"><span class="language-xml">            navigation.navigate(&#x27;CustomAndroidBackButtonBehaviorScreen&#x27;)</span></span><br><span class="line"><span class="language-xml">          &#125;&gt;</span></span><br><span class="line"><span class="language-xml">          Go CustomAndroidBackButtonBehavior</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">View</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><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">DetailsScreen</span> = (<span class="params">&#123;navigation, route&#125;</span>) =&gt; &#123;</span><br><span class="line">  <span class="comment">// 通过 props.route.params 接收参数</span></span><br><span class="line">  <span class="keyword">const</span> &#123;itemId, otherParam&#125; = route.<span class="property">params</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">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>&gt;</span>Details Screen<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>&gt;</span>itemId: &#123;itemId&#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>&gt;</span>otherParam: &#123;otherParam&#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">Button</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">        <span class="attr">type</span>=<span class="string">&quot;primary&quot;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">        // <span class="attr">返回上一页</span></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> navigation.goBack()&#125;&gt;</span></span><br><span class="line"><span class="language-xml">        Go back</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">Button</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">        <span class="attr">type</span>=<span class="string">&quot;primary&quot;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">        // <span class="attr">如果返回上一个页面需要传递参数</span>，<span class="attr">请使用</span> <span class="attr">navigate</span> <span class="attr">方法</span></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> navigation.navigate(&#x27;HomeScreen&#x27;, &#123;author: &#x27;紫升&#x27;&#125;)&#125;&gt;</span></span><br><span class="line"><span class="language-xml">        Go back with Params</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">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> <span class="title function_">SettingsScreen</span> = (<span class="params">&#123;navigation, route&#125;</span>) =&gt; &#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">SafeAreaView</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;flex:</span> <span class="attr">1</span>, <span class="attr">justifyContent:</span> &#x27;<span class="attr">space-between</span>&#x27;, <span class="attr">alignItems:</span> &#x27;<span class="attr">center</span>&#x27;&#125;&#125;&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">Text</span>&gt;</span>This is top text.<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>&gt;</span>This is bottom text.<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">SafeAreaView</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> <span class="title function_">SafeAreaViewScreen</span> = (<span class="params"></span>) =&gt; &#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">SafeAreaView</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;flex:</span> <span class="attr">1</span>, <span class="attr">justifyContent:</span> &#x27;<span class="attr">space-between</span>&#x27;, <span class="attr">alignItems:</span> &#x27;<span class="attr">center</span>&#x27;&#125;&#125;&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">Text</span>&gt;</span>This is top text.<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>&gt;</span>This is bottom text.<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">SafeAreaView</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> <span class="title function_">CustomAndroidBackButtonBehaviorScreen</span> = (<span class="params">&#123;navigation, route&#125;</span>) =&gt; &#123;</span><br><span class="line">  <span class="title function_">useFocusEffect</span>(</span><br><span class="line">    <span class="title class_">React</span>.<span class="title function_">useCallback</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">      <span class="keyword">const</span> <span class="title function_">onBackPress</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">        <span class="title function_">alert</span>(<span class="string">&#x27;物理返回键被拦截了！&#x27;</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><br><span class="line">      <span class="title class_">BackHandler</span>.<span class="title function_">addEventListener</span>(<span class="string">&#x27;hardwareBackPress&#x27;</span>, onBackPress);</span><br><span class="line"></span><br><span class="line">      <span class="keyword">return</span> <span class="function">() =&gt;</span></span><br><span class="line">        <span class="title class_">BackHandler</span>.<span class="title function_">removeEventListener</span>(<span class="string">&#x27;hardwareBackPress&#x27;</span>, onBackPress);</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>&gt;</span>AndroidBackHandlerScreen<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> <span class="title class_">Stack</span> = <span class="title function_">createStackNavigator</span>();</span><br><span class="line"><span class="keyword">const</span> <span class="title class_">BottomTab</span> = <span class="title function_">createBottomTabNavigator</span>();</span><br><span class="line"><span class="keyword">const</span> <span class="title function_">BottomTabScreen</span> = (<span class="params"></span>) =&gt; (</span><br><span class="line">  <span class="language-xml"><span class="tag">&lt;<span class="name">BottomTab.Navigator</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">    <span class="attr">screenOptions</span>=<span class="string">&#123;(&#123;route&#125;)</span> =&gt;</span> (&#123;</span></span><br><span class="line"><span class="language-xml">      tabBarIcon: (&#123;focused, color, size&#125;) =&gt; &#123;</span></span><br><span class="line"><span class="language-xml">        let iconName;</span></span><br><span class="line"><span class="language-xml">        if (route.name === &#x27;HomeScreen&#x27;) &#123;</span></span><br><span class="line"><span class="language-xml">          iconName = focused ? &#x27;apple&#x27; : &#x27;apple&#x27;;</span></span><br><span class="line"><span class="language-xml">          return (</span></span><br><span class="line"><span class="language-xml">            <span class="tag">&lt;<span class="name">IconWithBadge</span> <span class="attr">badgeCount</span>=<span class="string">&#123;90&#125;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">              <span class="tag">&lt;<span class="name">IconOutline</span> <span class="attr">name</span>=<span class="string">&#123;iconName&#125;</span> <span class="attr">size</span>=<span class="string">&#123;size&#125;</span> <span class="attr">color</span>=<span class="string">&#123;color&#125;</span> /&gt;</span></span></span><br><span class="line"><span class="language-xml">            <span class="tag">&lt;/<span class="name">IconWithBadge</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">          );</span></span><br><span class="line"><span class="language-xml">        &#125; else if (route.name === &#x27;SettingsScreen&#x27;) &#123;</span></span><br><span class="line"><span class="language-xml">          iconName = focused ? &#x27;twitter&#x27; : &#x27;twitter&#x27;;</span></span><br><span class="line"><span class="language-xml">        &#125;</span></span><br><span class="line"><span class="language-xml">        return <span class="tag">&lt;<span class="name">IconOutline</span> <span class="attr">name</span>=<span class="string">&#123;iconName&#125;</span> <span class="attr">size</span>=<span class="string">&#123;size&#125;</span> <span class="attr">color</span>=<span class="string">&#123;color&#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">    &#125;)&#125;</span></span><br><span class="line"><span class="language-xml">    tabBarOptions=&#123;&#123;</span></span><br><span class="line"><span class="language-xml">      activeTintColor: &#x27;tomato&#x27;,</span></span><br><span class="line"><span class="language-xml">      inactiveTintColor: &#x27;gray&#x27;,</span></span><br><span class="line"><span class="language-xml">    &#125;&#125;&gt;</span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;<span class="name">Stack.Screen</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">      <span class="attr">name</span>=<span class="string">&quot;HomeScreen&quot;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">      <span class="attr">component</span>=<span class="string">&#123;HomeScreen&#125;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">      <span class="attr">options</span>=<span class="string">&#123;&#123;tabBarLabel:</span> &#x27;<span class="attr">首页</span>&#x27;&#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">    <span class="tag">&lt;<span class="name">Stack.Screen</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">      <span class="attr">name</span>=<span class="string">&quot;SettingsScreen&quot;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">      <span class="attr">component</span>=<span class="string">&#123;SettingsScreen&#125;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">      <span class="attr">options</span>=<span class="string">&#123;&#123;tabBarLabel:</span> &#x27;<span class="attr">设置</span>&#x27;&#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">  <span class="tag">&lt;/<span class="name">BottomTab.Navigator</span>&gt;</span></span></span><br><span class="line">);</span><br><span class="line"><span class="keyword">const</span> <span class="title function_">App</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> routeNameRef = <span class="title class_">React</span>.<span class="title function_">useRef</span>();</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">NavigationContainer</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">        <span class="attr">ref</span>=<span class="string">&#123;navigationRef&#125;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">        <span class="attr">onStateChange</span>=<span class="string">&#123;state</span> =&gt;</span> &#123;</span></span><br><span class="line"><span class="language-xml">          const previousRouteName = routeNameRef.current;</span></span><br><span class="line"><span class="language-xml">          const currentRouteName = getActiveRouteName(state);</span></span><br><span class="line"><span class="language-xml">          if (previousRouteName !== currentRouteName) &#123;</span></span><br><span class="line"><span class="language-xml">            console.log(&#x27;[onStateChange]&#x27;, currentRouteName);</span></span><br><span class="line"><span class="language-xml">            if (currentRouteName === &#x27;HomeScreen&#x27;) &#123;</span></span><br><span class="line"><span class="language-xml">              StatusBar.setBarStyle(&#x27;dark-content&#x27;); // 修改 StatusBar</span></span><br><span class="line"><span class="language-xml">            &#125; else &#123;</span></span><br><span class="line"><span class="language-xml">              StatusBar.setBarStyle(&#x27;dark-content&#x27;); // 修改 StatusBar</span></span><br><span class="line"><span class="language-xml">            &#125;</span></span><br><span class="line"><span class="language-xml">          &#125;</span></span><br><span class="line"><span class="language-xml">          // Save the current route name for later comparision</span></span><br><span class="line"><span class="language-xml">          routeNameRef.current = currentRouteName;</span></span><br><span class="line"><span class="language-xml">        &#125;&#125;&gt;</span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">Stack.Navigator</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">          <span class="attr">initialRouteName</span>=<span class="string">&quot;HomeScreen&quot;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">          // <span class="attr">页面共享的配置</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">          <span class="attr">screenOptions</span>=<span class="string">&#123;getScreenOptions()&#125;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">          <span class="tag">&lt;<span class="name">Stack.Screen</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">            <span class="attr">name</span>=<span class="string">&quot;BottomTabScreen&quot;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">            <span class="attr">component</span>=<span class="string">&#123;BottomTabScreen&#125;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">            <span class="attr">options</span>=<span class="string">&#123;&#123;headerShown:</span> <span class="attr">false</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">          <span class="tag">&lt;<span class="name">Stack.Screen</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">            <span class="attr">name</span>=<span class="string">&quot;DetailsScreen&quot;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">            <span class="attr">component</span>=<span class="string">&#123;DetailsScreen&#125;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">            <span class="attr">options</span>=<span class="string">&#123;&#123;headerTitle:</span> &#x27;<span class="attr">详情</span>&#x27;&#125;&#125; // <span class="attr">headerTitle</span> <span class="attr">用来设置标题栏</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">            <span class="attr">initialParams</span>=<span class="string">&#123;&#123;itemId:</span> <span class="attr">42</span>&#125;&#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 class="tag">&lt;<span class="name">Stack.Screen</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">            <span class="attr">name</span>=<span class="string">&quot;SafeAreaViewScreen&quot;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">            <span class="attr">component</span>=<span class="string">&#123;SafeAreaViewScreen&#125;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">            <span class="attr">options</span>=<span class="string">&#123;&#123;headerTitle:</span> &#x27;<span class="attr">SafeAreaView</span>&#x27;&#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">          <span class="tag">&lt;<span class="name">Stack.Screen</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">            <span class="attr">name</span>=<span class="string">&quot;CustomAndroidBackButtonBehaviorScreen&quot;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">            <span class="attr">component</span>=<span class="string">&#123;CustomAndroidBackButtonBehaviorScreen&#125;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">            <span class="attr">options</span>=<span class="string">&#123;&#123;headerTitle:</span> &#x27;<span class="attr">拦截安卓物理返回键</span>&#x27;&#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">        <span class="tag">&lt;/<span class="name">Stack.Navigator</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;/<span class="name">NavigationContainer</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><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">flex</span>: <span class="number">1</span>,</span><br><span class="line">    <span class="attr">alignItems</span>: <span class="string">&#x27;center&#x27;</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">&#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_">App</span>;</span><br></pre></td></tr></table></figure><blockquote><p>路由名称的大小写无关紧要 – 你可以使用小写字母<code>home</code>或大写字母<code>Home</code>，这取决于你的喜好。 我们更喜欢将路由名称大写。 我们更喜欢利用我们的路由名称。</p></blockquote><blockquote><p>跳转方法有 <code>navigate</code>、 <code>push</code> 、<code>goBack</code>、<code>popToTop</code></p></blockquote><blockquote><p>可以用 <code>navigation.setParams</code> 方法更新页面的参数</p></blockquote><blockquote><p>我们可以通过 <code>options={({ route, navigation }) =&gt; ({ headerTitle: route.params.name })}</code> 的方式在标题中使用参数</p></blockquote><blockquote><p>我们可以用 <code>navigation.setOptions</code> 更新页面配置</p></blockquote><ul><li><code>Stack.Navigator</code><ul><li><code>initialRouteName</code> : 用来配置 <code>Stack.Navigator</code> 的初始路由</li><li><code>screenOptions</code>: 页面共享配置对象</li></ul></li><li><code>Stack.Screen</code><ul><li><code>name</code>: 页面名</li><li><code>component</code>: 页面对应组件</li><li><code>options</code>: 页面配置对象</li><li><code>initialParams</code>: 默认参数</li></ul></li></ul><h2 id="HeaderButtons-js"><a href="#HeaderButtons-js" class="headerlink" title="HeaderButtons.js"></a>HeaderButtons.js</h2><p>使用 <code>react-navigation-header-buttons</code> 组件搭配任意 Icon 组件可以自定义自己的 Header Button 组件，我这里为了演示方便，使用了 <code>@ant-design/icons-react-native</code>：</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">React</span> <span class="keyword">from</span> <span class="string">&#x27;react&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123;</span><br><span class="line">  <span class="title class_">HeaderButtons</span> <span class="keyword">as</span> <span class="title class_">RNHeaderButtons</span>,</span><br><span class="line">  <span class="title class_">HeaderButton</span> <span class="keyword">as</span> <span class="title class_">RNHeaderButton</span>,</span><br><span class="line">  <span class="title class_">Item</span>,</span><br><span class="line">&#125; <span class="keyword">from</span> <span class="string">&#x27;react-navigation-header-buttons&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123;<span class="title class_">IconOutline</span>&#125; <span class="keyword">from</span> <span class="string">&#x27;@ant-design/icons-react-native&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">HeaderButton</span> = props =&gt; &#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">RNHeaderButton</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">      &#123;<span class="attr">...props</span>&#125;</span></span></span><br><span class="line"><span class="tag"><span class="language-xml">      <span class="attr">IconComponent</span>=<span class="string">&#123;IconOutline&#125;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">      <span class="attr">iconSize</span>=<span class="string">&#123;props.iconSize</span> || <span class="attr">23</span>&#125;</span></span></span><br><span class="line"><span class="tag"><span class="language-xml">      <span class="attr">color</span>=<span class="string">&#123;props.color</span> || &#x27;#<span class="attr">000000</span>&#x27;&#125;</span></span></span><br><span class="line"><span class="tag"><span class="language-xml">    /&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> <span class="title function_">HeaderButtons</span> = props =&gt; &#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="language-xml"><span class="tag">&lt;<span class="name">RNHeaderButtons</span> <span class="attr">HeaderButtonComponent</span>=<span class="string">&#123;HeaderButton&#125;</span> &#123;<span class="attr">...props</span>&#125; /&gt;</span></span>;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="title class_">HeaderButtons</span>.<span class="property">Item</span> = <span class="title class_">Item</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="title class_">HeaderButtons</span>;</span><br></pre></td></tr></table></figure><h2 id="IconWithBadge-js"><a href="#IconWithBadge-js" class="headerlink" title="IconWithBadge.js"></a>IconWithBadge.js</h2><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">React</span> <span class="keyword">from</span> <span class="string">&#x27;react&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123;<span class="title class_">View</span>&#125; <span class="keyword">from</span> <span class="string">&#x27;react-native&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123;<span class="title class_">Badge</span>&#125; <span class="keyword">from</span> <span class="string">&#x27;@ant-design/react-native&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">IconWithBadge</span> = (<span class="params">&#123;children, badgeCount, ...props&#125;</span>) =&gt; &#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;&#123;width:</span> <span class="attr">24</span>, <span class="attr">height:</span> <span class="attr">24</span>, <span class="attr">margin:</span> <span class="attr">5</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">Badge</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">        &#123;<span class="attr">...props</span>&#125;</span></span></span><br><span class="line"><span class="tag"><span class="language-xml">        <span class="attr">style</span>=<span class="string">&#123;&#123;position:</span> &#x27;<span class="attr">absolute</span>&#x27;, <span class="attr">right:</span> <span class="attr">-6</span>, <span class="attr">top:</span> <span class="attr">-3</span>&#125;&#125;</span></span></span><br><span class="line"><span class="tag"><span class="language-xml">        <span class="attr">text</span>=<span class="string">&#123;badgeCount&#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">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">export</span> <span class="keyword">default</span> <span class="title class_">IconWithBadge</span>;</span><br></pre></td></tr></table></figure><h2 id="getActiveRouteName-js"><a href="#getActiveRouteName-js" class="headerlink" title="getActiveRouteName.js"></a>getActiveRouteName.js</h2><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * Gets the current screen from navigation state</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> <span class="variable">state</span></span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">getActiveRouteName</span> = state =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> route = state.<span class="property">routes</span>[state.<span class="property">index</span>];</span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (route.<span class="property">state</span>) &#123;</span><br><span class="line">    <span class="comment">// Dive into nested navigators</span></span><br><span class="line">    <span class="keyword">return</span> <span class="title function_">getActiveRouteName</span>(route.<span class="property">state</span>);</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> route.<span class="property">name</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> getActiveRouteName;</span><br><span class="line"></span><br></pre></td></tr></table></figure><h2 id="getScreenOptions-js"><a href="#getScreenOptions-js" class="headerlink" title="getScreenOptions.js"></a>getScreenOptions.js</h2><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123;<span class="title class_">TransitionPresets</span>&#125; <span class="keyword">from</span> <span class="string">&#x27;@react-navigation/stack&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">getScreenOptions</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">return</span> &#123;</span><br><span class="line">    <span class="attr">headerStyle</span>: &#123;</span><br><span class="line">      <span class="attr">backgroundColor</span>: <span class="string">&#x27;#ffffff&#x27;</span>,</span><br><span class="line">    &#125;, <span class="comment">// 一个应用于 header 的最外层 View 的 样式对象</span></span><br><span class="line">    <span class="attr">headerTintColor</span>: <span class="string">&#x27;#000000&#x27;</span>, <span class="comment">// 返回按钮和标题都使用这个属性作为它们的颜色</span></span><br><span class="line">    <span class="attr">headerTitleStyle</span>: &#123;</span><br><span class="line">      <span class="attr">fontWeight</span>: <span class="string">&#x27;bold&#x27;</span>,</span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="attr">headerBackTitleVisible</span>: <span class="literal">false</span>,</span><br><span class="line">    <span class="attr">headerTitleAlign</span>: <span class="string">&#x27;center&#x27;</span>,</span><br><span class="line">    <span class="attr">cardStyle</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">backgroundColor</span>: <span class="string">&#x27;#f5f5f9&#x27;</span>,</span><br><span class="line">    &#125;,</span><br><span class="line">    ...<span class="title class_">TransitionPresets</span>.<span class="property">SlideFromRightIOS</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> getScreenOptions;</span><br><span class="line"></span><br></pre></td></tr></table></figure><h2 id="NavigationService-js"><a href="#NavigationService-js" class="headerlink" title="NavigationService.js"></a>NavigationService.js</h2><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">React</span> <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">const</span> navigationRef = <span class="title class_">React</span>.<span class="title function_">createRef</span>();</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">navigate</span> = (<span class="params">name, params</span>) =&gt; &#123;</span><br><span class="line">  navigationRef.<span class="property">current</span> &amp;&amp; navigationRef.<span class="property">current</span>.<span class="title function_">navigate</span>(name, params);</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">getNavigation</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">return</span> navigationRef.<span class="property">current</span> &amp;&amp; navigationRef.<span class="property">current</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> &#123;</span><br><span class="line">  navigate,</span><br><span class="line">  getNavigation,</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><h2 id="页面生命周期与React-Navigation"><a href="#页面生命周期与React-Navigation" class="headerlink" title="页面生命周期与React Navigation"></a>页面生命周期与React Navigation</h2><p>一个包含 页面 A 和 B 的 StackNavigator ，当跳转到 A 时，<code>componentDidMount</code> 方法会被调用； 当跳转到 B 时，<code>componentDidMount</code> 方法也会被调用，但是 A 依然在堆栈中保持 被加载状态，他的 <code>componentWillUnMount</code> 也不会被调用。</p><p>当从 B 跳转到 A，B的 <code>componentWillUnmount</code> 方法会被调用，但是 A 的 <code>componentDidMount</code>方法不会被调用，应为此时 A 依然是被加载状态。</p><h2 id="React-Navigation-生命周期事件"><a href="#React-Navigation-生命周期事件" class="headerlink" title="React Navigation 生命周期事件"></a>React Navigation 生命周期事件</h2><h3 id="addListener"><a href="#addListener" class="headerlink" title="addListener"></a>addListener</h3><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">Profile</span>(<span class="params">&#123; navigation &#125;</span>) &#123;</span><br><span class="line">  <span class="title class_">React</span>.<span class="title function_">useEffect</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">const</span> unsubscribe = navigation.<span class="title function_">addListener</span>(<span class="string">&#x27;focus&#x27;</span>, <span class="function">() =&gt;</span> &#123;</span><br><span class="line">      <span class="comment">// Screen was focused</span></span><br><span class="line">      <span class="comment">// Do something</span></span><br><span class="line">    &#125;);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> unsubscribe;</span><br><span class="line">  &#125;, [navigation]);</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">ProfileContent</span> /&gt;</span></span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="useFocusEffect"><a href="#useFocusEffect" class="headerlink" title="useFocusEffect"></a>useFocusEffect</h3><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="title function_">useFocusEffect</span>(</span><br><span class="line">    <span class="title class_">React</span>.<span class="title function_">useCallback</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">      <span class="comment">// Do something when the screen is focused</span></span><br><span class="line">      <span class="keyword">return</span> <span class="function">() =&gt;</span> &#123;</span><br><span class="line">        <span class="comment">// Do something when the screen is unfocused</span></span><br><span class="line">        <span class="comment">// Useful for cleanup functions</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><h2 id="隐藏-Header-TabBar"><a href="#隐藏-Header-TabBar" class="headerlink" title="隐藏 Header&#x2F;TabBar"></a>隐藏 Header&#x2F;TabBar</h2><ul><li><code>headerMode:&quot;none&quot;</code>: hide Header for <code>Stack.Navigator</code></li><li><code>headerShown:false</code>: hide Header for <code>Stack.Screen</code></li><li><code>tabBar={() =&gt; null}</code>: hide TabBar for <code>BottomTab.Navigator</code></li></ul><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123;<span class="title class_">NavigationContainer</span>, useFocusEffect&#125; <span class="keyword">from</span> <span class="string">&#x27;@react-navigation/native&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123;createStackNavigator, <span class="title class_">TransitionPresets</span>, <span class="title class_">HeaderBackButton</span>&#125; <span class="keyword">from</span> <span class="string">&#x27;@react-navigation/stack&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123;createBottomTabNavigator&#125; <span class="keyword">from</span> <span class="string">&#x27;@react-navigation/bottom-tabs&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="title class_">Stack</span> = <span class="title function_">createStackNavigator</span>();</span><br><span class="line"><span class="keyword">const</span> <span class="title class_">BottomTab</span> = <span class="title function_">createBottomTabNavigator</span>();</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="title class_">App</span> = <span class="function">() =&gt;</span> &#123;</span><br><span class="line">  <span class="language-xml"><span class="tag">&lt;<span class="name">NavigationContainer</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">  <span class="tag">&lt;<span class="name">Stack.Navigator</span> <span class="attr">headerMode</span>=<span class="string">&quot;none&quot;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">Stack.Screen</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">        <span class="attr">...</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">        <span class="attr">options</span>=<span class="string">&#123;&#123;</span> <span class="attr">headerShown:</span> <span class="attr">false</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">      <span class="tag">&lt;<span class="name">Stack.Screen</span> <span class="attr">...</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">        &#123;() =&gt; (</span></span><br><span class="line"><span class="language-xml">          <span class="tag">&lt;<span class="name">BottomTab.Navigator</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">            <span class="attr">...</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">           <span class="attr">tabBar</span>=<span class="string">&#123;()</span> =&gt;</span> null&#125;</span></span><br><span class="line"><span class="language-xml">          &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">BottomTab.Navigator</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">Stack.Screen</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">Stack.Navigator</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">  <span class="tag">&lt;/<span class="name">NavigationContainer</span>&gt;</span></span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="TabBar-的-StatusBar-不同"><a href="#TabBar-的-StatusBar-不同" class="headerlink" title="TabBar 的 StatusBar 不同"></a>TabBar 的 StatusBar 不同</h2><p>一般我们会对特殊的那个TabBar进行处理。</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="title function_">getActiveRouteName</span> = state =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> route = state.<span class="property">routes</span>[state.<span class="property">index</span>];</span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (route.<span class="property">state</span>) &#123;</span><br><span class="line">    <span class="comment">// Dive into nested navigators</span></span><br><span class="line">    <span class="keyword">return</span> <span class="title function_">getActiveRouteName</span>(route.<span class="property">state</span>);</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> route.<span class="property">name</span>;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">App</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> ref = <span class="title class_">React</span>.<span class="title function_">useRef</span>(<span class="literal">null</span>);</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">    &#123;/* 访问 ref.current?.navigate */&#125;</span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">NavigationContainer</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&#125;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">        <span class="attr">onStateChange</span>=<span class="string">&#123;state</span> =&gt;</span> &#123;</span></span><br><span class="line"><span class="language-xml">          const previousRouteName = ref.current;</span></span><br><span class="line"><span class="language-xml">          const currentRouteName = getActiveRouteName(state);</span></span><br><span class="line"><span class="language-xml">          if (previousRouteName !== currentRouteName) &#123;</span></span><br><span class="line"><span class="language-xml">            console.log(&#x27;[onStateChange]&#x27;, currentRouteName);</span></span><br><span class="line"><span class="language-xml">            if (currentRouteName === &#x27;HomeScreen&#x27;) &#123;</span></span><br><span class="line"><span class="language-xml">              StatusBar.setBarStyle(&#x27;dark-content&#x27;);  // 修改 StatusBar</span></span><br><span class="line"><span class="language-xml">            &#125; else &#123;</span></span><br><span class="line"><span class="language-xml">              StatusBar.setBarStyle(&#x27;dark-content&#x27;);  // 修改 StatusBar</span></span><br><span class="line"><span class="language-xml">            &#125;</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 class="language-xml">      <span class="tag">&lt;/<span class="name">NavigationContainer</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="监听安卓物理返回键"><a href="#监听安卓物理返回键" class="headerlink" title="监听安卓物理返回键"></a>监听安卓物理返回键</h2><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123;<span class="title class_">View</span>, <span class="title class_">Text</span>, <span class="title class_">BackHandler</span>&#125; <span class="keyword">from</span> <span class="string">&#x27;react-native&#x27;</span>;</span><br><span class="line"><span class="keyword">const</span> <span class="title function_">CustomAndroidBackButtonBehaviorScreen</span> = (<span class="params">&#123;navigation, route&#125;</span>) =&gt; &#123;</span><br><span class="line">  <span class="title function_">useFocusEffect</span>(</span><br><span class="line">    <span class="title class_">React</span>.<span class="title function_">useCallback</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">      <span class="keyword">const</span> <span class="title function_">onBackPress</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">        <span class="title function_">alert</span>(<span class="string">&#x27;物理返回键被拦截了！&#x27;</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="title class_">BackHandler</span>.<span class="title function_">addEventListener</span>(<span class="string">&#x27;hardwareBackPress&#x27;</span>, onBackPress);</span><br><span class="line">      <span class="keyword">return</span> <span class="function">() =&gt;</span></span><br><span class="line">        <span class="title class_">BackHandler</span>.<span class="title function_">removeEventListener</span>(<span class="string">&#x27;hardwareBackPress&#x27;</span>, onBackPress);</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>&gt;</span>AndroidBackHandlerScreen<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="在子组件中访问-navigation"><a href="#在子组件中访问-navigation" class="headerlink" title="在子组件中访问 navigation"></a>在子组件中访问 <code>navigation</code></h2><p>我们可以通过 <code>useNavigation()</code> hook 来访问 navigation，再也不用传递多层 <code>navigation</code></p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">React</span> <span class="keyword">from</span> <span class="string">&#x27;react&#x27;</span>;</span><br><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&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; useNavigation &#125; <span class="keyword">from</span> <span class="string">&#x27;@react-navigation/native&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">GoToButton</span>(<span class="params">&#123; screenName &#125;</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> navigation = <span class="title function_">useNavigation</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></span></span><br><span class="line"><span class="tag"><span class="language-xml">      <span class="attr">title</span>=<span class="string">&#123;</span>`<span class="attr">Go</span> <span class="attr">to</span> $&#123;<span class="attr">screenName</span>&#125;`&#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> navigation.navigate(screenName)&#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><h2 id="给页面传递额外的属性"><a href="#给页面传递额外的属性" class="headerlink" title="给页面传递额外的属性"></a>给页面传递额外的属性</h2><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">&lt;<span class="title class_">Stack</span>.<span class="property">Screen</span></span><br><span class="line">  name=<span class="string">&quot;HomeScreen&quot;</span></span><br><span class="line">  options=&#123;&#123;<span class="attr">headerTitle</span>: <span class="string">&#x27;首页&#x27;</span>&#125;&#125;&gt;</span><br><span class="line">  &#123;<span class="function"><span class="params">props</span> =&gt;</span> <span class="language-xml"><span class="tag">&lt;<span class="name">HomeScreen</span> &#123;<span class="attr">...props</span>&#125; <span class="attr">extraData</span>=<span class="string">&#123;&#123;author:</span> &#x27;<span class="attr">紫升</span>&#x27;&#125;&#125; /&gt;</span></span>&#125;</span><br><span class="line">&lt;/<span class="title class_">Stack</span>.<span class="property">Screen</span>&gt;</span><br></pre></td></tr></table></figure><h2 id="获取-Header-Height"><a href="#获取-Header-Height" class="headerlink" title="获取 Header Height"></a>获取 Header Height</h2><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; useHeaderHeight &#125; <span class="keyword">from</span> <span class="string">&#x27;@react-navigation/stack&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">App</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">    <span class="keyword">const</span> <span class="title class_">HeaderHeight</span> = <span class="title function_">useHeaderHeight</span>() <span class="comment">// 获取Header Height</span></span><br><span class="line">    <span class="keyword">return</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="title class_">App</span></span><br></pre></td></tr></table></figure><h2 id="继续使用类组件"><a href="#继续使用类组件" class="headerlink" title="继续使用类组件"></a>继续使用类组件</h2><p>考虑到对于不适应 Hooks 的但是业务又很紧急的场景，我们可以再类组件之上封装一层来支持 React Navigation 的 Hooks 组件，之所以这么做，起因是因为 React Navigation 5 中我们只能通过 <code>useHeaderHeight()</code> 方法获取标题栏高度。</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Albums</span> <span class="keyword">extends</span> <span class="title class_ inherited__">React.Component</span> &#123;</span><br><span class="line">  <span class="title function_">render</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">ScrollView</span> <span class="attr">ref</span>=<span class="string">&#123;this.props.scrollRef&#125;</span>&gt;</span>&#123;/* content */&#125;<span class="tag">&lt;/<span class="name">ScrollView</span>&gt;</span></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="keyword">export</span> <span class="keyword">default</span> <span class="keyword">function</span>(<span class="params">props</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> ref = <span class="title class_">React</span>.<span class="title function_">useRef</span>(<span class="literal">null</span>);</span><br><span class="line">  <span class="title function_">useScrollToTop</span>(ref);</span><br><span class="line">  <span class="keyword">return</span> <span class="language-xml"><span class="tag">&lt;<span class="name">Albums</span> &#123;<span class="attr">...props</span>&#125; <span class="attr">scrollRef</span>=<span class="string">&#123;ref&#125;</span> /&gt;</span></span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/863ed71c199e/</id>
    <link href="https://blog.zisheng.pro/posts/863ed71c199e/"/>
    <published>2020-02-17T02:29:14.000Z</published>
    <summary>本文记录了 React Navigation 5 最佳实践</summary>
    <title>React Navigation 5 最佳实践</title>
    <updated>2026-09-18T08:15:28.632Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="JavaScript" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/JavaScript/"/>
    <category term="JavaScript" scheme="https://blog.zisheng.pro/tags/JavaScript/"/>
    <category term="闭包" scheme="https://blog.zisheng.pro/tags/%E9%97%AD%E5%8C%85/"/>
    <category term="循环" scheme="https://blog.zisheng.pro/tags/%E5%BE%AA%E7%8E%AF/"/>
    <category term="词法作用域" scheme="https://blog.zisheng.pro/tags/%E8%AF%8D%E6%B3%95%E4%BD%9C%E7%94%A8%E5%9F%9F/"/>
    <content>
      <![CDATA[<p>当函数可以记住并访问所在词法作用域时，就产生了闭包，即使函数是在当前词法作用域之外执行的。</p><p>要说明循环闭包，for 循环是最常见的例子。</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">for</span>(<span class="keyword">var</span> i=<span class="number">1</span>; i&lt;=<span class="number">5</span>; i++)&#123;</span><br><span class="line">  <span class="built_in">setTimeout</span>(<span class="keyword">function</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">log</span>(i)</span><br><span class="line">  &#125;, i * <span class="number">1000</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><blockquote><p>由于很多开发者对闭包的概念认识地不是很清楚，因此当循环内部包含函数定义时，代码格式检查器经常发出警告，我们在这里介绍如何才能正确使用闭包并发挥它的威力，但是代码格式检查器并没有那么灵敏，它会假设你并不真正了解自己在做什么，所以无论如何都会发出警告。</p></blockquote><p>正常情况下，我们对这段代码行为的预期是分别输出数字 1-5，每秒一次，每次一个。</p><p>但实际上，这段代码在运行时会以每秒一次的频率输出五次 <code>6</code>.</p><p>这是为什么？</p><p>首先解释 6 是从哪里来的。这个循环的终止条件是 <code>i</code> 不在 <code>&lt;=5</code>。条件首次成立时 <code>i</code> 的值是 <code>6</code>。因此，输出显示的是循环结束时 <code>i</code> 的最终值。</p><p>仔细想一下，这好像又是显而易见的，延迟函数的回调会在循环结束时才执行。事实上，当定时器运行时即使每个迭代中执行的是 <code>setTimeout(..., 0)</code>，所有的回调函数依然是在循环结束后才被执行，因此会每次输出一个 6 出来。</p><p>这里引申出一个更深入的问题，代码中到底有什么缺陷导致它的行为同语义所暗示的不一致呢？</p><p>缺陷是我们试图假设循环中的每个迭代在运行时都会给自己 “捕获” 一个 <code>i</code> 的副本。但是个根据作用域的工作原理，实际情况是尽管循环中的五个函数是在各个迭代中分别定义的，但是<strong>它们都被封闭在一个共享的全局作用域中</strong>，因此实际上只有一个 <code>i</code>。</p><p>这样的话，当然所有函数共享一个 <code>i</code> 的引用。循环结构让我们误以为背后还有更复杂的机制在起作用，但实际上没有。如果将延迟函数的回调重复定义五次，完全不使用循环，那它同这段代码时完全等价的。</p><p>下面回到正题。缺陷是什么？我们需要更多的闭包作用域，特别是在循环的过程中每个迭代都需要一个闭包作用域。</p><h3 id="IIFE"><a href="#IIFE" class="headerlink" title="IIFE"></a>IIFE</h3><p>通过 IIFE 会立即执行一个函数来创建作用域。我们可以利用 IIFE 的特性来创建一个作用域并保存每次循环的 <code>i</code>：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">for</span>(<span class="keyword">var</span> i; i &lt;= <span class="number">5</span>; i++) &#123;</span><br><span class="line">  (<span class="keyword">function</span>(<span class="params">j</span>) &#123;</span><br><span class="line">    <span class="built_in">setTimeout</span>(<span class="keyword">function</span>(<span class="params"></span>) &#123;</span><br><span class="line">      <span class="variable language_">console</span>.<span class="title function_">log</span>(j)</span><br><span class="line">    &#125;, j *<span class="number">1000</span>)</span><br><span class="line">  &#125;)(i)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>当然，这些 IIFE 也不过就是函数，因此我们可以将 i 传递进去，如果愿意的话可以将变量名定义为 j，当然也可以还叫做 i。无论如何这段代码现在可以工作了。</p><p>在迭代内使用 IIFE 会为每个迭代都生成一个新的作用域，使得延迟函数的回调可以将新的作用域封闭在每个迭代内部，每个迭代中都会含有一个具有正确值的变量供我们访问。</p><h3 id="let"><a href="#let" class="headerlink" title="let"></a>let</h3><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">for</span>(<span class="keyword">let</span> i=<span class="number">1</span>; i&lt;= <span class="number">5</span>; i++) &#123; <span class="comment">// 通过let创建闭包的作用域</span></span><br><span class="line">  <span class="built_in">setTimeout</span>(<span class="keyword">function</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">log</span>(i)</span><br><span class="line">  &#125;, i++)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>let 隐式地创建了一个作用域，起到了闭包的作用。很酷是吧？块作用域和闭包联手便可天下无敌。</p>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/082429a479e9/</id>
    <link href="https://blog.zisheng.pro/posts/082429a479e9/"/>
    <published>2020-02-16T15:57:59.000Z</published>
    <summary>本文介绍了 JavaScript 中的循环闭包</summary>
    <title>JavaScript 循环闭包</title>
    <updated>2026-09-18T08:15:28.629Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="React Native" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/React-Native/"/>
    <category term="React Native" scheme="https://blog.zisheng.pro/tags/React-Native/"/>
    <category term="metro" scheme="https://blog.zisheng.pro/tags/metro/"/>
    <content>
      <![CDATA[<p>一个 React Native 应用中可能包含许多不同的业务，这些不同的业务很可能是不同部门开发的，这样一个库中就有许许多多的重复的 React Native 代码和第三方库代码。</p><p>所以一般做法都是将重复的RN代码和第三方库打包成一个基础包，然后各个业务在基础包的基础上进行开发，这样做的好处是可以降低对内存的占用，减少加载时间，减少热更新时流量带宽等，在优化方面起到了非常大的作用。</p><h2 id="拆包方案选择"><a href="#拆包方案选择" class="headerlink" title="拆包方案选择"></a>拆包方案选择</h2><p>目前，最好的RN分包方案还是facebook官方提供的<a href="https://facebook.github.io/metro/docs/en/getting-started">metro bundle</a>，此方案是fb在0.50版本引入的，并随着RN版本的迭代不断完善。也即是说，只要你使用的是0.50以上的RN版本，就可以使用metro bundle进行差分包进行热更新。</p><p>我们选择 react-native-multibundler 作为我们的拆包方案，它基于react native的metro bundler的配置化开发来处理分包，支持iOS和Android，metro bundler为官方打包的工具，使用官方分包方法更灵活稳定，比网上的一些方法更实用可靠。</p><p>下图是我们的目标架构：</p><img src="https://i.loli.net/2019/10/12/AYqMTmdfp5jRlEi.png" style="zoom: 50%;" /><h2 id="metro-bundle"><a href="#metro-bundle" class="headerlink" title="metro bundle"></a>metro bundle</h2><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">react-native bundle</span><br><span class="line">\ --platform android</span><br><span class="line">\ --dev false</span><br><span class="line">\ --entry-file platformDep.js</span><br><span class="line">\ --bundle-output ./android/app/src/main/assets/platform.android.bundle</span><br><span class="line">\ --assets-dest android/app/src/main/res/</span><br><span class="line">\ --config /&#123;你的绝对路径&#125;/platform57.config.js</span><br></pre></td></tr></table></figure><ul><li><code>--platform [string]</code>：ios 或 android（默认ios）</li><li><code>--dev [boolean]</code>：打正式包请设置为false（默认true）</li><li><code>--entry-file &lt;path&gt;</code> ：打包入口文件，绝对路径、相对路径都可以</li><li>s&#96;–bundle-output [string]：bundle包存放的位置</li><li><code>--assets-dest [string]</code> ：存放静态资源的文件夹</li><li><code>--config [string]</code>：CLI 配置文件的路径，根据基础包业务包的不同，添加 <code>--config &lt;path/to/config&gt;</code> 参数对相应入口文件打包。</li></ul><h2 id="参考链接"><a href="#参考链接" class="headerlink" title="参考链接"></a>参考链接</h2><ul><li><a href="https://juejin.im/post/5cee0095f265da1b6d4006ec">React Native 拆包及实践「iOS&amp;Android」</a></li><li><a href="https://www.zhuanzhi.ai/document/70cdd36e33264f337529e3a18c27215c">React Native 分包哪家强？看这文就够了！</a></li><li><a href="https://github.com/ctripcorp/CRN">ctripcorp&#x2F;CRN</a>：偏向于性能优化，拆包这块不够详细</li><li><a href="https://github.com/smallnew/react-native-multibundler">react-native-multibundler</a>：react native可视化bundle拆包,支持debug,支持官方的0.57~0.60版本,使用官方的metro拆包,适用于Android、iOS</li><li><a href="https://juejin.im/post/5d906e19f265da5b9c3cf2ea">一种强大、可靠的React Native拆包以及热更新方案，基于CodePush，Metro</a></li></ul>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/c2664d63468a/</id>
    <link href="https://blog.zisheng.pro/posts/c2664d63468a/"/>
    <published>2020-02-15T08:56:19.000Z</published>
    <summary>一个 React Native 应用中可能包含许多不同的业务，这些不同的业务很可能是不同部门开发的，这样一个库中就有许许多多的重复的 React Native 代码和第三方库代码。</summary>
    <title>React Native 拆包解决方案</title>
    <updated>2026-09-18T08:15:28.632Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="React Native" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/React-Native/"/>
    <category term="React Native" scheme="https://blog.zisheng.pro/tags/React-Native/"/>
    <category term="收藏夹" scheme="https://blog.zisheng.pro/tags/%E6%94%B6%E8%97%8F%E5%A4%B9/"/>
    <content>
      <![CDATA[<h2 id="官方"><a href="#官方" class="headerlink" title="官方"></a>官方</h2><ul><li><a href="https://github.com/react-native-community/cli/">@react-native-community&#x2F;cli</a>: Command line tools that ship with react-native</li><li><a href="https://react-native-community.github.io/upgrade-helper/">React Native upgrade helper</a></li><li><a href="https://react-navigation.canny.io/">React Navigation FeedBack</a></li><li><a href="https://github.com/facebook/metro">metro</a>:  🚇 The JavaScript bundler for React Native</li><li><a href="https://fbflipper.com/">Flipper</a>: Extensible mobile app debugger</li></ul><h2 id="必备"><a href="#必备" class="headerlink" title="必备"></a>必备</h2><ul><li><a href="https://github.com/ant-design/ant-design-mobile-rn">ant-design&#x2F;ant-design-mobile-rn</a>: Ant Design for React Native</li><li><a href="https://reactnavigation.org/zh-Hans/">react-navigation</a>: React Native 应用程序的路由和导航</li><li><a href="https://github.com/Microsoft/react-native-code-push">react-native-code-push</a> React Native module for CodePush<ul><li><a href="https://github.com/strawferry/CodePushDialog">react-native-code-push-dialog</a></li></ul></li><li><a href="http://t.cn/RM8Gg7q">react-native-splash-screen</a>): React Native 启动屏，解决 iOS，Android 启动白屏问题，支持 Android 和 iOS</li><li><a href="https://github.com/jasonmerino/react-native-simple-store">react-native-simple-store</a>: <a href="https://github.com/jasonmerino/react-native-simple-store">https://github.com/jasonmerino/react-native-simple-store</a></li><li><a href="https://github.com/react-native-community/react-native-device-info">react-native-device-info</a>: Device Information for React Native iOS and Android</li></ul><h2 id="表单"><a href="#表单" class="headerlink" title="表单"></a>表单</h2><ul><li><a href="https://www.npmjs.com/package/rc-form">rc-form</a></li><li><a href="https://www.npmjs.com/package/react-native-validator">react-native-validator</a></li><li><a href="https://www.npmjs.com/package/react-native-confirmation-code-field">react-native-confirmation-code-field</a>: A react-native confirmation code field compatible with iOS, Android</li><li><a href="https://github.com/halilb/react-native-textinput-effects">react-native-textinput-effects</a>: Text inputs with custom label and icon animations for iOS and android. Built with react native and inspired by Codrops.</li><li><a href="https://www.npmjs.com/package/react-native-hide-show-password-input">react-native-hide-show-password-input</a>: React Native Hide Show Password InputText Component</li><li><a href="https://github.com/react-native-community/react-native-datetimepicker">react-native-community&#x2F;react-native-datetimepicker</a>: React Native date &amp; time picker component for iOS and Android</li><li><a href="https://github.com/umhan35/react-native-search-bar">react-native-search-bar</a>: The high-quality iOS native search bar for react native.</li></ul><h2 id="动画-手势-进度"><a href="#动画-手势-进度" class="headerlink" title="动画&#x2F;手势&#x2F;进度"></a>动画&#x2F;手势&#x2F;进度</h2><ul><li><a href="https://github.com/oblador/react-native-animatable">react-native-animatable</a>: React Native 标准易用的动画集和声明性过渡</li><li><a href="https://github.com/react-native-community/lottie-react-native">lottie-react-native</a>: Lottie wrapper for React Native</li><li><a href="http://t.cn/RsYAM96">react-native-linear-gradient</a>: A <code>&lt;LinearGradient&gt;</code> component for react-native</li><li><a href="http://t.cn/AiKMLWNy">react-native-gesture-handler</a>: 声明性 API 将平台原生触摸和手势系统暴露给 React Native</li><li><a href="http://t.cn/AiC4H5l3">react-native-spinkit</a>: React Native 的动画加载指示符集合</li><li><a href="https://github.com/oblador/react-native-progress">react-native-progress</a>: Progress indicators and spinners for React Native using ReactART</li><li><a href="https://github.com/oblador/react-native-image-progress">react-native-image-progress</a>: Progress indicator for networked images in React Native</li></ul><h2 id="UI"><a href="#UI" class="headerlink" title="UI"></a>UI</h2><ul><li><a href="https://github.com/tomLadder/react-native-echarts-wrapper">react-native-echarts-wrapper</a>: ECharts Wrapper for React Native</li><li><a href="https://github.com/avishayil/react-native-user-avatar">react-native-user-avatar</a>: React Native 的头像组件</li><li><a href="https://www.npmjs.com/package/react-native-icon-badge">react-native-icon-badge</a>: Icon badge is used to create icon badge on some element, such as avatar, icon, image…</li><li><a href="https://www.npmjs.com/package/react-native-signature-capture">react-native-signature-capture</a>: React Native library for capturing signature</li><li><a href="http://t.cn/AiC4Tgcw">@react-native-community&#x2F;blur</a>: React Native Blur component</li><li><a href="https://github.com/react-native-community/react-native-viewpager">react-native-viewpager</a>: This component allows the user to swipe left and right through pages of data</li><li><a href="http://t.cn/Ai9YU5QM">react-native-looped-carousel</a>: 🎠ReactNative 的循环旋转木马</li><li><a href="http://t.cn/Ai9YybZ6">react-native-snap-carousel</a>: deck、layout、parallax images</li><li><a href="http://t.cn/Ai9Y4qZ0">react-native-app-intro-slider</a>: Easy-to-use yet very configurable app introduction slider&#x2F;swiper based on FlatList</li><li><a href="https://github.com/bolan9999/react-native-largelist">react-native-largelist </a>: The best large list component for React Native.</li><li><a href="https://github.com/Spikef/react-native-gesture-password">react-native-gesture-password</a>: 一个React Native的手势密码组件，纯JavaScript实现，因此同时支持iOS、安卓和Web平台。</li><li><a href="https://github.com/i6mi6/react-native-parallax-scroll-view">react-native-parallax-scroll-view</a>: A ScrollView-like component with parallax and sticky header support.</li><li><a href="http://t.cn/EIJQNBS">react-native-loading-spinner-overlay</a>: React Native Loading Spinner Overlay</li><li><a href="https://github.com/crazycodeboy/react-native-easy-toast">react-native-easy-toast</a>: A react native module to show toast like android, it works on iOS and Android.</li><li><a href="https://github.com/jeanregisser/react-native-popover">react-native-popover</a>: A <code>&lt;Popover /&gt;</code> component for react-native</li><li><a href="https://github.com/morenoh149/react-native-contacts">react-native-contacts</a>: 通讯录</li><li><a href="https://github.com/jeanregisser/react-native-slider">react-native-slider</a>: React Native 的纯 JavaScript Slider 组件</li><li><a href="https://github.com/deanmcpherson/react-native-sortable-listview">react-native-sortable-listview</a>: Drag drop capable wrapper of ListView for React Native</li><li><a href="https://github.com/alexbrillant/react-native-deck-swiper">react-native-deck-swiper</a>: react-native deck swiper</li><li><a href="https://github.com/WheelerLee/react-native-dropdown-menu">react-native-dropdown-menu</a>: 易于使用的 React Native 下拉菜单组件</li><li><a href="https://github.com/gitim/react-native-sortable-list">react-native-sortable-list</a>: React Native Sortable List component</li></ul><h2 id="System"><a href="#System" class="headerlink" title="System"></a>System</h2><ul><li><a href="http://t.cn/AiCl5HTw">react-native-app-shortcuts</a>: 用于在 React Native 中创建 Android App 快捷方式的库</li><li><a href="http://t.cn/AiC5uTiP">react-native-push-notification</a>: React Native Local and Remote Notifications</li><li><a href="https://github.com/jordanbyron/react-native-quick-actions">react-native-quick-actions</a>: A react-native interface for Touch 3D home screen quick actions</li><li><a href="http://t.cn/RsYAuMA">react-native-orientation</a>: 在 react-native 中监听设备方向更改</li><li><a href="https://github.com/react-native-community/react-native-permissions">react-native-permissions</a>:An unified permissions API for React Native on iOS and Android</li><li><a href="https://github.com/douglasjunior/react-native-keyboard-manager">react-native-keyboard-manager</a>: ⚛ Library to prevent issues of keyboard sliding up and cover inputs on React-Native iOS projects.</li></ul><h2 id="多媒体"><a href="#多媒体" class="headerlink" title="多媒体"></a>多媒体</h2><ul><li><a href="http://t.cn/RcqvN9z">react-native-image-crop-picker</a>: iOS&#x2F;Android image picker with support for camera, video, configurable compression, multiple images and cropping</li><li><a href="https://github.com/huangzuizui/react-native-file-opener">react-native-file-opener</a>: A React Native module that allows you to open a file (mp3, mp4, pdf, word, excel, dwg etc.) on your device with its default application</li><li><a href="https://github.com/react-native-community/react-native-audio-toolkit">react-native-audio-toolkit</a>: Cross-platform audio library for React Native</li><li><a href="https://github.com/react-native-community/react-native-video">react-native-video</a>: A Video component for react-native</li><li><a href="https://github.com/react-native-community/react-native-camera">react-native-camera</a>: A Camera component for React Native. Also supports barcode scanning!</li><li><a href="https://github.com/DylanVann/react-native-fast-image">react-native-fast-image</a>: 🚩 FastImage, performant React Native image component.</li><li><a href="https://github.com/shahen94/react-native-video-processing">react-native-video-processing</a>: React Native 的原生视频编辑&#x2F;修剪&#x2F;压缩 🎥 库</li></ul><h2 id="Utils"><a href="#Utils" class="headerlink" title="Utils"></a>Utils</h2><ul><li><a href="https://github.com/ptelad/react-native-iphone-x-helper">react-native-iphone-x-helper</a>: A library to help you design your react-native app for the iPhone X, XS, XS Max &amp; XR</li><li><a href="https://github.com/oblador/react-native-keychain">react-native-keychain</a>: 🔑 Keychain Access for React Native</li><li><a href="https://github.com/joshswan/react-native-globalize">react-native-globalize</a>: 国际化</li></ul><h2 id="工具"><a href="#工具" class="headerlink" title="工具"></a>工具</h2><ul><li><a href="https://github.com/JonnyBurger/npx-visualize-bundle">visualize-bundle</a>: <code>npx visualize-bundle</code> allows you to inspect your React Native bundle in just one command and to diagnose big modules.</li><li><a href="https://github.com/smallnew/react-native-multibundler">react-native-multibundler</a>: react native 可视化 bundle 拆包，支持官方的 0.57~0.59 版本，使用官方的 metro 拆包，适用于 Android、iOS</li><li><a href="https://github.com/shwenzhang/AndResGuard">AndResGuard</a>: Android 资源混淆工具</li><li><a href="https://github.com/skylot/jadx">jadx</a>: 反编译工具</li><li><a href="https://github.com/frostney/react-native-create-library">react-native-create-library</a>: 📓 Command line tool to create a React Native library with a single command</li><li><a href="http://t.cn/Rk0o2em">lisong&#x2F;code-push-server</a>: 自建 code-push 服务器</li><li><a href="https://github.com/wix/wml">wml</a>: 符号链接的替代方法，它实际上将已更改的文件从源文件夹复制到目标文件夹</li><li><a href="https://ethercreative.github.io/react-native-shadow-generator/">react-native-shadow-generator</a></li></ul><h2 id="第三方SDK"><a href="#第三方SDK" class="headerlink" title="第三方SDK"></a>第三方SDK</h2><ul><li><a href="http://t.cn/RIPNOd5">jpush-react-native</a>: 极光推送官方支持的 React Native 插件</li><li><a href="https://github.com/sishuguojixuefu/react-native-qiniu">react-native-qiniu</a>: react-native-qiniu</li><li><a href="http://t.cn/E9W1RhM">react-native-amap3d</a>: react-native 高德地图组件，使用最新 3D SDK，支持 Android + iOS</li><li><a href="http://t.cn/E9W1rwb">react-native-amap-geolocation</a>: React Native geolocation module for Android + iOS</li><li><a href="https://github.com/getsentry/sentry-react-native">sentry-react-native</a>: Official Sentry SDK for react-native</li></ul><h2 id="开源项目"><a href="#开源项目" class="headerlink" title="开源项目"></a>开源项目</h2><ul><li><a href="https://github.com/stoneWeb/elm-react-native">elm-react-native</a>: A react native app simulating eleme app，run ios and android.</li><li><a href="https://github.com/listenzz/MyApp">React Native 工程实践</a></li></ul><h2 id="优质文章"><a href="#优质文章" class="headerlink" title="优质文章"></a>优质文章</h2><ul><li><a href="https://bre.is/JdW9YRdE">用React-Native+Mobx做一个迷你水果商城APP</a></li><li><a href="https://bre.is/xNQwNZbZ">react-native 通讯录字母跳转分组功能</a></li><li><a href="https://bre.is/DxEXYBfx">React Native应用部署&#x2F;热更新-CodePush最新集成总结(新)</a></li></ul><h2 id="其他"><a href="#其他" class="headerlink" title="其他"></a>其他</h2><ul><li>LeancloudPushDemo: React native + leancloud push notification demo</li><li>react-native-code-push-dialog: CodePush Custom Dialog，注意：苹果 App、中国的 android 市场必须采用静默更新</li><li>react-native-dropdown这是下拉菜单的简单实现</li><li>react-native-baidu-map百度地图 React Native 模块，支持 react native 0.57+，已更新到最新的百度地图 SDK 版本。</li><li>react-native-search-boxA simple search box with animation, inspired from ios search bar. Lightweight, fast, flexible.</li></ul>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/2d966006042c/</id>
    <link href="https://blog.zisheng.pro/posts/2d966006042c/"/>
    <published>2020-02-14T07:36:00.000Z</published>
    <summary>本文记录了一些 React Native 相关的收藏夹</summary>
    <title>React Native 收藏夹</title>
    <updated>2026-09-18T08:15:28.632Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="JavaScript" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/JavaScript/"/>
    <category term="防抖" scheme="https://blog.zisheng.pro/tags/%E9%98%B2%E6%8A%96/"/>
    <category term="节流" scheme="https://blog.zisheng.pro/tags/%E8%8A%82%E6%B5%81/"/>
    <category term="JavaScript throttle" scheme="https://blog.zisheng.pro/tags/JavaScript-throttle/"/>
    <category term="JavaScript debounce" scheme="https://blog.zisheng.pro/tags/JavaScript-debounce/"/>
    <content>
      <![CDATA[<p>在前端开发中有一部分的用户行为会频繁地触发事件执行，而对于 DOM 操作、资源加载等耗费性能的处理，很可能导致界面卡顿，甚至浏览器的崩溃，函数节流（<code>throttle</code>）和函数防抖（<code>debounce</code>）就是为了解决类似需求应运而生的。</p><h2 id="函数节流"><a href="#函数节流" class="headerlink" title="函数节流"></a>函数节流</h2><p>函数节流就是预定一个函数只有在大于等于执行周期时才执行，周期内调用不执行。好像水滴攒到一定重量才会落下一样。场景：窗口调整（resize）、页面滚动（scroll）、抢购疯狂点击（mousedown）。</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">throttle</span> (fn, wait = <span class="number">0</span>) &#123;</span><br><span class="line">  <span class="keyword">let</span> lastTime = <span class="number">0</span></span><br><span class="line">  <span class="keyword">return</span> <span class="keyword">function</span> (<span class="params"></span>) &#123;</span><br><span class="line">    <span class="keyword">let</span> now = <span class="title class_">Date</span>.<span class="title function_">now</span>()</span><br><span class="line">    <span class="keyword">if</span>(now - lastTime &gt;= wait) &#123;</span><br><span class="line">      <span class="comment">// 这里并不需要考虑执行环境，所以调用 apply() 时第一个参数是 null，之所以使用 apply 传递参数，是因为第二个参数可以是 arguments 对象</span></span><br><span class="line">      fn.<span class="title function_">apply</span>(<span class="literal">null</span>, <span class="variable language_">arguments</span>)</span><br><span class="line">      lastTime = now</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">function</span> <span class="title function_">boom</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">&quot;boom&quot;</span>)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="built_in">setInterval</span>(<span class="title function_">throttle</span>(boom,<span class="number">2000</span>), <span class="number">1000</span>) <span class="comment">// 一个周期内触发了两次定时器，但是一个周期内只会执行一次 boom 方法。</span></span><br></pre></td></tr></table></figure><h2 id="函数防抖"><a href="#函数防抖" class="headerlink" title="函数防抖"></a>函数防抖</h2><p>函数防抖就是在函数需要频繁触发情况时，只有足够空闲的时间，才执行一次。好像公交司机会等人都上车后才出站一样。场景：实时搜索（keyup）、拖拽（mousemove）</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">debounce</span>(<span class="params">fn, wait = <span class="number">0</span></span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="keyword">function</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="built_in">clearTimeout</span>(fn.<span class="property">tId</span>)</span><br><span class="line">    fn.<span class="property">tId</span> = <span class="built_in">setTimeout</span>(<span class="keyword">function</span>(<span class="params"></span>) &#123;</span><br><span class="line">      fn.<span class="title function_">apply</span>(<span class="variable language_">this</span>, <span class="variable language_">arguments</span>)</span><br><span class="line">    &#125;.<span class="title function_">bind</span>(<span class="variable language_">this</span>), wait) <span class="comment">// 使用 bind 来确保函数执行环境的正确性，否则就是 global 了</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"><span class="variable language_">document</span>.<span class="title function_">getElementById</span>(<span class="string">&quot;debounce&quot;</span>).<span class="property">onclick</span> = <span class="title function_">debounce</span>(boom, <span class="number">300</span>)</span><br><span class="line"><span class="comment">// 单位时间内无论操作多少次，都会合并成一次操作，并在最后一次操作后延迟 300 毫秒执行</span></span><br></pre></td></tr></table></figure>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/74820d054d6e/</id>
    <link href="https://blog.zisheng.pro/posts/74820d054d6e/"/>
    <published>2020-02-13T12:33:46.000Z</published>
    <summary>在前端开发中有一部分的用户行为会频繁地触发事件执行，而对于 DOM 操作、资源加载等耗费性能的处理，很可能导致界面卡顿，甚至浏览器的崩溃，JavaScript 函数节流与函数防抖就是为了解决类似需求应运而生的。</summary>
    <title>JavaScript 函数节流与函数防抖</title>
    <updated>2026-09-18T08:15:28.629Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="JavaScript" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/JavaScript/"/>
    <category term="JavaScript" scheme="https://blog.zisheng.pro/tags/JavaScript/"/>
    <category term="slice" scheme="https://blog.zisheng.pro/tags/slice/"/>
    <category term="substring" scheme="https://blog.zisheng.pro/tags/substring/"/>
    <category term="substr" scheme="https://blog.zisheng.pro/tags/substr/"/>
    <content>
      <![CDATA[<table><thead><tr><th>方法</th><th>参数</th><th>返回值</th></tr></thead><tbody><tr><td><code>slice(start,end)</code></td><td><code>start</code>(必需)-起始位置<br /><code>end</code>(可选)-结束位置，若未指定，则默认到末尾所有元素</td><td>返回 <code>[start,end]</code>之间的元素</td></tr><tr><td><code>substring(start, end)</code></td><td><code>start</code>(必需)-起始位置<br /><code>end</code>(必需)-结束位置，若未指定，则默认到末尾所有元素</td><td>返回 <code>[start, end]</code> 之间的元素</td></tr><tr><td><code>substr(start)</code></td><td><code>start</code>(必需)-起始位置<br /><code>length</code>(可选)-所截取的元素的个数，若未指定，则默认到末尾</td><td>返回 <code>[start, start+length]</code>之间的元素</td></tr></tbody></table><blockquote><p>当传递的参数都为正数时，<code>substring</code> 没有区别，当参数为负数时，三个函数的行为不尽相同。</p></blockquote><ul><li><code>slice</code>: 将传入的福参数与字符串长度想加；</li><li><code>substring</code>: 把所有的负值置为0；</li><li><code>substr</code>: 将负的第一个参数与字符串长度相加，负的第二个参数置为0。</li></ul><p><strong>参数为正数的例子</strong></p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> str = <span class="string">&#x27;hello world&#x27;</span>;</span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(str.<span class="title function_">slice</span>(<span class="number">3</span>)); <span class="comment">// lo world</span></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(str.<span class="title function_">substring</span>(<span class="number">3</span>));<span class="comment">// lo world</span></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(str.<span class="title function_">substr</span>(<span class="number">3</span>));<span class="comment">// lo world</span></span><br><span class="line"></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(str.<span class="title function_">slice</span>(<span class="number">3</span>, <span class="number">7</span>)); <span class="comment">// lo w</span></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(str.<span class="title function_">substring</span>(<span class="number">3</span>, <span class="number">7</span>)); <span class="comment">// lo w</span></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(str.<span class="title function_">substr</span>(<span class="number">3</span>, <span class="number">7</span>));<span class="comment">//lo worl</span></span><br></pre></td></tr></table></figure><p><strong>参数为负数</strong></p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> str = <span class="string">&#x27;hello world&#x27;</span>;</span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(str.<span class="title function_">slice</span>(-<span class="number">3</span>)); <span class="comment">// rld</span></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(str.<span class="title function_">substring</span>(-<span class="number">3</span>));<span class="comment">// hello world</span></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(str.<span class="title function_">substr</span>(-<span class="number">3</span>));<span class="comment">// rld</span></span><br><span class="line"></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(str.<span class="title function_">slice</span>(<span class="number">3</span>, -<span class="number">4</span>)); <span class="comment">// lo w</span></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(str.<span class="title function_">substring</span>(<span class="number">3</span>, -<span class="number">4</span>)); <span class="comment">// hel</span></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(str.<span class="title function_">substr</span>(<span class="number">3</span>, -<span class="number">4</span>));<span class="comment">// &quot;&quot;(空字符串)</span></span><br></pre></td></tr></table></figure>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/b0aeb6b939b0/</id>
    <link href="https://blog.zisheng.pro/posts/b0aeb6b939b0/"/>
    <published>2020-02-09T13:14:53.000Z</published>
    <summary>本文介绍了 slice、substring 和 substr 的区别</summary>
    <title>slice、substring 和 substr 的区别</title>
    <updated>2026-09-18T08:15:28.636Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="JavaScript" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/JavaScript/"/>
    <category term="JavaScript" scheme="https://blog.zisheng.pro/tags/JavaScript/"/>
    <category term="apply" scheme="https://blog.zisheng.pro/tags/apply/"/>
    <category term="call" scheme="https://blog.zisheng.pro/tags/call/"/>
    <category term="bind" scheme="https://blog.zisheng.pro/tags/bind/"/>
    <content>
      <![CDATA[<p>这三个函数的存在意义是什么？答案是<strong>改变函数执行时的上下文</strong>，再具体一点就是改变函数运行时的 <code>this</code> 指向。</p><h2 id="在特定的作用域中调用函数"><a href="#在特定的作用域中调用函数" class="headerlink" title="在特定的作用域中调用函数"></a>在特定的作用域中调用函数</h2><p><code>apply()</code> 和 <code>call()</code> 方法的用途都是在特定的作用域中调用函数。</p><h3 id="apply"><a href="#apply" class="headerlink" title="apply"></a>apply</h3><blockquote><p><code>apply</code> 方法接收两个参数：一个是在其中运行函数的作用域，另一个是参数数组。其中，第二个参数可以是 Array 的实例，也可以是 <code>arguments</code> 对象。</p></blockquote><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">sum</span>(<span class="params">num1, num2</span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> num1 + num2</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">function</span> <span class="title function_">callSum1</span>(<span class="params">num1, num2</span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> sum.<span class="title function_">apply</span>(<span class="variable language_">this</span>, <span class="variable language_">arguments</span>) <span class="comment">// 传入 arguments 对象</span></span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">function</span> <span class="title function_">callSum2</span>(<span class="params">num1, num2</span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> sum.<span class="title function_">apply</span>(<span class="variable language_">this</span>, [num1, num2]) <span class="comment">// 传入参数</span></span><br><span class="line">&#125;</span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="title function_">callSum1</span>(<span class="number">10</span>, <span class="number">10</span>))</span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="title function_">callSum2</span>(<span class="number">10</span>, <span class="number">10</span>))</span><br></pre></td></tr></table></figure><h2 id="call"><a href="#call" class="headerlink" title="call"></a>call</h2><blockquote><p><code>call()</code> 方法与 <code>apply()</code> 方法的作用相同，它们区别仅在于接受参数的方式不同。对于 <code>call()</code> 方法而言，第一个参数是 <code>this</code> 值没有变化，变化的是其余参数都直接传递给函数。</p></blockquote><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">sum</span>(<span class="params">num1, num2</span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> num1 + num2</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">function</span> <span class="title function_">callSum</span>(<span class="params">num1, num2</span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> sum.<span class="title function_">call</span>(<span class="variable language_">this</span>, num1, num2)</span><br><span class="line">&#125;</span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="title function_">callSum</span>(<span class="number">10</span>, <span class="number">10</span>))</span><br></pre></td></tr></table></figure><blockquote><p>不传递参数的情况下，使用 <code>call()</code> 方法语义跟明确</p></blockquote><h2 id="创建一个改变了上下文的函数"><a href="#创建一个改变了上下文的函数" class="headerlink" title="创建一个改变了上下文的函数"></a>创建一个改变了上下文的函数</h2><p><code>call()</code> 和 <code>apply()</code> 方法改变了函数的 <code>this</code> 上下文之后便立即执行该函数，而 <code>bind()</code> 方法会创建一个函数的实例并返回，其 <code>this</code> 值会被绑定到传递给 <code>bind()</code> 函数的值。</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> color = <span class="string">&#x27;red&#x27;</span></span><br><span class="line"><span class="keyword">var</span> o = &#123;</span><br><span class="line">  <span class="attr">color</span>: <span class="string">&#x27;blue&#x27;</span>,</span><br><span class="line">  <span class="attr">sayColor</span>: <span class="keyword">function</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="variable language_">this</span>.<span class="property">color</span>) <span class="comment">// 这个 this 的值是 o</span></span><br><span class="line">    <span class="built_in">setTimeout</span>(</span><br><span class="line">      <span class="keyword">function</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="variable language_">this</span>.<span class="property">color</span>) <span class="comment">// 默认的值是 this 的值是 window</span></span><br><span class="line">      &#125;.<span class="title function_">bind</span>(<span class="variable language_">this</span>),</span><br><span class="line">      <span class="number">1000</span>,</span><br><span class="line">    )</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_">console</span>.<span class="title function_">log</span>(o.<span class="title function_">sayColor</span>()) <span class="comment">// blue</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">sayColor</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="title function_">alert</span>(<span class="variable language_">this</span>.<span class="property">color</span>)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> objectSayColor = sayColor.<span class="title function_">bind</span>(o)</span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="title function_">objectSayColor</span>()) <span class="comment">// blue</span></span><br></pre></td></tr></table></figure>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/30547169b42e/</id>
    <link href="https://blog.zisheng.pro/posts/30547169b42e/"/>
    <published>2020-02-09T08:07:05.000Z</published>
    <summary>本文介绍了 JavaScript apply、call、bind 三兄弟的使用</summary>
    <title>JavaScript apply、call、bind 三兄弟</title>
    <updated>2026-09-18T08:15:28.629Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="JavaScript" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/JavaScript/"/>
    <category term="ES6" scheme="https://blog.zisheng.pro/tags/ES6/"/>
    <category term="let" scheme="https://blog.zisheng.pro/tags/let/"/>
    <category term="const" scheme="https://blog.zisheng.pro/tags/const/"/>
    <content>
      <![CDATA[<p><code>let</code> 与 <code>const</code> 都是只在声明所在的块级作用域内有效。</p><p><code>let</code> 声明的变量可以改变，值和类型都可以改变，没有限制。<br><code>const</code> 声明的变量不得改变值，这意味着，<code>const</code> 一旦声明变量，就必须立即初始化，不能留到以后赋值。</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> a <span class="comment">// Uncaught SyntaxError: Missing initializer in const declaration</span></span><br><span class="line"><span class="keyword">const</span> b = <span class="number">2</span></span><br><span class="line">b = <span class="number">3</span> <span class="comment">// Uncaught TypeError: Assignment to constant variable.</span></span><br></pre></td></tr></table></figure><p>对于引用类型的值，如数组和对象，变量保存的是值的引用。<code>const</code> 命令只是保证引用不变，并不保证值不变。</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> names = []</span><br><span class="line">names = [<span class="number">1</span>,<span class="number">2</span>,<span class="number">3</span>] <span class="comment">// Uncaught TypeError: Assignment to constant variable.</span></span><br></pre></td></tr></table></figure><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">//不会报错，因为names指向的地址不变，改变的只是内部数据</span></span><br><span class="line"><span class="keyword">const</span> names = []</span><br><span class="line">names[<span class="number">0</span>] = <span class="number">1</span></span><br><span class="line">names[<span class="number">1</span>] = <span class="number">2</span></span><br><span class="line">names[<span class="number">2</span>] = <span class="number">3</span></span><br></pre></td></tr></table></figure>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/d9064012188b/</id>
    <link href="https://blog.zisheng.pro/posts/d9064012188b/"/>
    <published>2020-02-07T10:49:11.000Z</published>
    <summary>let 与 const 都是只在声明所在的块级作用域内有效。</summary>
    <title>let 和 const 的区别</title>
    <updated>2026-09-18T08:15:28.632Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="JavaScript" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/JavaScript/"/>
    <category term="JavaScript" scheme="https://blog.zisheng.pro/tags/JavaScript/"/>
    <category term="浅拷贝" scheme="https://blog.zisheng.pro/tags/%E6%B5%85%E6%8B%B7%E8%B4%9D/"/>
    <category term="深拷贝" scheme="https://blog.zisheng.pro/tags/%E6%B7%B1%E6%8B%B7%E8%B4%9D/"/>
    <category term="基础类型" scheme="https://blog.zisheng.pro/tags/%E5%9F%BA%E7%A1%80%E7%B1%BB%E5%9E%8B/"/>
    <category term="引用类型" scheme="https://blog.zisheng.pro/tags/%E5%BC%95%E7%94%A8%E7%B1%BB%E5%9E%8B/"/>
    <content>
      <![CDATA[<h2 id="基础类型值和引用类型值的区别"><a href="#基础类型值和引用类型值的区别" class="headerlink" title="基础类型值和引用类型值的区别"></a>基础类型值和引用类型值的区别</h2><h3 id="保存方式"><a href="#保存方式" class="headerlink" title="保存方式"></a>保存方式</h3><blockquote><p>基础类型：变量 -&gt; 值<br>引用类型：变量 -&gt; 值的堆内存地址 -&gt; 值</p></blockquote><p>变量保存在栈内存中，栈内存是有序排列的，大小是固定的。在将一个值赋值给变量时，解析器必须确定这个值是基本类型值还是引用类型值。也就是说变量的值到底存的是实际值还是实际值的引用是根据实际值的类型决定的：基础类型的值是保存在变量中实际值，而引用类型的值是保存在堆内存中的堆内存是无序排列的，大小是不固定的），变量中存的值是实际值的堆内存地址。</p><h3 id="复制变量值"><a href="#复制变量值" class="headerlink" title="复制变量值"></a>复制变量值</h3><blockquote><p>基础类型：变量a -&gt; 值；变量b -&gt;值的副本<br>引用类型：变量b -&gt; 变量a -&gt; 值的堆内存地址 -&gt; 值</p></blockquote><p>在 ECMAScript 中，除了保存的方式不同之外，在从一个变量向另一个变量复制基本类型值和引用类型值时，也存在不同。</p><p>如果从一个变量向另一个变量复制基本类型的值，会在变量对象上创建一个新值，然后把该值复制到为新变量分配的位置上。新值只是一个副本。</p><p>当从一个变量向另一个变量复制引用类型的值时，同样也会将存储在变量对象上的值复制一份放到为新变量分配的空间上。不同的是，这个值的副本实际上是一个指针，而这个指针指向存储在堆中的一个对象。复制操作结束后，两个变量实际上将引用同一个对象。因此改变其中一个变量，就会影响另一个变量。</p><h3 id="传递参数"><a href="#传递参数" class="headerlink" title="传递参数"></a>传递参数</h3><p>ECMAScript 中所有函数的参数都是按值（变量的值，不一定是实际值）传递的。也就是说，把函数外部的值复制给函数内部参数，就和把值从一个变量复制给另一个变量是一样的。</p><p>在向参数传递基本类型的值时，被传递的值会被复制给一个局部变量（即命名参数，或者用 ECMAScript 的概念来说，就是 <code>arguments</code> 对象中的一个元素）。</p><p>在向参数传递引用类型的值时，会把这个值在内存中的地址复制给一个局部变量，因此这个局部变量的变化会反映在函数的外部。虽然变量是按值传递的，但是这个值它是实际值的引用，所以局部变量也是按照这个引用访问了同一个对象。</p><h2 id="浅拷贝"><a href="#浅拷贝" class="headerlink" title="浅拷贝"></a>浅拷贝</h2><p>浅复制只复制一层对象的属性，也就是说拷贝原对象的实例，但是对其内部的引用类型值，拷贝的是其引用</p><h3 id="自行实现浅拷贝"><a href="#自行实现浅拷贝" class="headerlink" title="自行实现浅拷贝"></a>自行实现浅拷贝</h3><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">shallowCopy</span>(<span class="params">src</span>) &#123;</span><br><span class="line">  <span class="keyword">var</span> dst = &#123;&#125;</span><br><span class="line">  <span class="keyword">for</span> (<span class="keyword">var</span> prop <span class="keyword">in</span> src) &#123;</span><br><span class="line">    <span class="keyword">if</span> (src.<span class="title function_">hasOwnProperty</span>(prop)) &#123;</span><br><span class="line">      dst[prop] = src[prop]</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">return</span> dst</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">var</span> obj = &#123; <span class="attr">a</span>:<span class="number">1</span>, <span class="attr">arr</span>: [<span class="number">2</span>,<span class="number">3</span>] &#125;</span><br><span class="line"><span class="keyword">var</span> shallowObj = <span class="title function_">shallowCopy</span>(obj)</span><br><span class="line"><span class="comment">// shallowObj.a = 1</span></span><br><span class="line"><span class="comment">// console.log(obj) // &#123; a:1, arr: [2,3] &#125;</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// shallowObj.arr[0] = 1</span></span><br><span class="line"><span class="comment">// console.log(obj) // &#123; a:1, arr: [1,3] &#125;</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// shallowObj.arr = [1,3]</span></span><br><span class="line"><span class="comment">// console.log(obj) // &#123; a:1, arr: [2,3] &#125;</span></span><br></pre></td></tr></table></figure><h3 id="工程化"><a href="#工程化" class="headerlink" title="工程化"></a>工程化</h3><ul><li><a href="https://www.lodashjs.com/docs/latest#_clonevalue">_.clone</a>: 创建一个 value 的浅拷贝。</li></ul><h2 id="深拷贝"><a href="#深拷贝" class="headerlink" title="深拷贝"></a>深拷贝</h2><p>深拷贝是对对象以及对象的所有子对象进行拷贝。我们可以使用 lodash 的 <code>cloneDeep</code> 方法，但是深拷贝大数据的时候，由于内部实现是递归，所以最好换实现思路。</p><h3 id="JSON-parse-JSON-stringify-深拷贝的局限"><a href="#JSON-parse-JSON-stringify-深拷贝的局限" class="headerlink" title="JSON.parse(JSON.stringify) 深拷贝的局限"></a><code>JSON.parse(JSON.stringify)</code> 深拷贝的局限</h3><ol><li>如果被拷贝的对象中有 <code>function</code>，则拷贝之后的对象就会<strong>丢失</strong>这个 <code>function</code></li><li>如果被拷贝的对象中有正则表达式，则拷贝之后的正则表达式会变成 <code>Object</code></li></ol><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> a = &#123;<span class="attr">name</span>: <span class="string">&#x27;youngjuning&#x27;</span>, <span class="attr">sayName</span>: <span class="keyword">function</span>(<span class="params"></span>) &#123;<span class="title function_">alert</span>(<span class="variable language_">this</span>.<span class="property">name</span>)&#125;, <span class="attr">numberReg</span>: <span class="regexp">/\d+/</span>&#125;</span><br><span class="line"><span class="keyword">let</span> b = <span class="title class_">JSON</span>.<span class="title function_">parse</span>(<span class="title class_">JSON</span>.<span class="title function_">stringify</span>(a))</span><br><span class="line"><span class="comment">// sayName丢失，numberReg变为了对象</span></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">dir</span>(b) <span class="comment">// &#123;name: &#x27;youngjuning&#x27;, numberReg: &#123;&#125;&#125;</span></span><br></pre></td></tr></table></figure><h2 id="深拷贝与浅拷贝的区别"><a href="#深拷贝与浅拷贝的区别" class="headerlink" title="深拷贝与浅拷贝的区别"></a>深拷贝与浅拷贝的区别</h2><ul><li>深拷贝：将 B 对象拷贝到 A 对象中，包括 B 里面的子对象，</li><li>浅拷贝：将 B 对象拷贝到 A 对象中，但不包括 B 里面的子对象</li></ul>]]>
    </content>
    <id>https://blog.zisheng.pro/posts/97c7a5c526fe/</id>
    <link href="https://blog.zisheng.pro/posts/97c7a5c526fe/"/>
    <published>2020-02-07T04:00:57.000Z</published>
    <summary>本文介绍了 JavaScript 中的浅拷贝与深拷贝</summary>
    <title>JavaScript 浅拷贝与深拷贝</title>
    <updated>2026-09-18T08:15:28.629Z</updated>
  </entry>
  <entry>
    <author>
      <name>紫升</name>
    </author>
    <category term="前端" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="JavaScript" scheme="https://blog.zisheng.pro/categories/%E5%89%8D%E7%AB%AF/JavaScript/"/>
    <category term="JavaScript" scheme="https://blog.zisheng.pro/tags/JavaScript/"/>
    <category term="笔记" scheme="https://blog.zisheng.pro/tags/%E7%AC%94%E8%AE%B0/"/>
    <content>
      <![CDATA[<div class="note danger modern"><p>All in One Post 的学习笔记不可持续且可读性差，之后不会再做此类尝试。</p></div><h2 id="JavaScript-由哪些部分组成？"><a href="#JavaScript-由哪些部分组成？" class="headerlink" title="JavaScript 由哪些部分组成？"></a>JavaScript 由哪些部分组成？</h2><ul><li>ECMAScript：由 ECMA-262d 定义并提供核心功能。</li><li>文档对象模型（DOM，Document Object Model）：提供与网页交互的方法和接口</li><li>浏览器对象模型（BOM，Browser Object Model）：提供与浏览器交互的方法和接口</li></ul><h2 id="ECMA-262-到底定义了什么？"><a href="#ECMA-262-到底定义了什么？" class="headerlink" title="ECMA-262 到底定义了什么？"></a>ECMA-262 到底定义了什么？</h2><ul><li>语法</li><li>类型</li><li>语句</li><li>关键字</li><li>保留字</li><li>操作符</li><li>全局对象</li></ul><p>ECMAScript 只是对实现这个规范描述的所有方面的一个抽象，它并不是一个完整的语言。JavaScript 实现了 ECMAScrip，而 Adobe ActionScript 同样实现了 ECMAScript。</p><h2 id="JavaScript-标识符"><a href="#JavaScript-标识符" class="headerlink" title="JavaScript 标识符"></a>JavaScript 标识符</h2><p>所谓标识符，就是变量、函数、函数参数或对象属性的名称。标识符可以由一或多个下列字符组成：</p><ul><li>第一个字符必须是一个字符、下划线（_）或美元符合（$）；</li><li>剩下的其他字符可以是字母、下划线（_）、美元符号或数字。</li></ul><blockquote><p>注意：标识符中的字母可以是扩展 ASCII（Extended ASCII） 中的字母，也可以是 Unicode 的字母字符。</p></blockquote><blockquote><p>注意：关键字、保留字、true、false、null 不能作为标识符。</p></blockquote><h2 id="var-声明作用域"><a href="#var-声明作用域" class="headerlink" title="var 声明作用域"></a>var 声明作用域</h2><p>使用 var 操作符定义的变量会成为包含的函数的局部变量，在函数内部使用，函数结束后变量就会被销毁。</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">test</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">var</span> message = <span class="string">&#x27;Hello World&#x27;</span>; <span class="comment">// 局部变量</span></span><br><span class="line">&#125;</span><br><span class="line"><span class="title function_">test</span>();</span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(message); <span class="comment">// ReferenceError: message is not defined</span></span><br></pre></td></tr></table></figure><blockquote><p>注意：去掉之前的 var 操作符之后，message 就变成了全局变量。只要调用一次函数 <code>test()</code>，就会定义这个变量，并且可以在函数外部访问到。</p></blockquote><h2 id="var-声明提升"><a href="#var-声明提升" class="headerlink" title="var 声明提升"></a>var 声明提升</h2><p>使用 <code>var</code> 声明的变量会被自动提升到函数作用域顶部：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">test</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="variable language_">console</span>.<span class="title function_">log</span>(age); <span class="comment">// undefined</span></span><br><span class="line">  <span class="keyword">var</span> age = <span class="number">18</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>之所以不会报错，是因为 ECMAScript 运行时把它看成等价如下的代码：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">foo</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">var</span> age;</span><br><span class="line">  <span class="variable language_">console</span>.<span class="title function_">log</span>(age); <span class="comment">// undefined</span></span><br><span class="line">  age = <span class="number">18</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这就是所谓的“提升”（hoist），也就是把所有变量声明都拉到函数作用域的顶部。</p><h2 id="var-重复声明"><a href="#var-重复声明" class="headerlink" title="var 重复声明"></a>var 重复声明</h2><p>反复多次使用 <code>var</code> 声明同一个变量也没有问题：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">foo</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">var</span> age = <span class="number">16</span>;</span><br><span class="line">  <span class="keyword">var</span> age = <span class="number">26</span>;</span><br><span class="line">  <span class="keyword">var</span> age = <span class="number">36</span>;</span><br><span class="line">  <span class="variable language_">console</span>.<span class="title function_">log</span>(age); <span class="comment">// 36</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="JS的原始类型有哪几种？"><a href="#JS的原始类型有哪几种？" class="headerlink" title="JS的原始类型有哪几种？"></a>JS的原始类型有哪几种？</h2><p>在 JS 中，存在这6种原始值，分别是：</p><ul><li><code>number</code></li><li><code>string</code></li><li><code>boolean</code></li><li><code>undefined</code></li><li><code>null</code></li><li><code>symbol</code></li></ul><h2 id="null-是对象吗？"><a href="#null-是对象吗？" class="headerlink" title="null 是对象吗？"></a>null 是对象吗？</h2><p>对于 <code>null</code> 来说，很多人会认为他是个对象类型，其实这是错误的。虽然 <code>typeof null</code> 会输出 <code>object</code>，但是这只是 JS 存在的一个悠久 Bug。在 JS 的最初版本中使用的是 32 位系统，为了性能考虑使用低位存储变量的类型信息，<code>000</code> 开头代表对象，然而 <code>null</code> 表示为全零，所以将它错误地判断为 <code>object</code>。虽然现在的内部类型判断代码已经改变了，但是对于这个 Bug 却是一直流传下来。</p><h2 id="栈内存和堆内存的区别"><a href="#栈内存和堆内存的区别" class="headerlink" title="栈内存和堆内存的区别"></a>栈内存和堆内存的区别</h2><ul><li>栈内存：栈是一种先进后出（FILO，First-In-Last-Out）的数据结构。栈内存中存放变量</li><li>堆内存：堆是一种经过排序的树形数据结构，每个结点都有一个值。堆的存取是随意的。堆内存中存放复杂对象</li></ul><h2 id="基本类型和引用类型的值"><a href="#基本类型和引用类型的值" class="headerlink" title="基本类型和引用类型的值"></a>基本类型和引用类型的值</h2><p>ECMAScript 变量可能包含两种不同数据类型的值：基本类型值和引用类型值。<strong>基本类型值</strong>指的是简单的数据段，而<strong>引用类型值</strong>指那些可能由多个值构成的对象。</p><h3 id="保存方式"><a href="#保存方式" class="headerlink" title="保存方式"></a>保存方式</h3><p>在将一个值赋给变量时，解析器必须确定这个值是基本类型值还是引用类型值。基本类型的值是直接保存在变量中的，因此可以直接操作保存在变量中的实际值，所以基本类型的值是按值访问的。引用类型的值是保存在内存中的对象，在操作对象时，实际上是在操作对象的引用而不是实际的对象，但在为对象添加属性时，操作的是实际的对象，为此，引用类型的值是按引用访问的。</p><h3 id="动态的属性"><a href="#动态的属性" class="headerlink" title="动态的属性"></a>动态的属性</h3><p>基本类型的值是不可变的，是没有函数可以调用的。对于引用类型的值，它的属性则是动态的，我们可以为其添加属性和方法，也可以改变和删除其属性和方法。</p><h3 id="复制变量值"><a href="#复制变量值" class="headerlink" title="复制变量值"></a>复制变量值</h3><p>除了保存的方式不同之外，在从一个变量向另一个变量复制基本类型值和引用类型值时，也存在不同。如果从一个变量向另一个变量复制基本类型的值，会在变量对象上创建一个新值，然后把该值复制到为新变量分配的位置上。当从一个变量向另一个变量复制引用类型的值时，同样也会将存储在变量对象中的值复制一份放到为新变量分配的空间中。不同的是，这个值的副本实际上是一个指针，而这个指针指向存储在堆中的一个对象。复制操作结束后，两个变量实际上将引用同一个对象。因此，改变其中一个变量，就会影响另一个变量。</p><blockquote><p>对象类型和原始类型的不同之处？</p><p>在 JS 中，除了原始类型那么其他的都是对象类型了。对象类型和原始类型不同的是，原始类型存储的值，对象类型存储的是地址（指针）。当你创建了一个对象类型的时候，计算机会在内存中帮我们开辟一个空间来存放值，但是我们需要找到这个空间，这个空间会拥有一个地址（指针）。</p></blockquote><h2 id="函数参数是对象会发生什么问题？"><a href="#函数参数是对象会发生什么问题？" class="headerlink" title="函数参数是对象会发生什么问题？"></a>函数参数是对象会发生什么问题？</h2><p>在向参数传递基本类型的值时，被传递的值会被复制给一个局部变量（即命名参数，或者用 ECMAScript 的概念来说，就是 <code>arguments</code> 对象中的一个元素）。在向参数传递引用类型的值时，会把这个值在内存中的地址复制给一个局部变量，因此这个局部变量的变化会反映在函数的外部。当在函数内部重写引用类型的值时，这个变量引用的就是一个局部对象了。而这个局部对象会在函数执行完毕之后立即被销毁。</p><h2 id="typeof-是否能正确判断类型？"><a href="#typeof-是否能正确判断类型？" class="headerlink" title="typeof 是否能正确判断类型？"></a>typeof 是否能正确判断类型？</h2><p><code>typeof</code> 对于原始类型来说，除了 <code>null</code> 都可以显示正确的类型</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">typeof</span> <span class="number">1</span> <span class="comment">// &#x27;number&#x27;</span></span><br><span class="line"><span class="keyword">typeof</span> <span class="string">&#x27;1&#x27;</span> <span class="comment">// &#x27;string&#x27;</span></span><br><span class="line"><span class="keyword">typeof</span> <span class="literal">true</span> <span class="comment">// &#x27;boolean&#x27;</span></span><br><span class="line"><span class="keyword">typeof</span> <span class="literal">undefined</span> <span class="comment">// &#x27;undefined&#x27;</span></span><br><span class="line"><span class="keyword">typeof</span> <span class="title class_">Symbol</span>() <span class="comment">// &#x27;symbol&#x27;</span></span><br><span class="line"><span class="keyword">typeof</span> <span class="literal">null</span> <span class="comment">// &#x27;object&#x27;</span></span><br></pre></td></tr></table></figure><p><code>typeof</code> 对于对象来说，除了函数都会显示 <code>object</code>，所以说 <code>typeof</code> 并不能准确判断变量到底是什么类型</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">typeof</span> [] <span class="comment">// &#x27;object&#x27;</span></span><br><span class="line"><span class="keyword">typeof</span> &#123;&#125; <span class="comment">// &#x27;object&#x27;</span></span><br><span class="line"><span class="keyword">typeof</span> <span class="variable language_">console</span>.<span class="property">log</span> <span class="comment">// &#x27;function&#x27;</span></span><br></pre></td></tr></table></figure><h2 id="instanceof-能正确判断对象的原理是什么？"><a href="#instanceof-能正确判断对象的原理是什么？" class="headerlink" title="instanceof 能正确判断对象的原理是什么？"></a>instanceof 能正确判断对象的原理是什么？</h2><p>如果我们想判断一个对象的正确类型，这时候可以考虑使用 <code>instanceof</code>，因为内部机制是通过原型链来判断的。</p><h2 id="类型转换"><a href="#类型转换" class="headerlink" title="类型转换"></a>类型转换</h2><table><thead><tr><th>值</th><th>转字符串</th><th>转数字</th><th>转布尔值</th><th>转对象</th></tr></thead><tbody><tr><td><code>undefined</code></td><td><code>”undefined“</code></td><td><code>NaN</code></td><td><code>false</code></td><td>throws TypeError</td></tr><tr><td><code>null</code></td><td><code>&quot;null&quot;</code></td><td><code>0</code></td><td><code>false</code></td><td>throws TypeError</td></tr><tr><td><code>true</code></td><td><code>&quot;true&quot;</code></td><td><code>1</code></td><td></td><td>new Boolean(true)</td></tr><tr><td><code>false</code></td><td><code>&quot;false&quot;</code></td><td><code>0</code></td><td></td><td>new Boolean(false)</td></tr><tr><td><code>&quot;&quot;</code>（空字符串）</td><td></td><td><code>0</code></td><td><code>false</code></td><td>new String(“”)</td></tr><tr><td><code>&quot;1.2&quot;</code>（非空数字字符串）</td><td></td><td><code>1.2</code></td><td><code>true</code></td><td>new String(“1.2”)</td></tr><tr><td><code>&quot;one&quot;</code>（非空非数字字符串）</td><td></td><td><code>NaN</code></td><td><code>true</code></td><td>new String(“one”)</td></tr><tr><td><code>0</code></td><td><code>&quot;0&quot;</code></td><td></td><td><code>false</code></td><td>new Number(0)</td></tr><tr><td><code>-0</code></td><td><code>&quot;-0&quot;</code></td><td></td><td><code>false</code></td><td>new Number(-0)</td></tr><tr><td><code>NaN</code></td><td><code>&quot;NaN&quot;</code></td><td></td><td><code>false</code></td><td>new Number(NaN)</td></tr><tr><td><code>Infinity</code></td><td><code>&quot;Infinity&quot;</code></td><td></td><td><code>true</code></td><td>new Number(Infinity)</td></tr><tr><td><code>-Infinity</code></td><td><code>&quot;-Infinity&quot;</code></td><td></td><td><code>true</code></td><td>new Number(-Infinity)</td></tr><tr><td><code>1</code>（非零无穷大）</td><td><code>&quot;1&quot;</code></td><td></td><td><code>true</code></td><td>new Number(1)</td></tr><tr><td><code>Symbol(&quot;name&quot;)</code></td><td>throws TypeError</td><td>throws TypeError</td><td><code>true</code></td><td></td></tr><tr><td><code>&#123;&#125;</code>（任意对象）</td><td><code>&quot;[object Object]&quot;</code></td><td><code>NaN</code></td><td><code>true</code></td><td></td></tr><tr><td><code>[]</code>（任意数组）</td><td><code>&quot;&quot;</code></td><td><code>0</code></td><td><code>true</code></td><td></td></tr><tr><td><code>[9]</code>（1个数字元素）</td><td><code>&quot;9&quot;</code></td><td><code>9</code></td><td><code>true</code></td><td></td></tr><tr><td><code>[&#39;a&#39;]</code>（其他数组）</td><td>使用<code>join</code> 方法</td><td><code>NaN</code></td><td><code>true</code></td><td></td></tr><tr><td><code>function() &#123;&#125;</code>（任意函数）</td><td>参考<strong>对象转基本类型</strong></td><td><code>NaN</code></td><td><code>true</code></td><td></td></tr></tbody></table><h3 id="转换为Boolean"><a href="#转换为Boolean" class="headerlink" title="转换为Boolean"></a>转换为Boolean</h3><p>除了 <code>0</code>、<code>-0</code>、<code>NaN</code>、<code>&#39;&#39;</code>、<code>false</code>、<code>undefined</code>、<code>null</code>，其他所有值都转为 <code>true</code>,包括对象</p><h3 id="对象转基本类型"><a href="#对象转基本类型" class="headerlink" title="对象转基本类型"></a>对象转基本类型</h3><p>对象在转换类型的时候，会调用内置的 <code>[[ToPrimitive]]</code> 对象，对于该函数来说，算法逻辑一般来说如下：</p><ul><li>如果已经是基本类型了，那就不需要转换了</li><li>如果需要转字符串类型就调用<code>x.toString()</code>，转换为基本类型的话就返回转换的值。不是字符串类型的话就先调用 <code>x.valueOf()</code>，如果转换为基本类型，就返回转换的值</li><li>如果都没有返回原始类型，就会报错</li></ul><p>当然你也可以重写 <code>Symbol.toPrimitive</code> ，该方法在转原始类型时调用优先级最高。</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> a = &#123;</span><br><span class="line">  <span class="title function_">valueOf</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span></span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="title function_">toString</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="string">&#x27;1&#x27;</span></span><br><span class="line">  &#125;,</span><br><span class="line">  [<span class="title class_">Symbol</span>.<span class="property">toPrimitive</span>]() &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="number">2</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"><span class="number">1</span> + a <span class="comment">// =&gt; 3</span></span><br></pre></td></tr></table></figure><h2 id="加法运算符"><a href="#加法运算符" class="headerlink" title="加法运算符"></a>加法运算符</h2><p>加法运算符比较特别，它有以下几个特点：</p><ul><li>运算中其中一方为字符串，那么就会把另一方也转换为字符串</li><li>如果一方不是字符串或者数字，那么会将它转换为数字或者字符串</li></ul><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="literal">undefined</span> + <span class="string">&#x27;&#x27;</span> <span class="comment">// -&gt; &quot;undefined&quot;</span></span><br><span class="line"><span class="literal">null</span> + <span class="string">&#x27;&#x27;</span> <span class="comment">// -&gt; &quot;null&quot;</span></span><br><span class="line"><span class="literal">true</span> + <span class="string">&#x27;&#x27;</span> <span class="comment">// -&gt; &quot;true&quot;</span></span><br><span class="line"><span class="literal">false</span> + <span class="string">&#x27;&#x27;</span> <span class="comment">// -&gt; &quot;false&quot;</span></span><br><span class="line"><span class="number">1</span> + <span class="string">&#x27;&#x27;</span> <span class="comment">// -&gt; &quot;1&quot;</span></span><br><span class="line"><span class="title class_">NaN</span> + <span class="string">&#x27;&#x27;</span></span><br><span class="line">(&#123;&#125;) +<span class="string">&#x27;&#x27;</span> <span class="comment">// -&gt; &quot;[object Object]&quot;</span></span><br><span class="line">&#123;&#125; + <span class="string">&#x27;&#x27;</span> <span class="comment">// -&gt; 0</span></span><br><span class="line">[] + <span class="string">&#x27;&#x27;</span> <span class="comment">// -&gt; &quot;&quot;</span></span><br><span class="line"><span class="literal">undefined</span> + <span class="number">1</span> <span class="comment">// -&gt; NaN</span></span><br><span class="line"><span class="literal">null</span> + <span class="number">1</span> <span class="comment">// -&gt; 1</span></span><br><span class="line"><span class="literal">true</span> + <span class="number">1</span> <span class="comment">// -&gt; 2</span></span><br><span class="line"><span class="literal">false</span> + <span class="number">1</span> <span class="comment">// -&gt; 1</span></span><br><span class="line">(&#123;&#125;) + <span class="number">2</span> <span class="comment">// -&gt; &quot;[object Object]2&quot;</span></span><br><span class="line">&#123;&#125; + <span class="number">2</span> <span class="comment">// -&gt; 2</span></span><br><span class="line">[] + <span class="number">1</span> <span class="comment">// -&gt; &quot;1&quot;</span></span><br><span class="line">+ <span class="string">&#x27;&#x27;</span> <span class="comment">// -&gt; 0</span></span><br><span class="line">+ <span class="string">&#x27;b&#x27;</span> <span class="comment">// -&gt; NaN</span></span><br><span class="line">+ &#123;&#125; <span class="comment">// -&gt; NaN</span></span><br><span class="line">+ [] <span class="comment">// -&gt; 0</span></span><br><span class="line"><span class="string">&#x27;a&#x27;</span> + + <span class="string">&#x27;b&#x27;</span> <span class="comment">// -&gt; &quot;aNaN&quot;</span></span><br></pre></td></tr></table></figure><h2 id="比较运算符"><a href="#比较运算符" class="headerlink" title="比较运算符"></a>比较运算符</h2><ol><li>如果是对象，就通过<code>[[toPrimitive]]</code> 转换对象</li><li>如果是字符串，就通过 <code>unicode</code> 字符串索引来比较</li></ol><h2 id="this对象"><a href="#this对象" class="headerlink" title="this对象"></a>this对象</h2><p><code>this</code> 对象是在运行时基于函数的执行环境绑定的：</p><ul><li>在全局函数中，<code>this</code> 等同于 <code>window</code></li><li>当函数被作为某个对象的方法调用时，<code>this</code> 等于那个对象</li><li>匿名函数的执行环境具有全局性，因此其 <code>this</code>对象通常指向 <code>window</code></li><li><code>new</code>：<code>this</code> 被固化到实例上，不会被任何方式改变</li><li><code>bind</code>、<code>apply</code>、<code>call</code>：<code>this</code> 对象是第一个参数</li><li>箭头函数其实是没有 <code>this</code> 的，箭头函数中的 <code>this</code> 只取决包裹箭头函数的第一个普通函数的 <code>this</code></li></ul><h2 id="和-有什么区别"><a href="#和-有什么区别" class="headerlink" title="&#x3D;&#x3D; 和 &#x3D;&#x3D;&#x3D; 有什么区别"></a>&#x3D;&#x3D; 和 &#x3D;&#x3D;&#x3D; 有什么区别</h2><p>对于 <code>==</code> 来说，如果对比双方的类型不一样的话，就会进行隐式类型转化。假如我们需要对比 <code>x</code> 和 <code>y</code> 是否相同，就会进行如下判断流程：</p><ol><li>首先会判断两者类型是否相同。相同的话就是比大小了</li><li>类型不同的话，那么就会进行类型转换</li><li>会先判断是否在对比 <code>null</code> 和 <code>undefined</code>，是的话就会返回 <code>true</code></li><li>判断两者类型是否为 <code>string</code> 和 <code>number</code>，是的话就会将字符串转为 <code>number</code></li><li>判断其中一方是否为 <code>boolean</code>，是的话就会把 <code>boolean</code> 转为 <code>number</code> 再进行判断</li><li>判断其中一方是否为 <code>object</code> 且另一方为 <code>string</code>、<code>number</code> 或者 <code>symbol</code>，是的话就会把 <code>object</code> 转为原始类型再进行判断</li></ol><blockquote><p><code>[] == ![紫升]</code> &#x3D;&gt; <code>true</code></p></blockquote><p><img src="https://i.loli.net/2020/04/30/inuaIUJvYQP5BjD.png" alt="紫升"></p><h2 id="闭包（Closure）"><a href="#闭包（Closure）" class="headerlink" title="闭包（Closure）"></a>闭包（Closure）</h2><h3 id="什么是闭包"><a href="#什么是闭包" class="headerlink" title="什么是闭包"></a>什么是闭包</h3><p><strong>函数A</strong>里面包含了<strong>函数B</strong>，而<strong>函数B</strong>里面使用了<strong>函数A</strong>的变量，那么<strong>函数B</strong>被称为闭包函数。在JS中，闭包的意义是能让你从内部函数访问外部函数作用域。</p><p>在计算机科学中，闭包（Closure），又称词法闭包（Lexical Closure）或函数闭包（Function Closure），是在支持头等函数的编程语言中实现词法绑定的一种技术。闭包实际上是一个结构体，它存储了一个函数和一个关联环境。环境里是若干对符号和值的对应关系，它既要包括约束变量（该函数内部的变量），也要包括自由变量（在函数外部定义但在函数内被引用）。闭包跟函数最大的不同在于，当捕捉闭包的时候，它的自由变量会在捕捉时被确定，这样即便脱离了捕捉时的上下文，它也能照常运行。</p><h3 id="闭包的副作用"><a href="#闭包的副作用" class="headerlink" title="闭包的副作用"></a>闭包的副作用</h3><ul><li>由于闭包会携带包含它的函数的作用域，因此会比其他函数占用更多的内存。</li><li>闭包只能取得包含函数中任何变量的最后一个值。闭包保存的是整个变量对象，而不是某个特殊的变量。</li><li>在IE9之前的浏览器中，如果闭包的作用域链中保存着HTML元素，那么就意味着该元素将无法被销毁。</li></ul><h3 id="循环中使用闭包解决-var-定义函数的问题"><a href="#循环中使用闭包解决-var-定义函数的问题" class="headerlink" title="循环中使用闭包解决 var 定义函数的问题"></a>循环中使用闭包解决 <code>var</code> 定义函数的问题</h3><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">for</span>(<span class="keyword">var</span> i = <span class="number">1</span>; i &lt;=<span class="number">5</span>: i++) &#123;</span><br><span class="line">  <span class="built_in">setTimeout</span>(<span class="keyword">function</span> <span class="title function_">timer</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">log</span>(i)</span><br><span class="line">  &#125;, i * <span class="number">1000</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>首先因为 <code>setTimeout</code> 是个异步函数，所以会把循环全部执行完毕，这时候 <code>i</code> 就是 6 了，所以会输出 5 个 6。</p><h4 id="使用闭包解决"><a href="#使用闭包解决" class="headerlink" title="使用闭包解决"></a>使用闭包解决</h4><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">for</span>(<span class="keyword">var</span> i = <span class="number">1</span>; i &lt;= <span class="number">5</span>; i++) &#123;</span><br><span class="line">  ;(<span class="keyword">function</span>(<span class="params">j</span>) &#123;</span><br><span class="line">    <span class="built_in">setTimeout</span>(<span class="keyword">function</span> <span class="title function_">timer</span>(<span class="params"></span>) &#123;</span><br><span class="line">      <span class="variable language_">console</span>.<span class="title function_">log</span>(j)</span><br><span class="line">    &#125;, j * <span class="number">1000</span>)</span><br><span class="line">  &#125;)(i)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在上述代码中，我们首先使用了立即执行函数将 <code>i</code> 传入函数内部，这个时候值就被固定在了参数 <code>j</code> 上面不会改变，当下次执行 <code>timer</code> 这个闭包的时候，就可以使用外部函数的变量 <code>j</code>，从而达到目的。</p><h4 id="使用-setTimeout-的第三个参数"><a href="#使用-setTimeout-的第三个参数" class="headerlink" title="使用 setTimeout 的第三个参数"></a>使用 setTimeout 的第三个参数</h4><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">for</span> (<span class="keyword">var</span> i = <span class="number">1</span>; i &lt;= <span class="number">5</span>; i++) &#123;</span><br><span class="line">  <span class="built_in">setTimeout</span>(</span><br><span class="line">    <span class="keyword">function</span> <span class="title function_">timer</span>(<span class="params">j</span>) &#123;</span><br><span class="line">      <span class="variable language_">console</span>.<span class="title function_">log</span>(j)</span><br><span class="line">    &#125;,</span><br><span class="line">    i * <span class="number">1000</span>,</span><br><span class="line">    i</span><br><span class="line">  )</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>第三个参数会被当成 <code>timer</code> 函数的参数传入。</p><h4 id="使用let定义i"><a href="#使用let定义i" class="headerlink" title="使用let定义i"></a>使用let定义i</h4><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">for</span> (<span class="keyword">let</span> i = <span class="number">1</span>; i &lt;= <span class="number">5</span>; i++) &#123;</span><br><span class="line">  <span class="built_in">setTimeout</span>(<span class="keyword">function</span> <span class="title function_">timer</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">log</span>(i)</span><br><span class="line">  &#125;, i * <span class="number">1000</span>)</span><br><span class="line">&#125;</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>浅复制只复制一层对象的属性，也就是说拷贝原对象的实例，但是对其内部的引用类型值，拷贝的是其引用。实现浅拷贝：</p><ul><li><code>Object.assign(&#123;&#125;, ...)</code></li><li><code>...</code> 扩展操作符</li><li>lodash <code>_.clone</code></li></ul><h3 id="深拷贝"><a href="#深拷贝" class="headerlink" title="深拷贝"></a>深拷贝</h3><p>深拷贝是对对象以及对象的所有子对象进行拷贝。我们可以使用 lodash 的 <code>cloneDeep</code> 方法，但是深拷贝大数据的时候，由于内部实现是递归，所以最好换实现思路。</p><p><strong><code>JSON.parse(JSON.stringify(object))</code> 深拷贝的局限：</strong></p><ol><li>会忽略 <code>undefined</code>、<code>symbol</code>、<code>function</code></li><li>如果被拷贝的对象中有正则表达式，则拷贝之后的正则表达式会变成 <code>Object</code></li><li>不能解决循环引用的对象</li></ol><h2 id="原型、原型链"><a href="#原型、原型链" class="headerlink" title="原型、原型链"></a>原型、原型链</h2><h3 id="原型"><a href="#原型" class="headerlink" title="原型"></a>原型</h3><p>每个 JS 对象都有 <code>__proto__</code> 属性，这个属性指向了原型。这个属性在现在来说已经不推荐直接去使用它了，这只是浏览器在早期为了让我们访问到内部属性 <code>[[prototype]]</code> 来实现的一个东西。原型也是一个对象，并且这个对象包含了很多函数。原型对象通过 <code>constructor</code> 指向了构造函数，构造函数又通过 <code>prototype</code> 指向了原型。</p><h3 id="原型链"><a href="#原型链" class="headerlink" title="原型链"></a>原型链</h3><p>原型链就是多个对象通过 <code>__proto__</code> 的方式连接了起来。</p><h2 id="执行环境及作用域链"><a href="#执行环境及作用域链" class="headerlink" title="执行环境及作用域链"></a>执行环境及作用域链</h2><h3 id="执行环境（execution-context）-作用域"><a href="#执行环境（execution-context）-作用域" class="headerlink" title="执行环境（execution context）&#x2F;作用域"></a>执行环境（execution context）&#x2F;作用域</h3><p>执行环境也称作用域，定义了变量或函数有权访问的其他数据，决定了它们各自的行为。每个执行环境都有一个与之关联的变量对象（variable object），执行环境中定义的所有变量和函数都保存在这个对象中。虽然我们编写的代码无法访问这个对象，但解析器在处理数据时会在后台使用它。</p><p>在 JS 中，执行环境分为全局执行环境和函数执行环境：</p><ul><li>全局执行环境：最外围的执行环境，根据 ECMAScript 实现所在的宿主环境不同，表示执行环境的对象也不一样。某个执行环境中的所有代码执行完毕后，该环境被销毁，保存在其中的所有变量和函数定义也随之销毁。全局执行环境直到应用程序退出（例如关闭网页或浏览器）时才会被销毁</li><li>函数执行环境：每个函数都有自己的执行环境。当执行流进入一个函数时，函数的环境就会被推入一个环境栈中。而在函数执行之后，栈将其环境弹出，把控制权返回给之前的执行环境。</li></ul><h3 id="作用域链"><a href="#作用域链" class="headerlink" title="作用域链"></a>作用域链</h3><p>当代码在一个环境中执行时，会创建变量对象的一个作用域链（scope chain）。作用域链的用途，是保证对执行环境有权访问的所有变量和函数的有序访问。作用域链的前端，始终都是当前执行的代码所在环境的变量对象。如果这个环境是函数，则将其活动对象（activation object）作为变量对象。活动对象在最开始只包含一个变量，即 arguments 对象（这个对象在全局执行环境中是不存在的）。作用域的下一个变量对象来自包含（外部）环境，而再下一个变量对象则来自下一个包含环境。这样，一直延续到全局执行环境；全局执行环境的变量对象始终都是作用域链中的最后一个对象。</p><p>标识符解析是沿着作用域链一级一级地搜索标识符的过程。搜索过程从作用域的前端开始，然后逐级地向后回溯，直到找到标识符为止（如果找不到标识符，通常会导致错误发生）。</p><h2 id="var、let-和-const-的区别"><a href="#var、let-和-const-的区别" class="headerlink" title="var、let 和 const 的区别"></a>var、let 和 const 的区别</h2><h3 id="声明提升（hoisting）"><a href="#声明提升（hoisting）" class="headerlink" title="声明提升（hoisting）"></a>声明提升（hoisting）</h3><ul><li>函数声明提升：解析器会率先读取函数声明，并使其在任何代码之前可用（可以访问）</li><li>变量声明提升：和函数声明一样，但是赋值操作是在代码执行时才进行的，所以函数表达式不能被提升</li><li>函数提升优先于变量提升，函数提升会把整个函数挪到作用域顶部，变量提升只会把声明挪到作用域顶部</li></ul><h3 id="暂时性死区（temporal-dead-zone）"><a href="#暂时性死区（temporal-dead-zone）" class="headerlink" title="暂时性死区（temporal dead zone）"></a>暂时性死区（temporal dead zone）</h3><p>在解析代码时，JavaScript 引擎也会注意出现在块后面的 <code>let</code> 声明，只不过在此之前不能以任何方式来引用未声明的变量。在 let 声明之前的执行瞬间被称为“暂时性死区”（temporal dead zone），在此阶段引用任何后面才声明的变量都会抛出 ReferenceError。</p><h3 id="let、const与-var-的区别"><a href="#let、const与-var-的区别" class="headerlink" title="let、const与 var 的区别"></a>let、const与 var 的区别</h3><ul><li>块级作用域：<code>let</code> 和 <code>const</code> 支持块级作用域</li><li>暂时性死区：<code>let</code> 和 <code>const</code> 因为存在暂时性死区所以不存在变量提升</li><li>声明冗余：使用 <code>let</code> 和 <code>const</code> 不能进行重复声明，且与 <code>var</code> 混用时也不允许声明冗余。</li><li>在全局作用域下使用 <code>let</code> 和 <code>const</code> 声明变量，变量并不会被挂在到 <code>window</code> 上。</li></ul><h3 id="let-和-const-的区别"><a href="#let-和-const-的区别" class="headerlink" title="let 和 const 的区别"></a>let 和 const 的区别</h3><p>使用 const 声明变量时必须同时初始化变量，且尝试修改 const 声明的变量会导致运行时错误。但是 const 声明的限制只适用于它指向的变量的引用。换句话说，如果 const 变量引用的是一个对象，那么修改这个对象内部的属性并不违反 const 的限制。</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> person = &#123;&#125;;</span><br><span class="line">person.<span class="property">name</span> = <span class="string">&#x27;Matt&#x27;</span>;</span><br></pre></td></tr></table></figure><h2 id="事件代理"><a href="#事件代理" class="headerlink" title="事件代理"></a>事件代理</h2><p>事件代理（Event Delegation）,又称之为事件委托。是JavaScript中绑定事件的常用技巧。顾名思义，“事件代理”即是把原本需要绑定的事件委托给父元素，让父元素担当事件监听的职务。事件代理的原理是DOM元素的事件冒泡。使用事件代理的好处是可以提高性能。</p><h2 id="标识符"><a href="#标识符" class="headerlink" title="标识符"></a>标识符</h2><p>所谓标识符，就是指变量、函数、属性的名字、或者函数的参数。</p><h2 id="变量"><a href="#变量" class="headerlink" title="变量"></a>变量</h2><p>ECMAScript 变量时松散类型的，所谓松散类型就是可以用来保存任何类型的数据。换句话说，每个变量仅仅是一个用于保存值的占位符而已。</p><blockquote><p>变量只是指向保存值的栈内存地址的指针而已</p></blockquote><h2 id="数据类型"><a href="#数据类型" class="headerlink" title="数据类型"></a>数据类型</h2><p>ECMAScript 中有 5 中简单的数据类型（也称为基本数据类型）: <code>Undefined</code>、<code>Null</code>、<code>Boolean</code>、<code>Number</code> 和 <code>String</code>。还有 1 中复杂数据类型（也称为引用数据类型）—— <code>Object</code>，<code>Object</code> 本质上是由一组无序名值对组成的。</p><h2 id="对象"><a href="#对象" class="headerlink" title="对象"></a>对象</h2><p>ECMAScript 对象其实就是一组数据和功能的集合。对象可以通过执行 <code>new</code> 操作符跟要创建的对象类型的名称来创建。</p><h2 id="操作符"><a href="#操作符" class="headerlink" title="操作符"></a>操作符</h2><p>ECMA-262 描述了一组用于操作数据值的操作符，包括算术操作符、位操作符、关系操作符和相等操作符。ECMAScript 操作符的与众不同之处在于，它们能够适用于很多值，例如字符串、数字值、布尔值，甚至对象。不过在应用于对象时，相应地操作符通常都会调用对象的 <code>valueOf()</code> 和(或)</p><h2 id="函数"><a href="#函数" class="headerlink" title="函数"></a>函数</h2><p>函数对任何语言来说都是一个核心概念。通过函数可以封装任意多条语句，而且可以再任何地方、任何时候调用执行。ECMAScript 中的函数使用 <code>function</code> 关键字来声明，后跟一组参数以及函数体。</p><h2 id="传递参数"><a href="#传递参数" class="headerlink" title="传递参数"></a>传递参数</h2><p>ECMAScript 中所有的函数的参数都是按值传递的。也就是说，把函数外部的值复制给函数内部的参数，就和把值从一个变量复制给另一个变量一样。<strong>基本类型值</strong>的传递如同基本类型变量的复制一样，而<strong>引用类型值</strong>的传递则如同引用类型变量的复制一样。</p><p>在向参数传递基本类型的值时，被传递的值会被复制给一个局部变量（即命名参数，或者用 ECMAScript 的概念来说，就是 arguments 对象中的一个元素）。在向参数传递引用类型的值时，会把这个值在内存地址中的地址复制给一个局部变量，因此这个局部变量的变化会反应在函数的外部。</p><h2 id="执行环境及作用域"><a href="#执行环境及作用域" class="headerlink" title="执行环境及作用域"></a>执行环境及作用域</h2><p>执行环境（execution context，为简单起见，有时也称为”环境“）是 JavaScript 中最为重要的一个概念。执行环境定义了变量和函数有权访问的其他数据，决定了它们各自的行为。每个执行环境都有一个与之关联的<strong>变量对象</strong>（variable object）。环境中定义的所有的所有变量和函数都保存在这个对象中。虽然我们编写的代码无法访问这个对象，但解析器在处理数据时会在后台使用它。</p><p>全局执行环境是最外围的一个执行环境。根据 ECMAScript 实现所在的宿主环境不同，表示执行环境的对象也不一样。在 Web 浏览器中,全局执行环境被认为是 <code>window</code> 对象，因此所有全局变量和函数都是作为 <code>window</code> 对象的属性和方法创建的。某个执行环境中的所有代码执行完毕后，该环境被销毁，保存在其中的所有变量和函数定义也随之销毁（全局执行环境直到应用程序退出——例如关闭网页或浏览器——时才会被销毁）。</p><p>每个函数都有自己的执行环境。当执行流进入一个函数时，函数的环境就会被推入一个环境栈中。而在函数执行之后，栈将其环境弹出，把控制权返回给之前的执行环境。ECMAScript 程序中的执行流正是由这个方便的机制控制着。</p><p>当代码在一个执行环境中执行时，会创建变量对象的一个作用域链。作用域链的用途，是保证对执行环境有权访问的所有变量和函数的有序访问。作用域链的前端，始终都是当前执行的代码所在环境的变量对象。如果这个环境是函数，则将其活动对象作为变量对象。活动对象在最开始时只包含一个变量，即 <code>arguments</code> 对象（这个对象在全局环境中是不存在的）。作用域链中的下一个变量对象来自包含（外部）环境，而下一个变量对象则来自下一个包含环境。这样，一直延续到全局执行环境；全局执行环境的变量对象始终都是作用域链中的最后一个对象。</p><p>标识符解析是沿着作用域链一级一级地搜索标识符的过程。搜索过程始终从作用域链的前端开始，然后逐级地向后回溯，直至找到标识符为止（如果找不到标识符，通常会导致错误发生）。</p><h2 id="查询标识符"><a href="#查询标识符" class="headerlink" title="查询标识符"></a>查询标识符</h2><p>当在某个环境中为了读取或写入而引入一个标识符时，必须通过搜索来确定该标识符实际代表什么。搜索过程从作用域的前端开始，向上逐级查询与给定名字匹配的标识符。如果在局部环境中找到了该标识符，搜索过程停止，变量就绪。如果在局部环境中没有找到该变量名，则继续沿作用域链向上搜索。搜索过程将一直追溯到全局环境的变量对象。如果在全局环境中也没找到这个标识符，则意味着该变量尚未声明。</p><h2 id="垃圾收集"><a href="#垃圾收集" class="headerlink" title="垃圾收集"></a>垃圾收集</h2><p>JavaScript 具有自动垃圾收集机制，也就是说，执行环境会负责管理代码执行过程中使用的内存。而在 C 和 C++ 之类的语言中，开发人员的一项基本任务就是手工跟踪内存的使用情况，这是造成许多问题的一个根源。在编写 JavaScript 程序时。开发人员不再关心内存使用问题，所需内存的分配以及无用内存的回收完全实现了自动管理。这种垃圾收集机制的原理其实很简单：找出那些不再使用的变量，然后释放其占用的内存。为此，垃圾收集器会按照固定的时间间隔（或代码执行中预定的收集时间），周期性地执行这一操作。</p><h2 id="引用类型"><a href="#引用类型" class="headerlink" title="引用类型"></a>引用类型</h2><p>引用类型的值（对象）是引用类型的一个实例。在 ECMAScript 中，引用类型是一种数据结构，用于将数据和功能组织在一起。引用类型有时也被称为对象定义，因为它们描述的是一类对象所具有的属性和方法。</p><blockquote><p>虽然引用类型与类看起来相似，但它们并不是相同的概念。</p></blockquote><p>对象是某个特定引用类型的实例。新对象是使用 <code>new</code> 操作符后跟一个构造函数来创建。构造函数本身就是一个函数，只不过这个函数是处于创建新对象的目的而定义的。请看下面这行代码：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> person = <span class="keyword">new</span> <span class="title class_">Object</span>();</span><br></pre></td></tr></table></figure><p>这行代码创建了 <code>Object</code> 引用类型的一个新实例，然后把该实例保存在了变量 <code>person</code> 中。使用的构造函数是 <code>Object</code>，它只为新对象定义了默认的属性和方法。 ECMAScript 提供了很多原生引用类型（例如 <code>Object</code>），以便开发人员用以实现常见的计算任务。</p><h3 id="Object-类型"><a href="#Object-类型" class="headerlink" title="Object 类型"></a>Object 类型</h3><p>到目前为止，我们看到的大多数引用类型值都是 <code>Object</code> 类型的实例；而且，<code>Object</code> 也是 ECMAScript 中使用最多的一个类型。虽然 <code>Object</code> 的实例不具备多少功能，但对于在应用程序中存储和传输数据而言，它们确实是非常理想的选择。</p><h3 id="Array-类型"><a href="#Array-类型" class="headerlink" title="Array 类型"></a>Array 类型</h3><p>除了 <code>Object</code> 之外，<code>Array</code> 类型恐怕是 ECMAScript 中最常用的类型了。而且，ECMAScript 中的数组与其他多数语言中的数组有着相当大的区别。虽然 ECMAScript 数组与其他语言中的数组都是数据的有序集合，但与其他语言不同的是，ECMAScript 数组的每一项可以保存任何类型的数据。也就是说，可以用数组的第一个位置来保存字符串，用第二个位置保存数值，用第三个位置保存对象，以此类推。而且 ECMAScript 数组的大小是可以动态调整的，即可以随着数据的添加自动增长以容纳新增数据。</p><h3 id="Date-类型"><a href="#Date-类型" class="headerlink" title="Date 类型"></a>Date 类型</h3><blockquote><p>推荐使用 <a href="https://github.com/iamkun/dayjs">dayjs</a> 处理时间相关的操作。</p></blockquote><p>ECMAScript 中的 <code>Date</code> 类型是在早期 Java 中的 <code>java.util.Date</code> 类型基础上构建的。为此，<code>Date</code> 类型使用自 UTC 1970 年 1 月 1 日午夜（零时）开始经过的毫秒数来保存日期。在使用这种数据存储格式的条件下，<code>Date</code> 类型保存的日期能够精确到 1970 年 1 月 1 日之前或之后的 2851616 年。</p><p>日期字符串在不同浏览器有不同的实现，例如 <code>new Date(&#39;1970-1-1&#39;)</code> 在 IE、Safari、Android 上是 Invalid Date，而在 Chrome 上是能正常解析的。请务必使用 <code>new Date(&#39;1970/1/1&#39;)</code> 或 <code>new Date(1970, 1, 1)</code> 代替 <code>new Date(&#39;1970-1-1&#39;)</code></p><h3 id="RegExp-类型"><a href="#RegExp-类型" class="headerlink" title="RegExp 类型"></a>RegExp 类型</h3><blockquote><p>推荐在线课程 <a href="https://www.imooc.com/learn/706">JavaScript 正则表达式</a></p></blockquote><p>ECMAScript 通过 RegExp 类型来支持正则表达式。使用下面类似 Perl 的语法，就可以创建一个正则表达式。</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> expression = <span class="regexp">/ pattern /</span> flags</span><br></pre></td></tr></table></figure><p>其中的模式（pattern）部分可以是任何简单或复杂的正则表达式，可以包含字符类、限定符、分组、向前查找以及反向引用。每个正则表达式都可带有一个或多个标志（flags），用以标明正则表达式的行为。正则表达式的匹配模式支持下列 3 个标志：</p><ul><li><code>g</code>: 表示全局（global）模式，即模式将被应用于所有字符串，而非在发现第一个匹配项时立即停止；</li><li><code>i</code>: 表示不区分大小写（case-insensitive）模式，即在确定匹配项时忽略模式与字符串的大小写；</li><li><code>m</code>: 表示多行（multiline）模式，即在到达一行文本末尾时还会继续朝查找下一行中是否存在与模式匹配的项。</li></ul><h3 id="Function-类型"><a href="#Function-类型" class="headerlink" title="Function 类型"></a>Function 类型</h3><p>说起来 ECMAScript 中什么最有意思，我想莫过于函数了——而有意思的根源，则在于函数实际上是对象。每个函数都是 <code>Function</code> 类型的实例，而且都与其他引用类型一样具有属性和方法。由于函数是对象，因此函数名实际上也是一个指向函数对象的指针，不会与某个函数绑定。函数通常是使用函数声明语法定义的，如下面的例子所示：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">sum</span>(<span class="params">num1, num2</span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> num1 + num2;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这与下面使用函数表达式定义函数的方式几乎相差无几：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> sum = <span class="keyword">function</span>(<span class="params">num1, num2</span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> num1 + num2;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><h2 id="函数的属性和方法"><a href="#函数的属性和方法" class="headerlink" title="函数的属性和方法"></a>函数的属性和方法</h2><ul><li><code>length</code>: 表示函数希望接收的命名参数的个数；</li><li><code>prototype</code>: 在 ECMAScript 中，<code>prototype</code> 属性时不可枚举的，因此使用 <code>for-in</code> 无法发现</li><li><code>apply</code>: <code>apply</code> 方法接收两个参数，一个是在其中运行函数的作用域，另一个是参数数组。</li><li><code>call</code>: <code>call</code> 方法与 <code>apply</code> 方法的作用相同，它们的区别仅在于接收参数的方式不同。对于 <code>call()</code> 方法而言，第一个参数是 <code>this</code> 值没有变化，变化的是其余参数都直接传递给函数。</li><li><code>bind</code>: 这个方法会创建一个函数实例，其中 <code>this</code> 值会被绑定到传给 <code>bind()</code> 函数的值。</li></ul><h2 id="toString、toLocalString、valueOf"><a href="#toString、toLocalString、valueOf" class="headerlink" title="toString、toLocalString、valueOf"></a>toString、toLocalString、valueOf</h2><table><thead><tr><th></th><th>toString</th><th>toLocalString</th><th>valueOf</th></tr></thead><tbody><tr><td>Array</td><td>由数组中每个值的字符串形式拼接而成的一个以逗号分隔的字符串</td><td>同 toString</td><td>返回的还是数组</td></tr><tr><td>Date</td><td>返回带有时区信息的日期和时间</td><td>会按照与浏览器设置的地区相适应的格式返回日期和时间</td><td>返回日期的毫秒表示</td></tr><tr><td>RegExp</td><td>返回正则表达式的字面量</td><td>同 toString</td><td>返回正则表达式本身</td></tr><tr><td>Function</td><td>返回函数的代码</td><td>同 toString</td><td>同 toString</td></tr></tbody></table><h2 id="访问对象属性"><a href="#访问对象属性" class="headerlink" title="访问对象属性"></a>访问对象属性</h2><p>一般来说，访问对象属性时使用的都是点表示法，这也是很多面向对象语言中通用的语法。不过，在 JavaScript 中也可以使用方括号表示法来访问对象的属性。在使用方括号语法时，应该将要访问的属性以字符串的形式放在方括号中，如下面的例子所示：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> person = &#123; <span class="attr">name</span>: <span class="string">&quot;张三&quot;</span> &#125;;</span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(person.<span class="property">name</sp