<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>涅槃快乐是金</title>
  
  
  <link href="https://www.nirvana.net.cn/atom.xml" rel="self"/>
  
  <link href="https://www.nirvana.net.cn/"/>
  <updated>2025-02-19T01:25:41.832Z</updated>
  <id>https://www.nirvana.net.cn/</id>
  
  <author>
    <name>nirvana</name>
    
  </author>
  
  <generator uri="https://hexo.io/">Hexo</generator>
  
  <entry>
    <title>使用 Playwright 和 GitHub Actions 自动化UI 测试</title>
    <link href="https://www.nirvana.net.cn/2025/0219BnYFoS555g.html"/>
    <id>https://www.nirvana.net.cn/2025/0219BnYFoS555g.html</id>
    <published>2025-02-19T08:28:03.000Z</published>
    <updated>2025-02-19T01:25:41.832Z</updated>
    
    <content type="html"><![CDATA[<p><img src="https://upload-images.jianshu.io/upload_images/10024246-5809585ddefcfd76.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240"></p><p><strong>视觉 UI 测试</strong>（也称为回归测试）是一种测试技术，用于验证您的更改是否对 UI 产生了意外的影响。通常，这类测试会对整个应用程序或特定元素进行图像快照，并将其与之前批准的基准图像进行比较。如果图像相同（在设定的像素容差范围内），则可以确定 web 应用程序的外观对用户来说没有变化。如果存在差异，则可能是 DOM 布局、字体、颜色或其他视觉属性发生了变化，需要进一步调查。</p><p>这篇文章将探讨如何使用 <strong>Playwright</strong> 和 <strong>GitHub Actions</strong> 自动化测试“现代”web 应用程序的视觉回归测试。目标是构建一个测试设置，在每个 Pull Request 上检查 UI 回归，并在需要时选择性地更新基准图像。</p><p>建议具备基本的 JavaScript（或 TypeScript）和 GitHub Actions 知识。</p><p>我们假设您会将此设置集成到现有的 web 应用程序中。如果您想从头开始尝试，建议使用 <strong>Vite</strong> 搭建一个新的 web 应用程序。</p><p>您可以在这个 <a href="#">GitHub 仓库</a> 中找到完整的示例应用程序。</p><p>以下是我们正在测试的小型 web 应用程序的外观：<br><img src="https://upload-images.jianshu.io/upload_images/10024246-d550083f540e4a90.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240"></p><h3 id="Playwright-设置"><a href="#Playwright-设置" class="headerlink" title="Playwright 设置"></a><strong>Playwright 设置</strong></h3><p>对于我们的测试设置，将使用 Playwright，这是一个端到端（E2E）测试框架。我喜欢 Playwright，因为它提供了优秀的开发体验和默认配置。不过，您也可以用类似工具（如 Cypress 或 Selenium）实现相同的结果。</p><h5 id="安装-Playwright"><a href="#安装-Playwright" class="headerlink" title="安装 Playwright"></a><strong>安装 Playwright</strong></h5><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">npm init playwright</span><br></pre></td></tr></table></figure><p>运行后会提示您进行一些选择（例如 JavaScript 或 TypeScript 等）。当被询问是否添加 GitHub Actions 工作流时，选择 <code>true</code> 以便于在 CI 上运行测试：</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">✔ Add a GitHub Actions workflow? (y/N) · <span class="literal">true</span></span><br></pre></td></tr></table></figure><p>安装完成后，Playwright 会生成一个示例测试文件（<code>./tests/example.spec.ts</code>）、一个演示文件（<code>./tests-examples/demo-todo-app.spec.ts</code>，可以忽略），以及配置文件（<code>./playwright.config.ts</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">✔ Success! Created a Playwright Test project at ~/your-project-dir</span><br></pre></td></tr></table></figure><p>在此目录中，您可以运行以下命令：</p><ul><li><code>npx playwright test</code><br>运行端到端测试。</li><li><code>npx playwright test --project=chromium</code><br>仅在桌面 Chrome 上运行测试。</li><li><code>npx playwright test example</code><br>运行特定文件中的测试。</li><li><code>npx playwright test --debug</code><br>在调试模式下运行测试。</li><li><code>npx playwright codegen</code><br>自动生成测试代码。</li></ul><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">npx playwright <span class="built_in">test</span></span><br></pre></td></tr></table></figure><p>并查看以下文件：</p><ul><li><code>./tests/example.spec.ts</code> - 示例端到端测试文件  </li><li><code>./tests-examples/demo-todo-app.spec.ts</code> - 示例 ToDo App 测试文件  </li><li><code>./playwright.config.ts</code> - Playwright 配置文件  </li></ul><p>访问 <a href="https://playwright.dev/docs/intro">Playwright 文档</a> 获取更多信息。✨</p><hr><h5 id="修改-Playwright-配置文件"><a href="#修改-Playwright-配置文件" class="headerlink" title="修改 Playwright 配置文件"></a><strong>修改 Playwright 配置文件</strong></h5><p>Playwright 在生成的配置文件（<code>./playwright.config.ts</code>）中提供了一些不错的默认设置，但可以做一些改进。</p><ol><li><p><strong>简化测试浏览器配置</strong><br>更新 <code>projects</code> 列表，仅在 Chromium 上运行测试：</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="attr">projects</span>: [</span><br><span class="line">  &#123;</span><br><span class="line">    <span class="attr">name</span>: <span class="string">&quot;chromium&quot;</span>,</span><br><span class="line">    <span class="attr">use</span>: &#123;</span><br><span class="line">      ...devices[<span class="string">&quot;Desktop Chrome&quot;</span>],</span><br><span class="line">    &#125;,</span><br><span class="line">  &#125;,</span><br><span class="line">]</span><br></pre></td></tr></table></figure><p>需要时，可以稍后再添加更多浏览器或设备支持。</p></li><li><p><strong>自动启动本地服务器</strong><br>配置 <code>webServer</code> 部分，以便 Playwright 在运行测试时自动启动应用程序：</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="attr">webServer</span>: &#123;</span><br><span class="line">  <span class="attr">command</span>: <span class="string">&#x27;npm run dev --port 8080&#x27;</span>,</span><br><span class="line">  <span class="attr">port</span>: <span class="number">8080</span>,</span><br><span class="line">  <span class="attr">reuseExistingServer</span>: <span class="literal">true</span>,</span><br><span class="line">&#125;,</span><br></pre></td></tr></table></figure></li></ol><hr><h5 id="生成初始快照"><a href="#生成初始快照" class="headerlink" title="生成初始快照"></a><strong>生成初始快照</strong></h5><p>Playwright 生成的示例测试文件（<code>./tests/example.spec.ts</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; test, expect &#125; <span class="keyword">from</span> <span class="string">&quot;@playwright/test&quot;</span>;</span><br><span class="line"></span><br><span class="line"><span class="title function_">test</span>(<span class="string">&quot;example test&quot;</span>, <span class="keyword">async</span> (&#123; page &#125;) =&gt; &#123;</span><br><span class="line">  <span class="keyword">await</span> page.<span class="title function_">goto</span>(<span class="string">&quot;/&quot;</span>); <span class="comment">// 此处的 baseURL 为 webServer 的 URL</span></span><br><span class="line">  <span class="keyword">await</span> <span class="title function_">expect</span>(page).<span class="title function_">toHaveScreenshot</span>();</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p>Playwright 支持通过 <code>await expect(page).toHaveScreenshot()</code> 生成并比较快照。首次运行时，会生成基准快照；后续运行将与基准进行比较。</p><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">npx playwright <span class="built_in">test</span></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">Error: example.spec.ts-snapshots/example-test-1-chromium-darwin.png is missing <span class="keyword">in</span> snapshots, writing actual.</span><br></pre></td></tr></table></figure><p>这是因为还没有基准图像，测试会不断生成快照，直到两次快照匹配，并将最后一次快照保存到文件系统中（例如：<code>tests/example.spec.ts-snapshots/example-test-1-chromium-darwin.png</code>）。</p><p><img src="https://upload-images.jianshu.io/upload_images/10024246-b4fc335f5c948aec.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240"></p><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">npx playwright <span class="built_in">test</span></span><br></pre></td></tr></table></figure><p>如果当前 UI 与之前生成的基准快照一致，测试将成功通过！</p><h5 id="本地更新快照"><a href="#本地更新快照" class="headerlink" title="本地更新快照"></a><strong>本地更新快照</strong></h5><p>让我们进入有趣的部分。</p><p>如果对 UI 进行了任何视觉更改并重新运行测试，测试会失败，Playwright 会显示“实际”与“预期”快照之间的清晰差异：<br><img src="https://upload-images.jianshu.io/upload_images/10024246-77dd3d9bb02d173a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240"></p><p>在这种情况下，如果我们希望页面的视觉变化是自愿的，就需要更新基准快照。可以使用 <code>--update-snapshots</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">npx playwright <span class="built_in">test</span> --update-snapshots</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">[chromium] › example.spec.ts:3:1 › example <span class="built_in">test</span>  </span><br><span class="line">tests/example.spec.ts-snapshots/example-test-1-chromium-darwin.png is re-generated, writing actual.</span><br></pre></td></tr></table></figure><p>现在，我们已经了解了如何运行视觉测试并在本地更新快照，接下来准备转向 CI 流程。</p><hr><h3 id="在-CI-上运行测试（使用-GitHub-Actions）"><a href="#在-CI-上运行测试（使用-GitHub-Actions）" class="headerlink" title="在 CI 上运行测试（使用 GitHub Actions）"></a><strong>在 CI 上运行测试（使用 GitHub Actions）</strong></h3><p><strong>GitHub Actions</strong> 是一个持续集成和持续交付（CI/CD）平台，可以自动化构建、测试和部署流水线。借助 GitHub Actions，可以为每次 Pull Request 创建和更新设置工作流，自动运行构建和测试。</p><p>一个良好的视觉回归测试设置起点是，在每次创建或更新 Pull Request 时运行测试。幸运的是，Playwright 已经在 <code>.github/workflows/playwright.yml</code> 中生成了一个方便的 GitHub Actions 工作流，专门用于此用例。</p><p>这个工作流可以开箱即用，但建议稍作调整：  </p><ol><li><strong>只安装目标测试所需的浏览器（<code>--with-deps chromium</code>）</strong>。  </li><li><strong>仅在测试失败时保存测试结果工件（<code>if: failure()</code>）</strong>，以避免存储不必要的文件。</li></ol><p>以下是修改后的 <code>playwright.yml</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><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="attr">name:</span> <span class="string">Playwright</span> <span class="string">Tests</span></span><br><span class="line"></span><br><span class="line"><span class="attr">on:</span></span><br><span class="line">  <span class="attr">push:</span></span><br><span class="line">    <span class="attr">branches:</span> [<span class="string">main</span>, <span class="string">master</span>]</span><br><span class="line">  <span class="attr">pull_request:</span></span><br><span class="line">    <span class="attr">branches:</span> [<span class="string">main</span>, <span class="string">master</span>]</span><br><span class="line"></span><br><span class="line"><span class="attr">jobs:</span></span><br><span class="line">  <span class="attr">test:</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">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="comment"># 检出代码并设置环境</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">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">&quot;14.x&quot;</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">npm</span> <span class="string">install</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Install</span> <span class="string">Playwright</span> <span class="string">Browsers</span></span><br><span class="line">        <span class="attr">run:</span> <span class="string">npx</span> <span class="string">playwright</span> <span class="string">install</span> <span class="string">--with-deps</span> <span class="string">chromium</span></span><br><span class="line">      </span><br><span class="line">      <span class="comment"># 运行 Playwright 测试</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Run</span> <span class="string">Playwright</span> <span class="string">tests</span></span><br><span class="line">        <span class="attr">run:</span> <span class="string">npx</span> <span class="string">playwright</span> <span class="string">test</span></span><br><span class="line">      </span><br><span class="line">      <span class="comment"># 上传测试结果工件</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">uses:</span> <span class="string">actions/upload-artifact@v2</span></span><br><span class="line">        <span class="attr">if:</span> <span class="string">failure()</span></span><br><span class="line">        <span class="attr">with:</span></span><br><span class="line">          <span class="attr">name:</span> <span class="string">playwright-report</span></span><br><span class="line">          <span class="attr">path:</span> <span class="string">playwright-report/</span></span><br><span class="line">          <span class="attr">retention-days:</span> <span class="number">30</span></span><br></pre></td></tr></table></figure><p>将此工作流提交到 GitHub 仓库后，每次提交新的 Pull Request 时，Playwright 测试将被触发。</p><hr><h3 id="注意：参考快照与-CI-环境的兼容性"><a href="#注意：参考快照与-CI-环境的兼容性" class="headerlink" title="注意：参考快照与 CI 环境的兼容性"></a><strong>注意：参考快照与 CI 环境的兼容性</strong></h3><p>测试在 CI 环境中运行时可能会失败，因为仓库中的基准快照可能是在与 CI 环境不同的机器上捕获的（除非这些快照是在运行 Ubuntu 的机器上生成的）。</p><p>要了解更多测试结果，可以检查 GitHub Action 的输出或查看测试工件。例如，如果在 macOS 上生成的基准快照会导致在 Linux 上的测试失败，错误可能会类似于：<br><img src="https://upload-images.jianshu.io/upload_images/10024246-202e9bf513d3e2f8.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240"></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">Error: tests/example.spec.ts-snapshots/example-test-1-chromium-linux.png is missing <span class="keyword">in</span> snapshots</span><br></pre></td></tr></table></figure><p>接下来，我们将学习如何为 CI 环境更新参考快照。</p><h5 id="在-CI-中通过-Pull-Request-评论更新快照"><a href="#在-CI-中通过-Pull-Request-评论更新快照" class="headerlink" title="在 CI 中通过 Pull Request 评论更新快照"></a><strong>在 CI 中通过 Pull Request 评论更新快照</strong></h5><p>使用 <code>--update-snapshots</code> 本地生成基准快照相对简单，而在 CI 中进行类似操作则更复杂，因为需要决定快照的存储位置、方式和时间。</p><p>有多种方法可以实现这个流程，为了简化，我们从简单的方式开始。<br>一种行之有效的模式是：通过 GitHub Action，当在 Pull Request 中发布包含 <code>/update-snapshots</code> 的特定评论时，触发工作流来更新基准快照。  </p><p>基本思路如下：<br>当我们提交一个预计会影响 UI 的 Pull Request 时，可以添加一条 <code>/update-snapshots</code> 评论，这会触发 CI 自动生成更新后的快照，并将它们提交到 Pull Request 的分支中。</p><hr><h3 id="工作流：-github-workflows-update-snapshots-yml"><a href="#工作流：-github-workflows-update-snapshots-yml" class="headerlink" title="工作流：.github/workflows/update-snapshots.yml"></a><strong>工作流：<code>.github/workflows/update-snapshots.yml</code></strong></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><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="attr">name:</span> <span class="string">Update</span> <span class="string">Snapshots</span></span><br><span class="line"></span><br><span class="line"><span class="attr">on:</span></span><br><span class="line">  <span class="attr">issue_comment:</span></span><br><span class="line">    <span class="attr">types:</span> [<span class="string">created</span>]</span><br><span class="line"></span><br><span class="line"><span class="attr">jobs:</span></span><br><span class="line">  <span class="attr">updatesnapshots:</span></span><br><span class="line">    <span class="attr">if:</span> <span class="string">$&#123;&#123;</span> <span class="string">github.event.issue.pull_request</span> <span class="string">&amp;&amp;</span> <span class="string">github.event.comment.body</span> <span class="string">==</span> <span class="string">&#x27;/update-snapshots&#x27;</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">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="comment"># 检出代码并加载所有提交 ID</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="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 class="attr">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><br><span class="line">      <span class="comment"># 获取 Pull Request 的分支名称和最新提交的 SHA</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Get</span> <span class="string">SHA</span> <span class="string">and</span> <span class="string">branch</span> <span class="string">name</span></span><br><span class="line">        <span class="attr">id:</span> <span class="string">get-branch-and-sha</span></span><br><span class="line">        <span class="attr">run:</span> <span class="string">|</span></span><br><span class="line"><span class="string">          sha_and_branch=$(\</span></span><br><span class="line"><span class="string">            curl \</span></span><br><span class="line"><span class="string">              -H &#x27;authorization: Bearer $&#123;&#123; secrets.GITHUB_TOKEN &#125;&#125;&#x27; \</span></span><br><span class="line"><span class="string">              https://api.github.com/repos/$&#123;&#123; github.repository &#125;&#125;/pulls/$&#123;&#123; github.event.issue.number &#125;&#125; \</span></span><br><span class="line"><span class="string">            | jq -r &#x27;.head.sha,&quot; &quot;,.head.ref&#x27;);</span></span><br><span class="line"><span class="string">          echo &quot;::set-output name=sha::$(echo $sha_and_branch | cut -d &quot; &quot; -f 1)&quot;;</span></span><br><span class="line"><span class="string">          echo &quot;::set-output name=branch::$(echo $sha_and_branch | cut -d &quot; &quot; -f 2)&quot;;</span></span><br><span class="line"><span class="string"></span></span><br><span class="line">      <span class="comment"># 检出评论对应的分支</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Fetch</span> <span class="string">Branch</span></span><br><span class="line">        <span class="attr">run:</span> <span class="string">git</span> <span class="string">fetch</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Checkout</span> <span class="string">Branch</span></span><br><span class="line">        <span class="attr">run:</span> <span class="string">git</span> <span class="string">checkout</span> <span class="string">$&#123;&#123;</span> <span class="string">steps.get-branch-and-sha.outputs.branch</span> <span class="string">&#125;&#125;</span></span><br><span class="line"></span><br><span class="line">      <span class="comment"># 设置测试环境</span></span><br><span class="line">      <span class="bullet">-</span> <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">&quot;14.x&quot;</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">npm</span> <span class="string">install</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Install</span> <span class="string">Playwright</span> <span class="string">browsers</span></span><br><span class="line">        <span class="attr">run:</span> <span class="string">npx</span> <span class="string">playwright</span> <span class="string">install</span> <span class="string">--with-deps</span> <span class="string">chromium</span></span><br><span class="line"></span><br><span class="line">      <span class="comment"># 根据当前 UI 更新快照</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Update</span> <span class="string">snapshots</span></span><br><span class="line">        <span class="attr">run:</span> <span class="string">npx</span> <span class="string">playwright</span> <span class="string">test</span> <span class="string">--update-snapshots</span> <span class="string">--reporter=list</span></span><br><span class="line"></span><br><span class="line">      <span class="comment"># 提交更新的快照到 Pull Request 分支</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">uses:</span> <span class="string">stefanzweifel/git-auto-commit-action@v4</span></span><br><span class="line">        <span class="attr">with:</span></span><br><span class="line">          <span class="attr">commit_message:</span> <span class="string">&quot;[CI] Update Snapshots&quot;</span></span><br></pre></td></tr></table></figure><hr><h3 id="运行流程概览"><a href="#运行流程概览" class="headerlink" title="运行流程概览"></a><strong>运行流程概览</strong></h3><ol><li>每当 Pull Request 中添加评论时，GitHub Actions 会触发此工作流。</li><li>若评论严格匹配 <code>/update-snapshots</code>，则获取当前 Pull Request 的分支名和最新的提交 SHA。</li><li>检出对应分支并设置环境。</li><li>使用 Playwright 根据分支当前 UI 更新基准快照。</li><li>将更新的快照提交到 Pull Request 分支中。</li></ol><p><img src="https://upload-images.jianshu.io/upload_images/10024246-e13c875fdb854545.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240"></p><p>发布此工作流后，创建新的 Pull Request 并添加 <code>/update-snapshots</code> 评论时，CI 将生成更新的快照。  </p><hr><h3 id="运行-Playwright-测试以验证部署预览（Netlify、Vercel-等）"><a href="#运行-Playwright-测试以验证部署预览（Netlify、Vercel-等）" class="headerlink" title="运行 Playwright 测试以验证部署预览（Netlify、Vercel 等）"></a><strong>运行 Playwright 测试以验证部署预览（Netlify、Vercel 等）</strong></h3><p>如果你的 Web 应用托管在 Netlify 或 Vercel 等平台上，你可能会使用它们的“部署预览”功能。这项功能允许你在不影响生产环境的情况下预览拉取请求的更改。部署预览默认对 GitHub 的拉取请求启用，它通过将更改部署到一个不同于生产站点的唯一 URL 来实现。</p><p>如果你的 Web 应用支持部署预览，我们可以将其集成到视觉回归测试工作流中，以对比快照，而不是依赖本地 Web 服务器。<br>这种方法有两个好处：</p><ol><li>避免为运行测试而启动本地 Web 服务器。  </li><li>使用预览链接运行测试会生成更可靠的快照，因为这些链接与生产环境的展示内容一致。</li></ol><p>从总体上看，要使 Playwright 测试支持部署预览，我们需要对代码库进行以下三项主要更改：</p><ol><li>允许通过参数传递测试 URL，使测试能够识别部署预览链接。  </li><li>更新 GitHub Action 工作流，等待部署预览完成。  </li><li>将部署预览的 URL（作为环境变量）传递给 Playwright。  </li></ol><p>以下是如何在 Netlify 中实现（如果你使用的是 Vercel 或其他支持部署预览的平台，所需更改几乎相同）。  </p><p>首先，在 <code>playwright.config.ts</code> 中更新 <code>use.baseURL</code> 的值，使其能够通过环境变量（<code>WEBSITE_URL</code>）接收部署 URL：  </p><figure class="highlight typescript"><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">use</span>: &#123;</span><br><span class="line">  <span class="attr">baseURL</span>: process.<span class="property">env</span>.<span class="property">WEBSITE_URL</span>,</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>此外，如果提供了 <code>WEBSITE_URL</code> 环境变量，禁用 Playwright 的 Web 服务器：  </p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><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">webServer</span>: process.<span class="property">env</span>.<span class="property">WEBSITE_URL</span></span><br><span class="line">  ? <span class="literal">undefined</span></span><br><span class="line">  : &#123;</span><br><span class="line">      <span class="attr">command</span>: <span class="string">&quot;npm run dev --port 8080&quot;</span>,</span><br><span class="line">      <span class="attr">port</span>: <span class="number">8080</span>,</span><br><span class="line">      <span class="attr">reuseExistingServer</span>: <span class="literal">true</span>,</span><br><span class="line">    &#125;,</span><br></pre></td></tr></table></figure><p>接下来，更新 <code>playwright.yaml</code> 工作流，以便针对部署预览运行测试。<br>要等待 Netlify 的部署预览 URL，我们可以使用 <code>mmazzarolo/wait-for-netlify-action</code> GitHub Action。</p><p><code>mmazzarolo/wait-for-netlify-action</code> 是 <code>probablyup/wait-for-netlify-action</code> 的一个分支。默认情况下，<code>probablyup/wait-for-netlify-action</code> 假定在由拉取请求推送触发的工作流中运行。而我们的 <code>update-snapshots.yml</code> 工作流是由评论触发的，因此我分叉了这个 GitHub Action，以确保它可以在任何工作流中运行，无论触发方式如何。  </p><p><code>wait-for-netlify-action</code> GitHub Action 需要两项配置：  </p><ol><li>设置一个 <code>NETLIFY_TOKEN</code> GitHub Action 密钥，使用 Netlify 个人访问令牌。  </li><li>将 Netlify 站点 ID（Netlify 中：设置 → 站点详情 → 通用）传递给工作流中的 <code>site_id</code> 参数。  </li></ol><p>以下是 <code>playwright.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><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="attr">name:</span> <span class="string">Playwright</span> <span class="string">Tests</span></span><br><span class="line"> </span><br><span class="line"><span class="attr">on:</span></span><br><span class="line">  <span class="attr">push:</span></span><br><span class="line">    <span class="attr">branches:</span> [<span class="string">main</span>, <span class="string">master</span>]</span><br><span class="line">  <span class="attr">pull_request:</span></span><br><span class="line">    <span class="attr">branches:</span> [<span class="string">main</span>, <span class="string">master</span>]</span><br><span class="line">  <span class="attr">workflow_run:</span></span><br><span class="line">    <span class="attr">workflows:</span> [<span class="string">&quot;Update Snapshots&quot;</span>]</span><br><span class="line">    <span class="attr">types:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">completed</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">timeout-minutes:</span> <span class="number">60</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">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">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">&quot;14.x&quot;</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">npm</span> <span class="string">install</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Install</span> <span class="string">Playwright</span> <span class="string">Browsers</span></span><br><span class="line">        <span class="attr">run:</span> <span class="string">npx</span> <span class="string">playwright</span> <span class="string">install</span> <span class="string">--with-deps</span> <span class="string">chromium</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Wait</span> <span class="string">for</span> <span class="string">Netlify</span> <span class="string">Deploy</span></span><br><span class="line">        <span class="attr">uses:</span> <span class="string">mmazzarolo/wait-for-netlify-action@8a7a8d8cf5b313c916d805b76cc498380062d268</span></span><br><span class="line">        <span class="attr">id:</span> <span class="string">get-netlify-preview-url</span></span><br><span class="line">        <span class="attr">with:</span></span><br><span class="line">          <span class="attr">site_id:</span> <span class="string">&quot;YOUR_SITE_ID&quot;</span></span><br><span class="line">        <span class="attr">env:</span></span><br><span class="line">          <span class="attr">NETLIFY_TOKEN:</span> <span class="string">$&#123;&#123;</span> <span class="string">secrets.NETLIFY_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">Run</span> <span class="string">Playwright</span> <span class="string">tests</span></span><br><span class="line">        <span class="attr">run:</span> <span class="string">WEBSITE_URL=$&#123;&#123;</span> <span class="string">steps.get-netlify-preview-url.outputs.url</span> <span class="string">&#125;&#125;</span> <span class="string">npx</span> <span class="string">playwright</span> <span class="string">test</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">uses:</span> <span class="string">actions/upload-artifact@v2</span></span><br><span class="line">        <span class="attr">if:</span> <span class="string">failure()</span></span><br><span class="line">        <span class="attr">with:</span></span><br><span class="line">          <span class="attr">name:</span> <span class="string">playwright-report</span></span><br><span class="line">          <span class="attr">path:</span> <span class="string">playwright-report/</span></span><br><span class="line">          <span class="attr">retention-days:</span> <span class="number">30</span></span><br></pre></td></tr></table></figure><p>最后，像上面一样更新 <code>update-snapshots.yml</code> 工作流，确保 Playwright 的视觉回归测试可以针对部署预览运行。</p><h3 id="结论"><a href="#结论" class="headerlink" title="结论"></a><strong>结论</strong></h3><p>希望这篇博客文章为你搭建视觉测试体系提供了一个坚实的基础。以下是一些可以进一步改进的思路：  </p><ul><li>你可能希望通过使用 <code>fullScreen</code> 参数测试全屏快照，而不仅仅是可见视口。此外，还可以尝试捕获移动端的快照。  </li><li>如果你的 Web 应用会异步加载部分 UI（例如图片、视频），可能需要在测试中等待它们加载完成，或者将它们从测试中排除。  </li><li>可以限制 <code>/update-snapshots</code> 命令的权限，仅允许仓库的所有者调用。  </li><li>你可以通过其他方式（例如 Webhooks）触发快照更新流程，而不是依赖 GitHub 评论。  </li><li>将快照存储在第三方存储解决方案中可能是更灵活的选择。  </li><li>如果你在使用部署预览，可以通过并行化等待预览链接的步骤与其他工作流步骤来优化工作流效率。  </li></ul>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;&lt;img src=&quot;https://upload-images.jianshu.io/upload_images/10024246-5809585ddefcfd76.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240</summary>
      
    
    
    
    <category term="前端" scheme="https://www.nirvana.net.cn/categories/%E5%89%8D%E7%AB%AF/"/>
    
    
  </entry>
  
  <entry>
    <title>BUG无处不在</title>
    <link href="https://www.nirvana.net.cn/2024/1027SpHVVlllll.html"/>
    <id>https://www.nirvana.net.cn/2024/1027SpHVVlllll.html</id>
    <published>2024-10-27T08:28:03.000Z</published>
    <updated>2024-10-27T08:45:58.418Z</updated>
    
    <content type="html"><![CDATA[<p><img src="https://images.nirvana.net.cn/static/images/images/10024246-b03a1c1fb6e39060.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="></p><h3 id="“犯错是人之常情”"><a href="#“犯错是人之常情”" class="headerlink" title="“犯错是人之常情”"></a><strong>“犯错是人之常情”</strong></h3><p>程序员中间经常互相调侃，说自己的主要工作就是写各种BUG，然后在费劲心力的去消灭它。bug 各式各样，从页面打不开到数据丢失，让人习以为常。</p><p>或许有人会忍不住问：为什么 bug 总是会出现？</p><p>其实，bug 是代码的副产品。代码是人写的，而人总会犯错。就像硬币有正反面，人性的缺陷在代码中也无法避免。</p><p>如果你对 bug 零容忍，甚至把它写入 KPI，只会让开发者不敢做大改动，害怕引入更多风险。经历过这种情况情况的团队会心里憔悴，结果是团队没有动力重构或尝试新技术，创新停滞不前。</p><p>所以，bug 不可能完全消失，关键是如何减少它们的出现。</p><h3 id="重新认识质量"><a href="#重新认识质量" class="headerlink" title="重新认识质量"></a><strong>重新认识质量</strong></h3><p>在 MoSCoW 方法中，功能优先级分为四类：Must Have、Should Have、Could Have 和 Won’t Have。质量如果被当作一个功能，它往往不会是“必须有”的。用户不会因为一个应用没 bug 就一直用下去，也不会因为有 bug 就立刻换到竞争对手。<br>这意味着：</p><ul><li><ol><li>一些缺陷是可以容忍的。</li></ol></li><li><ol start="2"><li>质量的投入永远是有限的。</li></ol></li></ul><p>在快速迭代和低成本的互联网产品中，用户对质量的容忍度变得非常高。</p><p>关键问题是：如何用有限的资源最大化提升质量？如果只通过增加人手来修复 bug，问题不会真正解决，因为这只是加快了“清理”的速度，而不是减少“制造垃圾”的人。</p><p>现实中，很多公司当线上问题增多时，第一反应是责怪 QA。这种做法像是“刻舟求剑”，因为 bug 是编码过程中产生的，而不是 QA 能完全避免的。</p><p>我的观点其实是老生常谈：质量应该在开发过程中内建，测试要尽量“左移”。这些概念在网上有很多专业资料，这里不再展开。</p><h3 id="质量的成本"><a href="#质量的成本" class="headerlink" title="质量的成本"></a><strong>质量的成本</strong></h3><p>接下来是一个关键问题：谁该对质量负责？官方回答是“每个人”，但实际上，程序员常常是首当其冲的。</p><p>假设你是团队经理，发现上个月线上问题数量翻了一倍，你冲到开发团队前怒吼：“我希望这个月问题数降到个位数！” 你觉得他们能做到吗？</p><p>事实上，质量是努力换来的，不是靠“希望”来的。提升质量有代价，关键是你愿意付出多少。</p><p>很多提升质量的做法其实早已被证明有效，比如重构、代码评审、结对编程和持续集成。以单元测试为例，在我所在的项目中，写 React 组件的单元测试时间几乎和开发功能的时间一样多。如果代码改动导致之前的测试失败，还要额外花时间修复，这些都是隐性的成本。</p><p>更大的难点在于改变工程文化。你需要保证开发者认真写测试，关注测试结果，并修复问题。同时，争取管理层的理解和支持，给这些实践留足空间。</p><p>工程实践往往很难立刻见效。比如你无法准确预测，在提高了测试覆盖率之后，bug 会减少多少。而实践带来的“负面”效应却很明显：团队交付速度会变慢。</p><p>测试的好处更多体现在未来，它可以增强我们在重构和改动时的信心，防止旧功能被新功能破坏。但这种收益往往是长期的，短期内难以看见。</p><p>于是，现状就是：迭代速度变慢了，质量的成本暂时看不到回报，最终质量成了可以被牺牲的部分。</p><h3 id="还债"><a href="#还债" class="headerlink" title="还债"></a><strong>还债</strong></h3><p>质量问题不是个人问题，而是团队甚至整个行业的问题。我们需要面对现实，虽然提升质量的投入见效慢，但这不是可有可无的事情。</p><p>虽然有时候“快”比“好”更重要，但当质量问题不可避免地出现时，别忘了它是在什么时候被忽视的。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;&lt;img src=&quot;https://images.nirvana.net.cn/static/images/images/10024246-b03a1c1fb6e39060.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2Awdb</summary>
      
    
    
    
    <category term="前端" scheme="https://www.nirvana.net.cn/categories/%E5%89%8D%E7%AB%AF/"/>
    
    
    <category term="web" scheme="https://www.nirvana.net.cn/tags/web/"/>
    
  </entry>
  
  <entry>
    <title>Safari无痕模式取不到url中的参数？</title>
    <link href="https://www.nirvana.net.cn/2024/1026iVepJXYmmm.html"/>
    <id>https://www.nirvana.net.cn/2024/1026iVepJXYmmm.html</id>
    <published>2024-10-26T08:28:03.000Z</published>
    <updated>2024-10-27T08:45:58.438Z</updated>
    
    <content type="html"><![CDATA[<h3 id="问题描述"><a href="#问题描述" class="headerlink" title="问题描述"></a>问题描述</h3><p>最近发现在Safari17中用户使用我们的服务时出现了一些问题，从A页面跳转到B页面，B页面无法正常运行，但是直接复制B页面链接在新标签页访问没有问题。该问题只影响Safari17无痕浏览模式，在iOS和macOS上均出现同样问题。</p><p>经过排查后发现，B页面在获取<code>location.href</code>时只能获取到<code>?</code>前面的部分，即<code>location.search</code>部分全部丢失。比如说我们的页面链接是<code>https://example.com?a=xxx&amp;b=xxx&amp;c=xxx</code>，但是通过<code>location.href</code>获取到的url是<code>https://example.com</code>。</p><p>同时我们尝试了从同域名跳转到B页面以及从不同域名跳转到B页面，发现只有在域名不相同时跳转会出现这个问题。</p><p>同时尝试了其他方法，比如直接在html文件中，或者同域名脚本中访问<code>location.href</code>，发现是没有问题的。</p><p>在网上搜索之后，我们考虑到是否是因为Safari17中新增加的高级跟踪和指纹识别保护（<code>Advanced Tracking and Fingerprinting Protection</code>）功能所做的限制，它其中的一个功能是会删除链接中的查询参数，也就是链接追踪保护。</p><p><a href="https://link.juejin.cn/?target=https://forums.developer.apple.com/forums/thread/739549" title="https://forums.developer.apple.com/forums/thread/739549">forums.developer.apple.com/forums/thre…</a></p><h3 id="链接追踪保护"><a href="#链接追踪保护" class="headerlink" title="链接追踪保护"></a>链接追踪保护</h3><p>链接追踪保护（<code>Link Tracking Protection</code>）：苹果公司在最新的 iOS 17 和 macOS Sonoma 系统中推出了一项新功能，这项功能可以在 Safari 私密浏览模式、信息和邮件应用中，<strong>自动删除 URL 中的追踪参数，让链接变得简洁而干净</strong>。</p><h4 id="什么是链接追踪保护？"><a href="#什么是链接追踪保护？" class="headerlink" title="什么是链接追踪保护？"></a>什么是链接追踪保护？</h4><p>LTP 将删除添加到某些 URL 末尾的一些跟踪信息。以下是 Apple 的新闻稿中关于此功能的说明：</p><blockquote><p>“一些网站会在其 URL 中添加额外信息，以便在其他网站上跟踪用户。现在，这些信息将从用户在信息和邮件中分享的链接中删除，并且链接仍将按预期工作。这些信息也将从 Safari 隐私浏览中的链接中删除。”</p></blockquote><p>什么是跟踪参数？它只是 URL 末尾的额外信息。当我们在网上浏览或复制一个链接时，它的末尾可能会携带一些看不懂的字符，这些字符其实是广告商用来追踪我们行为的参数，它们可以记录我们从哪里来、去了哪里、点击了什么等等。这样一来，广告商就可以根据我们的喜好和习惯，向我们推送定制化的广告。</p><h4 id="哪些跟踪参数被删除了？"><a href="#哪些跟踪参数被删除了？" class="headerlink" title="哪些跟踪参数被删除了？"></a>哪些跟踪参数被删除了？</h4><p>目前了解到以下参数会被删除：</p><ul><li>  <code>gclid </code>- Google AdWords // 分析</li><li>  <code>dclid</code> - Google 展示广告网络</li><li>  <code>foclid</code> - Facebook 广告</li><li>  <code>twelkd</code> - Twitter 广告</li><li>  <code>msclkid</code> - 微软广告</li><li>  <code>mc_eid</code> -Mailchimp</li><li>  <code>igshid</code> - Instagram</li></ul><p>示例：<a href="https://link.juejin.cn/?target=https://example.com/?fbclid=IwAR4HesRZLT-fxhhh3nZ7WKsOpaiFzsg4nH0K4WLRHw1h467GdRjaLilWbLs&utm_source=facebook&utm_medium=cpm&utm_campaign=test_campaign" title="https://example.com/?fbclid=IwAR4HesRZLT-fxhhh3nZ7WKsOpaiFzsg4nH0K4WLRHw1h467GdRjaLilWbLs&amp;utm_source=facebook&amp;utm_medium=cpm&amp;utm_campaign=test_campaign">链接</a></p><p>非IOS17点击链接</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"></span><br><span class="line">`https://example.com?fbclid=IwAR4HesRZLT-fxhhh3nZ7WKsOpaiFzsg4nH0K4WLRHw1h467GdRjaLilWbLs&amp;utm_source=facebook&amp;utm_medium=cpm&amp;utm_campaign=test_campaign` </span><br></pre></td></tr></table></figure><p>如果是IOS17并点击了邮件、信息或隐私模式下的Safari中的链接</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">`https://example.com?utm_source=facebook&amp;utm_medium=cpm&amp;utm_campaign=test_campaign`</span><br></pre></td></tr></table></figure><p>可见faclid参数被删除了</p><h4 id="对location-href的影响"><a href="#对location-href的影响" class="headerlink" title="对location.href的影响"></a>对location.href的影响</h4><p>以上似乎并不能解释我们遇到的问题，我们的参数并没有包含这些跟踪参数，在路径栏看到的也是完整的页面链接。</p><p>因此，继续查阅其他资料，发现这个链接追踪保护功能对我们的网站还有一些其他的影响<br><img src="https://images.nirvana.net.cn/static/images/images/10024246-28def1c1f5034efb.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="><br><a href="https://link.juejin.cn/?target=https://webkit.org/blog/15697/private-browsing-2-0/" title="https://webkit.org/blog/15697/private-browsing-2-0/">webkit.org/blog/15697/…</a></p><p>大致意思就是【在跨站点导航后的目标网站上，所有尝试读取完整 URL 的第三方脚本（例如使用 location.search、location.href 或 document.URL）都将获得不含查询参数或片段的 URL 版本】</p><p>也就是说，若同时满足以下两个条件：</p><ol><li> 跨域名跳转</li><li> 在第三方脚本中访问location.href或者location.search或者document</li></ol><p>将无法从url中获取query参数</p><p>刚好，我们的页面的html文件和js文件部署在不同的域名下，同时A站点跳转到我们的站点，域名也不一致，刚好踩中两个条件，导致了这个问题的发生。</p><h3 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h3><p>若从A页面跳转到B页面，两个页面在不同域名下，且B页面引入了第三方脚本（即js文件域名与html不一致），则在Sarafi的无痕浏览模式中会出现B页面的第三方<code>js</code>文件中无法获取到<code>url</code>中的参数的问题。</p><p>这里提到的页面跳转，我尝试了以下三种方式，都会出现上述问题：</p><ul><li>  <code>a</code>标签</li><li>  <code>window.open</code>方法</li><li>  <code>location.href=&quot;xxx&quot;</code></li></ul><h3 id="参考文章"><a href="#参考文章" class="headerlink" title="参考文章"></a>参考文章</h3><p><a href="https://link.juejin.cn/?target=https://webkit.org/blog/14205/news-from-wwdc23-webkit-features-in-safari-17-beta/%23security" title="https://webkit.org/blog/14205/news-from-wwdc23-webkit-features-in-safari-17-beta/#security">webkit.org/blog/14205/…</a></p><p><a href="https://link.juejin.cn/?target=https://webkit.org/blog/15697/private-browsing-2-0/" title="https://webkit.org/blog/15697/private-browsing-2-0/">webkit.org/blog/15697/…</a></p><p><a href="https://link.juejin.cn/?target=https://usefathom.com/blog/ios17-link-tracking-protection" title="https://usefathom.com/blog/ios17-link-tracking-protection">usefathom.com/blog/ios17-…</a></p>]]></content>
    
    
      
      
    <summary type="html">&lt;h3 id=&quot;问题描述&quot;&gt;&lt;a href=&quot;#问题描述&quot; class=&quot;headerlink&quot; title=&quot;问题描述&quot;&gt;&lt;/a&gt;问题描述&lt;/h3&gt;&lt;p&gt;最近发现在Safari17中用户使用我们的服务时出现了一些问题，从A页面跳转到B页面，B页面无法正常运行，但是直接复制B页面</summary>
      
    
    
    
    <category term="前端" scheme="https://www.nirvana.net.cn/categories/%E5%89%8D%E7%AB%AF/"/>
    
    
    <category term="web" scheme="https://www.nirvana.net.cn/tags/web/"/>
    
  </entry>
  
  <entry>
    <title>React Native 0.76 - 默认启用新架构，React Native DevTools等新功能</title>
    <link href="https://www.nirvana.net.cn/2024/1025nDDXFTgQ33.html"/>
    <id>https://www.nirvana.net.cn/2024/1025nDDXFTgQ33.html</id>
    <published>2024-10-25T13:12:40.000Z</published>
    <updated>2024-10-27T08:45:58.336Z</updated>
    
    <content type="html"><![CDATA[<p><img src="https://images.nirvana.net.cn/static/images/images/10024246-fc2786b7d4b5d28e.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="><br>今天我们很高兴发布 React Native 0.76！</p><p>这是 React Native 的一个重要里程碑，因为我们默认启用了新架构，并引入了 React Native DevTools。这是我们团队经过 6 年努力，与我们令人难以置信的开发者社区共同支持的成果。</p><p>亮点</p><ul><li>默认启用 React Native 新架构</li><li>引入 React Native DevTools</li><li>更快的 Metro 解析</li><li>支持 Box Shadow 和 Filter 样式属性</li></ul><p>重大变更</p><ul><li>移除了对 react-native-community/cli 的依赖</li><li>通过合并原生库，Android 应用减少了约 3.8MB</li><li>更新了最低 iOS 和 Android SDK 要求</li></ul><h2 id="亮点详解"><a href="#亮点详解" class="headerlink" title="亮点详解"></a>亮点详解</h2><h4 id="默认启用-React-Native-新架构"><a href="#默认启用-React-Native-新架构" class="headerlink" title="默认启用 React Native 新架构"></a><strong>默认启用 React Native 新架构</strong></h4><p>从 React Native 0.76 开始，默认在您的项目中启用新架构。新架构是对 React Native 内部的一次重写，使应用开发者能够使用 React 构建高质量的原生应用程序。</p><p>今天，我们很高兴宣布新架构已准备好投入生产使用。</p><p>这一变化是 React Native 发展中的一个重要里程碑，我们邀请您阅读专门的博客文章，了解新架构的具体内容以及它将如何塑造 React Native 的未来：新架构来了。</p><h4 id="React-Native-DevTools"><a href="#React-Native-DevTools" class="headerlink" title="React Native DevTools"></a><strong>React Native DevTools</strong></h4><p>我们发布了 React Native DevTools 的第一个稳定版本，这是我们新的默认调试体验。<br><img src="https://images.nirvana.net.cn/static/images/images/10024246-0700ff371c7567aa.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="><br>我们希望您使用的调试工具在所有平台上都能保持可靠、熟悉、简单且一致。React Native DevTools 实现了这些原则 —— 它是一款与 React Native 深度集成的浏览器风格的开发者工具。主要功能包括：</p><ul><li>熟悉的、与 Web 对齐的工具 —— 基于 Chrome DevTools 的全功能调试器，具有可靠的断点、监视值、逐步调试、堆栈检查和丰富的 JavaScript 控制台。这些核心功能现在可以在重新加载时稳定工作。</li><li>改进并集成了 React DevTools —— 内置的 React 组件检查器和性能分析工具无缝运行，组件高亮显示更快、更可靠。</li><li>改进的用户体验 —— 新的“Paused in Debugger”覆盖层，明确显示应用程序何时在断点上暂停。LogBox 中的警告现在以摘要形式显示，并在 DevTools 连接时隐藏。</li><li>修复了重新连接行为 —— JavaScript 断点现在在重新加载和 DevTools 断开后重新连接时可以可靠工作。DevTools 甚至可以在原生重建后重新连接到同一个应用。</li><li>即时启动 —— React Native DevTools 默认配置即可使用。可以从应用内的开发菜单或通过命令行的 <code>j</code> 命令打开进行调试，CLI 服务器现在支持多个模拟器和设备。</li></ul><p>React Native DevTools 与我们之前的调试选项有根本的不同，包括与 0.73 首次推出的实验性调试器体验的不同。它切换到了一个全新的后端调试栈，这是我们在过去一年里重建的。这意味着与之前工具的兼容性有所变化，您应该会体验到更可靠的端到端调试体验。我们计划基于这个新栈构建更多功能，比如性能和网络面板。</p><h6 id="逐步淘汰-Metro-日志"><a href="#逐步淘汰-Metro-日志" class="headerlink" title="逐步淘汰 Metro 日志"></a><strong>逐步淘汰 Metro 日志</strong></h6><p>在我们下一次发布中，我们将删除 Metro 中的转发日志，以与现代浏览器工具保持一致，移除旧的调试集成。请改用 React Native DevTools 的全功能控制台面板进行日志记录。详见我们的常见问题解答。</p><h6 id="链接"><a href="#链接" class="headerlink" title="链接"></a><strong>链接</strong></h6><ul><li><a href="https://reactnative.dev/docs/debugging">更新的调试文档</a></li><li><a href="https://www.youtube.com/watch?v=OwivVpg6Luc">React Universe Conf 大会的公告演讲</a></li><li><a href="https://github.com/react-native-community/discussions-and-proposals/discussions/819">反馈主题和常见问题解答</a></li></ul><h4 id="更快的-Metro-解析"><a href="#更快的-Metro-解析" class="headerlink" title="更快的 Metro 解析"></a><strong>更快的 Metro 解析</strong></h4><p>我们对 Metro 的解析器（负责从导入路径中查找模块的组件）进行了多项性能优化，使其速度提高了大约 15 倍。这显著提升了 Metro 的整体性能，特别是在增量构建（warm builds）时，速度提高了约 4 倍。</p><h4 id="Box-Shadow-和-Filter-样式属性"><a href="#Box-Shadow-和-Filter-样式属性" class="headerlink" title="Box Shadow 和 Filter 样式属性"></a><strong>Box Shadow 和 Filter 样式属性</strong></h4><p>在 0.76 版本中，我们新增了两个仅在新架构中支持的样式属性 —— <code>boxShadow</code> 和 <code>filter</code>。这两个属性都在 Web 上存在，并且团队在可能的情况下坚持符合规范，使这些属性更为可预测、熟悉，并且最终更容易采用（有关例外情况，请参阅“限制和规范偏差”部分）。因此，您可以查阅 Web 文档，全面了解它们的工作方式，但仍有一些重要的差异需要注意。</p><h6 id="boxShadow"><a href="#boxShadow" class="headerlink" title="boxShadow"></a><strong>boxShadow</strong></h6><p><code>boxShadow</code> 为元素添加阴影，并可控制阴影的位置、颜色、大小和模糊度。您可以查阅 MDN 文档，全面了解每个参数的功能，并尝试自己动手操作。以下是一些您可以创建的阴影示例。</p><ul><li>新的 <code>boxShadow</code> 样式属性<br><code>boxShadow</code> 可以接受字符串格式（模仿 CSS 语法）或包含变量的 JavaScript 对象。例如，字符串 <code>&#39;5 5 5 0 rgba(255, 0, 0, 0.5)&#39;</code> 和对象 <code>&#123;offsetX: 5, offsetY: 5, blurRadius: 5, spreadDistance: 0, color: &#39;rgba(255, 0, 0, 0.5)&#39;&#125;</code> 将产生相同的阴影效果。<br><img src="https://images.nirvana.net.cn/static/images/images/10024246-93f6d75dce7adeaa.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="></li></ul><p>之前的阴影功能存在一些不足之处，而这些不足现在通过 <code>boxShadow</code> 得到了改进：</p><ul><li>不支持 Android</li><li>不能设置为内阴影（inset）</li><li>没有扩展距离</li><li>在没有背景色的视图上无法工作</li><li>是单独的属性，因此无法与 Web 语法共享</li></ul><h6 id="限制与规范偏差"><a href="#限制与规范偏差" class="headerlink" title="限制与规范偏差"></a><strong>限制与规范偏差</strong></h6><ul><li>默认的阴影颜色是黑色，而不是父元素的颜色</li><li>Android 常规阴影支持 Android 9 及以上版本</li><li>Android 内阴影支持 Android 10 及以上版本</li></ul><h6 id="filter"><a href="#filter" class="headerlink" title="filter"></a><strong>filter</strong></h6><p><code>filter</code> 为元素添加某些图形滤镜。它包括修改亮度、饱和度和色调的颜色滤镜，以及添加模糊和阴影等的非颜色滤镜。您可以查阅 MDN 文档，全面了解每个滤镜函数，并尝试自己操作。以下是一个应用了不同滤镜的热狗图片示例。</p><p>滤镜演示<br>从左到右：无滤镜，饱和滤镜，模糊滤镜，反转滤镜</p><p>与 <code>boxShadow</code> 类似，<code>filter</code> 可以接受字符串（模仿 CSS 语法）或包含变量的 JavaScript 对象数组。例如，字符串 <code>&#39;saturate(0.5) grayscale(0.25)&#39;</code> 和数组 <code>[&#123;saturate: 0.5&#125;, &#123;grayscale: 0.25&#125;]</code> 将产生相同的滤镜效果。<code>filter</code> 有一个 <code>dropShadow</code> 值，与 <code>boxShadow</code> 略有不同。最大的差异在于 <code>dropShadow</code> 是一个 alpha 蒙版 —— 只有 alpha 值不为零的像素才会投射阴影。而 <code>boxShadow</code> 会围绕元素的边框框投射阴影，即使里面没有内容。此外，<code>dropShadow</code> 没有扩展距离参数，也不能设置为内阴影（阴影投射到元素内部）。</p><h6 id="限制与规范偏差-1"><a href="#限制与规范偏差-1" class="headerlink" title="限制与规范偏差"></a><strong>限制与规范偏差</strong></h6><ul><li>iOS 的 <code>filter</code> 仅支持亮度和不透明度</li><li>iOS 的 <code>filter</code> 不会应用于阴影、轮廓或任何元素边界外的图形元素</li><li>Android 的模糊和投影阴影仅支持 Android 12 及以上版本</li><li><code>filter</code> 隐含 <code>overflow: hidden</code>，因此带有滤镜的元素的子元素，如果超出父元素边界将被裁剪</li></ul><h2 id="重大变更"><a href="#重大变更" class="headerlink" title="重大变更"></a><strong>重大变更</strong></h2><h4 id="移除了对-react-native-community-cli-的依赖"><a href="#移除了对-react-native-community-cli-的依赖" class="headerlink" title="移除了对 @react-native-community/cli 的依赖"></a><strong>移除了对 @react-native-community/cli 的依赖</strong></h4><p>如在 0.75 版本中所述，我们的愿景是让 React Native 与框架无关。因此，我们完成了移除 @react-native-community/cli 作为 React Native 直接依赖项的工作。</p><p>将 React Native 与 CLI 解耦使我们能够更快地独立发布这些项目，并更好地划分两者的职责。</p><p>如果您在日常工作中依赖 CLI，请确保在您的 <code>package.json</code> 中显式添加对 CLI 的依赖：</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="attr">&quot;devDependencies&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">   <span class="attr">&quot;@react-native-community/cli&quot;</span><span class="punctuation">:</span> <span class="string">&quot;15.0.0&quot;</span><span class="punctuation">,</span></span><br><span class="line">   <span class="attr">&quot;@react-native-community/cli-platform-android&quot;</span><span class="punctuation">:</span> <span class="string">&quot;15.0.0&quot;</span><span class="punctuation">,</span></span><br><span class="line">   <span class="attr">&quot;@react-native-community/cli-platform-ios&quot;</span><span class="punctuation">:</span> <span class="string">&quot;15.0.0&quot;</span></span><br><span class="line"><span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br></pre></td></tr></table></figure><h6 id="通过原生库合并，Android-应用减少了约-3-8MB"><a href="#通过原生库合并，Android-应用减少了约-3-8MB" class="headerlink" title="通过原生库合并，Android 应用减少了约 3.8MB"></a><strong>通过原生库合并，Android 应用减少了约 3.8MB</strong></h6><p>React Native 0.76 发布时减少了原生库数量，因为我们将大量的原生代码合并到一个名为 <code>libreactnative.so</code> 的库中。</p><p>这一更改带来了应用体积的缩减和 Android 应用启动性能的提升。根据我们的基准测试，应用体积将减少约 3.8MB（总量的 20%），应用启动时间中位数将减少约 15 毫秒（约 8%）。</p><p>另一方面，这对应用和库开发者来说是一次重大变化。</p><p>应用开发者需要按如下方式更新 <code>Application</code> 的 <code>onCreate</code> 方法：</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><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> com.facebook.react.soloader.OpenSourceMergedSoMapping</span><br><span class="line"><span class="keyword">import</span> com.facebook.soloader.SoLoader</span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">MainApplication</span> : <span class="type">Application</span>(), ReactApplication &#123;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">override</span> <span class="function"><span class="keyword">fun</span> <span class="title">onCreate</span><span class="params">()</span></span> &#123;</span><br><span class="line">    <span class="keyword">super</span>.onCreate()</span><br><span class="line">    SoLoader.<span class="keyword">init</span>(<span class="keyword">this</span>, OpenSourceMergedSoMapping)</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>此更改对于正确加载 <code>libreactnative.so</code> 是必要的，并已包含在升级助手中。</p><p>除非您有自定义 C++ 代码，否则库作者不会受到此更改的影响。</p><p>有关此更改的技术深入解读，以及对库作者的建议，请阅读<a href="https://github.com/react-native-community/discussions-and-proposals/discussions/816">专门的文章</a>。</p><h4 id="最低-iOS-和-Android-SDK-要求的更新"><a href="#最低-iOS-和-Android-SDK-要求的更新" class="headerlink" title="最低 iOS 和 Android SDK 要求的更新"></a><strong>最低 iOS 和 Android SDK 要求的更新</strong></h4><p>我们更新了最低平台和 SDK 版本：</p><ul><li>iOS：从 13.4 升级到 15.1</li><li>Android：从 SDK 23 升级到 SDK 24（Android 7）</li></ul><p>此更改已在今年早些时候的 0.75 版本发布时宣布。有关更多背景信息，请参阅专门针对 Android 和 iOS 的文章。</p><h4 id="其他重大变更"><a href="#其他重大变更" class="headerlink" title="其他重大变更"></a><strong>其他重大变更</strong></h4><h6 id="动画"><a href="#动画" class="headerlink" title="动画"></a><strong>动画</strong></h6><ul><li>停止在循环动画中向 React 发送状态更新。这会导致循环动画的不必要重新渲染。</li></ul><h6 id="开发工具："><a href="#开发工具：" class="headerlink" title="开发工具："></a><strong>开发工具：</strong></h6><ul><li>在新架构中移除了 Inspector Panel 中的性能和网络标签。（RFC）</li></ul><h6 id="文本引擎"><a href="#文本引擎" class="headerlink" title="文本引擎"></a><strong>文本引擎</strong></h6><ul><li>在 TextLayoutManager 中始终使用 <code>AttributedStringBox</code> 代替 <code>AttributedString</code>。</li></ul><h6 id="Android"><a href="#Android" class="headerlink" title="Android"></a><strong>Android</strong></h6><ul><li><strong>渲染：</strong>视图背景不再直接是 <code>ReactViewBackgroundDrawable</code> 或 <code>CSSBackgroundDrawable</code>。</li></ul><h6 id="iOS"><a href="#iOS" class="headerlink" title="iOS"></a><strong>iOS</strong></h6><ul><li><strong>Turbo 模块</strong>：移除了用于自动链接纯 Cxx 模块的 <code>RCT_EXPORT_CXX_MODULE_EXPERIMENTAL</code> 宏。</li></ul><h2 id="鸣谢"><a href="#鸣谢" class="headerlink" title="鸣谢"></a><strong>鸣谢</strong></h2><p>React Native 0.76 包含来自 156 位贡献者的 1070 多个提交。感谢你们的辛勤工作！</p><p>感谢所有为本发布文档撰写功能的额外作者：</p><ul><li>Joe Vilches 和 Nick Gerleman：box-shadow 和 filter 样式属性。</li><li>Alex Hunt：React Native DevTools。</li><li>Nicola Corti：Android 作为单个库发布。</li></ul><h2 id="升级到-0-76"><a href="#升级到-0-76" class="headerlink" title="升级到 0.76"></a><strong>升级到 0.76</strong></h2><p>请使用 React Native 升级助手查看不同版本之间的代码变更，参考升级文档以帮助现有项目升级。</p><p>如果你使用 Expo，React Native 0.76 将在 Expo SDK 52 中得到支持。</p><p>如果你需要使用 CLI 创建新项目，可以运行以下命令：</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">npx @react-native-community/cli@latest init MyProject --version latest</span><br></pre></td></tr></table></figure><blockquote><p><strong>提示</strong>：0.76 现在是 React Native 的最新稳定版本，0.73.x 已不再支持。有关更多信息，请参阅 React Native 的支持政策。我们计划在不久的将来发布 0.73 的最终生命周期结束更新。</p></blockquote>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;&lt;img src=&quot;https://images.nirvana.net.cn/static/images/images/10024246-fc2786b7d4b5d28e.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2Awdb</summary>
      
    
    
    
    <category term="react-native" scheme="https://www.nirvana.net.cn/categories/react-native/"/>
    
    
  </entry>
  
  <entry>
    <title>ESLint 支持对 JSON 和 Markdown 的代码检查</title>
    <link href="https://www.nirvana.net.cn/2024/1024BeUI5WaaMM.html"/>
    <id>https://www.nirvana.net.cn/2024/1024BeUI5WaaMM.html</id>
    <published>2024-10-24T08:28:03.000Z</published>
    <updated>2024-10-27T08:45:58.337Z</updated>
    
    <content type="html"><![CDATA[<p><img src="https://images.nirvana.net.cn/static/images/images/10024246-8595617f3e538f45.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="><br>今年 7 月，我们宣布了 ESLint 未来的计划。部分公告提到将 ESLint 转变为一个更通用的代码检查工具，能够对任何语言进行检查。ESLint 的很多核心功能（如查找文件、解析文件、报告问题）都是通用的，因此过去几个月我们一直在提取核心中与 JavaScript 相关的部分。现在，我们很高兴地分享，这项工作已经取得了成果，使 ESLint 现在可以检查 JSON 和 Markdown 文件了！</p><h3 id="使用-eslint-json-进行-JSON-检查"><a href="#使用-eslint-json-进行-JSON-检查" class="headerlink" title="使用 @eslint/json 进行 JSON 检查"></a>使用 @eslint/json 进行 JSON 检查</h3><p>通过 @eslint/json 插件可以实现对 JSON 的检查，这是一个官方支持的语言插件。该插件提供对 JSON、JSONC（带注释的 JSON）和 JSON5 的解析。要使用该插件，可以从 npm 安装它：</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 @eslint/json -D</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><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> json <span class="keyword">from</span> <span class="string">&quot;@eslint/json&quot;</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">    &#123;</span><br><span class="line">        <span class="attr">plugins</span>: &#123;</span><br><span class="line">            json,</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">// 检查 JSON 文件</span></span><br><span class="line">    &#123;</span><br><span class="line">        <span class="attr">files</span>: [<span class="string">&quot;**/*.json&quot;</span>],</span><br><span class="line">        <span class="attr">language</span>: <span class="string">&quot;json/json&quot;</span>,</span><br><span class="line">        <span class="attr">rules</span>: &#123;</span><br><span class="line">            <span class="string">&quot;json/no-duplicate-keys&quot;</span>: <span class="string">&quot;error&quot;</span>,</span><br><span class="line">        &#125;,</span><br><span class="line">    &#125;,</span><br><span class="line">];</span><br></pre></td></tr></table></figure><p>该插件内置了多条规则，我们也欢迎更多建议。如果你有规则的想法，可以提交一个 Issue。</p><p>更多关于配置 JSON 检查的信息，请查看 README。</p><h3 id="使用-eslint-markdown-进行-Markdown-检查"><a href="#使用-eslint-markdown-进行-Markdown-检查" class="headerlink" title="使用 @eslint/markdown 进行 Markdown 检查"></a>使用 @eslint/markdown 进行 Markdown 检查</h3><p>通过 @eslint/markdown 插件可以实现对 Markdown 的检查，这是 eslint-plugin-markdown 插件的下一代版本，后者仅包含处理器。而新版本是官方支持的语言插件，支持 CommonMark 和 GitHub 风格的 Markdown 解析及规则。要使用该插件，可以从 npm 安装它：</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 @eslint/markdown -D</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><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// eslint.config.js</span></span><br><span class="line"><span class="keyword">import</span> markdown <span class="keyword">from</span> <span class="string">&quot;@eslint/markdown&quot;</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">    &#123;</span><br><span class="line">        <span class="attr">files</span>: [<span class="string">&quot;**/*.md&quot;</span>],</span><br><span class="line">        <span class="attr">plugins</span>: &#123;</span><br><span class="line">            markdown</span><br><span class="line">        &#125;,</span><br><span class="line">        <span class="attr">language</span>: <span class="string">&quot;markdown/commonmark&quot;</span>,</span><br><span class="line">        <span class="attr">rules</span>: &#123;</span><br><span class="line">            <span class="string">&quot;markdown/no-html&quot;</span>: <span class="string">&quot;error&quot;</span></span><br><span class="line">        &#125;</span><br><span class="line">    &#125;    </span><br><span class="line">];</span><br></pre></td></tr></table></figure><p>与 JSON 插件类似，该插件内置了多条规则，也欢迎更多建议。如果你有规则的想法，可以提交一个 Issue。</p><p>更多关于配置 Markdown 检查的信息，请查看 README。</p><h3 id="创建自定义规则和使用代码浏览器"><a href="#创建自定义规则和使用代码浏览器" class="headerlink" title="创建自定义规则和使用代码浏览器"></a>创建自定义规则和使用代码浏览器</h3><p>@eslint/json 和 @eslint/markdown 都会将源代码解析成 AST（抽象语法树），然后遍历 AST 来运行规则，就像 ESLint 对 JavaScript 所做的一样。这意味着你可以像为 JavaScript 编写自定义规则一样为 JSON 和 Markdown 编写自定义规则。主要的区别在于 AST 的格式。JSON 使用的是 Momoa AST，而 Markdown 使用的是 mdast。由于目前帮助你检查其他 AST 格式的资源较少，我们很高兴地宣布推出<a href="https://explorer.eslint.org/">代码浏览器</a>。<br><img src="https://images.nirvana.net.cn/static/images/images/10024246-60384ed37d2976f5.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="></p><h4 id="ESLint-代码浏览器"><a href="#ESLint-代码浏览器" class="headerlink" title="ESLint 代码浏览器"></a>ESLint 代码浏览器</h4><p>代码浏览器允许你查看和探索不同语言的 AST，有助于创建自定义规则。对于 JSON 和 Markdown，你将看到可展开的 AST 视图，而对于 JavaScript，你不仅可以看到 AST，还可以看到解析代码时 ESLint 生成的作用域和代码路径信息。今后，我们将继续为代码浏览器添加新的语言和功能，帮助你创建自定义规则。</p><p>最棒的是，代码浏览器是开源的，所以你也可以帮助我们让它变得更好。</p><h3 id="编写你自己的语言插件"><a href="#编写你自己的语言插件" class="headerlink" title="编写你自己的语言插件"></a>编写你自己的语言插件</h3><p>ESLint 语言设计成可以在任何插件中使用新的 <code>languages</code> 键。@eslint/json 和 @eslint/markdown 插件不仅用于提供对 JSON 和 Markdown 的检查，还可以作为如何创建自定义语言的示例。你可以查看语言文档，了解如何创建自己的语言。</p><h3 id="结论"><a href="#结论" class="headerlink" title="结论"></a>结论</h3><p>检查 JavaScript 以外的语言一直是 ESLint 路线图上的目标之一，因此我们很高兴能达到这一里程碑。我们的长期目标是确保 ESLint 能够检查任何 Web 项目中使用的文件，无论是通过官方支持的语言插件还是社区编写的插件。借助 JavaScript、JSON 和 Markdown，我们已经在实现这一目标的道路上迈出了重要一步。</p><p>最后提醒一下，ESLint 是一个由志愿者在业余时间维护的独立开源项目。如果你喜欢使用 ESLint，请和你的公司讨论赞助该项目的可能性。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;&lt;img src=&quot;https://images.nirvana.net.cn/static/images/images/10024246-8595617f3e538f45.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2Awdb</summary>
      
    
    
    
    <category term="前端" scheme="https://www.nirvana.net.cn/categories/%E5%89%8D%E7%AB%AF/"/>
    
    
    <category term="web" scheme="https://www.nirvana.net.cn/tags/web/"/>
    
  </entry>
  
  <entry>
    <title>Node.js 23发布</title>
    <link href="https://www.nirvana.net.cn/2024/1023M5Zk99mmmm.html"/>
    <id>https://www.nirvana.net.cn/2024/1023M5Zk99mmmm.html</id>
    <published>2024-10-23T08:28:03.000Z</published>
    <updated>2024-10-27T08:45:58.459Z</updated>
    
    <content type="html"><![CDATA[<p><img src="https://images.nirvana.net.cn/static/images/images/10024246-f5e2631ac8f982e3.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="></p><h3 id="主要功能与增强"><a href="#主要功能与增强" class="headerlink" title="主要功能与增强"></a>主要功能与增强</h3><p>Node.js 23 现已发布，带来了新功能、性能提升和更好的开发体验。该版本提升了兼容性和稳定性，为构建高效应用程序提供了更多工具。</p><p>此外，Node.js 22 将于10月29日那周升级为长期支持版本 (LTS)，进入长期维护阶段，以确保稳定和安全的更新。</p><p>Node.js 23 的亮点包括：</p><ul><li>使用 <code>require()</code> 加载原生 ES 模块的支持：在 v23.0.0 之前，这项功能需通过 <code>--experimental-require-module</code> 启用。</li><li>停止支持 32 位 Windows 系统：Node.js 23 不再支持 32 位 Windows，专注于现代环境。</li><li>稳定的 <code>--run</code> 命令：<code>--run</code> 标志已稳定，用于更高效的脚本执行。</li><li>测试运行器增强：测试运行器现在支持对覆盖文件使用 glob 模式，提高了测试配置的灵活性。<h3 id="require-esm-现在默认启用"><a href="#require-esm-现在默认启用" class="headerlink" title="require(esm) 现在默认启用"></a><code>require(esm)</code> 现在默认启用</h3>在 v20.x 和 v22.x 中，通过命令行标志 <code>--experimental-require-module</code> 可以加载原生 ES 模块。在 v23.x 中，这一功能现已默认启用。该功能仍处于实验阶段，我们正在收集用户反馈，以便在完全稳定之前进行更多的调整。为此，在 v23.x 中，当 Node.js 实例首次通过 <code>require()</code> 加载原生 ES 模块时，将会发出实验性警告。</li></ul><p>如果此功能导致任何回归问题，用户可以在 Node.js 的问题追踪器上报告。同时，作为解决方法，可以使用 <code>--no-experimental-require-module</code> 来禁用该功能。我们预计会在 v23.x 中测试该功能，查找潜在的回归问题并进行相应调整，然后在 Node.js 22.x 的 LTS 提升后的后续 semver-minor 版本中回溯该功能的解锁。</p><p>启用此功能后，Node.js 不会再抛出 <code>ERR_REQUIRE_ESM</code> 错误，即使 <code>require()</code> 被用于加载 ES 模块。不过，如果加载的 ES 模块或其依赖项包含顶级 <code>await</code>，可能会抛出 <code>ERR_REQUIRE_ASYNC_MODULE</code> 错误。当 <code>require()</code> 成功加载 ES 模块时，返回的对象将是一个 ES 模块命名空间对象，类似于 <code>import()</code> 返回的对象，可通过 <code>util.isModuleNamespaceObject()</code> 进行检查。</p><p>用户可以通过 <code>process.features.require_module</code> 检查当前 Node.js 实例中是否启用了 <code>require(esm)</code>。对于包，”module-sync” 导出条件可以作为检测当前 Node.js 实例中 <code>require(esm)</code> 支持的方式，允许同时使用 <code>require()</code> 和 <code>import</code> 来加载相同的原生 ES 模块。有关此功能的更多详情，请参阅文档。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;&lt;img src=&quot;https://images.nirvana.net.cn/static/images/images/10024246-f5e2631ac8f982e3.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2Awdb</summary>
      
    
    
    
    <category term="前端" scheme="https://www.nirvana.net.cn/categories/%E5%89%8D%E7%AB%AF/"/>
    
    
    <category term="node" scheme="https://www.nirvana.net.cn/tags/node/"/>
    
  </entry>
  
  <entry>
    <title>UI系统设计详解</title>
    <link href="https://www.nirvana.net.cn/2024/10227nsnnnnUUU.html"/>
    <id>https://www.nirvana.net.cn/2024/10227nsnnnnUUU.html</id>
    <published>2024-10-22T08:28:03.000Z</published>
    <updated>2024-10-27T08:45:58.396Z</updated>
    
    <content type="html"><![CDATA[<p><img src="https://images.nirvana.net.cn/static/images/images/10024246-b2542c07f9d7228f.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="></p><p>如果你曾经在项目之间复制粘贴 UI 代码，或是在大型应用程序中努力保持一致性，那么你并不孤独。这些是软件开发中的常见痛点，而设计系统正是为了解决这些问题而存在的。</p><p>在本文中，我们将解析设计系统的概念以及它的工作方式。我们会介绍设计系统的核心组件，讲解构建过程，并探讨一些有助于实现的工具。无论你只是对设计系统感到好奇，还是考虑实施一个，这篇文章都能为你提供一个全面的概述。</p><h3 id="什么是UI系统设计？"><a href="#什么是UI系统设计？" class="headerlink" title="什么是UI系统设计？"></a>什么是UI系统设计？</h3><p>UI系统设计不仅仅是一个样式指南或组件库。它是一个完整的标准、文档和可重用组件的集合，帮助团队构建更一致且高效的用户界面。<br><img src="https://images.nirvana.net.cn/static/images/images/10024246-6110816a90c0f3f2.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="><br>一个典型的设计系统通常包括以下内容：</p><ul><li><strong>可重用的 UI 组件</strong>：如按钮、表单、模态框和其他常见的 UI 元素。</li><li><strong>设计模式</strong>：针对常见用户体验问题的标准化解决方案。</li><li><strong>样式指南</strong>：关于字体排版、颜色使用、间距等视觉元素的文档。</li><li><strong>最佳实践</strong>：关于如何使用组件和应用设计原则的指南。</li><li><strong>设计 Tokens</strong>：用于存储视觉设计属性的变量。</li></ul><p>UI系统设计作为应用程序 UI 的唯一可靠来源，它弥合了设计与开发之间的鸿沟，确保所有人都在同一页面上协作。</p><h3 id="为什么开发者关心UI系统设计？"><a href="#为什么开发者关心UI系统设计？" class="headerlink" title="为什么开发者关心UI系统设计？"></a>为什么开发者关心UI系统设计？</h3><p>你可能会想，为什么要花时间去构建UI系统设计？以下是几个具体的好处：</p><ul><li><strong>一致性</strong>：不再出现组件在应用中略有差异的情况。设计系统确保按钮在所有使用的地方都具有相同的外观和行为。</li><li><strong>效率</strong>：不需要为每个项目从头编码相同的组件，可以重用预先构建和测试的组件，这可以显著加快开发速度。</li><li><strong>更好的团队合作</strong>：UI系统设计为设计师和开发者提供了一个共享的语言，这减少了沟通障碍并加快了交付过程。</li><li><strong>可扩展性</strong>：随着应用程序的增长，设计系统帮助保持一致性和可管理性。比起在大规模代码库中逐一查找并更新组件，更新设计系统中的单个组件要容易得多。</li><li><strong>质量控制</strong>：UI系统设计中的组件通常经过充分的测试和优化，减少了 bug，提升了最终产品的打磨度。</li><li><strong>无障碍访问</strong>：通过将无障碍标准融入UI系统设计，确保应用程序的各个部分都默认符合这些标准。</li><li><strong>品牌一致性</strong>：设计系统帮助在应用程序的所有部分，甚至是多个应用之间，保持一致的品牌形象。<h3 id="设计系统的构建模块"><a href="#设计系统的构建模块" class="headerlink" title="设计系统的构建模块"></a>设计系统的构建模块</h3>让我们来回顾一下UI系统设计的核心组件：</li></ul><p><img src="https://images.nirvana.net.cn/static/images/images/10024246-f0b4be7d30ebc3ef.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="></p><h5 id="模板库"><a href="#模板库" class="headerlink" title="模板库"></a>模板库</h5><p>模板库是解决常见设计问题的可重用方案的集合。它们不仅涉及视觉设计，还涵盖交互模式和用户体验的最佳实践。</p><p>例如，模板库可能包括：</p><ul><li>一个标准化的导航菜单结构</li><li>表单验证和错误处理的最佳实践</li><li>数据可视化的规范</li></ul><p>这些模板帮助在应用程序中创建一致的用户体验，尤其适用于应用中多处复杂交互。</p><h4 id="视觉元素"><a href="#视觉元素" class="headerlink" title="视觉元素"></a>视觉元素</h4><p>视觉元素构成了UI系统设计美学的基础。它们包括：</p><ul><li><strong>排版</strong>：不仅仅是选择字体，还包括字体大小、行高和排版比例的指导方针。</li><li><strong>色彩方案</strong>：不仅是一组颜色，还包括如何以及何时使用每种颜色的规则，可能包括主色、辅色以及文本颜色、背景颜色和强调颜色的使用规则。</li><li><strong>间距和布局</strong>：一致的间距对打造精致的 UI 至关重要，许多设计系统采用标准化的间距比例。</li><li><strong>图标</strong>：关于图标样式、大小和使用的规范。</li></ul><p>这些元素共同作用，形成应用程序的统一视觉语言。谷歌的 Material Design 就是一个关于如何定义和记录这些元素的全面示例。</p><h5 id="可重用组件"><a href="#可重用组件" class="headerlink" title="可重用组件"></a>可重用组件</h5><p>在任何设计系统的核心都是可重用组件，它们是 UI 的“乐高积木”：</p><ul><li>基本元素（按钮、输入框）</li><li>复杂小部件（日期选择器、模态框）</li><li>布局组件（网格、卡片）</li></ul><p>好的可重用组件应该具备以下特点：</p><ul><li><strong>模块化</strong>：它们可以独立工作。</li><li><strong>可定制化</strong>：它们能够适应不同的场景。</li><li><strong>文档齐全</strong>：它们附带清晰的使用指南。</li></ul><p>它们的作用在于：</p><ul><li><strong>一致性</strong>：每个人都使用相同的构建块。</li><li><strong>效率</strong>：无需为常见的 UI 元素重新设计。</li><li><strong>更易维护</strong>：更新一次，改动即可应用到所有地方。</li></ul><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><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><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 function_">Button</span> = (<span class="params">&#123; onClick, disabled, children, variant = <span class="string">&#x27;primary&#x27;</span> &#125;</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">className</span>=<span class="string">&#123;</span>`<span class="attr">button</span> <span class="attr">button--</span>$&#123;<span class="attr">variant</span>&#125;`&#125;</span></span></span><br><span class="line"><span class="tag"><span class="language-xml">    <span class="attr">onClick</span>=<span class="string">&#123;onClick&#125;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">    <span class="attr">disabled</span>=<span class="string">&#123;disabled&#125;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">  &gt;</span></span></span><br><span class="line"><span class="language-xml">    &#123;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></pre></td></tr></table></figure><p>记住，一个好的组件库应随着团队需求的变化不断进化。定期的审查和更新能保持组件的相关性和价值。</p><h3 id="构建自己的设计系统"><a href="#构建自己的设计系统" class="headerlink" title="构建自己的设计系统"></a>构建自己的设计系统</h3><p>创建设计系统是一个重要的工程，但从长远来看，它会带来巨大的回报。以下是更详细的构建过程：<br><img src="https://images.nirvana.net.cn/static/images/images/10024246-b797a236c6e83604.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="></p><h4 id="设计系统流程"><a href="#设计系统流程" class="headerlink" title="设计系统流程"></a>设计系统流程</h4><ol><li><strong>审计现有资源</strong>：首先分析你现有的 UI，寻找模式和不一致之处。这有助于你确定需要哪些组件以及可以在哪些方面进行标准化。</li><li><strong>定义设计原则</strong>：哪些核心价值观应该指导你的设计决策？这些原则可能包括清晰性、高效性或无障碍性等。</li><li><strong>创建视觉元素</strong>：开发颜色方案、排版、间距比例等基础元素，这些将为组件的设计提供依据。</li><li><strong>构建组件库</strong>：从最常用的组件开始。为每个组件考虑不同状态（悬停、激活、禁用）和变体（大小、颜色）。</li><li><strong>记录一切</strong>：清晰且全面的文档至关重要。包含使用指南、代码示例以及每个组件的使用场景说明。</li><li><strong>迭代</strong>：设计系统从未真正完成。收集团队和用户的反馈，不断优化和扩展你的系统。</li></ol><p>记住，你不需要一次构建所有内容。可以从最重要的元素开始，随着时间推移逐步扩展。</p><h3 id="无障碍设计很重要"><a href="#无障碍设计很重要" class="headerlink" title="无障碍设计很重要"></a>无障碍设计很重要</h3><p>无障碍设计不应是事后的考虑。通过将无障碍性融入设计系统，确保应用程序的各个部分对尽可能多的人都具有可用性。</p><p>你的设计系统应当：</p><ul><li>为每个组件包含具体的无障碍设计指南</li><li>确保颜色对比度符合 WCAG 标准</li><li>提供键盘导航支持</li><li>包含编写无障碍内容的指南</li></ul><p>使应用程序具有无障碍性不仅仅是合规性问题，更是为所有人创造更好用户体验的关键。</p><h3 id="团队合作，梦想成真"><a href="#团队合作，梦想成真" class="headerlink" title="团队合作，梦想成真"></a>团队合作，梦想成真</h3><p>设计系统是一项协作的工作，旨在弥合设计和开发之间的差距。为了成功，它应当：</p><ul><li>作为所有团队成员都能理解的共享语言</li><li>促进设计师、开发者和其他利益相关者之间的协作</li><li>接受整个团队的贡献和反馈</li></ul><p>定期召开会议讨论设计系统，收集反馈，计划更新。这有助于确保系统对所有团队成员都保持相关性和实用性。</p><h3 id="推动设计系统的采用"><a href="#推动设计系统的采用" class="headerlink" title="推动设计系统的采用"></a>推动设计系统的采用</h3><p>实施设计系统是一个重要的任务。以下是更详细的实施策略：<br><img src="https://images.nirvana.net.cn/static/images/images/10024246-e7a4e40af76c9516.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="></p><ul><li><strong>从小处着手</strong>：从几个关键组件开始，逐步扩展。这可以让你快速优化流程并展示设计系统的价值。</li><li><strong>让所有人参与其中</strong>：确保所有利益相关者都理解设计系统的价值。这可以通过演示、研讨会或一对一的讨论来实现。</li><li><strong>培训团队</strong>：提供资源和支持，帮助所有人有效使用系统。这可能包括文档、教程和实践培训课程。</li><li><strong>监控采用情况</strong>：跟踪设计系统的使用情况，并解决任何挑战。像分析工具和代码检查工具可以对此提供帮助。</li><li><strong>迭代和改进</strong>：不断收集反馈并优化系统。根据实际使用情况和不断变化的需求准备做出调整。</li></ul><p>实施设计系统是一个持续的过程，需时刻准备好适应和改进。</p><h3 id="Builder-与设计系统"><a href="#Builder-与设计系统" class="headerlink" title="Builder 与设计系统"></a>Builder 与设计系统</h3><p>设计系统承诺带来一致性和效率，但有效实施它们可能具有挑战性。Builder 通过创新解决方案解决了常见的痛点：</p><h4 id="Builder-和设计系统"><a href="#Builder-和设计系统" class="headerlink" title="Builder 和设计系统"></a>Builder 和设计系统</h4><p>设计系统承诺一致性和效率，但有效地实现它们可能具有挑战性。Builder通过创新的解决方案解决常见的痛点：<br><img src="https://images.nirvana.net.cn/static/images/images/10024246-1a69f6a04cbe376a.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="></p><ul><li><strong>推广与采用</strong>：Builder 的可视化编辑器通过弥合设计与开发之间的差距，使得集成变得无缝。</li><li><strong>多框架支持</strong>：Mitosis 是一个开源工具，允许你编写一次组件并将其编译到多个框架中。</li><li><strong>设计与代码同步</strong>：通过组件映射和自动化代码生成等功能，帮助保持设计与实现之间的一致性。</li><li><strong>一致性维护</strong>：设计系统更改的自动传播可以确保代码库的统一性，同时允许系统随时间演变。</li></ul><p>通过解决这些问题，Builder.io 的平台简化了从设计到生产的整个过程，使设计系统对各类规模的团队更加易于使用和高效。</p><h3 id="利用设计系统的工具"><a href="#利用设计系统的工具" class="headerlink" title="利用设计系统的工具"></a>利用设计系统的工具</h3><p>有许多工具可以帮助创建和管理设计系统：</p><ul><li><strong>Figma</strong> 用于协作设计：允许设计师和开发者实时协作。</li><li><strong>Storybook</strong> 用于构建组件库：提供一个隔离的环境来开发和测试 UI 组件。</li><li><strong>Zeroheight</strong> 用于文档编写：帮助创建全面且易于导航的设计系统文档。</li></ul><p>这些工具可以简化设计系统的创建、实施和维护过程。选择与现有工作流程和技术栈紧密集成的工具，以提高工作效率。</p><h3 id="保持设计系统的活力"><a href="#保持设计系统的活力" class="headerlink" title="保持设计系统的活力"></a>保持设计系统的活力</h3><p>设计系统从来不会真正“完成”。为了保持其相关性和有效性，你可以：</p><ul><li><strong>定期审查和更新组件</strong>：随着应用程序的发展，设计系统也应该随之演变。</li><li><strong>征求用户和团队成员的反馈</strong>：他们每天使用该系统，能够提供宝贵的见解。</li><li><strong>紧跟设计趋势和技术变化</strong>：虽然不需要追随每个趋势，但保持系统的现代性和相关性非常重要。</li><li><strong>记录变更并通知团队更新</strong>：确保每个人都知道新的功能或现有组件的更改。</li></ul><p>将设计系统视为一个“活文档”，确保它能够持续满足组织和用户不断变化的需求。</p><h3 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h3><p>设计系统是创建一致、高效且可扩展用户界面的强大工具。虽然它需要在初期投入一定的时间和资源，但从长远来看，改进的一致性、效率和协作使它对于任何重视数字体验的组织都物有所值。</p><p>记住，一个成功的设计系统会随着组织的发展而不断进化，不断适应数字领域中新的挑战和机遇。通过拥抱设计系统的理念，你不仅是在改进当前的产品，还在为未来更高效、更有效的开发打下基础。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;&lt;img src=&quot;https://images.nirvana.net.cn/static/images/images/10024246-b2542c07f9d7228f.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2Awdb</summary>
      
    
    
    
    <category term="前端" scheme="https://www.nirvana.net.cn/categories/%E5%89%8D%E7%AB%AF/"/>
    
    
    <category term="web" scheme="https://www.nirvana.net.cn/tags/web/"/>
    
  </entry>
  
  <entry>
    <title>React18 之 Suspense</title>
    <link href="https://www.nirvana.net.cn/2024/1021sxYhVVVmmm.html"/>
    <id>https://www.nirvana.net.cn/2024/1021sxYhVVVmmm.html</id>
    <published>2024-10-21T08:28:03.000Z</published>
    <updated>2024-10-27T08:45:58.458Z</updated>
    
    <content type="html"><![CDATA[<p><img src="https://images.nirvana.net.cn/static/images/images/10024246-227f4b3ce4043508.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="></p><h2 id="Suspense"><a href="#Suspense" class="headerlink" title="Suspense"></a>Suspense</h2><p><code>Suspense</code> 组件我们并不陌生，中文名可以理解为<code>暂停or悬停</code>  , 在 React16 中我们通常在路由懒加载中配合 Lazy 组件一起使用 ，当然这也是官方早起版本推荐的唯一用法。</p><p>那它暂停了什么？ 进行异步网络请求，然后再拿到请求后的数据进行渲染是很常见的需求，但这不可避免的需要先渲染一次没有数据的页面，数据返回后再去重新渲染。so , 我们想要暂停的就是第一次的无数据渲染。</p><p>通常我们在没有使用<code>Suspense</code> 时一般采用下面这种写法, 通过一个<code>isLoading</code>状态来显示加载中或数据。这样代码是不会有任何问题，但我们需要手动去维护一个<code>isLoading</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">if (isLoading) &#123;</span><br><span class="line">  return &lt;Spinner /&gt;;</span><br><span class="line">&#125;</span><br><span class="line">return &lt;MyComponent data=&#123;data&#125; /&gt;;</span><br></pre></td></tr></table></figure><p>当我们使用<code>Suspense</code> 后，使用方法会变为如下, 我们只需将进行异步数据获取的组件进行包裹，并将加载中组件通过<code>fallback</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">return (</span><br><span class="line">  &lt;Suspense fallback=&#123;&lt;Spinner /&gt;&#125;&gt;</span><br><span class="line">    &lt;MyComponent /&gt;</span><br><span class="line">  &lt;/Suspense&gt;</span><br><span class="line">);</span><br></pre></td></tr></table></figure><p>那 React 是如何知道该显示<code>MyComponent</code>还是<code>Spinner</code>的？</p><p>答案就在于<code>MyComponent</code>内部进行<code>fetch</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><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">export const App = () =&gt; &#123;</span><br><span class="line">  return (</span><br><span class="line">    &lt;div&gt;</span><br><span class="line">      &lt;Suspense fallback=&#123;&lt;Spining /&gt;&#125;&gt;</span><br><span class="line">        &lt;MyComponent /&gt;</span><br><span class="line">      &lt;/Suspense&gt;</span><br><span class="line">    &lt;/div&gt;</span><br><span class="line">  );</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line">function Spining() &#123;</span><br><span class="line">  return &lt;p&gt;loading...&lt;/p&gt;;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">let data = null;</span><br><span class="line"></span><br><span class="line">function MyComponent() &#123;</span><br><span class="line">  if (!data) &#123;</span><br><span class="line">    throw new Promise((resolve) =&gt; &#123;</span><br><span class="line">      setTimeout(() =&gt; &#123;</span><br><span class="line">        data = &#x27;kunkun&#x27;;</span><br><span class="line">        resolve(true);</span><br><span class="line">      &#125;, 2000);</span><br><span class="line">    &#125;);</span><br><span class="line">  &#125;</span><br><span class="line">  return (</span><br><span class="line">    &lt;p&gt;</span><br><span class="line">      My Component, data is &#123;data&#125;</span><br><span class="line">    &lt;/p&gt;</span><br><span class="line">  );</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>Suspense</code>是根据捕获子组件内的异常来实现决定展示哪个组件的。这有点类似于<code>ErrorBoundary</code> ，不过<code>ErrorBoundary</code>是捕获 Error 时就展示回退组件，而<code>Suspense</code> 捕获到的 Error 需要是一个<code>Promise</code>对象（并非必须是 Promise 类型，thenable 的都可以）。</p><p>我们知道 Promise 有三个状态，<code>pending</code>、<code>fullfilled</code>、<code>rejected</code> ，当我们进行远程数据获取时，会创建一个<code>Promise</code>，我们需要直接将这个<code>Promise</code> 作为<code>Error</code>进行抛出，由 Suspense 进行捕获，捕获后对该<code>thenable</code>对象的<code>then</code>方法进行回调注册<code>thenable.then(retry)</code> , 而 retry 方法就会开始一个调度任务进行更新，后面会详细讲。<br><img src="https://images.nirvana.net.cn/static/images/images/10024246-909aa87c9433947d.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="><br>知道了大致原理，这时还需要对我们的<code>fetcher</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><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></pre></td><td class="code"><pre><span class="line">// MyComponent.tsx</span><br><span class="line">const getList = wrapPromise(fetcher(&#x27;http://api/getList&#x27;));</span><br><span class="line"></span><br><span class="line">export function MyComponent() &#123;</span><br><span class="line">  const data = getList.read();</span><br><span class="line"></span><br><span class="line">  return (</span><br><span class="line">    &lt;ul&gt;</span><br><span class="line">      &#123;data?.map((item) =&gt; (</span><br><span class="line">        &lt;li&gt;&#123;item.name&#125;&lt;/li&gt;</span><br><span class="line">      ))&#125;</span><br><span class="line">    &lt;/ul&gt;</span><br><span class="line">  );</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">function fetcher(url) &#123;</span><br><span class="line">  return new Promise((resove, reject) =&gt; &#123;</span><br><span class="line">    setTimeout(() =&gt; &#123;</span><br><span class="line">      resove([&#123; name: &#x27;This is Item1&#x27; &#125;, &#123; name: &#x27;This is Item2&#x27; &#125;]);</span><br><span class="line">    &#125;, 1000);</span><br><span class="line">  &#125;);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">// Promise包裹函数，用来满足Suspense的要求，在初始化时默认就会throw出去</span><br><span class="line">function wrapPromise(promise) &#123;</span><br><span class="line">  let status = &#x27;pending&#x27;;</span><br><span class="line">  let response;</span><br><span class="line"></span><br><span class="line">  const suspend = promise.then(</span><br><span class="line">    (res) =&gt; &#123;</span><br><span class="line">      status = &#x27;success&#x27;;</span><br><span class="line">      response = res;</span><br><span class="line">    &#125;,</span><br><span class="line">    (err) =&gt; &#123;</span><br><span class="line">      status = &#x27;error&#x27;;</span><br><span class="line">      response = err;</span><br><span class="line">    &#125;</span><br><span class="line">  );</span><br><span class="line">  const read = () =&gt; &#123;</span><br><span class="line">    switch (status) &#123;</span><br><span class="line">      case &#x27;pending&#x27;:</span><br><span class="line">        throw suspend;</span><br><span class="line">      default:</span><br><span class="line">        return response;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;;</span><br><span class="line"></span><br><span class="line">  return &#123; read &#125;;</span><br></pre></td></tr></table></figure><p>从上述代码我们可以注意到，通过<code>const data = getList.read()</code> 这种同步的方式我们就能拿到数据了。 <code>注意： 上面这种写法并非一种范式，目前官方也没有给出推荐的写法</code><br>为了与<code>Suspense</code>配合，则我们的请求可能会变得很<code>不优雅</code> ，官方推荐是直接让我们使用第三方框架提供的能力使用<code>Suspense</code>请求数据，如 <code>useSWR</code> 等<br>下面时<code>useSWR</code>的示例，简明了很多，并且对于<code>Profile</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></pre></td><td class="code"><pre><span class="line">import &#123; Suspense &#125; from &#x27;react&#x27;</span><br><span class="line">import useSWR from &#x27;swr&#x27;</span><br><span class="line"></span><br><span class="line">function Profile () &#123;</span><br><span class="line">  const &#123; data &#125; = useSWR(&#x27;/api/user&#x27;, fetcher, &#123; suspense: true &#125;)</span><br><span class="line">  return &lt;div&gt;hello, &#123;data.name&#125;&lt;/div&gt;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">function App () &#123;</span><br><span class="line">  return (</span><br><span class="line">    &lt;Suspense fallback=&#123;&lt;div&gt;loading...&lt;/div&gt;&#125;&gt;</span><br><span class="line">      &lt;Profile/&gt;</span><br><span class="line">    &lt;/Suspense&gt;</span><br><span class="line">  )</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>Suspense</code>的另一种用法就是与<code>懒加载lazy组件</code>配合使用，在完成加载前展示Loading</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">&lt;Suspense fallback=&#123;&lt;GlobalLoading /&gt;&#125;&gt;</span><br><span class="line">   &#123;lazy(() =&gt; import(&#x27;xxx/xxx.tsx&#x27;))&#125;</span><br><span class="line">&lt;/Suspense&gt;</span><br></pre></td></tr></table></figure><p>由此得出，通过<code>lazy</code>返回的组件也应该包裹一层类似如上的 Promise，我们看看 lazy 内部是如何实现的。<br>其中<code>ctor</code>就是我们传入的<code>() =&gt; import(&#39;xxx/xxx.tsx&#39;)</code>, 执行<code>lazy</code>也只是帮我们封装了层数据结构。<a href="https://link.segmentfault.com/?enc=Yjl1ITQACkFd1mJUzLrvwQ==.DZaIYNARqlA3Iwh0/nqDmzgRQTcvK2RU10G6BHpY9oLEmg+caNdn8yuXlfUZaz0ZwZxRooZAGEuWKT5pAKjdVvFOnGgTdoWTOw0CIFts+2ip5bduhmvyRUkP9/oBBJNEUbC2PyVy0+izw2u35jx0WgCLkcaYIJiij7OjgDw44sU=">ReactLazy.js</a></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">export function lazy&lt;T&gt;(</span><br><span class="line">  ctor: () =&gt; Thenable&lt;&#123;default: T, ...&#125;&gt;,</span><br><span class="line">): LazyComponent&lt;T, Payload&lt;T&gt;&gt; &#123;</span><br><span class="line">  const payload: Payload&lt;T&gt; = &#123;</span><br><span class="line">    // We use these fields to store the result.</span><br><span class="line">    _status: Uninitialized,</span><br><span class="line">    _result: ctor,</span><br><span class="line">  &#125;;</span><br><span class="line">  const lazyType: LazyComponent&lt;T, Payload&lt;T&gt;&gt; = &#123;</span><br><span class="line">    $typeof: REACT_LAZY_TYPE,</span><br><span class="line">    _payload: payload,</span><br><span class="line">    _init: lazyInitializer,</span><br><span class="line">  &#125;;</span><br><span class="line">  return lazyType;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>React 会在<code>Reconciler</code>过程中去实际执行，在协调的<code>render</code>阶段<code>beginWork</code>中可以看到对lazy单独处理的逻辑。 <a href="https://link.segmentfault.com/?enc=HQMz1BBddNWNvmPfNGx30w==.amWCnPlZYwGyTaur6trsi83kv7labdHqJ0g9k+Ib9rB4bgqRiUrn8TpGisZj5HKjtkOzzbXnE/LvsHOao5AX1JCJmAjCgUWHeOrHajvlNuZ7ZNX5040ACa1Ui9rWAJAxkIj63ZWAy86HYP6kGsXo14PSiSmXKR6hhSzxos0miVxDnaHUOLIMRSCBd1qXln75">ReactFiberBeginWork.js</a></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 mountLazyComponent( _current,</span><br><span class="line">  workInProgress,</span><br><span class="line">  elementType,</span><br><span class="line">  renderLanes, ) &#123;</span><br><span class="line">  const props = workInProgress.pendingProps;</span><br><span class="line">  const lazyComponent: LazyComponentType&lt;any, any&gt; = elementType;</span><br><span class="line">  const payload = lazyComponent._payload;</span><br><span class="line">  const init = lazyComponent._init;</span><br><span class="line">    // 在此处初始化lazy</span><br><span class="line">  let Component = init(payload);</span><br><span class="line">    // 下略</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>那我们再来看看<code>init</code>干了啥，也就是封装前的<code>lazyInitializer</code>方法，整体跟我们之前实现的 fetch 封装是一样的。<br><a href="https://link.segmentfault.com/?enc=ES80XH2/vOt4/zONP0Gmkg==.cXIqKIeSgdPEhlMbkb1CWKPn/9OlOybexd2T6cSonnag7qH6QrI2/M1LJiJgk+tAPoL2clTZArjiptztVBAHWetCVd2O7x5SEPUeuHX5WKwUSb7FTOtSwVhEu79cWZCvszNJZnm56pwsR71Db3hXA7Q8QikvTZ67u8ep32gQ/TY=">ReactLazy.js</a></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><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">function lazyInitializer&lt;T&gt;(payload: Payload&lt;T&gt;): T &#123;</span><br><span class="line">  if (payload._status === Uninitialized) &#123;</span><br><span class="line">    const ctor = payload._result;</span><br><span class="line">    // 这时候开始进行远程模块的导入</span><br><span class="line">    const thenable = ctor();</span><br><span class="line">    thenable.then(</span><br><span class="line">      moduleObject =&gt; &#123;</span><br><span class="line">        if (payload._status === Pending || payload._status === Uninitialized) &#123;</span><br><span class="line">          // Transition to the next state.</span><br><span class="line">          const resolved: ResolvedPayload&lt;T&gt; = (payload: any);</span><br><span class="line">          resolved._status = Resolved;</span><br><span class="line">          resolved._result = moduleObject;</span><br><span class="line">        &#125;</span><br><span class="line">      &#125;,</span><br><span class="line">      error =&gt; &#123;</span><br><span class="line">        if (payload._status === Pending || payload._status === Uninitialized) &#123;</span><br><span class="line">          // Transition to the next state.</span><br><span class="line">          const rejected: RejectedPayload = (payload: any);</span><br><span class="line">          rejected._status = Rejected;</span><br><span class="line">          rejected._result = error;</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">  if (payload._status === Resolved) &#123;</span><br><span class="line">    const moduleObject = payload._result;</span><br><span class="line">    &#125;</span><br><span class="line">    return moduleObject.default;</span><br><span class="line">  &#125; else &#123;</span><br><span class="line">    // 第一次执行肯定会先抛出异常</span><br><span class="line">    throw payload._result;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="Suspense-底层是如何实现的？"><a href="#Suspense-底层是如何实现的？" class="headerlink" title="Suspense 底层是如何实现的？"></a>Suspense 底层是如何实现的？</h3><p>其底层细节非常之多，在开始之前，我们先回顾下 React 的大致架构</p><p><strong>Scheduler</strong>: 用于调度任务，我们每次<code>setState</code>可以看成是往其中塞入一个<code>Task</code>，由<code>Scheduler</code>内部的优先级策略进行判断何时调度运行该<code>Task</code></p><p><strong>Reconciler</strong>: 协调器，进行 diff 算法，构建 fiber 树</p><p><strong>Renderer</strong>: 渲染器，将 fiber 渲染成 dom 节点</p><p>Fiber 树的结构, 在 reconciler 阶段，采用<code>深度优先</code>的方式进行遍历，往下递即调用<code>beginWork</code>的过程，往上回溯即调用<code>ComplteWork</code>的过程<br><img src="https://images.nirvana.net.cn/static/images/images/10024246-f00481d794e690ab.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="></p><p>我们先直接进入<code>Reconciler</code> 中分析下<code>Suspense</code>的<code>fiber</code>节点是如何被创建的<br><a href="https://link.segmentfault.com/?enc=uRuFErnv2d0CAYS76fJpsQ==.z//vG0gAVL3t9o4i2zvweb1DfMwN3UuJ5NqgBEswbLHXdMr+8ET1LvL9CIG2yBsYZ5pauFnOXLjQk5ujo1sWBjW71v5oZ7I3iYE9G0e9I8zuBs9HOL+MXS1u774DwObe2WOHvz0Qfxv1/6xxCs8Sq8z55aXAU0F9wdUkxCU3RKHSV8iDG2Md0njyDBeaXuFU">beginWork</a></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">function beginWork( current: Fiber | null,</span><br><span class="line">  workInProgress: Fiber,</span><br><span class="line">  renderLanes: Lanes, ): Fiber | null &#123;</span><br><span class="line">    switch (workInProgress.tag) &#123;</span><br><span class="line">        case HostText:</span><br><span class="line">      return updateHostText(current, workInProgress);</span><br><span class="line">    case SuspenseComponent:</span><br><span class="line">      return updateSuspenseComponent(current, workInProgress, renderLanes);</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> 在<code>beginWork</code>中会根据<code>**不同的组件类型**</code>执行不同的创建方法， 而<code>Suspense</code> 对应的会进入到<a href="https://link.segmentfault.com/?enc=ii11w40ojDTCvdWyWMVLgQ==.D+jgC+f5XAV7WxBkdTqm/DGlthEl/GV3ggzFzfwZk3nV3Lkz8KVxe9P5Dx57owbiqhdEjtNOFHl3VgJqWiWUjT9JkP9i880Y4T+YG8rmj/5cGCxDMA384yWuHjMmBDwbNBmQ3v6CoYFjbKLrxLfgrQXAPBzDdjVIcKC6Ubj2igJtt2eyiEmCSg8mYJ5wLVoU">updateSuspenseComponent</a></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><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">function updateSuspenseComponent(current, workInProgress, renderLanes) &#123;</span><br><span class="line">  const nextProps = workInProgress.pendingProps;</span><br><span class="line"></span><br><span class="line">  let showFallback = false;</span><br><span class="line">  // 标识该Suspense是否已经捕获过子组件的异常了</span><br><span class="line">  const didSuspend = (workInProgress.flags &amp; DidCapture) !== NoFlags;</span><br><span class="line"></span><br><span class="line">  if (</span><br><span class="line">    didSuspend</span><br><span class="line">  ) &#123;</span><br><span class="line">    showFallback = true;</span><br><span class="line">    workInProgress.flags &amp;= ~DidCapture;</span><br><span class="line">  &#125; </span><br><span class="line"></span><br><span class="line">  // 第一次组件加载</span><br><span class="line">  if (current === null) &#123;</span><br><span class="line"></span><br><span class="line">    const nextPrimaryChildren = nextProps.children;</span><br><span class="line">    const nextFallbackChildren = nextProps.fallback;</span><br><span class="line"></span><br><span class="line">    // 第一次默认不展示fallback，因为要先走到children后才会产生异常</span><br><span class="line">    if (showFallback) &#123;</span><br><span class="line">      const fallbackFragment = mountSuspenseFallbackChildren(</span><br><span class="line">        workInProgress,</span><br><span class="line">        nextPrimaryChildren,</span><br><span class="line">        nextFallbackChildren,</span><br><span class="line">        renderLanes,</span><br><span class="line">      );</span><br><span class="line">      const primaryChildFragment: Fiber = (workInProgress.child: any);</span><br><span class="line">      primaryChildFragment.memoizedState = mountSuspenseOffscreenState(</span><br><span class="line">        renderLanes,</span><br><span class="line">      );</span><br><span class="line"></span><br><span class="line">      return fallbackFragment;</span><br><span class="line">    &#125; </span><br><span class="line">     else &#123;</span><br><span class="line">      return mountSuspensePrimaryChildren(</span><br><span class="line">        workInProgress,</span><br><span class="line">        nextPrimaryChildren,</span><br><span class="line">        renderLanes,</span><br><span class="line">      );</span><br><span class="line">    &#125;</span><br><span class="line">  &#125; else &#123;</span><br><span class="line">    // 如果是更新，操作差不多，此处略</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li>  第一次<code>updateSuspenseComponent</code> 时 ，我们会把<code>mountSuspensePrimaryChildren</code> 的结果作为下一个需要创建的<code>fiber</code> ， 因为需要先去触发异常。</li><li>  实际上<code>mountSuspensePrimaryChildren</code>  会为我们的<code>PrimaryChildren</code> 在包上一层<code>OffscreenFiber</code> 。</li></ul><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">function mountSuspensePrimaryChildren(</span><br><span class="line"> workInProgress,</span><br><span class="line"> primaryChildren,</span><br><span class="line"> renderLanes,</span><br><span class="line">) &#123;</span><br><span class="line">  const mode = workInProgress.mode;</span><br><span class="line"> const primaryChildProps: OffscreenProps = &#123;</span><br><span class="line"> mode: &#x27;visible&#x27;,</span><br><span class="line"> children: primaryChildren,</span><br><span class="line">  &#125;;</span><br><span class="line">  const primaryChildFragment = mountWorkInProgressOffscreenFiber(</span><br><span class="line"> primaryChildProps,</span><br><span class="line"> mode,</span><br><span class="line"> renderLanes,</span><br><span class="line">  );</span><br><span class="line">  primaryChildFragment.return = workInProgress;</span><br><span class="line">  workInProgress.child = primaryChildFragment;</span><br><span class="line">  return primaryChildFragment;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="什么是OffscreenFiber-Component-？"><a href="#什么是OffscreenFiber-Component-？" class="headerlink" title="什么是OffscreenFiber/Component  ？"></a>什么是<code>OffscreenFiber/Component</code>  ？</h3><p>通过其需要的 mode 参数值，我们可以大胆的猜测，应该是一个能控制是否显示子组件的组件，如果<code>hidden</code>，则会通过 CSS 样式隐藏子元素。<br><img src="https://images.nirvana.net.cn/static/images/images/10024246-8c8bb49ba6ed18ab.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="><br>在这之后的 Fiber 树结构<br><img src="https://images.nirvana.net.cn/static/images/images/10024246-da3e4b20321b9540.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="><br>当我们向下执行到<code>MyComponent</code> 时，由于抛出了错误，当前的<code>reconciler</code>阶段会被暂停<br>让我们再回到 Reconciler 阶段的起始点可以看到有<code>Catch</code>语句。<a href="https://link.segmentfault.com/?enc=S4jsOEVkoDMPaRk+eRGXpw==.PiTzLiIdkhpghLAyL0Ufkyp0DMYjAbA/Ixp5hhFkm/yTs5ZFfVdRq/8ET07RB+SFKsKrRUoMVxmXohekCDUawmJ98zwxh2L+SYjzZLsLW55Hdf1la5KTtAwJQfHQsVpncurTO67iOtnOQ9KZpy+Y68evsQ3Uc8YYWpQVZWcQJXJrc5BxJkG4HxXZNBdrfZwU">renderRootConcurrent</a></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></pre></td><td class="code"><pre><span class="line">function renderRootConcurrent(root: FiberRoot, lanes: Lanes) &#123;</span><br><span class="line"> // 省略..</span><br><span class="line">  do &#123;</span><br><span class="line">    try &#123;</span><br><span class="line">      workLoopConcurrent();</span><br><span class="line">      break;</span><br><span class="line">    &#125; catch (thrownValue) &#123;</span><br><span class="line">      handleError(root, thrownValue);</span><br><span class="line">    &#125;</span><br><span class="line">  &#125; while (true);</span><br><span class="line"> // 省略..</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">performConcurrentWorkOnRoot(root, didTimeout) &#123;</span><br><span class="line">    // 省略..</span><br><span class="line">    let exitStatus = shouldTimeSlice</span><br><span class="line">    ? renderRootConcurrent(root, lanes)</span><br><span class="line">    : renderRootSync(root, lanes);</span><br><span class="line">  // 省略..</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>我们再看看错误处理函数<code>handleError</code>中做了些什么  <a href="https://link.segmentfault.com/?enc=5fSpc/XBF4h1hGFtY7nCnw==.fNHgqwmG4VQ1OuGNS8zJCikwUcdl5+ejS5PgdAAvSirR4XO0dIJc5LbsFR+PzHdGxV7HwrTMQD5Yv7P4ySoN7PIl0sQtkL3Q1gpwoOLGrZCD8W+Jd49lDMtUjhLISWQNT7/y8V7Y7masj08OOjgAtRZeSSQO/sSRRrxN/8VxILG4HZSB//Dgs5qYBv7wCESv">handleError</a></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><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">function handleError(root, thrownValue): void &#123;</span><br><span class="line">    // 这时的workInProgress指向MyComponent</span><br><span class="line">  let erroredWork = workInProgress;</span><br><span class="line">  try &#123;</span><br><span class="line">    throwException(</span><br><span class="line">      root,</span><br><span class="line">      erroredWork.return,</span><br><span class="line">      erroredWork,</span><br><span class="line">      thrownValue,</span><br><span class="line">      workInProgressRootRenderLanes,</span><br><span class="line">    );</span><br><span class="line">    completeUnitOfWork(erroredWork);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">function throwException(root: FiberRoot, returnFiber: Fiber, sourceFiber: Fiber, value: mixed, rootRenderLanes: Lanes) </span><br><span class="line">&#123;</span><br><span class="line">  // 给MyComponent打上未完成标识</span><br><span class="line">  sourceFiber.flags |= Incomplete;</span><br><span class="line"></span><br><span class="line">  if (</span><br><span class="line">    value !== null &amp;&amp;</span><br><span class="line">    typeof value === &#x27;object&#x27; &amp;&amp;</span><br><span class="line">    typeof value.then === &#x27;function&#x27;</span><br><span class="line">  ) &#123;</span><br><span class="line">    // wakeable就是我们抛出的Promise</span><br><span class="line">    const wakeable: Wakeable = (value: any);</span><br><span class="line"></span><br><span class="line">    // 向上找到第一个Suspense边界</span><br><span class="line">    const suspenseBoundary = getNearestSuspenseBoundaryToCapture(returnFiber);</span><br><span class="line">    if (suspenseBoundary !== null) &#123;</span><br><span class="line">      // 打上标识</span><br><span class="line">      suspenseBoundary.flags &amp;= ~ForceClientRender;</span><br><span class="line">      suspenseBoundary.flags |= ShouldCapture;</span><br><span class="line">      // 注册监听器</span><br><span class="line">            attachRetryListener(suspenseBoundary, root, wakeable, rootRenderLanes);</span><br><span class="line">            return;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>主要做了三件事</p><ul><li>  给抛出错误的组件打上<code>Incomplete</code>标识</li><li>  如果捕获的错误是 thenable 类型，则认定为是 Suspense 的子组件，向上找到最接近的一个<code>Suspense</code> 边界，并打上<code>ShouldCapture</code> 标识</li><li>  执行<code>attachRetryListener</code> 对 Promise 错误监听，当状态改变后开启一个调度任务重新渲染 Suspense</li></ul><p>在错误处理的事情做完后，就不应该再往下递了，开始调用<code>completeUnitOfWork</code>往上归， 这时由于我们给 MyComponent 组件打上了<code>Incomplete</code> 标识，这个标识表示由于异常等原因渲染被搁置，那我们是不是就要开始往上找能够处理这个异常的组件？</p><p>我们再看看<code>completeUnitOfWork</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></pre></td><td class="code"><pre><span class="line">function completeUnitOfWork(unitOfWork: Fiber): void &#123;</span><br><span class="line"> // 大致逻辑</span><br><span class="line">  let completedWork = unitOfWork;</span><br><span class="line">  if ((completedWork.flags &amp; Incomplete) !== NoFlags) &#123;</span><br><span class="line">      const next = unwindWork(current, completedWork, subtreeRenderLanes);</span><br><span class="line">            if (next) &#123;</span><br><span class="line">                    workInProgress = next;</span><br><span class="line">                    return</span><br><span class="line">            &#125;</span><br><span class="line">            // 给父节点打上Incomplete标记</span><br><span class="line">            if (returnFiber !== null) &#123;</span><br><span class="line">              returnFiber.flags |= Incomplete;</span><br><span class="line">              returnFiber.subtreeFlags = NoFlags;</span><br><span class="line">              returnFiber.deletions = null;</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>Incomplete</code> 标识的组件都会进入<code>unwindWork</code>流程 ， 并一直将祖先节点打上<code>Incomplete</code> 标识，直到<code>unwindWork</code> 中找到一个能处理异常的边界组件，也就<code>ClassComponent</code>, <code>SuspenseComponent</code> , 会去掉<code>ShouldCapture</code>标识，加上<code>DidCapture</code>标识</p><p>这时，对于<code>Suspense</code>来说需要的<code>DidCapture</code>已经拿到了，下面就是重新从<code>Suspense</code> 开始走一遍<code>beginWork</code>流程</p><p>再次回到 Suspense 组件, 这时由于有了<code>DidCapture</code> 标识，则展示<code>fallback</code><br>对于<code>fallback</code>组件的<code>fiber</code>节点是通过<code>mountSuspenseFallbackChildren</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><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">function mountSuspenseFallbackChildren(</span><br><span class="line"> workInProgress,</span><br><span class="line"> primaryChildren,</span><br><span class="line"> fallbackChildren,</span><br><span class="line"> renderLanes,</span><br><span class="line">) &#123;</span><br><span class="line"> const primaryChildProps: OffscreenProps = &#123;</span><br><span class="line"> mode: &#x27;hidden&#x27;,</span><br><span class="line"> children: primaryChildren,</span><br><span class="line">  &#125;;</span><br><span class="line"></span><br><span class="line">  let primaryChildFragment = mountWorkInProgressOffscreenFiber(</span><br><span class="line"> primaryChildProps,</span><br><span class="line"> mode,</span><br><span class="line"> NoLanes,</span><br><span class="line">    );</span><br><span class="line">  let fallbackChildFragment = createFiberFromFragment(</span><br><span class="line"> fallbackChildren,</span><br><span class="line"> mode,</span><br><span class="line"> renderLanes,</span><br><span class="line"> null,</span><br><span class="line">    );</span><br><span class="line"></span><br><span class="line">  primaryChildFragment.return = workInProgress;</span><br><span class="line">  fallbackChildFragment.return = workInProgress;</span><br><span class="line">  primaryChildFragment.sibling = fallbackChildFragment;</span><br><span class="line">  workInProgress.child = primaryChildFragment;</span><br><span class="line">  return fallbackChildFragment;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>它主要做了三件事</p><ul><li>  将<code>PrimaryChild</code> 即<code>Offscreen</code>组件通过css隐藏</li><li>  将<code>fallback</code>组件又包了层<code>Fragment</code> 返回</li><li>  将<code>fallbackChild</code> 作为<code>sibling</code>链接至<code>PrimaryChild</code></li></ul><p><img src="https://images.nirvana.net.cn/static/images/images/10024246-d142c61614773001.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="></p><p>到这时渲染 fallback 的 fiber 树已经基本构建完了，之后进入<code>commit</code>阶段从根节点<code>rootFiber</code>开始深度遍历该<code>fiber树</code> 进行 render。</p><p>等待一段时间后，<code>primary</code>组件数据返回，我们之前在<code>handleError</code>中添加的监听器<code>attachRetryListener</code> 被触发，开始新的一轮任务调度。注：源码中调度回调实际在 Commit 阶段才添加的。</p><p>这时由于<code>Suspense</code> 节点已经存在，则走的是<code>updateSuspensePrimaryChildren</code> 中的逻辑，与之前首次加载时 <code>monutSuspensePrimaryChildren</code>不同的是多了删除的操作, 在 commit 阶段时则会删除<code>fallback</code> 组件, 展示<code>primary</code>组件。<a href="https://link.segmentfault.com/?enc=9/svcCMTzBpFP33OINT05w==.ldjHcVswfPcnoNHpgWKRyVeh1lhxkmCF0MRflBPmqRynE26kElQ6JxZaObIoe+bO+qsblVK8ycKu6JP/Rl42WSabK4KQX+/oekzOdqibb2YNCeZckTYWCG/nZzW5sLZFiZ4GWSeHPYb38gtDWUlih2Fi20A01Yp+JJrf5R37u0+7qPIDIOaXiZ66wyD7OKvQ">updateSuspensePrimaryChildren</a></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">if (currentFallbackChildFragment !== null) &#123;</span><br><span class="line">    // Delete the fallback child fragment</span><br><span class="line">    const deletions = workInProgress.deletions;</span><br><span class="line">    if (deletions === null) &#123;</span><br><span class="line">      workInProgress.deletions = [currentFallbackChildFragment];</span><br><span class="line">      workInProgress.flags |= ChildDeletion;</span><br><span class="line">    &#125; else &#123;</span><br><span class="line">      deletions.push(currentFallbackChildFragment);</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br></pre></td></tr></table></figure><p>至此，Suspense 的一生我们粗略的过完了，在源码中对 Suspense 的处理非常多，涉及到优先级相关的本篇都略过。<br>Suspense 中使用了<code>Offscreen</code>组件来渲染子组件，这个组件的特性是能根据传入 mode 来控制子组件样式的显隐，这有一个好处，就是能保存组件的状态，有些许类似于 Vue 的<code>keep-alive</code> 。其次，它拥有着最低的调度优先级，比空闲时优先级还要低，这也意味着当 mode 切换时，它会被任何其他调度任务插队打断掉。<br><img src="https://images.nirvana.net.cn/static/images/images/10024246-5804f76995eb250f.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="></p><h2 id="useTransition"><a href="#useTransition" class="headerlink" title="useTransition"></a>useTransition</h2><p><code>useTransition</code> 可以让我们在不阻塞 UI 渲染的情况下更新状态。<code>useTransition</code> 和 <code>startTransition</code> 允许将某些更新标记为<code>低优先级更新</code>。默认情况下，其他更新被视为<code>紧急更新</code>。React 将允许更紧急的更新（例如更新文本输入）来中断不太紧急的更新（例如展示搜索结果列表）。<br>其核心原理其实就是将<code>startTransition</code> 内调用的状态变更方法都标识为<code>低优先级的lane</code> (<a href="https://link.segmentfault.com/?enc=dqxD5BGa2zGkWe+TwUD9YA==.Cd1ciTX9EGV9ed8phXTL/lnfn/0J0MyDo/UCXhchQD2EgVP62nuriFwA6pG1aiQUxwEshYGt2nym2OcZuzyJVg==">lane优先级参考</a>)去更新。</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">const [isPending, startTransition] = useTransition()</span><br><span class="line"></span><br><span class="line">startTransition(() =&gt; &#123;</span><br><span class="line">    setData(xxx)</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><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><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">function Demo() &#123;</span><br><span class="line">  const [value, setValue] = useState();</span><br><span class="line">  const [isPending, startTransition] = useTransition();</span><br><span class="line"></span><br><span class="line">  return (</span><br><span class="line">    &lt;div&gt;</span><br><span class="line">      &lt;h1&gt;useTramsotopm Demo&lt;/h1&gt;</span><br><span class="line">      &lt;input</span><br><span class="line">        onChange=&#123;(e) =&gt; &#123;</span><br><span class="line">          startTransition(() =&gt; &#123;</span><br><span class="line">            setValue(e.target.value);</span><br><span class="line">          &#125;);</span><br><span class="line">        &#125;&#125;</span><br><span class="line">      /&gt;</span><br><span class="line">      &lt;hr /&gt;</span><br><span class="line">      &#123;isPending ? &lt;p&gt;加载中。。&lt;/p&gt; : &lt;List value=&#123;value&#125; /&gt;&#125;</span><br><span class="line">    &lt;/div&gt;</span><br><span class="line">  );</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">function List(&#123; value &#125;) &#123;</span><br><span class="line">  const items = new Array(5000).fill(1).map((_, index) =&gt; &#123;</span><br><span class="line">    return (</span><br><span class="line">      &lt;li&gt;</span><br><span class="line">        &lt;ListItem index=&#123;index&#125; value=&#123;value&#125; /&gt;</span><br><span class="line">      &lt;/li&gt;</span><br><span class="line">    );</span><br><span class="line">  &#125;);</span><br><span class="line">  return &lt;ul&gt;&#123;items&#125;&lt;/ul&gt;;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">function ListItem(&#123; index, value &#125;) &#123;</span><br><span class="line">  return (</span><br><span class="line">    &lt;div&gt;</span><br><span class="line">      &lt;span&gt;index: &lt;/span&gt;</span><br><span class="line">      &lt;span&gt;&#123;index&#125;&lt;/span&gt;</span><br><span class="line">      &lt;span&gt;value: &lt;/span&gt;</span><br><span class="line">      &lt;span&gt;&#123;value&#125;&lt;/span&gt;</span><br><span class="line">    &lt;/div&gt;</span><br><span class="line">  );</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>当我每次进行输入时，会触发 List 进行大量更新，但由于我使用了<code>startTransition</code>  对<code>List</code>的更新进行<code>延后</code> ，所以<code>Input</code>输入框不会出现明显卡顿现象<br>演示地址<a href="https://link.segmentfault.com/?enc=NHCbB7PNP8HwyC+gKRNlOg==.IuH6O6NG1n/+kIKX+719YCNuEfGUIkIWVAkIg+rrOrLZTU7KC6skkdzpwWEoQRtDcoz3ofN4peijNd1QMg+kFomgXwGyY5lm4twJy8PKhHTwVdgpESDUIbzUhOyYzrwi">https://stackblitz.com/edit/stackblitz-starters-kmkcjs?file=src%2Ftransition%2FList.tsx</a><br><img src="https://images.nirvana.net.cn/static/images/images/10024246-0edaa92d994adb22.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="><br>由于更新被滞后了，所以我们怎么知道当前有没有被更新呢？<br>这时候第一个返回参数<code>isPending</code> 就是用来告诉我们当前是否还在等待中。<br>但我们可以看到，<code>input</code>组件目前是<code>非受控组件</code> ，如果改为<code>受控组件</code> ，即使使用了<code>startTransition</code> 一样会出现卡顿，因为 input 响应输入事件进行状态更新应该是要同步的。<br>所以这时候下面介绍的<code>useDeferredValue</code> 作用就来了。</p><h2 id="useDeferredValue"><a href="#useDeferredValue" class="headerlink" title="useDeferredValue"></a>useDeferredValue</h2><p><code>useDeferredValue</code> 可让您推迟更新部分 UI， 它与<code>useTransition</code> 做的事差不多，不过<code>useTransition</code> 是在状态更新层，推迟状态更新来实现非阻塞，而<code>useDeferredValue</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><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">function Page() &#123;</span><br><span class="line">  const [value, setValue] = useState(&#x27;&#x27;);</span><br><span class="line">  const deferredValue = useDeferredValue(setValue);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>我们再用<code>useDeferredValue</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></pre></td><td class="code"><pre><span class="line">function Demo() &#123;</span><br><span class="line">  const [value, setValue] = useState(&#x27;&#x27;);</span><br><span class="line">  const deferredValue = useDeferredValue(value);</span><br><span class="line"></span><br><span class="line">  return (</span><br><span class="line">    &lt;div&gt;</span><br><span class="line">      &lt;h1&gt;useDeferedValue Demo&lt;/h1&gt;</span><br><span class="line">      &lt;input</span><br><span class="line">        value=&#123;value&#125;</span><br><span class="line">        onChange=&#123;(e) =&gt; &#123;</span><br><span class="line">          setValue(e.target.value)</span><br><span class="line">        &#125;&#125;</span><br><span class="line">      /&gt;</span><br><span class="line">      &lt;hr /&gt;</span><br><span class="line">      &lt;List value=&#123;deferredValue&#125; /&gt;</span><br><span class="line">    &lt;/div&gt;</span><br><span class="line">  );</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>我们将<code>input</code>作为<code>受控组件</code> ，对于会因输入框值而造成<code>大量渲染</code>的<code>List</code>,我们使用<code>deferredValue</code> 。</p><p>其变化过程如下</p><ol><li> 当输入变化时，<code>deferredValue</code> 首先会是变化前的旧值进行重新渲染，由于值没有变，所以 List 没有重新渲染，也就没有出现阻塞情况，这时，input 的值能够实时响应到页面上。</li><li> 在这次旧值渲染完成后，deferredValue 变更为新的值，React 会在后台开始对新值进行重新渲染，<code>List</code> 组件开始 rerender，且此次 rerender 会被标识为<code>低优先级渲染</code>，能够被<code>中断</code></li><li> 如果此时又有输入框输入，则中断此次后台的重新渲染，重新走1，2的流程</li></ol><p>我们可以打印下<code>deferredValue</code>  的值看下<br>初始情况输入框为1，打印了两次1<br><img src="https://images.nirvana.net.cn/static/images/images/10024246-b68304fecdccfd1c.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="></p><p>输入2时，再次打印了两次1，随后打印了两次2<br><img src="https://images.nirvana.net.cn/static/images/images/10024246-86461fdb5a4c73f0.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="></p><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ul><li>  <a href="https://link.segmentfault.com/?enc=6R2JZ3l853d+v0nZGZzo5w==.z6z50cUmS6ZKNFe7TnxptCnmG28aEBgPvfm2BGL4oRlFiPrKKFJqr/4I3JnMZ5vU">React 从 v15 升级到 v16 后，为什么要重构底层架构</a></li><li>  <a href="https://link.segmentfault.com/?enc=CvMmhO5lSRx8dg1rqKN8rg==.pD/rhj8CXtTbaMEpB44VHtER0W7b+AFVezuoHrZDBKKeLSvay2hM95HmBuy1XHm0JU8kCQXTUpMlA0WoCRNjkg==">React技术揭秘</a></li><li>  <a href="https://link.segmentfault.com/?enc=5wbiYOIcuncvGQBnx0A3yw==.MFjeQDOJYltDKAPcuMrcJ3ygX834yqAu8qsb6ya6Vj3VeQ1sVWJyQmwGF7njqy7k">React Suspense官方文档</a></li></ul>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;&lt;img src=&quot;https://images.nirvana.net.cn/static/images/images/10024246-227f4b3ce4043508.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2Awdb</summary>
      
    
    
    
    <category term="前端" scheme="https://www.nirvana.net.cn/categories/%E5%89%8D%E7%AB%AF/"/>
    
    
    <category term="react" scheme="https://www.nirvana.net.cn/tags/react/"/>
    
    <category term="web" scheme="https://www.nirvana.net.cn/tags/web/"/>
    
  </entry>
  
  <entry>
    <title>在node中使用.env 文件</title>
    <link href="https://www.nirvana.net.cn/2024/1020ee9booyyyZ.html"/>
    <id>https://www.nirvana.net.cn/2024/1020ee9booyyyZ.html</id>
    <published>2024-10-20T08:28:03.000Z</published>
    <updated>2024-10-27T08:45:58.438Z</updated>
    
    <content type="html"><![CDATA[<p><img src="https://images.nirvana.net.cn/static/images/images/10024246-f23e9a340824f4e4.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="><br>.env 文件是一种简单的文本文件，用于存储环境变量，通常用于在开发和生产环境中配置应用程序。文件中的内容以键值对的形式存储，每一行代表一个环境变量的设置，格式为 KEY=VALUE。</p><h2 id="env-文件的用途"><a href="#env-文件的用途" class="headerlink" title=".env 文件的用途"></a>.env 文件的用途</h2><p>.env 文件的主要作用是将应用程序的配置与代码分离，尤其是那些敏感信息或可能因环境不同而变化的配置项（如数据库凭证、API 密钥等）。通过 .env 文件，开发人员可以避免将敏感数据直接硬编码在代码中，并且可以轻松切换不同的环境配置。</p><h2 id="dotenv介绍"><a href="#dotenv介绍" class="headerlink" title="dotenv介绍"></a>dotenv介绍</h2><p><code>dotenv</code> 是一个流行的 Node.js 库，用于将环境变量加载到 Node.js 应用程序中，特别是从一个名为 <code>.env</code> 的文件中。它提供了一种方便的方式来管理应用程序的配置，尤其是在开发和生产环境中。</p><h4 id="主要功能"><a href="#主要功能" class="headerlink" title="主要功能"></a>主要功能</h4><ol><li><p><strong>加载环境变量</strong>：<code>dotenv</code> 可以读取 <code>.env</code> 文件，并将其内容加载到 <code>process.env</code> 中，使得应用程序可以方便地访问这些变量。</p></li><li><p><strong>支持默认值</strong>：你可以为环境变量设置默认值，以确保在某些变量未定义时应用程序仍然可以正常工作。</p></li><li><p><strong>简化配置管理</strong>：通过集中管理环境变量，<code>dotenv</code> 有助于将敏感信息（如 API 密钥、数据库凭据等）与应用程序代码分离，从而提高安全性和可维护性。</p><h4 id="1-安装-dotenv"><a href="#1-安装-dotenv" class="headerlink" title="1. 安装 dotenv"></a>1. 安装 <code>dotenv</code></h4></li></ol><p>使用 npm 或 yarn 安装 <code>dotenv</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">npm install dotenv</span><br><span class="line"><span class="comment"># 或者</span></span><br><span class="line">yarn add dotenv</span><br></pre></td></tr></table></figure><h4 id="2-创建-env-文件"><a href="#2-创建-env-文件" class="headerlink" title="2. 创建 .env 文件"></a>2. 创建 <code>.env</code> 文件</h4><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><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">PORT=3000</span><br><span class="line">DB_HOST=localhost</span><br><span class="line">DB_USER=root</span><br><span class="line">DB_PASS=s1mpl3</span><br><span class="line">API_KEY=your_api_key_here</span><br></pre></td></tr></table></figure><h4 id="3-在应用程序中引入-dotenv"><a href="#3-在应用程序中引入-dotenv" class="headerlink" title="3. 在应用程序中引入 dotenv"></a>3. 在应用程序中引入 <code>dotenv</code></h4><p>在项目的入口文件（通常是 <code>index.js</code> 或 <code>app.js</code>）中引入并配置 <code>dotenv</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="built_in">require</span>(<span class="string">&#x27;dotenv&#x27;</span>).<span class="title function_">config</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">`Server running on port: <span class="subst">$&#123;process.env.PORT&#125;</span>`</span>);</span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`Connecting to database at <span class="subst">$&#123;process.env.DB_HOST&#125;</span>`</span>);</span><br></pre></td></tr></table></figure><h4 id="4-使用环境变量"><a href="#4-使用环境变量" class="headerlink" title="4. 使用环境变量"></a>4. 使用环境变量</h4><p>一旦加载了 <code>.env</code> 文件中的变量，你就可以在应用程序的任何地方通过 <code>process.env</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">const</span> dbUser = process.<span class="property">env</span>.<span class="property">DB_USER</span>;</span><br><span class="line"><span class="keyword">const</span> dbPass = process.<span class="property">env</span>.<span class="property">DB_PASS</span>;</span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`Connecting to the database with user: <span class="subst">$&#123;dbUser&#125;</span>`</span>);</span><br></pre></td></tr></table></figure><h4 id="注意事项"><a href="#注意事项" class="headerlink" title="注意事项"></a>注意事项</h4><ol><li><p><strong>安全性</strong>：确保将 <code>.env</code> 文件添加到 <code>.gitignore</code> 文件中，以防止它被意外提交到版本控制系统中，从而泄露敏感信息。</p><p>在 <code>.gitignore</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">.env</span><br></pre></td></tr></table></figure></li><li><p><strong>环境变量的类型</strong>：所有从 <code>.env</code> 文件中读取的值都是字符串。如果你需要其他类型（如布尔值、数字），你需要在代码中进行转换。</p></li><li><p><strong>加载多个 <code>.env</code> 文件</strong>：如果需要在不同环境中使用不同的配置，可以创建多个 <code>.env</code> 文件（例如 <code>.env.development</code>, <code>.env.production</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">const</span> path = <span class="built_in">require</span>(<span class="string">&#x27;path&#x27;</span>);</span><br><span class="line"><span class="keyword">const</span> envFile = process.<span class="property">env</span>.<span class="property">NODE_ENV</span> === <span class="string">&#x27;production&#x27;</span> ? <span class="string">&#x27;.env.production&#x27;</span> : <span class="string">&#x27;.env.development&#x27;</span>;</span><br><span class="line"><span class="built_in">require</span>(<span class="string">&#x27;dotenv&#x27;</span>).<span class="title function_">config</span>(&#123; <span class="attr">path</span>: path.<span class="title function_">resolve</span>(__dirname, envFile) &#125;);</span><br></pre></td></tr></table></figure></li></ol><h4 id="示例"><a href="#示例" class="headerlink" title="示例"></a>示例</h4><p>这是一个使用 <code>dotenv</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></pre></td><td class="code"><pre><span class="line"><span class="comment">// index.js</span></span><br><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><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> app = <span class="title function_">express</span>();</span><br><span class="line"><span class="keyword">const</span> port = process.<span class="property">env</span>.<span class="property">PORT</span> || <span class="number">3000</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_">send</span>(<span class="string">`Hello, world! Running on port: <span class="subst">$&#123;port&#125;</span>`</span>);</span><br><span class="line">&#125;);</span><br><span class="line"></span><br><span class="line">app.<span class="title function_">listen</span>(port, <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">`Server is running on http://localhost:<span class="subst">$&#123;port&#125;</span>`</span>);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><h2 id="node原生支持"><a href="#node原生支持" class="headerlink" title="node原生支持"></a>node原生支持</h2><p>在 Node.js 版本 <strong>20.6.0</strong> 及更高版本中，Node.js 自带了对 <code>.env</code> 文件的原生支持，不需要额外安装 <code>dotenv</code> 库。这使得你可以直接在应用中加载 <code>.env</code> 文件，而无需依赖第三方库。</p><h4 id="使用-Node-js-原生-env-支持的方法："><a href="#使用-Node-js-原生-env-支持的方法：" class="headerlink" title="使用 Node.js 原生 .env 支持的方法："></a>使用 Node.js 原生 <code>.env</code> 支持的方法：</h4><ol><li><p><strong>在根目录创建 <code>.env</code> 文件</strong><br>跟之前的方式相同，创建 <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><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">PORT=3000</span><br><span class="line">DB_HOST=localhost</span><br><span class="line">DB_USER=admin</span><br><span class="line">DB_PASS=secretpassword</span><br></pre></td></tr></table></figure></li><li><p><strong>通过 <code>--env-file</code> 参数加载 <code>.env</code> 文件</strong><br>启动 Node.js 程序时，通过 <code>--env-file</code> 参数指定 <code>.env</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">node --env-file=.<span class="built_in">env</span> index.js</span><br></pre></td></tr></table></figure><p>这样，Node.js 会自动解析并将 <code>.env</code> 文件中的变量加载到 <code>process.env</code> 对象中，无需 <code>dotenv</code> 库。</p></li></ol><h4 id="示例-1"><a href="#示例-1" class="headerlink" title="示例"></a>示例</h4><p>假设你的项目目录中有一个 <code>index.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="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`Server will run on port: <span class="subst">$&#123;process.env.PORT&#125;</span>`</span>);</span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`Database host is: <span class="subst">$&#123;process.env.DB_HOST&#125;</span>`</span>);</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">node --env-file=.<span class="built_in">env</span> index.js</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></pre></td><td class="code"><pre><span class="line">Server will run on port: 3000</span><br><span class="line">Database host is: localhost</span><br></pre></td></tr></table></figure><h4 id="优势"><a href="#优势" class="headerlink" title="优势"></a>优势</h4><ul><li>**无需安装 <code>dotenv</code>**：直接使用 Node.js 内置功能来加载 <code>.env</code> 文件，减少了依赖。</li><li><strong>简化的命令行参数</strong>：通过 <code>--env-file</code> 直接加载文件，便于配置。</li></ul><h4 id="版本要求"><a href="#版本要求" class="headerlink" title="版本要求"></a>版本要求</h4><p>为了使用这项功能，你需要 Node.js <strong>20.6.0</strong> 或更高版本。如果你正在使用旧版本的 Node.js（如 20.6.0 之前的版本），仍然需要使用 <code>dotenv</code> 等第三方库。</p><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 -v</span><br></pre></td></tr></table></figure><p>如果版本低于 20.6.0，你可以通过升级 Node.js 来获得这个原生支持。</p><h2 id="process-loadEnvFile-介绍"><a href="#process-loadEnvFile-介绍" class="headerlink" title="process.loadEnvFile()介绍"></a>process.loadEnvFile()介绍</h2><p><code>process.loadEnvFile()</code> 是从 Node.js 21.7.0 版本开始引入的一个新方法，它用于进一步简化 <code>.env</code> 文件中环境变量的加载过程。这个方法允许你在运行时通过代码动态加载 <code>.env</code> 文件，而不需要依赖外部库如 <code>dotenv</code>，并且不再需要在启动命令中指定 <code>--env-file</code> 标志。</p><h4 id="主要功能-1"><a href="#主要功能-1" class="headerlink" title="主要功能"></a>主要功能</h4><ul><li><p><strong>动态加载 <code>.env</code> 文件</strong>：<code>process.loadEnvFile()</code> 可以在应用运行时通过代码加载 <code>.env</code> 文件，这意味着你可以在程序逻辑中按需加载环境变量。</p></li><li><p><strong>无需外部库</strong>：不需要使用 <code>dotenv</code> 或其他类似的库。它提供了 Node.js 原生的环境变量加载支持。</p></li><li><p><strong>文件路径灵活</strong>：你可以指定加载的 <code>.env</code> 文件的路径，方便在不同环境或配置下使用不同的文件。</p></li></ul><h4 id="使用示例"><a href="#使用示例" class="headerlink" title="使用示例"></a>使用示例</h4><ol><li><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">USER_NAME=&#x27;Paweł Grzybek&#x27;</span><br><span class="line">API_KEY=&#x27;your_api_key&#x27;</span><br></pre></td></tr></table></figure></li><li><p>在 Node.js 代码中使用 <code>process.loadEnvFile()</code> 方法加载这个 <code>.env</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></pre></td><td class="code"><pre><span class="line"><span class="comment">// index.js</span></span><br><span class="line">(<span class="keyword">async</span> () =&gt; &#123;</span><br><span class="line">    <span class="keyword">await</span> process.<span class="title function_">loadEnvFile</span>(<span class="string">&#x27;.env&#x27;</span>);</span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`Hi <span class="subst">$&#123;process.env.USER_NAME&#125;</span> 👋`</span>);</span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`Your API Key: <span class="subst">$&#123;process.env.API_KEY&#125;</span>`</span>);</span><br><span class="line">&#125;)();</span><br></pre></td></tr></table></figure></li><li><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">node index.js</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></pre></td><td class="code"><pre><span class="line">Hi Paweł Grzybek 👋</span><br><span class="line">Your API Key: your_api_key</span><br></pre></td></tr></table></figure></li></ol><h4 id="方法说明"><a href="#方法说明" class="headerlink" title="方法说明"></a>方法说明</h4><ul><li><p><strong>异步方法</strong>：<code>process.loadEnvFile()</code> 是异步方法，因此你需要使用 <code>await</code> 或 <code>then()</code> 来确保 <code>.env</code> 文件正确加载后再使用环境变量。</p></li><li><p><strong>错误处理</strong>：如果文件不存在或读取失败，你可以使用 <code>try...catch</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">try</span> &#123;</span><br><span class="line">    <span class="keyword">await</span> process.<span class="title function_">loadEnvFile</span>(<span class="string">&#x27;.env&#x27;</span>);</span><br><span class="line">&#125; <span class="keyword">catch</span> (err) &#123;</span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">error</span>(<span class="string">&#x27;Failed to load .env file&#x27;</span>, err);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></li></ul><h4 id="参数"><a href="#参数" class="headerlink" title="参数"></a>参数</h4><ul><li>**<code>filePath</code>**：你可以指定 <code>.env</code> 文件的路径（相对于当前工作目录），例如 <code>&#39;.env&#39;</code>、<code>&#39;config/.env.production&#39;</code> 等。</li><li><strong><code>options</code>（可选）</strong>：未来可能会扩展支持额外的选项，类似 <code>dotenv</code> 的配置参数，比如编码或变量替换等（截至 21.7.0 版本，还没有提供这些选项）。</li></ul><h4 id="使用场景"><a href="#使用场景" class="headerlink" title="使用场景"></a>使用场景</h4><ul><li><strong>按需加载</strong>：有时你可能需要在不同阶段或模块中加载不同的环境变量，这种情况下 <code>process.loadEnvFile()</code> 可以灵活地满足需求，而不像 <code>dotenv</code> 那样必须在应用启动时加载所有变量。</li><li><strong>简化配置</strong>：对于简单的项目，使用 <code>process.loadEnvFile()</code> 可以避免额外安装第三方库，简化配置过程。</li></ul><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>node读取环境变量的配置随着node版本的变化，使用方式也不同，可以根据个人使用的node版本进行选择。</p><ul><li><code>process.loadEnvFile()</code> 是 Node.js 21.7.0 版本新增的一个方法，用于动态加载 <code>.env</code> 文件。</li><li>Node.js 内置功能来加载 .env 文件，需要 Node.js 20.6.0 或更高版本。</li><li>dotenv 是一个流行的 Node.js 库，它提供了一种方便的方式来管理应用程序的配置，无版本兼容问题</li></ul>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;&lt;img src=&quot;https://images.nirvana.net.cn/static/images/images/10024246-f23e9a340824f4e4.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2Awdb</summary>
      
    
    
    
    <category term="前端" scheme="https://www.nirvana.net.cn/categories/%E5%89%8D%E7%AB%AF/"/>
    
    
    <category term="node" scheme="https://www.nirvana.net.cn/tags/node/"/>
    
    <category term="web" scheme="https://www.nirvana.net.cn/tags/web/"/>
    
  </entry>
  
  <entry>
    <title>闭包在 React 中的应用</title>
    <link href="https://www.nirvana.net.cn/2024/1019IyyY5teeee.html"/>
    <id>https://www.nirvana.net.cn/2024/1019IyyY5teeee.html</id>
    <published>2024-10-19T08:28:03.000Z</published>
    <updated>2024-10-27T08:45:58.338Z</updated>
    
    <content type="html"><![CDATA[<p><img src="https://images.nirvana.net.cn/static/images/images/10024246-19c5b0c2df166ebe.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="><br>JavaScript 中的闭包可能是该语言中最令人畏惧的特性之一。即使是无所不知的 ChatGPT 也会告诉你这一点。闭包也是最隐蔽的语言概念之一。我们在编写任何 React 代码会使用它，大部分时候甚至没有意识到它的存在。但最终我们无法避开闭包：如果我们想编写复杂且性能良好的 React 应用程序，就必须了解闭包。</p><h3 id="问题"><a href="#问题" class="headerlink" title="问题"></a>问题</h3><p>想象一下你正在实现一个带有几个输入字段的表单。其中一个字段是来自外部库的非常繁重的组件。你无法访问其内部实现，因此无法修复其性能问题。但你确实需要在表单中使用它，因此你决定用 <code>React.memo</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="title class_">HeavyComponentMemo</span> = <span class="title class_">React</span>.<span class="title function_">memo</span>(<span class="title class_">HeavyComponent</span>);</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">Form</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> [value, setValue] = <span class="title function_">useState</span>();</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">input</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">        <span class="attr">type</span>=<span class="string">&quot;text&quot;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">        <span class="attr">value</span>=<span class="string">&#123;value&#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;(e)</span> =&gt;</span> setValue(e.target.value)&#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">HeavyComponentMemo</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><p>到目前为止，一切顺利。这个繁重的组件只接受一个字符串属性，比如 <code>title</code>，还有一个 <code>onClick</code> 回调。这将在点击组件内部的 “完成” 按钮时触发。你希望在点击时提交表单数据。这也很简单：只需传递 <code>title</code> 和 <code>onClick</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="title class_">HeavyComponentMemo</span> = <span class="title class_">React</span>.<span class="title function_">memo</span>(<span class="title class_">HeavyComponent</span>);</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">Form</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> [value, setValue] = <span class="title function_">useState</span>();</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> <span class="title function_">onClick</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">    <span class="comment">// 在这里提交表单数据</span></span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">log</span>(value);</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">input</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">        <span class="attr">type</span>=<span class="string">&quot;text&quot;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">        <span class="attr">value</span>=<span class="string">&#123;value&#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;(e)</span> =&gt;</span> setValue(e.target.value)&#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">HeavyComponentMemo</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">onClick</span>=<span class="string">&#123;onClick&#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;/&gt;</span></span></span><br><span class="line">  );</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>现在，你会面临一个难题。正如我们所知，<code>React.memo</code> 包装的组件中的每个属性都需要是原始值或在重新渲染之间保持持久性。否则，记忆化（memoization）将不起作用。因此，技术上我们需要用 <code>useCallback</code> 包装 <code>onClick</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">const</span> onClick = <span class="title function_">useCallback</span>(<span class="function">() =&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><p>但是，我们也知道 <code>useCallback</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> onClick = <span class="title function_">useCallback</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">  <span class="comment">// 在这里提交数据</span></span><br><span class="line">  <span class="variable language_">console</span>.<span class="title function_">log</span>(value);</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 将 value 添加到依赖项中</span></span><br><span class="line">&#125;, [value]);</span><br></pre></td></tr></table></figure><p><strong>这就是难题</strong>：即使 <code>onClick</code> 是记忆化的，它仍然会在每次有人输入内容时发生变化。因此，我们的性能优化就变得毫无意义。</p><p>好吧，没关系，我们可以寻找其他解决方案。<code>React.memo</code> 有一个称为比较函数的特性。它允许我们更细粒度地控制 <code>React.memo</code> 中的属性比较。通常情况下，React 会自己比较所有的“之前”和“之后”属性。如果我们提供了这个函数，它将依赖于函数的返回结果。如果返回 <code>true</code>，React 就会知道属性是相同的，组件不需要重新渲染。听起来正是我们需要的。</p><p>我们只需要关心更新一个属性，即 <code>title</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="title class_">HeavyComponentMemo</span> = <span class="title class_">React</span>.<span class="title function_">memo</span>(</span><br><span class="line">  <span class="title class_">HeavyComponent</span>,</span><br><span class="line">  <span class="function">(<span class="params">before, after</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> before.<span class="property">title</span> === after.<span class="property">title</span>;</span><br><span class="line">  &#125;,</span><br><span class="line">);</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><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">const</span> <span class="title class_">HeavyComponentMemo</span> = <span class="title class_">React</span>.<span class="title function_">memo</span>(</span><br><span class="line">  <span class="title class_">HeavyComponent</span>,</span><br><span class="line">  <span class="function">(<span class="params">before, after</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> before.<span class="property">title</span> === after.<span class="property">title</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> <span class="title function_">Form</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> [value, setValue] = <span class="title function_">useState</span>();</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> <span class="title function_">onClick</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">    <span class="comment">// 在这里提交表单数据</span></span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">log</span>(value);</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">input</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">        <span class="attr">type</span>=<span class="string">&quot;text&quot;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">        <span class="attr">value</span>=<span class="string">&#123;value&#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;(e)</span> =&gt;</span> setValue(e.target.value)&#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">HeavyComponentMemo</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">onClick</span>=<span class="string">&#123;onClick&#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;/&gt;</span></span></span><br><span class="line">  );</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p><strong>它看起来有效了！</strong>我们在输入框中输入内容，繁重组件没有重新渲染，性能也没有受到影响。</p><p>除了一个小问题：它实际上并没有起作用。如果你在输入框中输入内容并按下按钮，<code>onClick</code> 中输出的 <code>value</code> 是 <code>undefined</code>。但它不应该是 <code>undefined</code>，输入框的行为是正常的，如果我在 <code>onClick</code> 之外添加 <code>console.log</code>，它会正确输出。只是 <code>onClick</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="comment">// 这些输出是正确的</span></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(value);</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">onClick</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">  <span class="comment">// 这里的 value 始终是 undefined</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><p>这就是所谓的“陈旧闭包”问题。为了修复它，我们首先需要深入了解 JavaScript 中可能最令人畏惧的主题：闭包及其工作原理。</p><h3 id="JavaScript、作用域与闭包"><a href="#JavaScript、作用域与闭包" class="headerlink" title="JavaScript、作用域与闭包"></a><strong>JavaScript、作用域与闭包</strong></h3><p>我们先从函数和变量开始。当我们通过正常声明或箭头函数在 JavaScript 中声明一个函数时，会发生什么？</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">function</span> <span class="title function_">something</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"></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">something</span> = (<span class="params"></span>) =&gt; &#123;&#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></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="title function_">something</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> value = <span class="string">&#x27;text&#x27;</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>(value); <span class="comment">// 无法运行，“value” 仅在 &quot;something&quot; 函数内部可见</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><span class="line">2</span><br><span class="line">3</span><br><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_">something</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> <span class="title function_">inside</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">    <span class="keyword">const</span> value = <span class="string">&#x27;text&#x27;</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>(value); <span class="comment">// 无法运行，“value” 仅在 &quot;inside&quot; 函数中可见</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><span class="line">4</span><br><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> <span class="title function_">something</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> value = <span class="string">&#x27;text&#x27;</span>;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> <span class="title function_">inside</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">    <span class="comment">// 完全可以，“value” 在这里是可用的</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><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>这就是闭包的形成：内部函数“闭合”了所有来自外部的数据。它本质上是对所有“外部”数据的快照，这些数据被单独存储在内存中。</p><p>如果我不在 <code>something</code> 函数内部创建那个值，而是将它作为参数传递并返回 <code>inside</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="keyword">const</span> <span class="title function_">something</span> = (<span class="params">value</span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> <span class="title function_">inside</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">    <span class="comment">// 完全可以，“value” 在这里是可用的</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><span class="line"></span><br><span class="line">  <span class="keyword">return</span> inside;</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> first = <span class="title function_">something</span>(<span class="string">&#x27;first&#x27;</span>);</span><br><span class="line"><span class="keyword">const</span> second = <span class="title function_">something</span>(<span class="string">&#x27;second&#x27;</span>);</span><br><span class="line"></span><br><span class="line"><span class="title function_">first</span>(); <span class="comment">// 输出 &quot;first&quot;</span></span><br><span class="line"><span class="title function_">second</span>(); <span class="comment">// 输出 &quot;second&quot;</span></span><br></pre></td></tr></table></figure><p>我们用值 “first” 调用了 <code>something</code> 函数，并将结果赋值给一个变量。结果是一个指向内部声明函数的引用，形成了一个闭包。从现在开始，只要保存这个引用的变量存在，我们传递的 “first” 值就会被冻结，<code>inside</code> 函数将始终能够访问它。</p><p>同样的情况发生在第二次调用：我们传递了一个不同的值，形成了一个闭包，返回的函数将永远可以访问这个变量。</p><p>这对于在 <code>something</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="title function_">something</span> = (<span class="params">value</span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> r = <span class="title class_">Math</span>.<span class="title function_">random</span>();</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> <span class="title function_">inside</span> = (<span class="params"></span>) =&gt; &#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="keyword">return</span> inside;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> first = <span class="title function_">something</span>(<span class="string">&#x27;first&#x27;</span>);</span><br><span class="line"><span class="keyword">const</span> second = <span class="title function_">something</span>(<span class="string">&#x27;second&#x27;</span>);</span><br><span class="line"></span><br><span class="line"><span class="title function_">first</span>(); <span class="comment">// 输出随机数</span></span><br><span class="line"><span class="title function_">second</span>(); <span class="comment">// 输出另一个随机数</span></span><br></pre></td></tr></table></figure><p>这就像给动态场景拍照：按下按钮的那一刻，整个场景就被“冻结”在照片中。下一次按下按钮并不会改变之前拍摄的照片。<br><img src="https://images.nirvana.net.cn/static/images/images/10024246-8fec4500b2a4e0d6.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="></p><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><span class="line">3</span><br><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_">Component</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> <span class="title function_">onClick</span> = (<span class="params"></span>) =&gt; &#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="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;onClick&#125;</span> /&gt;</span></span>;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p><code>useEffect</code> 或 <code>useCallback</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">const</span> <span class="title function_">Component</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> onClick = <span class="title function_">useCallback</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">    <span class="comment">// 闭包！</span></span><br><span class="line">  &#125;);</span><br><span class="line"></span><br><span class="line">  <span class="title function_">useEffect</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">    <span class="comment">// 闭包！</span></span><br><span class="line">  &#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">const</span> <span class="title function_">Component</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> [state, setState] = <span class="title function_">useState</span>();</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> onClick = <span class="title function_">useCallback</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">    <span class="comment">// 完全可以</span></span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">log</span>(state);</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="comment">// 完全可以</span></span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">log</span>(state);</span><br><span class="line">  &#125;);</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>每个组件内的函数都是闭包，因为组件本身就是一个函数。</p><h3 id="陈旧闭包问题"><a href="#陈旧闭包问题" class="headerlink" title="陈旧闭包问题"></a><strong>陈旧闭包问题</strong></h3><p>以上所有内容，对于没有闭包概念的语言背景下的人来说可能略显不寻常，但还是相对简单的。你创建了几个函数，使用一段时间后就会变得自然。事实上，理解“闭包”这一概念并不是写 React 应用的必要条件。</p><p>那么问题出在哪里？为什么闭包是 JavaScript 中最让人害怕的东西之一，并且让许多开发者感到困惑呢？</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">const</span> <span class="title function_">something</span> = (<span class="params">value</span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> <span class="title function_">inside</span> = (<span class="params"></span>) =&gt; &#123;</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><span class="line"></span><br><span class="line">  <span class="keyword">return</span> inside;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>但 <code>inside</code> 函数在每次调用 <code>something</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">const</span> cache = &#123;&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">something</span> = (<span class="params">value</span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">if</span> (!cache.<span class="property">current</span>) &#123;</span><br><span class="line">    cache.<span class="property">current</span> = <span class="function">() =&gt;</span> &#123;</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><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> cache.<span class="property">current</span>;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>表面上看，这段代码看起来无害。我们只创建了一个名为 <code>cache</code> 的外部变量，并将 <code>inside</code> 函数赋值给 <code>cache.current</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><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> first = <span class="title function_">something</span>(<span class="string">&#x27;first&#x27;</span>);</span><br><span class="line"><span class="keyword">const</span> second = <span class="title function_">something</span>(<span class="string">&#x27;second&#x27;</span>);</span><br><span class="line"><span class="keyword">const</span> third = <span class="title function_">something</span>(<span class="string">&#x27;third&#x27;</span>);</span><br><span class="line"></span><br><span class="line"><span class="title function_">first</span>(); <span class="comment">// 输出 &quot;first&quot;</span></span><br><span class="line"><span class="title function_">second</span>(); <span class="comment">// 输出 &quot;first&quot;</span></span><br><span class="line"><span class="title function_">third</span>(); <span class="comment">// 输出 &quot;first&quot;</span></span><br></pre></td></tr></table></figure><p>无论我们用不同的参数调用多少次 <code>something</code> 函数，输出的值总是第一个！</p><p>我们刚刚创造了所谓的“陈旧闭包”。每个闭包在创建时都会被冻结。当我们第一次调用 <code>something</code> 函数时，形成了一个包含变量 “first” 的闭包。然后，我们将其保存到 <code>something</code> 函数之外的一个对象中。</p><p>当我们下次调用 <code>something</code> 函数时，返回的不是新创建的函数和新的闭包，而是之前创建的那个函数，它的变量 “first” 已经被永远冻结。</p><p><img src="https://images.nirvana.net.cn/static/images/images/10024246-035110a3c37f6d8a.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="></p><p>为了修复这种行为，我们希望每次 <code>value</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> cache = &#123;&#125;;</span><br><span class="line"><span class="keyword">let</span> prevValue;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">something</span> = (<span class="params">value</span>) =&gt; &#123;</span><br><span class="line">  <span class="comment">// 检查 value 是否改变</span></span><br><span class="line">  <span class="keyword">if</span> (!cache.<span class="property">current</span> || value !== prevValue) &#123;</span><br><span class="line">    cache.<span class="property">current</span> = <span class="function">() =&gt;</span> &#123;</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><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 更新缓存</span></span><br><span class="line">  prevValue = value;</span><br><span class="line">  <span class="keyword">return</span> cache.<span class="property">current</span>;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>将 <code>value</code> 保存在一个变量中，以便与下一个值进行比较。当变量发生变化时，更新 <code>cache.current</code> 闭包。</p><p>现在它可以正确输出变量，如果我们比较相同值的函数，比较结果将是 <code>true</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="keyword">const</span> first = <span class="title function_">something</span>(<span class="string">&#x27;first&#x27;</span>);</span><br><span class="line"><span class="keyword">const</span> anotherFirst = <span class="title function_">something</span>(<span class="string">&#x27;first&#x27;</span>);</span><br><span class="line"><span class="keyword">const</span> second = <span class="title function_">something</span>(<span class="string">&#x27;second&#x27;</span>);</span><br><span class="line"></span><br><span class="line"><span class="title function_">first</span>(); <span class="comment">// 输出 &quot;first&quot;</span></span><br><span class="line"><span class="title function_">second</span>(); <span class="comment">// 输出 &quot;second&quot;</span></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(first === anotherFirst); <span class="comment">// 将返回 true</span></span><br></pre></td></tr></table></figure><h3 id="React-中的陈旧闭包：useCallback"><a href="#React-中的陈旧闭包：useCallback" class="headerlink" title="React 中的陈旧闭包：useCallback"></a><strong>React 中的陈旧闭包：useCallback</strong></h3><p>我们刚刚实现的几乎和 <code>useCallback</code> 钩子为我们做的事情一样！每次我们使用 <code>useCallback</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="keyword">const</span> onClick = <span class="title function_">useCallback</span>(<span class="function">() =&gt;</span> &#123;&#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></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="title function_">Component</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> [state, setState] = <span class="title function_">useState</span>();</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> onClick = <span class="title function_">useCallback</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">    <span class="comment">// 访问状态</span></span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">log</span>(state);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 需要将其添加到依赖项数组中</span></span><br><span class="line">  &#125;, [state]);</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>依赖项数组让 React 刷新缓存的闭包，正如我们在比较 <code>value !== prevValue</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">const</span> <span class="title function_">Component</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> [state, setState] = <span class="title function_">useState</span>();</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> onClick = <span class="title function_">useCallback</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">    <span class="comment">// 这里的 state 始终是初始状态值</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>(state);</span><br><span class="line"></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>undefined</code>。</p><h3 id="React-中的陈旧闭包：Refs"><a href="#React-中的陈旧闭包：Refs" class="headerlink" title="React 中的陈旧闭包：Refs"></a>React 中的陈旧闭包：Refs</h3><p>除了 <code>useCallback</code> 和 <code>useMemo</code> 钩子，第二个最常见引入陈旧闭包问题的方式是 Refs。</p><p>如果我尝试用 Ref 代替 <code>useCallback</code> 钩子来处理 <code>onClick</code> 回调，会发生什么呢？有时，网上的文章建议这样做以便在组件中缓存 props。表面上看，它似乎更简单：只需将一个函数传递给 <code>useRef</code>，并通过 <code>ref.current</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="title function_">Component</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> ref = <span class="title function_">useRef</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">    <span class="comment">// 点击处理函数</span></span><br><span class="line">  &#125;);</span><br><span class="line"></span><br><span class="line">  <span class="comment">// ref.current 存储函数，并在重新渲染之间保持稳定</span></span><br><span class="line">  <span class="keyword">return</span> <span class="language-xml"><span class="tag">&lt;<span class="name">HeavyComponent</span> <span class="attr">onClick</span>=<span class="string">&#123;ref.current&#125;</span> /&gt;</span></span>;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>但是，每个在组件内部的函数都会形成一个闭包，包括传递给 <code>useRef</code> 的那个函数。我们的 ref 只会在首次创建时初始化，并且不会自动更新。基本上，这和我们最开始创建的逻辑类似，只不过这次我们传递的是需要保存的函数，而不是值。代码类似这样：</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> ref = &#123;&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">useRef</span> = (<span class="params">callback</span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">if</span> (!ref.<span class="property">current</span>) &#123;</span><br><span class="line">    ref.<span class="property">current</span> = callback;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> ref.<span class="property">current</span>;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>因此，在这种情况下，闭包是在组件初次挂载时形成的，并且会被保存下来，永远不会刷新。当我们尝试访问存储在 Ref 中的函数中的 state 或 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></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="title function_">Component</span> = (<span class="params">&#123; someProp &#125;</span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> [state, setState] = <span class="title function_">useState</span>();</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> ref = <span class="title function_">useRef</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">    <span class="comment">// 这两者都会是陈旧的，并且永远不会改变</span></span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">log</span>(someProp);</span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">log</span>(state);</span><br><span class="line">  &#125;);</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>要修复这个问题，我们需要确保每次尝试访问内部的内容时都更新 ref 的值。基本上，我们需要实现类似 <code>useCallback</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="title function_">Component</span> = (<span class="params">&#123; someProp &#125;</span>) =&gt; &#123;</span><br><span class="line">  <span class="comment">// 初始化 ref - 形成闭包！</span></span><br><span class="line">  <span class="keyword">const</span> ref = <span class="title function_">useRef</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">    <span class="comment">// 这两者都会是陈旧的，并且永远不会改变</span></span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">log</span>(someProp);</span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">log</span>(state);</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="comment">// 当 state 或 props 改变时，更新闭包</span></span><br><span class="line">    ref.<span class="property">current</span> = <span class="function">() =&gt;</span> &#123;</span><br><span class="line">      <span class="variable language_">console</span>.<span class="title function_">log</span>(someProp);</span><br><span class="line">      <span class="variable language_">console</span>.<span class="title function_">log</span>(state);</span><br><span class="line">    &#125;;</span><br><span class="line">  &#125;, [state, someProp]);</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><h3 id="React-中的陈旧闭包：React-memo"><a href="#React-中的陈旧闭包：React-memo" class="headerlink" title="React 中的陈旧闭包：React.memo"></a>React 中的陈旧闭包：React.memo</h3><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><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">const</span> <span class="title class_">HeavyComponentMemo</span> = <span class="title class_">React</span>.<span class="title function_">memo</span>(</span><br><span class="line">  <span class="title class_">HeavyComponent</span>,</span><br><span class="line">  <span class="function">(<span class="params">before, after</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> before.<span class="property">title</span> === after.<span class="property">title</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> <span class="title function_">Form</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> [value, setValue] = <span class="title function_">useState</span>();</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> <span class="title function_">onClick</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">    <span class="comment">// 在这里提交表单数据</span></span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">log</span>(value);</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">input</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">        <span class="attr">type</span>=<span class="string">&quot;text&quot;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">        <span class="attr">value</span>=<span class="string">&#123;value&#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;(e)</span> =&gt;</span> setValue(e.target.value)&#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">HeavyComponentMemo</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">        <span class="attr">title</span>=<span class="string">&quot;Welcome to the form&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;onClick&#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;/&gt;</span></span></span><br><span class="line">  );</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>每次点击按钮时，日志中输出的都是“undefined”。<code>onClick</code> 中的 <code>value</code> 从未更新。你现在知道为什么了吗？</p><p>当然，这是因为陈旧闭包。当我们创建 <code>onClick</code> 时，闭包最初是用默认的状态值（即 “undefined”）形成的。我们将该闭包与 <code>title</code> prop 一起传递给我们的被 memoized 的组件。在比较函数中，我们只比较 <code>title</code>。它从未改变，只是一个字符串。比较函数始终返回 <code>true</code>，<code>HeavyComponent</code> 从未更新，结果是它始终引用最初的 <code>onClick</code> 闭包，保持了冻结的 “undefined” 值。</p><p>现在我们知道问题出在哪了，怎么修复它呢？说起来容易做起来难……</p><p>理想情况下，我们应该在比较函数中比较每个 prop，因此我们需要把 <code>onClick</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">(before, after) =&gt; &#123;</span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    before.<span class="property">title</span> === after.<span class="property">title</span> &amp;&amp;</span><br><span class="line">    before.<span class="property">onClick</span> === after.<span class="property">onClick</span></span><br><span class="line">  );</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>但是，这样的话我们其实是在重新实现 React 的默认行为，和没有比较函数的 <code>React.memo</code> 的作用完全一样。所以我们可以简单地去掉它，只保留 <code>React.memo(HeavyComponent)</code>。</p><p>但这样的话，我们需要将 <code>onClick</code> 包装在 <code>useCallback</code> 中。然而它依赖于状态，因此会随着每次输入的变化而改变。我们回到了起点：我们的重组件会在每次状态更改时重新渲染，正是我们想要避免的情况。</p><p>我们可以尝试通过组合的方式，提取并隔离状态或 <code>HeavyComponent</code>，但这并不容易：输入框和 <code>HeavyComponent</code> 都依赖于那个状态。</p><p>我们可以尝试很多其他方法，但其实并不需要做任何繁重的重构来逃脱闭包陷阱。这里有一个很酷的小技巧可以帮助我们。</p><h3 id="用-Refs-逃离闭包陷阱"><a href="#用-Refs-逃离闭包陷阱" class="headerlink" title="用 Refs 逃离闭包陷阱"></a>用 Refs 逃离闭包陷阱</h3><p>这个技巧绝对让人惊叹：它非常简单，但可能会永远改变你在 React 中 memoize 函数的方式。或者也许不会……但无论如何，这个技巧可能会派上用场，所以让我们深入了解它。</p><p>首先，我们先去掉 <code>React.memo</code> 中的比较函数和 <code>onClick</code> 实现。只保留一个纯状态组件和被 memoize 的 <code>HeavyComponent</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="title class_">HeavyComponentMemo</span> = <span class="title class_">React</span>.<span class="title function_">memo</span>(<span class="title class_">HeavyComponent</span>);</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">Form</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> [value, setValue] = <span class="title function_">useState</span>();</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">input</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">        <span class="attr">type</span>=<span class="string">&quot;text&quot;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">        <span class="attr">value</span>=<span class="string">&#123;value&#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;(e)</span> =&gt;</span> setValue(e.target.value)&#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">HeavyComponentMemo</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">        <span class="attr">title</span>=<span class="string">&quot;Welcome to the form&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;...&#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;/&gt;</span></span></span><br><span class="line">  );</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>现在我们需要添加一个在重新渲染之间保持稳定，但也能访问最新状态的 <code>onClick</code> 函数。</p><p>我们要将它存储在 Ref 中，所以让我们先添加一个空的 Ref：</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_">Form</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> [value, setValue] = <span class="title function_">useState</span>();</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 添加一个空的 ref</span></span><br><span class="line">  <span class="keyword">const</span> ref = <span class="title function_">useRef</span>();</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>为了让函数访问最新的状态，它需要在每次重新渲染时重新创建。这是闭包的本质，与 React 无关。我们应该在 <code>useEffect</code> 中修改 Refs，而不是直接在渲染中修改，所以让我们这么做。</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 function_">Form</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> [value, setValue] = <span class="title function_">useState</span>();</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 添加一个空的 ref</span></span><br><span class="line">  <span class="keyword">const</span> ref = <span class="title function_">useRef</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="comment">// 我们希望触发的回调函数</span></span><br><span class="line">    ref.<span class="property">current</span> = <span class="function">() =&gt;</span> &#123;</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><span class="line">  &#125;);</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>没有依赖数组的 <code>useEffect</code> 将在每次重新渲染时触发。这正是我们想要的。因此，现在 <code>ref.current</code> 中的闭包会在每次重新渲染时重新创建，因此在其中记录的状态始终是最新的。</p><p>但我们不能直接将 <code>ref.current</code> 传递给被 memoize 的组件。每次重新渲染时该值都会变化，所以 memoization 根本无法工作。</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">const</span> <span class="title function_">Form</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> ref = <span class="title function_">useRef</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">    ref.<span class="property">current</span> = <span class="function">() =&gt;</span> &#123;</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><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">      &#123;/* 这样做会破坏 memoization */&#125;</span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">HeavyComponentMemo</span> <span class="attr">onClick</span>=<span class="string">&#123;ref.current&#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><p>因此，我们创建一个用 <code>useCallback</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="title function_">Form</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> ref = <span class="title function_">useRef</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">    ref.<span class="property">current</span> = <span class="function">() =&gt;</span> &#123;</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><span class="line">  &#125;);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> onClick = <span class="title function_">useCallback</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">    <span class="comment">// 空的依赖数组！永远不会改变</span></span><br><span class="line">  &#125;, []);</span><br><span class="line"></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;/* 现在 memoization 正常工作，onClick 永远不会改变 */&#125;</span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">HeavyComponentMemo</span> <span class="attr">onClick</span>=<span class="string">&#123;onClick&#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><p>现在，memoization 完美工作了——<code>onClick</code> 永远不会改变。唯一的问题是：它什么都没做。</p><p>这是关键的魔法：要让它正常工作，我们只需在 memoized 回调中调用 <code>ref.current</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></pre></td><td class="code"><pre><span class="line"><span class="title function_">useEffect</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">  ref.<span class="property">current</span> = <span class="function">() =&gt;</span> &#123;</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><span class="line">&#125;);</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> onClick = <span class="title function_">useCallback</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">  <span class="comment">// 在这里调用 ref</span></span><br><span class="line">  ref.<span class="title function_">current</span>();</span><br><span class="line">&#125;, []);</span><br></pre></td></tr></table></figure><p>请注意 <code>useCallback</code> 中没有 <code>ref</code> 作为依赖项？它不需要。<code>ref</code> 本身永远不会改变。它只是一个对可变对象的引用，由 <code>useRef</code> 钩子返回。</p><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">const</span> a = &#123; <span class="attr">value</span>: <span class="string">&#x27;one&#x27;</span> &#125;;</span><br><span class="line"><span class="comment">// b 是不同的变量，但引用了同一个对象</span></span><br><span class="line"><span class="keyword">const</span> b = a;</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">a.<span class="property">value</span> = <span class="string">&#x27;two&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(b.<span class="property">value</span>); <span class="comment">// 将输出 &quot;two&quot;</span></span><br></pre></td></tr></table></figure><p>在我们的例子中，甚至连这种情况都没有发生：在 <code>useCallback</code> 和 <code>useEffect</code> 中，我们拥有完全相同的引用。因此，当我们在 <code>useEffect</code> 中修改 <code>ref</code> 对象的 <code>current</code> 属性时，我们可以在 <code>useCallback</code> 中访问它。这个属性正是捕获了最新状态数据的闭包。</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><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> <span class="title function_">Form</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> [value, setValue] = <span class="title function_">useState</span>();</span><br><span class="line">  <span class="keyword">const</span> ref = <span class="title function_">useRef</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">    ref.<span class="property">current</span> = <span class="function">() =&gt;</span> &#123;</span><br><span class="line">      <span class="comment">// 这是最新的值</span></span><br><span class="line">      <span class="variable language_">console</span>.<span class="title function_">log</span>(value);</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> onClick = <span class="title function_">useCallback</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">    <span class="comment">// 这是最新的值</span></span><br><span class="line">    ref.<span class="property">current</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;&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">input</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">        <span class="attr">type</span>=<span class="string">&quot;text&quot;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">        <span class="attr">value</span>=<span class="string">&#123;value&#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;(e)</span> =&gt;</span> setValue(e.target.value)&#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">HeavyComponentMemo</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">        <span class="attr">title</span>=<span class="string">&quot;Welcome closures&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;onClick&#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;/&gt;</span></span></span><br><span class="line">  );</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>现在，我们得到了两全其美的结果：重组件被正确地 memoize，不会在每次状态更改时重新渲染。而 <code>onClick</code> 回调可以访问组件中的最新数据而不破坏 memoization。现在我们可以安全地将所有需要的内容发送到后台了！</p><h3 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h3><p>希望以上内容对你有帮助，现在闭包对你来说应该已经是小菜一碟了。在你走之前，有几点关于闭包的内容需要记住：</p><ul><li>每当一个函数在另一个函数内部创建时，都会形成闭包。</li><li>由于 React 组件本质上是函数，因此在组件内部创建的每个函数都会形成闭包，包括像 <code>useCallback</code> 和 <code>useRef</code> 这样的钩子。</li><li>当调用形成闭包的函数时，闭包周围的所有数据都会被“冻结”，就像一个快照。</li><li>要更新这些数据，我们需要重新创建这个“闭合”的函数。这就是类似 <code>useCallback</code> 这样的钩子依赖数组的作用。</li><li>如果我们漏掉了某个依赖，或者没有刷新分配给 <code>ref.current</code> 的闭合函数，闭包就会变得“陈旧”。</li><li>我们可以利用 Ref 是一个可变对象的特性，来避免 React 中的“陈旧闭包”陷阱。我们可以在陈旧闭包之外修改 <code>ref.current</code>，然后在闭包内访问它，它将包含最新的数据。</li></ul>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;&lt;img src=&quot;https://images.nirvana.net.cn/static/images/images/10024246-19c5b0c2df166ebe.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2Awdb</summary>
      
    
    
    
    <category term="前端" scheme="https://www.nirvana.net.cn/categories/%E5%89%8D%E7%AB%AF/"/>
    
    
    <category term="node" scheme="https://www.nirvana.net.cn/tags/node/"/>
    
    <category term="web" scheme="https://www.nirvana.net.cn/tags/web/"/>
    
  </entry>
  
  <entry>
    <title>关于优化 LCP 的常见误解</title>
    <link href="https://www.nirvana.net.cn/2024/1018DYOL5zzWWW.html"/>
    <id>https://www.nirvana.net.cn/2024/1018DYOL5zzWWW.html</id>
    <published>2024-10-18T08:28:03.000Z</published>
    <updated>2024-10-27T08:45:58.418Z</updated>
    
    <content type="html"><![CDATA[<p>网页的 <a href="https://web.dev/articles/lcp?hl=zh-cn">Largest Contentful Paint (LCP)</a> 可能很难改进，通常需要涉及多个动态部分并做出取舍。此博文着眼于整个网络中实际网页加载的实测数据，以确定开发者应重点优化哪些方面。</p><h2 id="经典-LCP-建议：缩减图片大小！"><a href="#经典-LCP-建议：缩减图片大小！" class="headerlink" title="经典 LCP 建议：缩减图片大小！"></a>经典 LCP 建议：缩减图片大小！</h2><p>对于网络上的大多数网页，LCP 元素都是图片。因此，人们自然会认为改善 LCP 的最佳方法是优化 LCP 图片。</p><p>在 LCP 问世后的大约五年里，这一直是大势所趋。请确保图片尺寸合适且经过充分压缩，在制作图片时也可以采用 21 世纪的图片格式。Lighthouse 甚至有<a href="https://developer.chrome.com/docs/lighthouse/performance/uses-optimized-images?hl=zh-cn">三种</a> <a href="https://developer.chrome.com/docs/lighthouse/performance/uses-webp-images?hl=zh-cn">不同的</a> <a href="https://developer.chrome.com/docs/lighthouse/performance/uses-responsive-images?hl=zh-cn">审核</a>来提出这些建议。<br><img src="https://images.nirvana.net.cn/static/images/images/10024246-f811f36c1269e5e6.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="><br>之所以如此常见建议，原因之一在于：过多的字节数易于测量，而图像压缩工具容易推荐使用。根据您的构建和部署流水线，实现起来可能也很简单。</p><p>如果有，就去执行吧！向用户发送的字节数很少会带来优势。网络上有<em>许多</em>网站仍在提供不必要的大图片，即使进行基本压缩就可以解决问题。</p><p>然而，当我们开始查看 Chrome 中用户的现场性能数据，了解 LCP 时间通常都花在哪些地方时，我们发现图片下载时间几乎从未成为瓶颈。</p><p>相反，LCP 的其他部分是一个大得多的问题。</p><h2 id="LCP-子部分细分"><a href="#LCP-子部分细分" class="headerlink" title="LCP 子部分细分"></a>LCP 子部分细分</h2><p>为了了解改进 LCP 的最大机会领域，我们查看了 LCP 子部分的数据，如<a href="https://web.dev/articles/optimize-lcp?hl=zh-cn">优化 LCP</a> 中所述。</p><p>虽然每个网页和每个框架都可能会采用不同的方法来加载和显示将成为网页 LCP 元素的内容，但每个网页都可以分为以下子部分：<br><img src="https://images.nirvana.net.cn/static/images/images/10024246-90460c448b3f58e3.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="></p><p><a href="https://web.dev/articles/optimize-lcp?hl=zh-cn#lcp-breakdown:~:text=following%20sub-parts:-,Time%20to%20First%20Byte%20(TTFB),the%20LCP%20resource%20finishes%20loading%20and%20the%20LCP%20element%20rendering%20fully.,-Every%20page's%20LCP">引用该文章</a>中的各子部分如下：</p><ul><li>首字节时间 (TTFB)<br>从用户开始加载网页到浏览器加载网页之间的时间 接收 HTML 文档响应的第一个字节。</li><li>资源加载延迟<br>从 TTFB 到浏览器开始加载 LCP 资源所用的时间。如果 LCP 元素不需要加载资源即可渲染，现为 0。</li><li>资源加载时长<br>加载 LCP 资源本身所用的时长。如果 LCP 元素不需要加载资源即可渲染，时间为 0。</li><li>元素渲染延迟<br>从 LCP 资源完成加载到 LCP 元素完成加载之间的时间 才能完全呈现</li></ul><h2 id="真实的导航性能数据"><a href="#真实的导航性能数据" class="headerlink" title="真实的导航性能数据"></a>真实的导航性能数据</h2><p><img src="https://images.nirvana.net.cn/static/images/images/10024246-2545d45571e722cf.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="></p><p>| LCP 分级 | TTFB（毫秒） | 图片加载延迟（毫秒） | 图片加载时长（毫秒） | 渲染延迟（毫秒） |<br>| 良好 | 600 | 350 | 160 | 230 |<br>| 需要改进 | 1,360,000 | 720 | 270 | 310 |<br>| 差 | 2,270,000 | 1290 | 350 | 360 |</p><p>在这篇博文中，我们使用了 Chrome 中带有子资源图片 LCP 的网页导航数据来查看 LCP 子部分。<a href="https://youtu.be/fWoI9DXmpdk?t=561">我们以前了解过此类数据</a>，但从未通过实测数据来了解真实用户在等待网页 LCP 时将时间花在了何处。<br><a href="https://web.dev/articles/defining-core-web-vitals-thresholds?hl=zh-cn#choice_of_percentile">与 Core Web Vitals 一样，对于 CrUX 数据集中每个来源的每个 LCP 子部分，我们选取了第 75 个百分位 (p75)</a>，从而得出 p75 值的 4 次分布（每个子部分各一个）。为了总结这些分布情况，我们针对四个 LCP 子部分分别取了所有源中这些值的中位数。</p><p>最后，我们根据源<a href="https://web.dev/articles/lcp?hl=zh-cn#what-is-a-good-lcp-score">是“良好”“需要改进”还是“较差”将源分成多个存储分区第 75 百分位的 LCP</a>。这有助于显示 LCP 良好与 LCP 不良的来源有何区别。</p><h2 id="要缩减-LCP-图片的大小吗？这次有数据"><a href="#要缩减-LCP-图片的大小吗？这次有数据" class="headerlink" title="要缩减 LCP 图片的大小吗？这次有数据"></a>要缩减 LCP 图片的大小吗？这次有数据</h2><p>加载时长用于衡量提取 LCP 资源（在本例中为图片）所需的时间。该时间通常与映像中的字节数成正比，因此所有减少该字节数的性能建议都是如此。</p><p>从前面的图表中时间趋势来看，有一点也值得注意，那就是图片加载时长没有花费很多时间。事实上，在所有 LCP 分桶中，它是最短的 LCP 子部分。与 LCP 性能良好的源相比，LCP 性能不佳的来源的加载时长更长，但这并不是耗费大量时间的因素。</p><p><strong>大多数 LCP 表现不佳的来源在下载 LCP 映像时所花的时间都不到其 p75 LCP 时间的 10%。</strong></p><p>是的，您应确保对图片进行优化，但这只是改进 LCP 的一个环节。从这些数据可以看出，对于网络上的典型来源，无论压缩方案有多复杂，LCP 整体上的潜在毫秒增益都很小。</p><p><strong>注意</strong>：请注意，LCP 的实测数据包括到网页的所有类型的导航，包括 LCP 资源已存在于浏览器缓存中的导航。这可以降低这些数字，但是用每个源站的 p75 加载时长过滤掉了很多这种情况（在访问您的网站的 75% 的导航中，虽然有 75% 的导航已在缓存中保存了 LCP 图片，但这种情况不太可能发生）。</p><p>最后令人惊讶的是，加载速度过慢通常是在移动设备上和移动网络的质量造成的。我们可能曾以为，普通手机要像使用有线连接的桌面设备一样要多次下载同一张图片。数据显示，情况不再如此。<strong>对于 LCP 较低的来源，P75 图片在移动设备上的加载时间中位数仅比桌面设备慢 20%。</strong></p><h2 id="首字节时间-TTFB"><a href="#首字节时间-TTFB" class="headerlink" title="首字节时间 (TTFB)"></a>首字节时间 (TTFB)</h2><p>对于发出网络请求的导航，TTFB 始终需要一些时间。执行 DNS 查找和启动连接需要花费一些时间。物理问题无与伦比：一项请求必须通过电线和光缆在现实世界中穿行才能到达服务器，然后响应必须返回该服务器。即使是具有良好 LCP 的中位数来源，在第 75 个百分位的 TTFB 上花费的时间也超过 0.5 秒。</p><p>然而，良好 LCP 源与不良 LCP 源之间的 TTFB 差异表明了改进空间。<strong>对于至少一半的 LCP 较差的来源来说，<em>仅</em> 2,270 毫秒的 p75 TTFB 几乎可以保证 p75 LCP 不会比 2.5 秒的“良好”快阈值。</strong>即便是该时间的适度缩短百分比，也意味着 LCP 会得到显著提升。</p><p>你可能无法通过物理验证，但可以做到这一点。例如，如果您的用户经常与您的服务器位于不同的位置，那么 CDN 可以使您的内容更靠近他们。</p><p>如需了解详情，请参阅<a href="https://web.dev/articles/optimize-ttfb?hl=zh-cn">优化 TTFB 指南</a>。</p><h2 id="资源加载延迟、LCP-运行缓慢的罪魁祸首是被忽视"><a href="#资源加载延迟、LCP-运行缓慢的罪魁祸首是被忽视" class="headerlink" title="资源加载延迟、LCP 运行缓慢的罪魁祸首是被忽视"></a>资源加载延迟、LCP 运行缓慢的罪魁祸首是被忽视</h2><p>如果 TTFB 可以改进，但任何改进都受物理特性的约束，那么资源加载延迟或许可以消除，实际上，仅受服务架构约束。</p><p>该子部分测量从收到 HTML 响应 (TTFB) 的第一个字节到浏览器启动 LCP 图片请求所用的时间。多年来，我们一直在关注下载 LCP 图片所需的时间，但我们经常忽略了在浏览器收到开始下载提示之前就浪费的时间。</p><p><strong>对于 LCP 表现不佳的中位数网站，其等待开始下载 LCP 图片所用的时间几乎是实际下载 LCP 图片的四倍</strong>，在 TTFB 和图片请求之间等待 1.3 秒。这超过了 2.5 秒 LCP 预算的一半以上。</p><p>依赖项链是导致加载延迟时间较长的常见原因。较为简单的一点是，页面加载样式表，在浏览器完成布局后，系统会设置一个背景图片，该图片最终将成为 LCP。所有这些步骤都必须在浏览器确定开始下载 LCP 图片之前完成。</p><p>使用 HTTP Archive 公开抓取数据，其中记录了“发起者”从 HTML 文档到 LCP 图片的网络请求链，您可以看到请求链长度与 LCP 速度较慢之间有明确的关联。</p><p><img src="https://images.nirvana.net.cn/static/images/images/10024246-47d134a89d254699.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="><br>关键是要尽快让浏览器知道 LCP 是什么，这样浏览器就能开始加载，即使在页面布局中有可用位置之前也是如此。您可以使用一些工具完成此操作，例如在 HTML 中使用经典的 <code>&lt;img&gt;</code> 标记（这样<a href="https://web.dev/articles/preload-scanner?hl=zh-cn">预加载扫描器可以快速找到它</a>并开始下载），或者使用 <code>&lt;link rel=&quot;preload&quot;&gt;</code> 标记（或 HTTP 标头）来标记不属于 <code>&lt;img&gt;</code> 的图片。</p><p>帮助浏览器确定要优先处理的资源也很重要。如果您的网页在网页加载期间发出了很多请求，这一点尤为重要，因为浏览器通常只有在其中许多资源都已加载且布局已加载完毕之后才会知道 LCP 元素是什么。使用 <a href="https://web.dev/articles/fetch-priority?hl=zh-cn#fetchpriority-attribute"><code>fetchpriority=&quot;high&quot;</code> 属性</a>为可能的 LCP 元素添加注解（并确保避免使用 <code>loading=&quot;lazy&quot;</code>），可让浏览器明确知道应立即开始加载资源。</p><p>阅读<a href="https://web.dev/articles/optimize-lcp?hl=zh-cn#1_eliminate_resource_load_delay">有关优化加载延迟的更多建议</a>。</p><h2 id="渲染延迟"><a href="#渲染延迟" class="headerlink" title="渲染延迟"></a>渲染延迟</h2><p>呈现延迟时间用于衡量从浏览器加载并准备就绪 LCP 图片开始的时间，但出于某种原因，图片在屏幕上显示时会有延迟。有时，这是一个耗时较长的任务，在图片准备就绪时阻塞主线程，而在其他情况下，这可能是显示隐藏元素的界面选择。</p><p>对于网络上的典型来源，呈现延迟似乎并不大，但在优化期间，有时您可以<em>创建</em>渲染延迟，让之前在其他子部分花费的时间占到。例如，如果网页开始预加载 LCP 图片以便快速加载，则不会再有很长的加载延迟，但如果网页本身尚未准备好显示图片（例如，如果网页本身尚未准备好显示图片（例如，从会阻止内容呈现的大型样式表或客户端渲染应用中必须加载完所有 JavaScript 才能显示），则 LCP 会比它应该变慢，而且等待时间也会显示为呈现延迟。正因如此，在 LCP 方面，服务器端呈现或静态 HTML 通常具有优势。</p><p>如果您自己的内容受到影响，请参阅<a href="https://web.dev/articles/optimize-lcp?hl=zh-cn#2_eliminate_element_render_delay">有关优化渲染延迟的更多建议</a>。</p><h2 id="请检查所有这些子部分"><a href="#请检查所有这些子部分" class="headerlink" title="请检查所有这些子部分"></a>请检查所有这些子部分</h2><p>很明显，要有效优化 LCP，开发者需要全面关注网页加载情况，而不仅仅是专注于优化图片。请每时每刻检查一次 LCP，因为目前改进的空间可能要大得多。</p><p>为了在字段中收集此类数据，<a href="https://github.com/GoogleChrome/web-vitals#attribution-build">web-vitals 库的归因 build</a> 中会包含 LCP 子部分的计时。<a href="https://developer.chrome.com/docs/crux?hl=zh-cn">Chrome 用户体验报告 (CrUX)</a> 尚未包含所有数据，但包含 TTFB 和 LCP 条目，因此是一个开始。我们希望将来在 CrUX 中包含这篇博文使用的数据，敬请关注更多相关新闻。</p><p>如需在本地测试 LCP 子部分，请尝试使用<a href="https://web.dev/articles/optimize-lcp?hl=zh-cn#monitor_lcp_breakdowns_using_the_web_vitals_extension">网页指标扩展程序</a>或<a href="https://web.dev/articles/optimize-lcp?hl=zh-cn#monitor_lcp_breakdown_in_javascript">本文中的 JavaScript 代码段</a>。Lighthouse <a href="https://developer.chrome.com/docs/lighthouse/performance/lighthouse-largest-contentful-paint?hl=zh-cn#how_to_improve_your_lcp_score">还在其“Largest Contentful Paint 元素”中添加了细分。审核</a>。您可以在开发者工具性能面板中查看更多 LCP 子部分建议（<a href="https://developer.chrome.com/blog/perf-tooling-2024?hl=zh-cn#the_power_of_lighthouse_in_the_performance_panel">即将推出</a>）。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;网页的 &lt;a href=&quot;https://web.dev/articles/lcp?hl=zh-cn&quot;&gt;Largest Contentful Paint (LCP)&lt;/a&gt; 可能很难改进，通常需要涉及多个动态部分并做出取舍。此博文着眼于整个网络中实际网页加载的实测数据，以确</summary>
      
    
    
    
    <category term="前端" scheme="https://www.nirvana.net.cn/categories/%E5%89%8D%E7%AB%AF/"/>
    
    
    <category term="web" scheme="https://www.nirvana.net.cn/tags/web/"/>
    
  </entry>
  
  <entry>
    <title>在Web 应用中处理 API 错误</title>
    <link href="https://www.nirvana.net.cn/2024/1015W6i2Vooyyy.html"/>
    <id>https://www.nirvana.net.cn/2024/1015W6i2Vooyyy.html</id>
    <published>2024-10-15T13:12:40.000Z</published>
    <updated>2024-10-27T08:45:58.336Z</updated>
    
    <content type="html"><![CDATA[<p><img src="https://images.nirvana.net.cn/static/images/images/10024246-3645ba725d660ff0.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="><br>你的 Web 应用是否遇到了网络问题？</p><p>你的 Web 应用是否面临服务器相关的问题？还是数据解析错误？</p><p>你需要强大的错误处理策略，确保你的 Web 应用保持响应迅速且用户友好！</p><p>现代 Web 应用需要最佳性能，以在不断发展的技术前沿竞争。像 React Native 这样的流行框架已成为企业在市场上建立品牌的有力资源。</p><p>虽然移动应用开发已成为首要任务，但开发过程中涉及的步骤在使应用为市场做好准备方面起着关键作用。这引入了 API 的使用，它可以为你的 Web 应用提供最出色的现代软件体验。</p><p>API，或应用程序编程接口，是用于构建软件应用的工具，它使开发者能够从服务器或数据库等第三方服务访问数据和功能。</p><p>然而，如果管理不当，API 错误会破坏用户体验，并导致潜在的数据丢失或应用崩溃。多亏了 React Native 应用开发服务，通过其强大的工具和实践，API 错误的处理变得顺畅。</p><p>在本博客中，我们将了解 API 在 React Native 应用开发中的角色，并深入探讨在 Web 应用开发过程中可能遇到的各种 API 错误类型，同时探索 React Native 如何帮助管理这些错误。</p><h2 id="API-在-React-Native-框架中的角色"><a href="#API-在-React-Native-框架中的角色" class="headerlink" title="API 在 React Native 框架中的角色"></a>API 在 React Native 框架中的角色</h2><p>应用程序编程接口是一组协议、例程和工具，帮助开发者构建能够轻松与外部服务和服务器交互的应用。</p><p>API 使应用能够从远程服务器获取数据，如用户信息或产品详情，并发送数据以进行用户注册或下订单等操作。</p><p>它们对于集成外部服务至关重要，例如处理用户身份验证、执行 CRUD（创建、读取、更新、删除）操作，并通过 Websockets 等技术确保实时更新。</p><p>通过为应用提供与各种外部资源交互的结构化方式，API 提升了 React Native Web 应用的功能性、可扩展性和用户体验。</p><h2 id="什么是-Web-应用中的-API-错误处理？"><a href="#什么是-Web-应用中的-API-错误处理？" class="headerlink" title="什么是 Web 应用中的 API 错误处理？"></a>什么是 Web 应用中的 API 错误处理？</h2><p>API 错误处理指的是在应用与 API 交互时管理和响应错误的过程。这包括检测 API 请求失败时，识别错误类型，并实施策略以优雅地处理这些错误，确保应用仍然功能正常并提供良好的用户体验。</p><h2 id="React-Native-中-Web-应用-API-错误处理的关键方面"><a href="#React-Native-中-Web-应用-API-错误处理的关键方面" class="headerlink" title="React Native 中 Web 应用 API 错误处理的关键方面"></a>React Native 中 Web 应用 API 错误处理的关键方面</h2><h4 id="1-错误检测："><a href="#1-错误检测：" class="headerlink" title="1. 错误检测："></a>1. 错误检测：</h4><ul><li>网络错误：当应用无法连接到 API 服务器时出现，通常由于网络连接问题或服务器停机。</li><li>HTTP 状态码：API 响应包含指示成功或失败的状态码。常见的错误码包括 400（错误请求）、401（未授权）、404（未找到）和 500（内部服务器错误）。</li><li>超时错误：当 API 请求响应时间过长时出现，可能是由于网络条件不佳或服务器性能问题。</li><li>解析错误：当应用无法正确解释从 API 接收到的数据时发生，通常是由于数据格式不符合预期或响应损坏。<h4 id="2-错误处理策略："><a href="#2-错误处理策略：" class="headerlink" title="2. 错误处理策略："></a>2. 错误处理策略：</h4></li><li>Try-Catch 块：在异步代码中使用 try-catch 块（例如 async/await），以捕获错误并适当地处理它们。</li><li>检查 HTTP 状态码：检查 API 响应中的状态码，以确定错误类型并采取适当的行动。</li><li>重试逻辑：实施重试机制，在失败后再次尝试 API 请求，特别适用于网络问题等暂时性错误。</li><li>优雅降级：确保在发生 API 错误时，应用仍然能够在有限的情况下运行，例如显示缓存数据或提供默认内容。</li><li>用户反馈：显示用户友好的错误消息，并给出继续操作或重试的指示。</li><li>日志记录与监控：记录错误日志并监控它们，以识别和解决反复出现的问题，或提高应用的错误弹性。<br>####3. 示例实现：<br>以下是一个简单的示例，演示了如何在使用 Axios 库的 React Native 应用中处理 API 错误：</li></ul><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><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">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> <span class="title class_">React</span>, &#123; useEffect, 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; <span class="title class_">View</span>, <span class="title class_">Text</span>, <span class="title class_">ActivityIndicator</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><br><span class="line"><span class="keyword">const</span> <span class="variable constant_">API_URL</span> = <span class="string">&#x27;https://api.example.com/data&#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> [data, setData] = <span class="title function_">useState</span>(<span class="literal">null</span>);</span><br><span class="line">  <span class="keyword">const</span> [loading, setLoading] = <span class="title function_">useState</span>(<span class="literal">true</span>);</span><br><span class="line">  <span class="keyword">const</span> [error, setError] = <span class="title function_">useState</span>(<span class="literal">null</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> <span class="title function_">fetchData</span> = <span class="keyword">async</span> (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">      <span class="keyword">try</span> &#123;</span><br><span class="line">        <span class="keyword">const</span> response = <span class="keyword">await</span> axios.<span class="title function_">get</span>(<span class="variable constant_">API_URL</span>);</span><br><span class="line">        <span class="title function_">setData</span>(response.<span class="property">data</span>);</span><br><span class="line">      &#125; <span class="keyword">catch</span> (err) &#123;</span><br><span class="line">        <span class="title function_">handleApiError</span>(err);</span><br><span class="line">      &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">        <span class="title function_">setLoading</span>(<span class="literal">false</span>);</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;;</span><br><span class="line"></span><br><span class="line">    <span class="title function_">fetchData</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_">handleApiError</span> = (<span class="params">error</span>) =&gt; &#123;</span><br><span class="line">    <span class="keyword">if</span> (error.<span class="property">response</span>) &#123;</span><br><span class="line">      <span class="comment">// 服务器返回了非 200 范围的状态码</span></span><br><span class="line">      <span class="title function_">setError</span>(<span class="string">`Error: <span class="subst">$&#123;error.response.status&#125;</span> <span class="subst">$&#123;error.response.statusText&#125;</span>`</span>);</span><br><span class="line">      <span class="title class_">Alert</span>.<span class="title function_">alert</span>(<span class="string">&#x27;API 错误&#x27;</span>, <span class="string">`服务器错误：<span class="subst">$&#123;error.response.status&#125;</span>`</span>);</span><br><span class="line">    &#125; <span class="keyword">else</span> <span class="keyword">if</span> (error.<span class="property">request</span>) &#123;</span><br><span class="line">      <span class="comment">// 请求已发送但未收到响应</span></span><br><span class="line">      <span class="title function_">setError</span>(<span class="string">&#x27;网络错误：未收到响应&#x27;</span>);</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="comment">// 在设置请求时发生错误</span></span><br><span class="line">      <span class="title function_">setError</span>(<span class="string">`错误：<span class="subst">$&#123;error.message&#125;</span>`</span>);</span><br><span class="line">      <span class="title class_">Alert</span>.<span class="title function_">alert</span>(<span class="string">&#x27;错误&#x27;</span>, error.<span class="property">message</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> (loading) &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="language-xml"><span class="tag">&lt;<span class="name">ActivityIndicator</span> /&gt;</span></span>;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (error) &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="language-xml"><span class="tag">&lt;<span class="name">Text</span>&gt;</span>&#123;error&#125;<span class="tag">&lt;/<span class="name">Text</span>&gt;</span></span>;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">View</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;JSON.stringify(data)&#125;<span class="tag">&lt;/<span class="name">Text</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">View</span>&gt;</span></span></span><br><span class="line">  );</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="title class_">App</span>;</span><br></pre></td></tr></table></figure><p>在这个例子中，API 错误处理被实现，用于检测各种类型的错误，提供用户反馈，并确保应用在发生错误时仍保持响应。</p><h2 id="在使用-React-Native-处理-Web-应用中的-API-错误时需要的步骤"><a href="#在使用-React-Native-处理-Web-应用中的-API-错误时需要的步骤" class="headerlink" title="在使用 React Native 处理 Web 应用中的 API 错误时需要的步骤"></a>在使用 React Native 处理 Web 应用中的 API 错误时需要的步骤</h2><p>为了检测、管理和响应错误，React Native 遵循一系列步骤，以确保流畅的用户体验和强大的应用功能。让我们深入了解这些步骤：</p><h4 id="步骤-1：设置-API-请求"><a href="#步骤-1：设置-API-请求" class="headerlink" title="步骤 1：设置 API 请求"></a>步骤 1：设置 API 请求</h4><p>首先，你需要配置你的 React Native Web 应用以发出 API 请求。可以使用像 Axios 这样的流行 HTTP 客户端库，它简化了 HTTP 请求的处理。接下来配置 API 端点的基本 URL，以简化你的请求。</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="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="keyword">const</span> <span class="variable constant_">API_URL</span> = <span class="string">&#x27;https://api.example.com&#x27;</span>;</span><br></pre></td></tr></table></figure><h4 id="步骤-2：发出-API-请求"><a href="#步骤-2：发出-API-请求" class="headerlink" title="步骤 2：发出 API 请求"></a>步骤 2：发出 API 请求</h4><p>在发出 API 请求时，开发人员必须使用异步函数来处理异步操作。将 API 请求包装在 try-catch 块中，以捕获在请求执行期间发生的任何错误。</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="title function_">fetchData</span> = <span class="keyword">async</span> (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">try</span> &#123;</span><br><span class="line">    <span class="keyword">const</span> response = <span class="keyword">await</span> axios.<span class="title function_">get</span>(<span class="string">`<span class="subst">$&#123;API_URL&#125;</span>/data`</span>);</span><br><span class="line">    <span class="keyword">return</span> response.<span class="property">data</span>;</span><br><span class="line">  &#125; <span class="keyword">catch</span> (error) &#123;</span><br><span class="line">    <span class="keyword">throw</span> error;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><h4 id="步骤-3：检测错误"><a href="#步骤-3：检测错误" class="headerlink" title="步骤 3：检测错误"></a>步骤 3：检测错误</h4><p>检测错误涉及检查 HTTP 状态码、网络错误和响应解析错误。例如，400、401、404 和 500 等 HTTP 状态码可以指示不同类型的错误。在 catch 块中实现错误检测：</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">try</span> &#123;</span><br><span class="line">  <span class="keyword">const</span> response = <span class="keyword">await</span> axios.<span class="title function_">get</span>(<span class="string">`<span class="subst">$&#123;API_URL&#125;</span>/data`</span>);</span><br><span class="line">  <span class="keyword">return</span> response.<span class="property">data</span>;</span><br><span class="line">&#125; <span class="keyword">catch</span> (error) &#123;</span><br><span class="line">  <span class="keyword">if</span> (error.<span class="property">response</span>) &#123;</span><br><span class="line">    <span class="comment">// 服务器返回了非 200 范围的状态码</span></span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`Error: <span class="subst">$&#123;error.response.status&#125;</span> <span class="subst">$&#123;error.response.statusText&#125;</span>`</span>);</span><br><span class="line">  &#125; <span class="keyword">else</span> <span class="keyword">if</span> (error.<span class="property">request</span>) &#123;</span><br><span class="line">    <span class="comment">// 请求已发送但未收到响应</span></span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">&#x27;网络错误：未收到响应&#x27;</span>);</span><br><span class="line">  &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    <span class="comment">// 在设置请求时发生错误</span></span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`错误：<span class="subst">$&#123;error.message&#125;</span>`</span>);</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="步骤-4：实施错误处理逻辑"><a href="#步骤-4：实施错误处理逻辑" class="headerlink" title="步骤 4：实施错误处理逻辑"></a>步骤 4：实施错误处理逻辑</h4><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="title function_">handleApiError</span> = (<span class="params">error</span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">if</span> (error.<span class="property">response</span>) &#123;</span><br><span class="line">    <span class="keyword">switch</span> (error.<span class="property">response</span>.<span class="property">status</span>) &#123;</span><br><span class="line">      <span class="keyword">case</span> <span class="number">401</span>:</span><br><span class="line">        <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">Error</span>(<span class="string">&#x27;未授权访问&#x27;</span>);</span><br><span class="line">      <span class="keyword">case</span> <span class="number">404</span>:</span><br><span class="line">        <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">Error</span>(<span class="string">&#x27;资源未找到&#x27;</span>);</span><br><span class="line">      <span class="attr">default</span>:</span><br><span class="line">        <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">Error</span>(<span class="string">`服务器错误：<span class="subst">$&#123;error.response.status&#125;</span>`</span>);</span><br><span class="line">    &#125;</span><br><span class="line">  &#125; <span class="keyword">else</span> <span class="keyword">if</span> (error.<span class="property">request</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">&#x27;网络错误：未收到响应&#x27;</span>);</span><br><span class="line">  &#125; <span class="keyword">else</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">`错误：<span class="subst">$&#123;error.message&#125;</span>`</span>);</span><br><span class="line">  &#125;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><h4 id="步骤-5：重试机制"><a href="#步骤-5：重试机制" class="headerlink" title="步骤 5：重试机制"></a>步骤 5：重试机制</h4><p>为像网络问题这样的暂时性错误实施重试机制，在请求失败后再尝试几次 API 请求。使用指数退避策略来增加重试的间隔时间：</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">const</span> <span class="title function_">fetchDataWithRetry</span> = <span class="keyword">async</span> (<span class="params">retryCount = <span class="number">0</span></span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">try</span> &#123;</span><br><span class="line">    <span class="keyword">const</span> response = <span class="keyword">await</span> axios.<span class="title function_">get</span>(<span class="string">`<span class="subst">$&#123;API_URL&#125;</span>/data`</span>);</span><br><span class="line">    <span class="keyword">return</span> response.<span class="property">data</span>;</span><br><span class="line">  &#125; <span class="keyword">catch</span> (error) &#123;</span><br><span class="line">    <span class="keyword">if</span> (retryCount &lt; <span class="number">3</span>) &#123;</span><br><span class="line">      <span class="built_in">setTimeout</span>(<span class="function">() =&gt;</span> <span class="title function_">fetchDataWithRetry</span>(retryCount + <span class="number">1</span>), <span class="number">2</span> ** retryCount * <span class="number">1000</span>);</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">      <span class="title function_">handleApiError</span>(error);</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="6-优雅降级和回退"><a href="#6-优雅降级和回退" class="headerlink" title="6. 优雅降级和回退"></a>6. 优雅降级和回退</h4><p>确保当集成到 React Native 中的 API 调用失败时，你的 Web 应用仍然能够在有限的情况下运行，提供默认内容或缓存数据。显示用户友好的错误信息和后续操作建议：</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></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="keyword">const</span> [data, setData] = <span class="title function_">useState</span>(<span class="literal">null</span>);</span><br><span class="line">  <span class="keyword">const</span> [loading, setLoading] = <span class="title function_">useState</span>(<span class="literal">true</span>);</span><br><span class="line">  <span class="keyword">const</span> [error, setError] = <span class="title function_">useState</span>(<span class="literal">null</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> <span class="title function_">loadData</span> = <span class="keyword">async</span> (<span class="params"></span>) =&gt; &#123;</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_">fetchDataWithRetry</span>();</span><br><span class="line">        <span class="title function_">setData</span>(result);</span><br><span class="line">      &#125; <span class="keyword">catch</span> (err) &#123;</span><br><span class="line">        <span class="title function_">setError</span>(err.<span class="property">message</span>);</span><br><span class="line">      &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">        <span class="title function_">setLoading</span>(<span class="literal">false</span>);</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;;</span><br><span class="line"></span><br><span class="line">    <span class="title function_">loadData</span>();</span><br><span class="line">  &#125;, []);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (loading) &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="language-xml"><span class="tag">&lt;<span class="name">ActivityIndicator</span> /&gt;</span></span>;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (error) &#123;</span><br><span class="line">    <span class="keyword">return</span> (</span><br><span class="line">      <span class="language-xml"><span class="tag">&lt;<span class="name">View</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">Text</span>&gt;</span>&#123;error&#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 class="attr">title</span>=<span class="string">&quot;重试&quot;</span> <span class="attr">onPress</span>=<span class="string">&#123;()</span> =&gt;</span> loadData()&#125; /&gt;</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">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">View</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">Text</span>&gt;</span>数据: &#123;JSON.stringify(data)&#125;<span class="tag">&lt;/<span class="name">Text</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">View</span>&gt;</span></span></span><br><span class="line">  );</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><h4 id="7-日志记录和监控"><a href="#7-日志记录和监控" class="headerlink" title="7. 日志记录和监控"></a>7. 日志记录和监控</h4><p>实现日志记录和监控，以跟踪错误及其发生情况。使用如 Sentry 或 Firebase Crashlytics 这样的工具来监控和分析生产环境中的错误。将错误记录到集中位置，以便更轻松地进行调试：</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="title function_">handleApiError</span> = (<span class="params">error</span>) =&gt; &#123;</span><br><span class="line">  <span class="comment">// 将错误记录到外部服务</span></span><br><span class="line">  <span class="title function_">logErrorToService</span>(error);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (error.<span class="property">response</span>) &#123;</span><br><span class="line">    <span class="keyword">switch</span> (error.<span class="property">response</span>.<span class="property">status</span>) &#123;</span><br><span class="line">      <span class="keyword">case</span> <span class="number">401</span>:</span><br><span class="line">        <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">Error</span>(<span class="string">&#x27;未经授权的访问&#x27;</span>);</span><br><span class="line">      <span class="keyword">case</span> <span class="number">404</span>:</span><br><span class="line">        <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">Error</span>(<span class="string">&#x27;资源未找到&#x27;</span>);</span><br><span class="line">      <span class="attr">default</span>:</span><br><span class="line">        <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">Error</span>(<span class="string">`服务器错误: <span class="subst">$&#123;error.response.status&#125;</span>`</span>);</span><br><span class="line">    &#125;</span><br><span class="line">  &#125; <span class="keyword">else</span> <span class="keyword">if</span> (error.<span class="property">request</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">&#x27;网络错误：未收到响应&#x27;</span>);</span><br><span class="line">  &#125; <span class="keyword">else</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">`错误: <span class="subst">$&#123;error.message&#125;</span>`</span>);</span><br><span class="line">  &#125;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><h4 id="8-用户反馈和通知"><a href="#8-用户反馈和通知" class="headerlink" title="8. 用户反馈和通知"></a>8. 用户反馈和通知</h4><p>为用户提供清晰简洁的错误信息，并在等待 API 响应时显示加载指示器。允许用户重试导致错误的操作：</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">if</span> (loading) &#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="language-xml"><span class="tag">&lt;<span class="name">ActivityIndicator</span> /&gt;</span></span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> (error) &#123;</span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">View</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">Text</span>&gt;</span>&#123;error&#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 class="attr">title</span>=<span class="string">&quot;重试&quot;</span> <span class="attr">onPress</span>=<span class="string">&#123;()</span> =&gt;</span> loadData()&#125; /&gt;</span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">View</span>&gt;</span></span></span><br><span class="line">  );</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>通过遵循这些步骤，你可以在 React Native Web 应用中有效处理 API 错误，确保应用具有强大且用户友好的功能。</p><h2 id="结论"><a href="#结论" class="headerlink" title="结论"></a>结论</h2><p>在使用 React Native 开发服务的 Web 应用中处理 API 错误对于创建稳健、用户友好的应用至关重要。通过实施全面的错误检测、利用 try-catch 块并提供用户友好的反馈，开发者可以确保其应用保持可靠和响应迅速。</p><p>集成重试机制和优雅降级策略进一步增强了用户体验，即使在网络或服务器问题出现时也是如此。最终，有效的 API 错误处理不仅提高了应用的稳定性，还能建立用户信任和满意度。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;&lt;img src=&quot;https://images.nirvana.net.cn/static/images/images/10024246-3645ba725d660ff0.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2Awdb</summary>
      
    
    
    
    <category term="react-native" scheme="https://www.nirvana.net.cn/categories/react-native/"/>
    
    
  </entry>
  
  <entry>
    <title>如何使用 Jest 测试 TypeScript 应用</title>
    <link href="https://www.nirvana.net.cn/2024/10156SLVL55CCa.html"/>
    <id>https://www.nirvana.net.cn/2024/10156SLVL55CCa.html</id>
    <published>2024-10-15T08:28:03.000Z</published>
    <updated>2024-10-27T08:45:58.437Z</updated>
    
    <content type="html"><![CDATA[<p><img src="https://images.nirvana.net.cn/static/images/images/10024246-7c73370044e88f3a.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="><br>自动化测试是软件工程中的一个重要方面。在 JavaScript 生态系统中，TypeScript 通过在 JavaScript 中添加类型提供了额外的保障。在测试方面，Jest 一直是 JavaScript 的事实标准测试框架。</p><p>本指南将向你展示如何使用 Jest 为 TypeScript 应用编写有效的单元测试。</p><h3 id="什么是-TypeScript？"><a href="#什么是-TypeScript？" class="headerlink" title="什么是 TypeScript？"></a>什么是 TypeScript？</h3><p>根据官网的描述，TypeScript 是“带有类型语法的 JavaScript”。由微软开发的 TypeScript 是 JavaScript 的一个超集，具有强类型、面向对象的特性，并且拥有更好的 IDE 支持。</p><p>TypeScript 代码不能直接运行；代码以 TypeScript 编写（.ts 文件），然后通过 TypeScript 编译器将其编译为 JavaScript（.js 文件），编译后的代码才能执行。由于这种编译过程，你可以根据需要将 TypeScript 编译为较旧版本的 JavaScript。近年来，TypeScript 的流行度显著上升。根据 2022 年的《State of JS》调查，28% 的受访者一直使用 TypeScript 编写代码，而始终使用 JavaScript 编写代码的仅有 11%。TypeScript 的另一个优势是，它可以用于前端和后端（如 Node.js）应用程序。</p><p>在 JavaScript 这种动态语言中引入类型，使得软件在执行时更加安全。借助 TypeScript，在 IDE 中对类型的支持可以让软件工程师清楚地了解可用的内容。例如，使用 GitHub API 的类型化 SDK 时，你将准确知道在请求中发送哪些参数以及响应中会有哪些字段。必填参数和可选参数都会在类型中定义，帮助你快速找到答案，而无需从文档中搜索。接下来，我们将讨论自动化测试的重要性。</p><h3 id="为什么自动化测试很重要？"><a href="#为什么自动化测试很重要？" class="headerlink" title="为什么自动化测试很重要？"></a>为什么自动化测试很重要？</h3><p>通常，任何编写的代码都会被测试，关键在于何时以及如何测试代码的输出。测试 Web 应用程序最基本的方法是通过 Web 浏览器访问 URL，然后通过目视验证输出是否正确。通常，软件工程师编写代码后会重新加载浏览器标签页，以查看结果是否符合预期。这是一种手动测试形式。</p><p>根据项目的规模和资源的分配，可能会有专门的质量保证（QA）工程师或部门在软件发布前进行这种类型的测试。使用功能开关可以帮助减少如果出错时的影响范围（功能开关将在另一篇文章中讨论）。</p><p>手动测试的问题在于反馈周期太长。工程师需要修改代码，切换到浏览器，刷新页面，查看代码修改是否生效。这时，自动化单元测试就非常有用。如果设置正确，并且软件工程师实践测试驱动开发（TDD），他们会先编写测试，然后编写代码。这也被称为 TDD 的“红绿重构”循环。</p><p>首先，作为软件工程师，你会编写一个失败的测试（红色），因为此时尚未编写执行工作的代码；接着编写最少的代码使测试通过（绿色）。然后，在不破坏单元测试的前提下重构代码并/或编写实际的实现。</p><p>需要注意的是，单元测试只测试一段代码。通常，这段代码是一个函数，因此测试非常专注、快速，并且不依赖于外部因素（如网络和文件系统）。如果测试以监听模式运行，当代码发生变化时会重新运行测试，反馈周期可以缩短到毫秒级。如果在 IDE 中运行测试，几乎不需要进行上下文切换。这不仅提高了开发者的生产力，还提升了软件质量。</p><p>编写测试的最重要原因是减少到达最终用户的 bug 数量。为实现这一主要目标，有多种形式和层次的测试可以帮助你。像 TDD 和单元测试这样的实践也有助于创建良好的开发者体验。自动化单元测试增加了代码按预期运行的信心。当你更改现有代码或添加新功能时，这也很有用，因为运行整个测试套件可以捕获引入的任何回归。</p><p>简而言之，自动化单元测试由于其快速的反馈循环更具优势，并且可以重复进行，且结果一致。如果你想提高软件质量，仅依赖手动测试是不可扩展的。通过自动化测试覆盖大部分用例，并通过手动测试验证主流程是否正常工作。</p><p>在 JavaScript 和 TypeScript 中，Jest 是使用最广泛的测试框架</p><h3 id="示例应用"><a href="#示例应用" class="headerlink" title="示例应用"></a>示例应用</h3><p>在本指南中，你将为一个使用 Express.js 构建的简单名言 API 编写测试。它将每页提供 10 条信息，最终输出将如下所示：<br><img src="https://images.nirvana.net.cn/static/images/images/10024246-308ec81d7d4fa4e9.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="></p><h3 id="使用-Express-js-和-TypeScript-构建的-Quotes-API-输出"><a href="#使用-Express-js-和-TypeScript-构建的-Quotes-API-输出" class="headerlink" title="使用 Express.js 和 TypeScript 构建的 Quotes API 输出"></a>使用 Express.js 和 TypeScript 构建的 Quotes API 输出</h3><p>这是一个简单的 API，它从数组中返回模拟的静态数据。由于本教程的重点是编写测试，因此它没有连接数据库。应用程序的结构如下，没有包含测试部分：<br><img src="https://images.nirvana.net.cn/static/images/images/10024246-9ef12574f2741a58.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="></p><h4 id="使用-Express-js-和-TypeScript-构建的-Quotes-API-应用结构"><a href="#使用-Express-js-和-TypeScript-构建的-Quotes-API-应用结构" class="headerlink" title="使用 Express.js 和 TypeScript 构建的 Quotes API 应用结构"></a>使用 Express.js 和 TypeScript 构建的 Quotes API 应用结构</h4><p>TypeScript 的设置与本指南中进行的设置类似。大部分代码位于 <code>src</code> 文件夹中，其中包含 <code>QuotesController</code> 和 <code>QuotesService</code>，分别位于 <code>controllers</code> 和 <code>services</code> 文件夹中。应用程序中有一个名为 <code>Quote</code> 的自定义类型，它包含 <code>id</code>、<code>quote</code> 和 <code>author</code> 等属性。</p><p>应用程序的入口点是 <code>index.js</code>，<code>app.js</code> 文件中有一个 <code>App</code> 类，它实例化了 Express 并将控制器和路由结合在一起。</p><p>所有代码都可以在此 <a href="https://github.com/geshan/typescript-jest">GitHub 仓库</a> 中查看，你也可以访问部署在 Render 上的工作示例应用程序。</p><p>此应用程序使用 TypeDI 进行依赖注入。此外，它还使用了 NPM 的 <code>concurrently</code> 包，以同时运行 TypeScript 编译器和 Nodemon 下的服务器，具体命令可以在 <code>package.json</code> 中查看。你可以通过运行以下命令克隆不带测试或 Jest 的分支：</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">git clone -b no-tests-or-jest git@github.com:geshan/typescript-jest.git</span><br></pre></td></tr></table></figure><p>进入目录 <code>cd typescript-jest</code> 并使用 <code>npm install</code> 安装所需的 NPM 模块。要以开发模式运行项目，可以运行：</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">npm run dev</span><br></pre></td></tr></table></figure><p>然后在浏览器的地址栏中输入 <code>http://localhost:3000/api/quotes</code>，你将看到如下的结果：<br><img src="https://images.nirvana.net.cn/static/images/images/10024246-9dfa5a98b99d142b.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="></p><h3 id="单元测试与依赖注入的关系"><a href="#单元测试与依赖注入的关系" class="headerlink" title="单元测试与依赖注入的关系"></a>单元测试与依赖注入的关系</h3><p>单元测试专注于被测试的单元，即函数或类。单元测试中，任何外部的依赖都应该被模拟（mock）。这些依赖不仅包括网络调用和文件系统访问，还包括其他类的依赖或不同类的方法调用。这就是单元测试与依赖注入的交汇点。</p><p>依赖注入是一种设计模式，任何在类中使用的依赖项都不会在类内部实例化，而是从外部源注入。这个概念源自控制反转（IoC）范式，旨在创建松耦合的软件。通过依赖注入容器将任何依赖注入类中，可以轻松地为单元测试提供模拟类。</p><p>在下面的示例中，你将看到如何在测试 <code>QuotesController</code> 时注入并使用模拟的 <code>QuotesService</code> 类。</p><p>这就是为什么编写可测试代码是编写有用测试的基础。</p><p>依赖注入是编写可测试的面向对象代码的基石。在使用 Jest 和 TypeScript 的示例中，使用了 TypeDI 库进行依赖注入。你可以在这篇 <a href="https://github.com/typestack/typedi">TypeDI 教程</a> 中学习更多相关知识。在下一部分中，你将安装、配置并使用 Jest 来为编写单元测试做准备。</p><h3 id="安装-Jest"><a href="#安装-Jest" class="headerlink" title="安装 Jest"></a>安装 Jest</h3><p>在代码层面上，你已经克隆了 <code>no-tests-or-jest</code> 分支。在此分支中没有任何测试，也没有安装 Jest。你将安装 Jest 并为 TypeScript 测试进行配置。</p><p>要安装支持 TypeScript 的 Jest，可以执行以下命令：</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">npm install -D jest @types/jest ts-jest</span><br></pre></td></tr></table></figure><p>上述命令安装了 Jest 及其类型文件。Jest 是主要的测试库，同时还添加了 Jest 的相关类型文件。它还安装了 <code>ts-jest</code>，这是一个带有源映射支持的转换器，用于帮助你使用 Jest 测试 TypeScript 项目。如果你感兴趣，可以阅读更多关于 <a href="https://kulshekhar.github.io/ts-jest/">ts-jest 的文档</a>.</p><p>你可以在 <code>package.json</code> 文件中添加一个名为 <code>jest</code> 的新配置键来配置 Jest，配置如下：</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></pre></td><td class="code"><pre><span class="line"><span class="attr">&quot;jest&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;moduleFileExtensions&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">    <span class="string">&quot;js&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;ts&quot;</span></span><br><span class="line">  <span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;rootDir&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;testMatch&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">    <span class="string">&quot;/test/**/*.(spec|test).ts?(x)&quot;</span></span><br><span class="line">  <span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;transform&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;^.+\\.(t|j)s$&quot;</span><span class="punctuation">:</span> <span class="string">&quot;ts-jest&quot;</span></span><br><span class="line">  <span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;coverageReporters&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">    <span class="string">&quot;text&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 class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;collectCoverageFrom&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">    <span class="string">&quot;**/src/**/**/*.&#123;ts,tsx,js,jsx&#125;&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="string">&quot;!app.ts&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="string">&quot;!index.ts&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="string">&quot;!dist/**&quot;</span></span><br><span class="line">  <span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;coverageDirectory&quot;</span><span class="punctuation">:</span> <span class="string">&quot;/test/.coverage&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;testEnvironment&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;setupFiles&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span><span class="string">&quot;/test/setup.ts&quot;</span><span class="punctuation">]</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p>Jest 有许多配置选项。上面的配置首先定义了 <code>moduleFileExtensions</code>，这是一个数组，用于定义应用模块中的文件扩展名。对于这个示例应用，<code>.ts</code>、<code>.js</code> 和 <code>.json</code> 已经足够。接着，<code>rootDir</code> 被设置为项目的根目录 <code>/</code>，Jest 配置文件和 <code>package.json</code> 都放在这里。之后，测试文件预计位于根目录的 <code>/test</code> 目录中，并带有 <code>.spec.ts</code> 或 <code>.test.ts</code> 后缀，同时支持 <code>.tsx</code> 文件扩展名。</p><p>Jest 会将项目的代码作为 JavaScript 运行，因此需要通过 <code>ts-jest</code> 将 TypeScript 代码编译为 JavaScript。之后，测试覆盖率报告将以文本和 HTML 格式提供。覆盖范围应用于 <code>src</code> 文件夹中的 <code>.ts</code>、<code>.tsx</code>、<code>.js</code> 和 <code>.jsx</code> 文件，排除了 <code>app.ts</code> 和 <code>index.ts</code> 文件以及 <code>dist</code> 文件夹。覆盖率报告将放置在 <code>/test/.coverage</code> 文件夹中。Jest 将在 Node 环境中运行，并在每个测试文件之前执行 <code>/test/setup.ts</code> 文件。</p><p>你还可以在 <code>package.json</code> 的 <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></pre></td><td class="code"><pre><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;test:watch&quot;</span><span class="punctuation">:</span> <span class="string">&quot;jest --watch&quot;</span><span class="punctuation">,</span></span><br><span class="line"><span class="attr">&quot;test:cov&quot;</span><span class="punctuation">:</span> <span class="string">&quot;jest --coverage&quot;</span></span><br></pre></td></tr></table></figure><p>有三个命令。第一个是 <code>test</code> 命令，可以通过执行 <code>npm t</code> 或 <code>npm test</code> 来运行。它会使用 Jest 运行测试套件中的所有测试。第二个是 <code>watch</code> 命令，可以通过运行 <code>npm run test:watch</code> 启动。此命令会在文件发生更改时运行测试，但仅针对已保存的文件进行测试。你可以按下 <code>A</code> 键在监视模式下运行所有测试，退出监视模式时按 <code>Ctrl-C</code>。</p><p>列表中的最后一个 npm 脚本是 <code>test:cov</code>，用于测试覆盖率。它会运行所有测试并根据上述配置生成代码覆盖率报告。Jest 背后使用的是 Istanbul JS 来报告代码覆盖率。你将在本教程后续部分了解更多有关代码覆盖率的信息。在接下来的部分中，你将看到 Quotes 控制器的代码，并为该代码编写单元测试。</p><h3 id="Quotes-控制器及其测试"><a href="#Quotes-控制器及其测试" class="headerlink" title="Quotes 控制器及其测试"></a>Quotes 控制器及其测试</h3><p>作为单元测试的最佳实践，你应该始终为你编写的代码编写测试。因此，你需要为控制器和服务编写测试。虽然实例化 Express 并将其与控制器结合起来的测试不是必需的，但可以编写。以下是 <code>QuotesController</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">import</span> &#123; <span class="title class_">Service</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;typedi&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">QuotesService</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;../services/QuotesService&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">Quote</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;../types/Quote&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Service</span>()</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> <span class="title class_">QuotesController</span> &#123;</span><br><span class="line">  <span class="keyword">private</span> <span class="attr">quotesService</span>: <span class="title class_">QuotesService</span>;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">public</span> <span class="title function_">constructor</span>(<span class="params">quotesService: QuotesService</span>) &#123;</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">quotesService</span> = quotesService;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">public</span> <span class="title function_">getQuotes</span>(<span class="attr">page</span>: <span class="built_in">number</span> = <span class="number">1</span>): <span class="title class_">Quote</span>[] &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="variable language_">this</span>.<span class="property">quotesService</span>.<span class="title function_">getQuotes</span>(page);</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>此类首先导入了 <code>Service</code>，这是用作装饰器的工具，以便 <code>QuotesController</code> 类可以通过容器注入。接下来，导入了 <code>QuotesService</code> 和 <code>Quote</code> 类型。<code>QuotesService</code> 将从数据源获取引言，每个引言的类型为 <code>Quote</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">export</span> <span class="keyword">type</span> <span class="title class_">Quote</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">quote</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">&#125;;</span><br></pre></td></tr></table></figure><p>它是一个简单的类型，包含一个数字类型的 <code>id</code>，以及两个字符串类型的属性：<code>quote</code> 和 <code>author</code>。</p><p>回到控制器，<code>QuotesController</code> 类通过 <code>Service</code> 装饰器定义。为了使该装饰器正常工作，你需要在 <code>tsconfig.json</code> 文件中将 <code>experimentalDecorators</code> 和 <code>emitDecoratorMetadata</code> 设置为 <code>true</code>。</p><p>接着是 <code>QuotesController</code> 的构造函数，它将 <code>QuotesService</code> 作为依赖注入。随后定义了一个名为 <code>getQuotes</code> 的方法，它接收 <code>page</code> 参数，默认为 <code>1</code>，并返回 <code>Quote</code> 类型的数组。在此方法中，你调用了 <code>quotesService.getQuotes</code> 方法，并传入页码进行分页。<code>QuotesService</code> 负责从适当的数据源（在此示例中是静态数组）获取引言数据。</p><p>你可以为 <code>QuotesController</code> 编写测试，甚至无需 <code>QuotesService</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">QuotesController</span> &#125; <span class="keyword">from</span> <span class="string">&quot;../../../src/controllers/QuotesController&quot;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">QuotesService</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;../../../src/services/QuotesService&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">Quote</span> &#125; <span class="keyword">from</span> <span class="string">&quot;../../../src/types/Quote&quot;</span>;</span><br><span class="line"></span><br><span class="line"><span class="title function_">describe</span>(<span class="string">&#x27;QuotesController&#x27;</span>, <span class="function">() =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">let</span> <span class="attr">controller</span>: <span class="title class_">QuotesController</span>;</span><br><span class="line">  <span class="keyword">const</span> mockQuotesService = &#123;</span><br><span class="line">    <span class="attr">getQuotes</span>: jest.<span class="title function_">fn</span>()</span><br><span class="line">  &#125; <span class="keyword">as</span> <span class="title class_">QuotesService</span>;</span><br><span class="line"></span><br><span class="line">  <span class="title function_">beforeEach</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">    controller = <span class="keyword">new</span> <span class="title class_">QuotesController</span>(mockQuotesService);</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;should define quotes controller&#x27;</span>, <span class="function">() =&gt;</span> &#123;</span><br><span class="line">    <span class="title function_">expect</span>(controller).<span class="title function_">toBeInstanceOf</span>(<span class="title class_">QuotesController</span>);</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;getQuotes&#x27;</span>, <span class="function">() =&gt;</span> &#123;</span><br><span class="line">    <span class="title function_">it</span>(<span class="string">&#x27;should get quotes&#x27;</span>, <span class="function">() =&gt;</span> &#123;</span><br><span class="line">      mockQuotesService.<span class="property">getQuotes</span> = jest.<span class="title function_">fn</span>().<span class="title function_">mockReturnValueOnce</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">quote</span>: <span class="string">&#x27;There are only two kinds of languages: the ones people complain about and the ones nobody uses.&#x27;</span>,</span><br><span class="line">          <span class="attr">author</span>: <span class="string">&#x27;Bjarne Stroustrup&#x27;</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">quote</span>: <span class="string">&#x27;Any fool can write code that a computer can understand. Good programmers write code that humans can understand.&#x27;</span>,</span><br><span class="line">          <span class="attr">author</span>: <span class="string">&#x27;Martin Fowler&#x27;</span></span><br><span class="line">        &#125;,</span><br><span class="line">      ] <span class="keyword">as</span> <span class="title class_">Quote</span>[]);</span><br><span class="line"></span><br><span class="line">      <span class="keyword">const</span> <span class="attr">quotes</span>: <span class="title class_">Quote</span>[] = controller.<span class="title function_">getQuotes</span>();</span><br><span class="line">      <span class="title function_">expect</span>(quotes).<span class="title function_">toHaveLength</span>(<span class="number">2</span>);</span><br><span class="line">      <span class="title function_">expect</span>(quotes[<span class="number">0</span>].<span class="property">author</span>).<span class="title function_">toBe</span>(<span class="string">&#x27;Bjarne Stroustrup&#x27;</span>);</span><br><span class="line">      <span class="title function_">expect</span>(quotes[<span class="number">1</span>].<span class="property">quote</span>).<span class="title function_">toEqual</span>(expect.<span class="title function_">stringContaining</span>(<span class="string">&#x27;Any fool can write code that&#x27;</span>));</span><br><span class="line">      <span class="title function_">expect</span>(quotes[<span class="number">1</span>]).<span class="title function_">toEqual</span>(expect.<span class="title function_">objectContaining</span>(&#123; <span class="attr">id</span>: <span class="number">2</span> &#125;));</span><br><span class="line">      <span class="title function_">expect</span>(quotes[<span class="number">1</span>]).<span class="title function_">toEqual</span>(&#123;</span><br><span class="line">        <span class="attr">id</span>: <span class="number">2</span>,</span><br><span class="line">        <span class="attr">quote</span>: <span class="string">&#x27;Any fool can write code that a computer can understand. Good programmers write code that humans can understand.&#x27;</span>,</span><br><span class="line">        <span class="attr">author</span>: <span class="string">&#x27;Martin Fowler&#x27;</span></span><br><span class="line">      &#125;);</span><br><span class="line">      <span class="title function_">expect</span>(mockQuotesService.<span class="property">getQuotes</span>).<span class="title function_">toHaveBeenCalledTimes</span>(<span class="number">1</span>);</span><br><span class="line">      <span class="title function_">expect</span>(mockQuotesService.<span class="property">getQuotes</span>).<span class="title function_">toHaveBeenCalledWith</span>(<span class="number">1</span>);</span><br><span class="line">    &#125;);</span><br><span class="line">  &#125;);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p>Jest 单元测试从导入 <code>QuotesController</code> 开始，这是被测试的系统（SUT）。你还需要导入 <code>QuotesService</code> 类（它是控制器的依赖）和 <code>Quote</code> 类型。然后，主 <code>describe</code> 块以待测试的类命名，即 <code>QuotesController</code>。在 <code>describe</code> 下方定义了 <code>controller</code> 变量（类型为 <code>QuotesController</code>）和 <code>mockQuotesService</code>。模拟的引言服务有一个 <code>getQuotes</code> 函数，该函数被赋值为一个 Jest 函数。</p><p>在 <code>beforeEach</code> 钩子中，<code>controller</code> 被赋值为传入模拟服务的 <code>QuotesController</code> 实例。由于 <code>controller</code> 变量将在多个测试和函数中重复使用，因此它在外部作用域中的 <code>beforeEach</code> 函数内定义。接着是第一个测试：简单地测试 <code>controller</code> 变量是 <code>QuotesController</code> 类的实例。</p><p>随后，另一个 <code>describe</code> 块开始测试 <code>getQuotes</code> 方法。它包含一个 <code>it</code> 函数，用于测试控制器方法是否能返回一些引言。在这里，你设置了 <code>QuotesService</code> 的 <code>getQuotes</code> 方法，返回一对引言，作为 <code>Quote</code> 类型的数组。然后，你调用了控制器的 <code>getQuotes</code>，并期望其长度为 2。接着，你断言第一个引言的作者为 <code>Bjarne Stroustrup</code>，第二个引言的 <code>quote</code> 属性包含 “Any fool can write code that” 字符串。</p><p>测试中包含了多种断言，以便展示在 Jest 中如何使用 <code>expect</code> 函数。你可以宽松地只期望对象包含某个 <code>id</code>，而忽略其他属性。另一方面，你也可以严格要求整个对象完全等于传入的值。</p><p>在测试的最后，你期望模拟服务的 <code>getQuotes</code> 方法被调用一次，并且参数为 <code>1</code>。</p><p>如果你运行 <code>npm t</code> 或 <code>npm test</code>，测试将通过，并显示以下输出：<br><img src="https://images.nirvana.net.cn/static/images/images/10024246-123ca7dafdd29ccc.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="></p><h3 id="Quotes服务及其测试"><a href="#Quotes服务及其测试" class="headerlink" title="Quotes服务及其测试"></a>Quotes服务及其测试</h3><p>Quotes服务层负责通过查询数据源获取数据，并将数据源与调用方隔离。为简化本教程的范围，这里使用了一个包含17条引用的静态数组作为数据源。Quotes服务本可以通过对象关系映射器（ORM）与关系数据库或NoSQL数据库通信，但这些内容不在本教程测试的范围之内。以下是Quotes服务的代码：</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">Service</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;typedi&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">Quote</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;../types/Quote&#x27;</span>;</span><br><span class="line"><span class="meta">@Service</span>()</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> <span class="title class_">QuotesService</span> &#123;</span><br><span class="line">  <span class="keyword">public</span> <span class="title function_">getQuotes</span>(<span class="attr">page</span>: <span class="built_in">number</span>): <span class="title class_">Quote</span>[] &#123;</span><br><span class="line">    <span class="keyword">if</span> (page &lt; <span class="number">1</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">&#x27;Page number should be 1 or more&#x27;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">const</span> <span class="attr">quotes</span>:<span class="title class_">Quote</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">quote</span>: <span class="string">&#x27;There are only two kinds of languages: the ones people complain about and the ones nobody uses.&#x27;</span>,</span><br><span class="line">        <span class="attr">author</span>: <span class="string">&#x27;Bjarne Stroustrup&#x27;</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">quote</span>:</span><br><span class="line">          <span class="string">&#x27;Any fool can write code that a computer can understand. Good programmers write code that humans can understand.&#x27;</span>,</span><br><span class="line">        <span class="attr">author</span>: <span class="string">&#x27;Martin Fowler&#x27;</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">quote</span>: <span class="string">&#x27;First, solve the problem. Then, write the code.&#x27;</span>,</span><br><span class="line">        <span class="attr">author</span>: <span class="string">&#x27;John Johnson&#x27;</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">4</span>,</span><br><span class="line">        <span class="attr">quote</span>: <span class="string">&#x27;Java is to JavaScript what car is to Carpet.&#x27;</span>,</span><br><span class="line">        <span class="attr">author</span>: <span class="string">&#x27;Chris Heilmann&#x27;</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">5</span>,</span><br><span class="line">        <span class="attr">quote</span>:</span><br><span class="line">          <span class="string">&#x27;Always code as if the guy who ends up maintaining your code will be a violent psychopath who knows where you live.&#x27;</span>,</span><br><span class="line">        <span class="attr">author</span>: <span class="string">&#x27;John Woods&#x27;</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">6</span>,</span><br><span class="line">        <span class="attr">quote</span>: <span class="string">&quot;I&#x27;m not a great programmer; I&#x27;m just a good programmer with great habits.&quot;</span>,</span><br><span class="line">        <span class="attr">author</span>: <span class="string">&#x27;Kent Beck&#x27;</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">7</span>,</span><br><span class="line">        <span class="attr">quote</span>: <span class="string">&#x27;Truth can only be found in one place: the code.&#x27;</span>,</span><br><span class="line">        <span class="attr">author</span>: <span class="string">&#x27;Robert C. Martin&#x27;</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">8</span>,</span><br><span class="line">        <span class="attr">quote</span>:</span><br><span class="line">          <span class="string">&#x27;If you have to spend effort looking at a fragment of code and figuring out what it\&#x27;s doing, then you should extract it into a function and name the function after the &quot;what&quot;.&#x27;</span>,</span><br><span class="line">        <span class="attr">author</span>: <span class="string">&#x27;Martin Fowler&#x27;</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">9</span>,</span><br><span class="line">        <span class="attr">quote</span>:</span><br><span class="line">          <span class="string">&#x27;The real problem is that programmers have spent far too much time worrying about efficiency in the wrong places and at the wrong times; premature optimization is the root of all evil (or at least most of it) in programming.&#x27;</span>,</span><br><span class="line">        <span class="attr">author</span>: <span class="string">&#x27;Donald Knuth&#x27;</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">10</span>,</span><br><span class="line">        <span class="attr">quote</span>:</span><br><span class="line">          <span class="string">&#x27;SQL, Lisp, and Haskell are the only programming languages that I’ve seen where one spends more time thinking than typing.&#x27;</span>,</span><br><span class="line">        <span class="attr">author</span>: <span class="string">&#x27;Philip Greenspun&#x27;</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">11</span>,</span><br><span class="line">        <span class="attr">quote</span>: <span class="string">&#x27;Deleted code is debugged code.&#x27;</span>,</span><br><span class="line">        <span class="attr">author</span>: <span class="string">&#x27;Jeff Sickel&#x27;</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">12</span>,</span><br><span class="line">        <span class="attr">quote</span>:</span><br><span class="line">          <span class="string">&#x27;There are two ways of constructing a software design: One way is to make it so simple that there are obviously no deficiencies and the other way is to make it so complicated that there are no obvious deficiencies.&#x27;</span>,</span><br><span class="line">        <span class="attr">author</span>: <span class="string">&#x27;C.A.R. Hoare&#x27;</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">13</span>,</span><br><span class="line">        <span class="attr">quote</span>: <span class="string">&#x27;Simplicity is prerequisite for reliability.&#x27;</span>,</span><br><span class="line">        <span class="attr">author</span>: <span class="string">&#x27;Edsger W. Dijkstra&#x27;</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">14</span>,</span><br><span class="line">        <span class="attr">quote</span>: <span class="string">&#x27;There are only two hard things in Computer Science: cache invalidation and naming things.&#x27;</span>,</span><br><span class="line">        <span class="attr">author</span>: <span class="string">&#x27;Phil Karlton&#x27;</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">15</span>,</span><br><span class="line">        <span class="attr">quote</span>:</span><br><span class="line">          <span class="string">&#x27;Measuring programming progress by lines of code is like measuring aircraft building progress by weight.&#x27;</span>,</span><br><span class="line">        <span class="attr">author</span>: <span class="string">&#x27;Bill Gates&#x27;</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">16</span>,</span><br><span class="line">        <span class="attr">quote</span>: <span class="string">&#x27;Controlling complexity is the essence of computer programming.&#x27;</span>,</span><br><span class="line">        <span class="attr">author</span>: <span class="string">&#x27;Brian Kernighan&#x27;</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">17</span>,</span><br><span class="line">        <span class="attr">quote</span>: <span class="string">&#x27;The only way to learn a new programming language is by writing programs in it.&#x27;</span>,</span><br><span class="line">        <span class="attr">author</span>: <span class="string">&#x27;Dennis Ritchie&#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="attr">itemsPerPage</span>:<span class="built_in">number</span> = <span class="number">10</span>;</span><br><span class="line">    <span class="keyword">const</span> <span class="attr">end</span>:<span class="built_in">number</span> = page * itemsPerPage;</span><br><span class="line">    <span class="keyword">let</span> <span class="attr">start</span>:<span class="built_in">number</span> = <span class="number">0</span>;</span><br><span class="line">    <span class="keyword">if</span>(page &gt; <span class="number">1</span>) &#123;</span><br><span class="line">      start = (page - <span class="number">1</span>) * itemsPerPage;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> quotes.<span class="title function_">slice</span>(start , end);</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>Quotes服务首先从<code>typedi</code>中导入<code>Service</code>，以及从<code>types/Quote</code>中导入<code>Quote</code>类型。此类没有构造函数依赖，但如果使用真实的数据源，它可以通过构造函数传入Quotes存储库，以便从关系数据库等数据源获取数据。</p><p>接下来，定义了<code>getQuotes</code>方法，它接收一个<code>page</code>参数用于基本分页。方法首先检查<code>page</code>参数是否小于1，如果是则抛出错误。然后，它使用<code>slice</code>方法实现分页逻辑，每页显示10条数据。</p><p>接下来是对Quotes服务的单元测试代码：</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><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">import</span> &#123; <span class="title class_">QuotesService</span> &#125; <span class="keyword">from</span> <span class="string">&quot;../../../src/services/QuotesService&quot;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">Quote</span> &#125; <span class="keyword">from</span> <span class="string">&quot;../../../src/types/Quote&quot;</span>;</span><br><span class="line"><span class="title function_">describe</span>(<span class="string">&quot;QuotesService&quot;</span>, <span class="function">() =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">let</span> <span class="attr">service</span>: <span class="title class_">QuotesService</span>;</span><br><span class="line">  <span class="title function_">beforeEach</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">    service = <span class="keyword">new</span> <span class="title class_">QuotesService</span>();</span><br><span class="line">  &#125;);</span><br><span class="line">  <span class="title function_">it</span>(<span class="string">&quot;should define quotes service&quot;</span>, <span class="function">() =&gt;</span> &#123;</span><br><span class="line">    <span class="title function_">expect</span>(service).<span class="title function_">toBeInstanceOf</span>(<span class="title class_">QuotesService</span>);</span><br><span class="line">  &#125;);</span><br><span class="line">  <span class="title function_">describe</span>(<span class="string">&quot;getQuotes&quot;</span>, <span class="function">() =&gt;</span> &#123;</span><br><span class="line">    <span class="title function_">it</span>(<span class="string">&quot;should get mock fixed quotes&quot;</span>, <span class="function">() =&gt;</span> &#123;</span><br><span class="line">      <span class="keyword">const</span> <span class="attr">quotes</span>: <span class="title class_">Quote</span>[] = service.<span class="title function_">getQuotes</span>(<span class="number">1</span>);</span><br><span class="line">      <span class="title function_">expect</span>(quotes).<span class="title function_">toHaveLength</span>(<span class="number">10</span>);</span><br><span class="line">      <span class="title function_">expect</span>(quotes[<span class="number">0</span>]).<span class="title function_">toEqual</span>(&#123;</span><br><span class="line">        <span class="attr">author</span>: <span class="string">&quot;Bjarne Stroustrup&quot;</span>,</span><br><span class="line">        <span class="attr">id</span>: <span class="number">1</span>,</span><br><span class="line">        <span class="attr">quote</span>:</span><br><span class="line">          <span class="string">&quot;There are only two kinds of languages: the ones people complain about and the ones nobody uses.&quot;</span>,</span><br><span class="line">      &#125;);</span><br><span class="line">    &#125;);</span><br><span class="line">    <span class="title function_">it</span>(<span class="string">&quot;should get mock fixed quotes for page &gt; 1&quot;</span>, <span class="function">() =&gt;</span> &#123;</span><br><span class="line">      <span class="keyword">const</span> <span class="attr">quotes</span>: <span class="title class_">Quote</span>[] = service.<span class="title function_">getQuotes</span>(<span class="number">2</span>);</span><br><span class="line">      <span class="title function_">expect</span>(quotes).<span class="title function_">toHaveLength</span>(<span class="number">7</span>);</span><br><span class="line">    &#125;);</span><br><span class="line">    <span class="title function_">it</span>(<span class="string">&quot;should throw error for page number less than 0&quot;</span>, <span class="function">() =&gt;</span> &#123;</span><br><span class="line">      <span class="title function_">expect</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">        service.<span class="title function_">getQuotes</span>(-<span class="number">1</span>);</span><br><span class="line">      &#125;).<span class="title function_">toThrow</span>(<span class="string">&quot;Page number should be 1 or more&quot;</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>QuotesService</code>类的<code>getQuotes</code>方法。与前面针对控制器的测试类似，文件中导入了<code>Quote</code>类型，本次测试的系统被测对象（SUT）是Quotes服务。</p><p>第一个测试检查<code>quotesService</code>变量是否是<code>QuotesService</code>的实例。然后在<code>getQuotes</code>的<code>describe</code>块中，第一个测试验证如果传入页码为1，则返回10条引用，并且第一条引用与预期的对象匹配。</p><p>接下来的测试同样验证了传入的页码为2时的情况，期望返回7条相关的引用。最后一个测试处理的是页码小于1时抛出错误的场景，它传入页码-1，并期望抛出带有正确消息的错误。通过这些测试，该文件实现了完整的代码覆盖率。你可以通过运行<code>npm test</code>来查看，测试脚本已配置在<code>package.json</code>中，运行后会得到以下输出：</p><p>运行Quotes控制器和Quotes服务的测试结果<br><img src="https://images.nirvana.net.cn/static/images/images/10024246-9ebe58d78f256860.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="></p><h3 id="检查代码覆盖率"><a href="#检查代码覆盖率" class="headerlink" title="检查代码覆盖率"></a>检查代码覆盖率</h3><p>代码覆盖率是一个可能引发激烈讨论的指标。首先，拥有100%的代码覆盖率并不意味着代码没有缺陷，它仅表明软件工程师已经尽力编写了覆盖所有代码的测试。代码中仍然可能存在逻辑错误或与数据相关的错误，这些问题往往会揭示未曾预料到的边缘情况。</p><p>使用Jest来检查代码覆盖率，你可以执行<code>jest --coverage</code>命令；它已在<code>package.json</code>的scripts部分被包含为<code>test:cov</code>。这意味着你可以运行<code>npm run test:cov</code>来查看代码覆盖率，其输出如下：</p><p><img src="https://images.nirvana.net.cn/static/images/images/10024246-0ebc1c7a54afb033.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="></p><p>如前所示，你已经编写了足够的测试来覆盖所有相关文件及其中的代码。由于配置中启用了HTML报告器，你可以在<code>/test/.coverage/index.html</code>路径下查看包含代码覆盖率信息的HTML文件。打开该文件时，你将看到类似如下的内容：<br><img src="https://images.nirvana.net.cn/static/images/images/10024246-3336c2aba7206afb.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="></p><p>例如，如果删除<code>QuotesService.spec.ts</code>文件中描述为“should throw error for page number less than 0”的测试（见第31-34行），然后重新运行覆盖率检查，你会看到：</p><p><img src="https://images.nirvana.net.cn/static/images/images/10024246-e955da6e4c58a089.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="></p><p>这意味着<code>QuotesService</code>中的第10行代码没有被任何测试覆盖，这是因为删除了上述测试。这就是代码覆盖率的工作原理。</p><p>作为一个软件工程团队，追求较高的代码覆盖率是明智的做法，但100%覆盖率并不应成为技术文化的强制要求。与大多数事情一样，达到目标代码覆盖率所花费的时间和精力应该是合理、优化且合乎情理的。</p><h3 id="结论"><a href="#结论" class="headerlink" title="结论"></a>结论</h3><p>在本教程中，你学习了如何将 Jest 添加到现有的 TypeScript 项目中。随后你为两个重要的文件——Quotes 控制器和 Quotes 服务——编写了单元测试。</p><p>除了学习测试的相关概念外，你还通过实际示例了解了代码覆盖率的工作原理，并掌握了单元测试中常用的测试术语。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;&lt;img src=&quot;https://images.nirvana.net.cn/static/images/images/10024246-7c73370044e88f3a.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2Awdb</summary>
      
    
    
    
    <category term="前端" scheme="https://www.nirvana.net.cn/categories/%E5%89%8D%E7%AB%AF/"/>
    
    
    <category term="TypeScript" scheme="https://www.nirvana.net.cn/tags/TypeScript/"/>
    
  </entry>
  
  <entry>
    <title>理解script标签的 defer 和 async 属性</title>
    <link href="https://www.nirvana.net.cn/2024/1014R0NjeK6GGG.html"/>
    <id>https://www.nirvana.net.cn/2024/1014R0NjeK6GGG.html</id>
    <published>2024-10-14T08:28:03.000Z</published>
    <updated>2024-10-27T08:45:58.437Z</updated>
    
    <content type="html"><![CDATA[<p><img src="https://images.nirvana.net.cn/static/images/images/10024246-0cde890ff9a18309.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="><br>在 HTML 中会遇到以下三类 script：</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">&lt;script src=&#x27;xxx&#x27;&gt;&lt;/script&gt;</span><br><span class="line">&lt;script src=&#x27;xxx&#x27; async&gt;&lt;/script&gt;</span><br><span class="line">&lt;script src=&#x27;xxx&#x27; defer&gt;&lt;/script&gt;</span><br></pre></td></tr></table></figure><p>那么这三类 script 有什么区别呢？</p><h2 id="script"><a href="#script" class="headerlink" title="script"></a>script</h2><p>浏览器在解析 HTML 的时候，如果遇到一个没有任何属性的 script 标签，就会暂停解析，先发送网络请求获取该 JS 脚本的代码内容，然后让 JS 引擎执行该代码，当代码执行完毕后恢复解析。整个过程如下图所示：<br><img src="https://images.nirvana.net.cn/static/images/images/10024246-69aacf74c658e737.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="><br>可以看到，script 阻塞了浏览器对 HTML 的解析，如果获取 JS 脚本的网络请求迟迟得不到响应，或者 JS 脚本执行时间过长，都会导致白屏，用户看不到页面内容。</p><h2 id="async-script"><a href="#async-script" class="headerlink" title="async script"></a>async script</h2><p>当HTML解析过程中遇到script async标签时，不会中断HTML解析，同时并行下载script文件，下载完成后中断HTML解析并执行script，执行完成后再继续HTML解析<br><img src="https://images.nirvana.net.cn/static/images/images/10024246-bc831820deb4207e.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="><br>script的执行顺序不一定按照script标签的出现顺序，而是取决于script下载完成的顺序</p><h2 id="defer-script"><a href="#defer-script" class="headerlink" title="defer script"></a>defer script</h2><p>当HTML解析过程中遇到script defer标签时，不会中断HTML解析，同时并行下载script文件，直到HTML解析完成再执行script（script的执行顺序与script标签出现顺序一致）<br><img src="https://images.nirvana.net.cn/static/images/images/10024246-9f2b23f6e9403907.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="></p><h2 id="async-与-defer-的区别"><a href="#async-与-defer-的区别" class="headerlink" title="async 与 defer 的区别"></a>async 与 defer 的区别</h2><p><code>async</code> 和 <code>defer</code> 是两种用于控制外部 JavaScript 脚本加载和执行行为的属性，它们都可以提高页面性能，但它们的工作方式和适用场景有所不同。下面是它们的详细区别：</p><h4 id="1-加载与执行顺序"><a href="#1-加载与执行顺序" class="headerlink" title="1. 加载与执行顺序"></a>1. <strong>加载与执行顺序</strong></h4><h6 id="async-："><a href="#async-：" class="headerlink" title="**async**："></a>**<code>async</code>**：</h6><ul><li><strong>加载</strong>：当浏览器解析 HTML 文档时，遇到带有 <code>async</code> 属性的 <code>&lt;script&gt;</code> 标签，脚本会被异步下载，与 HTML 的解析同时进行。</li><li><strong>执行</strong>：一旦脚本下载完成，浏览器会立即停止 HTML 解析，执行该脚本。这意味着脚本的执行顺序取决于下载完成的时间，无法保证按照 HTML 中的顺序执行。</li></ul><h6 id="defer-："><a href="#defer-：" class="headerlink" title="**defer**："></a>**<code>defer</code>**：</h6><ul><li><strong>加载</strong>：和 <code>async</code> 类似，脚本在后台异步下载，与 HTML 文档解析同时进行。</li><li><strong>执行</strong>：<code>defer</code> 保证脚本会在 HTML 文档完全解析之后才执行，而且多个带 <code>defer</code> 的脚本会按它们在 HTML 中的顺序执行。</li></ul><h4 id="2-执行时机"><a href="#2-执行时机" class="headerlink" title="2. 执行时机"></a>2. <strong>执行时机</strong></h4><ul><li><p>**<code>async</code>**：当脚本文件下载完成时立即执行，无论 HTML 文档是否解析完毕。这可能导致脚本在页面 DOM 完全构建之前执行，因此如果脚本需要操作 DOM，可能会遇到问题。</p></li><li><p>**<code>defer</code>**：保证在整个 HTML 文档解析完成后，才会执行脚本。适合需要操作 DOM 的脚本，因为这时整个文档结构已经构建完毕。</p></li></ul><h4 id="3-适用场景"><a href="#3-适用场景" class="headerlink" title="3. 适用场景"></a>3. <strong>适用场景</strong></h4><h6 id="async-：-1"><a href="#async-：-1" class="headerlink" title="**async**："></a>**<code>async</code>**：</h6><ul><li>适合不依赖页面内容（DOM）的脚本，比如广告、数据分析、第三方服务（如 Google Analytics）。</li><li>因为脚本不依赖于页面的其他部分，快速加载和执行是关键，因此 <code>async</code> 属性可以最大限度地加快这些脚本的加载。</li></ul><h6 id="defer-：-1"><a href="#defer-：-1" class="headerlink" title="**defer**："></a>**<code>defer</code>**：</h6><ul><li>适合依赖 DOM 结构的脚本，通常是需要在页面加载后进行操作的代码（如初始化 UI、添加事件监听等）。</li><li>在大型项目中，<code>defer</code> 常用于需要确保所有 DOM 元素都已加载完成时再执行的脚本。</li></ul><h4 id="4-脚本顺序控制"><a href="#4-脚本顺序控制" class="headerlink" title="4. 脚本顺序控制"></a>4. <strong>脚本顺序控制</strong></h4><ul><li>**<code>async</code>**：脚本下载完就执行，因此多个脚本的执行顺序不可控，可能会出现某个脚本比其他脚本提前执行，即使它们在 HTML 中的位置较后。</li><li>**<code>defer</code>**：多个带有 <code>defer</code> 的脚本按它们在 HTML 文档中的顺序执行，顺序性得到保证。</li></ul><h4 id="5-浏览器支持"><a href="#5-浏览器支持" class="headerlink" title="5. 浏览器支持"></a>5. <strong>浏览器支持</strong></h4><ul><li><strong><code>async</code></strong> 和 <strong><code>defer</code></strong> 都受到主流浏览器的广泛支持，包括现代桌面和移动浏览器。老版本的浏览器可能对 <code>defer</code> 处理不如现代浏览器精确，但在绝大多数情况下，它们的行为是一致的。</li></ul><h4 id="总结比较："><a href="#总结比较：" class="headerlink" title="总结比较："></a>总结比较：</h4><table><thead><tr><th>特性</th><th><code>async</code></th><th><code>defer</code></th></tr></thead><tbody><tr><td><strong>加载方式</strong></td><td>异步加载，与 HTML 解析同时进行</td><td>异步加载，与 HTML 解析同时进行</td></tr><tr><td><strong>执行时机</strong></td><td>脚本下载完成后立即执行，可能在 HTML 解析完毕前执行</td><td>HTML 解析完毕后，按顺序执行</td></tr><tr><td><strong>执行顺序</strong></td><td>无法保证执行顺序</td><td>保证按 HTML 中的顺序执行</td></tr><tr><td><strong>适用场景</strong></td><td>不依赖 DOM、非关键脚本（如广告、分析、第三方 SDK）</td><td>依赖 DOM 的脚本，通常是与页面结构相关的脚本</td></tr><tr><td><strong>性能优化</strong></td><td>加快脚本加载，但可能会中断 HTML 解析</td><td>不会中断 HTML 解析，推荐用于需要 DOM 完全加载的脚本</td></tr></tbody></table><h4 id="示例对比："><a href="#示例对比：" class="headerlink" title="示例对比："></a>示例对比：</h4><ul><li><p><strong><code>async</code> 示例</strong>：</p><figure class="highlight html"><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">script</span> <span class="attr">src</span>=<span class="string">&quot;analytics.js&quot;</span> <span class="attr">async</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">script</span> <span class="attr">src</span>=<span class="string">&quot;ads.js&quot;</span> <span class="attr">async</span>&gt;</span><span class="tag">&lt;/<span class="name">script</span>&gt;</span></span><br></pre></td></tr></table></figure><ul><li>这些脚本会并行下载，且各自下载完成后立即执行，可能会出现 <code>ads.js</code> 比 <code>analytics.js</code> 先执行。</li></ul></li><li><p><strong><code>defer</code> 示例</strong>：</p><figure class="highlight html"><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">script</span> <span class="attr">src</span>=<span class="string">&quot;main.js&quot;</span> <span class="attr">defer</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">script</span> <span class="attr">src</span>=<span class="string">&quot;ui.js&quot;</span> <span class="attr">defer</span>&gt;</span><span class="tag">&lt;/<span class="name">script</span>&gt;</span></span><br></pre></td></tr></table></figure><ul><li><code>main.js</code> 和 <code>ui.js</code> 都会在 HTML 完全解析后按顺序执行，<code>main.js</code> 会先执行，接着是 <code>ui.js</code>。</li></ul></li></ul><h4 id="何时使用哪个？"><a href="#何时使用哪个？" class="headerlink" title="何时使用哪个？"></a>何时使用哪个？</h4><ul><li>如果你需要确保脚本按顺序执行且依赖页面结构（如操作 DOM），使用 <code>defer</code> 是最佳选择。</li><li>如果脚本可以独立于页面内容并立即执行（如第三方追踪代码或广告），则使用 <code>async</code> 可以提高页面加载性能。</li></ul><p>这两个属性都可以帮助优化网页的加载速度，关键在于选择合适的场景应用。</p><h2 id="type-”module”"><a href="#type-”module”" class="headerlink" title="type=”module”"></a>type=”module”</h2><p>type=”module”  代表的是引入 JavaScript 模块的方式，模块（Modules）是一种更有组织性、更灵活的代码管理方式。随着 ES6（ECMAScript 2015）的引入，JavaScript 增加了对模块的原生支持，使得我们可以将代码分成独立的文件，并根据需要引入它们。<br>为了让浏览器正确识别 JavaScript 模块，我们需要在 <code>&lt;script&gt;</code> 标签中使用 <code>type=&quot;module&quot;</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;module&quot;</span> <span class="attr">src</span>=<span class="string">&quot;path/to/your-module.js&quot;</span>&gt;</span><span class="tag">&lt;/<span class="name">script</span>&gt;</span></span><br></pre></td></tr></table></figure><h4 id="主要特性"><a href="#主要特性" class="headerlink" title="主要特性"></a>主要特性</h4><ol><li><p><strong>模块作用域</strong>：模块内部的变量默认是私有的，只有通过 <code>export</code> 导出的内容才能在其他模块中访问。模块间不会共享全局作用域，避免了变量冲突。</p></li><li><p><strong>延迟执行（Deferred Execution）</strong>：模块默认是延迟执行的，类似于使用了 <code>defer</code> 的 <code>&lt;script&gt;</code> 标签。这意味着它们会在 HTML 文档完全解析后执行。</p></li><li><p><strong>严格模式（Strict Mode）</strong>：模块始终在严格模式下运行（即使未显式声明），这有助于避免常见的 JavaScript 错误。</p></li><li><p><strong>模块的异步加载</strong>：浏览器会异步加载模块，不会阻塞 HTML 文档的解析过程。</p></li></ol>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;&lt;img src=&quot;https://images.nirvana.net.cn/static/images/images/10024246-0cde890ff9a18309.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2Awdb</summary>
      
    
    
    
    <category term="前端" scheme="https://www.nirvana.net.cn/categories/%E5%89%8D%E7%AB%AF/"/>
    
    
    <category term="web" scheme="https://www.nirvana.net.cn/tags/web/"/>
    
  </entry>
  
  <entry>
    <title>web性能优化之Gzip</title>
    <link href="https://www.nirvana.net.cn/2024/101356o5iVVV22.html"/>
    <id>https://www.nirvana.net.cn/2024/101356o5iVVV22.html</id>
    <published>2024-10-13T08:28:03.000Z</published>
    <updated>2024-10-13T08:46:57.760Z</updated>
    
    <content type="html"><![CDATA[<p><img src="https://images.nirvana.net.cn/static/images/images/10024246-611d1412c2aeabfb.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="><br>最近开发活动页，启用了Lottie用于动画的播放，动画播放流畅了，但是发现动画的json资源很大，大点的都超过10M了，首页启动平均时间超过了10S，性能优化切实必行，精简请求资源大小是第一步，GZzip正是为此服务的。</p><h2 id="gzip-是什么-？"><a href="#gzip-是什么-？" class="headerlink" title="gzip 是什么 ？"></a>gzip 是什么 ？</h2><p>Gzip 是一种用于文件压缩和解压缩的算法与工具，全称是 “GNU zip”。它最初由 Jean-loup Gailly 和 Mark Adler 于 1992 年开发，作为 Unix 系统中的文件压缩程序。<br>Gzip 是一种无损压缩算法，它通过减少数据的冗余性来压缩文件大小，同时保证解压缩后的文件与原始文件完全一致。它使用 DEFLATE 算法，这是一种结合了 LZ77 和霍夫曼编码的压缩算法。</p><h2 id="Gzip-在-Web-中的应用"><a href="#Gzip-在-Web-中的应用" class="headerlink" title="Gzip 在 Web 中的应用"></a>Gzip 在 Web 中的应用</h2><p>在 Web 开发中，Gzip 压缩广泛用于减小静态文件（如 HTML、CSS、JavaScript、JSON、XML）和动态内容的大小。当浏览器向服务器请求资源时，服务器可以将这些文件以 Gzip 格式压缩后传输给浏览器，从而减少传输的数据量并加快页面加载速度。首先我们要了解什么是HTTP压缩。</p><h4 id="HTTP压缩概述"><a href="#HTTP压缩概述" class="headerlink" title="HTTP压缩概述"></a>HTTP压缩概述</h4><p>HTTP压缩是在Web服务器和浏览器间传输压缩文本内容的方法。HTTP压缩采用通用的压缩算法如gzip等压缩HTML、JavaScript或CSS文件。压缩的最大好处就是降低了网络传输的数据量，从而提高客户端浏览器的访问速度。当然，同时也会增加一点点服务器的负担。Gzip是比较常见的一种HTTP压缩算法。</p><h4 id="HTTP压缩工作原理"><a href="#HTTP压缩工作原理" class="headerlink" title="HTTP压缩工作原理"></a>HTTP压缩工作原理</h4><p><img src="https://images.nirvana.net.cn/static/images/images/10024246-746c5c89fa8aecb7.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="><br>Web服务器处理HTTP压缩的工作原理如下：</p><ul><li>1.Web服务器接收到浏览器的HTTP请求后，检查浏览器是否支持HTTP压缩；<br>在用户浏览器发送请求的HTTP头中,  带有”Accept-Encoding: gzip, deflate”参数则表明支持gzip和deflate两种压缩算法.</li><li>2.如果浏览器支持HTTP压缩，Web服务器检查请求文件的后缀名；<br>静态文件和动态文件后缀启动要所都需要在MetaBase.xml中设置.<br>静态文件需要设置:   HcFileExtensions Metabase Property<br>动态文件需要设置:   HcScriptFileExtensions Metabase Property </li><li>3.如果请求文件是HTML、CSS等静态文件并且文件后缀启用了压缩，则Web服务器到压缩缓冲目录中检查是否已经存在请求文件的最新压缩文件；</li><li>4.如果请求文件的压缩文件不存在，Web服务器向浏览器返回未压缩的请求文件，并在压缩缓冲目录中存放请求文件的压缩文件；</li><li>5.如果请求文件的最新压缩文件已经存在，则直接返回请求文件的压缩文件；</li><li>6.如果请求文件是ASPX等动态文件并且文件后缀启用了压缩，Web服务器动态压缩内容并返回浏览器，压缩内容不存放到压缩缓存目录中。<h4 id="Gzip-有多优秀-？"><a href="#Gzip-有多优秀-？" class="headerlink" title="Gzip 有多优秀 ？"></a>Gzip 有多优秀 ？</h4></li></ul><p><strong>开启 gzip 压缩前：</strong><br><img src="https://images.nirvana.net.cn/static/images/images/10024246-08030ce100546062.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="><br><strong>开启 gzip 压缩后（Tomcat）：</strong><br><img src="https://images.nirvana.net.cn/static/images/images/10024246-2a5659071454d20f.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="></p><h2 id="生成Gzip"><a href="#生成Gzip" class="headerlink" title="生成Gzip"></a>生成Gzip</h2><h4 id="webpack构建支持Gzip"><a href="#webpack构建支持Gzip" class="headerlink" title="webpack构建支持Gzip"></a>webpack构建支持Gzip</h4><p>在 Webpack 中，可以通过插件来支持生成 Gzip 压缩文件，最常用的是 <code>compression-webpack-plugin</code>。这个插件会在构建时为生成的文件创建对应的 <code>.gz</code> 文件。</p><p>以下是配置 Webpack 生成 Gzip 压缩文件的步骤：</p><h5 id="1-安装-compression-webpack-plugin"><a href="#1-安装-compression-webpack-plugin" class="headerlink" title="1. 安装 compression-webpack-plugin"></a>1. 安装 <code>compression-webpack-plugin</code></h5><p>首先，你需要安装 <code>compression-webpack-plugin</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">npm install compression-webpack-plugin --save-dev</span><br></pre></td></tr></table></figure><p>或者使用 Yarn：</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 compression-webpack-plugin --dev</span><br></pre></td></tr></table></figure><h6 id="2-配置-Webpack"><a href="#2-配置-Webpack" class="headerlink" title="2. 配置 Webpack"></a>2. 配置 Webpack</h6><p>在 Webpack 配置文件（例如 <code>webpack.config.js</code>）中使用 <code>compression-webpack-plugin</code> 来生成 Gzip 压缩文件。</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">const</span> <span class="title class_">CompressionPlugin</span> = <span class="built_in">require</span>(<span class="string">&#x27;compression-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><br><span class="line">    <span class="keyword">new</span> <span class="title class_">CompressionPlugin</span>(&#123;</span><br><span class="line">      <span class="attr">filename</span>: <span class="string">&#x27;[path][base].gz&#x27;</span>, <span class="comment">// 生成的 gzip 文件名</span></span><br><span class="line">      <span class="attr">algorithm</span>: <span class="string">&#x27;gzip&#x27;</span>, <span class="comment">// 使用 gzip 压缩</span></span><br><span class="line">      <span class="attr">test</span>: <span class="regexp">/\.(js|css|html|svg)$/</span>, <span class="comment">// 匹配哪些文件进行压缩</span></span><br><span class="line">      <span class="attr">threshold</span>: <span class="number">10240</span>, <span class="comment">// 只有大小超过 10KB 的文件才会被压缩</span></span><br><span class="line">      <span class="attr">minRatio</span>: <span class="number">0.8</span>, <span class="comment">// 只有压缩比小于这个值的文件才会被生成 gzip</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">&#125;;</span><br></pre></td></tr></table></figure><h6 id="3-选项说明："><a href="#3-选项说明：" class="headerlink" title="3. 选项说明："></a>3. 选项说明：</h6><ul><li><strong>filename</strong>: 生成的 Gzip 文件名，默认是 <code>[path][base].gz</code>，即在原文件的路径和名称后加上 <code>.gz</code> 后缀。</li><li><strong>algorithm</strong>: 使用的压缩算法，这里指定为 <code>gzip</code>。你也可以选择 <code>brotliCompress</code> 来生成 Brotli 文件。</li><li><strong>test</strong>: 指定要压缩的文件类型，通常包括 <code>.js</code>、<code>.css</code>、<code>.html</code>、<code>.svg</code> 等。</li><li><strong>threshold</strong>: 只有文件大小超过这个值时，才会进行压缩，单位为字节。默认是 10KB。</li><li><strong>minRatio</strong>: 只有压缩比低于此值时，才生成对应的 <code>.gz</code> 文件。比如 0.8 表示压缩后的文件大小至少是原始文件的 80%。<h4 id="Vite构建支持Gzip"><a href="#Vite构建支持Gzip" class="headerlink" title="Vite构建支持Gzip"></a>Vite构建支持Gzip</h4>在 Vite 中构建时，支持 Gzip 压缩是通过配置插件来实现的。Vite 本身并不会自动生成 Gzip 文件，但可以使用插件如 <code>vite-plugin-compression</code> 来实现 Gzip 压缩支持。以下是具体步骤：<h6 id="1-安装-vite-plugin-compression"><a href="#1-安装-vite-plugin-compression" class="headerlink" title="1. 安装 vite-plugin-compression"></a>1. 安装 <code>vite-plugin-compression</code></h6>首先，在你的项目中安装 <code>vite-plugin-compression</code>，这是一个用于生成 Gzip 压缩文件的插件。<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 vite-plugin-compression --save-dev</span><br></pre></td></tr></table></figure>或者使用 Yarn：<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 vite-plugin-compression --dev</span><br></pre></td></tr></table></figure><h6 id="2-配置-Vite"><a href="#2-配置-Vite" class="headerlink" title="2. 配置 Vite"></a>2. 配置 Vite</h6>接下来，在 Vite 的配置文件 <code>vite.config.js</code> 中引入并使用这个插件。这个插件会在 Vite 构建的过程中生成 <code>.gz</code> 压缩文件。<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></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; defineConfig &#125; <span class="keyword">from</span> <span class="string">&#x27;vite&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> viteCompression <span class="keyword">from</span> <span class="string">&#x27;vite-plugin-compression&#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">plugins</span>: [</span><br><span class="line">    <span class="title function_">viteCompression</span>(&#123;</span><br><span class="line">      <span class="attr">verbose</span>: <span class="literal">true</span>, <span class="comment">// 是否在控制台输出压缩结果</span></span><br><span class="line">      <span class="attr">disable</span>: <span class="literal">false</span>, <span class="comment">// 是否禁用插件</span></span><br><span class="line">      <span class="attr">threshold</span>: <span class="number">10240</span>, <span class="comment">// 只有大于这个大小的文件才会压缩，单位是字节，默认值为 10240 (10kb)</span></span><br><span class="line">      <span class="attr">algorithm</span>: <span class="string">&#x27;gzip&#x27;</span>, <span class="comment">// 使用 gzip 压缩</span></span><br><span class="line">      <span class="attr">ext</span>: <span class="string">&#x27;.gz&#x27;</span>, <span class="comment">// 生成的压缩包后缀</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></li></ul><h6 id="配置项说明："><a href="#配置项说明：" class="headerlink" title="配置项说明："></a>配置项说明：</h6><ul><li><strong>verbose</strong>: 是否在构建时在控制台输出哪些文件被压缩。</li><li><strong>disable</strong>: 是否禁用该插件，默认值为 <code>false</code>。</li><li><strong>threshold</strong>: 只有文件大小大于指定值时，才会生成压缩文件，单位是字节（默认 10KB）。</li><li><strong>algorithm</strong>: 压缩算法，默认为 <code>gzip</code>，你也可以使用 <code>brotliCompress</code> 来生成 Brotli 压缩文件。</li><li><strong>ext</strong>: 压缩文件的后缀名，默认是 <code>.gz</code>。<h4 id="Linux手动生成Gzip文件"><a href="#Linux手动生成Gzip文件" class="headerlink" title="Linux手动生成Gzip文件"></a>Linux手动生成Gzip文件</h4>在 Linux 中，<code>gzip</code> 是一个常用的压缩命令，用于压缩文件。它会将原始文件压缩为 <code>.gz</code> 格式。以下是一些常见的 <code>gzip</code> 命令使用方法：<h6 id="1-压缩文件"><a href="#1-压缩文件" class="headerlink" title="1. 压缩文件"></a>1. <strong>压缩文件</strong></h6>将指定文件压缩为 <code>.gz</code> 格式：<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">gzip filename</span><br></pre></td></tr></table></figure></li><li>例如，将 <code>test.txt</code> 压缩为 <code>test.txt.gz</code>：<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">gzip test.txt</span><br></pre></td></tr></table></figure><h6 id="2-解压文件"><a href="#2-解压文件" class="headerlink" title="2. 解压文件"></a>2. <strong>解压文件</strong></h6>使用 <code>gzip -d</code> 或 <code>gunzip</code> 来解压 <code>.gz</code> 文件：<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">gzip -d filename.gz</span><br><span class="line"><span class="comment"># 或者</span></span><br><span class="line">gunzip filename.gz</span><br></pre></td></tr></table></figure></li><li>例如，解压 <code>test.txt.gz</code>：<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">gzip -d test.txt.gz</span><br><span class="line"><span class="comment"># 或者</span></span><br><span class="line">gunzip test.txt.gz</span><br></pre></td></tr></table></figure></li></ul><h6 id="3-保留原文件的压缩"><a href="#3-保留原文件的压缩" class="headerlink" title="3. 保留原文件的压缩"></a>3. <strong>保留原文件的压缩</strong></h6><p>如果你希望压缩文件时保留原文件，可以使用 <code>-k</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">gzip -k filename</span><br></pre></td></tr></table></figure><ul><li>例如，将 <code>test.txt</code> 压缩为 <code>test.txt.gz</code>，并保留 <code>test.txt</code> 原文件：<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">gzip -k test.txt</span><br></pre></td></tr></table></figure></li></ul><h3 id="进阶用法"><a href="#进阶用法" class="headerlink" title="进阶用法"></a>进阶用法</h3><h6 id="4-压缩目录"><a href="#4-压缩目录" class="headerlink" title="4. 压缩目录"></a>4. <strong>压缩目录</strong></h6><p><code>gzip</code> 不能直接压缩目录，但可以先使用 <code>tar</code> 打包目录，再使用 <code>gzip</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">tar -czvf archive.tar.gz directory_name</span><br></pre></td></tr></table></figure><ul><li><code>-c</code>：创建压缩文件</li><li><code>-z</code>：通过 gzip 进行压缩</li><li><code>-v</code>：显示压缩过程中的详细信息</li><li><code>-f</code>：指定输出文件名</li></ul><p>例如，压缩 <code>myfolder</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">tar -czvf myfolder.tar.gz myfolder</span><br></pre></td></tr></table></figure><h6 id="5-查看压缩比"><a href="#5-查看压缩比" class="headerlink" title="5. 查看压缩比"></a>5. <strong>查看压缩比</strong></h6><p>使用 <code>-l</code> 选项来查看 <code>.gz</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">gzip -l filename.gz</span><br></pre></td></tr></table></figure><ul><li>例如，查看 <code>test.txt.gz</code> 的压缩比：<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">gzip -l test.txt.gz</span><br></pre></td></tr></table></figure></li></ul><h4 id="6-递归压缩目录中的所有文件"><a href="#6-递归压缩目录中的所有文件" class="headerlink" title="6. 递归压缩目录中的所有文件"></a>6. <strong>递归压缩目录中的所有文件</strong></h4><p>使用 <code>find</code> 命令与 <code>gzip</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">find directory_name -<span class="built_in">type</span> f -<span class="built_in">exec</span> gzip &#123;&#125; \;</span><br></pre></td></tr></table></figure><ul><li>例如，压缩 <code>myfolder</code> 目录下的所有文件：<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">find myfolder -<span class="built_in">type</span> f -<span class="built_in">exec</span> gzip &#123;&#125; \;</span><br></pre></td></tr></table></figure></li></ul><h6 id="7-指定压缩级别"><a href="#7-指定压缩级别" class="headerlink" title="7. 指定压缩级别"></a>7. <strong>指定压缩级别</strong></h6><p><code>gzip</code> 提供了从 1 到 9 的压缩级别，数字越大压缩率越高，但速度越慢。默认压缩级别是 6。</p><ul><li><strong>快速压缩（较低压缩比）</strong>：<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">gzip -1 filename</span><br></pre></td></tr></table></figure></li><li><strong>最大压缩（较慢速度）</strong>：<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">gzip -9 filename</span><br></pre></td></tr></table></figure></li></ul><h6 id="8-压缩多个文件"><a href="#8-压缩多个文件" class="headerlink" title="8. 压缩多个文件"></a>8. <strong>压缩多个文件</strong></h6><p>如果你有多个文件想要压缩为单独的 <code>.gz</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">gzip file1 file2 file3</span><br></pre></td></tr></table></figure><ul><li>例如，压缩 <code>file1.txt</code> 和 <code>file2.txt</code>：<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">gzip file1.txt file2.txt</span><br></pre></td></tr></table></figure></li></ul><h6 id="9-检查文件完整性"><a href="#9-检查文件完整性" class="headerlink" title="9. 检查文件完整性"></a>9. <strong>检查文件完整性</strong></h6><p>你可以使用 <code>-t</code> 选项来测试 <code>.gz</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">gzip -t filename.gz</span><br></pre></td></tr></table></figure><h6 id="10-重定向输出到标准输出"><a href="#10-重定向输出到标准输出" class="headerlink" title="10. 重定向输出到标准输出"></a>10. <strong>重定向输出到标准输出</strong></h6><p>使用 <code>-c</code> 选项将压缩结果输出到标准输出（stdout），可以与其他命令配合使用：</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">gzip -c filename &gt; output.gz</span><br></pre></td></tr></table></figure><h4 id="配置服务器处理-Gzip-文件"><a href="#配置服务器处理-Gzip-文件" class="headerlink" title="配置服务器处理 Gzip 文件"></a>配置服务器处理 Gzip 文件</h4><p>虽然 Vite 可以生成 Gzip 或 Brotli 文件，但为了让浏览器请求这些文件时生效，你需要配置 Web 服务器来处理它们。例如，如果你使用 <strong>Nginx</strong>，可以按以下方式配置：</p><h5 id="Nginx-配置："><a href="#Nginx-配置：" class="headerlink" title="Nginx 配置："></a>Nginx 配置：</h5><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></pre></td><td class="code"><pre><span class="line"><span class="section">server</span> &#123;</span><br><span class="line">    <span class="attribute">gzip</span> <span class="literal">on</span>;</span><br><span class="line">    <span class="attribute">gzip_types</span> text/plain application/json application/javascript text/css image/svg+xml;</span><br><span class="line">    <span class="attribute">gzip_static</span> <span class="literal">on</span>;  <span class="comment"># 自动提供 .gz 文件</span></span><br><span class="line">    <span class="section">location</span> / &#123;</span><br><span class="line">        <span class="attribute">try_files</span> <span class="variable">$uri</span> <span class="variable">$uri</span>.gz <span class="variable">$uri</span>.html =<span class="number">404</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这会告诉 Nginx 优先提供 <code>.gz</code> 文件，并自动检测是否存在这些压缩文件。</p><h4 id="Apache-配置示例："><a href="#Apache-配置示例：" class="headerlink" title="Apache 配置示例："></a>Apache 配置示例：</h4><p>如果使用 Apache，可以通过启用 <code>mod_rewrite</code> 和 <code>mod_headers</code> 模块来处理 Gzip 文件。</p><figure class="highlight apache"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><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"># 启用 Gzip 静态文件</span></span><br><span class="line"><span class="attribute">RewriteEngine</span> <span class="literal">On</span></span><br><span class="line"><span class="attribute">RewriteCond</span> <span class="variable">%&#123;REQUEST_FILENAME&#125;</span> !-f</span><br><span class="line"><span class="attribute">RewriteCond</span> <span class="variable">%&#123;REQUEST_FILENAME&#125;</span>.gz -f</span><br><span class="line"><span class="attribute">RewriteRule</span> ^(.*)$ $<span class="number">1</span>.gz<span class="meta"> [QSA]</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 设置正确的 Content-Encoding 头</span></span><br><span class="line"><span class="section">&lt;FilesMatch <span class="string">&quot;\.gz$&quot;</span>&gt;</span></span><br><span class="line">    <span class="attribute">Header</span> set Content-Encoding gzip</span><br><span class="line">    <span class="attribute">Header</span> set Cache-Control <span class="string">&quot;max-age=31536000&quot;</span></span><br><span class="line"><span class="section">&lt;/FilesMatch&gt;</span></span><br></pre></td></tr></table></figure><h2 id="在浏览器中检测-Gzip-压缩"><a href="#在浏览器中检测-Gzip-压缩" class="headerlink" title="在浏览器中检测 Gzip 压缩"></a>在浏览器中检测 Gzip 压缩</h2><p>构建完成并配置好服务器后，你可以通过浏览器的开发者工具或者线上工具（如 <a href="https://www.giftofspeed.com/gzip-test/">Gzip Test</a>）来检查 JSON 文件是否成功启用了 Gzip 压缩。。在响应头中，<code>Content-Encoding: gzip</code> 表示服务器成功返回了 Gzip 压缩的内容。</p><p>也可以使用 <code>curl</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">curl -I -H <span class="string">&quot;Accept-Encoding: gzip&quot;</span> https://your-domain.com/bundle.js</span><br></pre></td></tr></table></figure><p>如果 Gzip 压缩有效，你应该会看到类似如下的响应头：</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">Content-Encoding: gzip</span><br></pre></td></tr></table></figure><p><img src="https://images.nirvana.net.cn/static/images/images/10024246-a6cce506c5e1b8b1.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="><br> 我们通过Http头中的: 属性判断返回后的数据已经启用了gzip压缩:<br><img src="https://images.nirvana.net.cn/static/images/images/10024246-192a553cac990f49.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="></p><blockquote><p>注意：如果未收到gzip的返回，需要看下服务器端的gzip_types是否支持你访问的文件类型</p></blockquote><h2 id="Gzip兼容性"><a href="#Gzip兼容性" class="headerlink" title="Gzip兼容性"></a>Gzip兼容性</h2><p>所有主流浏览器都支持 Gzip 压缩。这意味着如果服务器返回 Gzip 压缩的资源，浏览器可以自动解压缩并呈现页面。<br><img src="https://images.nirvana.net.cn/static/images/images/10024246-a5ece7c12ebc8268.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="><br>Gzip 支持非常广泛，从早期版本的浏览器（如 Internet Explorer 6）到现代浏览器都默认支持它，因此从浏览器兼容性的角度来看，使用 Gzip 是非常安全的。</p><h4 id="Gzip-支持的文件类型"><a href="#Gzip-支持的文件类型" class="headerlink" title="Gzip 支持的文件类型"></a><strong>Gzip 支持的文件类型</strong></h4><p>虽然 Gzip 对大多数文本内容（如 HTML、CSS、JavaScript、JSON 等）压缩效果显著，但某些文件类型并不适合使用 Gzip。一般来说，Gzip 适合以下类型的文件：</p><ul><li><strong>HTML、CSS、JavaScript</strong>：这些是最常压缩的文件类型，压缩率通常在 60%-80% 左右。</li><li><strong>JSON 和 XML</strong>：用于 API 的数据格式，压缩率较高。</li><li><strong>SVG</strong>：虽然是图像文件，但由于它是 XML 格式，压缩率也较好。</li></ul><p>不适合 Gzip 压缩的文件类型包括：</p><ul><li><strong>图片</strong>（如 JPEG、PNG、GIF 等）：这些文件通常已经压缩，再次使用 Gzip 压缩效果不佳，甚至可能导致文件体积增大。</li><li><strong>视频和音频文件</strong>（如 MP4、MP3 等）：这些多媒体文件已经过压缩，使用 Gzip 无效。</li><li><strong>PDF 文件</strong>：PDF 文件通常也已压缩，再使用 Gzip 可能没有显著效果。</li></ul><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>Gzip 是一种高效、广泛兼容的压缩算法，在提升网站性能方面表现突出。<br>适用于大多数文本文件，特别是在 Web 环境中压缩 HTML、CSS、JavaScript、JSON 等资源文件，能够显著缩短页面加载时间并节省带宽。<br>现代浏览器和服务器均支持 Gzip，因此在大多数 Web 应用中使用 Gzip 压缩是安全且高效的性能优化方案。</p><p>因此，在大多数网站和 Web 应用中，启用 Gzip 压缩是提升性能的最佳实践之一。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;&lt;img src=&quot;https://images.nirvana.net.cn/static/images/images/10024246-611d1412c2aeabfb.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2Awdb</summary>
      
    
    
    
    <category term="前端" scheme="https://www.nirvana.net.cn/categories/%E5%89%8D%E7%AB%AF/"/>
    
    
    <category term="web" scheme="https://www.nirvana.net.cn/tags/web/"/>
    
  </entry>
  
  <entry>
    <title>使用 Vite 极快速度构建 Web 应用</title>
    <link href="https://www.nirvana.net.cn/2024/10125UFo555gg2.html"/>
    <id>https://www.nirvana.net.cn/2024/10125UFo555gg2.html</id>
    <published>2024-10-12T08:28:03.000Z</published>
    <updated>2024-10-13T08:46:57.761Z</updated>
    
    <content type="html"><![CDATA[<p><img src="https://images.nirvana.net.cn/static/images/images/10024246-685b98d1c88e3e91.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="><br>前端构建过程已经慢且复杂了太长时间了。无论你是在使用 Webpack、Parcel，还是你自己组合的“怪物”配置（我们都经历过），配置和打包一个 Web 应用往往是个令人头疼的事情。</p><p>Vite 是个新晋者，它以速度和简洁令人眼前一亮。而 Vite 不仅仅对开发者重要，它对于企业来说也是一次颠覆。快速的开发周期意味着开发者更开心。开心的开发者写出更好的代码，这也意味着更快的上市时间和更低的业务或客户成本。</p><p>Vite 旨在让缓慢的构建痛苦消失。它利用浏览器中的原生 ES 模块，消除了开发构建中对打包代码的需求。翻译一下：你将体验到如此快速的模块热替换（HMR），以至于你的 F5 键都要感到失宠了。让我们深入探讨如何让你的项目使用 Vite，并从此再无回头之路。</p><p>Vite 是一个法语单词，意思是“快速”或“迅速”。发音是“veet”，与“meet”押韵。</p><h3 id="为什么选择-Vite？谁有时间浪费在慢速构建上？"><a href="#为什么选择-Vite？谁有时间浪费在慢速构建上？" class="headerlink" title="为什么选择 Vite？谁有时间浪费在慢速构建上？"></a>为什么选择 Vite？谁有时间浪费在慢速构建上？</h3><p>我们都知道盯着屏幕看进度条慢慢移动有多痛苦，那简直让人怀疑人生。</p><p>Vite 提供了几乎瞬时的冷启动服务器、闪电般的模块热替换（HMR），以及比你说“npm install”还要快的构建。</p><ul><li><strong>闪电般的开发启动</strong>：像 Webpack 这样的传统工具在启动之前会先打包你的所有代码。而 Vite 使用原生的 ES 模块来服务你的代码，因此启动很快——浏览器只加载它需要的部分。</li><li><strong>模块热替换</strong>：修改后的代码立即反映，让你的开发体验像爵士萨克斯风独奏一样顺畅。</li><li><strong>开箱即用的 TypeScript 支持</strong>：Vite 可以处理 TypeScript，而无需设置复杂的构建管道。</li><li><strong>优化的构建</strong>：当需要上线时，Vite 使用 Rollup 来高效地打包你的代码。它会将你的代码分成多个块，这样用户就不会下载不需要的内容。</li></ul><h3 id="开始使用-Vite"><a href="#开始使用-Vite" class="headerlink" title="开始使用 Vite"></a>开始使用 Vite</h3><p>让我们试一试 Vite，看看它能做些什么！</p><h5 id="第一步：设置你的-Vite-项目"><a href="#第一步：设置你的-Vite-项目" class="headerlink" title="第一步：设置你的 Vite 项目"></a>第一步：设置你的 Vite 项目</h5><p>首先，让我们创建一个新的 Vite 项目。假设你已经安装了 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">npm create vite@latest my-awesome-vite-project -- --template react</span><br></pre></td></tr></table></figure><p>这个命令会创建一个基于 React 的 Vite 项目。如果你更喜欢 Vue 或者纯 Vanilla JS，随时可以替换“react”为“vue”或“vanilla”。Svelte、Preact、Qwik 等也都支持！</p><h5 id="第二步：安装依赖并运行开发服务器"><a href="#第二步：安装依赖并运行开发服务器" class="headerlink" title="第二步：安装依赖并运行开发服务器"></a>第二步：安装依赖并运行开发服务器</h5><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">cd</span> my-awesome-vite-project</span><br><span class="line">npm install</span><br><span class="line">npm run dev</span><br></pre></td></tr></table></figure><p>砰！你的开发服务器现在已经像打了咖啡因的松鼠一样运行了。打开浏览器，修改源文件之一，惊叹于它的速度！</p><h5 id="第三步：构建项目结构"><a href="#第三步：构建项目结构" class="headerlink" title="第三步：构建项目结构"></a>第三步：构建项目结构</h5><p>Vite 给你提供了一个相当简洁的起始结构。以下是一个典型的项目设置：</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">my-awesome-vite-project/</span><br><span class="line">├── public/</span><br><span class="line">│   └── vite.svg</span><br><span class="line">├── src/</span><br><span class="line">│   └── assets/</span><br><span class="line">│       └── react.svg</span><br><span class="line">│   ├── App.css</span><br><span class="line">│   ├── App.jsx</span><br><span class="line">│   ├── index.css</span><br><span class="line">│   └── main.jsx</span><br><span class="line">├── index.html</span><br><span class="line">└── vite.config.js</span><br></pre></td></tr></table></figure><p>你可以根据需要添加更多的文件夹，比如 styles、utils 或 services。Vite 不会对你指手画脚。</p><h4 id="第四步：自定义你的-Vite-配置"><a href="#第四步：自定义你的-Vite-配置" class="headerlink" title="第四步：自定义你的 Vite 配置"></a>第四步：自定义你的 Vite 配置</h4><p>打开 <code>vite.config.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><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><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; defineConfig &#125; <span class="keyword">from</span> <span class="string">&quot;vite&quot;</span>;</span><br><span class="line"><span class="keyword">import</span> react <span class="keyword">from</span> <span class="string">&quot;@vitejs/plugin-react&quot;</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">plugins</span>: [<span class="title function_">react</span>()],</span><br><span class="line">  <span class="attr">server</span>: &#123;</span><br><span class="line">    <span class="attr">port</span>: <span class="number">3000</span>,</span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="attr">build</span>: &#123;</span><br><span class="line">    <span class="attr">outDir</span>: <span class="string">&quot;dist&quot;</span>,</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">&#125;);</span><br></pre></td></tr></table></figure><p>这个配置设置了开发服务器端口、构建输出目录，并启用了源映射。还有很多其他的方式可以配置 Vite，以满足你的需求！</p><h5 id="第五步：添加一些代码并见证奇迹"><a href="#第五步：添加一些代码并见证奇迹" class="headerlink" title="第五步：添加一些代码并见证奇迹"></a>第五步：添加一些代码并见证奇迹</h5><p>让我们创建一个新组件，看看 Vite 的 HMR（模块热替换）是如何工作的。创建一个新文件 <code>src/Hello.jsx</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></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">&quot;react&quot;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="keyword">function</span> <span class="title function_">Hello</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> [name, setName] = <span class="title function_">useState</span>(<span class="string">&quot;World&quot;</span>);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">h1</span>&gt;</span>Hello, &#123;name&#125;!<span class="tag">&lt;/<span class="name">h1</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">input</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">        <span class="attr">type</span>=<span class="string">&quot;text&quot;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">        <span class="attr">value</span>=<span class="string">&#123;name&#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;(e)</span> =&gt;</span> setName(e.target.value)&#125;</span></span><br><span class="line"><span class="language-xml">        placeholder=&quot;Enter a name&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">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>App.jsx</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><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; useState &#125; <span class="keyword">from</span> <span class="string">&quot;react&quot;</span>;</span><br><span class="line"><span class="keyword">import</span> reactLogo <span class="keyword">from</span> <span class="string">&quot;./assets/react.svg&quot;</span>;</span><br><span class="line"><span class="keyword">import</span> viteLogo <span class="keyword">from</span> <span class="string">&quot;../public/vite.svg&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 class="keyword">import</span> <span class="title class_">Hello</span> <span class="keyword">from</span> <span class="string">&quot;./Hello.jsx&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">const</span> [count, setCount] = <span class="title function_">useState</span>(<span class="number">0</span>);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">Hello</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">a</span> <span class="attr">href</span>=<span class="string">&quot;https://vitejs.dev&quot;</span> <span class="attr">target</span>=<span class="string">&quot;_blank&quot;</span>&gt;</span></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;viteLogo&#125;</span> <span class="attr">className</span>=<span class="string">&quot;logo&quot;</span> <span class="attr">alt</span>=<span class="string">&quot;Vite logo&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></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://react.dev&quot;</span> <span class="attr">target</span>=<span class="string">&quot;_blank&quot;</span>&gt;</span></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;reactLogo&#125;</span> <span class="attr">className</span>=<span class="string">&quot;logo react&quot;</span> <span class="attr">alt</span>=<span class="string">&quot;React logo&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></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>Vite + React<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> <span class="attr">className</span>=<span class="string">&quot;card&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">onClick</span>=<span class="string">&#123;()</span> =&gt;</span> setCount((count) =&gt; count + 1)&#125;&gt;</span></span><br><span class="line"><span class="language-xml">          count is &#123;count&#125;</span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;/<span class="name">button</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">p</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">          Edit <span class="tag">&lt;<span class="name">code</span>&gt;</span>src/App.jsx<span class="tag">&lt;/<span class="name">code</span>&gt;</span> and save to test HMR</span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;/<span class="name">p</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">p</span> <span class="attr">className</span>=<span class="string">&quot;read-the-docs&quot;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">        Click on the Vite and React logos to learn more</span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;/<span class="name">p</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/&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>保存这些文件，看看 Vite 如何比你说出“为什么我没早点切换到 Vite？”还快地更新你的应用！</p><h3 id="Vite-的一些酷炫功能"><a href="#Vite-的一些酷炫功能" class="headerlink" title="Vite 的一些酷炫功能"></a>Vite 的一些酷炫功能</h3><p>Vite 不止于此，它有很多很棒的功能：</p><ul><li><strong>静态资源？搞定</strong>：Vite 会自动优化你的图片和其他资源。</li><li><strong>环境变量？没问题</strong>：使用 <code>.env</code> 文件可以轻松管理不同环境。</li><li><strong>插件？应有尽有</strong>：需要更多功能？可能已经有插件了。</li><li><strong>CSS 预处理器？当然可以</strong>：想用 Sass？安装并直接使用，Vite 会帮你搞定一切。</li></ul><h5 id="添加-Sass-到-Vite-的示例"><a href="#添加-Sass-到-Vite-的示例" class="headerlink" title="添加 Sass 到 Vite 的示例"></a>添加 Sass 到 Vite 的示例</h5><p>运行以下命令安装 Sass：</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 --save-dev sass</span><br></pre></td></tr></table></figure><p>在 <code>src</code> 文件夹中创建一个名为 <code>App.scss</code> 的文件，并添加一些代码，例如：</p><figure class="highlight scss"><table><tr><td class="gutter"><pre><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">$awesome-color</span>: <span class="number">#ff69b4</span>;</span><br><span class="line"><span class="selector-class">.card</span> &#123;</span><br><span class="line">  <span class="attribute">color</span>: <span class="variable">$awesome-color</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>更新 <code>App.jsx</code>，导入这个新的 Sass 文件：</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> <span class="string">&#x27;./App.scss&#x27;</span>;</span><br></pre></td></tr></table></figure><h3 id="总结：Vite-超棒！"><a href="#总结：Vite-超棒！" class="headerlink" title="总结：Vite 超棒！"></a>总结：Vite 超棒！</h3><ul><li><strong>速度很重要</strong>——Vite 的开发服务器如此之快，以至于你会忘记曾经还需要刷新页面。</li><li><strong>设置简单</strong>——只需一个简单的命令，你就可以启动一个准备就绪的项目。</li><li><strong>框架无关</strong>——无论你使用 React、Vue、Svelte、Preact、Solid、Qwik，或者只是喜欢使用纯 JavaScript，Vite 都能支持你。</li><li><strong>生产就绪</strong>——Rollup 优化确保你的代码尽可能精简和高效。</li></ul><p>现在，去构建一些令人惊叹的东西吧！Vite 出发！</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;&lt;img src=&quot;https://images.nirvana.net.cn/static/images/images/10024246-685b98d1c88e3e91.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2Awdb</summary>
      
    
    
    
    <category term="前端" scheme="https://www.nirvana.net.cn/categories/%E5%89%8D%E7%AB%AF/"/>
    
    
    <category term="web" scheme="https://www.nirvana.net.cn/tags/web/"/>
    
  </entry>
  
  <entry>
    <title>谷歌的全新 CrUX Vis 工具：探索核心网页指标数据</title>
    <link href="https://www.nirvana.net.cn/2024/1012mNWE5M2q55.html"/>
    <id>https://www.nirvana.net.cn/2024/1012mNWE5M2q55.html</id>
    <published>2024-10-12T08:28:03.000Z</published>
    <updated>2024-10-13T08:46:57.790Z</updated>
    
    <content type="html"><![CDATA[<p><img src="https://images.nirvana.net.cn/static/images/images/10024246-a98b19f02ee4ac6a.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="><br>虽然谷歌已经通过API提供了超过一年的历史CrUX数据，现在CrUX Vis提供了一个易于访问的工具来获取这些历史核心网页指标数据。</p><h2 id="如何使用CrUX-Vis测试您的网站"><a href="#如何使用CrUX-Vis测试您的网站" class="headerlink" title="如何使用CrUX Vis测试您的网站"></a><strong>如何使用CrUX Vis测试您的网站</strong></h2><p>要在CrUX Vis上查看您的核心网页指标数据：</p><ul><li><ol><li>打开 cruxvis.withgoogle.com</li></ol></li><li><ol start="2"><li>在URL字段中输入您的网站</li></ol></li><li><ol start="3"><li>按回车键<br><img src="https://images.nirvana.net.cn/static/images/images/10024246-057886cbc2380f32.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="><br>接着，您将看到过去25周内三个核心网页指标的变化趋势。<br><img src="https://images.nirvana.net.cn/static/images/images/10024246-7d881d32f20f0e29.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="></li></ol></li></ul><h2 id="什么是Chrome用户体验报告（CrUX）？"><a href="#什么是Chrome用户体验报告（CrUX）？" class="headerlink" title="什么是Chrome用户体验报告（CrUX）？"></a><strong>什么是Chrome用户体验报告（CrUX）？</strong></h2><p>Chrome用户体验报告是谷歌从实际Chrome用户那里收集的网站性能数据集。仅收集已登录谷歌账户、启用了使用统计报告功能并使用Chrome浏览器的用户的数据。</p><blockquote><p>在移动端，CrUX数据仅在Android上收集，而不会在iOS的Chrome浏览器中收集。</p></blockquote><p>CrUX数据并非适用于每个网站，只有流量足够的网站才能提供。如果您的网站流量不足，您会看到“CrUX数据不可用”的提示。<br><img src="https://images.nirvana.net.cn/static/images/images/10024246-a69c31ff13c183bb.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="><br>谷歌每周更新历史CrUX数据，但每个数值覆盖一个滚动的28天周期。这意味着如果您对网站进行了更改，您的网页指标数据将需要4周时间才能完全更新。<br>翻译：</p><p><strong>CrUX Vis 提供哪些数据？</strong><br>在首页，CrUX Vis 提供了包含三个核心网页指标数据的时间线：</p><ul><li><a href="https://www.debugbear.com/docs/metrics/largest-contentful-paint">最大内容绘制（Largest Contentful Paint）</a>：加载性能</li><li><a href="https://www.debugbear.com/docs/metrics/cumulative-layout-shift">累积布局偏移（Cumulative Layout Shift）</a>：视觉稳定性</li><li><a href="https://www.debugbear.com/docs/metrics/interaction-to-next-paint">与下一次绘制的交互时间（Interaction to Next Paint）</a>：交互性<br>CrUX Vis 还会告诉您，您的网站在这些指标上是有所改善还是退步。<br><img src="https://images.nirvana.net.cn/static/images/images/10024246-cdadee4782d1f00f.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="><br>CrUX Vis 的侧边栏提供了查看每个指标详细信息的方式。<br><img src="https://images.nirvana.net.cn/static/images/images/10024246-d0d5174da505e2c7.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="><h4 id="配置显示哪些数据"><a href="#配置显示哪些数据" class="headerlink" title="配置显示哪些数据"></a><strong>配置显示哪些数据</strong></h4>默认情况下，谷歌的全新 CrUX 数据工具显示的是每个指标的第75百分位。这意味着，如果您的LCP得分是1.5秒，那么75%的访客等待时间不超过1.5秒，而另有25%的访客等待时间更长。</li></ul><p>然而，在各个指标标签中，CrUX Vis还提供了一个选项，可以查看体验评级为“优秀”、“需要改进”或“不佳”的分布情况。<br><img src="https://images.nirvana.net.cn/static/images/images/10024246-a5ca4144583a3725.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="></p><h4 id="加载性能详情"><a href="#加载性能详情" class="headerlink" title="加载性能详情"></a><strong>加载性能详情</strong></h4><p>除了最大内容绘制（LCP）指标，您还可以查看其他相关指标，以了解和优化网站性能。</p><p>例如，您可以查看首次内容绘制（First Contentful Paint）或首次字节时间（Time to First Byte）。这两个指标可以帮助您了解阻碍主要页面内容显示的原因。</p><p>例如，如果首次内容绘制时间较长，您需要检查网站上是否有很多阻止渲染的资源。如果首次字节时间较慢，说明需要优化服务器响应时间。<br><img src="https://images.nirvana.net.cn/static/images/images/10024246-44e5656b2b695844.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="><br>此外，CrUX Vis还提供了有关用户如何访问您网站的信息，例如设备形态或导航类型。</p><p>导航类型告诉您，您的网站是否受益于HTTP缓存、内容预渲染，或有多少访问可以通过更好地支持前进/后退缓存受益。</p><p><img src="https://images.nirvana.net.cn/static/images/images/10024246-67dde08f1099174d.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="></p><h2 id="过滤-CrUX-Vis-数据"><a href="#过滤-CrUX-Vis-数据" class="headerlink" title="过滤 CrUX Vis 数据"></a><strong>过滤 CrUX Vis 数据</strong></h2><p>该工具提供了多种过滤选项，例如按日期范围过滤。</p><p>您还可以选择只查看移动端或桌面端数据。工具还会方便地显示您网站的流量中有多少来自移动设备或桌面设备。<br><img src="https://images.nirvana.net.cn/static/images/images/10024246-cf84cf84ec85a866.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="></p><h2 id="使用-CrUX-Vis-查看-URL-级数据"><a href="#使用-CrUX-Vis-查看-URL-级数据" class="headerlink" title="使用 CrUX Vis 查看 URL 级数据"></a><strong>使用 CrUX Vis 查看 URL 级数据</strong></h2><p>与 PageSpeed Insights 类似，CrUX Vis 不仅可以显示整个来源的数据，还可以显示特定 URL 的数据。不过请记住，只有流量足够的 URL 才会有特定的 URL 数据。</p><p><img src="https://images.nirvana.net.cn/static/images/images/10024246-62a649c46f7ade9b.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="></p><h2 id="CrUX-Vis-与其他工具的区别"><a href="#CrUX-Vis-与其他工具的区别" class="headerlink" title="CrUX Vis 与其他工具的区别"></a><strong>CrUX Vis 与其他工具的区别</strong></h2><p>CrUX Vis 比 PageSpeed Insights 更高级一些，因为它包括历史数据且数据更为深入。然而，与 PageSpeed Insights 不同，CrUX Vis 不提供具体的性能优化建议。在 PageSpeed Insights 中，这些建议是通过运行 Lighthouse 实验测试得出的。</p><p>谷歌的 CrUX Dashboard 提供查看历史数据的功能，但其用户体验较差，且只提供每月数据，而非每周数据。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;&lt;img src=&quot;https://images.nirvana.net.cn/static/images/images/10024246-a98b19f02ee4ac6a.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2Awdb</summary>
      
    
    
    
    <category term="前端" scheme="https://www.nirvana.net.cn/categories/%E5%89%8D%E7%AB%AF/"/>
    
    
    <category term="web" scheme="https://www.nirvana.net.cn/tags/web/"/>
    
    <category term="chrome" scheme="https://www.nirvana.net.cn/tags/chrome/"/>
    
  </entry>
  
  <entry>
    <title>Web 上HTTP工作原理</title>
    <link href="https://www.nirvana.net.cn/2024/1011tiOIiVVV22.html"/>
    <id>https://www.nirvana.net.cn/2024/1011tiOIiVVV22.html</id>
    <published>2024-10-11T08:28:03.000Z</published>
    <updated>2024-10-13T08:46:57.761Z</updated>
    
    <content type="html"><![CDATA[<p><img src="https://images.nirvana.net.cn/static/images/images/10024246-e7eccd45b65a18a6.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="><br>Web 的工作原理是客户端使用 HTTP 协议从服务器请求资源。需要服务器连接才能确保数据可靠且安全地传输。</p><p>本文将探讨浏览器如何在 Web 上创建与服务器的连接，创建连接所需的网络往返过程，以及所有这些如何影响页面加载速度。</p><h2 id="什么是-Web-服务器连接？"><a href="#什么是-Web-服务器连接？" class="headerlink" title="什么是 Web 服务器连接？"></a>什么是 Web 服务器连接？</h2><p>在浏览器发出 HTTP 请求以获取资源之前，首先需要与要加载资源的 Web 服务器建立连接。如果不先建立连接，数据传输将不可靠且不安全——稍后我们将解释其含义。</p><p>当你查看请求瀑布图时，可以看到服务器连接的创建过程，例如在我们免费的速度测试工具或 Chrome 开发者工具中。</p><p>你会看到三个分别用青色、橙色和紫色表示的步骤，紧随其后的绿色 HTTP 请求和蓝色下载。这三个步骤就是创建连接的过程。<br><img src="https://images.nirvana.net.cn/static/images/images/10024246-991526499e169180.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="><br>上面的截图还展示了 Chrome 为服务器连接分配的 ID。当向同一服务器发送另一个请求时，Chrome 可以重用该连接。然而，当向新服务器发出请求时，必须创建一个新的连接。</p><h2 id="建立-HTTP-服务器连接需要哪些步骤？"><a href="#建立-HTTP-服务器连接需要哪些步骤？" class="headerlink" title="建立 HTTP 服务器连接需要哪些步骤？"></a>建立 HTTP 服务器连接需要哪些步骤？</h2><p>通常，建立 HTTP 服务器连接需要三个步骤：</p><ul><li>DNS 查询：根据域名查找服务器的 IP 地址</li><li>TCP 连接：确保可靠通信</li><li>TLS/SSL 连接：确保安全的加密通信</li></ul><p>之后，就可以通过 HTTP 请求向服务器请求资源。</p><h4 id="DNS-查询"><a href="#DNS-查询" class="headerlink" title="DNS 查询"></a>DNS 查询</h4><p>IP 地址（如 12.34.456.789）用于在网络上不同计算机之间传递消息。然而，它们难以记忆，对于人类来说也不方便操作。一个网站可能会从一台服务器迁移到另一台，而我们不希望在每次发生这种情况时都更新对该网站的所有引用。</p><p>因此，我们使用域名来访问网站。域名系统 (DNS) 允许我们将一个域名（如 example.com）映射到 IP 地址。</p><p>在发送 HTTP 请求之前，我们需要进行 DNS 查询以找到服务器的 IP 地址。</p><p>你可以在命令行中使用 <code>dig</code> 来查找服务器的 IP 地址：</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">dig +short example.com</span><br><span class="line">93.184.216.34</span><br></pre></td></tr></table></figure><p>如果你知道计算机的 IP 地址，可以使用互联网协议 (IP) 向其发送消息。</p><p>在这个瀑布图中，你可以看到 DNS 查询作为第一个步骤以青色显示，表示 HTML 文档请求的第一部分。<br><img src="https://images.nirvana.net.cn/static/images/images/10024246-27d04a43f21b378a.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="></p><h4 id="TCP-连接"><a href="#TCP-连接" class="headerlink" title="TCP 连接"></a>TCP 连接</h4><p>不幸的是，互联网协议并不可靠：消息可能会在传输途中丢失，而客户端对此一无所知。</p><p>消息的最大大小以及一次可以发送的消息数量也存在限制，但这些限制取决于服务器及其网络连接情况。如果网络仅支持每秒 1 兆字节的数据传输，而你每秒发送 10 兆字节的数据，大多数消息将会丢失。</p><p>因此，浏览器使用传输控制协议 (TCP) 来创建服务器连接，并确保数据可靠传输。</p><p>例如，TCP 确保：</p><ul><li>丢失的消息会被重新发送</li><li>长消息会被分割成多个数据块，并且这些块会按照正确的顺序处理</li><li>消息发送速率会根据网络连接的状况调整</li></ul><p>在请求资源之前，客户端首先交换一条消息以协调数据传输方式。这种消息交换称为 TCP 握手，它建立了 TCP 连接。</p><p>你可以在截图中看到 TCP 握手以橙色标记。<br><img src="https://images.nirvana.net.cn/static/images/images/10024246-71a4a5e44d0e43b4.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="></p><h4 id="TLS-SSL-连接"><a href="#TLS-SSL-连接" class="headerlink" title="TLS/SSL 连接"></a><strong>TLS/SSL 连接</strong></h4><p>一旦建立了 TCP 连接，理论上浏览器可以通过它开始请求资源。然而，这里有一个问题：消息不仅可以被接收方读取，沿途的每个网络基础设施也能读取它。<br><img src="https://images.nirvana.net.cn/static/images/images/10024246-2470cc474575ce46.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="><br>这意味着你家中的路由器、你手机或笔记本连接的设备可以读取你的消息。你的互联网服务提供商可以看到消息的内容。拥有海底光缆的互联网骨干服务提供商也能看到你的消息。这些网络节点还可以拦截消息，假冒你试图访问的网站。<br><img src="https://images.nirvana.net.cn/static/images/images/10024246-c29933ff24673213.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="><br>传输层安全协议（TLS）用于保护 TCP 连接。客户端会加密它想发送给服务器的消息，这样沿途的网络节点只能看到元数据，例如数据包的数量或目标 IP 地址。因此，他们可以知道你正在加载哪个网站，但看不到具体的页面内容或你发送到该网站的数据。</p><p>那么，客户端和服务器如何从不安全的连接创建安全连接呢？互联网中的计算机不使用共享密码来加密和解密消息，而是使用公钥加密技术，它有两个独立的“密码”（称为密钥）。</p><p>公钥通过不安全的连接发送，可以用来加密消息。但只有私钥可以解密消息，且私钥从不通过网络传输。公钥包含在服务器的 TLS 证书中。</p><p>证书可以通过受信任的证书颁发机构（如 DigiCert 或 Let’s Encrypt）进行验证，防止沿途的设备伪装成你访问的网站。</p><p>你可能还听说过 SSL 证书和 SSL 连接。SSL 是 TLS 之前的一个旧协议，虽然它已经不再使用，但人们仍然常用这个旧名来指代加密连接。</p><p>为了建立安全连接，浏览器和网站服务器会在不安全的 TCP 连接上进行 TLS 握手。这个截图显示了以紫色标记的 TLS 握手。</p><p><img src="https://images.nirvana.net.cn/static/images/images/10024246-ea89bd954f940c57.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="></p><p>在建立安全的 TLS 连接后，浏览器可以使用 HTTP 协议向服务器请求它想要加载的资源。</p><p>当服务器开始发送响应时，浏览器只需下载它。在这种情况下，下载只需很短的时间。</p><h2 id="服务器连接如何影响网站速度？"><a href="#服务器连接如何影响网站速度？" class="headerlink" title="服务器连接如何影响网站速度？"></a><strong>服务器连接如何影响网站速度？</strong></h2><p>我们看到，在浏览器实际开始下载网站之前，需要经过几个步骤。每一步都需要一定时间在网络上传递消息并由服务器处理。</p><p>HTTP 步骤显示为服务器响应时间或首次字节时间 (TTFB)。<br><img src="https://images.nirvana.net.cn/static/images/images/10024246-f8723195cb764028.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="><br><strong>网络请求分解显示 DNS、TCP、SSL、TTFB 和下载步骤及其耗时</strong></p><p>处理 DNS 查询或建立 TCP 或 TLS 连接相对简单，所需时间不多。在这些步骤中，消息在网络上传输的时间通常占用了最多时间。</p><p>HTTP 请求通常耗时更长，尤其是在动态生成 HTML 文档或从数据库加载数据时。</p><h2 id="什么是网络往返时间及其决定因素？"><a href="#什么是网络往返时间及其决定因素？" class="headerlink" title="什么是网络往返时间及其决定因素？"></a><strong>什么是网络往返时间及其决定因素？</strong></h2><p>网络往返时间（round trip time, RTT）测量从发送消息到服务器并收到回复所需的时间。这包括：</p><ul><li>从你的手机或笔记本将消息发送到你的 ISP（互联网服务提供商）</li><li>通过网络将消息路由到服务器</li><li>生成服务器响应并将其发送回来</li></ul><p>互联网上两台计算机之间的 RTT 测量交换消息所需的时间。</p><p>如前所述，单单请求网站的初始 HTML 文档就需要至少 4 次网络往返。如果每次往返需要 50 毫秒，意味着请求至少需要 200 毫秒。如果往返时间为 150 毫秒，初始请求需要至少 600 毫秒。</p><h4 id="最后一公里延迟"><a href="#最后一公里延迟" class="headerlink" title="最后一公里延迟"></a><strong>最后一公里延迟</strong></h4><p>“最后一公里” 指的是从一个枢纽到最终目的地的最后一步。例如，邮递员可以很容易地将一大袋信件运送到全国各地，但将每封信投入正确的邮箱则需要更多工作。</p><p>在网络中，”最后一公里” 指的是终端用户与其互联网服务提供商之间的数据交换。这可能意味着将消息发送到家用路由器或通过空中传送到移动设备。</p><p>在快速的有线连接上，最后一公里的延迟通常小于 10 毫秒。然而，在 4G 网络上，40 毫秒的往返延迟更为常见。在 3G 网络中，延迟可能为 80 毫秒或更高。</p><p>以下是英国不同网络提供商的平均移动延迟数据。<br><img src="https://images.nirvana.net.cn/static/images/images/10024246-9ec1dd399c230344.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="></p><h4 id="互联网传播"><a href="#互联网传播" class="headerlink" title="互联网传播"></a><strong>互联网传播</strong></h4><p>如果你在美国打开托管在意大利的一个网站，会发生什么？从你的 ISP 到那个特定服务器没有直接路径。相反，消息会在网络上执行多个“跳跃”，直到到达最终目的地。</p><p>你可以使用 <code>traceroute</code> 命令或可视化工具来查看消息在网络中的移动方式。在这个例子中，消息先被传递到瑞典的一个网络节点，然后到达意大利。<br><img src="https://images.nirvana.net.cn/static/images/images/10024246-f5e3b98bff3aafc1.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="><br>消息在网络上传播需要时间。假设我们从芝加哥发送一条消息到罗马，二者相距 5000 英里。即便数据以光速传播，单程也需要 26 毫秒。往返则需要 52 毫秒。</p><p>实际上，没有直接路线，数据也不会以光速传播，因此更常见的往返延迟为 118 毫秒。</p><h4 id="其他因素"><a href="#其他因素" class="headerlink" title="其他因素"></a><strong>其他因素</strong></h4><p>还有许多其他因素影响网络的往返时间，例如沿途服务器处理其他消息而导致的处理时间延迟。</p><h4 id="计算请求时长"><a href="#计算请求时长" class="headerlink" title="计算请求时长"></a><strong>计算请求时长</strong></h4><p>假设我们使用的移动网络最后一公里延迟为 60 毫秒。我们向意大利的服务器发送请求，其往返延迟为 118 毫秒。假设服务器立即响应。请求总共需要多长时间？</p><p>首先，我们需要通过域名系统 (DNS) 获取 IP 地址。DNS 是一个分布式系统，如果我们访问的是一个受欢迎的网站，附近的 DNS 服务器可能已经缓存了 IP 地址。因此，这一步只需 60 毫秒，假设联系 DNS 服务器耗时不到 1 毫秒。</p><p>接下来我们需要对网站服务器进行三次往返。每次往返耗时 178 毫秒（60 + 118），总共耗时 534 毫秒。</p><p>我们得到的请求时长为 594 毫秒。这假设服务器响应很小，并完全包含在 HTTP 请求的初始响应消息中。如果我们需要下载更大的文件，则需要更多时间。</p><h2 id="网络往返时间如何影响下载时间？"><a href="#网络往返时间如何影响下载时间？" class="headerlink" title="网络往返时间如何影响下载时间？"></a><strong>网络往返时间如何影响下载时间？</strong></h2><p>在互联网上下载资源的时长不仅取决于连接的带宽，还取决于每次网络往返的时间。</p><p>例如，如果服务器响应的大小为 1 兆字节，它不会一次性发送全部数据，因为服务器连接可能无法处理如此大的数据量，导致丢失数据包并需要重新发送。</p><p>相反，使用了一种称为 TCP 慢启动的机制，服务器开始时只并行发送 10 个数据包，然后逐渐增加发送的数据包数量，直到开始丢包。</p><p>一般规则是，最初服务器响应中可以发送 14 KB 的数据，任何额外数据都需要额外的网络往返。理论上，服务器下一次可以发送两倍的数据量，即在下一个往返中下载 28 KB 的数据。</p><p>请注意，HTTP 响应通常使用 gzip 或 Brotli 压缩，因此 14 KB 的压缩数据实际上可能相当于 50 KB 的未压缩 HTML 代码。</p><p>我们进行了一次基于实验室的性能测试，网络延迟为 1 秒。因此，我们可以清楚地看到每次往返。<br><img src="https://images.nirvana.net.cn/static/images/images/10024246-ef9e59e20e7dc02c.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="><br>测试结果如下图所示：<br><img src="https://images.nirvana.net.cn/static/images/images/10024246-75c69c9e44042b8d.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="><br>如果 HTML 文档的大小不超过约 10 KB，则一次网络往返就足够下载响应。之后最多可以下载 38 KB 的数据，然后再次需要另一次往返。请谨慎使用这些数据，因为可能存在小的测量误差。</p><h2 id="HTTP-2-相比-HTTP-1-1-有哪些优势？"><a href="#HTTP-2-相比-HTTP-1-1-有哪些优势？" class="headerlink" title="HTTP/2 相比 HTTP/1.1 有哪些优势？"></a><strong>HTTP/2 相比 HTTP/1.1 有哪些优势？</strong></h2><p>HTTP/2 是 2015 年发布的 HTTP 协议新版本。</p><p>HTTP/2 的主要优势是可以通过同一个连接同时进行多个请求。而在使用 HTTP/1.1 加载多个资源时，需要建立多个服务器连接。</p><p>让我们来看一个通过 HTTP/1.1 加载的网站，重点关注连接 88。当连接首次建立时，我们可以看到浏览器执行了 DNS 查询，接着通过 TCP 连接并进行 TLS 握手。</p><p>同一个连接可以再次使用，但只能在初始请求完成之后。截图显示了两个后续请求依次完成。</p><p><img src="https://images.nirvana.net.cn/static/images/images/10024246-b1eb2d5aa1e22182.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="></p><p>然而，浏览器还创建了三个额外的连接（#123、#127 和 #133）到同一服务器，以加载其他资源。由于 IP 地址已经知道，不需要再次执行 DNS 查询。<br><img src="https://images.nirvana.net.cn/static/images/images/10024246-0571dc0daadeca37.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="><br>如果仔细观察，你可能还会注意到连接 #84 在瀑布图中没有显示连接步骤。这是 Chrome 猜测在加载 HTML 文档后可能还需要更多连接，因此预先创建的连接。因此，当发出第一个请求时，Chrome 实际上创建了两个连接。</p><p>浏览器对单一来源最多只创建 6 个连接。当达到最大连接数时，其他请求会排队等待可用的连接。在瀑布图中，这显示为灰色的“等待”时间。<br><img src="https://images.nirvana.net.cn/static/images/images/10024246-3dd3b89ce06ad790.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="></p><p>排队请求意味着它们的完成时间更长，每个连接都会消耗额外带宽，比如证书交换。</p><p>相比之下，HTTP/2 网站可以通过单个连接并行加载多个资源。</p><p><img src="https://images.nirvana.net.cn/static/images/images/10024246-8e9e492753575e4e.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="></p><h2 id="HTTP-3-对服务器连接意味着什么？"><a href="#HTTP-3-对服务器连接意味着什么？" class="headerlink" title="HTTP/3 对服务器连接意味着什么？"></a><strong>HTTP/3 对服务器连接意味着什么？</strong></h2><p>HTTP/3 只需要一次往返即可建立安全可靠的连接。它不再使用 TCP 协议，而是使用 UDP 和 QUIC。</p><p>然而，并不是所有服务器都支持 HTTP/3，因此浏览器仍然需要先建立 TCP 连接并使用旧版本的 HTTP 协议。</p><p>服务器会返回一个 HTTP 头部，告诉浏览器它支持 HTTP/3，浏览器会切换到该协议。如果将来再次创建连接，HTTP/3 可以直接使用。</p><p><img src="https://images.nirvana.net.cn/static/images/images/10024246-c26e3f60b5348b99.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="></p><h2 id="什么是凭证连接？"><a href="#什么是凭证连接？" class="headerlink" title="什么是凭证连接？"></a><strong>什么是凭证连接？</strong></h2><p>我们提到过，HTTP/2 可以使用单个连接来发起多个请求，但在某些情况下，你可能仍会看到第二个连接被建立到服务器。</p><p>某些连接是“凭证连接”，这意味着在发出请求时会发送 cookie 和其他数据。大多数浏览器会为有凭证请求和无凭证请求创建不同的连接。</p><p>下图展示了一次凭证的 JavaScript 请求使用了与 <code>crossorigin=&quot;anonymous&quot;</code> 请求不同的连接。</p><p><img src="https://images.nirvana.net.cn/static/images/images/10024246-f2cc295cd76d7369.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="></p><p>鉴于凭证是作为 HTTP 连接的一部分发送的，且与连接本身无关，是否有必要使用单独的连接？不一定，Microsoft Edge 使用相同的服务器连接处理这两种类型的请求。</p><h2 id="TLS-1-2-和-TLS-1-3-的性能差异是什么？"><a href="#TLS-1-2-和-TLS-1-3-的性能差异是什么？" class="headerlink" title="TLS 1.2 和 TLS 1.3 的性能差异是什么？"></a><strong>TLS 1.2 和 TLS 1.3 的性能差异是什么？</strong></h2><p>TLS 1.3 比 TLS 1.2 更快，因为它只需要一次网络往返即可建立安全连接。而使用 TLS 1.2 的安全连接则需要两次往返，因此建立连接所需的时间更长。</p><p>在这个请求瀑布图中，可以看到 <a href="http://www.bbc.com/">www.bbc.com</a> 使用 TLS 1.3 只需要一次往返进行连接。然而，另外两个服务器使用的是 TLS 1.2，建立连接花费的时间较长。</p><p><img src="https://images.nirvana.net.cn/static/images/images/10024246-ce3ebe7e6796a0e1.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="></p><h2 id="内容分发网络（CDN）如何影响服务器连接？"><a href="#内容分发网络（CDN）如何影响服务器连接？" class="headerlink" title="内容分发网络（CDN）如何影响服务器连接？"></a><strong>内容分发网络（CDN）如何影响服务器连接？</strong></h2><p>内容分发网络（CDN）通过在全球范围内布置服务器，用户可以连接到离他们更近的服务器，从而减少网络往返时间。</p><p>通常，CDN 还会提供缓存的响应，意味着不需要向源服务器发出请求。假设使用快速的 WiFi 连接且 CDN 节点靠近用户，网络往返可能只需 10 毫秒，而整个 HTTP 请求加上建立连接可能只需 40 毫秒。</p><p>即使响应不能缓存，CDN 也能加快建立初始服务器连接的时间。然后，CDN 会将请求转发到源服务器，而源服务器可能离用户较远。</p><p>需要注意的是，在某些情况下，仅为部分资源使用 CDN 可能会降低页面速度。如果您的主网站是 example.com，而 CDN 位于 cdn.com，那么需要建立第二个连接到 CDN 服务器。而在某些情况下，直接从主域加载资源可能更快，因为可以重用现有连接。</p><h2 id="加载网站的其他页面时的服务器连接"><a href="#加载网站的其他页面时的服务器连接" class="headerlink" title="加载网站的其他页面时的服务器连接"></a><strong>加载网站的其他页面时的服务器连接</strong></h2><p>在首次打开网站后，加载同一站点的另一个页面通常会更快，因为某些资源已被缓存并可以重复使用。这也适用于在初次页面加载后仍保持打开的服务器连接。</p><p>这个瀑布图展示了第二次加载页面的情况。即使需要通过 HTTP 重新加载资源，也可以重用现有的服务器连接。</p><p><img src="https://images.nirvana.net.cn/static/images/images/10024246-f943864eabf96f2f.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="></p><h2 id="如何在-Chrome-DevTools-中查看服务器连接步骤"><a href="#如何在-Chrome-DevTools-中查看服务器连接步骤" class="headerlink" title="如何在 Chrome DevTools 中查看服务器连接步骤"></a><strong>如何在 Chrome DevTools 中查看服务器连接步骤</strong></h2><p>Chrome 开发者工具的网络标签显示了请求瀑布图，其中包括连接的往返时间（如适用）。</p><p><img src="https://images.nirvana.net.cn/static/images/images/10024246-5a0c1a9664b0cac7.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="></p><p>但是，如果你之前已经加载过页面，请求分解中可能不会显示连接步骤。</p><p><img src="https://images.nirvana.net.cn/static/images/images/10024246-b6e08430c6acda7f.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="></p><p>这是因为 Chrome 会重用现有的连接，DNS 响应也会被操作系统缓存。如果你需要这些信息，可以在重新加载页面前清除缓存。</p><h2 id="运行网站速度测试查看服务器连接"><a href="#运行网站速度测试查看服务器连接" class="headerlink" title="运行网站速度测试查看服务器连接"></a><strong>运行网站速度测试查看服务器连接</strong></h2><p>您可以使用我们免费的网页速度测试工具，查看网站加载了哪些资源，以及服务器连接如何影响性能。</p><p>在请求标签中，您可以查看网站的请求瀑布图。</p><p><img src="https://images.nirvana.net.cn/static/images/images/10024246-1d62589e1c1f29cd.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="></p><p>您还可以通过“列”下拉菜单自定义每个请求的显示信息。例如，您可以显示每个连接的标识符以及连接协议。</p><p><img src="https://images.nirvana.net.cn/static/images/images/10024246-49cb794070510673.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="></p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;&lt;img src=&quot;https://images.nirvana.net.cn/static/images/images/10024246-e7eccd45b65a18a6.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2Awdb</summary>
      
    
    
    
    <category term="前端" scheme="https://www.nirvana.net.cn/categories/%E5%89%8D%E7%AB%AF/"/>
    
    
    <category term="web" scheme="https://www.nirvana.net.cn/tags/web/"/>
    
  </entry>
  
  <entry>
    <title>为什么类型检查需要 TypeScript</title>
    <link href="https://www.nirvana.net.cn/2024/1010c75uL566ii.html"/>
    <id>https://www.nirvana.net.cn/2024/1010c75uL566ii.html</id>
    <published>2024-10-10T08:28:03.000Z</published>
    <updated>2024-10-13T08:46:57.836Z</updated>
    
    <content type="html"><![CDATA[<p><img src="https://images.nirvana.net.cn/static/images/images/10024246-3a222d7e69821ed3.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="></p><p>类型化的代码检查非常强大，但需要完整的类型检查器才能良好运行。目前，这意味着使用 TypeScript。这也是为什么我们还没有找到合适替代方案的原因。</p><h3 id="什么是类型化-Linting？"><a href="#什么是类型化-Linting？" class="headerlink" title="什么是类型化 Linting？"></a>什么是类型化 Linting？</h3><p>带有类型信息的 linting，也称为“类型化 linting”或“类型感知 linting”，是指编写使用类型信息来理解代码的 lint 规则。由 <code>typescript-eslint</code> 提供的类型化 linting 是当前常用的 JavaScript/TypeScript 中最强大的 linting 方式。使用类型信息的 lint 规则比传统的仅基于 AST 的规则更为强大。</p><p>许多流行的 lint 规则要么依赖类型化 linting，要么在没有类型化 linting 时不得不处理已知的漏洞或功能差距。ESLint 的核心规则并不理解类型信息，这使得一些 <code>typescript-eslint</code> 的“扩展”规则增加了类型信息。</p><p>因此，类型化 linting 对于 lint 工具来说非常重要。</p><p>那么，如何获取类型信息呢？</p><h3 id="TypeScript-提供类型信息"><a href="#TypeScript-提供类型信息" class="headerlink" title="TypeScript 提供类型信息"></a>TypeScript 提供类型信息</h3><p>TypeScript 是为 JavaScript 或 TypeScript 代码提供完整类型信息的工具。在当今的 Web 开发时代，它无疑是最受欢迎的 JavaScript 变体。</p><p>事实上，TypeScript 是目前唯一能够合理地获取类型信息的工具。所有试图重新创建它的公开努力要么被放弃，要么停滞不前。Flow 明确表示不会与 TypeScript 竞争公众认知度。当前最接近的公开尝试是 Ezno，但还处于非常早期的阶段。</p><p>如果你希望为 JavaScript/TypeScript 项目使用完整的类型检查器（实际上你确实需要），那么 TypeScript 是目前唯一合理的选择。</p><h3 id="替代方案与难题"><a href="#替代方案与难题" class="headerlink" title="替代方案与难题"></a>替代方案与难题</h3><p>TypeScript 是一个庞大的依赖项，类型检查是一个众所周知的耗时过程。TypeScript 以 JavaScript 速度执行（“JIT”，即时编译），因此比使用 Go 或 Rust 等本地语言编写的工具占用更多的内存和执行时间。</p><p>如果我们能找到一种方法，不必在类型化 linting 中运行完整的 JIT 速度类型检查器，那将对 lint 用户非常有利。遗憾的是，这样的壮举尚未成功实现。</p><h3 id="仅基于-AST-的类型"><a href="#仅基于-AST-的类型" class="headerlink" title="仅基于 AST 的类型"></a>仅基于 AST 的类型</h3><p>避免 TypeScript 依赖的一种方法可能是仅支持有限的类型检索：即仅查看 AST 中可见的内容。通过在文件中进行基本的 AST 检查，你可能可以在许多函数上取得一些进展，并通过基本的 TypeScript 解析器进一步构建每个文件的作用域管理器，有效地查找标识符的声明位置。</p><p>遗憾的是，仅基于 AST 的类型查找系统在遇到任何复杂的 TypeScript 类型时很快就会崩溃（例如条件类型或映射类型）。大多数较大的 TypeScript 项目在某些地方都会使用复杂类型。任何现代 ORM（如 Prisma、Supabase）或架构验证库（如 Arktype、Zod）都会使用条件类型等技术。如果无法理解这些类型，规则就无法理解引用这些类型的代码。</p><p>不一致的类型感知水平对用户来说充其量是混乱的，在实践中则几乎阻碍了 linter 的真正采用。</p><p>完整的类型系统（如 TypeScript 的）是唯一能够编写真正有效的 lint 规则的途径，这些规则能够执行任何定义跳转或基于类型的逻辑。</p><h3 id="原生速度的重新实现"><a href="#原生速度的重新实现" class="headerlink" title="原生速度的重新实现"></a>原生速度的重新实现</h3><p>如果类型检查如此重要，且原生速度的类型检查器如此有益，为什么还没有人用 Go 或 Rust 重新实现 TypeScript？</p><p>TypeScript 是一个由微软资助的经验丰富的团队以及活跃的社区开发的庞大项目。每三个月发布一个新版本，增加类型检查的漏洞修复和功能。跟上 TypeScript 的发展对于任何团队来说都是巨大的挑战。</p><p>我希望有一天会有一个工具可以与 TypeScript 竞争。但这样的工具需要多年时间才能发展。</p><h5 id="子集重实现"><a href="#子集重实现" class="headerlink" title="子集重实现"></a>子集重实现</h5><p>TypeScript 由多个功能领域组成，其中类型检查只是其中之一。类型化 linting 使用的类型检查主要涉及两个 API 领域：</p><ul><li>类型检索（“此节点的类型是什么？”）</li><li>类型关系（“此类型是否可分配给彼类型？”）</li></ul><p>大多数类型化 lint 规则实际上只使用了类型检索部分。仅重新实现 TypeScript 的这一部分可能会大大降低开发成本。</p><p>不幸的是，TypeScript 的类型关系部分最近被确认可供 linters 使用。<code>typescript-eslint</code> 将开始构建基于 <code>checker.isTypeAssignableTo</code> 的规则逻辑。</p><p>任何子集的 TypeScript 重新实现都需要包含现有类型检索和类型关系 API 的等价部分。</p><h5 id="自动将-TypeScript-移植到原生速度"><a href="#自动将-TypeScript-移植到原生速度" class="headerlink" title="自动将 TypeScript 移植到原生速度"></a>自动将 TypeScript 移植到原生速度</h5><p>与其从头重新实现 TypeScript，我们能否自动将其源代码移植到更快的语言中？</p><p>TypeScript 有数万行源代码，专为单线程 JavaScript 和动态对象设计。它的架构确实不适合在另一种范式下工作。</p><p>我认为这种方法非常有希望可以得到一个始终保持最新的 TypeScript 等价物，但难度非常高。目前还没有看到有人在这个想法上取得超出概念验证的重大进展。</p><h3 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h3><p>TypeScript 本身是目前唯一稳定、可用于生产环境的 TypeScript 代码类型化 linting 工具。它垄断了可用的类型检查 API。能够使用更快的等价工具对类型化 linting 用户来说将是非常棒的，但目前还没有任何工具可以达到生产就绪的状态。</p><p>重写 TypeScript 或其等价物存在许多障碍。而且每年 TypeScript 都变得更加复杂和功能齐全。</p><p>我对最终出现一个 TypeScript 的真正竞争者充满希望，这将为生态系统带来更健康的竞争。即使没有实现这一目标，至少也可以有一个 TypeScript 子集，加速类型化 linting 等 API 消费者的速度，或者一个自动将 TypeScript 转移到原生速度的移植工具。</p><p>但要等到能够与 TypeScript 竞争的工具稳定下来，还需要一段时间。就目前而言，我们只能继续依赖 TypeScript 进行类型化 linting。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;&lt;img src=&quot;https://images.nirvana.net.cn/static/images/images/10024246-3a222d7e69821ed3.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2Awdb</summary>
      
    
    
    
    <category term="前端" scheme="https://www.nirvana.net.cn/categories/%E5%89%8D%E7%AB%AF/"/>
    
    
    <category term="TypeScript" scheme="https://www.nirvana.net.cn/tags/TypeScript/"/>
    
    <category term="web" scheme="https://www.nirvana.net.cn/tags/web/"/>
    
  </entry>
  
  <entry>
    <title>【译】AI会取代程序员吗？</title>
    <link href="https://www.nirvana.net.cn/2024/1009uQ4i66QQCC.html"/>
    <id>https://www.nirvana.net.cn/2024/1009uQ4i66QQCC.html</id>
    <published>2024-10-09T08:28:03.000Z</published>
    <updated>2024-10-13T08:46:57.763Z</updated>
    
    <content type="html"><![CDATA[<p><img src="https://images.nirvana.net.cn/static/images/images/10024246-ea09abfa12f5ec51.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="></p><h3 id="“人工智能会取代我的工作吗？”"><a href="#“人工智能会取代我的工作吗？”" class="headerlink" title="“人工智能会取代我的工作吗？”"></a><strong>“人工智能会取代我的工作吗？”</strong></h3><p>这是随着人工智能迅速发展，软件工程师们挥之不去的一个问题。这种担忧是可以理解的，甚至是合情合理的。毕竟，如果人工智能可以编写代码、调试错误，甚至设计系统，那么我们还剩下什么呢？</p><p>但作为一名开发人员，同时也是一位致力于为开发人员构建人工智能工具的首席执行官，我可以告诉你，不仅你的工作是安全的，它还将变得更加令人兴奋、具有影响力和价值。</p><h3 id="历史的相似性：技术赋能而非取代"><a href="#历史的相似性：技术赋能而非取代" class="headerlink" title="历史的相似性：技术赋能而非取代"></a><strong>历史的相似性：技术赋能而非取代</strong></h3><p>这种技术增强而不是取代熟练工人的模式，在历史上已经多次出现。让我们来看一些具体的例子：<br><img src="https://images.nirvana.net.cn/static/images/images/10024246-92d90f533aa45f2e.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="></p><blockquote><p><strong>“科技悲观主义的简短历史”时间轴，显示1979年VisiCalc推出，1980年代是计算机辅助设计（CAD），1990年代是数码摄影</strong></p></blockquote><h5 id="会计师曾担心电子表格软件的崛起"><a href="#会计师曾担心电子表格软件的崛起" class="headerlink" title="会计师曾担心电子表格软件的崛起"></a><strong>会计师曾担心电子表格软件的崛起</strong></h5><p>当1979年VisiCalc（第一个电子表格程序）推出时，许多会计师担心他们的工作会不保。但实际上，电子表格彻底革新了这一领域：</p><ul><li>常规计算被自动化，解放了会计师的时间，使他们能够专注于分析和策略</li><li>财务建模的普及推动了更为复杂的商业规划的兴起</li><li>接受了这一技术的会计师变得更具价值，能够为客户提供更深入的见解</li></ul><p>如今，会计仍是一个蓬勃发展的行业，专业人士利用先进的软件提供高水平的财务指导。</p><p><strong>工作数据显示，在过去30年里，会计工作的数量一直在稳步增加</strong><br><img src="https://images.nirvana.net.cn/static/images/images/10024246-24e0957dd9c50239.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="></p><h5 id="建筑师曾担心计算机辅助设计（CAD）的影响"><a href="#建筑师曾担心计算机辅助设计（CAD）的影响" class="headerlink" title="建筑师曾担心计算机辅助设计（CAD）的影响"></a><strong>建筑师曾担心计算机辅助设计（CAD）的影响</strong></h5><p>1980年代CAD软件的引入改变了建筑领域：</p><ul><li>绘图桌被电脑屏幕取代</li><li>结构完整性的复杂计算实现了自动化</li><li>3D建模使得更为雄心勃勃和复杂的设计成为可能</li></ul><p>事实上，CAD并没有淘汰建筑师，反而帮助他们：</p><ul><li>更快地迭代设计</li><li>以新的方式可视化项目</li><li>承担更复杂和宏大的项目</li><li>更多地专注于创意设计，而不是技术绘图</li></ul><p><strong>工作数据显示，在过去30年里，建筑师的工作数量一直在稳步增加</strong><br><img src="https://images.nirvana.net.cn/static/images/images/10024246-559c0cdb4476693e.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="></p><h5 id="摄影师曾担心数码相机的出现"><a href="#摄影师曾担心数码相机的出现" class="headerlink" title="摄影师曾担心数码相机的出现"></a><strong>摄影师曾担心数码相机的出现</strong></h5><p>从胶片到数码的转变本可以给专业摄影师带来灭顶之灾，但它实际上改变了整个行业：</p><ul><li>后期处理变得更为便捷，使创意得到释放</li><li>拍摄更多照片的能力提高了抓拍精彩瞬间的机会</li><li>分享和发布变得更加容易，创造了新的机遇</li></ul><p>虽然一些传统岗位（如胶片冲印工）消失了，但新的岗位（如数码修图师、社交媒体摄影师）也应运而生。构图、光影以及抓拍瞬间的核心技能仍然是人类独有的。</p><h3 id="为何软件工程不同"><a href="#为何软件工程不同" class="headerlink" title="为何软件工程不同"></a><strong>为何软件工程不同</strong></h3><p>虽然技术进步确实取代了一些行业的工人，但软件工程仍然保持了强大的韧性。让我们来看几个工人被取代的案例，并将其与软件工程进行对比：</p><h5 id="制造自动化"><a href="#制造自动化" class="headerlink" title="制造自动化"></a><strong>制造自动化</strong></h5><p>机器人和先进机械极大减少了制造业对人类工人的需求：</p><ul><li>在汽车制造中，装配线工人被机器人取代</li><li>纺织生产大部分实现了自动化，导致许多工人失业</li></ul><h5 id="旅行社"><a href="#旅行社" class="headerlink" title="旅行社"></a><strong>旅行社</strong></h5><p>在线预订平台几乎消除了传统旅行社的需求：</p><ul><li>消费者可以轻松比较价格，并在线预订航班和酒店</li><li>目的地信息可以在互联网上轻松获取</li></ul><p><strong>工作数据显示，过去20年里，旅行社的工作数量一直在下降</strong><br><img src="https://images.nirvana.net.cn/static/images/images/10024246-30902aa7b0c95515.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="></p><h5 id="电话接线员"><a href="#电话接线员" class="headerlink" title="电话接线员"></a><strong>电话接线员</strong></h5><p>自动化交换系统使得电话接线员成为历史：</p><ul><li>直接拨号消除了人工干预来连接电话</li><li>自动化目录取代了人工帮助查询</li></ul><h5 id="为何软件工程不同-1"><a href="#为何软件工程不同-1" class="headerlink" title="为何软件工程不同"></a><strong>为何软件工程不同</strong></h5><p><img src="https://images.nirvana.net.cn/static/images/images/10024246-cda25a4c281856cd.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="><br>图表显示，被取代的行业通常具有有限的复杂性，工作是重复性的等，而软件工程却是无限复杂的，涉及创造性问题解决，并且在不断演变。</p><ul><li><strong>无限复杂性</strong>：与实体商品或标准化服务不同，软件可以变得极其复杂，不断带来新的挑战。</li><li><strong>创造性与问题解决</strong>：软件开发需要持续的创造性投入和复杂问题解决，不像被取代行业的重复性或程序化任务。</li><li><strong>持续演变</strong>：软件领域随着新技术、平台和用户需求快速变化，要求不断适应和学习。</li><li><strong>定制化</strong>：软件通常需要为每个客户或用户提供独特的解决方案，与其他行业的标准化趋势形成对比。</li><li><strong>决策复杂性</strong>：软件架构涉及几乎无限的决策树，远超被取代角色中有限的选择集。</li></ul><p>这些因素使得软件工程在自动化和人工智能面前更加具有韧性，它被定位为一个通过技术进步增强而不是被取代的领域。</p><h3 id="AI作为加速器"><a href="#AI作为加速器" class="headerlink" title="AI作为加速器"></a><strong>AI作为加速器</strong></h3><p>人工智能工具，如GitHub Copilot和ChatGPT，不仅没有让开发人员多余，反而成为了倍增器——大幅提升了单个工程师的工作效率。通过处理常规的编码任务、捕捉错误和提供建议，AI使开发人员能够腾出时间应对更复杂的挑战，并专注于架构决策。</p><p>这不是取代人类智慧，而是增强它。想象一下，你有一个不知疲倦的机器人助手团队，实施你的想法并处理繁琐的工作，而你则负责整体的规划与协调。这就是人工智能对软件工程的承诺。</p><p>例如，AI可以：</p><ul><li>自动化冗余任务，比如将Figma设计转化为代码</li><li>快速原型化创意，从而加快迭代</li><li>协助文档编写，简化知识共享</li><li>通过让非开发团队成员处理开发人员常感繁琐或耗时的任务，促进更好的协作</li></ul><p>这些能力并不会取代需要熟练开发人员的需求——它放大了开发人员在给定时间内可以实现的成果。<br><img src="https://images.nirvana.net.cn/static/images/images/10024246-76a45970a7a12125.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="></p><h5 id="从逐行编码到更高层次的思考"><a href="#从逐行编码到更高层次的思考" class="headerlink" title="从逐行编码到更高层次的思考"></a><strong>从逐行编码到更高层次的思考</strong></h5><p>随着AI接管更多的逐行编码，开发人员将自然转向更高层次的思考和问题解决。这不是角色的退化，而是它向着更具影响力和更具智力挑战的方向演变。</p><p>软件工程是一个无底的领域。当我们解决一层问题时，我们会在更高抽象层面发现新的挑战。AI将加速我们在这个阶梯上的进步，让开发人员能够处理越来越复杂且有意义的项目。</p><p>每迈向这个阶梯的一个新层次，都会让开发人员有机会构建更复杂、强大和用户友好的应用。<br><img src="https://images.nirvana.net.cn/static/images/images/10024246-8faed9941514677a.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="><br>图表显示“战略和创新”位于价值链的顶端，而“代码语法和格式化”位于底端。</p><p>你不再手写汇编代码，对吧？你认为我们能够用JS编写高级代码，是否让公司想在代码上做更多事情而不是更少？</p><p>你不会手动格式化所有代码，对吧？你使用格式化工具如prettier，还是手动打磨每一行代码？</p><p>AI将继续这一趋势，推动开发人员专注于更高层次的问题，如系统架构、用户体验设计以及解决复杂的业务问题。<br>翻译：</p><h3 id="拥抱AI"><a href="#拥抱AI" class="headerlink" title="拥抱AI"></a><strong>拥抱AI</strong></h3><p>与其害怕AI，我鼓励开发者将它视为自己武器库中的一款强大工具。以下是一些建议：</p><ul><li>尝试使用AI编码助手，如GitHub Copilot。实时体验它们的优势和局限。</li><li>使用ChatGPT或Claude来探索想法、调试问题或学习新概念。把它们当作随时可以咨询的导师。</li><li>专注于提升自己的高层次问题解决和架构设计能力。这些领域仍然是人类创造力的天下。</li><li>通过与大型语言模型（LLMs）合作构建副业项目，进一步探索它们在自动化工作中枯燥部分的潜力，从而让你专注于最喜欢的部分。</li></ul><h5 id="享受过程"><a href="#享受过程" class="headerlink" title="享受过程"></a><strong>享受过程</strong></h5><p>归根结底，如果你在过程中感到乐趣，你将成为更好的开发者。我们都知道，大多数开发者喜欢花一周时间自动化一项手动完成只需一小时的任务。<br><img src="https://images.nirvana.net.cn/static/images/images/10024246-a0bb1b3166a0106f.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="><br>有了LLMs，我们现在可以利用一种新的基本工具来增强我们自动化更多任务的能力。</p><p>所以，使用它！找到那些你不喜欢的工作，尝试使用LLM API来自动化这些任务。根据我的经验，这很有趣。如果你想更深入，可以尝试自己训练模型，玩得更高端一些。</p><h3 id="CEO的视角"><a href="#CEO的视角" class="headerlink" title="CEO的视角"></a><strong>CEO的视角</strong></h3><p>作为一家科技公司的CEO，我并不将AI视为减少开发团队的工具。恰恰相反，我认为它是一种能极大提升每个开发者对公司贡献的方式。有了AI处理常规任务，我们的人类工程师可以专注于创新、复杂问题解决和推动可能性的极限。</p><p>这种提高的生产力和专注于高价值工作的能力使得每个开发者变得比以往任何时候都更有价值。我们并不希望减少开发者，反而希望增加更多的开发人员，因为AI能够放大每个人的能力，尤其是在我们需要始终保持领先竞争对手的情况下。<br><img src="https://images.nirvana.net.cn/static/images/images/10024246-f2b51299d1b2dfa6.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="><br><strong>CEO示意图：CEO指向肌肉强壮的小柴犬梗图，上面写着“我使用AI”，并说“我想要更多像这样的开发者”</strong></p><p>考虑以下几点：</p><ul><li>更快速的原型开发带来更具创新性的产品</li><li>开发者可以腾出更多时间去理解和解决核心业务问题</li><li>更有能力承担更复杂、更雄心勃勃的项目</li><li>一支由AI增强的开发者团队是击败竞争对手的竞争优势</li></ul><p>在这种环境下，限制因素不是开发者的成本，而是如何充分利用他们增强的能力。</p><h3 id="那么，AI会取代程序员吗？"><a href="#那么，AI会取代程序员吗？" class="headerlink" title="那么，AI会取代程序员吗？"></a><strong>那么，AI会取代程序员吗？</strong></h3><p>不会。<br><img src="https://images.nirvana.net.cn/static/images/images/10024246-3d77c044e2b058ee.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2AwdbuZ5LxbUySwqJ1HBKf4H4rWRweoArKyPtgRGs68tijz3gt5FE7Z511u6uMb2HP6HmqhN&Signature=f2DPWFzGx+U+ELJYUqFfcGJRLgQ="><br><strong>图表显示，编程工作在过去十年中每年都在增长</strong><br>软件开发中AI的崛起不是值得恐惧的事情，而是值得热情拥抱的事。通过自动化常规任务并增强人类的能力，AI工具正带来软件工程的又一个黄金时代。</p><p>那些适应并利用这些工具的开发者将变得更加高效、更具创造力，并且比以往更加有价值。软件工程的未来不是与AI竞争，而是通过协调它来构建惊人的事物。</p><p>所以，不要只是旁观AI革命——引领它。拥抱这些新工具，专注于提升你独有的人类技能，如创造力和复杂问题解决，准备构建下一代改变世界的软件。</p><p>毕竟，编码的目的不就是突破可能性的边界吗？AI只是我们武器库中最新且最强大的工具，用来实现这一目标。那些茁壮成长的开发者，将是那些将AI视为构建未来的强大盟友的人。</p><blockquote><p>原文：<a href="https://www.builder.io/blog/ai-jobs-truth">https://www.builder.io/blog/ai-jobs-truth</a></p></blockquote>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;&lt;img src=&quot;https://images.nirvana.net.cn/static/images/images/10024246-ea09abfa12f5ec51.png?OSSAccessKeyId=TMP.3KeXZGnENvALyisAJT1gqw2Awdb</summary>
      
    
    
    
    <category term="前端" scheme="https://www.nirvana.net.cn/categories/%E5%89%8D%E7%AB%AF/"/>
    
    
    <category term="AI" scheme="https://www.nirvana.net.cn/tags/AI/"/>
    
  </entry>
  
</feed>
