<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>雨浣潇湘</title>
  
  <subtitle>前端 / 旅行 / 摄影 / 骑行 / 羽毛球</subtitle>
  <link href="/atom.xml" rel="self"/>
  
  <link href="https://www.thjiang.com/"/>
  <updated>2026-03-22T16:02:56.480Z</updated>
  <id>https://www.thjiang.com/</id>
  
  <author>
    <name>雨浣潇湘</name>
    
  </author>
  
  <generator uri="http://hexo.io/">Hexo</generator>
  
  <entry>
    <title>从 HSL 到 OKLCH — 让深色模式的颜色更自然</title>
    <link href="https://www.thjiang.com/2026/01/07/%E4%BB%8E-HSL-%E5%88%B0-OKLCH-%E2%80%94-%E8%AE%A9%E6%B7%B1%E8%89%B2%E6%A8%A1%E5%BC%8F%E7%9A%84%E9%A2%9C%E8%89%B2%E6%9B%B4%E8%87%AA%E7%84%B6/"/>
    <id>https://www.thjiang.com/2026/01/07/从-HSL-到-OKLCH-—-让深色模式的颜色更自然/</id>
    <published>2026-01-07T13:55:43.000Z</published>
    <updated>2026-03-22T16:02:56.480Z</updated>
    
    <content type="html"><![CDATA[<p>在 <a href="https://www.thjiang.com/2025/12/28/%E6%B5%85%E8%B0%88%E7%AC%AC%E4%B8%89%E6%96%B9-HTML-%E7%9A%84%E6%9A%97%E9%BB%91%E6%A8%A1%E5%BC%8F%E9%80%82%E9%85%8D/">上一篇文章</a> 中，我们聊到了第三方 HTML 暗黑模式的适配方案，并实现了一套基于 HSL 的颜色转换。但是随着研究的深入，我发现 HSL 方案还是有一些问题。</p><p>回顾一下我们之前的需求：第三方 HTML 的内容中的颜色多种多样，怎么把它们转换成深色模式下看起来都很舒服的颜色并且还尽量保持原来的语义？</p><h2 id="HSL-的局限性"><a href="#HSL-的局限性" class="headerlink" title="HSL 的局限性"></a>HSL 的局限性</h2><h3 id="为什么深色模式适配不能直接取反？"><a href="#为什么深色模式适配不能直接取反？" class="headerlink" title="为什么深色模式适配不能直接取反？"></a>为什么深色模式适配不能直接取反？</h3><p>最直觉的想法：RGB 直接取反不就可以了吗</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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 看起来很合理？</span></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">invertRGB</span>(<span class="params">r: <span class="built_in">number</span>, g: <span class="built_in">number</span>, b: <span class="built_in">number</span></span>) </span>&#123;</span><br><span class="line">    <span class="keyword">return</span> [<span class="number">255</span> - r, <span class="number">255</span> - g, <span class="number">255</span> - b];</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>试试黑色：</p><figure class="highlight plain"><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">#000000 → 取反 → #FFFFFF</span><br><span class="line">黑色变白色，完美！✓</span><br></pre></td></tr></table></figure><p>再试试红色：</p><figure class="highlight plain"><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">#FF0000 → 取反 → #00FFFF</span><br><span class="line">红色变成了青色...</span><br></pre></td></tr></table></figure><p>问题来了：<strong>本来一般用于表示警告的红色变成了青色</strong>，语义丢失了。</p><p>直接取反有两个致命问题：</p><ol><li>颜色的”含义”丢了（红色不再是红色）</li><li>没考虑亮度（有些颜色在深色背景上根本看不清）</li></ol><h3 id="中间层的思路"><a href="#中间层的思路" class="headerlink" title="中间层的思路"></a>中间层的思路</h3><p>既然 RGB 三个通道混在一起不好控制，那就拆开。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><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">RGB：r, g, b → 三个值混在一起，牵一发动全身</span><br><span class="line">         ↓</span><br><span class="line">中间层：色相, 饱和度, 亮度 → 各管各的，互不影响</span><br><span class="line">         ↓</span><br><span class="line">深色模式的颜色</span><br></pre></td></tr></table></figure><p>这就是 HSL 的思路：把颜色拆成色相（Hue）、饱和度（Saturation）、亮度（Lightness）三个独立的维度。</p><p>想调亮度只改 L 就行，色相和饱和度不变。红色还是红色，只是亮了或暗了。</p><p>听起来不错，但 HSL 有个容易被忽略的问题。</p><h3 id="为什么-HSL-的亮度看起来不真实"><a href="#为什么-HSL-的亮度看起来不真实" class="headerlink" title="为什么 HSL 的亮度看起来不真实"></a>为什么 HSL 的亮度看起来不真实</h3><p>HSL 的 L 是”数学上的亮度”，不是”人眼看起来的亮度”。</p><p>什么意思呢？看这个例子：</p><div style="display:flex; gap:20px; margin:20px 0; align-items:center;"><br><br>  <!-- 蓝色 --><br>  <div style="display:flex; flex-direction:column; align-items:center; gap:8px;"><br>    <div style="width:140px; height:80px; background:hsl(240, 100%, 50%); border-radius:8px; display:flex; align-items:center; justify-content:center; color:#fff; font-size:13px;">示例文本<br>    </div><br>  </div>  <!-- 黄色 -->  <div style="display:flex; flex-direction:column; align-items:center; gap:8px;"><br>    <div style="width:140px; height:80px; background:hsl(60, 100%, 50%); border-radius:8px; display:flex; align-items:center; justify-content:center; color:#fff; font-size:13px;">示例文本<br>    </div><br>  </div><p></div></p><p>按 HSL 的判断，它们的亮度一样，都是 50%。但眼睛会告诉我们：黄色特别亮，而蓝色相对暗很多。</p><p>这就是 HSL 的问题：<strong>亮度不均匀</strong>。HSL 的 L 是纯数学计算值，不是人眼感知到的亮度。同样的 L 值，不同色相看起来亮度差很多，而且同一个颜色，增减同样的 L 值，颜色变化也是不均匀的。</p><h2 id="选择更合适的色彩空间"><a href="#选择更合适的色彩空间" class="headerlink" title="选择更合适的色彩空间"></a>选择更合适的色彩空间</h2><p>既然 HSL 也不完美，那有没有更好的解决方案呢？随着调研的深入，我发现原来现在已经有这么多 Web 上可用的色彩空间了：</p><table><thead><tr><th>色彩空间</th><th>特点</th><th>问题</th></tr></thead><tbody><tr><td>RGB/HEX</td><td>与设备硬件直接对应</td><td>通道耦合，无法独立控制亮度</td></tr><tr><td>HSL/HSV</td><td>表达直观，易于调整</td><td>不具备感知均匀性</td></tr><tr><td>CIE Lab/LCH</td><td>工业标准，印刷常用</td><td>实现复杂，存在色域裁剪问题</td></tr><tr><td>CAM16/HCT</td><td>感知模型更完整 Material Design 3 标准</td><td>模型复杂，缺乏原生 CSS 支持</td></tr><tr><td><strong>Oklab/OKLCH</strong></td><td>近似感知均匀，结构简单</td><td>✅ 简单、准确、CSS 原生支持</td></tr></tbody></table><p>从工程角度看，一个可行的方案至少需要满足：</p><blockquote><p>亮度维度与人眼感知尽可能一致<br>色相和饱和度调整相对独立<br>能在 Web 环境中直接使用或低成本实现</p></blockquote><p>在这些约束下，Oklab color space 提供了一个更平衡的选择。它由 <a href="https://bottosson.github.io/" target="_blank" rel="noopener">Björn Ottosson</a> 在 2020 年提出，目标是构建一个在计算成本和感知一致性之间折中的色彩空间。OKLCH 是它的极坐标形式，L（亮度）、C（彩度）、H（色相），和 HSL 的概念类似，但 L 是真正的”感知亮度”。</p><p>在 OKLCH 里：</p><figure class="highlight plain"><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">黄色 oklch(0.97, 0.21, 110) ← L ≈ 0.97，确实很亮</span><br><span class="line">蓝色 oklch(0.45, 0.31, 264) ← L ≈ 0.45，确实暗</span><br></pre></td></tr></table></figure><p>目前，CSS Color Level 4 已经把 <code>oklch()</code> 写进了标准，Chrome 111+、Firefox 113+、Safari 15.4+ 也都原生支持了。<br>综上考虑，OKLCH 是目前更合适的方案。</p><h2 id="实现步骤"><a href="#实现步骤" class="headerlink" title="实现步骤"></a>实现步骤</h2><h3 id="一、色彩空间转换"><a href="#一、色彩空间转换" class="headerlink" title="一、色彩空间转换"></a>一、色彩空间转换</h3><p>要把 HEX 颜色转成 OKLCH，需要经过这样的一条链路：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sRGB → Linear RGB → LMS → Oklab → OKLCH</span><br></pre></td></tr></table></figure><p><strong>sRGB → Linear RGB</strong>：显示器的颜色值不是线性的，有个 Gamma 曲线。#808080 看起来是 50% 灰，但实际光强度只有 22% 左右。颜色运算要在真实光强度上做，所以先去掉 Gamma。</p><p><strong>Linear RGB → LMS</strong>：LMS 对应人眼的三种视锥细胞（长波、中波、短波），是 Oklab “感知均匀” 的基础。</p><p><strong>LMS → Oklab → OKLCH</strong>：Oklab 是笛卡尔坐标 (L, a, b)，OKLCH 是极坐标 (L, C, H)，后者更直观。</p><p>先做基础转换：</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> SRGB_THRESHOLD = <span class="number">0.04045</span>;</span><br><span class="line"><span class="keyword">const</span> SRGB_LINEAR_SLOPE = <span class="number">12.92</span>;</span><br><span class="line"><span class="keyword">const</span> SRGB_GAMMA = <span class="number">2.4</span>;</span><br><span class="line"><span class="keyword">const</span> SRGB_OFFSET = <span class="number">0.055</span>;</span><br><span class="line"><span class="keyword">const</span> SRGB_SCALE = <span class="number">1.055</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// sRGB → Linear RGB</span></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">srgbToLinear</span>(<span class="params">value: <span class="built_in">number</span></span>): <span class="title">number</span> </span>&#123;</span><br><span class="line">    <span class="keyword">const</span> normalized = value / <span class="number">255</span>;</span><br><span class="line">    <span class="keyword">if</span> (normalized &lt;= SRGB_THRESHOLD) &#123;</span><br><span class="line">        <span class="keyword">return</span> normalized / SRGB_LINEAR_SLOPE;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> <span class="built_in">Math</span>.pow((normalized + SRGB_OFFSET) / SRGB_SCALE, SRGB_GAMMA);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// Linear RGB → sRGB</span></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">linearToSrgb</span>(<span class="params">value: <span class="built_in">number</span></span>): <span class="title">number</span> </span>&#123;</span><br><span class="line">    <span class="keyword">const</span> clamped = <span class="built_in">Math</span>.max(<span class="number">0</span>, <span class="built_in">Math</span>.min(<span class="number">1</span>, value));</span><br><span class="line">    <span class="keyword">if</span> (clamped &lt;= SRGB_THRESHOLD / SRGB_LINEAR_SLOPE) &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="built_in">Math</span>.round(clamped * SRGB_LINEAR_SLOPE * <span class="number">255</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> <span class="built_in">Math</span>.round(</span><br><span class="line">        (SRGB_SCALE * <span class="built_in">Math</span>.pow(clamped, <span class="number">1</span> / SRGB_GAMMA) - SRGB_OFFSET) * <span class="number">255</span>,</span><br><span class="line">    );</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这里有个细节：sRGB 线性化的阈值。WCAG 2.x 文档里写的是 0.03928，但这其实是个历史遗留的错误值。正确的值是 0.04045，来自 IEC 61966-2-1:1999 标准。可以参考 W3C 的 <a href="https://www.w3.org/WAI/GL/wiki/Relative_luminance" target="_blank" rel="noopener">这篇文章</a> 了解细节。</p><p>接下来是 Oklab 转换，这部分涉及到一些矩阵运算：</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">interface</span> LinearRGB &#123;</span><br><span class="line">    r: <span class="built_in">number</span>;</span><br><span class="line">    g: <span class="built_in">number</span>;</span><br><span class="line">    b: <span class="built_in">number</span>;</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">interface</span> Oklab &#123;</span><br><span class="line">    L: <span class="built_in">number</span>;</span><br><span class="line">    a: <span class="built_in">number</span>;</span><br><span class="line">    b: <span class="built_in">number</span>;</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">interface</span> Oklch &#123;</span><br><span class="line">    L: <span class="built_in">number</span>;</span><br><span class="line">    C: <span class="built_in">number</span>;</span><br><span class="line">    H: <span class="built_in">number</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// Linear RGB → Oklab</span></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">linearRgbToOklab</span>(<span class="params">rgb: LinearRGB</span>): <span class="title">Oklab</span> </span>&#123;</span><br><span class="line">    <span class="keyword">const</span> &#123; r, g, b &#125; = rgb;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Linear RGB → LMS</span></span><br><span class="line">    <span class="keyword">const</span> l = <span class="number">0.4122214708</span> * r + <span class="number">0.5363325363</span> * g + <span class="number">0.0514459929</span> * b;</span><br><span class="line">    <span class="keyword">const</span> m = <span class="number">0.2119034982</span> * r + <span class="number">0.6806995451</span> * g + <span class="number">0.1073969566</span> * b;</span><br><span class="line">    <span class="keyword">const</span> s = <span class="number">0.0883024619</span> * r + <span class="number">0.2817188376</span> * g + <span class="number">0.6299787005</span> * b;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 立方根变换（关键步骤，实现感知均匀化）</span></span><br><span class="line">    <span class="keyword">const</span> l_ = <span class="built_in">Math</span>.cbrt(l);</span><br><span class="line">    <span class="keyword">const</span> m_ = <span class="built_in">Math</span>.cbrt(m);</span><br><span class="line">    <span class="keyword">const</span> s_ = <span class="built_in">Math</span>.cbrt(s);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// LMS → Oklab</span></span><br><span class="line">    <span class="keyword">return</span> &#123;</span><br><span class="line">        L: <span class="number">0.2104542553</span> * l_ + <span class="number">0.793617785</span> * m_ - <span class="number">0.0040720468</span> * s_,</span><br><span class="line">        a: <span class="number">1.9779984951</span> * l_ - <span class="number">2.428592205</span> * m_ + <span class="number">0.4505937099</span> * s_,</span><br><span class="line">        b: <span class="number">0.0259040371</span> * l_ + <span class="number">0.7827717662</span> * m_ - <span class="number">0.808675766</span> * s_,</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">// Oklab → Linear RGB</span></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">oklabToLinearRgb</span>(<span class="params">oklab: Oklab</span>): <span class="title">LinearRGB</span> </span>&#123;</span><br><span class="line">    <span class="keyword">const</span> &#123; L, a, b &#125; = oklab;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">const</span> l_ = L + <span class="number">0.3963377774</span> * a + <span class="number">0.2158037573</span> * b;</span><br><span class="line">    <span class="keyword">const</span> m_ = L - <span class="number">0.1055613458</span> * a - <span class="number">0.0638541728</span> * b;</span><br><span class="line">    <span class="keyword">const</span> s_ = L - <span class="number">0.0894841775</span> * a - <span class="number">1.291485548</span> * b;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">const</span> l = l_ * l_ * l_;</span><br><span class="line">    <span class="keyword">const</span> m = m_ * m_ * m_;</span><br><span class="line">    <span class="keyword">const</span> s = s_ * s_ * s_;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> &#123;</span><br><span class="line">        r: +<span class="number">4.0767416621</span> * l - <span class="number">3.3077115913</span> * m + <span class="number">0.2309699292</span> * s,</span><br><span class="line">        g: <span class="number">-1.2684380046</span> * l + <span class="number">2.6097574011</span> * m - <span class="number">0.3413193965</span> * s,</span><br><span class="line">        b: <span class="number">-0.0041960863</span> * l - <span class="number">0.7034186147</span> * m + <span class="number">1.707614701</span> * s,</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">// Oklab ↔ OKLCH（笛卡尔坐标 ↔ 极坐标）</span></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">oklabToOklch</span>(<span class="params">oklab: Oklab</span>): <span class="title">Oklch</span> </span>&#123;</span><br><span class="line">    <span class="keyword">const</span> &#123; L, a, b &#125; = oklab;</span><br><span class="line">    <span class="keyword">const</span> C = <span class="built_in">Math</span>.sqrt(a * a + b * b);</span><br><span class="line">    <span class="keyword">let</span> H = (<span class="built_in">Math</span>.atan2(b, a) * <span class="number">180</span>) / <span class="built_in">Math</span>.PI;</span><br><span class="line">    <span class="keyword">if</span> (H &lt; <span class="number">0</span>) H += <span class="number">360</span>;</span><br><span class="line">    <span class="keyword">return</span> &#123; L, C, H &#125;;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">oklchToOklab</span>(<span class="params">oklch: Oklch</span>): <span class="title">Oklab</span> </span>&#123;</span><br><span class="line">    <span class="keyword">const</span> &#123; L, C, H &#125; = oklch;</span><br><span class="line">    <span class="keyword">const</span> hRad = (H * <span class="built_in">Math</span>.PI) / <span class="number">180</span>;</span><br><span class="line">    <span class="keyword">return</span> &#123; L, a: C * <span class="built_in">Math</span>.cos(hRad), b: C * <span class="built_in">Math</span>.sin(hRad) &#125;;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这些矩阵系数来自 Björn Ottosson 的 <a href="https://bottosson.github.io/posts/oklab/" target="_blank" rel="noopener">这篇论文</a>。</p><p>有了这些基础函数，就可以组装出 HEX ↔ OKLCH 的转换了：</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="function"><span class="keyword">function</span> <span class="title">hexToOklch</span>(<span class="params">hex: <span class="built_in">string</span></span>): <span class="title">Oklch</span> </span>&#123;</span><br><span class="line">    <span class="keyword">const</span> rgb = parseHex(hex);</span><br><span class="line">    <span class="keyword">const</span> linear: LinearRGB = &#123;</span><br><span class="line">        r: srgbToLinear(rgb.r),</span><br><span class="line">        g: srgbToLinear(rgb.g),</span><br><span class="line">        b: srgbToLinear(rgb.b),</span><br><span class="line">    &#125;;</span><br><span class="line">    <span class="keyword">return</span> oklabToOklch(linearRgbToOklab(linear));</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="function"><span class="keyword">function</span> <span class="title">oklchToHex</span>(<span class="params">oklch: Oklch</span>): <span class="title">string</span> </span>&#123;</span><br><span class="line">    <span class="keyword">const</span> oklab = oklchToOklab(oklch);</span><br><span class="line">    <span class="keyword">const</span> linear = oklabToLinearRgb(oklab);</span><br><span class="line">    <span class="keyword">return</span> rgbToHex(&#123;</span><br><span class="line">        r: linearToSrgb(linear.r),</span><br><span class="line">        g: linearToSrgb(linear.g),</span><br><span class="line">        b: linearToSrgb(linear.b),</span><br><span class="line">    &#125;);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="二、亮度映射"><a href="#二、亮度映射" class="headerlink" title="二、亮度映射"></a>二、亮度映射</h3><p>转换解决了，下一步是亮度映射。</p><p>核心思路很简单：只有浅色元素需要变深，深色元素保持原样。深色代码块、深色卡片这类元素，本身就是深色设计，在 Dark Mode 下保持原样即可。唯一需要处理的边界情况是：如果深色元素的亮度和页面背景（L ≈ 0.08）太接近（差值 &lt; 0.10），就略微提亮，确保不会和背景融合。</p><p>亮度转换最简单的做法：</p><figure class="highlight typescript"><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">// 深色模式：亮度不够就拉到 0.5</span></span><br><span class="line">l = <span class="built_in">Math</span>.max(l, <span class="number">0.5</span>);</span><br></pre></td></tr></table></figure><p>但是这样粗暴处理可能会导致一些颜色产生突变，视觉上会有明显的断层。</p><p>同时还有另一个问题：深色范围 [0.20, 0.40] 比浅色范围 [0.5, 1.0] 的可用亮度要少很多。如果用一条线性函数做映射，高明度浅色（L &gt; 0.90，比如白色、浅灰）会被压到 0.20–0.25 的狭窄区间，和页面背景差值不够。</p><p>但换个角度想：L = 0.90 和 L = 0.98 在 Light Mode 下的视觉差异本来就不大 —— 它们都是”接近白色”。真正需要保持层次感的是中等浅色区域，比如 <code>#c8e6c9</code>（浅绿，L ≈ 0.80）和 <code>#ef9a9a</code>（浅红，L ≈ 0.72），这些颜色在 UI 里承载着明确的语义。</p><p>这背后有一个 19 世纪的心理物理学定律在起作用：<strong>Weber-Fechner 定律</strong>。人眼对亮度变化的感知是对数关系 —— 在暗处，你对微小的亮度差异非常敏感；在亮处，同样的差异你几乎感觉不到。就像在漆黑的房间里点亮一支蜡烛，感觉天翻地覆；但在正午的阳光下点同一支蜡烛，几乎注意不到。</p><p>基于这个原理，我们把映射分成两段：</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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 三次贝塞尔曲线（简化版，只用 y 坐标控制点）</span></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">cubicBezier</span>(<span class="params">t: <span class="built_in">number</span>, p1: <span class="built_in">number</span>, p2: <span class="built_in">number</span></span>): <span class="title">number</span> </span>&#123;</span><br><span class="line">    <span class="keyword">const</span> t2 = t * t;</span><br><span class="line">    <span class="keyword">const</span> t3 = t2 * t;</span><br><span class="line">    <span class="keyword">const</span> mt = <span class="number">1</span> - t;</span><br><span class="line">    <span class="keyword">const</span> mt2 = mt * mt;</span><br><span class="line">    <span class="keyword">const</span> mt3 = mt2 * mt;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> mt3 * <span class="number">0</span> + <span class="number">3</span> * mt2 * t * p1 + <span class="number">3</span> * mt * t2 * p2 + t3 * <span class="number">1</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">mapLightness</span>(<span class="params">L: <span class="built_in">number</span></span>): <span class="title">number</span> </span>&#123;</span><br><span class="line">    <span class="keyword">if</span> (L &lt;= <span class="number">0.85</span>) &#123;</span><br><span class="line">        <span class="comment">// 中等浅色 [0.5, 0.85] → [0.28, 0.40]，保留层次</span></span><br><span class="line">        <span class="keyword">const</span> t = (L - <span class="number">0.5</span>) / <span class="number">0.35</span>;</span><br><span class="line">        <span class="keyword">return</span> cubicBezier(t, <span class="number">0.4</span>, <span class="number">0.38</span>, <span class="number">0.32</span>, <span class="number">0.28</span>);</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        <span class="comment">// 高明度 [0.85, 1.0] → [0.20, 0.28]，压缩但可见</span></span><br><span class="line">        <span class="keyword">const</span> t = (L - <span class="number">0.85</span>) / <span class="number">0.15</span>;</span><br><span class="line">        <span class="keyword">return</span> cubicBezier(t, <span class="number">0.28</span>, <span class="number">0.26</span>, <span class="number">0.22</span>, <span class="number">0.2</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><table><thead><tr><th>区间</th><th>含义</th><th>映射到</th><th>设计考量</th></tr></thead><tbody><tr><td>[0.50, 0.85]</td><td>中等浅色（语义色、标签等）</td><td>[0.28, 0.40]</td><td>分配 0.12 空间，保留层次</td></tr><tr><td>[0.85, 1.00]</td><td>高明度（白色、浅灰等结构性背景）</td><td>[0.20, 0.28]</td><td>分配 0.08 空间，确保可见即可</td></tr></tbody></table><p>贝塞尔曲线的 S 形特性天然符合 Weber-Fechner 定律 —— 在低亮度端（映射目标区域）保留更多层次，在高亮度端适度压缩。控制点的选择原则是：段 1 的起点 0.40 要和页面背景（L=0.08）有足够对比，终点 0.28 要和段 2 平滑衔接；段 2 的终点 0.20 是最暗的映射目标，和页面背景差值 0.12，超过最小可辨识阈值 0.10。</p><h3 id="三、语义色检测与色度补偿"><a href="#三、语义色检测与色度补偿" class="headerlink" title="三、语义色检测与色度补偿"></a>三、语义色检测与色度补偿</h3><p>分段映射解决了亮度问题，但还有一个容易忽略的维度：<strong>色度（Chroma）</strong>。</p><p><code>#ffebee</code> 是 Material Design 的浅粉色，通常用于错误提示背景。它在 OKLCH 里的值是 L=0.95, C=0.02, H≈20。注意色度只有 0.02 —— 这是一个几乎没有颜色感的“带一点粉的白色”。</p><p>降低明度后，这么低的色度在深色区域里根本看不出是红色，只会呈现为灰黑色，会导致语义丢失。</p><p>解决方案是：<strong>降低明度时，同时提升色度以保持可辨识性</strong>。但不能对所有颜色都这么做 —— 比如纯灰色提升色度会变成彩色。只对语义色（红、绿、黄、蓝）做增强：</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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 识别语义色的色相范围</span></span><br><span class="line"><span class="keyword">const</span> SEMANTIC_RANGES = &#123;</span><br><span class="line">    error: &#123; hMin: <span class="number">0</span>, hMax: <span class="number">40</span> &#125;, <span class="comment">// 红色（含 340°-360°）</span></span><br><span class="line">    success: &#123; hMin: <span class="number">130</span>, hMax: <span class="number">170</span> &#125;, <span class="comment">// 绿色</span></span><br><span class="line">    warning: &#123; hMin: <span class="number">50</span>, hMax: <span class="number">90</span> &#125;, <span class="comment">// 黄色/橙色</span></span><br><span class="line">    info: &#123; hMin: <span class="number">230</span>, hMax: <span class="number">270</span> &#125;, <span class="comment">// 蓝色</span></span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 色度增强：明度降低越多，色度补偿越大</span></span><br><span class="line"><span class="keyword">const</span> boost = <span class="built_in">Math</span>.sqrt(originalL / newL);</span><br><span class="line"><span class="keyword">const</span> newC = <span class="built_in">Math</span>.min(C * boost, <span class="number">0.18</span>);</span><br></pre></td></tr></table></figure><p>用平方根是因为明度比例可能很大（比如 0.95/0.22 ≈ 4.3），直接乘会导致色度过高。平方根让增长更温和（√4.3 ≈ 2.1），同时设置了 0.18 的上限防止过饱和。</p><!-- Hexo 的 markdown 不支持 oklch --><!-- <div style="margin:24px 0;">  <div style="display:flex; gap:20px;">    <div style="flex:1;">      <div style="font-size:13px; margin-bottom:8px; opacity:.7;">        未做色度补偿      </div>      <div style="height:100px; background:oklch(0.22 0.02 20); border-radius:10px; display:flex; align-items:center; justify-content:center; color:#fff; font-size:13px;">        接近灰黑      </div>    </div>    <div style="flex:1;">      <div style="font-size:13px; margin-bottom:8px; opacity:.7;">        色度补偿后      </div>      <div style="height:100px; background:oklch(0.22 0.14 20); border-radius:10px; display:flex; align-items:center; justify-content:center; color:#fff; font-size:13px;">        深红（语义保留）      </div>    </div>  </div>  <div style="font-size:12px; margin-top:10px; opacity:.6;">    示例：#ffebee → 明度降低至 L≈0.22  </div></div> --><!-- 图片: #ffebee 的两种转换对比。左：不做色度补偿 → 灰黑色。右：色度补偿后 → 深红色 --><h3 id="四、Helmholtz-Kohlrausch-效应补偿"><a href="#四、Helmholtz-Kohlrausch-效应补偿" class="headerlink" title="四、Helmholtz-Kohlrausch 效应补偿"></a>四、Helmholtz-Kohlrausch 效应补偿</h3><p>亮度和色度都处理好了，在深色背景上测试时又发现一个问题：某些高饱和的文字颜色看起来特别刺眼，尤其是红色和青色。</p><p>这和一个叫 <strong>Helmholtz-Kohlrausch（H-K）效应</strong> 的视觉现象有关：高饱和度的颜色看起来比相同亮度的低饱和色更亮，即使物理亮度相同。你可能有过类似体验——红色的霓虹灯看起来总比白色的”更亮”，即使它们的实际功率相同。</p><p>这个效应的强度因色相而异。红色和青色最明显，黄色和蓝色相对弱一些。在深色背景上，高亮度 + 高饱和度的文字会产生明显的视觉不适。</p><p>补偿策略：对高亮度（L &gt; 0.6）且高饱和度（C &gt; 0.08）的文字，根据色相降低饱和度。<br>公式：C’ = C × (1 - k × √((L - 0.6) / 0.4))，其中 k 是补偿系数，红色/青色系用更大的 k。</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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">applyHKCompensation</span>(<span class="params">C, L, H</span>) </span>&#123;</span><br><span class="line">    <span class="comment">// 只对高亮度高饱和度的颜色进行补偿</span></span><br><span class="line">    <span class="keyword">if</span> (L &lt;= <span class="number">0.6</span> || C &lt;= <span class="number">0.08</span>) &#123;</span><br><span class="line">        <span class="keyword">return</span> C;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// H-K 效应在不同色相上的强度不同</span></span><br><span class="line">    <span class="comment">// 红色（H≈30）和青色（H≈210）效应最强</span></span><br><span class="line">    <span class="comment">// 黄色（H≈100）和蓝色（H≈260）效应较弱</span></span><br><span class="line">    <span class="keyword">let</span> hkFactor = <span class="number">0.12</span>; <span class="comment">// 基础补偿系数</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 红色系增强 (H: 0-50, 340-360)</span></span><br><span class="line">    <span class="keyword">if</span> ((H &gt;= <span class="number">0</span> &amp;&amp; H &lt;= <span class="number">50</span>) || (H &gt;= <span class="number">340</span> &amp;&amp; H &lt;= <span class="number">360</span>)) &#123;</span><br><span class="line">        hkFactor = <span class="number">0.18</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 青色系增强 (H: 170-230)</span></span><br><span class="line">    <span class="keyword">else</span> <span class="keyword">if</span> (H &gt;= <span class="number">170</span> &amp;&amp; H &lt;= <span class="number">230</span>) &#123;</span><br><span class="line">        hkFactor = <span class="number">0.16</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 绿色和紫色中等 (H: 100-170, 270-340)</span></span><br><span class="line">    <span class="keyword">else</span> <span class="keyword">if</span> ((H &gt;= <span class="number">100</span> &amp;&amp; H &lt;= <span class="number">170</span>) || (H &gt;= <span class="number">270</span> &amp;&amp; H &lt;= <span class="number">340</span>)) &#123;</span><br><span class="line">        hkFactor = <span class="number">0.14</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 计算补偿量：亮度越高，补偿越多</span></span><br><span class="line">    <span class="keyword">const</span> lightnessFactor = <span class="built_in">Math</span>.sqrt((L - <span class="number">0.6</span>) / <span class="number">0.4</span>);</span><br><span class="line">    <span class="keyword">const</span> reductionFactor = <span class="number">1</span> - hkFactor * lightnessFactor;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 限制最大补偿，避免颜色过于灰暗</span></span><br><span class="line">    <span class="keyword">const</span> compensatedC = C * <span class="built_in">Math</span>.max(reductionFactor, <span class="number">0.7</span>);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (compensatedC &lt; C) &#123;</span><br><span class="line">        stats.hkCompensated++;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> compensatedC;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="五、APCA-对比度保障"><a href="#五、APCA-对比度保障" class="headerlink" title="五、APCA 对比度保障"></a>五、APCA 对比度保障</h3><p>做完亮度映射和色度调整，还需要一道兜底：<strong>确保文字和背景之间有足够的对比度</strong>。</p><p>传统方案一般会采用 WCAG 2.x 的对比度算法，但这个算法有个已知问题：它对深色背景上的浅色文字给出的对比度值偏高，导致实际阅读体验不如数值显示的那么好。WCAG 3.0 草案提出了新的 <a href="https://www.myndex.com/APCA/" target="_blank" rel="noopener">APCA（Advanced Perceptual Contrast Algorithm）</a>，由 Andrew Somers 主导设计。APCA 的核心改进是<strong>区分了“浅底深字”和“深底浅字”两种模式</strong>，使用不同的幂次系数，在深色背景场景下的准确性明显优于 WCAG 2.x，更符合人眼感知。</p><p>最后一步，对文字颜色做 APCA 对比度检测。如果 |Lc| &lt; 60（正文的推荐阈值），就迭代微调文字的亮度，直到达标：</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="keyword">while</span> (<span class="built_in">Math</span>.abs(calculateAPCA(textRgb, bgRgb)) &lt; <span class="number">60</span>) &#123;</span><br><span class="line">  newL += <span class="number">0.02</span>;  <span class="comment">// 逐步提亮文字，只调整必要的量，避免过度</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><table><thead><tr><th>场景</th><th style="text-align:center">APCA 推荐阈值</th></tr></thead><tbody><tr><td>正文 (14–16px)</td><td style="text-align:center">&#124;Lc&#124; ≥ 60</td></tr><tr><td>大标题 (24px+)</td><td style="text-align:center">&#124;Lc&#124; ≥ 45</td></tr><tr><td>非文本元素</td><td style="text-align:center">&#124;Lc&#124; ≥ 30</td></tr></tbody></table><h3 id="六、性能优化：缓存"><a href="#六、性能优化：缓存" class="headerlink" title="六、性能优化：缓存"></a>六、性能优化：缓存</h3><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> ColorCache &#123;</span><br><span class="line">    <span class="keyword">private</span> cache = <span class="keyword">new</span> Map&lt;<span class="built_in">string</span>, <span class="built_in">string</span>&gt;();</span><br><span class="line">    <span class="keyword">private</span> maxSize = <span class="number">1000</span>;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 量化颜色，合并相似色</span></span><br><span class="line">    <span class="keyword">private</span> quantize(hex: <span class="built_in">string</span>): <span class="built_in">string</span> &#123;</span><br><span class="line">        <span class="keyword">const</span> rgb = parseHex(hex);</span><br><span class="line">        <span class="keyword">const</span> quantized = [rgb.r, rgb.g, rgb.b].map(</span><br><span class="line">            (v) =&gt; <span class="built_in">Math</span>.round(v / <span class="number">32</span>) * <span class="number">32</span>,</span><br><span class="line">        );</span><br><span class="line">        <span class="keyword">return</span> <span class="string">`<span class="subst">$&#123;quantized[<span class="number">0</span>]&#125;</span>,<span class="subst">$&#123;quantized[<span class="number">1</span>]&#125;</span>,<span class="subst">$&#123;quantized[<span class="number">2</span>]&#125;</span>`</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">get</span>(hex: <span class="built_in">string</span>): <span class="built_in">string</span> | <span class="literal">undefined</span> &#123;</span><br><span class="line">        <span class="keyword">const</span> key = <span class="keyword">this</span>.quantize(hex);</span><br><span class="line">        <span class="keyword">const</span> result = <span class="keyword">this</span>.cache.get(key);</span><br><span class="line">        <span class="keyword">if</span> (result) &#123;</span><br><span class="line">            <span class="comment">// LRU：访问过的移到最后</span></span><br><span class="line">            <span class="keyword">this</span>.cache.delete(key);</span><br><span class="line">            <span class="keyword">this</span>.cache.set(key, result);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> result;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">set</span>(hex: <span class="built_in">string</span>, result: <span class="built_in">string</span>): <span class="built_in">void</span> &#123;</span><br><span class="line">        <span class="keyword">const</span> key = <span class="keyword">this</span>.quantize(hex);</span><br><span class="line">        <span class="keyword">if</span> (<span class="keyword">this</span>.cache.size &gt;= <span class="keyword">this</span>.maxSize) &#123;</span><br><span class="line">            <span class="comment">// 满了就删最老的</span></span><br><span class="line">            <span class="keyword">const</span> firstKey = <span class="keyword">this</span>.cache.keys().next().value;</span><br><span class="line">            <span class="keyword">if</span> (firstKey) <span class="keyword">this</span>.cache.delete(firstKey);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">this</span>.cache.set(key, result);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这里有个技巧：<code>quantize</code> 把颜色量化到 8 级（256/32=8），相近的颜色会被合并成同一个 key。视觉上 #FF0000 和 #FF0808 几乎没区别，没必要分开算。</p><h2 id="效果与演示"><a href="#效果与演示" class="headerlink" title="效果与演示"></a>效果与演示</h2><p>我做了一个交互式的在线演示工具，支持粘贴任意 HTML 内容实时预览转换效果，包括色度增强、H-K 补偿等过程数据的展示。感兴趣可以试试：</p><p><a href="https://lab.thjiang.com/dark-mode/index.html" target="_blank" rel="noopener">https://lab.thjiang.com/dark-mode/index.html</a></p><h2 id="CSS-原生方案与未来"><a href="#CSS-原生方案与未来" class="headerlink" title="CSS 原生方案与未来"></a>CSS 原生方案与未来</h2><p>如果你的场景不需要适配第三方 HTML，CSS 原生方案可能就够了：</p><figure class="highlight css"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><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">/* OKLCH 颜色直接定义 */</span></span><br><span class="line"><span class="selector-class">.tag</span> &#123;</span><br><span class="line">    <span class="attribute">background</span>: <span class="built_in">oklch</span>(<span class="number">0.7</span> <span class="number">0.15</span> <span class="number">145</span>);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">/* 相对颜色语法：基于已有颜色计算新值 */</span></span><br><span class="line"><span class="selector-class">.dark</span> <span class="selector-class">.tag</span> &#123;</span><br><span class="line">    <span class="attribute">background</span>: <span class="built_in">oklch</span>(from var(--color) <span class="built_in">calc</span>(<span class="number">0.95</span> - l) c h);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">/* light-dark() 函数：一行搞定亮暗两套色 */</span></span><br><span class="line"><span class="selector-class">.tag</span> &#123;</span><br><span class="line">    <span class="attribute">color</span>: <span class="built_in">light-dark</span>(oklch(<span class="number">0.3</span> <span class="number">0.1</span> <span class="number">250</span>), <span class="built_in">oklch</span>(<span class="number">0.85</span> <span class="number">0.1</span> <span class="number">250</span>));</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><table><thead><tr><th>特性</th><th style="text-align:center">Chrome</th><th style="text-align:center">Firefox</th><th style="text-align:center">Safari</th></tr></thead><tbody><tr><td><code>oklch()</code></td><td style="text-align:center">111+</td><td style="text-align:center">113+</td><td style="text-align:center">15.4+</td></tr><tr><td>相对颜色 <code>from</code></td><td style="text-align:center">119+</td><td style="text-align:center">128+</td><td style="text-align:center">16.4+</td></tr><tr><td><code>light-dark()</code></td><td style="text-align:center">123+</td><td style="text-align:center">120+</td><td style="text-align:center">17.5+</td></tr></tbody></table><p>能枚举所有颜色的场景下，用 CSS 原生方案更简洁。但对于邮件客户端、富文本编辑器这类需要处理任意第三方 HTML 的场景，颜色无法预知，只能在运行时做自动转换，这种时候就是前述的算法发挥的空间了。</p><hr><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><ul><li><a href="https://bottosson.github.io/posts/oklab/" target="_blank" rel="noopener">Björn Ottosson - A perceptual color space for image processing</a></li><li><a href="https://www.w3.org/WAI/GL/wiki/Relative_luminance" target="_blank" rel="noopener">W3C Relative Luminance 勘误</a></li><li><a href="https://webstore.iec.ch/en/publication/6169" target="_blank" rel="noopener">IEC 61966-2-1:1999</a></li><li><a href="https://www.myndex.com/APCA/" target="_blank" rel="noopener">APCA Contrast Calculator — Myndex</a></li><li><a href="https://www.w3.org/TR/css-color-4/" target="_blank" rel="noopener">CSS Color Module Level 4 — W3C</a></li><li><a href="https://www.w3.org/TR/css-color-5/" target="_blank" rel="noopener">CSS Color Module Level 5 — W3C</a></li></ul>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;在 &lt;a href=&quot;https://www.thjiang.com/2025/12/28/%E6%B5%85%E8%B0%88%E7%AC%AC%E4%B8%89%E6%96%B9-HTML-%E7%9A%84%E6%9A%97%E9%BB%91%E6%A8%A1%E5%
      
    
    </summary>
    
      <category term="前端工程" scheme="https://www.thjiang.com/categories/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B/"/>
    
    
      <category term="前端工程" scheme="https://www.thjiang.com/tags/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B/"/>
    
  </entry>
  
  <entry>
    <title>浅谈第三方 HTML 的暗黑模式适配</title>
    <link href="https://www.thjiang.com/2025/12/28/%E6%B5%85%E8%B0%88%E7%AC%AC%E4%B8%89%E6%96%B9-HTML-%E7%9A%84%E6%9A%97%E9%BB%91%E6%A8%A1%E5%BC%8F%E9%80%82%E9%85%8D/"/>
    <id>https://www.thjiang.com/2025/12/28/浅谈第三方-HTML-的暗黑模式适配/</id>
    <published>2025-12-28T13:45:31.000Z</published>
    <updated>2026-01-07T12:21:50.438Z</updated>
    
    <content type="html"><![CDATA[<p>前段时间在做富文本编辑器的暗黑模式适配时，遇到了一个有趣的问题：如何优雅地处理第三方 HTML 内容的颜色转换？</p><p>这个问题在很多场景下都会遇到：</p><ul><li><strong>富文本编辑器</strong>：用户粘贴进来的 HTML 内容</li><li><strong>邮件客户端</strong>：展示收到的邮件</li><li><strong>Markdown 渲染器</strong>：渲染用户输入的 HTML</li><li><strong>内容聚合平台</strong>：展示来自不同来源的文章</li></ul><p>这些场景的共同特点是：<strong>内容不可控</strong>。你不知道用户会粘贴什么样的 HTML，也不知道邮件发送方会用什么样的颜色。理论上，HTML 可以包含 1600 多万种颜色（RGB 每个通道 0-255），如何让这些五颜六色的内容在 Dark Mode 下也能保持良好的可读性？</p><h2 id="问题分析"><a href="#问题分析" class="headerlink" title="问题分析"></a>问题分析</h2><h3 id="第三方-HTML-的特点"><a href="#第三方-HTML-的特点" class="headerlink" title="第三方 HTML 的特点"></a>第三方 HTML 的特点</h3><p>在深入方案之前，我们先看看第三方 HTML 的特点：</p><p><strong>1. 颜色来源多样</strong><br><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">&lt;!-- 内联样式 --&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">p</span> <span class="attr">style</span>=<span class="string">"color: #FF0000;"</span>&gt;</span>红色文字<span class="tag">&lt;/<span class="name">p</span>&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="comment">&lt;!-- style 标签 --&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">style</span>&gt;</span></span><br><span class="line"><span class="css">  <span class="selector-class">.warning</span> &#123; <span class="attribute">color</span>: orange; &#125;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">style</span>&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="comment">&lt;!-- HTML 属性 --&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">font</span> <span class="attr">color</span>=<span class="string">"blue"</span>&gt;</span>蓝色文字<span class="tag">&lt;/<span class="name">font</span>&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="comment">&lt;!-- 系统色关键字 --&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">span</span> <span class="attr">style</span>=<span class="string">"color: red;"</span>&gt;</span>红色<span class="tag">&lt;/<span class="name">span</span>&gt;</span></span><br></pre></td></tr></table></figure></p><p><strong>2. 颜色数量巨大</strong></p><p>理论上可以有 256³ = 16,777,216 种颜色，实际场景中也常常有成百上千种不同的颜色组合。</p><p><strong>3. 混合内容</strong></p><p>一段 HTML 可能既有文字、也有图片、logo、代码块等，不同元素对颜色的需求不同：</p><ul><li>文字和背景需要转换颜色</li><li>图片和 logo 应该保持原色</li><li>代码块可能需要特殊处理</li></ul><h3 id="核心挑战"><a href="#核心挑战" class="headerlink" title="核心挑战"></a>核心挑战</h3><p>基于上面的特点，我们面临三个核心挑战：</p><ol><li><strong>如何智能转换颜色？</strong> - 不可能为每种颜色手动指定映射关系</li><li><strong>如何保证视觉一致性？</strong> - 用户设置的”强调色”（比如红色）在转换后应该还能保持”红色系”</li><li><strong>如何保证性能？</strong> - 实时转换大量 HTML 内容不能影响页面性能</li></ol><h2 id="方案对比"><a href="#方案对比" class="headerlink" title="方案对比"></a>方案对比</h2><p>业界对这个问题有几种常见的处理方式，让我们逐一分析。</p><h3 id="方案一：CSS-Filter-全局反色"><a href="#方案一：CSS-Filter-全局反色" class="headerlink" title="方案一：CSS Filter 全局反色"></a>方案一：CSS Filter 全局反色</h3><p>这是最简单直接的方案：</p><figure class="highlight css"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="selector-class">.dark-mode</span> <span class="selector-class">.content</span> &#123;</span><br><span class="line">  <span class="attribute">filter</span>: <span class="built_in">invert</span>(<span class="number">1</span>) <span class="built_in">hue-rotate</span>(<span class="number">180deg</span>);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">/* 图片不反色 */</span></span><br><span class="line"><span class="selector-class">.dark-mode</span> <span class="selector-class">.content</span> <span class="selector-tag">img</span> &#123;</span><br><span class="line">  <span class="attribute">filter</span>: <span class="built_in">invert</span>(<span class="number">1</span>) <span class="built_in">hue-rotate</span>(<span class="number">180deg</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>原理</strong>：</p><ul><li><code>invert(1)</code> 反转所有颜色（黑变白，白变黑）</li><li><code>hue-rotate(180deg)</code> 旋转色相，让颜色看起来不那么奇怪</li><li>对图片再反转一次，抵消效果</li></ul><p><strong>优点</strong>：</p><ul><li>实现极其简单，几行 CSS 搞定</li><li>不需要解析 HTML</li><li>浏览器原生支持，兼容性好</li></ul><p><strong>缺点</strong>：</p><ul><li><strong>性能差</strong>：filter 会导致全局重排重绘，首屏渲染时间会显著增加</li><li><strong>视觉问题</strong>：图片双重反转后颜色可能不准确，而且边缘会有伪影</li><li><strong>不够精确</strong>：某些颜色转换后视觉效果不佳</li></ul><p>我实际测试了一下，对一个包含 1000 行文本和 5 张图片的 HTML 应用 filter，首屏渲染时间从 300ms 增加到了 2.1s，用户体验很差。</p><p><strong>适用场景</strong>：原型验证、纯文本内容</p><h3 id="方案二：RGB-颜色反转"><a href="#方案二：RGB-颜色反转" class="headerlink" title="方案二：RGB 颜色反转"></a>方案二：RGB 颜色反转</h3><p>第二种思路是解析 HTML，对每个颜色值进行反转：</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="function"><span class="keyword">function</span> <span class="title">invertRGB</span>(<span class="params">r, g, b</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">return</span> &#123;</span><br><span class="line">    r: <span class="number">255</span> - r,</span><br><span class="line">    g: <span class="number">255</span> - g,</span><br><span class="line">    b: <span class="number">255</span> - b</span><br><span class="line">  &#125;;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 使用示例</span></span><br><span class="line"><span class="keyword">const</span> red = &#123; <span class="attr">r</span>: <span class="number">255</span>, <span class="attr">g</span>: <span class="number">0</span>, <span class="attr">b</span>: <span class="number">0</span> &#125;;</span><br><span class="line"><span class="keyword">const</span> inverted = invertRGB(red.r, red.g, red.b);</span><br><span class="line"><span class="comment">// 结果：&#123; r: 0, g: 255, b: 255 &#125; - 青色</span></span><br></pre></td></tr></table></figure><p><strong>优点</strong>：</p><ul><li>算法简单，容易理解</li><li>可以精准控制哪些元素需要转换</li><li>性能比 filter 好很多</li></ul><p><strong>缺点</strong>：</p><ul><li><strong>色相完全改变</strong>：红色变成青色，蓝色变成黄色</li><li><strong>用户意图丢失</strong>：如果用户用红色表示”警告”，转换后变成青色就失去了语义</li></ul><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="comment">// Light Mode 的配色</span></span><br><span class="line"><span class="keyword">const</span> colors = &#123;</span><br><span class="line">  danger: <span class="string">'rgb(255, 0, 0)'</span>,    <span class="comment">// 红色表示危险</span></span><br><span class="line">  success: <span class="string">'rgb(0, 255, 0)'</span>,   <span class="comment">// 绿色表示成功</span></span><br><span class="line">  info: <span class="string">'rgb(0, 0, 255)'</span>,      <span class="comment">// 蓝色表示信息</span></span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="comment">// RGB 反转后</span></span><br><span class="line"><span class="keyword">const</span> darkColors = &#123;</span><br><span class="line">  danger: <span class="string">'rgb(0, 255, 255)'</span>,  <span class="comment">// 青色？？？</span></span><br><span class="line">  success: <span class="string">'rgb(255, 0, 255)'</span>, <span class="comment">// 品红？？？</span></span><br><span class="line">  info: <span class="string">'rgb(255, 255, 0)'</span>,    <span class="comment">// 黄色？？？</span></span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>语义完全错乱了，这显然不是我们想要的。</p><p><strong>适用场景</strong>：不关心色相语义的场景（比如纯黑白灰）</p><h3 id="方案三：HSL-色相旋转（推荐）"><a href="#方案三：HSL-色相旋转（推荐）" class="headerlink" title="方案三：HSL 色相旋转（推荐）"></a>方案三：HSL 色相旋转（推荐）</h3><p>第三种方案是在 HSL 色彩空间中进行转换。HSL 是一种更符合人类直觉的颜色表示方式：</p><ul><li><strong>H (Hue)</strong> - 色相：0-360°，表示颜色种类（红/橙/黄/绿/青/蓝/紫）</li><li><strong>S (Saturation)</strong> - 饱和度：0-100%，表示颜色的纯度</li><li><strong>L (Lightness)</strong> - 明度：0-100%，表示颜色的明暗</li></ul><p>核心思路是：<strong>旋转色相 180°，反转明度</strong></p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">rgbToHsl</span>(<span class="params">r, g, b</span>) </span>&#123;</span><br><span class="line">  r /= <span class="number">255</span>;</span><br><span class="line">  g /= <span class="number">255</span>;</span><br><span class="line">  b /= <span class="number">255</span>;</span><br><span class="line">  </span><br><span class="line">  <span class="keyword">const</span> max = <span class="built_in">Math</span>.max(r, g, b);</span><br><span class="line">  <span class="keyword">const</span> min = <span class="built_in">Math</span>.min(r, g, b);</span><br><span class="line">  <span class="keyword">let</span> h, s, l = (max + min) / <span class="number">2</span>;</span><br><span class="line">  </span><br><span class="line">  <span class="keyword">if</span> (max === min) &#123;</span><br><span class="line">    h = s = <span class="number">0</span>; <span class="comment">// 灰色</span></span><br><span class="line">  &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    <span class="keyword">const</span> d = max - min;</span><br><span class="line">    s = l &gt; <span class="number">0.5</span> ? d / (<span class="number">2</span> - max - min) : d / (max + min);</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">switch</span> (max) &#123;</span><br><span class="line">      <span class="keyword">case</span> r: h = ((g - b) / d + (g &lt; b ? <span class="number">6</span> : <span class="number">0</span>)) / <span class="number">6</span>; <span class="keyword">break</span>;</span><br><span class="line">      <span class="keyword">case</span> g: h = ((b - r) / d + <span class="number">2</span>) / <span class="number">6</span>; <span class="keyword">break</span>;</span><br><span class="line">      <span class="keyword">case</span> b: h = ((r - g) / d + <span class="number">4</span>) / <span class="number">6</span>; <span class="keyword">break</span>;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">  </span><br><span class="line">  <span class="keyword">return</span> &#123;</span><br><span class="line">    h: <span class="built_in">Math</span>.round(h * <span class="number">360</span>),</span><br><span class="line">    s: <span class="built_in">Math</span>.round(s * <span class="number">100</span>),</span><br><span class="line">    l: <span class="built_in">Math</span>.round(l * <span class="number">100</span>)</span><br><span class="line">  &#125;;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">invertHSL</span>(<span class="params">h, s, l</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">return</span> &#123;</span><br><span class="line">    h: (h + <span class="number">180</span>) % <span class="number">360</span>,  <span class="comment">// 色相旋转 180°</span></span><br><span class="line">    s: s,                <span class="comment">// 保持饱和度</span></span><br><span class="line">    l: <span class="number">100</span> - l           <span class="comment">// 反转明度</span></span><br><span class="line">  &#125;;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>让我们看看同样的颜色用 HSL 转换后的效果：</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="comment">// Light Mode</span></span><br><span class="line"><span class="keyword">const</span> red = &#123; <span class="attr">h</span>: <span class="number">0</span>, <span class="attr">s</span>: <span class="number">100</span>, <span class="attr">l</span>: <span class="number">50</span> &#125;;     <span class="comment">// 红色</span></span><br><span class="line"><span class="keyword">const</span> green = &#123; <span class="attr">h</span>: <span class="number">120</span>, <span class="attr">s</span>: <span class="number">100</span>, <span class="attr">l</span>: <span class="number">50</span> &#125;; <span class="comment">// 绿色</span></span><br><span class="line"><span class="keyword">const</span> blue = &#123; <span class="attr">h</span>: <span class="number">240</span>, <span class="attr">s</span>: <span class="number">100</span>, <span class="attr">l</span>: <span class="number">50</span> &#125;;  <span class="comment">// 蓝色</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// HSL 转换后（色相旋转 180°，明度反转）</span></span><br><span class="line"><span class="keyword">const</span> darkRed = &#123; <span class="attr">h</span>: <span class="number">180</span>, <span class="attr">s</span>: <span class="number">100</span>, <span class="attr">l</span>: <span class="number">50</span> &#125;;   <span class="comment">// 青色系，但明度适中</span></span><br><span class="line"><span class="keyword">const</span> darkGreen = &#123; <span class="attr">h</span>: <span class="number">300</span>, <span class="attr">s</span>: <span class="number">100</span>, <span class="attr">l</span>: <span class="number">50</span> &#125;; <span class="comment">// 品红系，但明度适中</span></span><br><span class="line"><span class="keyword">const</span> darkBlue = &#123; <span class="attr">h</span>: <span class="number">60</span>, <span class="attr">s</span>: <span class="number">100</span>, <span class="attr">l</span>: <span class="number">50</span> &#125;;   <span class="comment">// 黄色系，但明度适中</span></span><br></pre></td></tr></table></figure><p>等等，这不是和 RGB 反转一样吗？别急，这里有个关键点：我们还需要<strong>调整明度</strong>来保证可读性。实际上应该这样：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">invertHSL</span>(<span class="params">h, s, l</span>) </span>&#123;</span><br><span class="line">  <span class="comment">// 深色模式下，深色变浅，浅色变深</span></span><br><span class="line">  <span class="keyword">const</span> newL = l &lt; <span class="number">50</span> ? l + <span class="number">40</span> : l - <span class="number">40</span>;</span><br><span class="line">  </span><br><span class="line">  <span class="keyword">return</span> &#123;</span><br><span class="line">    h: (h + <span class="number">180</span>) % <span class="number">360</span>,</span><br><span class="line">    s: s,</span><br><span class="line">    l: <span class="built_in">Math</span>.max(<span class="number">10</span>, <span class="built_in">Math</span>.min(<span class="number">90</span>, newL)) <span class="comment">// 确保在合理范围内</span></span><br><span class="line">  &#125;;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>优点</strong>：</p><ul><li><strong>保持色相语义</strong>：红色依然是偏红的色系</li><li><strong>可精确控制</strong>：可以独立调整色相、饱和度、明度</li><li><strong>性能可控</strong>：只在必要时转换，比 filter 快很多</li><li><strong>符合直觉</strong>：HSL 的调整更接近人类对颜色的认知</li></ul><p><strong>缺点</strong>：</p><ul><li>实现稍复杂（需要 RGB ↔ HSL 转换）</li><li>需要处理边界场景（下一篇文章会详细讲）</li></ul><h3 id="方案对比总结"><a href="#方案对比总结" class="headerlink" title="方案对比总结"></a>方案对比总结</h3><table><thead><tr><th>方案</th><th>实现难度</th><th>色相保持</th><th>性能</th><th>适用场景</th></tr></thead><tbody><tr><td><strong>CSS Filter</strong></td><td>⭐</td><td>❌</td><td>⭐</td><td>原型验证</td></tr><tr><td><strong>RGB 反转</strong></td><td>⭐⭐</td><td>❌</td><td>⭐⭐⭐</td><td>黑白内容</td></tr><tr><td><strong>HSL 旋转</strong></td><td>⭐⭐⭐</td><td>✅</td><td>⭐⭐⭐⭐</td><td><strong>推荐</strong></td></tr></tbody></table><h2 id="实践：基于-HSL-的完整方案"><a href="#实践：基于-HSL-的完整方案" class="headerlink" title="实践：基于 HSL 的完整方案"></a>实践：基于 HSL 的完整方案</h2><p>确定了技术方向后，我们来看看如何实现。</p><h3 id="技术架构"><a href="#技术架构" class="headerlink" title="技术架构"></a>技术架构</h3><p>我们可以将整体架构分为三层：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><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">第三方 HTML</span><br><span class="line">    ↓</span><br><span class="line">1. 安全过滤层（DOMPurify）</span><br><span class="line">    ↓</span><br><span class="line">2. 颜色处理层（Cheerio + 颜色转换算法）</span><br><span class="line">    ↓</span><br><span class="line">3. 渲染层（React&#x2F;Vue&#x2F;原生）</span><br></pre></td></tr></table></figure><p><strong>为什么需要 DOMPurify？</strong></p><p>第三方 HTML 可能包含 XSS 攻击代码，必须先过滤。DOMPurify 是业界标准的 HTML 清理库。</p><p><strong>为什么用 Cheerio？</strong></p><p>Cheerio 可以理解为服务端的 jQuery，可以方便地解析和操作 HTML。它比正则表达式更可靠，比浏览器 DOM API 更快。</p><h3 id="核心代码实现"><a href="#核心代码实现" class="headerlink" title="核心代码实现"></a>核心代码实现</h3><h4 id="1-颜色解析"><a href="#1-颜色解析" class="headerlink" title="1. 颜色解析"></a>1. 颜色解析</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><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></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 解析各种格式的颜色</span></span><br><span class="line"><span class="comment"> * 支持：hex、rgb、rgba、hsl、hsla、颜色关键字</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">parseColor</span>(<span class="params">color</span>) </span>&#123;</span><br><span class="line">  color = color.trim().toLowerCase();</span><br><span class="line">  </span><br><span class="line">  <span class="comment">// Hex 格式：#RGB 或 #RRGGBB</span></span><br><span class="line">  <span class="keyword">const</span> hexMatch = color.match(<span class="regexp">/^#([0-9a-f]&#123;3&#125;|[0-9a-f]&#123;6&#125;)$/</span>);</span><br><span class="line">  <span class="keyword">if</span> (hexMatch) &#123;</span><br><span class="line">    <span class="keyword">let</span> hex = hexMatch[<span class="number">1</span>];</span><br><span class="line">    <span class="keyword">if</span> (hex.length === <span class="number">3</span>) &#123;</span><br><span class="line">      hex = hex.split(<span class="string">''</span>).map(<span class="function"><span class="params">c</span> =&gt;</span> c + c).join(<span class="string">''</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> &#123;</span><br><span class="line">      r: <span class="built_in">parseInt</span>(hex.substr(<span class="number">0</span>, <span class="number">2</span>), <span class="number">16</span>),</span><br><span class="line">      g: <span class="built_in">parseInt</span>(hex.substr(<span class="number">2</span>, <span class="number">2</span>), <span class="number">16</span>),</span><br><span class="line">      b: <span class="built_in">parseInt</span>(hex.substr(<span class="number">4</span>, <span class="number">2</span>), <span class="number">16</span>),</span><br><span class="line">      a: <span class="number">1</span></span><br><span class="line">    &#125;;</span><br><span class="line">  &#125;</span><br><span class="line">  </span><br><span class="line">  <span class="comment">// RGB/RGBA 格式</span></span><br><span class="line">  <span class="keyword">const</span> rgbMatch = color.match(<span class="regexp">/^rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)$/</span>);</span><br><span class="line">  <span class="keyword">if</span> (rgbMatch) &#123;</span><br><span class="line">    <span class="keyword">return</span> &#123;</span><br><span class="line">      r: <span class="built_in">parseInt</span>(rgbMatch[<span class="number">1</span>]),</span><br><span class="line">      g: <span class="built_in">parseInt</span>(rgbMatch[<span class="number">2</span>]),</span><br><span class="line">      b: <span class="built_in">parseInt</span>(rgbMatch[<span class="number">3</span>]),</span><br><span class="line">      a: rgbMatch[<span class="number">4</span>] ? <span class="built_in">parseFloat</span>(rgbMatch[<span class="number">4</span>]) : <span class="number">1</span></span><br><span class="line">    &#125;;</span><br><span class="line">  &#125;</span><br><span class="line">  </span><br><span class="line">  <span class="comment">// HSL/HSLA 格式</span></span><br><span class="line">  <span class="keyword">const</span> hslMatch = color.match(<span class="regexp">/^hsla?\((\d+),\s*([\d.]+)%,\s*([\d.]+)%(?:,\s*([\d.]+))?\)$/</span>);</span><br><span class="line">  <span class="keyword">if</span> (hslMatch) &#123;</span><br><span class="line">    <span class="keyword">const</span> rgb = hslToRgb(</span><br><span class="line">      <span class="built_in">parseInt</span>(hslMatch[<span class="number">1</span>]),</span><br><span class="line">      <span class="built_in">parseFloat</span>(hslMatch[<span class="number">2</span>]),</span><br><span class="line">      <span class="built_in">parseFloat</span>(hslMatch[<span class="number">3</span>])</span><br><span class="line">    );</span><br><span class="line">    <span class="keyword">return</span> &#123;</span><br><span class="line">      ...rgb,</span><br><span class="line">      a: hslMatch[<span class="number">4</span>] ? <span class="built_in">parseFloat</span>(hslMatch[<span class="number">4</span>]) : <span class="number">1</span></span><br><span class="line">    &#125;;</span><br><span class="line">  &#125;</span><br><span class="line">  </span><br><span class="line">  <span class="comment">// 颜色关键字（部分示例）</span></span><br><span class="line">  <span class="keyword">const</span> colorKeywords = &#123;</span><br><span class="line">    black: <span class="string">'#000000'</span>,</span><br><span class="line">    white: <span class="string">'#ffffff'</span>,</span><br><span class="line">    red: <span class="string">'#ff0000'</span>,</span><br><span class="line">    green: <span class="string">'#00ff00'</span>,</span><br><span class="line">    blue: <span class="string">'#0000ff'</span>,</span><br><span class="line">    <span class="comment">// ... 多个标准颜色关键字</span></span><br><span class="line">  &#125;;</span><br><span class="line">  </span><br><span class="line">  <span class="keyword">if</span> (colorKeywords[color]) &#123;</span><br><span class="line">    <span class="keyword">return</span> parseColor(colorKeywords[color]);</span><br><span class="line">  &#125;</span><br><span class="line">  </span><br><span class="line">  <span class="keyword">return</span> <span class="literal">null</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="2-HSL-转换核心算法"><a href="#2-HSL-转换核心算法" class="headerlink" title="2. HSL 转换核心算法"></a>2. HSL 转换核心算法</h4><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><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></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * RGB 转 HSL</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">rgbToHsl</span>(<span class="params">r, g, b</span>) </span>&#123;</span><br><span class="line">  r /= <span class="number">255</span>;</span><br><span class="line">  g /= <span class="number">255</span>;</span><br><span class="line">  b /= <span class="number">255</span>;</span><br><span class="line">  </span><br><span class="line">  <span class="keyword">const</span> max = <span class="built_in">Math</span>.max(r, g, b);</span><br><span class="line">  <span class="keyword">const</span> min = <span class="built_in">Math</span>.min(r, g, b);</span><br><span class="line">  <span class="keyword">let</span> h, s, l = (max + min) / <span class="number">2</span>;</span><br><span class="line">  </span><br><span class="line">  <span class="keyword">if</span> (max === min) &#123;</span><br><span class="line">    h = s = <span class="number">0</span>;</span><br><span class="line">  &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    <span class="keyword">const</span> d = max - min;</span><br><span class="line">    s = l &gt; <span class="number">0.5</span> ? d / (<span class="number">2</span> - max - min) : d / (max + min);</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">switch</span> (max) &#123;</span><br><span class="line">      <span class="keyword">case</span> r: h = ((g - b) / d + (g &lt; b ? <span class="number">6</span> : <span class="number">0</span>)) / <span class="number">6</span>; <span class="keyword">break</span>;</span><br><span class="line">      <span class="keyword">case</span> g: h = ((b - r) / d + <span class="number">2</span>) / <span class="number">6</span>; <span class="keyword">break</span>;</span><br><span class="line">      <span class="keyword">case</span> b: h = ((r - g) / d + <span class="number">4</span>) / <span class="number">6</span>; <span class="keyword">break</span>;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">  </span><br><span class="line">  <span class="keyword">return</span> &#123;</span><br><span class="line">    h: <span class="built_in">Math</span>.round(h * <span class="number">360</span>),</span><br><span class="line">    s: <span class="built_in">Math</span>.round(s * <span class="number">100</span>),</span><br><span class="line">    l: <span class="built_in">Math</span>.round(l * <span class="number">100</span>)</span><br><span class="line">  &#125;;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * HSL 转 RGB</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">hslToRgb</span>(<span class="params">h, s, l</span>) </span>&#123;</span><br><span class="line">  h /= <span class="number">360</span>;</span><br><span class="line">  s /= <span class="number">100</span>;</span><br><span class="line">  l /= <span class="number">100</span>;</span><br><span class="line">  </span><br><span class="line">  <span class="keyword">let</span> r, g, b;</span><br><span class="line">  </span><br><span class="line">  <span class="keyword">if</span> (s === <span class="number">0</span>) &#123;</span><br><span class="line">    r = g = b = l;</span><br><span class="line">  &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    <span class="keyword">const</span> hue2rgb = <span class="function">(<span class="params">p, q, t</span>) =&gt;</span> &#123;</span><br><span class="line">      <span class="keyword">if</span> (t &lt; <span class="number">0</span>) t += <span class="number">1</span>;</span><br><span class="line">      <span class="keyword">if</span> (t &gt; <span class="number">1</span>) t -= <span class="number">1</span>;</span><br><span class="line">      <span class="keyword">if</span> (t &lt; <span class="number">1</span>/<span class="number">6</span>) <span class="keyword">return</span> p + (q - p) * <span class="number">6</span> * t;</span><br><span class="line">      <span class="keyword">if</span> (t &lt; <span class="number">1</span>/<span class="number">2</span>) <span class="keyword">return</span> q;</span><br><span class="line">      <span class="keyword">if</span> (t &lt; <span class="number">2</span>/<span class="number">3</span>) <span class="keyword">return</span> p + (q - p) * (<span class="number">2</span>/<span class="number">3</span> - t) * <span class="number">6</span>;</span><br><span class="line">      <span class="keyword">return</span> p;</span><br><span class="line">    &#125;;</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">const</span> q = l &lt; <span class="number">0.5</span> ? l * (<span class="number">1</span> + s) : l + s - l * s;</span><br><span class="line">    <span class="keyword">const</span> p = <span class="number">2</span> * l - q;</span><br><span class="line">    </span><br><span class="line">    r = hue2rgb(p, q, h + <span class="number">1</span>/<span class="number">3</span>);</span><br><span class="line">    g = hue2rgb(p, q, h);</span><br><span class="line">    b = hue2rgb(p, q, h - <span class="number">1</span>/<span class="number">3</span>);</span><br><span class="line">  &#125;</span><br><span class="line">  </span><br><span class="line">  <span class="keyword">return</span> &#123;</span><br><span class="line">    r: <span class="built_in">Math</span>.round(r * <span class="number">255</span>),</span><br><span class="line">    g: <span class="built_in">Math</span>.round(g * <span class="number">255</span>),</span><br><span class="line">    b: <span class="built_in">Math</span>.round(b * <span class="number">255</span>)</span><br><span class="line">  &#125;;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 反转颜色（简化版）</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">invertColor</span>(<span class="params">color, isDark = true</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">if</span> (!isDark) <span class="keyword">return</span> color;</span><br><span class="line">  </span><br><span class="line">  <span class="keyword">const</span> parsed = parseColor(color);</span><br><span class="line">  <span class="keyword">if</span> (!parsed) <span class="keyword">return</span> color;</span><br><span class="line">  </span><br><span class="line">  <span class="keyword">const</span> &#123; r, g, b, a &#125; = parsed;</span><br><span class="line">  <span class="keyword">const</span> &#123; h, s, l &#125; = rgbToHsl(r, g, b);</span><br><span class="line">  </span><br><span class="line">  <span class="comment">// 色相旋转 180°</span></span><br><span class="line">  <span class="keyword">const</span> newH = (h + <span class="number">180</span>) % <span class="number">360</span>;</span><br><span class="line">  </span><br><span class="line">  <span class="comment">// 明度反转，但要确保可读性</span></span><br><span class="line">  <span class="keyword">let</span> newL;</span><br><span class="line">  <span class="keyword">if</span> (l &lt; <span class="number">50</span>) &#123;</span><br><span class="line">    <span class="comment">// 深色变浅</span></span><br><span class="line">    newL = <span class="built_in">Math</span>.min(l + <span class="number">50</span>, <span class="number">85</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">    newL = <span class="built_in">Math</span>.max(l - <span class="number">50</span>, <span class="number">15</span>);</span><br><span class="line">  &#125;</span><br><span class="line">  </span><br><span class="line">  <span class="keyword">const</span> rgb = hslToRgb(newH, s, newL);</span><br><span class="line">  </span><br><span class="line">  <span class="comment">// 转回原格式</span></span><br><span class="line">  <span class="keyword">if</span> (a &lt; <span class="number">1</span>) &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="string">`rgba(<span class="subst">$&#123;rgb.r&#125;</span>, <span class="subst">$&#123;rgb.g&#125;</span>, <span class="subst">$&#123;rgb.b&#125;</span>, <span class="subst">$&#123;a&#125;</span>)`</span>;</span><br><span class="line">  &#125;</span><br><span class="line">  </span><br><span class="line">  <span class="comment">// 转为 hex</span></span><br><span class="line">  <span class="keyword">const</span> toHex = <span class="function">(<span class="params">n</span>) =&gt;</span> n.toString(<span class="number">16</span>).padStart(<span class="number">2</span>, <span class="string">'0'</span>);</span><br><span class="line">  <span class="keyword">return</span> <span class="string">`#<span class="subst">$&#123;toHex(rgb.r)&#125;</span><span class="subst">$&#123;toHex(rgb.g)&#125;</span><span class="subst">$&#123;toHex(rgb.b)&#125;</span>`</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="3-HTML-处理"><a href="#3-HTML-处理" class="headerlink" title="3. HTML 处理"></a>3. HTML 处理</h4><p>使用 Cheerio 解析和转换 HTML：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> * <span class="keyword">as</span> cheerio <span class="keyword">from</span> <span class="string">'cheerio'</span>;</span><br><span class="line"><span class="keyword">import</span> DOMPurify <span class="keyword">from</span> <span class="string">'dompurify'</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 应用 Dark Mode 转换</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">applyDarkMode</span>(<span class="params">html, isDark = true</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">if</span> (!isDark) <span class="keyword">return</span> html;</span><br><span class="line">  </span><br><span class="line">  <span class="comment">// 1. 先用 DOMPurify 清理</span></span><br><span class="line">  <span class="keyword">const</span> clean = DOMPurify.sanitize(html);</span><br><span class="line">  </span><br><span class="line">  <span class="comment">// 2. 用 Cheerio 解析</span></span><br><span class="line">  <span class="keyword">const</span> $ = cheerio.load(clean);</span><br><span class="line">  </span><br><span class="line">  <span class="comment">// 3. 处理内联样式</span></span><br><span class="line">  $(<span class="string">'[style]'</span>).each(<span class="function">(<span class="params">i, el</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">let</span> style = $(el).attr(<span class="string">'style'</span>);</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 转换 color</span></span><br><span class="line">    style = style.replace(</span><br><span class="line">      /color\s*:\s*([^;]+)/gi,</span><br><span class="line">      (match, color) =&gt; <span class="string">`color: <span class="subst">$&#123;invertColor(color, isDark)&#125;</span>`</span></span><br><span class="line">    );</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 转换 background-color</span></span><br><span class="line">    style = style.replace(</span><br><span class="line">      /background-color\s*:\s*([^;]+)/gi,</span><br><span class="line">      (match, color) =&gt; <span class="string">`background-color: <span class="subst">$&#123;invertColor(color, isDark)&#125;</span>`</span></span><br><span class="line">    );</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 转换 border-color</span></span><br><span class="line">    style = style.replace(</span><br><span class="line">      /border-color\s*:\s*([^;]+)/gi,</span><br><span class="line">      (match, color) =&gt; <span class="string">`border-color: <span class="subst">$&#123;invertColor(color, isDark)&#125;</span>`</span></span><br><span class="line">    );</span><br><span class="line">    </span><br><span class="line">    $(el).attr(<span class="string">'style'</span>, style);</span><br><span class="line">  &#125;);</span><br><span class="line">  </span><br><span class="line">  <span class="comment">// 4. 处理 HTML 属性</span></span><br><span class="line">  $(<span class="string">'[color]'</span>).each(<span class="function">(<span class="params">i, el</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">const</span> color = $(el).attr(<span class="string">'color'</span>);</span><br><span class="line">    $(el).attr(<span class="string">'color'</span>, invertColor(color, isDark));</span><br><span class="line">  &#125;);</span><br><span class="line">  </span><br><span class="line">  $(<span class="string">'[bgcolor]'</span>).each(<span class="function">(<span class="params">i, el</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">const</span> bgcolor = $(el).attr(<span class="string">'bgcolor'</span>);</span><br><span class="line">    $(el).attr(<span class="string">'bgcolor'</span>, invertColor(bgcolor, isDark));</span><br><span class="line">  &#125;);</span><br><span class="line">  </span><br><span class="line">  <span class="comment">// 5. 处理 style 标签</span></span><br><span class="line">  $(<span class="string">'style'</span>).each(<span class="function">(<span class="params">i, el</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">let</span> css = $(el).html();</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 简单的 CSS 颜色替换（生产环境建议用 PostCSS）</span></span><br><span class="line">    css = css.replace(</span><br><span class="line">      /color\s*:\s*([^;&#125;\s]+)/gi,</span><br><span class="line">      (match, color) =&gt; <span class="string">`color: <span class="subst">$&#123;invertColor(color, isDark)&#125;</span>`</span></span><br><span class="line">    );</span><br><span class="line">    </span><br><span class="line">    $(el).html(css);</span><br><span class="line">  &#125;);</span><br><span class="line">  </span><br><span class="line">  <span class="keyword">return</span> $.html();</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><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="comment">// React 示例</span></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">Content</span>(<span class="params">&#123; html, isDarkMode &#125;</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">const</span> processedHtml = useMemo(<span class="function"><span class="params">()</span> =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> applyDarkMode(html, isDarkMode);</span><br><span class="line">  &#125;, [html, isDarkMode]);</span><br><span class="line">  </span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    &lt;div </span><br><span class="line">      className=<span class="string">"content"</span></span><br><span class="line">      dangerouslySetInnerHTML=&#123;&#123; <span class="attr">__html</span>: processedHtml &#125;&#125;</span><br><span class="line">    /&gt;</span><br><span class="line">  );</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// Vue 示例</span></span><br><span class="line">&lt;template&gt;</span><br><span class="line">  &lt;div <span class="class"><span class="keyword">class</span></span>=<span class="string">"content"</span> v-html=<span class="string">"processedHtml"</span>&gt;&lt;<span class="regexp">/div&gt;</span></span><br><span class="line"><span class="regexp">&lt;/</span>template&gt;</span><br><span class="line"></span><br><span class="line">&lt;script&gt;</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> &#123;</span><br><span class="line">  props: [<span class="string">'html'</span>, <span class="string">'isDarkMode'</span>],</span><br><span class="line">  computed: &#123;</span><br><span class="line">    processedHtml() &#123;</span><br><span class="line">      <span class="keyword">return</span> applyDarkMode(<span class="keyword">this</span>.html, <span class="keyword">this</span>.isDarkMode);</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line">&lt;<span class="regexp">/script&gt;</span></span><br></pre></td></tr></table></figure><h3 id="图片和媒体元素处理"><a href="#图片和媒体元素处理" class="headerlink" title="图片和媒体元素处理"></a>图片和媒体元素处理</h3><p>图片、视频、logo 等媒体元素应该保持原色：</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="function"><span class="keyword">function</span> <span class="title">applyDarkMode</span>(<span class="params">html, isDark = true</span>) </span>&#123;</span><br><span class="line">  <span class="comment">// ... 前面的代码 ...</span></span><br><span class="line">  </span><br><span class="line">  <span class="comment">// 图片保持原色，移除可能的 style</span></span><br><span class="line">  $(<span class="string">'img, video, svg'</span>).each(<span class="function">(<span class="params">i, el</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">const</span> style = $(el).attr(<span class="string">'style'</span>);</span><br><span class="line">    <span class="keyword">if</span> (style) &#123;</span><br><span class="line">      <span class="comment">// 移除 filter 相关的样式</span></span><br><span class="line">      <span class="keyword">const</span> newStyle = style.replace(<span class="regexp">/filter\s*:[^;]+;?/gi</span>, <span class="string">''</span>);</span><br><span class="line">      $(el).attr(<span class="string">'style'</span>, newStyle);</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> $.html();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="性能优化"><a href="#性能优化" class="headerlink" title="性能优化"></a>性能优化</h2><h3 id="1-缓存颜色转换结果"><a href="#1-缓存颜色转换结果" class="headerlink" title="1. 缓存颜色转换结果"></a>1. 缓存颜色转换结果</h3><p>相同的颜色不需要重复转换：</p><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">const</span> colorCache = <span class="keyword">new</span> <span class="built_in">Map</span>();</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">invertColorCached</span>(<span class="params">color, isDark</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">const</span> cacheKey = <span class="string">`<span class="subst">$&#123;color&#125;</span>_<span class="subst">$&#123;isDark&#125;</span>`</span>;</span><br><span class="line">  </span><br><span class="line">  <span class="keyword">if</span> (colorCache.has(cacheKey)) &#123;</span><br><span class="line">    <span class="keyword">return</span> colorCache.get(cacheKey);</span><br><span class="line">  &#125;</span><br><span class="line">  </span><br><span class="line">  <span class="keyword">const</span> result = invertColor(color, isDark);</span><br><span class="line">  colorCache.set(cacheKey, result);</span><br><span class="line">  </span><br><span class="line">  <span class="keyword">return</span> result;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="2-批量处理"><a href="#2-批量处理" class="headerlink" title="2. 批量处理"></a>2. 批量处理</h3><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="function"><span class="keyword">function</span> <span class="title">batchInvertColors</span>(<span class="params">colors, isDark</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">return</span> colors.map(<span class="function"><span class="params">color</span> =&gt;</span> invertColorCached(color, isDark));</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="3-Web-Worker"><a href="#3-Web-Worker" class="headerlink" title="3. Web Worker"></a>3. Web Worker</h3><p>对于大量 HTML 内容，可以放到 Web Worker 中处理：</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="comment">// worker.js</span></span><br><span class="line">self.addEventListener(<span class="string">'message'</span>, (e) =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> &#123; html, isDark &#125; = e.data;</span><br><span class="line">  <span class="keyword">const</span> processed = applyDarkMode(html, isDark);</span><br><span class="line">  self.postMessage(&#123; processed &#125;);</span><br><span class="line">&#125;);</span><br><span class="line"></span><br><span class="line"><span class="comment">// main.js</span></span><br><span class="line"><span class="keyword">const</span> worker = <span class="keyword">new</span> Worker(<span class="string">'worker.js'</span>);</span><br><span class="line"></span><br><span class="line">worker.postMessage(&#123; <span class="attr">html</span>: largeHtml, <span class="attr">isDark</span>: <span class="literal">true</span> &#125;);</span><br><span class="line"></span><br><span class="line">worker.addEventListener(<span class="string">'message'</span>, (e) =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> &#123; processed &#125; = e.data;</span><br><span class="line">  <span class="comment">// 使用处理后的 HTML</span></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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">lazyApplyDarkMode</span>(<span class="params">html, isDark</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">const</span> $ = cheerio.load(html);</span><br><span class="line">  </span><br><span class="line">  <span class="comment">// 只处理前面的内容</span></span><br><span class="line">  <span class="keyword">const</span> firstScreen = $(<span class="string">'body'</span>).children().slice(<span class="number">0</span>, <span class="number">50</span>);</span><br><span class="line">  </span><br><span class="line">  firstScreen.each(<span class="function">(<span class="params">i, el</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="comment">// 转换颜色</span></span><br><span class="line">  &#125;);</span><br><span class="line">  </span><br><span class="line">  <span class="comment">// 其余内容用 IntersectionObserver 懒加载</span></span><br><span class="line">  <span class="keyword">return</span> $.html();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>第三方 HTML 内容的 Dark Mode 适配是一个有趣的技术挑战。通过对比几种主流方案，我们可以得出如下结论：</p><ol><li><strong>CSS Filter</strong> 适合快速原型验证，但性能差</li><li><strong>RGB 反转</strong> 实现简单但色相语义丢失</li><li><strong>HSL 旋转</strong> 是当前较优的方案，兼顾性能和效果</li></ol><p>不过，实际应用中还有很多细节需要处理，比如：</p><ul><li>纯黑白颜色如何处理？</li><li>低饱和度的灰色如何处理？</li><li>半透明颜色如何处理？</li><li>系统色关键字（red、blue 等）如何映射？</li></ul><p>这些边界场景的处理直接影响最终效果。下一篇文章，我会继续深入讲解 HSL 算法的边界场景处理，以及如何确保绝大部分颜色都能正确转换。</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;前段时间在做富文本编辑器的暗黑模式适配时，遇到了一个有趣的问题：如何优雅地处理第三方 HTML 内容的颜色转换？&lt;/p&gt;
&lt;p&gt;这个问题在很多场景下都会遇到：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;富文本编辑器&lt;/strong&gt;：用户粘贴进来的 HTML 内容&lt;/li&gt;
      
    
    </summary>
    
      <category term="前端工程" scheme="https://www.thjiang.com/categories/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B/"/>
    
    
      <category term="前端工程" scheme="https://www.thjiang.com/tags/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B/"/>
    
  </entry>
  
  <entry>
    <title>更优雅的让 AI 生成代码——从一个实战项目看提示词的一些心得</title>
    <link href="https://www.thjiang.com/2025/04/06/%E6%9B%B4%E4%BC%98%E9%9B%85%E7%9A%84%E8%AE%A9-AI-%E7%94%9F%E6%88%90%E4%BB%A3%E7%A0%81%E2%80%94%E2%80%94%E4%BB%8E%E4%B8%80%E4%B8%AA%E5%AE%9E%E6%88%98%E9%A1%B9%E7%9B%AE%E7%9C%8B%E6%8F%90%E7%A4%BA%E8%AF%8D%E7%9A%84%E4%B8%80%E4%BA%9B%E5%BF%83%E5%BE%97/"/>
    <id>https://www.thjiang.com/2025/04/06/更优雅的让-AI-生成代码——从一个实战项目看提示词的一些心得/</id>
    <published>2025-04-06T03:23:12.000Z</published>
    <updated>2025-12-29T11:54:48.108Z</updated>
    
    <content type="html"><![CDATA[<p>最近用 AI 实现了一个 Dota2 队长模式 BP 模拟器的前端项目。整个过程中，很明显能发现提示词的质量直接决定了生成代码的可用性，一个模糊的需求可能需要反复修改十几次，而一个精确的提示词往往能一次性生成 90% 可用的代码。结合这次实践，分享一些心得，抛砖引玉。</p><h2 id="AI-生成代码的常见问题"><a href="#AI-生成代码的常见问题" class="headerlink" title="AI 生成代码的常见问题"></a>AI 生成代码的常见问题</h2><p>在开始分享经验之前，先看看 AI 生成代码常见的几类问题：</p><h3 id="1-需求理解偏差"><a href="#1-需求理解偏差" class="headerlink" title="1. 需求理解偏差"></a>1. 需求理解偏差</h3><p><strong>场景：</strong> “帮我实现一个倒计时功能”</p><p><strong>问题：</strong> AI 可能实现一个简单的 <code>setInterval</code> 递减数字，但你实际需要的是：</p><ul><li>倒计时归零后使用备用时间</li><li>时间不能显示负数</li><li>最后 10 秒要红色警告</li><li>时间用完后自动触发某个操作</li></ul><p>这类问题的根源是：<strong>你知道你要什么，但 AI 不知道你的上下文</strong>。</p><h3 id="2-技术栈不匹配"><a href="#2-技术栈不匹配" class="headerlink" title="2. 技术栈不匹配"></a>2. 技术栈不匹配</h3><p><strong>场景：</strong> “实现一个状态管理”</p><p><strong>问题：</strong> AI 可能使用：</p><ul><li>Redux（你的项目很小，用不着）</li><li>Zustand（你想用原生方案）</li><li>MobX（你不熟悉）</li></ul><p>如果不明确指定技术栈和限制条件，AI 往往会选择它”认为”最流行的方案，而不是最适合你的。</p><h3 id="3-细节缺失"><a href="#3-细节缺失" class="headerlink" title="3. 细节缺失"></a>3. 细节缺失</h3><p><strong>场景：</strong> “创建一个英雄卡片组件”</p><p><strong>问题：</strong> AI 创建了一个基础组件，但缺少：</p><ul><li>不同状态的样式（已选中、已禁用、可点击）</li><li>响应式适配</li><li>动画效果</li><li>无障碍支持</li></ul><p>这类问题源于：<strong>你脑海中有完整的视觉效果，但提示词只描述了 20%</strong>。</p><h3 id="4-逻辑漏洞"><a href="#4-逻辑漏洞" class="headerlink" title="4. 逻辑漏洞"></a>4. 逻辑漏洞</h3><p><strong>场景：</strong> “实现 BP 流程”</p><p><strong>问题：</strong> AI 实现了基本流程，但没考虑：</p><ul><li>边界情况（时间用完怎么办？）</li><li>错误处理（非法操作如何拦截？）</li><li>状态一致性（数据可能冲突吗？）</li></ul><p>经验丰富的开发者能预见这些问题，但如果不在提示词中明确，AI 往往会忽略。</p><h2 id="提示词工程的核心原则"><a href="#提示词工程的核心原则" class="headerlink" title="提示词工程的核心原则"></a>提示词工程的核心原则</h2><p>基于这些问题，我总结出了几个核心原则：</p><h3 id="原则一：结构化-gt-自然语言"><a href="#原则一：结构化-gt-自然语言" class="headerlink" title="原则一：结构化 &gt; 自然语言"></a>原则一：结构化 &gt; 自然语言</h3><p><strong>❌ 模糊描述：</strong><br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">帮我做一个 Dota2 的 BP 系统，要有英雄选择、倒计时这些功能</span><br></pre></td></tr></table></figure></p><p><strong>✅ 结构化描述：</strong><br><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><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="section">## 项目需求：Dota2 BP 模拟器</span></span><br><span class="line"></span><br><span class="line"><span class="section">### 技术栈</span></span><br><span class="line"><span class="bullet">- </span>React 19 + TypeScript + Rsbuild</span><br><span class="line"><span class="bullet">- </span>CSS Modules（不用 Tailwind）</span><br><span class="line"><span class="bullet">- </span>Context + useReducer（不用 Redux）</span><br><span class="line"></span><br><span class="line"><span class="section">### 核心功能</span></span><br><span class="line"><span class="bullet">1. </span>英雄网格</span><br><span class="line"><span class="bullet">   - </span>Grid 布局，50px 卡片</span><br><span class="line"><span class="bullet">   - </span>支持搜索和属性筛选</span><br><span class="line"><span class="bullet">   - </span>三种状态：已Ban/已Pick/可选</span><br><span class="line"></span><br><span class="line"><span class="bullet">2. </span>BP 流程</span><br><span class="line"><span class="bullet">   - </span>24 步固定顺序（见附录）</span><br><span class="line"><span class="bullet">   - </span>每回合 30 秒 + 所有回合总计 130 秒备用时间</span><br><span class="line"><span class="bullet">   - </span>时间用完自动随机选择</span><br><span class="line"></span><br><span class="line"><span class="section">### 附录：BP 顺序</span></span><br><span class="line">[详细列出 24 步...]</span><br></pre></td></tr></table></figure></p><p><strong>效果对比：</strong></p><ul><li>模糊描述：需要 5-10 轮对话才能理清需求</li><li>结构化描述：第一次就能生成 80% 可用的代码</li></ul><h3 id="原则二：约束-gt-开放"><a href="#原则二：约束-gt-开放" class="headerlink" title="原则二：约束 &gt; 开放"></a>原则二：约束 &gt; 开放</h3><p>给 AI 太多自由度反而是负担。明确的约束能让它专注在真正重要的部分。</p><p><strong>❌ 开放式：</strong><br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">实现一个状态管理方案</span><br></pre></td></tr></table></figure></p><p><strong>✅ 约束式：</strong><br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><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">使用 React Context + useReducer 实现状态管理：</span><br><span class="line">- 不使用 Redux、Zustand 等第三方库</span><br><span class="line">- 状态结构如下：</span><br><span class="line">  - roomId: string</span><br><span class="line">  - currentStep: number (0-23)</span><br><span class="line">  - bannedHeroes: number[]</span><br><span class="line">  - ...</span><br><span class="line">- 需要的 Actions：BAN_HERO、PICK_HERO、NEXT_STEP...</span><br></pre></td></tr></table></figure></p><p><strong>为什么约束更好：</strong></p><ol><li>减少选择，提高效率</li><li>确保技术栈一致</li><li>代码风格更统一</li></ol><h3 id="原则三：示例-gt-描述"><a href="#原则三：示例-gt-描述" class="headerlink" title="原则三：示例 &gt; 描述"></a>原则三：示例 &gt; 描述</h3><p>一个好的示例胜过千言万语。</p><p><strong>❌ 纯描述：</strong><br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">BP 顺序面板要用两列布局，左边显示天辉的步骤，右边显示夜魇的步骤</span><br></pre></td></tr></table></figure></p><p><strong>✅ 带示例：</strong><br><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line">BP 顺序面板采用两列布局：</span><br><span class="line"></span><br><span class="line">布局示例：</span><br><span class="line">┌─────────────────────┐</span><br><span class="line">│  天辉  │  夜魇       │</span><br><span class="line">├────────┼────────────┤</span><br><span class="line">│ 1 Ban  │ 2 Ban      │</span><br><span class="line">│ 4 Ban  │ 3 Ban      │</span><br><span class="line">│ 7 Ban  │ 5 Ban      │</span><br><span class="line">│ ...    │ 6 Ban      │</span><br><span class="line">└────────┴────────────┘</span><br><span class="line"></span><br><span class="line">实现方式：</span><br><span class="line">const radiantSteps = BP_SEQUENCE.filter(step =&gt; step.team === 'radiant');</span><br><span class="line">const direSteps = BP_SEQUENCE.filter(step =&gt; step.team === 'dire');</span><br><span class="line"></span><br><span class="line">渲染两列：</span><br><span class="line"><span class="xml"><span class="tag">&lt;<span class="name">div</span> <span class="attr">className</span>=<span class="string">&#123;styles.twoColumnLayout&#125;</span>&gt;</span></span></span><br><span class="line">  <span class="xml"><span class="tag">&lt;<span class="name">div</span> <span class="attr">className</span>=<span class="string">&#123;styles.teamColumn&#125;</span>&gt;</span></span>&#123;radiantSteps.map(...)&#125;<span class="xml"><span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line">  <span class="xml"><span class="tag">&lt;<span class="name">div</span> <span class="attr">className</span>=<span class="string">&#123;styles.teamColumn&#125;</span>&gt;</span></span>&#123;direSteps.map(...)&#125;<span class="xml"><span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="xml"><span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br></pre></td></tr></table></figure></p><h3 id="原则四：规则-gt-期望"><a href="#原则四：规则-gt-期望" class="headerlink" title="原则四：规则 &gt; 期望"></a>原则四：规则 &gt; 期望</h3><p>与其告诉 AI 你期望什么结果，不如告诉它必须遵守什么规则。</p><p><strong>❌ 期望式：</strong></p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">倒计时最好不要显示负数</span><br></pre></td></tr></table></figure><p><strong>✅ 规则式：</strong></p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">倒计时规则（必须严格遵守）：</span><br><span class="line">1. 使用 Math.max(0, value) 确保不出现负数</span><br><span class="line">2. 当 remainingTime &lt;&#x3D; 1 时开始处理归零逻辑</span><br><span class="line">3. 归零后保持显示 0，不继续递减</span><br><span class="line">4. 备用时间用完后自动执行随机选择</span><br><span class="line"></span><br><span class="line">关键代码逻辑：</span><br><span class="line">if (remainingTime &lt;&#x3D; 1) &#123;</span><br><span class="line">  if (reserveTime &gt; 0) &#123;</span><br><span class="line">    使用备用时间，倒计时保持为 0</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; else &#123;</span><br><span class="line">  正常递减：Math.max(0, remainingTime - 1)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="原则五：上下文-gt-片段"><a href="#原则五：上下文-gt-片段" class="headerlink" title="原则五：上下文 &gt; 片段"></a>原则五：上下文 &gt; 片段</h3><p>给 AI 足够的上下文，让它理解每个功能在整体中的位置。</p><p><strong>❌ 孤立片段：</strong><br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">实现一个英雄卡片组件</span><br></pre></td></tr></table></figure></p><p><strong>✅ 带上下文：</strong><br><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">## HeroCard 组件</span></span><br><span class="line"></span><br><span class="line"><span class="section">### 在系统中的位置</span></span><br><span class="line"><span class="bullet">- </span>父组件：HeroGrid（英雄网格）</span><br><span class="line"><span class="bullet">- </span>数据来源：HEROES 常量（124 个英雄）</span><br><span class="line"><span class="bullet">- </span>状态管理：通过 Context 获取 bannedHeroes、radiantPicks、direPicks</span><br><span class="line"></span><br><span class="line"><span class="section">### 功能需求</span></span><br><span class="line">显示单个英雄信息，根据 BP 状态展示不同样式：</span><br><span class="line"><span class="bullet">1. </span>未使用：正常显示，可 hover</span><br><span class="line"><span class="bullet">2. </span>已 Ban：灰度 + "已禁用"遮罩</span><br><span class="line"><span class="bullet">3. </span>已 Pick：队伍颜色边框 + 队伍标识</span><br><span class="line"><span class="bullet">4. </span>可选（当前轮次）：亮色边框</span><br><span class="line"><span class="bullet">5. </span>不可选（其他轮次）：半透明 + 禁用点击</span><br><span class="line"></span><br><span class="line"><span class="section">### Props 接口</span></span><br><span class="line">interface HeroCardProps &#123;</span><br><span class="line">  hero: Hero;</span><br><span class="line">  isBanned: boolean;</span><br><span class="line">  isPicked: boolean;</span><br><span class="line">  pickedByTeam: Team | null;</span><br><span class="line">  isSelected: boolean;</span><br><span class="line">  isAvailable: boolean;</span><br><span class="line">  onClick: () =&gt; void;</span><br><span class="line">  onHover: () =&gt; void;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><h2 id="实战：一个完整的提示词模板"><a href="#实战：一个完整的提示词模板" class="headerlink" title="实战：一个完整的提示词模板"></a>实战：一个完整的提示词模板</h2><p>基于以上原则，这是我总结的提示词模板：</p><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="section"># [项目名称]</span></span><br><span class="line"></span><br><span class="line"><span class="section">## 一、项目背景</span></span><br><span class="line"><span class="bullet">- </span>简述项目目的</span><br><span class="line"><span class="bullet">- </span>目标用户</span><br><span class="line"><span class="bullet">- </span>核心价值</span><br><span class="line"></span><br><span class="line"><span class="section">## 二、技术栈（必选）</span></span><br><span class="line"><span class="section">### 必须使用</span></span><br><span class="line"><span class="bullet">- </span>框架：React 19</span><br><span class="line"><span class="bullet">- </span>语言：TypeScript</span><br><span class="line"><span class="bullet">- </span>构建工具：Rsbuild</span><br><span class="line"></span><br><span class="line"><span class="section">### 必须不用</span></span><br><span class="line"><span class="bullet">- </span>不用 Tailwind</span><br><span class="line"><span class="bullet">- </span>不用组件库（MUI、Ant Design 等）</span><br><span class="line"><span class="bullet">- </span>不用第三方状态管理（Redux、Zustand）</span><br><span class="line"></span><br><span class="line"><span class="section">### 样式方案</span></span><br><span class="line"><span class="bullet">- </span>CSS Modules</span><br><span class="line"><span class="bullet">- </span>手写响应式</span><br><span class="line"></span><br><span class="line"><span class="section">## 三、项目结构</span></span><br></pre></td></tr></table></figure><p>src/<br>├── components/<br>│   ├── ComponentA/<br>│   │   ├── ComponentA.tsx<br>│   │   ├── ComponentA.module.css<br>│   │   └── index.ts<br>├── …<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><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><br><span class="line">## 四、核心功能</span><br><span class="line"></span><br><span class="line">### 功能 A</span><br><span class="line">**需求描述：** [清晰的业务需求]</span><br><span class="line">**技术方案：** [指定实现方式]</span><br><span class="line">**关键逻辑：** [伪代码或示例]</span><br><span class="line">**边界情况：** [需要处理的特殊情况]</span><br><span class="line"></span><br><span class="line">### 功能 B</span><br><span class="line">[同上结构]</span><br><span class="line"></span><br><span class="line">## 五、数据结构</span><br><span class="line"></span><br><span class="line">### State 定义</span><br><span class="line">&#96;&#96;&#96;typescript</span><br><span class="line">interface AppState &#123;</span><br><span class="line">  field1: type;  &#x2F;&#x2F; 说明</span><br><span class="line">  field2: type;  &#x2F;&#x2F; 说明</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><h3 id="Actions-列表"><a href="#Actions-列表" class="headerlink" title="Actions 列表"></a>Actions 列表</h3><ul><li>ACTION_1: 描述</li><li>ACTION_2: 描述</li></ul><h2 id="六、UI-规范"><a href="#六、UI-规范" class="headerlink" title="六、UI 规范"></a>六、UI 规范</h2><h3 id="布局"><a href="#布局" class="headerlink" title="布局"></a>布局</h3><ul><li>整体：[描述总体布局]</li><li>区域 A：[尺寸、位置、内容]</li><li>区域 B：[同上]</li></ul><h3 id="颜色"><a href="#颜色" class="headerlink" title="颜色"></a>颜色</h3><ul><li>主色：#xxx</li><li>辅色：#xxx</li><li>警告：#xxx</li></ul><h3 id="尺寸"><a href="#尺寸" class="headerlink" title="尺寸"></a>尺寸</h3><ul><li>组件 A：宽 x 高</li><li>间距：Xpx</li><li>圆角：Xpx</li></ul><h3 id="动画"><a href="#动画" class="headerlink" title="动画"></a>动画</h3><ul><li>Hover：效果描述</li><li>点击：效果描述</li><li>过渡：时间 + 缓动函数</li></ul><h2 id="七、关键实现细节"><a href="#七、关键实现细节" class="headerlink" title="七、关键实现细节"></a>七、关键实现细节</h2><h3 id="问题-1：-描述一个技术难点"><a href="#问题-1：-描述一个技术难点" class="headerlink" title="问题 1：[描述一个技术难点]"></a>问题 1：[描述一个技术难点]</h3><p><strong>错误做法：</strong> [常见错误]<br><strong>正确做法：</strong> [正确方案]<br><strong>关键代码：</strong> [示例代码]</p><h3 id="问题-2：-同上"><a href="#问题-2：-同上" class="headerlink" title="问题 2：[同上]"></a>问题 2：[同上]</h3><h2 id="八、测试清单"><a href="#八、测试清单" class="headerlink" title="八、测试清单"></a>八、测试清单</h2><ul><li style="list-style: none"><input type="checkbox"></input> 功能 A 正常工作</li><li style="list-style: none"><input type="checkbox"></input> 边界情况处理正确</li><li style="list-style: none"><input type="checkbox"></input> 响应式布局正常</li><li style="list-style: none"><input type="checkbox"></input> 无 linter 错误</li></ul><h2 id="九、注意事项"><a href="#九、注意事项" class="headerlink" title="九、注意事项"></a>九、注意事项</h2><ol><li>[特别需要注意的点]</li><li>[容易出错的地方]</li><li>[性能考虑]<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line">## 实践案例：倒计时功能</span><br><span class="line"></span><br><span class="line">让我们用这个模板实现刚才提到的倒计时功能：</span><br><span class="line"></span><br><span class="line">### ❌ 第一版（模糊）</span><br></pre></td></tr></table></figure>实现一个倒计时功能，时间到了要有提示<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line">**AI 生成结果：**</span><br><span class="line">&#96;&#96;&#96;typescript</span><br><span class="line">const [time, setTime] &#x3D; useState(30);</span><br><span class="line"></span><br><span class="line">useEffect(() &#x3D;&gt; &#123;</span><br><span class="line">  const timer &#x3D; setInterval(() &#x3D;&gt; &#123;</span><br><span class="line">    setTime(t &#x3D;&gt; t - 1);</span><br><span class="line">  &#125;, 1000);</span><br><span class="line">  return () &#x3D;&gt; clearInterval(timer);</span><br><span class="line">&#125;, []);</span><br><span class="line"></span><br><span class="line">return &lt;div&gt;&#123;time&#125; 秒&lt;&#x2F;div&gt;;</span><br></pre></td></tr></table></figure></li></ol><p><strong>问题：</strong></p><ul><li>时间会变成负数</li><li>没有备用时间机制</li><li>没有警告样式</li><li>时间到了没有任何操作</li></ul><h3 id="✅-第二版（精确）"><a href="#✅-第二版（精确）" class="headerlink" title="✅ 第二版（精确）"></a>✅ 第二版（精确）</h3><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">## 倒计时功能实现</span></span><br><span class="line"></span><br><span class="line"><span class="section">### 业务需求</span></span><br><span class="line">实现 Dota2 BP 流程的计时器系统：</span><br><span class="line"><span class="bullet">- </span>每回合基础时间：30 秒</span><br><span class="line"><span class="bullet">- </span>每队备用时间：130 秒</span><br><span class="line"><span class="bullet">- </span>基础时间用完后消耗备用时间</span><br><span class="line"><span class="bullet">- </span>备用时间用完后自动随机选择未使用的英雄</span><br><span class="line"></span><br><span class="line"><span class="section">### 技术方案</span></span><br><span class="line">使用自定义 Hook：<span class="code">`useTimer`</span></span><br><span class="line"><span class="bullet">- </span>只在 phase 为 'banning' 或 'picking' 时运行</span><br><span class="line"><span class="bullet">- </span>每秒通过 dispatch 更新状态</span><br><span class="line"><span class="bullet">- </span>返回格式化的时间和警告状态</span><br><span class="line"></span><br><span class="line"><span class="section">### 关键逻辑（伪代码）</span></span><br><span class="line"><span class="code">```typescript</span></span><br><span class="line"><span class="code">每秒执行：</span></span><br><span class="line"><span class="code">  if (remainingTime &lt;= 1) &#123;</span></span><br><span class="line"><span class="code">    // 提前判断，防止显示负数</span></span><br><span class="line"><span class="code">    if (当前队伍备用时间 &gt; 0) &#123;</span></span><br><span class="line"><span class="code">      扣除 1 秒备用时间</span></span><br><span class="line"><span class="code">      主倒计时保持为 0</span></span><br><span class="line"><span class="code">    &#125; else &#123;</span></span><br><span class="line"><span class="code">      从未使用的英雄中随机选一个</span></span><br><span class="line"><span class="code">      执行 BAN 或 PICK</span></span><br><span class="line"><span class="code">      进入下一步</span></span><br><span class="line"><span class="code">    &#125;</span></span><br><span class="line"><span class="code">  &#125; else &#123;</span></span><br><span class="line"><span class="code">    remainingTime = Math.max(0, remainingTime - 1)</span></span><br><span class="line"><span class="code">  &#125;</span></span><br></pre></td></tr></table></figure><h3 id="必须处理的边界情况"><a href="#必须处理的边界情况" class="headerlink" title="必须处理的边界情况"></a>必须处理的边界情况</h3><ol><li><strong>防止负数显示：</strong> 所有时间值用 <code>Math.max(0, value)</code></li><li><strong>防止 -1:-1 bug：</strong> 在 <code>remainingTime &lt;= 1</code> 时就处理，不要等到 0</li><li><strong>随机选择逻辑：</strong> <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="keyword">const</span> usedHeroIds = <span class="keyword">new</span> Set([</span><br><span class="line">  ...bannedHeroes, </span><br><span class="line">  ...radiantPicks, </span><br><span class="line">  ...direPicks</span><br><span class="line">]);</span><br><span class="line"><span class="keyword">const</span> available = HEROES.filter(<span class="function"><span class="params">h</span> =&gt;</span> !usedHeroIds.has(h.id));</span><br><span class="line"><span class="keyword">const</span> random = available[<span class="built_in">Math</span>.floor(<span class="built_in">Math</span>.random() * available.length)];</span><br></pre></td></tr></table></figure></li><li><strong>时间显示：</strong> 格式化为 <code>MM:SS</code>，如 <code>02:30</code></li></ol><h3 id="UI-要求"><a href="#UI-要求" class="headerlink" title="UI 要求"></a>UI 要求</h3><ul><li>最后 10 秒：红色（#FF4444）+ 闪烁动画</li><li>字体：32px Courier New</li><li>警告动画：<figure class="highlight css"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">@keyframes</span> warning &#123;</span><br><span class="line">  0%, 100% &#123; <span class="attribute">opacity</span>: <span class="number">1</span>; &#125;</span><br><span class="line">  50% &#123; <span class="attribute">opacity</span>: <span class="number">0.6</span>; &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></li></ul><h3 id="依赖项"><a href="#依赖项" class="headerlink" title="依赖项"></a>依赖项</h3><p>useEffect 的依赖数组必须包含：</p><ul><li>state.phase</li><li>state.remainingTime</li><li>state.currentTeam</li><li>state.currentAction</li><li>state.bannedHeroes</li><li>state.radiantPicks</li><li>state.direPicks</li><li>state.radiantReserveTime</li><li>state.direReserveTime</li><li>dispatch<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line">**AI 生成结果：** 完整、正确、考虑周全的实现</span><br><span class="line"></span><br><span class="line">## 提示词编写的实用技巧</span><br><span class="line"></span><br><span class="line">### 1. 分层描述</span><br><span class="line"></span><br><span class="line">将需求分为三个层次：</span><br><span class="line">- **What（做什么）：** 业务需求，给产品经理看的</span><br><span class="line">- **How（怎么做）：** 技术方案，给开发看的</span><br><span class="line">- **Why（为什么）：** 设计决策，给 AI 理解上下文</span><br><span class="line"></span><br><span class="line">### 2. 先整体后局部</span><br><span class="line"></span><br><span class="line">不要上来就让 AI 实现某个函数，而是：</span><br><span class="line">1. 先描述整体项目结构</span><br><span class="line">2. 再描述模块关系</span><br><span class="line">3. 最后到具体实现</span><br><span class="line"></span><br><span class="line">这样 AI 能理解代码在系统中的位置。</span><br><span class="line"></span><br><span class="line">### 3. 用反例</span><br><span class="line"></span><br><span class="line">告诉 AI 什么是错的，往往比告诉它什么是对的更有效：</span><br><span class="line"></span><br><span class="line">&#96;&#96;&#96;markdown</span><br><span class="line">### 常见错误（不要这样做）</span><br><span class="line">❌ 直接 &#96;remainingTime - 1&#96; 可能导致负数</span><br><span class="line">❌ 在 &#96;remainingTime &#x3D;&#x3D;&#x3D; 0&#96; 时处理太晚</span><br><span class="line">❌ 忘记清理 setInterval</span><br><span class="line"></span><br><span class="line">### 正确做法</span><br><span class="line">✅ 使用 &#96;Math.max(0, remainingTime - 1)&#96;</span><br><span class="line">✅ 在 &#96;remainingTime &lt;&#x3D; 1&#96; 时开始处理</span><br><span class="line">✅ useEffect return 中清理定时器</span><br></pre></td></tr></table></figure></li></ul><h3 id="4-提供检查清单"><a href="#4-提供检查清单" class="headerlink" title="4. 提供检查清单"></a>4. 提供检查清单</h3><p>在提示词最后加上验收标准：</p><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">## 完成标准</span></span><br><span class="line">实现后必须满足：</span><br><span class="line"><span class="bullet">- </span>[ ] 倒计时不会显示负数</span><br><span class="line"><span class="bullet">- </span>[ ] 备用时间用完后自动选择</span><br><span class="line"><span class="bullet">- </span>[ ] 最后 10 秒红色警告</span><br><span class="line"><span class="bullet">- </span>[ ] 无 TypeScript 错误</span><br><span class="line"><span class="bullet">- </span>[ ] 无 ESLint 警告</span><br></pre></td></tr></table></figure><h3 id="5-迭代式提示词"><a href="#5-迭代式提示词" class="headerlink" title="5. 迭代式提示词"></a>5. 迭代式提示词</h3><p>不要期望一次就完美。我的做法是：</p><p><strong>第一次：</strong> 给框架式提示词</p><ul><li>项目结构</li><li>核心功能列表</li><li>技术栈</li></ul><p><strong>第二次：</strong> 看生成结果，补充细节</p><ul><li>“BP 顺序要改成两列”</li><li>“倒计时有 bug，要修复”</li></ul><p><strong>第三次：</strong> 优化和调整</p><ul><li>“英雄卡片再小一点”</li><li>“布局要更紧凑”</li></ul><p>每次迭代，提示词都会更精确。</p><h2 id="常见场景的提示词模式"><a href="#常见场景的提示词模式" class="headerlink" title="常见场景的提示词模式"></a>常见场景的提示词模式</h2><h3 id="场景-1：实现一个新组件"><a href="#场景-1：实现一个新组件" class="headerlink" title="场景 1：实现一个新组件"></a>场景 1：实现一个新组件</h3><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">## [ComponentName] 组件</span></span><br><span class="line"></span><br><span class="line"><span class="section">### 在系统中的位置</span></span><br><span class="line"><span class="bullet">- </span>父组件：[Parent]</span><br><span class="line"><span class="bullet">- </span>子组件：[Children]</span><br><span class="line"><span class="bullet">- </span>数据来源：[Props/Context/API]</span><br><span class="line"></span><br><span class="line"><span class="section">### Props 接口</span></span><br><span class="line"><span class="code">```typescript</span></span><br><span class="line"><span class="code">interface Props &#123;</span></span><br><span class="line"><span class="code">  // 带注释</span></span><br><span class="line"><span class="code">&#125;</span></span><br></pre></td></tr></table></figure><h3 id="功能清单"><a href="#功能清单" class="headerlink" title="功能清单"></a>功能清单</h3><ol><li>[功能 1]</li><li>[功能 2]</li></ol><h3 id="样式要求"><a href="#样式要求" class="headerlink" title="样式要求"></a>样式要求</h3><ul><li>布局：[描述]</li><li>尺寸：[宽高]</li><li>颜色：[色值]</li><li>动画：[效果]</li></ul><h3 id="状态变化"><a href="#状态变化" class="headerlink" title="状态变化"></a>状态变化</h3><ul><li>状态 A：[样式描述]</li><li>状态 B：[样式描述]<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><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><br><span class="line">### 场景 2：修复一个 Bug</span><br><span class="line"></span><br><span class="line">&#96;&#96;&#96;markdown</span><br><span class="line">## Bug 描述</span><br><span class="line">**现象：** [详细描述问题]</span><br><span class="line">**复现步骤：** [1, 2, 3]</span><br><span class="line">**期望行为：** [应该怎样]</span><br><span class="line"></span><br><span class="line">## 问题分析</span><br><span class="line">**原因：** [技术原因]</span><br><span class="line">**错误代码：**</span><br><span class="line">&#96;&#96;&#96;typescript</span><br><span class="line">&#x2F;&#x2F; 有问题的代码</span><br></pre></td></tr></table></figure></li></ul><h2 id="修复方案"><a href="#修复方案" class="headerlink" title="修复方案"></a>修复方案</h2><p><strong>正确做法：</strong><br><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 修复后的代码</span></span><br></pre></td></tr></table></figure></p><p><strong>为什么这样改：</strong> [解释]</p><h2 id="需要同时修改的地方"><a href="#需要同时修改的地方" class="headerlink" title="需要同时修改的地方"></a>需要同时修改的地方</h2><ul><li style="list-style: none"><input type="checkbox"></input> 文件 A 的 X 函数</li><li style="list-style: none"><input type="checkbox"></input> 文件 B 的 Y 函数<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line">### 场景 3：优化性能</span><br><span class="line"></span><br><span class="line">&#96;&#96;&#96;markdown</span><br><span class="line">## 性能问题</span><br><span class="line"></span><br><span class="line">### 问题现象</span><br><span class="line">- [卡顿&#x2F;内存泄漏&#x2F;重渲染...]</span><br><span class="line">- 发生场景：[何时发生]</span><br><span class="line"></span><br><span class="line">### 性能指标</span><br><span class="line">- 当前：[FPS&#x2F;加载时间&#x2F;内存]</span><br><span class="line">- 目标：[期望指标]</span><br><span class="line"></span><br><span class="line">### 优化方案</span><br><span class="line">1. **方案 A：** [描述]</span><br><span class="line">   - 优点：[...]</span><br><span class="line">   - 缺点：[...]</span><br><span class="line">   - 优先级：高&#x2F;中&#x2F;低</span><br><span class="line"></span><br><span class="line">2. **方案 B：** [同上]</span><br><span class="line"></span><br><span class="line">### 实施方案</span><br><span class="line">选择方案 A，具体实现：</span><br><span class="line">[详细步骤]</span><br></pre></td></tr></table></figure></li></ul><h2 id="提示词工程的反模式"><a href="#提示词工程的反模式" class="headerlink" title="提示词工程的反模式"></a>提示词工程的反模式</h2><h3 id="反模式-1：过度依赖-AI"><a href="#反模式-1：过度依赖-AI" class="headerlink" title="反模式 1：过度依赖 AI"></a>反模式 1：过度依赖 AI</h3><p><strong>错误心态：</strong><br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&quot;反正 AI 会帮我想，我就简单说说&quot;</span><br></pre></td></tr></table></figure></p><p><strong>问题：</strong></p><ul><li>AI 不是你肚子里的蛔虫</li><li>模糊的输入 → 模糊的输出</li><li>反复返工浪费时间</li></ul><p><strong>正确心态：</strong><br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&quot;我已经想清楚要什么，只是让 AI 帮我写代码&quot;</span><br></pre></td></tr></table></figure></p><h3 id="反模式-2：期望-AI-理解隐含需求"><a href="#反模式-2：期望-AI-理解隐含需求" class="headerlink" title="反模式 2：期望 AI 理解隐含需求"></a>反模式 2：期望 AI 理解隐含需求</h3><p><strong>错误示例：</strong><br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&quot;实现一个专业的 BP 系统&quot;</span><br></pre></td></tr></table></figure></p><p>AI 不知道”专业”是什么标准。</p><p><strong>正确做法：</strong><br>明确定义”专业”：</p><ul><li>布局紧凑，一屏展示</li><li>动画流畅</li><li>响应式适配</li><li>无障碍支持</li><li>…</li></ul><h3 id="反模式-3：忽视技术约束"><a href="#反模式-3：忽视技术约束" class="headerlink" title="反模式 3：忽视技术约束"></a>反模式 3：忽视技术约束</h3><p><strong>错误示例：</strong><br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&quot;随便用什么技术栈都行&quot;</span><br></pre></td></tr></table></figure></p><p><strong>问题：</strong></p><ul><li>AI 可能选择你不熟悉的技术</li><li>代码风格不统一</li><li>维护成本高</li></ul><p><strong>正确做法：</strong><br>明确技术栈和限制。</p><h3 id="反模式-4：一次想要所有功能"><a href="#反模式-4：一次想要所有功能" class="headerlink" title="反模式 4：一次想要所有功能"></a>反模式 4：一次想要所有功能</h3><p><strong>错误示例：</strong><br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&quot;实现完整的 Dota2 BP 系统，包括前端、后端、数据库、部署...&quot;</span><br></pre></td></tr></table></figure></p><p><strong>问题：</strong></p><ul><li>AI 容易顾此失彼</li><li>代码质量下降</li><li>整合困难</li></ul><p><strong>正确做法：</strong><br>分模块、分阶段实现。</p><h2 id="工具和辅助"><a href="#工具和辅助" class="headerlink" title="工具和辅助"></a>工具和辅助</h2><h3 id="1-提示词模板库"><a href="#1-提示词模板库" class="headerlink" title="1. 提示词模板库"></a>1. 提示词模板库</h3><p>我会维护一个自己的模板库：<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><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">templates&#x2F;</span><br><span class="line">├── component.md        # 组件实现模板</span><br><span class="line">├── feature.md          # 功能实现模板</span><br><span class="line">├── bugfix.md           # Bug 修复模板</span><br><span class="line">├── refactor.md         # 重构模板</span><br><span class="line">└── optimization.md     # 优化模板</span><br></pre></td></tr></table></figure></p><p>每次开始新任务，复制对应模板填空即可。</p><h3 id="2-需求清单"><a href="#2-需求清单" class="headerlink" title="2. 需求清单"></a>2. 需求清单</h3><p>项目开始前，先列清单：<br><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><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="section">## 技术栈确认</span></span><br><span class="line"><span class="bullet">- </span>[ ] 框架和版本</span><br><span class="line"><span class="bullet">- </span>[ ] 状态管理方案</span><br><span class="line"><span class="bullet">- </span>[ ] 样式方案</span><br><span class="line"><span class="bullet">- </span>[ ] 构建工具</span><br><span class="line"></span><br><span class="line"><span class="section">## 功能清单</span></span><br><span class="line"><span class="bullet">- </span>[ ] 功能 A</span><br><span class="line"><span class="bullet">- </span>[ ] 功能 B</span><br><span class="line"></span><br><span class="line"><span class="section">## UI 规范</span></span><br><span class="line"><span class="bullet">- </span>[ ] 颜色定义</span><br><span class="line"><span class="bullet">- </span>[ ] 尺寸标准</span><br><span class="line"><span class="bullet">- </span>[ ] 动画效果</span><br><span class="line"></span><br><span class="line"><span class="section">## 技术决策</span></span><br><span class="line"><span class="bullet">- </span>[ ] 为什么用 X 不用 Y</span><br><span class="line"><span class="bullet">- </span>[ ] 核心难点如何解决</span><br></pre></td></tr></table></figure></p><h3 id="3-代码审查清单"><a href="#3-代码审查清单" class="headerlink" title="3. 代码审查清单"></a>3. 代码审查清单</h3><p>AI 生成代码后的检查项：<br><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="bullet">- </span>[ ] 功能是否完整</span><br><span class="line"><span class="bullet">- </span>[ ] 边界情况是否处理</span><br><span class="line"><span class="bullet">- </span>[ ] 类型定义是否准确</span><br><span class="line"><span class="bullet">- </span>[ ] 性能是否有问题</span><br><span class="line"><span class="bullet">- </span>[ ] 代码风格是否一致</span><br><span class="line"><span class="bullet">- </span>[ ] 注释是否清晰</span><br></pre></td></tr></table></figure></p><h2 id="结语"><a href="#结语" class="headerlink" title="结语"></a>结语</h2><p>回到文章开头的问题：如何让 AI 生成完美的代码？</p><p>答案是：<strong>完美的代码来自完美的需求</strong>。</p><p>AI 是工具，提示词是接口。就像我们调用 API 一样，参数传得清楚，返回结果才会准确。</p><p>如果你也有 AI 辅助编程的经验，欢迎一起分享~</p><hr>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;最近用 AI 实现了一个 Dota2 队长模式 BP 模拟器的前端项目。整个过程中，很明显能发现提示词的质量直接决定了生成代码的可用性，一个模糊的需求可能需要反复修改十几次，而一个精确的提示词往往能一次性生成 90% 可用的代码。结合这次实践，分享一些心得，抛砖引玉。&lt;/p
      
    
    </summary>
    
      <category term="人工智能" scheme="https://www.thjiang.com/categories/%E4%BA%BA%E5%B7%A5%E6%99%BA%E8%83%BD/"/>
    
    
      <category term="人工智能" scheme="https://www.thjiang.com/tags/%E4%BA%BA%E5%B7%A5%E6%99%BA%E8%83%BD/"/>
    
  </entry>
  
  <entry>
    <title>那些年，我们监控过的前端性能指标</title>
    <link href="https://www.thjiang.com/2023/10/06/%E9%82%A3%E4%BA%9B%E5%B9%B4%EF%BC%8C%E6%88%91%E4%BB%AC%E7%9B%91%E6%8E%A7%E8%BF%87%E7%9A%84%E5%89%8D%E7%AB%AF%E6%80%A7%E8%83%BD%E6%8C%87%E6%A0%87/"/>
    <id>https://www.thjiang.com/2023/10/06/那些年，我们监控过的前端性能指标/</id>
    <published>2023-10-06T12:20:53.000Z</published>
    <updated>2023-11-09T05:51:26.133Z</updated>
    
    <content type="html"><![CDATA[<p>性能在前端领域的重要性已经是老生常谈了，比如早在 2012 年，亚马逊就发布过一个研究结果：页面加载速度一旦下降一秒，每年就会损失 16 亿美元的销售额。那这个一秒又是怎么统计的呢？</p><p>同样是在 2012 年， Web 性能工作组针对页面加载场景制定了一个加载过程模型，用来衡量页面加载各个阶段的耗时情况，相信这张图大家应该都见过：</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://c.icewish.top/const/blog/images/monitor/ttfb.jpg!w960" alt="W3C 性能模型" title="">                </div>                <div class="image-caption">W3C 性能模型</div>            </figure><p>但是随着时代的发展，图中的指标已经不太能准确的反映性能表现了，比如在一个 SPA 应用中，页面容器的 DOM 结构已经生成了，但实际上当前页面还是一个白屏的空壳、又比如页面中绝大部分内容已经渲染好了，但是有一张特别大的图片还在加载中…</p><p>针对这些问题，浏览器厂商、web 工作组等都提出了一些关键指标，比如谷歌提出了 4 个衡量用户体验的新指标（LCP、CLS、FID、INP），这 4 个指标将会影响谷歌搜索引擎的排名，Chrome 开发团队也提出了 4 个指标（FP、FCP、LCP、CLS），W3C 性能工作组则认为 LCP、CLS 和 TBT 是最值得关注的。</p><p>在现代前端性能监控系统中，我们也经常会见到它们，我们对页面性能的衡量，基本就是通过对这些指标的收集和计算得来的。有了这些指标，我们也可以有效的还原出页面加载的瀑布图、火焰图等，从而更有针对性的做出优化。</p><p>例如，这是一张火山引擎前端监控系统的截图：</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://c.icewish.top/const/blog/images/monitor/2.png!w960" alt="火山引擎前端监控" title="">                </div>                <div class="image-caption">火山引擎前端监控</div>            </figure><p>在浏览器的 devtools 中，我们也可以通过 performance 来统计一次加载过程中的一些指标：</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://c.icewish.top/const/blog/images/monitor/1.jpg!w960" alt="Chrome Devtools" title="">                </div>                <div class="image-caption">Chrome Devtools</div>            </figure><p>下面我们就来一起看一下这些指标的具体含义：</p><h2 id="渲染相关"><a href="#渲染相关" class="headerlink" title="渲染相关"></a>渲染相关</h2><h3 id="TTFB-Time-to-First-Byte"><a href="#TTFB-Time-to-First-Byte" class="headerlink" title="TTFB(Time to First Byte )"></a>TTFB(Time to First Byte )</h3><p>TTFB 是资源请求与响应的第一个字节开始到达之间的时间，如下图所示，TTFB 是 startTime 和 responseStart 之间经过的时间。</p><p>我们可以使用 Google 提供的 <a href="https://github.com/GoogleChrome/web-vitals#api" target="_blank" rel="noopener">web-vitals</a> 这个库来测量 TTFB 时间。同样，后文中的 CLS、FCP、FID、INP、LCP 时间也都可以通过这个库来获取。使用方式可以参考如下代码：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; onTTFB, onCLS, onFID, onLCP &#125; <span class="keyword">from</span> <span class="string">"web-vitals"</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// Measure and log TTFB as soon as it's available.</span></span><br><span class="line">onTTFB(<span class="built_in">console</span>.log);</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">sendToAnalytics</span>(<span class="params">metric</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">const</span> body = <span class="built_in">JSON</span>.stringify(metric);</span><br><span class="line">  <span class="comment">// Use `navigator.sendBeacon()` if available, falling back to `fetch()`.</span></span><br><span class="line">  (navigator.sendBeacon &amp;&amp; navigator.sendBeacon(<span class="string">"/analytics"</span>, body)) ||</span><br><span class="line">    fetch(<span class="string">"/analytics"</span>, &#123; body, <span class="attr">method</span>: <span class="string">"POST"</span>, <span class="attr">keepalive</span>: <span class="literal">true</span> &#125;);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">onCLS(sendToAnalytics);</span><br><span class="line">onFID(sendToAnalytics);</span><br><span class="line">onLCP(sendToAnalytics);</span><br></pre></td></tr></table></figure><p>如果希望能自己通过底层 Web API 直接衡量（可能兼容性会弱于<code>web-vitals</code>），可以这样实现：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">new</span> PerformanceObserver(<span class="function">(<span class="params">entryList</span>) =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">const</span> [pageNav] = entryList.getEntriesByType(<span class="string">"navigation"</span>);</span><br><span class="line"></span><br><span class="line">  <span class="built_in">console</span>.log(<span class="string">`TTFB: <span class="subst">$&#123;pageNav.responseStart&#125;</span>`</span>);</span><br><span class="line">&#125;).observe(&#123;</span><br><span class="line">  type: <span class="string">"navigation"</span>,</span><br><span class="line">  buffered: <span class="literal">true</span>,</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><h3 id="FP-First-Paint"><a href="#FP-First-Paint" class="headerlink" title="FP(First Paint)"></a>FP(First Paint)</h3><p>FP 代表浏览器第一次向屏幕传输像素的时间，也就是页面在屏幕上首次发生视觉变化的时间。在 FP 时间点之前，用户看到的都是没有任何内容的白色屏幕。在现代前端应用中，如前文所述， FP 已经不能准确反映页面性能了，目前 chrome、Lighthouse 等都已经不再统计 FP 了。</p><h4 id="获取方式"><a href="#获取方式" class="headerlink" title="获取方式"></a>获取方式</h4><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">performance</span><br><span class="line">  .getEntriesByType(<span class="string">"paint"</span>)</span><br><span class="line">  .filter(<span class="function">(<span class="params">item</span>) =&gt;</span> item.name === <span class="string">"first-paint"</span>)[<span class="number">0</span>].startTime;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 1376.300000011921</span></span><br></pre></td></tr></table></figure><h3 id="FCP-First-Contentful-Paint"><a href="#FCP-First-Contentful-Paint" class="headerlink" title="FCP(First Contentful Paint)"></a>FCP(First Contentful Paint)</h3><p>FCP 用于测量从网页开始加载到网页任何一部分内容呈现在屏幕上的时间。“内容”是指文本、图片（包括背景图片）、svg 元素或非白色 canvas 元素。</p><p>如下图中，第二帧的时间，就是 FCP 时间。</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://c.icewish.top/const/blog/images/monitor/fcp.jpg!w960" alt="FCP" title="">                </div>                <div class="image-caption">FCP</div>            </figure><h4 id="获取方式-1"><a href="#获取方式-1" class="headerlink" title="获取方式"></a>获取方式</h4><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">performance</span><br><span class="line">  .getEntriesByType(<span class="string">"paint"</span>)</span><br><span class="line">  .filter(<span class="function">(<span class="params">item</span>) =&gt;</span> item.name === <span class="string">"first-contentful-paint"</span>)[<span class="number">0</span>].startTime;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 1376.300000011921</span></span><br></pre></td></tr></table></figure><h3 id="FMP-First-Meaningful-Paint"><a href="#FMP-First-Meaningful-Paint" class="headerlink" title="FMP(First Meaningful Paint)"></a>FMP(First Meaningful Paint)</h3><p>FMP 用来测量页面的主要内容何时对用户可见。由于对“主要内容”的判定很复杂，所以大部分情况下，FMP 是通过一些猜测算法得到的。具体实现方式可以参考 <a href="https://docs.google.com/document/d/1BR94tJdZLsin5poeet0XoTW60M0SjvOJQttKT-JK8HI/view#heading=h.tdqghbi9ia5d" target="_blank" rel="noopener">这篇论文</a>。</p><p>FMP 和 FCP 最主要的区别在于 FMP 测量的是“有意义的内容”绘制的时间，但是这个指标的测量依赖于浏览器的实现细节，同时会受到多种因素影响引起剧烈波动，目前已经基本被弃用了，取而代之的是 <code>LCP</code>。</p><h3 id="SI"><a href="#SI" class="headerlink" title="SI"></a>SI</h3><p>SI 是 Lighthouse 提出的一个指标，用于衡量页面加载期间内容视觉显示的速度。<br>SI 是通过 <a href="https://github.com/paulirish/speedline" target="_blank" rel="noopener">speedline</a> 计算得到的。</p><h3 id="LCP-Largest-Contentful-Paint"><a href="#LCP-Largest-Contentful-Paint" class="headerlink" title="LCP(Largest Contentful Paint)"></a>LCP(Largest Contentful Paint)</h3><p>由于 FMP 和 SI 的实现非常复杂，同时难以解释，而且往往是错误的，这意味着它们仍然无法确定页面主要内容的加载时间。根据 W3C Web 性能工作组中的讨论和 Google 的研究，我们发现，要衡量网页主要内容的加载时间，更为准确的方法是查看最大元素的呈现时间，LCP 应运而生。<br>LCP 是指视口内可见的最大图片或文本块的呈现时间，包括以下元素：</p><blockquote><ul><li><code>img</code> 元素</li><li><code>svg</code> 元素内的 <code>image</code> 元素</li><li>包含海报图片的 <code>video</code> 元素（系统会使用海报图片加载时间）</li><li>一个元素，带有通过 url() 函数（而不是 CSS 渐变）加载的背景图片</li><li>包含文本节点或其他内嵌级别文本元素的子项的块级元素。</li><li>为自动播放 <code>video</code> 元素而绘制的第一帧（截至 <a href="https://chromium.googlesource.com/chromium/src/+/refs/heads/main/docs/speed/metrics_changelog/lcp.md" target="_blank" rel="noopener">2023 年 8 月</a>）</li><li>动画图片格式（例如 GIF 动画）的第一帧（截至 <a href="https://chromium.googlesource.com/chromium/src/+/refs/heads/main/docs/speed/metrics_changelog/lcp.md" target="_blank" rel="noopener">2023 年 8 月</a>）</li></ul></blockquote><p>同时，浏览器还会通过自己的启发式算法来排除一些元素，比如在 Chromium 中，下列元素会被排除：</p><blockquote><ul><li>不透明度为 0 且对用户不可见的元素</li><li>覆盖整个视口的元素，很可能被视为背景而非内容</li><li>占位符图片或其他低熵的图片，可能无法反映网页真实内容</li></ul></blockquote><p>下图可以参考 LCP 时间：</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://c.icewish.top/const/blog/images/monitor/lcp.jpg!w960" alt="LCP" title="">                </div>                <div class="image-caption">LCP</div>            </figure><h4 id="获取方式-2"><a href="#获取方式-2" class="headerlink" title="获取方式"></a>获取方式</h4><p>可以如前文所述，使用 <a href="https://github.com/GoogleChrome/web-vitals#api" target="_blank" rel="noopener">web-vitals</a> 获取。</p><p>如果希望自己获取，可以这样使用 <a href="https://w3c.github.io/largest-contentful-paint/" target="_blank" rel="noopener">Largest Contentful Paint API</a></p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">new</span> PerformanceObserver(<span class="function">(<span class="params">entryList</span>) =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">for</span> (<span class="keyword">const</span> entry <span class="keyword">of</span> entryList.getEntries()) &#123;</span><br><span class="line">    <span class="built_in">console</span>.log(<span class="string">"LCP candidate:"</span>, entry.startTime, entry);</span><br><span class="line">  &#125;</span><br><span class="line">&#125;).observe(&#123; <span class="attr">type</span>: <span class="string">"largest-contentful-paint"</span>, <span class="attr">buffered</span>: <span class="literal">true</span> &#125;);</span><br></pre></td></tr></table></figure><p>但是实际应用中 LCP 的测量远比这里复杂，详细信息可以参考<a href="https://web.dev/articles/lcp?hl=zh-cn#measure_lcp_in_javascript" target="_blank" rel="noopener">这篇文章</a>。</p><h2 id="交互相关"><a href="#交互相关" class="headerlink" title="交互相关"></a>交互相关</h2><h3 id="TTI-Time-to-Interactive"><a href="#TTI-Time-to-Interactive" class="headerlink" title="TTI (Time to Interactive)"></a>TTI (Time to Interactive)</h3><p>TTI 是指从网页开始加载到主要子资源已加载且能够快速可靠地响应用户输入的时间。</p><p>TTI 的计算方式如下：</p><ol><li>从 First Contentful Paint (FCP) 开始。</li><li>向前搜索一个至少 5 秒的静默期，其中静默期的定义为：没有 Long Task，以及不超过两个正在进行的网络 GET 请求。</li><li>向后搜索静默窗口之前的最后一个长任务，如果找不到 Long Task，则在 FCP 处停止搜索。</li><li>TTI 是静默期之前的最后一个长任务的结束时间（如果未找到 Long Task，则与 FCP 相同）。</li></ol><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://c.icewish.top/const/blog/images/monitor/tti.jpg!w960" alt="TTI" title="">                </div>                <div class="image-caption">TTI</div>            </figure><h3 id="TBT-Total-Blocking-Time"><a href="#TBT-Total-Blocking-Time" class="headerlink" title="TBT(Total Blocking Time)"></a>TBT(Total Blocking Time)</h3><p>TBT 表示从 FCP 到 TTI 之间，所有 Long Task 的阻塞时间之和。</p><p>每当存在 Long Task 时，主线程就会被视为“阻塞”。我们称主线程“阻塞”，因为浏览器无法中断正在进行的任务。因此，如果用户在执行耗时较长的任务时与网页互动，浏览器必须等待任务完成才能响应。</p><p>如果任务的时间足够长（超过 50 毫秒的任何任务），用户很可能会注意到延迟，并认为页面运行缓慢或卡顿。</p><p>TBT 就是在 FCP 和 TTI 之间发生的每个耗时较长任务的阻塞时间之和。</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://c.icewish.top/const/blog/images/monitor/tbt.jpg!w960" alt="TBT" title="">                </div>                <div class="image-caption">TBT</div>            </figure><h3 id="FID-First-Input-Delay"><a href="#FID-First-Input-Delay" class="headerlink" title="FID (First Input Delay)"></a>FID (First Input Delay)</h3><p>FID 测量的是从用户第一次与网页互动（点击链接、点按按钮等操作）到浏览器实际能够开始处理事件处理脚本以响应该互动的时间。TTI 可以告诉我们网页什么时候可以开始流畅地响应用户的交互，但是如果用户在 TTI 的时间内，没有与网页产生交互，那么 TTI 其实是影响不到用户的，FID 则体现了真实的用户交互反馈。</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://c.icewish.top/const/blog/images/monitor/fid.jpg!w960" alt="FID" title="">                </div>                <div class="image-caption">FID</div>            </figure><h4 id="获取方式-3"><a href="#获取方式-3" class="headerlink" title="获取方式"></a>获取方式</h4><p>同样，<a href="https://github.com/GoogleChrome/web-vitals#api" target="_blank" rel="noopener">web-vitals</a> 也可以直接提供 FID。</p><p>我们也可以这样记录一条 <code>first-input</code> 数据：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">new</span> PerformanceObserver(<span class="function">(<span class="params">entryList</span>) =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">for</span> (<span class="keyword">const</span> entry <span class="keyword">of</span> entryList.getEntries()) &#123;</span><br><span class="line">    <span class="keyword">const</span> delay = entry.processingStart - entry.startTime;</span><br><span class="line">    <span class="built_in">console</span>.log(<span class="string">'FID candidate:'</span>, delay, entry);</span><br><span class="line">  &#125;</span><br><span class="line">&#125;).observe(&#123;<span class="attr">type</span>: <span class="string">'first-input'</span>, <span class="attr">buffered</span>: <span class="literal">true</span>&#125;);</span><br></pre></td></tr></table></figure><h2 id="其他"><a href="#其他" class="headerlink" title="其他"></a>其他</h2><h3 id="CLS-Cumulative-Layout-Shift"><a href="#CLS-Cumulative-Layout-Shift" class="headerlink" title="CLS (Cumulative Layout Shift)"></a>CLS (Cumulative Layout Shift)</h3><p>CLS 衡量的是页面的整个生命周期内发生的每次意外布局偏移的最大突发性布局偏移量。</p><p>这句话可能有点难以理解，让我们来看一下下面这个场景：<br>我们正在一个购物网站中浏览，准备点击返回按钮，此时页面顶部忽然加载出了一条广告，导致页面布局发生了变化，造成误点了提交按钮，遇到这种情况，用户的心态应该是爆炸的。</p><video width="100%" controls><br>    <source src="https://c.icewish.top/const/blog/images/monitor/layout-instability2.webm" type="video/mp4"><br></video><p>CLS 可以帮我们统计出我们的页面上类似的布局偏移量，以便针对优化。</p><p>布局偏移由 <a href="https://github.com/WICG/layout-instability" target="_blank" rel="noopener">Layout Instability API</a> 定义，该 API 会在视口内可见的元素在两帧之间更改其起始位置（例如，在默认写入模式下的顶部和左侧位置）时，报告 <code>layout-shift</code> 条目。此类元素被视为不稳定的元素。<br>仅当现有元素更改其起始位置时，才会发生布局偏移。如果向 DOM 添加新元素或现有元素更改了尺寸，则不计为布局偏移，只要此类更改不会导致其他可见元素更改其起始位置即可。</p><h4 id="获取方式-4"><a href="#获取方式-4" class="headerlink" title="获取方式"></a>获取方式</h4><p>和前面的几个指标一样，CLS 也可以使用 <a href="https://github.com/GoogleChrome/web-vitals#api" target="_blank" rel="noopener">web-vitals</a> 获取。</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><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">// 最终的 CLS 数据应该由贯穿网页生命周期的多条数据计算得出</span></span><br><span class="line"><span class="keyword">new</span> PerformanceObserver(<span class="function">(<span class="params">entryList</span>) =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">for</span> (<span class="keyword">const</span> entry <span class="keyword">of</span> entryList.getEntries()) &#123;</span><br><span class="line">    <span class="built_in">console</span>.log(<span class="string">"Layout shift:"</span>, entry);</span><br><span class="line">  &#125;</span><br><span class="line">&#125;).observe(&#123; <span class="attr">type</span>: <span class="string">"layout-shift"</span>, <span class="attr">buffered</span>: <span class="literal">true</span> &#125;);</span><br></pre></td></tr></table></figure><h3 id="Long-Task"><a href="#Long-Task" class="headerlink" title="Long Task"></a>Long Task</h3><p><a href="https://w3c.github.io/longtasks/" target="_blank" rel="noopener">Long Task</a> 是指在主线程上运行达 50 毫秒或以上的任务，我们通过对 Long Task 的分析，可以有效的找到具体哪些任务导致了页面卡顿。<br>Long Task 可以通过 PerformanceObserver 获取。</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> observer = <span class="keyword">new</span> PerformanceObserver(<span class="function">(<span class="params">list</span>) =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">for</span> (<span class="keyword">const</span> entry <span class="keyword">of</span> list.getEntries()) &#123;</span><br><span class="line">    <span class="built_in">console</span>.log(<span class="string">"longtask candidate: "</span>, entry.startTime);</span><br><span class="line">  &#125;</span><br><span class="line">&#125;);</span><br><span class="line"></span><br><span class="line">observer.observe(&#123; <span class="attr">entryTypes</span>: [<span class="string">"longtask"</span>] &#125;);</span><br></pre></td></tr></table></figure><h3 id="DNS-lookup"><a href="#DNS-lookup" class="headerlink" title="DNS lookup"></a>DNS lookup</h3><p>DNS 解析耗时，可以使用 <code>performance</code> 中的 <code>domainLookupEnd</code> - <code>domainLookupStart</code> 获取。</p><h2 id="参考链接"><a href="#参考链接" class="headerlink" title="参考链接"></a>参考链接</h2><p><a href="https://web.dev/articles/vitals" target="_blank" rel="noopener">https://web.dev/articles/vitals</a><br><a href="https://web.dev/explore/learn-core-web-vitals" target="_blank" rel="noopener">https://web.dev/explore/learn-core-web-vitals</a><br><a href="https://developer.chrome.com/docs/lighthouse/performance/" target="_blank" rel="noopener">https://developer.chrome.com/docs/lighthouse/performance/</a><br><a href="https://mp.weixin.qq.com/s/TRY2mEMl4rZz3442SzC1EA?poc_token=HGPaSGWjW1CXHu3f8iyJPZiXY5QPKzU4e7Wowbf8" target="_blank" rel="noopener">https://mp.weixin.qq.com/s/TRY2mEMl4rZz3442SzC1EA?poc_token=HGPaSGWjW1CXHu3f8iyJPZiXY5QPKzU4e7Wowbf8</a><br><a href="https://github.com/berwin/Blog/issues/46" target="_blank" rel="noopener">https://github.com/berwin/Blog/issues/46</a></p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;性能在前端领域的重要性已经是老生常谈了，比如早在 2012 年，亚马逊就发布过一个研究结果：页面加载速度一旦下降一秒，每年就会损失 16 亿美元的销售额。那这个一秒又是怎么统计的呢？&lt;/p&gt;
&lt;p&gt;同样是在 2012 年， Web 性能工作组针对页面加载场景制定了一个加载过
      
    
    </summary>
    
      <category term="前端工程" scheme="https://www.thjiang.com/categories/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B/"/>
    
    
      <category term="前端工程" scheme="https://www.thjiang.com/tags/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B/"/>
    
      <category term="性能优化" scheme="https://www.thjiang.com/tags/%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/"/>
    
  </entry>
  
  <entry>
    <title>在 Nginx 1.25+ 中开启 HTTP/3</title>
    <link href="https://www.thjiang.com/2023/06/16/%E5%9C%A8-Nginx-1-25-%E4%B8%AD%E5%BC%80%E5%90%AF-HTTP-3/"/>
    <id>https://www.thjiang.com/2023/06/16/在-Nginx-1-25-中开启-HTTP-3/</id>
    <published>2023-06-16T11:05:36.000Z</published>
    <updated>2023-11-09T05:51:36.677Z</updated>
    
    <content type="html"><![CDATA[<p>闲逛发现 nginx 官网 多了一行字： “Support for HTTP/3.”，距离第一次体验 HTTP3 已经过去了 4 年，4 年前，我们体验 HTTP3 是用 Cloudflare 提供的 <a href="https://github.com/cloudflare/quiche" target="_blank" rel="noopener">quiche</a> 这个库来实现的，如今，nginx 终于通过这个 ngx_http_v3_module 提供了官方支持。</p><p>SSL 库方面，最流行的 OpenSSL 决定自己实现 QUIC 支持，按照官网给出的 <a href="https://www.openssl.org/roadmap.html" target="_blank" rel="noopener">roadmap</a>，看起来至少半年内是没什么希望的。<br>nginx 方面自己实现了一个 OpenSSL 的<a href="https://www.nginx.com/blog/quic-http3-support-openssl-nginx/" target="_blank" rel="noopener">兼容层</a>，1.25.0 以后版本的 nginx 中已经自带了。但是这个兼容层不支持 <code>early data</code>。有些营销号直接把 <code>early data</code> 机翻成了“早期数据”，并解释为旧数据，实际上 <code>early data</code> 指的是 TLS1.3 中的 0-RTT data，详见<a href="https://datatracker.ietf.org/doc/html/rfc8446#section-2.3" target="_blank" rel="noopener">这里</a>。<br>所以目前 SSL 库最好从以下 3 个中 BoringSSL、 LibreSSL 或 QuicTLS 三选一。</p><h3 id="开始体验"><a href="#开始体验" class="headerlink" title="开始体验"></a>开始体验</h3><p>以 <a href="https://boringssl.googlesource.com/boringssl" target="_blank" rel="noopener">BoringSSL</a> 为例</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 安装 go</span></span><br><span class="line">wget https://go.dev/dl/go1.21.3.linux-amd64.tar.gz</span><br><span class="line"></span><br><span class="line">tar -C /usr/<span class="built_in">local</span> -xzf go1.21.3.linux-amd64.tar.gz</span><br><span class="line"></span><br><span class="line"><span class="built_in">export</span> PATH=<span class="variable">$PATH</span>:/usr/<span class="built_in">local</span>/go/bin</span><br><span class="line"></span><br><span class="line">go version</span><br><span class="line"><span class="comment"># go version go1.21.3 linux/amd64</span></span><br></pre></td></tr></table></figure><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 安装 cmake 和 ninja</span></span><br><span class="line">wget -c https://github.com/Kitware/CMake/releases/download/v3.28.0-rc2/cmake-3.28.0-rc2.tar.gz</span><br><span class="line"></span><br><span class="line">tar zxvf cmake-3.28.0-rc2.tar.gz</span><br><span class="line"></span><br><span class="line"><span class="built_in">cd</span> cmake-3.28.0-rc2</span><br><span class="line"></span><br><span class="line">./configure --prefix=/usr/<span class="built_in">local</span>/cmake-3.28.0-rc2</span><br><span class="line"></span><br><span class="line">gmake &amp;&amp; gmake install</span><br><span class="line"></span><br><span class="line">vim /etc/profile</span><br><span class="line"></span><br><span class="line">PATH=/usr/<span class="built_in">local</span>/cmake/bin:<span class="variable">$PATH</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">export</span> PATH</span><br><span class="line"></span><br><span class="line"><span class="built_in">source</span> /etc/profile</span><br><span class="line"></span><br><span class="line">cmake --version</span><br><span class="line"></span><br><span class="line"><span class="comment"># cmake version cmake-3.28.0-rc2</span></span><br><span class="line"></span><br><span class="line">yum install ninja-build</span><br></pre></td></tr></table></figure><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 安装新版本 GCC 和 G++</span></span><br><span class="line">yum install centos-release-scl</span><br><span class="line">yum -y install devtoolset-10-gcc devtoolset-10-gcc-c++ devtoolset-10-binutils</span><br><span class="line"></span><br><span class="line"><span class="comment"># 临时指定版本</span></span><br><span class="line">scl <span class="built_in">enable</span> devtoolset-10 bash</span><br><span class="line"><span class="comment"># 持久化使用</span></span><br><span class="line"><span class="built_in">source</span> /opt/rh/devtoolset-10/<span class="built_in">enable</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># cmake -D CMAKE_C_COMPILER=/opt/rh/devtoolset-10/root/usr/bin/gcc -D CMAKE_CXX_COMPILER=/opt/rh/devtoolset-10/root/usr/bin/gcc -GNinja -B build</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 编译 BoringSSL</span></span><br><span class="line">git <span class="built_in">clone</span> --depth=1 https://github.com/google/boringssl.git</span><br><span class="line"></span><br><span class="line"><span class="built_in">cd</span> boringssl &amp;&amp; mkdir build &amp;&amp; <span class="built_in">cd</span> build</span><br><span class="line"></span><br><span class="line"><span class="comment"># 官方建议使用 [ninja](https://ninja-build.org/)，比 make 更快</span></span><br><span class="line">cmake -GNinja -B build</span><br><span class="line"></span><br><span class="line">ninja -C build</span><br></pre></td></tr></table></figure><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 安装 nginx (1.25.0 版本开始支持HTTP3)</span></span><br><span class="line">wget https://nginx.org/download/nginx-1.25.2.tar.gz</span><br><span class="line">tar zxvf nginx-1.25.2.tar.gz</span><br><span class="line"><span class="built_in">cd</span> nginx-1.25.2</span><br><span class="line"></span><br><span class="line">./configure --prefix=/etc/nginx</span><br><span class="line">    --with-debug</span><br><span class="line">    --with-http_v3_module</span><br><span class="line">    --with-cc-opt=<span class="string">"-I../boringssl/include"</span></span><br><span class="line">    --with-ld-opt=<span class="string">"-L../boringssl/build/ssl</span></span><br><span class="line"><span class="string">                   -L../boringssl/build/crypto"</span></span><br><span class="line"></span><br><span class="line">./configure --prefix=/etc/nginx --with-debug --with-http_v3_module --with-cc-opt=<span class="string">"-I../boringssl/include"</span> --with-ld-opt=<span class="string">"-L../boringssl/build/ssl -L../boringssl/build/crypto"</span></span><br><span class="line"></span><br><span class="line">./configure --prefix=/etc/nginx --with-debug --with-http_v3_module --with-openssl=<span class="string">"../boringssl"</span> --with-cc-opt=<span class="string">"-I../boringssl/include"</span> --with-ld-opt=<span class="string">"-L../boringssl/build/ssl -L../boringssl/build/crypto"</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># configure 后，需要 touch 标记一下 ssl.h 文件的时间</span></span><br><span class="line">touch ../boringssl/.openssl/include/openssl/ssl.h</span><br><span class="line"></span><br><span class="line">make &amp;&amp; make install</span><br><span class="line"></span><br><span class="line">nginx -V</span><br><span class="line"><span class="comment"># built with OpenSSL 1.1.0 (compatible; BoringSSL) (running with BoringSSL)</span></span><br><span class="line"><span class="comment"># TLS SNI support enabled</span></span><br></pre></td></tr></table></figure><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">; 基础配置，更多详细内容可以参考这里：</span><br><span class="line">; https:&#x2F;&#x2F;nginx.org&#x2F;en&#x2F;docs&#x2F;http&#x2F;ngx_http_v3_module.html</span><br><span class="line">server &#123;</span><br><span class="line">  server_name xxx.com;</span><br><span class="line"></span><br><span class="line">  listen 443 quic reuseport;</span><br><span class="line">  listen 443 ssl;</span><br><span class="line"></span><br><span class="line">  ssl_certificate     xxx.crt;</span><br><span class="line">  ssl_certificate_key xxx.key;</span><br><span class="line">  ssl_protocols       TLSv1.3;</span><br><span class="line"></span><br><span class="line">  location &#x2F; &#123;</span><br><span class="line">      add_header Alt-Svc &#39;h3&#x3D;&quot;:443&quot;; ma&#x3D;86400&#39;;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">vim /etc/firewalld/zones/public.xml</span><br><span class="line"></span><br><span class="line"><span class="comment"># 放开对应的防火墙</span></span><br><span class="line">&lt;service name=<span class="string">"http"</span>/&gt;</span><br><span class="line">&lt;service name=<span class="string">"https"</span>/&gt;</span><br><span class="line">&lt;port protocol=<span class="string">"udp"</span> port=<span class="string">"443"</span>/&gt;</span><br><span class="line">&lt;port protocol=<span class="string">"tcp"</span> port=<span class="string">"443"</span>/&gt;</span><br></pre></td></tr></table></figure><p>配置好之后，可以在这里监测一下，如图显示，我们已经成功启用了HTTP/3。</p><p><a href="https://http3check.net/?host=xxx.com" target="_blank" rel="noopener">https://http3check.net/?host=xxx.com</a></p><p>同时，打开浏览器控制台，也能看到 protocol 这一项也已经升级为 h3。</p><p>如果前面的流程没有问题，还是显示不支持，可以在这里检测一下浏览器是否已经支持<br><a href="https://quic.nginx.org/" target="_blank" rel="noopener">https://quic.nginx.org/</a></p><p>记得关闭代理软件，有些代理协议不支持HTTP/3。</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;闲逛发现 nginx 官网 多了一行字： “Support for HTTP/3.”，距离第一次体验 HTTP3 已经过去了 4 年，4 年前，我们体验 HTTP3 是用 Cloudflare 提供的 &lt;a href=&quot;https://github.com/cloudfla
      
    
    </summary>
    
      <category term="运维" scheme="https://www.thjiang.com/categories/%E8%BF%90%E7%BB%B4/"/>
    
    
      <category term="前端工程" scheme="https://www.thjiang.com/tags/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B/"/>
    
      <category term="Linux" scheme="https://www.thjiang.com/tags/Linux/"/>
    
  </entry>
  
  <entry>
    <title>显示器色域漫谈</title>
    <link href="https://www.thjiang.com/2022/03/01/%E6%98%BE%E7%A4%BA%E5%99%A8%E8%89%B2%E5%9F%9F%E6%BC%AB%E8%B0%88/"/>
    <id>https://www.thjiang.com/2022/03/01/显示器色域漫谈/</id>
    <published>2022-03-01T13:37:48.000Z</published>
    <updated>2022-03-02T13:39:24.569Z</updated>
    
    <content type="html"><![CDATA[<p>提到显示器，很多同学第一时间想到的可能是分辨率（2K、4K）、尺寸（24、27、32）、面板（IPS、TN、LED）等，常玩游戏的同学可能还会想到刷新率、响应时间等参数。不过色域却常常被大家所忽视，其实色域是一个特别直接的影响视觉体验的参数。</p><h2 id="色域是什么？"><a href="#色域是什么？" class="headerlink" title="色域是什么？"></a>色域是什么？</h2><p>维基百科给的概念是这样的：</p><blockquote><p>色域是对一种颜色进行编码的方法，也指一个技术系统能够产生的颜色的总合。</p></blockquote><p>顶级显示器制造商艺卓是这样解释的：</p><blockquote><p>色域在人眼能够识别的色彩范围（即可见光谱）内限定了一个更具体的范围。彩色成像设备包括多种设备，例如数码相机、扫描仪、显示器和打印机，由于它们能够还原的色彩范围各不相同，所以采用色域这个概念来区分这些差别，并协调各个设备之间可以通用的颜色。</p></blockquote><p>说人话就是：我们为了让相同的颜色在不同设备上看起来一样，需要一个标准。</p><h3 id="色彩空间"><a href="#色彩空间" class="headerlink" title="色彩空间"></a>色彩空间</h3><p>我们知道在绘画时把红、黄、蓝这三种原色混合，可以生成不同的颜色。我们将红黄蓝三种原色的量分别定义为 XYZ 三个坐标轴，这样就得到了一个三维空间，每种可能的颜色在这个三维空间中都有唯一的一个位置，这就是一种色彩空间。<br>同样，在显示器领域，通常使用 RGB（红色、绿色、蓝色）三原色，以红色、绿色、蓝色为三维坐标轴，就得到了 <code>RGB 色彩空间</code>。类似地，使用色相、饱和度（色度）和明度作为坐标轴，我们可以得到 <code>HSV 色彩空间</code>，使用油墨的三原色（青、品、黄）搭配黑色油墨，我们可以得到印刷行业的通用标准 <code>CMYK 色彩空间</code>。</p><p>由于人眼可见光的范围有限，我们制造显示器等成像设备时，没有必要也不可能做到支持完整的 RGB 色彩空间。</p><p><a href="https://zh.wikipedia.org/wiki/%E5%9B%BD%E9%99%85%E7%85%A7%E6%98%8E%E5%A7%94%E5%91%98%E4%BC%9A" target="_blank" rel="noopener">国际照明委员会</a>（英文：International Commission on Illumination，法文：Commission internationale de l’éclairage，采用法文缩写：CIE）在 1931 年提出了一个基于人眼对于色彩的感知度量用数学方式定义色彩空间的方法：<code>CIE 1931 XYZ</code>色彩空间，后续又在此基础上衍生出了<code>CIE L*a*b*(CIELAB)</code> 色彩空间，它们涵盖了正常人可见范围内的所有色彩。后续人们定义色彩空间时，通常使用这两个色彩空间作为参考标准。</p><p>依照在 RGB 色彩模型和 CIE 1931 XYZ 色彩空间之间的映射函数，在这个色彩空间中会出现一个有限的“覆盖区”，这个区域就被称为 CIE 色域，也就是人眼的色域，它在平面中的投影如图所示：</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://img.alicdn.com/imgextra/i3/O1CN01ywczmF1rDTWOPLNbD_!!6000000005597-0-tps-800-823.jpg" alt="CIE xy 色度图" title="">                </div>                <div class="image-caption">CIE xy 色度图</div>            </figure><p>这就是 CIE xy 色度图，图中马蹄形的左右两边的轮廓线代表了波长由 380nm-700nm 连续变化的单色光，马蹄形的底边代表了紫红色光。值得一提的是，紫红色光并不是单色光，而是由红色（700nm）和紫色（380nm）混合而成。在马蹄形内部，越靠近马蹄形边缘的颜色饱和度越高。</p><h2 id="常见显示器色域"><a href="#常见显示器色域" class="headerlink" title="常见显示器色域"></a>常见显示器色域</h2><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://img.alicdn.com/imgextra/i4/O1CN01kRV6Qy1XA0Cdlf2MX_!!6000000002882-0-tps-1432-862.jpg" alt="某显示器宣传图" title="">                </div>                <div class="image-caption">某显示器宣传图</div>            </figure><p>我们在看显示器介绍的时候，经常会看到如 99% sRGB 色域，72% NTSC 色域等等，这些代表什么呢？</p><p>由于技术限制，目前的显示器还不能做到支持完整的 CIE 色域。因此，厂商们制定了一些新的色域标准，这些标准都是人眼全色域的子集，只能覆盖全色域中的一部分。对于显示器来说，色域代表了颜色的区域，也就是颜色的区域范围，色域越大，能显示的颜色就越多。</p><p>目前常见的显示器色域标准有：sRGB、AdobeRGB、NTSC 以及 DCI-P3 等。</p><p>那么，这些标准都代表什么，之间有什么区别呢？</p><h3 id="sRGB"><a href="#sRGB" class="headerlink" title="sRGB"></a>sRGB</h3><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://img.alicdn.com/imgextra/i3/O1CN01k2521T1PJ9BJFfa4A_!!6000000001819-2-tps-1440-1080.png" alt="sRGB 色域" title="">                </div>                <div class="image-caption">sRGB 色域</div>            </figure><p>sRGB 全名 <code>standard Red Green Blue</code>，是由微软主导，联合惠普、爱普生等厂商在 1996 年联合制定的一套彩色语言协议，可以让显示器、打印机、扫描仪等设备色彩交互更加统一，就是让显示器上的颜色能够尽可能的完美打印出来的一套标准。因为通用性高，所以 sRGB 标准也是平面设计师、视频创作者等用户首选的色域标准。但是由于该色域标准比较老旧，当时的设备技术的色彩还原能力不高，所以 sRGB 的色彩容积也较低，只有 CIE 1931 标准的 30% 左右，同时该标准对绿色部分色域的覆盖非常少。上图的三角形划出的便是 sRGB 所能展现出的色彩容积，目前大部分较好的显示器都能展示 99% 以上的 sRGB 色域。。</p><h3 id="Adobe-RGB"><a href="#Adobe-RGB" class="headerlink" title="Adobe RGB"></a>Adobe RGB</h3><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://img.alicdn.com/imgextra/i4/O1CN014X0kDo1XV4QIG7XAu_!!6000000002928-0-tps-700-394.jpg" alt="Adobe RGB 色域" title="">                </div>                <div class="image-caption">Adobe RGB 色域</div>            </figure><p>我们前面提到过在印刷行业中，一般使用 CMYK 作为通用的色彩标准，而 CMYK 和 sRGB 的偏差是比较大的，从而使用 sRGB 模式展示的颜色在印刷时就会出现偏色。为了解决这个问题，Adobe 提出了 Adobe RGB， 从上图可以看出，Adobe RGB 的色域容积远比 sRGB 大。</p><h3 id="NTSC"><a href="#NTSC" class="headerlink" title="NTSC"></a>NTSC</h3><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://img.alicdn.com/imgextra/i3/O1CN011ouusb1vXrcBRKxq7_!!6000000006183-0-tps-700-394.jpg" alt="NTSC 色域" title="">                </div>                <div class="image-caption">NTSC 色域</div>            </figure><p>NTSC 全名 <code>National Television Standards Committee</code>，就是美国国家电视标准委员会的全名，因为该标准由美国国家电视标准委员会开发，故直接用该名作为简称。该标准主要用于美国标准电视广播传输和接受协议。很多笔记本电脑习惯使用 NTSC 来标注支持的色域，一般有 72% NTSC 和 45% NTSC 两种，其中 45% NTSC 的屏幕也常被人戏称”瞎眼屏”。72% NTSC 在面积上约等于 99% sRGB，注意这只是在面积上的大小比较，而这种对比意义不大。可以这么认为，故意标注自己的显示器支持 72% NTSC 色域的厂商，几乎所有的都是在暗示自己的产品基本覆盖了全部的 sRGB，而实际上一般还差很多。</p><h3 id="BT-2020、DCI-P3、Rec-709、Display-P3"><a href="#BT-2020、DCI-P3、Rec-709、Display-P3" class="headerlink" title="BT.2020、DCI-P3、Rec.709、Display P3"></a>BT.2020、DCI-P3、Rec.709、Display P3</h3><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://img.alicdn.com/imgextra/i1/O1CN01M4w41t26JAgLJ2MOo_!!6000000007640-0-tps-1920-1080.jpg" alt="图片源自 https://www.bgteach.com/article/291" title="">                </div>                <div class="image-caption">图片源自 https://www.bgteach.com/article/291</div>            </figure><p>前三种都是主要面向影视领域的色彩标准，覆盖范围 BT.2020 最大，DCI-P3 其次，Rec.709 最小。目前家用设备仅有少数高端电视和投影机支持 BT.2020，电影行业一般以 DCI-P3 作为标准使用。对比 sRGB 等标准，DCI-P3 在红绿等暖色系的显色范围更广，给人的视觉观感更好。</p><p>Display P3 是苹果主推的色域，覆盖区域和 DCI-P3 相同，而 Display P3 的 gamma 值近似为 2.2，DCI-P3 则是 2.6，所以 Display P3 的画面会更明亮一些。</p><p>webkit 提供了一个小工具可以方便的检测你的设备是否支持 DCI-P3 标准：</p><p><a href="https://webkit.org/blog-files/color-gamut/comparison.html" target="_blank" rel="noopener">https://webkit.org/blog-files/color-gamut/comparison.html</a></p><p>如果你在第一张图中能明显的看到左侧部分比右侧偏暗，同时可以看到右侧图中的图标，说明你的显示器还不错。</p><p>也可以试试看这张图：</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://img.alicdn.com/imgextra/i3/O1CN01dQJUox1HVUwF5Bv3Z_!!6000000000763-2-tps-1440-720.png" alt="P3 测试" title="">                </div>                <div class="image-caption">P3 测试</div>            </figure><p>如果能看到第二个笑脸，说明你的显示器支持 100% 的 sRGB 标准，如果能看到第三个笑脸，说明支持 DCI-P3。</p><p>Windows 用户看不到的也不用折腾了，显示器模式、icc 配置、浏览器版本等都有可能有影响，Windows 的色彩管理实在一言难尽。</p><h2 id="扩展"><a href="#扩展" class="headerlink" title="扩展"></a>扩展</h2><h3 id="前端可能用得到的-P3-色域知识"><a href="#前端可能用得到的-P3-色域知识" class="headerlink" title="前端可能用得到的 P3 色域知识"></a>前端可能用得到的 P3 色域知识</h3><h4 id="CANVAS"><a href="#CANVAS" class="headerlink" title="CANVAS"></a>CANVAS</h4><p>Chrome 92 支持在创建 2D canvas 时，使用 Display P3 色域。</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">canvas.getContext(<span class="string">"2d"</span>, &#123; <span class="attr">colorSpace</span>: <span class="string">"display-p3"</span> &#125;);</span><br></pre></td></tr></table></figure><h4 id="CSS"><a href="#CSS" class="headerlink" title="CSS"></a>CSS</h4><p>CSS Color Module Level 4 提供了一种新语法来设置 Display-P3 色域：</p><figure class="highlight css"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="selector-tag">color</span>: <span class="selector-tag">color</span>(<span class="selector-tag">display-p3</span> 1 0<span class="selector-class">.5</span> 0);</span><br></pre></td></tr></table></figure><p>目前这个功能只有 Safari 15 以上版本支持，Chrome 正在规划中(<a href="https://bugs.chromium.org/p/chromium/issues/detail?id=1068610" target="_blank" rel="noopener">https://bugs.chromium.org/p/chromium/issues/detail?id=1068610</a>)</p><p>在不支持的浏览器上我们可以这样降级到 sRGB 模式：</p><figure class="highlight css"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="selector-tag">header</span> &#123;</span><br><span class="line">    <span class="attribute">color</span>: <span class="built_in">rgb</span>(<span class="number">0</span>, <span class="number">255</span>, <span class="number">0</span>);</span><br><span class="line">    <span class="attribute">color</span>: <span class="built_in">color</span>(display-p3 <span class="number">0</span> <span class="number">1</span> <span class="number">0</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>也可以通过 @supports 查询的方式来实现，如：</p><figure class="highlight css"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/* sRGB color. */</span></span><br><span class="line"><span class="selector-pseudo">:root</span> &#123;</span><br><span class="line">    <span class="attribute">--bright-green</span>: <span class="built_in">rgb</span>(<span class="number">0</span>, <span class="number">255</span>, <span class="number">0</span>);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">/* Display-P3 color, when supported. */</span></span><br><span class="line"><span class="keyword">@supports</span> (<span class="attribute">color:</span> color(display-p3 <span class="number">1</span> <span class="number">1</span> <span class="number">1</span>)) &#123;</span><br><span class="line">    <span class="selector-pseudo">:root</span> &#123;</span><br><span class="line">        <span class="attribute">--bright-green</span>: <span class="built_in">color</span>(display-p3 <span class="number">0</span> <span class="number">1</span> <span class="number">0</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="selector-tag">header</span> &#123;</span><br><span class="line">    <span class="attribute">color</span>: <span class="built_in">var</span>(--bright-green);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="图片"><a href="#图片" class="headerlink" title="图片"></a>图片</h4><p>除了 Safari 外，Chrome 实验特性中也提供了对 Display P3 的支持，可以通过<br>chrome://flags 中的 <code>force color profile</code> 选项手动选择 <code>Display P3 D65</code> 模式打开。注意在 Windows 系统上如果显示器不支持或没有配置好对应的驱动/ ICC 文件，系统将按照一定的规范把 P3 色域映射到 sRGB 色域上展示，会出现一定的过饱和现象。</p><h3 id="埋个坑，色深和色准"><a href="#埋个坑，色深和色准" class="headerlink" title="埋个坑，色深和色准"></a>埋个坑，色深和色准</h3><p>如果一个显示器的色域足够广，是不是就可以说它的显示效果一定非常好呢？<br>并不是，还有其他几个非常重要的参数，比如色深和色准，下期再来分享~</p><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><p><a href="https://zh.wikipedia.org/wiki/CIE_1931%E8%89%B2%E5%BD%A9%E7%A9%BA%E9%97%B4" target="_blank" rel="noopener">https://zh.wikipedia.org/wiki/CIE_1931%E8%89%B2%E5%BD%A9%E7%A9%BA%E9%97%B4</a><br><a href="https://www.zhangxiaochun.com/color-space-1/" target="_blank" rel="noopener">https://www.zhangxiaochun.com/color-space-1/</a><br><a href="https://bbs.nga.cn/read.php?tid=20309949" target="_blank" rel="noopener">https://bbs.nga.cn/read.php?tid=20309949</a><br><a href="https://yuque.antfin-inc.com/lili.qu/xioo87/fcxm2m" target="_blank" rel="noopener">https://yuque.antfin-inc.com/lili.qu/xioo87/fcxm2m</a><br><a href="https://zhuanlan.zhihu.com/p/166413369" target="_blank" rel="noopener">https://zhuanlan.zhihu.com/p/166413369</a><br><a href="https://webkit.org/blog/10042/wide-gamut-color-in-css-with-display-p3/" target="_blank" rel="noopener">https://webkit.org/blog/10042/wide-gamut-color-in-css-with-display-p3/</a></p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;提到显示器，很多同学第一时间想到的可能是分辨率（2K、4K）、尺寸（24、27、32）、面板（IPS、TN、LED）等，常玩游戏的同学可能还会想到刷新率、响应时间等参数。不过色域却常常被大家所忽视，其实色域是一个特别直接的影响视觉体验的参数。&lt;/p&gt;
&lt;h2 id=&quot;色域是
      
    
    </summary>
    
    
      <category term="数码" scheme="https://www.thjiang.com/tags/%E6%95%B0%E7%A0%81/"/>
    
  </entry>
  
  <entry>
    <title>海南自驾线路推荐</title>
    <link href="https://www.thjiang.com/2021/03/15/%E6%B5%B7%E5%8D%97%E8%87%AA%E9%A9%BE%E7%BA%BF%E8%B7%AF%E6%8E%A8%E8%8D%90/"/>
    <id>https://www.thjiang.com/2021/03/15/海南自驾线路推荐/</id>
    <published>2021-03-15T03:02:21.000Z</published>
    <updated>2021-05-14T15:40:03.471Z</updated>
    
    <content type="html"><![CDATA[<p>本来是写给同事的一个攻略，感觉可能有同学会用得上，在这里也分享一下。</p><blockquote><ul><li>文中的『车程』不包含游览时间</li></ul></blockquote><h2 id="南山-天涯海角-椰梦长廊-鹿回头-太阳湾（单程-75-公里，车程约-2-小时）"><a href="#南山-天涯海角-椰梦长廊-鹿回头-太阳湾（单程-75-公里，车程约-2-小时）" class="headerlink" title="南山-天涯海角-椰梦长廊-鹿回头-太阳湾（单程 75 公里，车程约 2 小时）"></a>南山-天涯海角-椰梦长廊-鹿回头-太阳湾（单程 75 公里，车程约 2 小时）</h2><h3 id="路线总览"><a href="#路线总览" class="headerlink" title="路线总览"></a>路线总览</h3><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://img.alicdn.com/imgextra/i3/O1CN01kG48511zCZat7bDLt_!!6000000006678-0-tps-1915-945.jpg" alt="路线1总览" title="">                </div>                <div class="image-caption">路线1总览</div>            </figure><h3 id="南山文化旅游区"><a href="#南山文化旅游区" class="headerlink" title="南山文化旅游区"></a>南山文化旅游区</h3><p>南山文化旅游区是一座展示中国佛教传统文化的大型园区，福如东海寿比南山的南山就是这里了。主要景点有南山寺、南海观音佛像、不二法门、十方塔林与归根园、佛教文化交流中心等，世界上最大的一尊海上观音也坐落在此。有摇号需求的可以去参拜一下，此处 @安笺 @麦棋。</p><p>门票旺季 150 元，景区很大，建议多花 30 元购买游览车票。</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://c.icewish.top/const/blog/images/hainan/21.jpg!w960" alt="南海观音" title="">                </div>                <div class="image-caption">南海观音</div>            </figure><h3 id="天涯海角"><a href="#天涯海角" class="headerlink" title="天涯海角"></a>天涯海角</h3><p>天涯海角是人民币 2 元背面图案的取景地，有人说这是来三亚必须要去而且只会去一次的景点，第一次来的话还是去打个卡吧。有一说一 80 元的票价看几块石头还是有点心痛的，文昌免费的石头公园明显更适合我这种白嫖党。<br>同样建议坐观光车，可以大幅节约时间和体力。</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://c.icewish.top/const/blog/images/hainan/23.jpg!w960" alt="2元人民币" title="">                </div>                <div class="image-caption">2元人民币</div>            </figure><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://c.icewish.top/const/blog/images/hainan/22.jpg!w960" alt="天涯海角" title="">                </div>                <div class="image-caption">天涯海角</div>            </figure><h3 id="椰梦长廊"><a href="#椰梦长廊" class="headerlink" title="椰梦长廊"></a>椰梦长廊</h3><p>椰梦长廊是一条 20 公里长的海滨风景大道，临海的一侧是热带植物园林，另一侧是休闲度假区。建议导航至<strong>Club Med 三亚度假村</strong>，然后沿三亚湾路游览。</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://c.icewish.top/const/blog/images/hainan/24.jpg!w960" alt="椰梦长廊" title="">                </div>                <div class="image-caption">椰梦长廊</div>            </figure><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://c.icewish.top/const/blog/images/hainan/25.jpg!w960" alt="导航点-Club Med三亚度假村" title="">                </div>                <div class="image-caption">导航点-Club Med三亚度假村</div>            </figure><h3 id="鹿回头"><a href="#鹿回头" class="headerlink" title="鹿回头"></a>鹿回头</h3><p>鹿回头景区三面环海，一面毗邻三亚市区，是登高望海俯瞰全城的绝佳观景点。</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://c.icewish.top/const/blog/images/hainan/26.jpeg!w960" alt="鹿回头" title="">                </div>                <div class="image-caption">鹿回头</div>            </figure><h3 id="太阳湾"><a href="#太阳湾" class="headerlink" title="太阳湾"></a>太阳湾</h3><p>太阳湾路号称“三亚最美公路”，是三亚自驾必打卡的地点之一，直接导航到<strong>太阳湾柏悦酒店</strong>即可，可以开到酒店内部观景台附近。顺便说一句，太阳湾柏悦是我心目中三亚最好的酒店，没有之一，值得体验。</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://c.icewish.top/const/blog/images/hainan/27.jpeg!w960" alt="太阳湾" title="">                </div>                <div class="image-caption">太阳湾</div>            </figure><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://c.icewish.top/const/blog/images/hainan/27.jpg!w960" alt="太阳湾路" title="">                </div>                <div class="image-caption">太阳湾路</div>            </figure><h2 id="南湾猴岛-日月湾-石梅湾-神州半岛-大花角（往返共-400-公里，约需-6-小时）"><a href="#南湾猴岛-日月湾-石梅湾-神州半岛-大花角（往返共-400-公里，约需-6-小时）" class="headerlink" title="南湾猴岛-日月湾-石梅湾-神州半岛-大花角（往返共 400 公里，约需 6 小时）"></a>南湾猴岛-日月湾-石梅湾-神州半岛-大花角（往返共 400 公里，约需 6 小时）</h2><h3 id="路线总览-1"><a href="#路线总览-1" class="headerlink" title="路线总览"></a>路线总览</h3><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://img.alicdn.com/imgextra/i1/O1CN01HWQNig1KmOJwsFRL1_!!6000000001206-0-tps-1913-938.jpg" alt="路线2总览" title="">                </div>                <div class="image-caption">路线2总览</div>            </figure><h3 id="精华路段"><a href="#精华路段" class="headerlink" title="精华路段"></a>精华路段</h3><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://img.alicdn.com/imgextra/i1/O1CN01K52XmS1zdaxhFuVcH_!!6000000006737-0-tps-1530-726.jpg" alt="路线2精华路段1" title="">                </div>                <div class="image-caption">路线2精华路段1</div>            </figure><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://img.alicdn.com/imgextra/i1/O1CN01N7Eul61f1lw59RcRH_!!6000000003947-0-tps-1018-836.jpg" alt="路线2精华路段2" title="">                </div>                <div class="image-caption">路线2精华路段2</div>            </figure><h3 id="南湾猴岛"><a href="#南湾猴岛" class="headerlink" title="南湾猴岛"></a>南湾猴岛</h3><p>南湾猴岛是世界上唯一的岛屿型猕猴自然保护区，现居住着 2500 多只猕猴。猴岛二期又名呆呆岛，是海南岛东海岸线最长的，保护最好的绝美沙滩。</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://c.icewish.top/const/blog/images/hainan/32.jpeg!w960" alt="南湾猴岛" title="">                </div>                <div class="image-caption">南湾猴岛</div>            </figure><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://c.icewish.top/const/blog/images/hainan/31.jpeg!w960" alt="呆呆岛" title="">                </div>                <div class="image-caption">呆呆岛</div>            </figure><h3 id="石梅湾"><a href="#石梅湾" class="headerlink" title="石梅湾"></a>石梅湾</h3><p>又一个因《非诚勿扰 2》的取景地爆火的海湾，也是一个非常适合观赏日落的最佳地点。</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://c.icewish.top/const/blog/images/hainan/33.jpg!w960" alt="石梅湾威斯汀度假酒店海滩" title="">                </div>                <div class="image-caption">石梅湾威斯汀度假酒店海滩</div>            </figure><p>建议打卡点<strong>凤凰九里书屋</strong>，面朝大海，是中国最美书店之一。</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://c.icewish.top/const/blog/images/hainan/34.jpg!w960" alt="凤凰九里书屋" title="">                </div>                <div class="image-caption">凤凰九里书屋</div>            </figure><h3 id="南燕湾-神州半岛"><a href="#南燕湾-神州半岛" class="headerlink" title="南燕湾/神州半岛"></a>南燕湾/神州半岛</h3><p>从书屋出发，建议导航至<strong>神州半岛泰悦居</strong>，这样可以保证一路沿着滨海旅游公路前进。</p><p>经过石梅湾艾美度假酒店后大约 800 米，路边会有南燕湾的观景台和停车场，可以休息游玩。</p><p>继续前进 8 公里，到达神州半岛泰悦居，路边有停车场，然后需要步行一公里前往海边。这里有一个网红灯塔，同样又双叒叕是一个看日落的地方（想看日出需要去文昌），如果想在这里看日落的话，可以从石梅湾先前往大花角，回程再来这里。</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://c.icewish.top/const/blog/images/hainan/35.jpg!w960" alt="神州半岛灯塔" title="">                </div>                <div class="image-caption">神州半岛灯塔</div>            </figure><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://c.icewish.top/const/blog/images/hainan/36.jpg!w960" alt="神州半岛落日" title="">                </div>                <div class="image-caption">神州半岛落日</div>            </figure><h3 id="大花角"><a href="#大花角" class="headerlink" title="大花角"></a>大花角</h3><p>大花角由前鞍和后鞍两个山峦构成，双峰之间，有一处宽近百米的峡谷，与双峰一起伸向浩瀚的南海，于是形成一个天然的海湾，很适合坐在海边的岩石上思考人生。</p><p>从石梅湾/神州半岛出发可以直接导航到<strong>大花角</strong>，中途添加途经点<strong>蓝田观景台</strong>即可，一路上有很多观景点和停车场，随时可以停下来去海边游玩。</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://c.icewish.top/const/blog/images/hainan/310.jpg!w960" alt="观景台" title="">                </div>                <div class="image-caption">观景台</div>            </figure><p>到达导航终点附近，会看到一个部队的营地，右转有一个停车场，会有当地村民来收费 10 元，没有任何票据，可以无视他。不想麻烦的话也可以直接把车停在部队门口的路边。</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://c.icewish.top/const/blog/images/hainan/311.jpg!w960" alt="大花角" title="">                </div>                <div class="image-caption">大花角</div>            </figure><p>到了大花角，万宁滨海旅游公路就结束了，意犹未尽的，可以继续前往山钦湾（导航到上卿村）和玉带滩，需要注意还车时间。</p><h2 id="昌化江-玉龙山-王下乡-鱼鳞洲（往返共-400-公里，约需-8-小时）"><a href="#昌化江-玉龙山-王下乡-鱼鳞洲（往返共-400-公里，约需-8-小时）" class="headerlink" title="昌化江-玉龙山-王下乡-鱼鳞洲（往返共 400 公里，约需 8 小时）"></a>昌化江-玉龙山-王下乡-鱼鳞洲（往返共 400 公里，约需 8 小时）</h2><p>如果觉得三亚的海已经看腻了，更推荐这条山水风光线路。</p><h3 id="路线总览-2"><a href="#路线总览-2" class="headerlink" title="路线总览"></a>路线总览</h3><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://img.alicdn.com/imgextra/i1/O1CN01f6vMl91GSv1MShZ8c_!!6000000000622-0-tps-1099-920.jpg" alt="路线3总览" title="">                </div>                <div class="image-caption">路线3总览</div>            </figure><h3 id="路线详情"><a href="#路线详情" class="headerlink" title="路线详情"></a>路线详情</h3><ol><li><p>崖州湾出发，导航至<strong>玉龙山观景台</strong>，经环岛高速和海三高速后，沿昌化江一路前进，沿途一步一景，全程 131 公里，车程约 2 小时。</p></li><li><p>如果出发较早，从玉龙山观景台可以导航到<strong>王下乡</strong>看木棉花，3 月正是木棉花开的时节。途中会经过大广坝水库，值得一看。全程 51 公里，车程约 2 小时。</p></li></ol><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://c.icewish.top/const/blog/images/hainan/18.jpg!w960" alt="大广坝水库" title="">                </div>                <div class="image-caption">大广坝水库</div>            </figure><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://c.icewish.top/const/blog/images/hainan/17.jpeg!w960" alt="木棉花" title="">                </div>                <div class="image-caption">木棉花</div>            </figure><ol><li>王下乡到鱼鳞洲自然保护区全程 91 公里，车程约 2 小时，3 月 27 日鱼鳞洲日落时间为 18:51:33，需要注意时间安排。</li></ol><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://c.icewish.top/const/blog/images/hainan/15.jpg!w960" alt="鱼鳞洲" title="">                </div>                <div class="image-caption">鱼鳞洲</div>            </figure><ol><li>鱼鳞洲回酒店，直接导航就可以了，全程高速，大约 130 公里，约需 2 小时。</li></ol><h2 id="租车渠道"><a href="#租车渠道" class="headerlink" title="租车渠道"></a>租车渠道</h2><ol><li><p>携程租车<br>优势：供应商多、车型全、交互方便、价格相对较低，每周三会员日可以领取 50 元无门槛代金券。</p></li><li><p>枫叶出行<br>优势：新车、新车、新车<br>劣势：价格稍贵，只有 A4L、320、C200、E260 和一些跑车等少数车型可选。</p><p>新用户的话可以用我的这个邀请链接 <a href="https://share.fyrentcar.com/staticPage/index.html?mobileShare=941035f6faa67ab8edbac3d8fdf761c74321b5454d61e033&amp;rand=ae75fabc-6efc-4a8e-a36e-9e6f042c7297" target="_blank" rel="noopener">https://share.fyrentcar.com/staticPage/index.html?mobileShare=941035f6faa67ab8edbac3d8fdf761c74321b5454d61e033&amp;rand=ae75fabc-6efc-4a8e-a36e-9e6f042c7297</a> 注册，可以得到一张7折优惠券。</p></li></ol><ol><li><p>飞猪租车<br>优势：遇到问题可以内网发帖找飞猪运营帮忙解决。。。。<br>劣势：交互极其反人类</p></li><li><p>一嗨/神州/凹凸等<br>优势：平台保障、取还方便（三亚海口机场均支持自助）、价格优惠<br>劣势：车型较少、车龄较长</p></li><li><p>白嫖怪的福利<br>5.1 哪吒汽车48小时深度体验试驾<br> 路径： 支付宝 - 芝麻信用 - 芝麻粒 - 信用试用 - 哪吒汽车48小时深度体验试驾<br>5.2 运通白金信用卡持卡人蔚来 ES8 36小时免费体验<br> 可以叠加两张附属卡，一次刷满36*3小时，还可以顺便白嫖3张蔚来中心咖啡券。<br> 活动地址：<a href="https://www.amexpressnetwork.com/rewards/product/204" target="_blank" rel="noopener">https://www.amexpressnetwork.com/rewards/product/204</a> (好像被公司屏蔽了，需要开代理)。</p></li></ol><h2 id="一些注意事项"><a href="#一些注意事项" class="headerlink" title="一些注意事项"></a>一些注意事项</h2><ol><li>海南高速免费，但是油价很贵，可以提前准备一些优惠途径，如高德、团油等。</li><li>跑车基本都有里程限制，一般为每天 200 公里。</li><li>海南人民开车很随意，需要集中注意力。双黄线是什么？没听过，应急车道？我现在很急啊。</li><li>海南对超速和违停的处罚极其严格，路边见到限速 40、60 的牌子建议给予足够尊重，停车尽量找到停车场，租车处理违章很麻烦。至于其他实线变道、灯光之类的倒是几乎不拍（对这条言论不负任何责任，建议大家遵章驾驶）。</li></ol>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;本来是写给同事的一个攻略，感觉可能有同学会用得上，在这里也分享一下。&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;文中的『车程』不包含游览时间&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;南山-天涯海角-椰梦长廊-鹿回头-太阳湾（单程-75-公
      
    
    </summary>
    
    
      <category term="旅行" scheme="https://www.thjiang.com/tags/%E6%97%85%E8%A1%8C/"/>
    
  </entry>
  
  <entry>
    <title>2020， 再谈 Polyfill 最佳实践</title>
    <link href="https://www.thjiang.com/2020/03/24/2020%EF%BC%8C-%E5%86%8D%E8%B0%88-Polyfill-%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5/"/>
    <id>https://www.thjiang.com/2020/03/24/2020，-再谈-Polyfill-最佳实践/</id>
    <published>2020-03-24T07:55:56.000Z</published>
    <updated>2020-03-29T10:34:46.822Z</updated>
    
    <content type="html"><![CDATA[<p>去年这个时候，曾经写过一篇文章 <a href="https://www.thjiang.com/2019/03/02/Polyfill-%E7%9A%84%E5%89%8D%E4%B8%96%E4%BB%8A%E7%94%9F/">Polyfill 的前世今生</a>，总结了常见的 polyfill 方案。受限于当时的认知以及 Babel 版本，有很多疏漏，觉得有必要更新一篇目前（2020.03） polyfill 的一些进展。</p><h2 id="core-js-3"><a href="#core-js-3" class="headerlink" title="core-js@3"></a>core-js@3</h2><p>在我写下上一篇文章之后两周，2019.03.19，<code>Babel@7.4.0</code> 正式发布了。在 <code>Babel@7.4.0</code> 中，提供了对管道运算符、私有方法、TypeScript 3.4 等的支持，同时，polyfill 开始支持 <code>core-js@3</code>。对于 core-js ，可能有些人还很陌生，根据 <a href="https://www.npmtrends.com/core-js-vs-es5-shim-vs-es6-shim-vs-airbnb-js-shims-vs-polyfill-library-vs-polyfill-service-vs-js-polyfills" target="_blank" rel="noopener">npmtrends</a> 的统计， core-js 事实上已经是目前最流行的 polyfill 方案了，<code>@babel/polyfill</code> 就是靠它来转换代码的，只是很多人可能没有留意到其实自己已经在间接使用它了。回想一下执行 <code>npm install</code> 的时候，是不是对 <code>As advertising: the author is looking for a good job -)</code> 似曾相识？ 这条广告还引发过技术人员能不能在 npm log 中为自己打广告的争论， <code>npm fund</code> 也是为此而生。</p><p>提到这个，顺便想起了前几天在这条 <a href="https://github.com/zloirock/core-js/issues/767" target="_blank" rel="noopener">issue</a> 中看到的一件事， core-js 的作者看起来因交通肇事被判处了 1.5 年的监禁，希望他好好改造重新做人 →_→</p><h2 id="升级指南"><a href="#升级指南" class="headerlink" title="升级指南"></a>升级指南</h2><h3 id="babel-config-json-与-babelrc"><a href="#babel-config-json-与-babelrc" class="headerlink" title="babel.config.json 与 .babelrc"></a>babel.config.json 与 .babelrc</h3><p>在 babel@6.x 版本中，我们通常使用 <code>.babelrc</code> 作为 babel 的配置文件，但是在实践中，遇到了很多问题。现在 babel 团队建议使用这两种方式来对 babel 做出配置：</p><blockquote><p>Project-wide configuration</p><ul><li><code>babel.config.json</code> files, with the different extensions</li></ul><p>File-relative configuration</p><ul><li><code>.babelrc.json</code> files, with the different extensions</li><li><code>package.json</code> files with a ‘babel’ key</li></ul></blockquote><p>更详细的信息可以参考这里： <a href="https://babeljs.io/docs/en/config-files" target="_blank" rel="noopener">https://babeljs.io/docs/en/config-files</a></p><h3 id="Browserslist"><a href="#Browserslist" class="headerlink" title="Browserslist"></a>Browserslist</h3><p>Babel 团队建议使用 <code>.browserslistrc</code> 文件来指定需要兼容到的浏览器，这样该配置可以被 <code>autoprefixer、 eslint</code> 等其他工具共用。</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 示例：</span></span><br><span class="line">&gt; <span class="number">1</span>%</span><br><span class="line">last <span class="number">2</span> version</span><br><span class="line">not ie &lt;= <span class="number">10</span></span><br></pre></td></tr></table></figure><h3 id="Polyfill"><a href="#Polyfill" class="headerlink" title="Polyfill"></a>Polyfill</h3><p>在之前的版本中，如果我们将 babel 的 <code>useBuiltIns</code> 属性设置为 <code>entry</code> 或 <code>false</code>，我们需要在代码中手动引入 <code>@babel/polyfill</code>，现在则只需要引入 regenerator 和 core-js 就可以了。在代码入口文件前引入它们，可以模拟完整的 ES2015+ 环境（不包含 &lt; Stage 4 的提案）。</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// before</span></span><br><span class="line"><span class="keyword">import</span> <span class="string">"@babel/polyfill"</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// after</span></span><br><span class="line"><span class="keyword">import</span> <span class="string">"core-js/stable"</span>;</span><br><span class="line"><span class="keyword">import</span> <span class="string">"regenerator-runtime/runtime"</span>;</span><br></pre></td></tr></table></figure><p>而如果我们对构建产物的大小有限制，我们可以继续使用 <code>useBuiltIns: usage</code> 来按需导入所需的 polyfill 内容。</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// babel.config.json</span></span><br><span class="line">presets: [</span><br><span class="line">    [</span><br><span class="line">        <span class="string">"@babel/preset-env"</span>, &#123;</span><br><span class="line">            <span class="string">"useBuiltIns"</span>: <span class="string">"usage"</span></span><br><span class="line">            <span class="string">"corejs"</span>: <span class="number">3</span></span><br><span class="line">        &#125;</span><br><span class="line">    ]</span><br><span class="line">]</span><br></pre></td></tr></table></figure><h3 id="transform-runtime"><a href="#transform-runtime" class="headerlink" title="transform-runtime"></a>transform-runtime</h3><p>我们曾经提到过，transform-runtime 的方案不支持如 <code>&quot;foobar&quot;.includes(&quot;foo&quot;)</code> 这样的实例方法。在 core-js@3 中，这个问题得到了解决。</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">npm remove @babel/runtime-corejs2</span><br><span class="line">npm install --save @babel/runtime-corejs3</span><br></pre></td></tr></table></figure><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// babel.config.json</span></span><br><span class="line">plugins: [</span><br><span class="line">    [</span><br><span class="line">        <span class="string">"@babel/transform-runtime"</span>, &#123;</span><br><span class="line">            <span class="string">"corejs"</span>: <span class="number">3</span></span><br><span class="line">        &#125;</span><br><span class="line">    ]</span><br><span class="line">];</span><br></pre></td></tr></table></figure><p>如果我们需要支持 Stage &lt; 4 阶段的提案，可以配置 <code>proposals</code> 属性来实现：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// babel.config.json</span></span><br><span class="line"><span class="string">"plugins"</span>: [</span><br><span class="line">    [</span><br><span class="line">        <span class="string">"@babel/plugin-transform-runtime"</span>, &#123;</span><br><span class="line">            <span class="string">"corejs"</span>: &#123;</span><br><span class="line">                <span class="string">"version"</span>: <span class="number">3</span>,</span><br><span class="line">                <span class="string">"proposals"</span>: <span class="literal">true</span></span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    ]</span><br><span class="line">]</span><br></pre></td></tr></table></figure><p>但是，<code>@babel/plugin-transform-runtime</code> 依然存在问题：它不能像 <code>@babel/preset-env</code> 一样指定 target。也就是说，哪怕针对现代浏览器，它依然会注入大量并不需要的内容，从而使得构建产物的体积大大增加。针对这个问题， babel 团队也还在思考中: <a href="https://github.com/babel/babel/issues/10008" target="_blank" rel="noopener">https://github.com/babel/babel/issues/10008</a></p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>在 Babel &lt; 7.4.0 的时代，我们可能都听说过，由于 <code>@babel/polyfill</code> 有全局污染，如果我们在开发一个库/框架，我们只能使用 <code>@babel/runtime</code> 来进行部分 polyfill，其余部分需要库的使用者自行通过 <code>@babel/polyfill</code> 来完成。</p><p>现在，如果我们对文件大小不是特别敏感，我们可以不用顾虑这个问题了，直接使用 <code>@babel/preset-env</code> 来进行语法转换，剩余部分 <code>@babel/runtime</code> 即可完成。附上一份完整的 <code>babel.config.json</code> 配置供参考：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// babel.config.json</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="string">"presets"</span>: [</span><br><span class="line">        [</span><br><span class="line">            <span class="string">"@babel/preset-env"</span></span><br><span class="line">        ]</span><br><span class="line">    ],</span><br><span class="line">    <span class="string">"plugins"</span>: [</span><br><span class="line">        [</span><br><span class="line">            <span class="string">"@babel/plugin-transform-runtime"</span>, &#123;</span><br><span class="line">                <span class="string">"useESModules"</span>: <span class="literal">true</span>,</span><br><span class="line">                <span class="string">"corejs"</span>: &#123;</span><br><span class="line">                    <span class="string">"version"</span>: <span class="number">3</span>,</span><br><span class="line">                    <span class="string">"proposals"</span>: <span class="literal">true</span></span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">        ]</span><br><span class="line">    ]</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>如果我们的环境对性能有极致要求，我们则需要使用 <code>@babel/preset-env</code> 并设置 <code>useBuiltIns: &quot;usage&quot;</code> 来完成 ES6+ 的转换。而想要避免全局污染，则需手动引入我们代码中使用到的新特性相关的插件，如 <code>@babel/transform-es2015-classes</code>、 <code>@babel/transform-es2015-arrow-functions</code> 等。附上一份 <code>@babel/preset-env</code> 配置参考：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// babel.config.json</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="string">"presets"</span>: [</span><br><span class="line">        [</span><br><span class="line">            <span class="string">"@babel/preset-env"</span>,</span><br><span class="line">            &#123;</span><br><span class="line">              <span class="string">"modules"</span>: <span class="literal">false</span>,</span><br><span class="line">              <span class="string">"useBuiltIns"</span>: <span class="string">"usage"</span>,</span><br><span class="line">              <span class="string">"corejs"</span>: <span class="number">3</span>,</span><br><span class="line">              <span class="comment">// "targets": "&gt; 1%, not dead" // 推荐使用全局 browserslist</span></span><br><span class="line">            &#125;</span><br><span class="line">        ]</span><br><span class="line">    ]</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;去年这个时候，曾经写过一篇文章 &lt;a href=&quot;https://www.thjiang.com/2019/03/02/Polyfill-%E7%9A%84%E5%89%8D%E4%B8%96%E4%BB%8A%E7%94%9F/&quot;&gt;Polyfill 的前世今生&lt;/a&gt;，总
      
    
    </summary>
    
      <category term="前端工程" scheme="https://www.thjiang.com/categories/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B/"/>
    
    
      <category term="前端工程" scheme="https://www.thjiang.com/tags/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B/"/>
    
  </entry>
  
  <entry>
    <title>扔掉你的扩展坞 —— DELL U2720QM 4K 显示器开箱</title>
    <link href="https://www.thjiang.com/2020/02/27/%E6%89%94%E6%8E%89%E4%BD%A0%E7%9A%84%E6%89%A9%E5%B1%95%E5%9D%9E-%E2%80%94%E2%80%94-DELL-U2720QM-4K-%E6%98%BE%E7%A4%BA%E5%99%A8%E5%BC%80%E7%AE%B1/"/>
    <id>https://www.thjiang.com/2020/02/27/扔掉你的扩展坞-——-DELL-U2720QM-4K-显示器开箱/</id>
    <published>2020-02-27T12:56:37.000Z</published>
    <updated>2021-05-14T15:39:57.326Z</updated>
    
    <content type="html"><![CDATA[<p>自从去年末在苹果店里看到了 UltraFine 5K 之后，就觉得自己的 AOC U2790VQ 一点都不香了，但是9948的价格哪怕是24期免息也只能告辞。</p><p>UltraFine 4K 看起来倒是很合适，但是用惯了27寸，总觉得24寸的屏幕实在太小。看来看去，3000-5000 的价位只剩下3款备选：<code>DELL U2718Q</code>、<code>BenQ PD2700U</code>、<code>LG 27UL850</code>，对比了很久，还是觉得都不完美，正想着就明基算了，毕竟号称护眼。下单前论坛闲逛忽然发现 DELL 更新了 U2720Q，居然带有 90W Type-C 接口，虽然 MacBook Pro 16 的标配充电是 96W，90W 也勉强凑合吧，论坛内搜了一下，已经有了开箱体验，看起来还不错，果断决定入手。</p><p>官网联系客服，第一个就直接报价 3970 五年保修，听说有 3850 拿到的，但是换了几个售前，一个比一个高，懒得折腾了，3970 下单。2月17日付款，2.21日从厦门发货，嘉里大通物流2.23到杭州。</p><h2 id="开箱"><a href="#开箱" class="headerlink" title="开箱"></a>开箱</h2><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://c.icewish.top/const/blog/images/U2720QM/1.jpg!w960" alt="居然没有再包装，直接这样运输的" title="">                </div>                <div class="image-caption">居然没有再包装，直接这样运输的</div>            </figure><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://c.icewish.top/const/blog/images/U2720QM/2.jpg!w960" alt="居然没有再包装，直接这样运输的" title="">                </div>                <div class="image-caption">居然没有再包装，直接这样运输的</div>            </figure><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://c.icewish.top/const/blog/images/U2720QM/4.jpg!w960" alt="一些基本参数" title="">                </div>                <div class="image-caption">一些基本参数</div>            </figure><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://c.icewish.top/const/blog/images/U2720QM/27.jpg!w960" alt="配件全家桶，依次是电源线× 1、HDMI 线缆× 1、USB-C 线缆（C 转 A）× 1、Type-C 线缆（C 转 C） × 1，附带了校色报告，标称ΔE＜2" title="">                </div>                <div class="image-caption">配件全家桶，依次是电源线× 1、HDMI 线缆× 1、USB-C 线缆（C 转 A）× 1、Type-C 线缆（C 转 C） × 1，附带了校色报告，标称ΔE＜2</div>            </figure><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://c.icewish.top/const/blog/images/U2720QM/3.jpg!w960" alt="号称买支架送显示器的升降旋转支架" title="">                </div>                <div class="image-caption">号称买支架送显示器的升降旋转支架</div>            </figure><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://c.icewish.top/const/blog/images/U2720QM/5.jpg!w960" alt="背面接口依次是电源、HDMI 2.0、DP-in 1.4、Type-C(DP 1.4)、USB3.0（下行）、USB3.0（下行、BC1.2）" title="">                </div>                <div class="image-caption">背面接口依次是电源、HDMI 2.0、DP-in 1.4、Type-C(DP 1.4)、USB3.0（下行）、USB3.0（下行、BC1.2）</div>            </figure><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://c.icewish.top/const/blog/images/U2720QM/6.jpg!w960" alt="侧面还有2个接口，分别是 Type-C 和 USB3.0（下行）" title="">                </div>                <div class="image-caption">侧面还有2个接口，分别是 Type-C 和 USB3.0（下行）</div>            </figure><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://c.icewish.top/const/blog/images/U2720QM/7.jpg!w960" alt="正面照" title="">                </div>                <div class="image-caption">正面照</div>            </figure><h2 id="娱乐测试"><a href="#娱乐测试" class="headerlink" title="娱乐测试"></a>娱乐测试</h2><p>没有专业的校色软件，随便找了个网页的在线测试娱乐一下：</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://c.icewish.top/const/blog/images/U2720QM/8.jpg!w960" alt="红" title="">                </div>                <div class="image-caption">红</div>            </figure><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://c.icewish.top/const/blog/images/U2720QM/9.jpg!w960" alt="绿" title="">                </div>                <div class="image-caption">绿</div>            </figure><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://c.icewish.top/const/blog/images/U2720QM/10.jpg!w960" alt="蓝" title="">                </div>                <div class="image-caption">蓝</div>            </figure><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://c.icewish.top/const/blog/images/U2720QM/11.jpg!w960" alt="黄" title="">                </div>                <div class="image-caption">黄</div>            </figure><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://c.icewish.top/const/blog/images/U2720QM/12.jpg!w960" alt="青" title="">                </div>                <div class="image-caption">青</div>            </figure><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://c.icewish.top/const/blog/images/U2720QM/13.jpg!w960" alt="紫" title="">                </div>                <div class="image-caption">紫</div>            </figure><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://c.icewish.top/const/blog/images/U2720QM/14.jpg!w960" alt="白" title="">                </div>                <div class="image-caption">白</div>            </figure><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://c.icewish.top/const/blog/images/U2720QM/15.jpg!w960" alt="被这个漏光水平震惊到了，这真的是漏光戴的 IPS 吗？" title="">                </div>                <div class="image-caption">被这个漏光水平震惊到了，这真的是漏光戴的 IPS 吗？</div>            </figure><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://c.icewish.top/const/blog/images/U2720QM/16.jpg!w960" alt="十字线" title="">                </div>                <div class="image-caption">十字线</div>            </figure><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://c.icewish.top/const/blog/images/U2720QM/17.jpg!w960" alt="灰阶1" title="">                </div>                <div class="image-caption">灰阶1</div>            </figure><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://c.icewish.top/const/blog/images/U2720QM/18.jpg!w960" alt="灰阶2" title="">                </div>                <div class="image-caption">灰阶2</div>            </figure><p>没有找到在哪里看面板型号，工程模式里也没看到，盲猜应该是 <code>LM270WR3-SSA3</code> 或者 <code>LM270WR5-SSB1</code> ？</p><h2 id="对比-AOC-U2790VQ"><a href="#对比-AOC-U2790VQ" class="headerlink" title="对比 AOC U2790VQ"></a>对比 AOC U2790VQ</h2><p>和之前用的 AOC U2790VQ 对比一下：</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://c.icewish.top/const/blog/images/U2720QM/22.jpg!w960" alt="DELL" title="">                </div>                <div class="image-caption">DELL</div>            </figure><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://c.icewish.top/const/blog/images/U2720QM/23.jpg!w960" alt="AOC" title="">                </div>                <div class="image-caption">AOC</div>            </figure><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://c.icewish.top/const/blog/images/U2720QM/24.jpg!w960" alt="DELL" title="">                </div>                <div class="image-caption">DELL</div>            </figure><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://c.icewish.top/const/blog/images/U2720QM/25.jpg!w960" alt="AOC" title="">                </div>                <div class="image-caption">AOC</div>            </figure><p>两款都设置成 sRGB 模式，不知道我是不是瞎了，我竟然觉得看照片好像差不多？肉眼直接看上去还是有比较明显的差距的，DELL 的色彩明显更浓郁艳丽。</p><p>再来几张，左 DELL 右 AOC：</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://c.icewish.top/const/blog/images/U2720QM/19.jpg!w960" alt="对比" title="">                </div>                <div class="image-caption">对比</div>            </figure><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://c.icewish.top/const/blog/images/U2720QM/20.jpg!w960" alt="对比" title="">                </div>                <div class="image-caption">对比</div>            </figure><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://c.icewish.top/const/blog/images/U2720QM/21.jpg!w960" alt="对比" title="">                </div>                <div class="image-caption">对比</div>            </figure><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><h3 id="优势"><a href="#优势" class="headerlink" title="优势"></a>优势</h3><p>优点还是很明显的，出厂校准的 99% sRGB、95% DCI-P3，最重要的优点，如果你的设备支持 Type-C 输出，桌面上只需要一根 Type-C 线即可实现视频音频传输、USB-HUB、反向 90W 充电等功能，从此可以把各种绿联之类的辣鸡都扔掉了，目前 4000 的价位上应该没有其他能打的了。</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://c.icewish.top/const/blog/images/U2720QM/26.jpg!w960" alt="Type-C" title="">                </div>                <div class="image-caption">Type-C</div>            </figure><h3 id="不足"><a href="#不足" class="headerlink" title="不足"></a>不足</h3><ol><li>缺少一个 DP-out 接口</li></ol><p>各种宣传渠道都标出了的 DP1.4(支持MST菊花链) 功能根本没有，想要双显示器，必须再从电脑上再连一根 DP 或 HDMI 线，或者拿另一台支持 MST 的显示器作为主显示器。我查了一下，25 寸的版本 U2520DR 上反倒有这个功能，不懂为什么高端一点的版本反倒缩掉了这个接口。</p><ol><li>缺少一个 USB-B 上行接口</li></ol><p>虽然显示器附带了很多 USB 下行接口，但是要使用这些接口，必须使用 USB-C 转 USB-C 线或 USB-A 转 USB-C 线从电脑连接到显示器背后的 TYPE-C 接口，这样的话如果需要经常在 Mac 和 Windows 电脑之间切换，就必须频繁插拔这根线，在之前的众多型号（比如 U2718Q）上，是一个 USB-B 上行接口就解决了的问题，同样不知道为什么缩水掉了。</p><ol><li>售后和品控捉鸡</li></ol><p>之前一直听说戴尔显示器售后天下无双，无论什么渠道购买都一样，售后有专门的工程师一对一处理。</p><p>结果刚开箱就体验了一波，附送的 USB-A 转 USB-C 线是坏的。联系售后，客服一脸懵逼，转接给所谓的售后工程师，指导我拍了N张照片，插拔N次之后，我发现他居然连USB-B和USB-C都不认识，而且对相应的显示器型号、接口等完全不了解。最终证明给他是这根线有问题之后提出帮我换一根，催了几次之后才告诉我要第二天才能提交到系统上去，号称发戴尔内部物流，看不到进度，查不到什么时候发货，也不知道大约多久可以到，『您只需要耐心等待就可以了呢』。。。要不是觉得退回去会被别人买到一个二手的，真的好想直接退了再买个新的。</p><p>以后再也不会为了 200 块在戴尔官网买东西了，还是要去和东哥做兄弟。</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;自从去年末在苹果店里看到了 UltraFine 5K 之后，就觉得自己的 AOC U2790VQ 一点都不香了，但是9948的价格哪怕是24期免息也只能告辞。&lt;/p&gt;
&lt;p&gt;UltraFine 4K 看起来倒是很合适，但是用惯了27寸，总觉得24寸的屏幕实在太小。看来看去，
      
    
    </summary>
    
      <category term="剁手" scheme="https://www.thjiang.com/categories/%E5%89%81%E6%89%8B/"/>
    
    
      <category term="数码" scheme="https://www.thjiang.com/tags/%E6%95%B0%E7%A0%81/"/>
    
      <category term="开箱" scheme="https://www.thjiang.com/tags/%E5%BC%80%E7%AE%B1/"/>
    
  </entry>
  
  <entry>
    <title>不止于秒开 —— 在 Nginx 中开启 HTTP3/QUIC、TLS1.3、Brotli</title>
    <link href="https://www.thjiang.com/2019/11/21/%E4%B8%8D%E6%AD%A2%E4%BA%8E%E7%A7%92%E5%BC%80%20%E2%80%94%E2%80%94%20%E5%9C%A8-Nginx-%E4%B8%AD%E5%BC%80%E5%90%AF-HTTP3-QUIC%E3%80%81TLS1-3%E3%80%81Brotli/"/>
    <id>https://www.thjiang.com/2019/11/21/不止于秒开 —— 在-Nginx-中开启-HTTP3-QUIC、TLS1-3、Brotli/</id>
    <published>2019-11-21T10:45:26.000Z</published>
    <updated>2021-05-14T15:40:09.451Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>本文所用环境：<br>操作系统： Linux September 5.3.11-1.el7.elrepo.x86_64 GNU/Linux<br>内核版本： centos-release-7-7.1908.0.el7.centos.x86_64</p></blockquote><h2 id="HTTP3"><a href="#HTTP3" class="headerlink" title="HTTP3"></a>HTTP3</h2><p>目前，最新版本的 nginx （1.17.5） 仍然不能原生支持 HTTP3，好在 CloudFlare 提供了基于 Quiche 和 Boringssl 实现的一个 patch，使得我们可以在 nginx 上尝鲜一下。</p><p>开启过程如下：</p><h3 id="下载最新版本-nginx-并打上-quiche-的-patch"><a href="#下载最新版本-nginx-并打上-quiche-的-patch" class="headerlink" title="下载最新版本 nginx 并打上 quiche 的 patch"></a>下载最新版本 nginx 并打上 quiche 的 patch</h3><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><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">wget https:&#x2F;&#x2F;nginx.org&#x2F;download&#x2F;nginx-1.17.5.tar.gz</span><br><span class="line">tar xvzf tar xvzf</span><br><span class="line"></span><br><span class="line">git clone --recursive https:&#x2F;&#x2F;github.com&#x2F;cloudflare&#x2F;quiche</span><br><span class="line"></span><br><span class="line">cd nginx-1.17.5</span><br><span class="line"></span><br><span class="line">yum install patch</span><br><span class="line"></span><br><span class="line">patch -p01 &lt; ..&#x2F;quiche&#x2F;extras&#x2F;nginx&#x2F;nginx-1.16.patch</span><br></pre></td></tr></table></figure><h3 id="安装相关依赖"><a href="#安装相关依赖" class="headerlink" title="安装相关依赖"></a>安装相关依赖</h3><ol><li>CMAKE</li></ol><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 前往 https://cmake.org/files/ 选择最新版本下载</span></span><br><span class="line">wget https://cmake.org/files/v3.16/cmake-3.16.0-rc2.tar.gz</span><br><span class="line">tar xvzf cmake*</span><br><span class="line"><span class="comment"># 安装编译依赖</span></span><br><span class="line">yum install -y gcc gcc-c++ make automake openssl-devel</span><br><span class="line"><span class="comment"># 编译安装 cmake</span></span><br><span class="line"><span class="built_in">cd</span> cmake-3.16.0-rc2/</span><br><span class="line">./bootstrap</span><br><span class="line">gmake</span><br><span class="line">gmake install</span><br></pre></td></tr></table></figure><ol><li>PERL</li></ol><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 前往 https://www.cpan.org/src/ 选择最新版本下载</span></span><br><span class="line">wget https://www.cpan.org/src/5.0/perl-5.30.0.tar.gz</span><br><span class="line">tar -xzf perl-5.30.0.tar.gz</span><br><span class="line"></span><br><span class="line"><span class="comment"># 编译安装</span></span><br><span class="line"><span class="built_in">cd</span> perl-5.30.0</span><br><span class="line">./Configure -des -Dprefix=/usr/mysf</span><br><span class="line">make</span><br><span class="line">make <span class="built_in">test</span></span><br><span class="line">make install</span><br><span class="line"></span><br><span class="line"><span class="comment"># 建立新的软链接</span></span><br><span class="line">mv /usr/bin/perl /usr/bin/perl.bak</span><br><span class="line">ln -s /usr/mysf/bin/perl /usr/bin/perl</span><br><span class="line"></span><br><span class="line"><span class="comment"># 检查是否安装成功</span></span><br><span class="line">perl -v</span><br></pre></td></tr></table></figure><ol><li>Rust</li></ol><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="comment"># 为了支持 quiche，Rust 需要至少 1.39 版本</span></span><br><span class="line">yum install gcc clang go -y</span><br><span class="line">yum install rust cargo -y</span><br></pre></td></tr></table></figure><h3 id="安装-nginx"><a href="#安装-nginx" class="headerlink" title="安装 nginx"></a>安装 nginx</h3><p>在 nginx 的编译参数中加入如下模块，编译安装</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">--with-http_ssl_module  --with-http_v2_module  --with-http_v3_module  --with-openssl&#x3D;..&#x2F;quiche&#x2F;deps&#x2F;boringssl  --with-quiche&#x3D;..&#x2F;quiche</span><br></pre></td></tr></table></figure><p>安装完成之后，在 nginx 的配置文件中加入如下信息，重启 nginx 即可</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><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">server &#123;</span><br><span class="line">    ···</span><br><span class="line"></span><br><span class="line">    # 支持的 http3 和 quic 版本</span><br><span class="line">    add_header alt-svc  &#39;quic&#x3D;&quot;:443&quot;; ma&#x3D;2592000; v&#x3D;&quot;46,43&quot;, h3-Q050&#x3D;&quot;:443&quot;; ma&#x3D;2592000, h3-Q049&#x3D;&quot;:443&quot;; ma&#x3D;2592000, h3-Q048&#x3D;&quot;:443&quot;; ma&#x3D;2592000,h3-Q046&#x3D;&quot;:443&quot;; ma&#x3D;2592000, h3-Q043&#x3D;&quot;:443&quot;; ma&#x3D;2592000, h3-23&#x3D;&quot;:443&quot;; ma&#x3D;2592000&#39;</span><br><span class="line"></span><br><span class="line">    ···</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>完成之后，我们可以在这里查看配置是否成功： <a href="https://http3check.net/" target="_blank" rel="noopener">https://http3check.net/</a></p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://c.icewish.top/const/blog/images/http3/http3check.jpg" alt="check" title="">                </div>                <div class="image-caption">check</div>            </figure><h2 id="TLS1-3"><a href="#TLS1-3" class="headerlink" title="TLS1.3"></a>TLS1.3</h2><h3 id="安装-OpenSSL"><a href="#安装-OpenSSL" class="headerlink" title="安装 OpenSSL"></a>安装 OpenSSL</h3><p>TLS1.3 需要 OpenSSL 1.1.1 以上版本，系统自带版本一般较旧，我们需要重新安装（前文所述的 quiche patch 中已经内置了 Boringssl，不需要再重新安装）</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">yum install gc gcc gcc-c++ pcre-devel zlib-devel make wget openssl-devel libxml2-devel libxslt-devel gd-devel perl-ExtUtils-Embed GeoIP-devel gperftools gperftools-devel libatomic_ops-devel perl-ExtUtils-Embed dpkg-dev libpcrecpp0 libgd2-xpm-dev libgeoip-dev libperl-dev -y</span><br></pre></td></tr></table></figure><p>然后从官网 <a href="https://www.openssl.org/source/" target="_blank" rel="noopener">https://www.openssl.org/source/</a> 获取最新的 OpenSSL 下载链接，下载并解压编译安装：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><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="built_in">cd</span> /usr/src</span><br><span class="line">wget https://www.openssl.org/<span class="built_in">source</span>/openssl-1.1.1d.tar.gz</span><br><span class="line">tar -zxf openssl-1.1.1d.tar.gz</span><br><span class="line"><span class="built_in">cd</span> openssl-1.1.1d.tar.gz</span><br><span class="line">./config</span><br><span class="line">make</span><br><span class="line">make <span class="built_in">test</span></span><br><span class="line">make install</span><br><span class="line"></span><br><span class="line"><span class="comment"># 备份原来的openssl</span></span><br><span class="line">mv /usr/bin/openssl /root/</span><br><span class="line">ln -s /usr/<span class="built_in">local</span>/ssl/bin/openssl /usr/bin/openssl</span><br></pre></td></tr></table></figure><p>完成后使用openssl version查看版本，如出现 /usr/bin/openssl: No such file or directory，可以使用如下方式解决：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">ln -s /usr/<span class="built_in">local</span>/lib64/libssl.so.1.1 /usr/lib64/libssl.so.1.1</span><br><span class="line">ln -s /usr/<span class="built_in">local</span>/lib64/libcrypto.so.1.1 /usr/lib64/libcrypto.so.1.1</span><br><span class="line"><span class="comment"># 删除旧的符号链接</span></span><br><span class="line">rm /bin/openssl</span><br><span class="line"><span class="comment"># 添加新版本的符号链接</span></span><br><span class="line">ln -s /usr/<span class="built_in">local</span>/bin/openssl /bin/openssl</span><br><span class="line"><span class="comment"># 重新查看版本</span></span><br><span class="line">openssl version</span><br><span class="line"><span class="comment"># OpenSSL 1.1.1d  10 Sep 2019</span></span><br></pre></td></tr></table></figure><p>使用最新的 OpenSSL 重新编译 nginx：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 在 nginx 编译信息中加入如下参数</span></span><br><span class="line">--with-openssl=/usr/src/openssl-1.1.1d</span><br></pre></td></tr></table></figure><p>安装完成之后，修改 nginx 配置：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">server &#123;</span><br><span class="line">    ···</span><br><span class="line"></span><br><span class="line">    ssl_protocols TLSv1.2 TLSv1.3;</span><br><span class="line">    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;</span><br><span class="line">    ssl_prefer_server_ciphers off;</span><br><span class="line"></span><br><span class="line">    # 如果只需要兼容现代浏览器，也可以如下配置：</span><br><span class="line">    # ssl_protocols TLSv1.3;</span><br><span class="line">    # ssl_prefer_server_ciphers off;</span><br><span class="line"></span><br><span class="line">    ···</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>重启 nginx，打开网站，在控制台 - Security - Overview - Connection 中可查看 TLS1.3 是否开启成功，如图所示：</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://c.icewish.top/const/blog/images/http3/tls13check.jpg" alt="check" title="">                </div>                <div class="image-caption">check</div>            </figure><h2 id="Brotli"><a href="#Brotli" class="headerlink" title="Brotli"></a>Brotli</h2><p>下载 Brotli：<br><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">git <span class="built_in">clone</span> https://github.com/google/ngx_brotli</span><br><span class="line"><span class="built_in">cd</span> ngx_brotli</span><br><span class="line">git submodule update --init</span><br></pre></td></tr></table></figure></p><p>在 nginx 的编译参数中加入 <code>--add-dynamic-module=/path/to/ngx_brotli</code>，重新编译安装 nginx，即可使 nginx 支持 brotli 压缩。</p><p>新版本的 nginx 支持动态模块，也可以只编译 brotli 模块。</p><p>安装完成后，在 nginx 配置文件中加入如下信息，重启 nginx，访问网站，应该就可以看到压缩方式为 br 了。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line">http &#123;</span><br><span class="line">    ...</span><br><span class="line"></span><br><span class="line">    # brotli</span><br><span class="line">    brotli on;</span><br><span class="line">    brotli_comp_level 6;</span><br><span class="line">    brotli_buffers 16 8k;</span><br><span class="line">    brotli_min_length 20;</span><br><span class="line">    brotli_types text&#x2F;plain text&#x2F;css application&#x2F;json application&#x2F;x-javascript text&#x2F;xml application&#x2F;xml application&#x2F;xml+rss text&#x2F;javascript application&#x2F;javascript image&#x2F;svg+xml;</span><br><span class="line"></span><br><span class="line">    ...</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h3><p><a href="https://www.mf8.biz/nginx-install-tls1-3/" target="_blank" rel="noopener">https://www.mf8.biz/nginx-install-tls1-3/</a><br><a href="https://zach.vip/server/%e4%bd%bf%e7%94%a8cloudflare%e7%9a%84quic%e5%ae%9e%e7%8e%b0quiche%e9%83%a8%e7%bd%b2nginx%e7%9a%84http3-quic%e5%8d%8f%e8%ae%ae/" target="_blank" rel="noopener">https://zach.vip/server/%e4%bd%bf%e7%94%a8cloudflare%e7%9a%84quic%e5%ae%9e%e7%8e%b0quiche%e9%83%a8%e7%bd%b2nginx%e7%9a%84http3-quic%e5%8d%8f%e8%ae%ae/</a></p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;blockquote&gt;
&lt;p&gt;本文所用环境：&lt;br&gt;操作系统： Linux September 5.3.11-1.el7.elrepo.x86_64 GNU/Linux&lt;br&gt;内核版本： centos-release-7-7.1908.0.el7.centos.x86_64&lt;/
      
    
    </summary>
    
    
      <category term="性能优化" scheme="https://www.thjiang.com/tags/%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/"/>
    
  </entry>
  
  <entry>
    <title>你可能不需要协商缓存</title>
    <link href="https://www.thjiang.com/2019/08/17/%E4%BD%A0%E5%8F%AF%E8%83%BD%E4%B8%8D%E9%9C%80%E8%A6%81%E5%8D%8F%E5%95%86%E7%BC%93%E5%AD%98/"/>
    <id>https://www.thjiang.com/2019/08/17/你可能不需要协商缓存/</id>
    <published>2019-08-17T08:56:51.000Z</published>
    <updated>2019-09-11T03:54:35.652Z</updated>
    
    <content type="html"><![CDATA[<p>为了提升页面加载性能，改善用户体验，我们希望我们的静态资源能够长期缓存在用户的浏览器中，为此，我们通常会给静态资源设置一个较长的 <code>max-age</code> ，而在资源内容有更新时，则生成一个名字不同的新文件，以便用户能够及时收到最新的内容。</p><p>我们都知道， HTTP 缓存可以分为强缓存和协商缓存，根据我们的配置，强缓存过期后，浏览器会向服务器发出一个校验请求，如果服务器判断资源没有更新过，则通知浏览器使用缓存，也就是我们常说的 304 响应。</p><h2 id="Immutable"><a href="#Immutable" class="headerlink" title="Immutable"></a>Immutable</h2><p>但是在现代前端工程体系下，一个静态资源从发布之日起，可能永远也没有更新的需求了（更新后文件名/版本号/hash值等参数一定会变，也就是相当于一个全新的资源了）。但是即使我们配置了较长的 max-age ，用户在刷新页面的时候，浏览器依然会向服务器发出请求，在用户体量较大的时候，对网络资源也有着极大的浪费。按照 Facebook 的统计，大约有 20% 的请求是响应了 304 的。为此， Facebook 建议给 <code>Cache-Control</code> 增加一个新的属性： <code>dont-revalidate</code> ，在响应头里加入该字段表示该资源永不过期，不需要再发送条件请求。这个字段最终被 Firefox 49 根据 <a href="https://tools.ietf.org/html/rfc7234#section-5.2.3" target="_blank" rel="noopener">Cache Control Extensions</a> 规范实现为一个 <code>Behavioral extension</code>： <code>immutable</code> 。我们可以这样使用：</p><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Cache-Control: max-age=31536000, immutable</span><br></pre></td></tr></table></figure><p>对于支持 immutable 的浏览器，会一直使用本地的缓存，而对于尚未支持的浏览器，也可以根据 max-age 平滑的降级。</p><h3 id="浏览器支持情况"><a href="#浏览器支持情况" class="headerlink" title="浏览器支持情况"></a>浏览器支持情况</h3><blockquote><ul><li>Firefox： 49+ （仅支持HTTPS请求）</li><li>Microsoft Edge： 15+</li><li>Safari： 24+</li><li>Chrome： 不支持，Chrome 自己实现了另一个解决这个问题的方案</li></ul></blockquote><h2 id="Chrome-中的刷新"><a href="#Chrome-中的刷新" class="headerlink" title="Chrome 中的刷新"></a>Chrome 中的刷新</h2><p>在看 Chrome 的新方案之前，我们先来看一下浏览器在刷新时做了什么：</p><h3 id="F5"><a href="#F5" class="headerlink" title="F5"></a>F5</h3><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://c.icewish.top/const/blog/images/cache/chromef5.jpg" alt="F5" title="">                </div>                <div class="image-caption">F5</div>            </figure><h3 id="Ctrl-F5"><a href="#Ctrl-F5" class="headerlink" title="Ctrl + F5"></a>Ctrl + F5</h3><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://c.icewish.top/const/blog/images/cache/ctrl+f5.jpg" alt="Ctrl + F5" title="">                </div>                <div class="image-caption">Ctrl + F5</div>            </figure><p>可以看到，在按下 F5 时，Chrome 为请求加上了 <code>max-age=0</code> 的 Cache-Control，从而保证至少要向服务器进行一次过期校验。而在 Ctrl + F5 时，则更为激进的直接设置了 <code>no-cache</code>。</p><p>这里有一张较详尽的表格可以参考：</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://c.icewish.top/const/blog/images/cache/refresh.jpg" alt="各浏览器刷新表现" title="">                </div>                <div class="image-caption">各浏览器刷新表现</div>            </figure><p>好像和 immutable 关系不大？我们再来看看 Chrome 中静态资源的刷新：</p><h3 id="静态资源刷新"><a href="#静态资源刷新" class="headerlink" title="静态资源刷新"></a>静态资源刷新</h3><p>为了方便测试，我们给资源设置了一个 8 秒的 max-age，等待 3 秒后，我们按下 F5：</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://c.icewish.top/const/blog/images/cache/static1.jpg" alt="max-age过期前刷新" title="">                </div>                <div class="image-caption">max-age过期前刷新</div>            </figure><p>好像有哪里不对？说好的 max-age=0 呢？说好的 304 呢？</p><p>一番操作之后我们找到了这里：<br><a href="https://bugs.chromium.org/p/chromium/issues/detail?id=654378" target="_blank" rel="noopener">https://bugs.chromium.org/p/chromium/issues/detail?id=654378</a><br><a href="https://bugs.chromium.org/p/chromium/issues/detail?id=611416" target="_blank" rel="noopener">https://bugs.chromium.org/p/chromium/issues/detail?id=611416</a></p><p>可以看到，在 Chrome 54+ 版本中，Chrome 开始在刷新页面时只为 <code>main resource</code> 也就是我们的 html 页面加上 <code>max-age=0</code> 的设置，其他资源会遵循资源自身设定的缓存策略。通过修改 reload 的行为， Chrome 同样大量降低了刷新页面带来的带宽消耗。</p><p>还是上面的页面，我们等待 10 秒之后再刷新一次验证一下：</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://c.icewish.top/const/blog/images/cache/static2.jpg" alt="max-age过期后刷新" title="">                </div>                <div class="image-caption">max-age过期后刷新</div>            </figure><p>可以看到， max-age=8 的静态资源响应变成了 304 ，符合我们的预期。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>由于我们的静态资源更新依赖于 html 中资源地址的更新，通常我们会对 html 文件设置为不缓存或仅缓存极短的时间。而绝大部分静态资源本身自发布起就不再需要更新了，所以我们可以大胆的将静态资源的缓存时间设置的足够长（比如一年），同时加入 immutable 属性，这样，几乎所有的现代浏览器都会帮我们取消缓存协商的过程，从而节约大量协商请求的带宽和流量。</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;为了提升页面加载性能，改善用户体验，我们希望我们的静态资源能够长期缓存在用户的浏览器中，为此，我们通常会给静态资源设置一个较长的 &lt;code&gt;max-age&lt;/code&gt; ，而在资源内容有更新时，则生成一个名字不同的新文件，以便用户能够及时收到最新的内容。&lt;/p&gt;
&lt;p&gt;我
      
    
    </summary>
    
      <category term="前端工程" scheme="https://www.thjiang.com/categories/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B/"/>
    
    
      <category term="前端工程" scheme="https://www.thjiang.com/tags/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B/"/>
    
      <category term="性能优化" scheme="https://www.thjiang.com/tags/%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/"/>
    
  </entry>
  
  <entry>
    <title>解放你的 node_modules —— Plug&#39;n&#39;Play</title>
    <link href="https://www.thjiang.com/2019/07/13/%E8%A7%A3%E6%94%BE%E4%BD%A0%E7%9A%84-node-modules-%E2%80%94%E2%80%94-Plug-n-Play/"/>
    <id>https://www.thjiang.com/2019/07/13/解放你的-node-modules-——-Plug-n-Play/</id>
    <published>2019-07-13T13:21:05.000Z</published>
    <updated>2019-07-26T07:29:13.572Z</updated>
    
    <content type="html"><![CDATA[<h3 id="node-模块现状"><a href="#node-模块现状" class="headerlink" title="node 模块现状"></a>node 模块现状</h3><p>在我们执行 npm install 时，npm 会做出如下操作：</p><ol><li>向 registry 查询获取模块的地址</li><li>根据 package.json 中的配置确定需要安装的模块版本</li><li>下载对应的压缩包，存放在 <code>~/.npm</code> 目录</li><li>解压压缩包到当前项目的 <code>node_modules</code> 目录</li></ol><p>而在 Node.js 中，我们提供给 require 方法的参数如果不是一个路径，也不是 node 的核心模块， node 将试图去当前目录的 node_modules 文件夹里搜索。如果当前目录的 node_modules 里没有找到， node 会继续试图在父目录的 node_modules 里搜索，这样递归下去直到根目录。</p><p>这些过程都需要进行大量的文件 I/O 操作，这无疑是非常低效的。为了解决这些问题，Facebook 提出了 Plug’n’Play(PnP) 方案。</p><h3 id="PnP-原理"><a href="#PnP-原理" class="headerlink" title="PnP 原理"></a>PnP 原理</h3><p>在 Yarn 中，当我们开启 PnP 后，Yarn 会生成一个 <code>.png.js</code> 文件来描述项目的依赖信息和所需模块的查找路径。同时，项目目录下不再需要一个 node_modules 目录，取而代之的是一个全局的缓存目录，项目所需依赖都可以从这个目录中获取。</p><h3 id="使用方法"><a href="#使用方法" class="headerlink" title="使用方法"></a>使用方法</h3><p>可以在项目目录下执行 <code>yarn --pnp</code> ，或直接在 package.json 中修改如下字段开启 PnP：<br><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">&#123;</span><br><span class="line">    <span class="attr">"installConfig"</span>: &#123;</span><br><span class="line">        <span class="attr">"pnp"</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><p>如果你使用 CRA 来创建项目，也可以直接在命令中加入 <code>--use-pnp</code>。</p><h3 id="PnP-速度"><a href="#PnP-速度" class="headerlink" title="PnP 速度"></a>PnP 速度</h3><p>按照官方的说法， <code>Generating it makes up for more than 70% of the time needed to run yarn install with a hot cache.</code></p><p>在我的一个实际项目中，使用 npm i、yarn 和 PnP 安装依赖完成时间分别为 26.48s、19.71s 和 11.25s，提升极为可观。</p><h3 id="扩展"><a href="#扩展" class="headerlink" title="扩展"></a>扩展</h3><p><a href="https://pnpm.js.org/" target="_blank" rel="noopener">pnpm</a></p><p>pnpm 使用了 symlink 来记录模块路径，如下所示：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><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">-&gt; - a symlink (or junction on Windows)</span><br><span class="line"></span><br><span class="line">node_modules</span><br><span class="line">├─ foo -&gt; .registry.npmjs.org&#x2F;foo&#x2F;1.0.0&#x2F;node_modules&#x2F;foo</span><br><span class="line">└─ .registry.npmjs.org</span><br><span class="line">   ├─ foo&#x2F;1.0.0&#x2F;node_modules</span><br><span class="line">   |  ├─ bar -&gt; ..&#x2F;..&#x2F;bar&#x2F;2.0.0&#x2F;node_modules&#x2F;bar</span><br><span class="line">   |  └─ foo</span><br><span class="line">   |     ├─ index.js</span><br><span class="line">   |     └─ package.json</span><br><span class="line">   └─ bar&#x2F;2.0.0&#x2F;node_modules</span><br><span class="line">      └─ bar</span><br><span class="line">         ├─ index.js</span><br><span class="line">         └─ package.json</span><br></pre></td></tr></table></figure><p>pnpm 在维护了模块层级的同时大幅度提升了安装速度，但是由于它的实现，无法保证和 npm 行为一致。</p><p><a href="https://github.com/npm/tink" target="_blank" rel="noopener">tink</a></p><p>tink 是 npm 官方提出的一种类似 PnP 的解决方案，和 <code>.pnp.js</code> 文件类似， tink 会在项目中生成一个  <code>.package-map.json</code> 文件用来记录各安装包内文件的 hash 值。目前 tink 依然处于测试阶段，在 npm 8 中我们将能尝试这一特性。</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h3 id=&quot;node-模块现状&quot;&gt;&lt;a href=&quot;#node-模块现状&quot; class=&quot;headerlink&quot; title=&quot;node 模块现状&quot;&gt;&lt;/a&gt;node 模块现状&lt;/h3&gt;&lt;p&gt;在我们执行 npm install 时，npm 会做出如下操作：&lt;/p&gt;
&lt;ol&gt;

      
    
    </summary>
    
    
      <category term="前端工程" scheme="https://www.thjiang.com/tags/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B/"/>
    
  </entry>
  
  <entry>
    <title>Polyfill 的前世今生</title>
    <link href="https://www.thjiang.com/2019/03/02/Polyfill-%E7%9A%84%E5%89%8D%E4%B8%96%E4%BB%8A%E7%94%9F/"/>
    <id>https://www.thjiang.com/2019/03/02/Polyfill-的前世今生/</id>
    <published>2019-03-02T08:12:38.000Z</published>
    <updated>2019-03-02T12:04:13.537Z</updated>
    
    <content type="html"><![CDATA[<p>我们知道，ES2015+ 在低版本浏览器中存在严重的兼容问题，为此前人们尝试了各种方式：</p><h2 id="Shim-Sham"><a href="#Shim-Sham" class="headerlink" title="Shim/Sham"></a>Shim/Sham</h2><p>在远古时代的代码中，我们可能看到过这样的处理：</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">"https://cdnjs.cloudflare.com/ajax/libs/es5-shim/0.34.2/es5-shim.min.js"</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">"https://cdnjs.cloudflare.com/ajax/libs/es5-shim/0.34.2/es5-sham.min.js"</span>&gt;</span><span class="tag">&lt;/<span class="name">script</span>&gt;</span></span><br></pre></td></tr></table></figure><p><code>shim</code> 仅依靠旧版浏览器中现有的 API，实现了一些新版本 ES 的特性，而 <code>sham</code> 则是仅仅为了保证新版本特性能在浏览器中不抛出错误（因为有些新特性在低版本浏览器中根本无法实现）。</p><h2 id="Babel"><a href="#Babel" class="headerlink" title="Babel"></a>Babel</h2><p>为了彻底解决上面的问题，<code>Babel</code> 诞生了。Babel 这个名字指的是巴比伦文明里面的通天塔，在那里，人们只说同一种语言。而在前端工程中，我们可以依靠 Babel 将 ES6+ 的语法转换为旧浏览器可以识别的版本。</p><p>在浏览器端，Babel 提供了一个运行时的转换器 <code>babel-standlone</code> ，它内置了大量的插件，所以可以直接在浏览器中运行并编译特定标签（type=”text/babel” 的 script 标签）内的代码。</p><p>而在 Webpack 中，我们则可以使用 <code>babel-loader</code> 来完成语法的转换。</p><h2 id="Polyfill"><a href="#Polyfill" class="headerlink" title="Polyfill"></a>Polyfill</h2><p>但是 Babel 并不是万能的，它默认只转换 JavaScript 语法，而不转换新的 API，比如 Promise、Set、Map 等全局对象， Array.from、Object.assign 等全局静态函数和 Array.prototype.includes 等 实例方法也不会被转码。为了解决这个问题，我们需要使用 polyfill。</p><p>polyfill 可以认为是 shim 的一种，和常规的 shim 不同的是，它致力于抹平不同浏览器之间的差异。</p><h3 id="polyfill-io"><a href="#polyfill-io" class="headerlink" title="polyfill.io"></a>polyfill.io</h3><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&lt;script src&#x3D;&quot;https:&#x2F;&#x2F;cdn.polyfill.io&#x2F;v2&#x2F;polyfill.min.js&quot;&gt;&lt;&#x2F;script&gt;</span><br></pre></td></tr></table></figure><p>我们可以在页面中引入这样一个 polyfill.io 提供的服务来实现最简单的 polyfill。服务器会根据用户浏览器的 UA 来判断对新特性的支持程度而返回不同的 polyfill 文件，如果用户的浏览器足够强大，那么这个服务将不返回任何内容，做到了一定意义上的按需加载。</p><p>但是它的缺点也很明显，这种方式并不能按照代码所用到的新特性按需进行 polyfill，而是会 polyfill 用户浏览器不支持的所有特性。</p><h3 id="babel-plugins"><a href="#babel-plugins" class="headerlink" title="babel-plugins"></a>babel-plugins</h3><p>babel 为我们提供了几乎所有新特性的插件，比如 <code>@babel/transform-es2015-classes</code>、 <code>@babel/transform-es2015-arrow-functions</code> 等，使用方式如下：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><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">&#x2F;&#x2F; .babelrc</span><br><span class="line">&#123;</span><br><span class="line">    &quot;presets&quot;: [</span><br><span class="line">        [</span><br><span class="line">            &quot;@babel&#x2F;env&quot;</span><br><span class="line">        ]</span><br><span class="line">    ],</span><br><span class="line">    &quot;plugins&quot;: [</span><br><span class="line">        &quot;@babel&#x2F;plugin-transform-object-assign&quot;,</span><br><span class="line">        &quot;@babel&#x2F;transform-es2015-arrow-functions&quot;</span><br><span class="line">    ]</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这样，babel 就可以帮我们转换指定的特性了，可以看一下下面的栗子：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> foo = <span class="function">(<span class="params">a, b</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="built_in">Object</span>.assign(a, b);</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 打包后</span></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">_extends</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">    _extends =</span><br><span class="line">        <span class="built_in">Object</span>.assign ||</span><br><span class="line">        <span class="function"><span class="keyword">function</span>(<span class="params">target</span>) </span>&#123;</span><br><span class="line">            <span class="keyword">for</span> (<span class="keyword">var</span> i = <span class="number">1</span>; i &lt; <span class="built_in">arguments</span>.length; i++) &#123;</span><br><span class="line">                <span class="keyword">var</span> source = <span class="built_in">arguments</span>[i];</span><br><span class="line">                <span class="keyword">for</span> (<span class="keyword">var</span> key <span class="keyword">in</span> source) &#123;</span><br><span class="line">                    <span class="keyword">if</span> (</span><br><span class="line">                        <span class="built_in">Object</span>.prototype.hasOwnProperty.call(</span><br><span class="line">                            source,</span><br><span class="line">                            key</span><br><span class="line">                        )</span><br><span class="line">                    ) &#123;</span><br><span class="line">                        target[key] = source[key];</span><br><span class="line">                    &#125;</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">return</span> target;</span><br><span class="line">        &#125;;</span><br><span class="line">    <span class="keyword">return</span> _extends.apply(<span class="keyword">this</span>, <span class="built_in">arguments</span>);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> foo = <span class="function"><span class="keyword">function</span> <span class="title">foo</span>(<span class="params">a, b</span>) </span>&#123;</span><br><span class="line">    <span class="keyword">return</span> _extends(a, b);</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>可以看到，@babel/plugin-transform-object-assign 使用 ES5 自己实现了一份 Object.assign 并插入到了我们的代码之前。</p><p>但是这样做有两个问题，一是在不同的模块中如果都使用了 Object.assign，它可能在每个使用到的模块中都被实现一次并将这些 helpers 代码添加到模块顶部，会大幅增加构建包的体积；另一个问题则是我们需要手动为我们用到的特性添加所有的插件。</p><p>在这个背景下， <code>@babel/plugin-transform-runtime</code> 应运而生。在 Babel 的插件配置中，我们只需要引入一个 @babel/plugin-transform-runtime ，它会自动检测我们代码中需要转换的部分并重写，例如 Object.assign 会被重写为 _extends 并引入，而重复的模块也会被抽离成一份。配置如下：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><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">&#x2F;&#x2F; .babelrc</span><br><span class="line">&#123;</span><br><span class="line">    &quot;presets&quot;: [</span><br><span class="line">        [</span><br><span class="line">            &quot;@babel&#x2F;env&quot;</span><br><span class="line">        ]</span><br><span class="line">    ],</span><br><span class="line">    &quot;plugins&quot;: [</span><br><span class="line">        &quot;@babel&#x2F;plugin-transform-runtime&quot;</span><br><span class="line">    ]</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>和单独引入各个插件一样，transform-runtime 同样存在模块级别的限制，因此它也依然无法转换 Array.prototype.includes 这样的方法。</p><h3 id="babel-polyfill"><a href="#babel-polyfill" class="headerlink" title="babel-polyfill"></a>babel-polyfill</h3><p>对于这种全局方法，Babel 提供了 babel-polyfill，我们可以这样引入：<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&lt;script src&#x3D;&quot;https:&#x2F;&#x2F;cdnjs.cloudflare.com&#x2F;ajax&#x2F;libs&#x2F;babel-polyfill&#x2F;7.2.5&#x2F;polyfill.js&quot;&gt;&lt;&#x2F;script&gt;</span><br></pre></td></tr></table></figure></p><p>显然，这样会全量引入所有的补丁，不论你的项目是否需要。</p><p>也可以这样使用 npm 包的形式加载：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><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">import &#39;@babel&#x2F;polyfill&#39;;</span><br><span class="line"></span><br><span class="line">const test &#x3D; [1, 2, 3].includes(1);</span><br><span class="line"></span><br><span class="line">&#x2F;&#x2F; 打包后</span><br><span class="line">$export($export.P, &#39;Array&#39;, &#123;</span><br><span class="line">  includes: function includes(el &#x2F;* , fromIndex &#x3D; 0 *&#x2F;) &#123;</span><br><span class="line">    return $includes(this, el, arguments.length &gt; 1 ? arguments[1] : undefined);</span><br><span class="line">  &#125;</span><br><span class="line">&#125;);</span><br><span class="line"></span><br><span class="line">const test &#x3D; [1, 2, 3].includes(1);</span><br></pre></td></tr></table></figure><p>和使用 script 标签引入类似，全量引入后，这个仅有2行的原始文件，打包后的大小变成了 246K：<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><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">Build completed in 1.286s</span><br><span class="line"></span><br><span class="line">Hash: 3103b6ec6e9cb29c304b</span><br><span class="line">Version: webpack 4.29.6</span><br><span class="line">Time: 1464ms</span><br><span class="line">Built at: 2019-03-01 16:05:13</span><br><span class="line">        Asset      Size  Chunks                    Chunk Names</span><br><span class="line">       app.js   246 KiB     app  [emitted]  [big]  app</span><br></pre></td></tr></table></figure></p><p>为了实现按需引入，我们需要依靠 <code>@babel/env</code>，而在 Babel7 中，提供了 <code>useBuiltIns: usage</code> 这样一个新配置。结合配置如下：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><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">&quot;presets&quot;: [</span><br><span class="line">    [</span><br><span class="line">        &quot;@babel&#x2F;env&quot;,</span><br><span class="line">        &#123;</span><br><span class="line">            &quot;useBuiltIns&quot;: &quot;usage&quot;,</span><br><span class="line">            &quot;targets&quot;: &quot;&gt; 1%, last 3 versions, not ie &lt;&#x3D; 7&quot;</span><br><span class="line">        &#125;</span><br><span class="line">    ]</span><br><span class="line">]</span><br></pre></td></tr></table></figure><p>useBuiltIns 配置为 entry 时， 会根据指定的 targets 中的 browserlist 将polyfills拆分引入，仅引入有浏览器不支持的polyfill。而配置为 usage ，babel 则会检测代码中新特性等的使用情况，仅仅加载代码中用到的 polyfills，同时，不需要再手动 <code>import &#39;@babel/polyfill&#39;</code>。browserlist 的配置可以在这里查看： <a href="https://browserl.ist/" target="_blank" rel="noopener">https://browserl.ist/</a></p><p>重新打包，app.js 恢复了正常大小：<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><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">Build completed in 0.769s</span><br><span class="line"></span><br><span class="line">Hash: 21afcbb413dad4242589</span><br><span class="line">Version: webpack 4.29.6</span><br><span class="line">Time: 946ms</span><br><span class="line">Built at: 2019-03-01 16:13:23</span><br><span class="line">        Asset      Size  Chunks             Chunk Names</span><br><span class="line">       app.js  22.8 KiB     app  [emitted]  app</span><br></pre></td></tr></table></figure></p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>对于常规项目来说，我们可以手动指定要兼容到的 targets，进行如下配置来保证尽可能的安全并且按需加载：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><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">&#123;</span><br><span class="line">    &quot;presets&quot;: [</span><br><span class="line">        [</span><br><span class="line">            &quot;@babel&#x2F;env&quot;,</span><br><span class="line">            &#123;</span><br><span class="line">                &quot;modules&quot;: false, &#x2F;&#x2F; 使用 es modules</span><br><span class="line">                &quot;useBuiltIns&quot;: &quot;usage&quot;,</span><br><span class="line">                &quot;targets&quot;: &quot;&gt; 1%, last 3 versions, not ie &lt;&#x3D; 7&quot;</span><br><span class="line">            &#125;</span><br><span class="line">        ]</span><br><span class="line">    ],</span><br><span class="line">    &quot;plugins&quot;: [</span><br><span class="line">        &quot;@babel&#x2F;plugin-transform-runtime&quot;</span><br><span class="line">    ]</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>而如果在开发的是一个库，为了避免全局的污染，则只能使用 transform-runtime 的方案，这种方案不能解决 Array.prototype.includes 等实例方法的转换，如果库中有应用，需要在文档中特意提醒开发者注意。</p><h2 id="展望"><a href="#展望" class="headerlink" title="展望"></a>展望</h2><p>在HTML5中，有一个新 API <code>module</code>，我们可以使用这个特性来检测浏览器对 ES6的支持程度，例如，支持 <code>&lt;script type=&quot;module&quot;&gt;</code> 的浏览器也支持 async、await、Class、Promise 等新特性。</p><p>因此，我们可以打包出2份 JavaScript 文件同时引入：</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">type</span>=<span class="string">"module"</span> <span class="attr">src</span>=<span class="string">"app.js"</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">nomodule</span> <span class="attr">src</span>=<span class="string">"app-legacy.js"</span>&gt;</span><span class="tag">&lt;/<span class="name">script</span>&gt;</span></span><br></pre></td></tr></table></figure><p>大部分现代浏览器都会自动识别 script 标签中的 <code>type=&quot;module&quot;</code> 和 <code>nomodule</code> 属性，加载 app.js 忽略 app-legacy.js。</p><p>我们只需在 app-legacy.js 中添加 polyfill 即可，这样，在兼容低版本浏览器的同时，我们也将在现代浏览器中体验到巨大的性能提升。</p><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><p><a href="https://www.babeljs.cn/docs/usage/polyfill/" target="_blank" rel="noopener">https://www.babeljs.cn/docs/usage/polyfill/</a><br><a href="https://github.com/sorrycc/blog/issues/80" target="_blank" rel="noopener">https://github.com/sorrycc/blog/issues/80</a><br><a href="https://segmentfault.com/a/1190000010106158" target="_blank" rel="noopener">https://segmentfault.com/a/1190000010106158</a></p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;我们知道，ES2015+ 在低版本浏览器中存在严重的兼容问题，为此前人们尝试了各种方式：&lt;/p&gt;
&lt;h2 id=&quot;Shim-Sham&quot;&gt;&lt;a href=&quot;#Shim-Sham&quot; class=&quot;headerlink&quot; title=&quot;Shim/Sham&quot;&gt;&lt;/a&gt;Shim/Sh
      
    
    </summary>
    
      <category term="前端工程" scheme="https://www.thjiang.com/categories/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B/"/>
    
    
      <category term="前端工程" scheme="https://www.thjiang.com/tags/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B/"/>
    
  </entry>
  
  <entry>
    <title>Webpack 长缓存（Long Term Cache）的一些探索</title>
    <link href="https://www.thjiang.com/2018/12/30/Webpack-%E9%95%BF%E7%BC%93%E5%AD%98%EF%BC%88Long-Term-Cache%EF%BC%89%E7%9A%84%E4%B8%80%E4%BA%9B%E6%8E%A2%E7%B4%A2/"/>
    <id>https://www.thjiang.com/2018/12/30/Webpack-长缓存（Long-Term-Cache）的一些探索/</id>
    <published>2018-12-30T12:58:08.000Z</published>
    <updated>2019-07-28T03:54:21.738Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>本文基于 Webpack@4.28.3 版本</p></blockquote><p>在现代前端工程中，为了更有效的利用浏览器和 CDN 的缓存，我们通常会给静态资源设置一个比较长的缓存时间（cache-control等），在有内容更新的时候，通过在静态资源路径中加入 hash 的方式来实现。具体可以参考这里： <a href="https://www.zhihu.com/question/20790576/answer/32602154" target="_blank" rel="noopener">大公司里怎样开发和部署前端代码？</a>。</p><p>但是在 Webpack 中，实现可靠的长期缓存并不容易。在 github 上，关于这个问题的 <a href="https://github.com/webpack/webpack/issues/1315" target="_blank" rel="noopener">issue</a> 已经讨论了三年之久。在 Webpack 4 时代，我们终于看到了一点希望。</p><p>我们从一个最基础的 Webpack 配置开始：</p><h3 id="基础配置"><a href="#基础配置" class="headerlink" title="基础配置"></a>基础配置</h3><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// webpack.config.js </span></span><br><span class="line"><span class="keyword">const</span> path = <span class="built_in">require</span>(<span class="string">'path'</span>); </span><br><span class="line"><span class="keyword">const</span> webpack = <span class="built_in">require</span>(<span class="string">'webpack'</span>); </span><br><span class="line"><span class="built_in">module</span>.exports = &#123; </span><br><span class="line">    entry: &#123; </span><br><span class="line">        main: <span class="string">'./src/app'</span>, </span><br><span class="line">    &#125;, </span><br><span class="line">    output: &#123; </span><br><span class="line">        path: path.join(__dirname, <span class="string">'build'</span>), </span><br><span class="line">        filename: <span class="string">'[name].[hash:8].js'</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 plain"><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">          Asset       Size  Chunks             Chunk Names</span><br><span class="line">app.23b699d1.js    115 KiB       0  [emitted]  app</span><br><span class="line">     index.html  253 bytes          [emitted]</span><br></pre></td></tr></table></figure><p>看起来好像很完美。</p><h3 id="Vendor-Chunks"><a href="#Vendor-Chunks" class="headerlink" title="Vendor Chunks"></a>Vendor Chunks</h3><p>在实际项目中，我们通常会选择将公共模块提取成一个单独的文件，在 Webpack 3 时代，我们一般会选用 <code>CommonsChunkPlugin</code>，在 Webpack 4 中，我们则可以使用 <code>splitChunks</code> 来实现这一需求。这里我们将来自 node_modules 的模块提取成 <code>vendors.js</code>：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// webpack.config.js </span></span><br><span class="line"><span class="keyword">const</span> path = <span class="built_in">require</span>(<span class="string">'path'</span>); </span><br><span class="line"><span class="keyword">const</span> webpack = <span class="built_in">require</span>(<span class="string">'webpack'</span>); </span><br><span class="line"><span class="built_in">module</span>.exports = &#123; </span><br><span class="line">    entry: &#123; </span><br><span class="line">        main: <span class="string">'./src/main'</span>, </span><br><span class="line">    &#125;, </span><br><span class="line">    output: &#123; </span><br><span class="line">        path: path.join(__dirname, <span class="string">'dist'</span>), </span><br><span class="line">        filename: <span class="string">'[name].[hash:8].js'</span></span><br><span class="line">    &#125;,</span><br><span class="line">    optimization: &#123;</span><br><span class="line">        splitChunks: &#123;</span><br><span class="line">            cacheGroups: &#123;</span><br><span class="line">                commons: &#123;</span><br><span class="line">                    test: <span class="regexp">/[\\/]node_modules[\\/]/</span>,</span><br><span class="line">                    name: <span class="string">"vendors"</span>,</span><br><span class="line">                    chunks: <span class="string">"all"</span></span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>打包结果如下：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><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">              Asset       Size  Chunks             Chunk Names</span><br><span class="line">    app.6105d149.js    2.3 KiB       0  [emitted]  app</span><br><span class="line">         index.html  319 bytes          [emitted]</span><br><span class="line">vendors.6105d149.js    114 KiB       1  [emitted]  vendors</span><br></pre></td></tr></table></figure><p>你可能已经注意到了，我们的 app 和 vendors 的 hash 是相同的。这样打包，对 app 模块的任何更改同时也会导致我们的 vendors 模块的 hash 失效。</p><p>要解决这个问题，我们必须在文件名中把 hash 改成 <code>chunkhash</code>。这是因为 hash 会为我们构建的所有内容生成一个全局哈希，而 <code>chunkhash</code> 只会使用它自己的模块中的内容来生成哈希值。</p><p>修改 webpack 配置如下，再次构建，我们得到了两个不同的哈希值：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// webpack.config.js </span></span><br><span class="line"><span class="comment">// ... </span></span><br><span class="line">output: &#123; </span><br><span class="line">    path: path.join(__dirname, <span class="string">'build'</span>), </span><br><span class="line">    filename: <span class="string">'[name].[chunkhash:8].js'</span>, </span><br><span class="line">&#125;, </span><br><span class="line"><span class="comment">// ...</span></span><br></pre></td></tr></table></figure><figure class="highlight plain"><table><tr><td class="gutter"><pre><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">Asset       Size  Chunks             Chunk Names</span><br><span class="line">    app.41d05796.js    2.3 KiB       0  [emitted]  app</span><br><span class="line">         index.html  319 bytes          [emitted]</span><br><span class="line">vendors.c2375741.js    114 KiB       1  [emitted]  vendors</span><br></pre></td></tr></table></figure><p>现在，更改 app 模块中的内容， vendor 模块不会再受影响了（最新版本的 Webpack 中，不再需要为了 hash 的稳定单独提取出 runtime 了）。</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="comment">// app.js</span></span><br><span class="line"><span class="comment">// ...</span></span><br><span class="line"><span class="built_in">console</span>.log(<span class="string">"Hello World"</span>);</span><br></pre></td></tr></table></figure><p>打包之后，很完美：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><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"> Asset       Size  Chunks             Chunk Names</span><br><span class="line">    app.2a740abf.js   2.33 KiB       0  [emitted]  app</span><br><span class="line">         index.html  319 bytes          [emitted]</span><br><span class="line">vendors.c2375741.js    114 KiB       1  [emitted]  vendors</span><br></pre></td></tr></table></figure><h3 id="增加依赖"><a href="#增加依赖" class="headerlink" title="增加依赖"></a>增加依赖</h3><p>随着项目的增长，我们不可避免的出现了更多的依赖：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// app.js</span></span><br><span class="line"><span class="comment">// ...</span></span><br><span class="line"><span class="keyword">import</span> hello <span class="keyword">from</span> <span class="string">'./hello'</span>;</span><br></pre></td></tr></table></figure><p>我们再来构建一次。在这次构建中，显然我们只希望 app.js 的 hash 值更新，但是事情总是不尽如人意的：<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><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">Asset       Size  Chunks             Chunk Names</span><br><span class="line">    app.adcbdfaa.js   2.44 KiB       0  [emitted]  app</span><br><span class="line">         index.html  319 bytes          [emitted]</span><br><span class="line">vendors.a7e68620.js    114 KiB       1  [emitted]  vendors</span><br></pre></td></tr></table></figure></p><p>尽管我们的 vendors 模块没有任何变化，它的 hash 还是又一次改变了。原因又是一个 Webpack 中的细节： Webpack 会为每个 chunk 按顺序给出一个依次增加的 chunk id，随着新依赖的增加，chunk 顺序也可能会发生变化，于是 chunk id 也会随之更新。</p><p>为了解决这个问题，我们引入了一个新的插件： <code>NamedChunksPlugin</code>。这是在 Webpack 2.4 版本中加入的一项特性，借助它我们可以让模块有自己的名字，而不是冷冰冰的数字（Webpack 4 中， development mode下已默认开启）。</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// weback.config.js </span></span><br><span class="line"><span class="comment">// ... </span></span><br><span class="line">plugins: [ </span><br><span class="line">    <span class="keyword">new</span> webpack.NamedChunksPlugin(), </span><br><span class="line">]</span><br></pre></td></tr></table></figure><p>这样配置，Webpack 将使用唯一的 chunk 名称而不是其 id 来标识一个 chunk。</p><p>在最新版的 Webpack 中，我们也可以使用 optimization.chunkIds 来达成同样的效果：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// weback.config.js </span></span><br><span class="line"><span class="comment">// ... </span></span><br><span class="line">optimization: &#123;</span><br><span class="line">    chunkIds: <span class="string">'named'</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>我们在添加和不添加 hello.js 的情况下分别再构建一次，应该就可以看到，vendor 块的哈希是保持不变的了。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><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">&#x2F;&#x2F; 不添加 hello.js</span><br><span class="line">Asset       Size   Chunks             Chunk Names</span><br><span class="line">    app.25a10650.js   2.43 KiB      app  [emitted]  app</span><br><span class="line">         index.html  319 bytes           [emitted]</span><br><span class="line">vendors.83fb4100.js    114 KiB  vendors  [emitted]  vendors</span><br><span class="line">&#x2F;&#x2F; 添加 hello.js</span><br><span class="line">Asset       Size   Chunks             Chunk Names</span><br><span class="line">    app.e66a4b27.js   2.34 KiB      app  [emitted]  app</span><br><span class="line">         index.html  319 bytes           [emitted]</span><br><span class="line">vendors.1a4b3a3e.js    114 KiB  vendors  [emitted]  vendors</span><br></pre></td></tr></table></figure><p>好吧，并没有。</p><p>这是因为和 chunk id 类似，Webpack 同样会对 module 使用自增数字命名。类似地，我们可以使用 <code>NamedModulesPlugin</code> 或 <code>HashedModuleIdsPlugin</code> 来命名 module，从而使 hash 固定。其中 NamedModulesPlugin 使用 module 的路径来命名，生成的名字更可读，但是和使用 module 路径生成的4位（可能出现重复的情况下位数会增加） hash 命名 module 的 HashedModuleIdsPlugin 相比，会明显增大文件体积，适合用于开发环境，生产环境适合使用 HashedModuleIdsPlugin。</p><p>和 chunkIds 类似，在最新版本的 Webpack 中，我们也可以使用 <code>optimization.moduleIds</code> 来配置这一功能（development 模式下 <code>optimization.namedModules</code> 已默认开启）。</p><p>继续更新我们的 Webpack 配置：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// webpack.config.js </span></span><br><span class="line"><span class="comment">// ... </span></span><br><span class="line">optimization: &#123;</span><br><span class="line">    moduleIds: <span class="string">'hashed'</span></span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">// ...</span></span><br></pre></td></tr></table></figure><p>再次更改 app 模块的内容打包，我们可以看到，更新前后其余文件的 hash 是不变的了。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><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">&#x2F;&#x2F; 不添加 hello.js</span><br><span class="line">Asset      Size   Chunks                    Chunk Names</span><br><span class="line">    app.ca09737b.js  10.6 KiB      app  [emitted]         app</span><br><span class="line">         index.html  1.38 KiB           [emitted]</span><br><span class="line">vendors.9d671f37.js   335 KiB  vendors  [emitted]  [big]  vendors</span><br><span class="line">&#x2F;&#x2F; 添加 hello.js</span><br><span class="line"> Asset      Size   Chunks                    Chunk Names</span><br><span class="line">    app.9dc8a07a.js  10.8 KiB      app  [emitted]         app</span><br><span class="line">         index.html  1.38 KiB           [emitted]</span><br><span class="line">vendors.9d671f37.js   335 KiB  vendors  [emitted]  [big]  vendors</span><br></pre></td></tr></table></figure><h3 id="异步模块"><a href="#异步模块" class="headerlink" title="异步模块"></a>异步模块</h3><p>为了首屏性能等需求，我们不可避免的需要在项目中使用异步模块。我们添加一个异步模块再次打包：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// router.js</span></span><br><span class="line"><span class="comment">// ...</span></span><br><span class="line">path: <span class="string">'/async'</span>,</span><br><span class="line">component: <span class="function"><span class="params">()</span> =&gt;</span> <span class="keyword">import</span>(<span class="string">'./async.vue'</span>)</span><br></pre></td></tr></table></figure><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><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"> Asset      Size   Chunks                    Chunk Names</span><br><span class="line">      0.03d30b56.js  2.04 KiB        0  [emitted]</span><br><span class="line">    app.63c81cf2.js  13.6 KiB      app  [emitted]         app</span><br><span class="line">         index.html  1.38 KiB           [emitted]</span><br><span class="line">vendors.2a461436.js   335 KiB  vendors  [emitted]  [big]  vendors</span><br></pre></td></tr></table></figure><p>可以看到 vendors 的 hash 又更新了。打开对应的文件，发现这样一段内容：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">window[&quot;webpackJsonp&quot;] || []).push([[0],</span><br></pre></td></tr></table></figure><p>显然，对新加入的这个异步模块的命名失效了，又变成了从 0 开始的自增序列。</p><p>查看<a href="https://github.com/webpack/webpack/blob/master/lib/NamedChunksPlugin.js" target="_blank" rel="noopener">源码</a>可以得知，<code>NamedChunksPlugin</code> 仅对有 name 的 chunk 有效，但是可以通过自定义 <code>nameResolver</code> 的方式来实现我们需要的功能：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">new</span> webpack.NamedChunksPlugin(<span class="function"><span class="params">chunk</span> =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> (chunk.name) &#123;</span><br><span class="line">        <span class="keyword">return</span> chunk.name;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> <span class="built_in">Array</span>.from(chunk.modulesIterable, m =&gt; m.id).join(<span class="string">"_"</span>);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p>还有一个更简单的方案则是使用 Webpack 的魔法注释来给异步模块命名：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// router.js</span></span><br><span class="line"><span class="comment">// ...</span></span><br><span class="line">path: <span class="string">'/async'</span>,</span><br><span class="line">component: <span class="function"><span class="params">()</span> =&gt;</span> <span class="keyword">import</span>(<span class="comment">/* webpackChunkName: "async" */</span> <span class="string">'./async.vue'</span>)</span><br></pre></td></tr></table></figure><p>打包结果如下：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><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">Asset      Size   Chunks                    Chunk Names</span><br><span class="line">    app.d10d68b9.js  13.6 KiB      app  [emitted]         app</span><br><span class="line">  async.4e35edb1.js  2.05 KiB    async  [emitted]         async</span><br><span class="line">         index.html  1.38 KiB           [emitted]</span><br><span class="line">vendors.9d671f37.js   335 KiB  vendors  [emitted]  [big]  vendors</span><br></pre></td></tr></table></figure><p>可以看到，vendors 的 hash 恢复到了之前的状态。</p><p>是不是感觉好像还缺点什么？</p><h3 id="CSS模块"><a href="#CSS模块" class="headerlink" title="CSS模块"></a>CSS模块</h3><p>在生产环境中，我们通常会将 css 模块打包为独立文件。我们增加一个 css 模块来看一下：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// app.js</span></span><br><span class="line"><span class="comment">// ...</span></span><br><span class="line"><span class="keyword">import</span> <span class="string">'./hello.css'</span>;</span><br></pre></td></tr></table></figure><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// webpack.config.js </span></span><br><span class="line"><span class="comment">// ... </span></span><br><span class="line">plugins: [</span><br><span class="line">    <span class="keyword">new</span> MiniCssExtractPlugin(&#123;</span><br><span class="line">        filename: <span class="string">"[name].[chunkhash:8].css"</span>,</span><br><span class="line">        chunkFilename: <span class="string">"[name].[chunkhash:8].css"</span></span><br><span class="line">    &#125;)</span><br><span class="line">]</span><br><span class="line"><span class="comment">// ...</span></span><br></pre></td></tr></table></figure><p>改变一下 hello.css 的内容，打包两次，对比如下： </p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><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"> Asset      Size   Chunks                    Chunk Names</span><br><span class="line">   app.884f7d50.css  28 bytes      app  [emitted]         app</span><br><span class="line">    app.884f7d50.js  13.8 KiB      app  [emitted]         app</span><br><span class="line">  async.4e35edb1.js  2.05 KiB    async  [emitted]         async</span><br><span class="line">         index.html  1.43 KiB           [emitted]</span><br><span class="line">vendors.9d671f37.js   335 KiB  vendors  [emitted]  [big]  vendors</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"> Asset      Size   Chunks                    Chunk Names</span><br><span class="line">   app.1bedc677.css  29 bytes      app  [emitted]         app</span><br><span class="line">    app.1bedc677.js  13.8 KiB      app  [emitted]         app</span><br><span class="line">  async.4e35edb1.js  2.05 KiB    async  [emitted]         async</span><br><span class="line">         index.html  1.43 KiB           [emitted]</span><br><span class="line">vendors.9d671f37.js   335 KiB  vendors  [emitted]  [big]  vendors</span><br></pre></td></tr></table></figure><p>只修改了 CSS 文件，app.js 的 hash 值又变了。很容易理解，毕竟它们属于同一 chunk。<br>为了解决这个问题，Webpack 4.3 中引入了一个新的概念： <code>contenthash</code>。<br>修改 Webpack 配置如下：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// webpack.config.js </span></span><br><span class="line"><span class="comment">// ... </span></span><br><span class="line">output: &#123;</span><br><span class="line">    filename: <span class="string">"app.[contenthash:8].js"</span>,</span><br><span class="line">    chunkFilename: <span class="string">"[name].[contenthash:8].js"</span></span><br><span class="line">&#125;,</span><br><span class="line">plugins: [</span><br><span class="line">    <span class="keyword">new</span> MiniCssExtractPlugin(&#123;</span><br><span class="line">        filename: <span class="string">"[name].[contenthash:8].css"</span>,</span><br><span class="line">        chunkFilename: <span class="string">"[name].[contenthash:8].css"</span></span><br><span class="line">    &#125;)</span><br><span class="line">]</span><br><span class="line"><span class="comment">// ...</span></span><br></pre></td></tr></table></figure><p>再次打包对比更改 css 文件前后的变化，只有 css 文件的 hash 改变了。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><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">Asset      Size   Chunks                    Chunk Names</span><br><span class="line">    app.a8ec5b81.js  13.8 KiB      app  [emitted]         app</span><br><span class="line">   app.eb9b9ca0.css  28 bytes      app  [emitted]         app</span><br><span class="line">  async.ca0780b4.js  2.05 KiB    async  [emitted]         async</span><br><span class="line">         index.html  1.43 KiB           [emitted]</span><br><span class="line">vendors.bf514953.js   335 KiB  vendors  [emitted]  [big]  vendors</span><br><span class="line"></span><br><span class="line">Asset      Size   Chunks                    Chunk Names</span><br><span class="line">    app.a8ec5b81.js  13.8 KiB      app  [emitted]         app</span><br><span class="line">   app.da6259d4.css  29 bytes      app  [emitted]         app</span><br><span class="line">  async.ca0780b4.js  2.05 KiB    async  [emitted]         async</span><br><span class="line">         index.html  1.43 KiB           [emitted]</span><br><span class="line">vendors.bf514953.js   335 KiB  vendors  [emitted]  [big]  vendors</span><br></pre></td></tr></table></figure><h3 id="external-模块"><a href="#external-模块" class="headerlink" title="external 模块"></a>external 模块</h3><p>有些场景下，我们需要从 CDN 引入一个模块，以 jQuery 为例，一般会如下配置：</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="comment">&lt;!-- index.html --&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">script</span> <span class="attr">src</span>=<span class="string">"https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"</span>&gt;</span><span class="tag">&lt;/<span class="name">script</span>&gt;</span></span><br></pre></td></tr></table></figure><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">//webpack.config.js</span></span><br><span class="line"><span class="built_in">module</span>.exports = &#123;</span><br><span class="line">    externals: &#123;</span><br><span class="line">        jquery: <span class="string">'jQuery'</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// app.js</span></span><br><span class="line"><span class="keyword">import</span> $ <span class="keyword">from</span> <span class="string">'jquery'</span>;</span><br></pre></td></tr></table></figure><p>和异步模块类似，NamedChunksPlugin 也不会对 externals 模块起作用，我们可以参考<a href="https://github.com/timse/name-all-modules-plugin/pull/2/commits/18b460556e625908ca419c1e4798451ab0c5d788" target="_blank" rel="noopener">这里</a>，使用 <code>NameAllModulesPlugin</code> 来做一些相关的配置，从而为它命名。</p><h3 id="展望"><a href="#展望" class="headerlink" title="展望"></a>展望</h3><p>目前，Webpack 已经发布了 v5.0.0-alpha.3 版本。</p><p>在 Webpack 5 中，采用了全新的算法来生成 chunkIds 和 moduleIds，在打包后的文件大小和控制缓存之间有了一个更好的平衡。</p><p>在 Webpack 4 的更新说明中，开发团队提到了 Webpack 5 中会有开箱即用的长缓存配置，在这个 alpha 版本中，我们也看到了 <code>cache: { type: &quot;filesystem&quot; }</code> 这样的配置，然而，这个策略依然是实验性质的。希望在正式版本中能一劳永逸的解决这个问题。</p><h3 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h3><p><a href="https://medium.com/webpack/predictable-long-term-caching-with-webpack-d3eee1d3fa31" target="_blank" rel="noopener">https://medium.com/webpack/predictable-long-term-caching-with-webpack-d3eee1d3fa31</a><br><a href="https://webpack.js.org/guides/caching/" target="_blank" rel="noopener">https://webpack.js.org/guides/caching/</a><br><a href="https://zhuanlan.zhihu.com/p/38456425" target="_blank" rel="noopener">https://zhuanlan.zhihu.com/p/38456425</a><br><a href="https://segmentfault.com/a/1190000016355127" target="_blank" rel="noopener">https://segmentfault.com/a/1190000016355127</a><br><a href="https://segmentfault.com/a/1190000015919928" target="_blank" rel="noopener">https://segmentfault.com/a/1190000015919928</a><br><a href="https://github.com/pigcan/blog/issues/9" target="_blank" rel="noopener">https://github.com/pigcan/blog/issues/9</a></p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;blockquote&gt;
&lt;p&gt;本文基于 Webpack@4.28.3 版本&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在现代前端工程中，为了更有效的利用浏览器和 CDN 的缓存，我们通常会给静态资源设置一个比较长的缓存时间（cache-control等），在有内容更新的时候，通
      
    
    </summary>
    
      <category term="前端工程" scheme="https://www.thjiang.com/categories/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B/"/>
    
    
      <category term="前端工程" scheme="https://www.thjiang.com/tags/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B/"/>
    
      <category term="性能优化" scheme="https://www.thjiang.com/tags/%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/"/>
    
  </entry>
  
  <entry>
    <title>让你的 Webpack 起飞 —— 网易考拉会员 Webpack4 性能优化实战</title>
    <link href="https://www.thjiang.com/2018/08/02/%E8%AE%A9%E4%BD%A0%E7%9A%84%20Webpack%20%E8%B5%B7%E9%A3%9E%20%E2%80%94%E2%80%94%20%E7%BD%91%E6%98%93%E8%80%83%E6%8B%89%E4%BC%9A%E5%91%98%20Webpack4%20%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96%E5%AE%9E%E6%88%98/"/>
    <id>https://www.thjiang.com/2018/08/02/让你的 Webpack 起飞 —— 网易考拉会员 Webpack4 性能优化实战/</id>
    <published>2018-08-02T13:33:57.000Z</published>
    <updated>2019-07-28T03:54:02.050Z</updated>
    
    <content type="html"><![CDATA[<p>最近看到了美团的前端团队的一篇文章，文中提到前端发布仅需10秒，默默的看了一下我们自己的发布时间。。。</p><p>先定一个小目标，争取把 Webpack 的打包时间优化到10秒以内吧。</p><p>先看一下现在打包一次需要的时间，73013ms，下面开始一步一步见证奇迹：<br><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://c.icewish.top/const/blog/images/webpack/73013-1.jpg" alt="73013" title="">                </div>                <div class="image-caption">73013</div>            </figure></p><h3 id="0-模块分析"><a href="#0-模块分析" class="headerlink" title="0. 模块分析"></a>0. 模块分析</h3><p>有很多工具提供了可视化的分析，如<a href="https://www.npmjs.com/package/webpack-bundle-analyzer" target="_blank" rel="noopener">Webpack-bundle-analyzer</a>、<a href="http://alexkuz.github.io/webpack-chart/" target="_blank" rel="noopener">webpack-chart</a>、 <a href="http://webpack.github.io/analyse/" target="_blank" rel="noopener">webpack-analyse</a>。<br>以Webpack-bundle-analyzer为例，它提供了一个下图所示的图表，展示了引入的所有模块的大小、路径等信息，可以针对性的做出优化。</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://c.icewish.top/const/blog/images/webpack/22.07M.png" alt="Webpack-bundle-analyzer" title="">                </div>                <div class="image-caption">Webpack-bundle-analyzer</div>            </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">// 安装：</span><br><span class="line">npm install webpack-bundle-analyzer --save-dev</span><br></pre></td></tr></table></figure><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// webpack.config.js 配置</span></span><br><span class="line"><span class="keyword">const</span> BundleAnalyzerPlugin = <span class="built_in">require</span>(<span class="string">'webpack-bundle-analyzer'</span>).BundleAnalyzerPlugin;</span><br><span class="line"></span><br><span class="line">plugins: [</span><br><span class="line">    <span class="keyword">new</span> BundleAnalyzerPlugin(&#123;</span><br><span class="line">      analyzerMode: <span class="string">'server'</span>,</span><br><span class="line">      analyzerHost: <span class="string">'127.0.0.1'</span>,</span><br><span class="line">      analyzerPort: <span class="number">8888</span>,</span><br><span class="line">      reportFilename: <span class="string">'report.html'</span>,</span><br><span class="line">      defaultSizes: <span class="string">'parsed'</span>,</span><br><span class="line">      openAnalyzer: <span class="literal">true</span>,</span><br><span class="line">      generateStatsFile: <span class="literal">false</span>,</span><br><span class="line">      statsFilename: <span class="string">'stats.json'</span>,</span><br><span class="line">      logLevel: <span class="string">'info'</span></span><br><span class="line">    &#125;)</span><br><span class="line">]</span><br></pre></td></tr></table></figure><p>运行<code>webpack</code>命令，会自动在浏览器中打开<code>http://127.0.0.1:8888/</code>页面，展示可视化图表。</p><h3 id="1-升级到-Weback4-x"><a href="#1-升级到-Weback4-x" class="headerlink" title="1. 升级到 Weback4.x"></a>1. 升级到 Weback4.x</h3><p>Webpack4 带来了极大的性能提升，按照<a href="https://medium.com/webpack/webpack-4-released-today-6cdb994702d4" target="_blank" rel="noopener">开发者博客中的说法</a>，构建速度最多甚至有高达98%的提升。</p><p>升级过程中遇到了一些网上的“Webpack4升级指南”等文章中没有列出的问题，在此分享一下：</p><p>1.1 升级 Vue-loader</p><p><a href="https://github.com/vuejs/vue-loader" target="_blank" rel="noopener">Vue-loader</a> 目前最新版本为 v15.2.6，使用方式有了很大不同。<br>现在，我们需要引入一个新的插件 <code>VueLoaderPlugin</code> ，具体使用方式如下：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// webpack.config.js</span></span><br><span class="line"><span class="keyword">const</span> VueLoaderPlugin = <span class="built_in">require</span>(<span class="string">'vue-loader/lib/plugin'</span>)</span><br><span class="line"></span><br><span class="line"><span class="built_in">module</span>.exports = &#123;</span><br><span class="line">    plugins: [</span><br><span class="line">        <span class="keyword">new</span> VueLoaderPlugin()</span><br><span class="line">    ]</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>同时，在 v15版本的 Vue-loader 中，不再需要单独为 .vue 组件中的模板、CSS等内容单独配置 loader，可以共用普通文件的配置，如下所示：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// webpack.config.js</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">module</span>: &#123;</span><br><span class="line">    rules: [&#123;</span><br><span class="line">        test: <span class="regexp">/\.vue$/</span>,</span><br><span class="line">        loader: <span class="string">'vue-loader'</span></span><br><span class="line">    &#125;, &#123;</span><br><span class="line">        <span class="comment">// 它会应用到普通的 `.js` 文件</span></span><br><span class="line">        <span class="comment">// 以及 `.vue` 文件中的 `&lt;script&gt;` 块</span></span><br><span class="line">        test: <span class="regexp">/\.js$/</span>,</span><br><span class="line">        loader: <span class="string">'babel-loader'</span></span><br><span class="line">    &#125;, &#123;</span><br><span class="line">        <span class="comment">// 它会应用到普通的 `.css` 文件</span></span><br><span class="line">        <span class="comment">// 以及 `.vue` 文件中的 `&lt;style&gt;` 块</span></span><br><span class="line">        test: <span class="regexp">/\.css$/</span>,</span><br><span class="line">        use: [</span><br><span class="line">            <span class="string">'vue-style-loader'</span>,</span><br><span class="line">            <span class="string">'css-loader'</span></span><br><span class="line">        ]</span><br><span class="line">    &#125;]</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>1.2 升级 Vue-router</p><p>在Vue-router v13.0.0版本中对模块导入做了更新，需要加入 default 配置，如下所示：<br><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> Foo = <span class="function"><span class="params">()</span> =&gt;</span> <span class="keyword">import</span>(<span class="string">'./Foo.vue'</span>)</span><br><span class="line"><span class="comment">// 需要改为</span></span><br><span class="line"><span class="keyword">const</span> Foo = <span class="function"><span class="params">()</span> =&gt;</span> <span class="keyword">import</span>(<span class="string">'./Foo.vue'</span>).then(<span class="function"><span class="params">m</span> =&gt;</span> m.default)</span><br></pre></td></tr></table></figure><br>同理：<br><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> Foo = <span class="built_in">require</span>(<span class="string">'./Foo.vue'</span>)</span><br><span class="line"><span class="comment">// 需要改为</span></span><br><span class="line"><span class="keyword">const</span> Foo = <span class="built_in">require</span>(<span class="string">'./Foo.vue'</span>).default</span><br></pre></td></tr></table></figure></p><p>详情可以参考<a href="https://github.com/vuejs/vue-loader/releases/tag/v13.0.0" target="_blank" rel="noopener">https://github.com/vuejs/vue-loader/releases/tag/v13.0.0</a></p><p>1.3 Chunk 的命名<br>如果使用 webpackchunkname 魔法注释来命名，需要注意 .babelrc 中 comment 必须为true</p><p>1.4 提取 CSS 文件</p><p>在 Webpack4 环境下 <code>extract-text-webpack-plugin</code> 需要安装 <code>@next</code> 版本，我们这里直接使用了 <code>mini-css-extract-plugin</code> 来代替。</p><p>同时，在 mode 为 <code>development</code> 时，<code>NamedChunksPlugin</code> 和 <code>NamedModulesPlugin</code> 会默认开启，不需要再显式指定。</p><p>1.5 本地 mock 处理</p><p>2.x版本的 Vue-CLI 启动了一个 express 服务来处理本地数据的 mock，我们尝试做了一些简化，在 webpack-dev-server 的 <code>before</code> 方法中，使用 <code>webpack-api-mocker</code> 插件拦截了请求，读取本地的 mock 数据（JSON文件）返回。其中，<code>mock/index.js</code> 是通过服务启动过程中遍历本地数据文件生成的。</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// webpack.dev.conf.js</span></span><br><span class="line"><span class="keyword">const</span> apiMocker = <span class="built_in">require</span>(<span class="string">'webpack-api-mocker'</span>);</span><br><span class="line"><span class="built_in">require</span>(<span class="string">'./mock-generator.js'</span>)();</span><br><span class="line">devServer: &#123;</span><br><span class="line">        before(app) &#123;</span><br><span class="line">            apiMocker(app, path.resolve(<span class="string">'./mock/index.js'</span>));</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// mock/index.js</span></span><br><span class="line"><span class="keyword">const</span> fs = <span class="built_in">require</span>(<span class="string">'fs'</span>);</span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">fromJSONFile</span>(<span class="params">filepath</span>) </span>&#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="function">(<span class="params">req, res</span>) =&gt;</span> &#123;</span><br><span class="line">        <span class="keyword">const</span> data = fs.readFileSync(<span class="string">'mock'</span> + filepath).toString();</span><br><span class="line">        <span class="keyword">const</span> json = <span class="built_in">JSON</span>.parse(data);</span><br><span class="line">        <span class="keyword">return</span> res.json(json);</span><br><span class="line">    &#125;;</span><br><span class="line">&#125;;</span><br><span class="line"><span class="keyword">const</span> proxy = &#123;</span><br><span class="line">    <span class="string">'GET /aaa/bbb'</span>: fromJSONFile(<span class="string">'/aaa/bbb.json'</span>),</span><br><span class="line">    <span class="string">'GET /aaa/ccc'</span>: fromJSONFile(<span class="string">'/aaa/ccc.json'</span>)</span><br><span class="line">&#125;;</span><br><span class="line"><span class="built_in">module</span>.exports = proxy;</span><br></pre></td></tr></table></figure><p>升级完成之后，打包时间直接减少了半分钟，达到了44.534秒，离小目标还有很大距离，我们继续。<br><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://c.icewish.top/const/blog/images/webpack/44534.jpg" alt="44534" title="">                </div>                <div class="image-caption">44534</div>            </figure></p><h3 id="2-路由处理（异步加载）"><a href="#2-路由处理（异步加载）" class="headerlink" title="2. 路由处理（异步加载）"></a>2. 路由处理（异步加载）</h3><p>在前面的打包完成的图片中，我们可以看到生成了大量的文件，统计了一下，体积总计高达22.07M，文件近60个。<br>权衡了诸如加载时间等方面之后，我们决定采用按照一级路由来打包的方式。<br>具体实现如下：<br><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line">Vue.use(Router);</span><br><span class="line"><span class="keyword">import</span> Vue <span class="keyword">from</span> <span class="string">'vue'</span>;</span><br><span class="line"><span class="keyword">import</span> Router <span class="keyword">from</span> <span class="string">'vue-router'</span>;</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="keyword">new</span> Router(&#123;</span><br><span class="line">routes: [&#123;</span><br><span class="line">    path: <span class="string">'/a'</span>,</span><br><span class="line">    component: <span class="function"><span class="params">()</span> =&gt;</span></span><br><span class="line">        <span class="keyword">import</span> ( <span class="comment">/* webpackChunkName: 'a' */</span> <span class="string">'@/pages/a'</span>),</span><br><span class="line">    name: <span class="string">'a'</span></span><br><span class="line">&#125;, &#123;</span><br><span class="line">    path: <span class="string">'/b'</span>,</span><br><span class="line">    component: <span class="function"><span class="params">()</span> =&gt;</span> <span class="keyword">import</span> ( <span class="comment">/* webpackChunkName: 'b' */</span> <span class="string">'@/pages/b'</span>),</span><br><span class="line">    name: <span class="string">'b'</span>,</span><br><span class="line">    children: [&#123;</span><br><span class="line">            path: <span class="string">'b/m'</span>,</span><br><span class="line">            component: <span class="built_in">require</span>(<span class="string">'@/pages/b/m'</span>).default,</span><br><span class="line">            name: <span class="string">'m'</span>,</span><br><span class="line">            children: [&#123;</span><br><span class="line">                path: <span class="string">'b/m/p'</span>,</span><br><span class="line">                component: <span class="built_in">require</span>(<span class="string">'@/pages/b/m/p'</span>).default,</span><br><span class="line">                name: <span class="string">'p'</span></span><br><span class="line">            &#125;, &#123;</span><br><span class="line">                path: <span class="string">'b/m/q'</span>,</span><br><span class="line">                component: <span class="built_in">require</span>(<span class="string">'@/pages/b/m/q'</span>).default,</span><br><span class="line">                name: <span class="string">'q'</span></span><br><span class="line">            &#125;,</span><br><span class="line">        ]</span><br><span class="line">    &#125;]</span><br><span class="line">&#125;]</span><br></pre></td></tr></table></figure></p><p>经过上面的优化，我们将生成的js文件数量减少到了8个，大小减小到了5M.<br>来看一下打包时间：21.959秒！距离小目标越来越近了。<br><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://c.icewish.top/const/blog/images/webpack/21959.jpg" alt="21959" title="">                </div>                <div class="image-caption">21959</div>            </figure></p><h3 id="3-HappyPack-thread-loader"><a href="#3-HappyPack-thread-loader" class="headerlink" title="3. HappyPack/thread-loader"></a>3. HappyPack/thread-loader</h3><p>HappyPack 可以将原有的 webpack 对 loader 的执行过程，从单一进程的形式扩展为多进程的模式，从而加速代码构建。使用方式如下：<br><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> HappyPack = <span class="built_in">require</span>(<span class="string">'happypack'</span>);</span><br><span class="line"><span class="keyword">const</span> happyThreadPool = HappyPack.ThreadPool(&#123; <span class="attr">size</span>: os.cpus().length &#125;);</span><br><span class="line"></span><br><span class="line"><span class="built_in">module</span>: &#123;</span><br><span class="line">loaders: [&#123;</span><br><span class="line">test: <span class="regexp">/\.less$/</span>,</span><br><span class="line">loader: ExtractTextPlugin.extract(</span><br><span class="line"><span class="string">'style'</span>, path.resolve(__dirname, <span class="string">'./node_modules'</span>, <span class="string">'happypack/loader'</span>) + <span class="string">'?id=less'</span></span><br><span class="line">)</span><br><span class="line">&#125;]</span><br><span class="line">&#125;,</span><br><span class="line">plugins: [</span><br><span class="line"><span class="keyword">new</span> HappyPack(&#123;</span><br><span class="line">id: <span class="string">'less'</span>,</span><br><span class="line">loaders: [<span class="string">'css!less'</span>],</span><br><span class="line">threadPool: happyThreadPool,</span><br><span class="line">cache: <span class="literal">true</span>,</span><br><span class="line">verbose: <span class="literal">true</span></span><br><span class="line">&#125;)</span><br><span class="line">]</span><br></pre></td></tr></table></figure></p><p>经过测试，在我们的项目中，对 js 和 ts 文件使用 happypack 收益最大。</p><p>此处需要注意的是，vue-loader 不支持 happypack，可以使用 <code>thread-loader</code> 来进行加速，同样是新建一个进程来执行 loader 操作，使用方式也很简单：<br><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"> <span class="built_in">module</span>: &#123;</span><br><span class="line">    rules: [&#123;</span><br><span class="line">        test: <span class="regexp">/\.vue$/</span>,</span><br><span class="line">        use: [</span><br><span class="line">            <span class="string">'thread-loader'</span>,</span><br><span class="line">            <span class="string">'vue-loader'</span></span><br><span class="line">        ]</span><br><span class="line">    &#125;]</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><br>但是在我们的项目中，经过测试，thread-loader 对于打包速度几乎没有影响，是因为它本身的额外开销导致，建议只在极高性能消耗的场景下使用。</p><p>完成之后，测试一下打包时间：15.101秒。<br><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://c.icewish.top/const/blog/images/webpack/14814.jpg" alt="14814" title="">                </div>                <div class="image-caption">14814</div>            </figure></p><h3 id="4-缓存loader的执行结果-cacheDirectory-cache-loader"><a href="#4-缓存loader的执行结果-cacheDirectory-cache-loader" class="headerlink" title="4. 缓存loader的执行结果(cacheDirectory/cache-loader)"></a>4. 缓存loader的执行结果(cacheDirectory/cache-loader)</h3><p>我们可以对loader做如下配置来开启缓存：<br><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">loader: <span class="string">'babel-loader?cacheDirectory=true'</span></span><br></pre></td></tr></table></figure></p><p>或者我们也可以使用 cache-loader ：<br><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">rules: [&#123;</span><br><span class="line">    test: <span class="regexp">/\.vue$/</span>,</span><br><span class="line">    use: [</span><br><span class="line">        <span class="string">'cache-loader'</span>,</span><br><span class="line">        <span class="string">'vue-loader'</span></span><br><span class="line">    ]</span><br><span class="line">&#125;]</span><br></pre></td></tr></table></figure><br>加入缓存之后，再次测试打包时间：13.915秒。<br><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://c.icewish.top/const/blog/images/webpack/13915.jpg" alt="13915" title="">                </div>                <div class="image-caption">13915</div>            </figure></p><h3 id="5-模块进一步细分（splitChunks）"><a href="#5-模块进一步细分（splitChunks）" class="headerlink" title="5. 模块进一步细分（splitChunks）"></a>5. 模块进一步细分（splitChunks）</h3><p>在 Webpack4 中移除了我们此前常用的 <code>CommonsChunkPlugin</code> 插件，取而代之的是 <code>splitChunks</code> 。<br><code>splitChunks</code> 的默认配置已经足够我们日常使用，没有特殊需求可以不必特意处理。<br>我们此处的配置如下（生产环境）：<br><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line">optimization: &#123;</span><br><span class="line">    splitChunks: &#123;</span><br><span class="line">        cacheGroups: &#123;</span><br><span class="line">            commons: &#123;</span><br><span class="line">                test: <span class="regexp">/[\\/]node_modules[\\/]/</span>,</span><br><span class="line">                name: <span class="string">'vendors'</span>,</span><br><span class="line">                chunks: <span class="string">'all'</span></span><br><span class="line">            &#125;,</span><br><span class="line">            <span class="comment">// styles: &#123;</span></span><br><span class="line">            <span class="comment">//     name: 'index',</span></span><br><span class="line">            <span class="comment">//     test: /.stylus|css$/,</span></span><br><span class="line">            <span class="comment">//     chunks: 'all',</span></span><br><span class="line">            <span class="comment">//     enforce: true</span></span><br><span class="line">            <span class="comment">// &#125;</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><br>其中，commons 部分的作用是分离出 node_modules 中引入的模块，styles 部分则是合并 CSS 文件。<br>经过测试，在我们的项目中，styles 部分使构建时间增加了大约2秒，因此我们放弃了这部分操作。</p><h3 id="6-使用DllPlugin拆分模块"><a href="#6-使用DllPlugin拆分模块" class="headerlink" title="6. 使用DllPlugin拆分模块"></a>6. 使用DllPlugin拆分模块</h3><p>开发过程中，我们经常需要引入大量第三方库，这些库并不需要随时修改或调试，我们可以使用DllPlugin和DllReferencePlugin单独构建它们。<br>具体使用如下：<br><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> HtmlWebpackPlugin = <span class="built_in">require</span>(<span class="string">'html-webpack-plugin'</span>);</span><br><span class="line"></span><br><span class="line"><span class="built_in">module</span>.exports = &#123;</span><br><span class="line">    entry: &#123;</span><br><span class="line">        vendor: [</span><br><span class="line">            <span class="string">'axios'</span>,</span><br><span class="line">            <span class="string">'vue-i18n'</span>,</span><br><span class="line">            <span class="string">'vue-router'</span>,</span><br><span class="line">            <span class="string">'vuex'</span></span><br><span class="line">        ]</span><br><span class="line">    &#125;,</span><br><span class="line">    output: &#123;</span><br><span class="line">        path: path.resolve(__dirname, <span class="string">'../static/'</span>),</span><br><span class="line">        filename: <span class="string">'[name].dll.js'</span>,</span><br><span class="line">        library: <span class="string">'[name]_library'</span></span><br><span class="line">    &#125;,</span><br><span class="line">    plugins: [</span><br><span class="line">        <span class="keyword">new</span> webpack.DllPlugin(&#123;</span><br><span class="line">            path: path.join(__dirname, <span class="string">'build'</span>, <span class="string">'[name]-manifest.json'</span>),</span><br><span class="line">            name: <span class="string">'[name]_library'</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><br>执行webpack命令，build目录下即可生成 dll.js 文件和对应的 manifest 文件，使用 DLLReferencePlugin 引入：<br><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">plugins: [</span><br><span class="line">    <span class="keyword">new</span> webpack.DllReferencePlugin(&#123;</span><br><span class="line">    context: __dirname,</span><br><span class="line">    manifest: <span class="built_in">require</span>(<span class="string">'./build/vendor-manifest.json'</span>)</span><br><span class="line">    &#125;)</span><br><span class="line">]</span><br></pre></td></tr></table></figure><br>由于我们的项目中原本已经通过这种方式打包了大部分第三方库，所以这里对打包速度的提升不大，仅仅提升2秒，来到了11.509秒。<br><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://c.icewish.top/const/blog/images/webpack/11509.jpg" alt="11509" title="">                </div>                <div class="image-caption">11509</div>            </figure></p><h3 id="7-精简不必要的模块"><a href="#7-精简不必要的模块" class="headerlink" title="7. 精简不必要的模块"></a>7. 精简不必要的模块</h3><p>在我们的项目中，引入了一些如 moment、lodash 等重型库，然而他们提供的绝大部分功能都是我们不需要的，权衡之后，我们移除了他们，自己实现了部分功能或使用了更小体积的库代替。</p><p>移除了这些库之后，我们的打包时间来到了8.921秒！小目标达成了~<br><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://c.icewish.top/const/blog/images/webpack/8921.jpg" alt="8921" title="">                </div>                <div class="image-caption">8921</div>            </figure></p><p>但是这距离10秒发布还不够，我们需要争取压缩出更多时间留给发布系统。还能不能继续提升呢？答案是肯定的。</p><h3 id="8-优化模块查找路径"><a href="#8-优化模块查找路径" class="headerlink" title="8. 优化模块查找路径"></a>8. 优化模块查找路径</h3><p>Node.js的模块的载入及缓存机制如下：</p><p>载入内置模块<br>载入文件模块<br>载入文件目录模块<br>载入node_modules里的模块<br>自动缓存已载入模块<br>如果模块名不是路径，也不是内置模块，Node将试图去当前目录的node_modules文件夹里搜索。如果当前目录的node_modules里没有找到，Node会从父目录的node_modules里搜索，这样递归下去直到根目录。</p><p>我们可以对搜索过程进行一些优化，比如可以像下面这样指定路径：<br><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">exclude: <span class="regexp">/node_modules/</span>, <span class="comment">// 排除不处理的目录</span></span><br><span class="line">include: path.resolve(__dirname, <span class="string">'src'</span>) <span class="comment">// 精确指定要处理的目录</span></span><br><span class="line">resolve: &#123;</span><br><span class="line">    modules: [path.resolve(__dirname, <span class="string">'node_modules'</span>)], <span class="comment">// 指定node_modules的位置</span></span><br><span class="line">    alias: &#123;</span><br><span class="line">        <span class="string">'api'</span>: resolve(<span class="string">'src/api'</span>) <span class="comment">// 创建别名</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><p>我们再来看一下时间：7.66秒！<br><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://c.icewish.top/const/blog/images/webpack/7660.jpg" alt="7660" title="">                </div>                <div class="image-caption">7660</div>            </figure></p><p>到这里，我们的这次优化基本完成了，其实还有很多可以优化的空间，比如升级一颗 i9 处理器~<br>这里也只是列举出了一些常见的收益较大的优化方式，希望能对大家有一点帮助，也欢迎有兴趣的同学一起交流。</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;最近看到了美团的前端团队的一篇文章，文中提到前端发布仅需10秒，默默的看了一下我们自己的发布时间。。。&lt;/p&gt;
&lt;p&gt;先定一个小目标，争取把 Webpack 的打包时间优化到10秒以内吧。&lt;/p&gt;
&lt;p&gt;先看一下现在打包一次需要的时间，73013ms，下面开始一步一步见证奇
      
    
    </summary>
    
      <category term="前端工程" scheme="https://www.thjiang.com/categories/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B/"/>
    
    
      <category term="前端工程" scheme="https://www.thjiang.com/tags/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B/"/>
    
      <category term="性能优化" scheme="https://www.thjiang.com/tags/%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/"/>
    
  </entry>
  
  <entry>
    <title>npm 备忘录</title>
    <link href="https://www.thjiang.com/2018/07/15/npm-%E5%A4%87%E5%BF%98%E5%BD%95/"/>
    <id>https://www.thjiang.com/2018/07/15/npm-备忘录/</id>
    <published>2018-07-15T07:42:34.000Z</published>
    <updated>2019-05-15T07:45:00.163Z</updated>
    
    <content type="html"><![CDATA[<h2 id="常用命令"><a href="#常用命令" class="headerlink" title="常用命令"></a>常用命令</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ npm init / install / uninstall / update</span><br></pre></td></tr></table></figure><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">$ npm ls</span><br><span class="line">$ npm list -g --depth 0</span><br></pre></td></tr></table></figure><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">npm config <span class="built_in">set</span> proxy xxx.com</span><br><span class="line">npm config list</span><br></pre></td></tr></table></figure><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">$ npm outdated</span><br><span class="line">Package                                  Current        Wanted    Latest  Location</span><br><span class="line">@commitlint/cli                            7.1.1         7.5.2     7.5.2  <span class="built_in">test</span></span><br><span class="line">@commitlint/config-angular                 7.1.1         7.5.0     7.5.0  <span class="built_in">test</span></span><br></pre></td></tr></table></figure><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><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">$ npm view @<span class="built_in">test</span>/<span class="built_in">test</span>-util</span><br><span class="line"></span><br><span class="line">@<span class="built_in">test</span>/<span class="built_in">test</span>-util@0.3.101 | ISC | deps: none | versions: 242</span><br><span class="line"></span><br><span class="line">dist</span><br><span class="line">.tarball http://rnpm.hz.netease.com/@<span class="built_in">test</span>/<span class="built_in">test</span>-util/download/@<span class="built_in">test</span>/<span class="built_in">test</span>-util-0.3.101.tgz</span><br><span class="line">.shasum: a10f3595eb4cbbdd0d1d49691acf2116bfa832bf</span><br><span class="line"></span><br><span class="line">maintainers:</span><br><span class="line">- aaa &lt;aaa@163.com&gt;</span><br><span class="line">- bbb &lt;bbb@163.com&gt;</span><br><span class="line">- ccc &lt;ccc@163.com&gt;</span><br><span class="line"></span><br><span class="line">dist-tags:</span><br><span class="line">dev: 0.3.101-0   latest: 0.3.101  <span class="built_in">test</span>: 0.0.66</span><br><span class="line"></span><br><span class="line">published 2 days ago by ddd &lt;ddd@163.com&gt;</span><br></pre></td></tr></table></figure><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">$ <span class="built_in">cd</span> path/project</span><br><span class="line">$ npm install path/package</span><br><span class="line"></span><br><span class="line">$ <span class="built_in">cd</span> path/project</span><br><span class="line">$ npm link path/package</span><br></pre></td></tr></table></figure><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">$ npm adduser</span><br><span class="line">Username:</span><br><span class="line">Password:</span><br><span class="line">Email: (this IS public)</span><br></pre></td></tr></table></figure><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">$ npm publish</span><br><span class="line">$ npm publish --tag=<span class="built_in">test</span></span><br></pre></td></tr></table></figure><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 access <span class="comment"># 设置权限</span></span><br></pre></td></tr></table></figure><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">$ npm <span class="built_in">help</span> install</span><br><span class="line"><span class="comment"># 打开 file:///C:/Users/Forever/AppData/Roaming/npm/node_modules/npm/html/doc/cli/npm-install.html</span></span><br></pre></td></tr></table></figure><h2 id="私有-NPM"><a href="#私有-NPM" class="headerlink" title="私有 NPM"></a>私有 NPM</h2><p>私有包托管在内部服务器或者单独的服务器上；<br>可以同步整个官方仓库，也可以只同步需要的；<br>对于下载，发布，有对应的权限管理。</p><h3 id="官方"><a href="#官方" class="headerlink" title="官方"></a>官方</h3><p>缺点：贵<br>优点：开源闭源项目统一托管</p><h3 id="sinopia"><a href="#sinopia" class="headerlink" title="sinopia"></a>sinopia</h3><p><a href="https://github.com/rlidwka/sinopia" target="_blank" rel="noopener">https://github.com/rlidwka/sinopia</a><br>很久不维护了；<br>权限管理比较弱；<br>缓存优化不足；<br>不能做官方仓库的镜像。</p><h3 id="verdaccio"><a href="#verdaccio" class="headerlink" title="verdaccio"></a>verdaccio</h3><p><a href="https://github.com/verdaccio/verdaccio" target="_blank" rel="noopener">https://github.com/verdaccio/verdaccio</a><br>优点：免费；本地速度快,带公有库缓存；支持 yarn<br>缺点：需要自己托管维护</p><h3 id="cnpm"><a href="#cnpm" class="headerlink" title="cnpm"></a>cnpm</h3><p><a href="https://github.com/cnpm/cnpmjs.org" target="_blank" rel="noopener">https://github.com/cnpm/cnpmjs.org</a></p><p>配置：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// ~/.cnpmjs.org/config.json</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="attr">"mysqlServers"</span>: [</span><br><span class="line">        &#123;</span><br><span class="line">            <span class="attr">"host"</span>: <span class="string">"host"</span>,</span><br><span class="line">            <span class="attr">"port"</span>: <span class="number">3306</span>,</span><br><span class="line">            <span class="attr">"user"</span>: <span class="string">"lakers"</span>,</span><br><span class="line">            <span class="attr">"password"</span>: <span class="string">"password"</span></span><br><span class="line">        &#125;</span><br><span class="line">    ],</span><br><span class="line">    <span class="attr">"mysqlDatabase"</span>: <span class="string">"cnpmjs"</span>, <span class="comment">// 数据库名</span></span><br><span class="line">    <span class="attr">"enablePrivate"</span>: <span class="literal">true</span>, <span class="comment">// 是否启用私有化，这样只有定义在 `admins` 中的用户才能发布</span></span><br><span class="line">    <span class="attr">"admins"</span>: &#123;</span><br><span class="line">        <span class="comment">// 管理员配置，可以配置多个</span></span><br><span class="line">        <span class="attr">"senntyou"</span>: <span class="string">"LakersChampionship@163.com"</span></span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="attr">"syncModel"</span>: <span class="string">"exist"</span>, <span class="comment">// 同步模式</span></span><br><span class="line">    <span class="attr">"scopes"</span>: [</span><br><span class="line">        <span class="comment">// 包前缀，如果不是以这个前缀命名的包将不能发布，可以配置多个</span></span><br><span class="line">        <span class="string">"@lakers"</span></span><br><span class="line">    ],</span><br><span class="line">    <span class="attr">"registryHost"</span>: <span class="string">"http://lakers.company.npm.registry.com"</span>, <span class="comment">// 你的服务器对应的 npm registry 地址</span></span><br><span class="line">    <span class="attr">"officialNpmRegistry"</span>: <span class="string">"https://registry.npm.taobao.org"</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><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">$ cnpmjs.org start</span><br><span class="line"></span><br><span class="line">$ cnpm config <span class="built_in">set</span> registry http://your.company.npm.registry.com</span><br></pre></td></tr></table></figure>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h2 id=&quot;常用命令&quot;&gt;&lt;a href=&quot;#常用命令&quot; class=&quot;headerlink&quot; title=&quot;常用命令&quot;&gt;&lt;/a&gt;常用命令&lt;/h2&gt;&lt;figure class=&quot;highlight bash&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre
      
    
    </summary>
    
    
      <category term="前端工程" scheme="https://www.thjiang.com/tags/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B/"/>
    
  </entry>
  
  <entry>
    <title>CentOS7 开启 BBR 加速</title>
    <link href="https://www.thjiang.com/2018/02/23/CentOS7-%E5%BC%80%E5%90%AF%20BBR%20%E5%8A%A0%E9%80%9F/"/>
    <id>https://www.thjiang.com/2018/02/23/CentOS7-开启 BBR 加速/</id>
    <published>2018-02-23T13:16:16.000Z</published>
    <updated>2018-11-01T13:54:39.440Z</updated>
    
    <content type="html"><![CDATA[<p><a href="https://github.com/google/bbr" target="_blank" rel="noopener">BBR</a> 是 Google 提出的一种新型拥塞控制算法，可以使 Linux 服务器显著地提高吞吐量和减少 TCP 连接的延迟。</p><h3 id="升级内核"><a href="#升级内核" class="headerlink" title="升级内核"></a>升级内核</h3><p>开启 BBR 要求 4.10 以上版本 Linux 内核，可使用如下命令查看当前内核版本：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">uname -r</span><br></pre></td></tr></table></figure><p>可以得到类似如下的结果：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">3.10.0-514.10.2.el7.x86_64</span><br></pre></td></tr></table></figure><p>如果当前内核版本低于 4.10，可使用 ELRepo 源更新：<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">sudo rpm --import https:&#x2F;&#x2F;www.elrepo.org&#x2F;RPM-GPG-KEY-elrepo.org</span><br><span class="line">sudo rpm -Uvh http:&#x2F;&#x2F;www.elrepo.org&#x2F;elrepo-release-7.0-2.el7.elrepo.noarch.rpm</span><br><span class="line">sudo yum --enablerepo&#x3D;elrepo-kernel install kernel-ml -y</span><br></pre></td></tr></table></figure></p><p>安装完成后，查看已安装的内核：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">rpm -qa | grep kernel</span><br></pre></td></tr></table></figure><p>得到结果如下：<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><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">kernel-3.10.0-123.el7.x86_64</span><br><span class="line">kernel-headers-3.10.0-514.16.1.el7.x86_64</span><br><span class="line">kernel-ml-4.11.0-1.el7.elrepo.x86_64</span><br><span class="line">kernel-tools-3.10.0-514.16.1.el7.x86_64</span><br><span class="line">kernel-3.10.0-514.16.1.el7.x86_64</span><br><span class="line">kernel-tools-libs-3.10.0-514.16.1.el7.x86_64</span><br></pre></td></tr></table></figure><br>在输出中看到类似<code>kernel-ml-4.11.0-1.el7.elrepo.x86_64</code>的内容，表示安装成功。</p><h3 id="修改grub2引导"><a href="#修改grub2引导" class="headerlink" title="修改grub2引导"></a>修改grub2引导</h3><p>执行：<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sudo egrep ^menuentry &#x2F;etc&#x2F;grub2.cfg | cut -f 2 -d \&#39;</span><br></pre></td></tr></table></figure><br>会得到如下结果：<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><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">CentOS Linux 7 Rescue a0cbf86a6ef1416a8812657bb4f2b860 (4.11.0-1.el7.elrepo.x86_64)</span><br><span class="line">CentOS Linux (4.11.0-1.el7.elrepo.x86_64) 7 (Core)</span><br><span class="line">CentOS Linux (3.10.0-514.16.1.el7.x86_64) 7 (Core)</span><br><span class="line">CentOS Linux (3.10.0-123.el7.x86_64) 7 (Core)</span><br><span class="line">CentOS Linux (0-rescue-2d3f9371c20d3e90a544ccc814d485e3) 7 (Core)</span><br></pre></td></tr></table></figure></p><p>由于序号从0开始，设置默认启动项为1并重启系统：<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">sudo grub2-set-default 1</span><br><span class="line">reboot</span><br></pre></td></tr></table></figure></p><p>重启完成后，重新登录并重新运行uname命令来确认你是否使用了正确的内核：<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">uname -r</span><br></pre></td></tr></table></figure></p><p>得到如下结果则升级成功：<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">4.11.0-1.el7.elrepo.x86_64</span><br></pre></td></tr></table></figure></p><h3 id="开启BBR"><a href="#开启BBR" class="headerlink" title="开启BBR"></a>开启BBR</h3><p>执行：<br><figure class="highlight plain"><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">echo &#39;net.core.default_qdisc&#x3D;fq&#39; | sudo tee -a &#x2F;etc&#x2F;sysctl.conf</span><br><span class="line">echo &#39;net.ipv4.tcp_congestion_control&#x3D;bbr&#39; | sudo tee -a &#x2F;etc&#x2F;sysctl.conf</span><br><span class="line">sudo sysctl -p</span><br></pre></td></tr></table></figure></p><p>完成后，分别执行如下命令来检查 BBR 是否开启成功：<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><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">sudo sysctl net.ipv4.tcp_available_congestion_control</span><br><span class="line"># 输出应为 net.ipv4.tcp_available_congestion_control &#x3D; bbr cubic reno</span><br><span class="line"></span><br><span class="line">sudo sysctl -n net.ipv4.tcp_congestion_control</span><br><span class="line"># 输出应为 bbr</span><br><span class="line"></span><br><span class="line">lsmod | grep bbr</span><br><span class="line"># 输出应类似 tcp_bbr  16384  28</span><br></pre></td></tr></table></figure></p><h3 id="速度测试"><a href="#速度测试" class="headerlink" title="速度测试"></a>速度测试</h3><figure class="highlight plain"><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"># 需先在 firewalld 中开启 http 服务</span><br><span class="line">sudo dd if&#x3D;&#x2F;dev&#x2F;zero of&#x3D;500mb.zip bs&#x3D;1024k count&#x3D;500</span><br></pre></td></tr></table></figure><p>访问 <code>http://[your-server-IP]/500mb.zip</code> 来测试一下下载速度吧~</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;&lt;a href=&quot;https://github.com/google/bbr&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;BBR&lt;/a&gt; 是 Google 提出的一种新型拥塞控制算法，可以使 Linux 服务器显著地提高吞吐量和减少 TCP 连接的延迟。
      
    
    </summary>
    
      <category term="运维" scheme="https://www.thjiang.com/categories/%E8%BF%90%E7%BB%B4/"/>
    
    
      <category term="Linux" scheme="https://www.thjiang.com/tags/Linux/"/>
    
  </entry>
  
  <entry>
    <title>Webpack4 初探</title>
    <link href="https://www.thjiang.com/2018/02/01/Webpack4-%E5%88%9D%E6%8E%A2/"/>
    <id>https://www.thjiang.com/2018/02/01/Webpack4-初探/</id>
    <published>2018-02-01T10:55:47.000Z</published>
    <updated>2018-11-01T13:55:00.742Z</updated>
    
    <content type="html"><![CDATA[<p>1月25日，Webpack 发布了 4.0.0-beta.0 版本，做了较大改动。我们第一时间来体验一下。</p><h2 id="安装"><a href="#安装" class="headerlink" title="安装"></a>安装</h2><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm install webpack@next webpack-cli --save-dev</span><br></pre></td></tr></table></figure><p>Webpack 团队切出了 <code>next</code> 分支来开发 Webpack 4，原有的 <code>master</code> 分支将继续 3.x 版本的维护。</p><h2 id="新特性"><a href="#新特性" class="headerlink" title="新特性"></a>新特性</h2><h3 id="大幅度提升了构建性能"><a href="#大幅度提升了构建性能" class="headerlink" title="大幅度提升了构建性能"></a>大幅度提升了构建性能</h3><blockquote><ul><li><p>默认情况下，使用 <code>production</code> 模式时，会使用UglifyJS自动并行处理压缩工作并缓存。</p></li><li><p>发布了一个新的插件系统（<a href="https://github.com/webpack/tapable）" target="_blank" rel="noopener">https://github.com/webpack/tapable）</a> ，可以更有效的处理插件。</p></li><li><p>不再支持 Node 4，从而可以大量使用ES6，对V8引擎也进行了一些优化。</p></li></ul></blockquote><h3 id="向-parcel-看齐-——-0配置"><a href="#向-parcel-看齐-——-0配置" class="headerlink" title="向 parcel 看齐 —— 0配置"></a>向 parcel 看齐 —— 0配置</h3><p>在 Webpack 4 中，不再强制要求指定 entry 和 output 路径。webpack 4 会默认 entry 为 <code>./src</code>，output 为 <code>./dist</code> 。</p><p>也就是说，我们可以不再需要一个庞杂的 <code>webpack.config.js</code> 了。</p><h3 id="提供默认模式选择"><a href="#提供默认模式选择" class="headerlink" title="提供默认模式选择"></a>提供默认模式选择</h3><p>现在在 Webpack 中，需要手动选择生产模式（production）和开发模式（development）。</p><p>生产模式中，提供了各种各样的优化，如代码压缩、作用域提升、移除未引用代码（tree-shaking）等，同时还引入了一些如 <code>NoEmitOnErrorsPlugin</code> 这样的原本需要手动使用的插件。</p><p>开发模式优化了开发速度和开发体验。同样地，在开发模式中，Webpack 4 提供了 <code>path names</code> 、 <code>eval-source-maps</code> 等功能来优化开发体验和构建速度。</p><h3 id="sideEffects-设置-——-构建体积优化"><a href="#sideEffects-设置-——-构建体积优化" class="headerlink" title="sideEffects 设置 —— 构建体积优化"></a>sideEffects 设置 —— 构建体积优化</h3><p>Webpack 4 提供了 <code>sideEffects</code> 的配置，通过该配置，可以极大幅度地减小打包出的文件的体积，例如，从 <code>lodash-es</code> 中单独导入 <code>export</code> ，大小为223KB，而将该选项配置为 <code>false</code> 后，打包体积仅为 3 KB（压缩后）。</p><h3 id="JSON-和-Tree-Shaking-的支持"><a href="#JSON-和-Tree-Shaking-的支持" class="headerlink" title="JSON 和 Tree Shaking 的支持"></a>JSON 和 Tree Shaking 的支持</h3><p>使用 ESModule 语法 导入 JSON 时，Webpack 会从 JSON Module 中移除未用到的部分，从而大幅度减小打包体积。</p><h3 id="升级到-UglifyJS2"><a href="#升级到-UglifyJS2" class="headerlink" title="升级到 UglifyJS2"></a>升级到 UglifyJS2</h3><p>这意味着可以不再通过转换器，直接使用 ES6 语法。</p><h3 id="新的模块类型和-mjs-的支持"><a href="#新的模块类型和-mjs-的支持" class="headerlink" title="新的模块类型和 .mjs 的支持"></a>新的模块类型和 .mjs 的支持</h3><blockquote><ul><li>javascript/auto: (Webpack 3 默认类型) 支持所有的 Javascript 模块系统：CommonJS、AMD、ESM</li><li>javascript/esm: EcmaScript 模块，不支持其他所有模块系统（ .mjs 文件的默认类型）</li><li>javascript/dynamic: 仅支持 CommonJS 和 AMD，不支持 EcmaScript 模块</li><li>json: JSON 数据，可以使用 require 和 import 引入（.json 文件的默认类型）</li><li>webassembly/experimental: WebAssembly 模块（目前是 .wasm 文件的默认类型）</li></ul></blockquote><h3 id="移除了-CommonsChunkPlugin"><a href="#移除了-CommonsChunkPlugin" class="headerlink" title="移除了 CommonsChunkPlugin"></a>移除了 CommonsChunkPlugin</h3><p>Webpack 4 删除了 CommonsChunkPlugin，并默认启用了它的许多功能。此外，对于那些需要细粒度的缓存策略的用户，增加了 <code>optimization.splitChunks</code> 和 <code>optimization.runtimeChunk</code> 它们更灵活。</p><h3 id="支持-WebAssembly"><a href="#支持-WebAssembly" class="headerlink" title="支持 WebAssembly"></a>支持 WebAssembly</h3><p>Webpack 现在默认支持 import 和 export 任意本地 WebAssembly 模块。这意味着你可以自己实现 loader 来 import Rust，C++，C 等语言。</p><h3 id="其他更新"><a href="#其他更新" class="headerlink" title="其他更新"></a>其他更新</h3><p>更详细的内容可以参考官方更新日志：<a href="https://github.com/webpack/webpack/releases/tag/v4.0.0-beta.0" target="_blank" rel="noopener">https://github.com/webpack/webpack/releases/tag/v4.0.0-beta.0</a></p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;1月25日，Webpack 发布了 4.0.0-beta.0 版本，做了较大改动。我们第一时间来体验一下。&lt;/p&gt;
&lt;h2 id=&quot;安装&quot;&gt;&lt;a href=&quot;#安装&quot; class=&quot;headerlink&quot; title=&quot;安装&quot;&gt;&lt;/a&gt;安装&lt;/h2&gt;&lt;figure clas
      
    
    </summary>
    
    
      <category term="前端工程" scheme="https://www.thjiang.com/tags/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B/"/>
    
  </entry>
  
  <entry>
    <title>域名发散和域名收敛</title>
    <link href="https://www.thjiang.com/2017/10/20/%E5%9F%9F%E5%90%8D%E5%8F%91%E6%95%A3%E5%92%8C%E5%9F%9F%E5%90%8D%E6%94%B6%E6%95%9B/"/>
    <id>https://www.thjiang.com/2017/10/20/域名发散和域名收敛/</id>
    <published>2017-10-20T12:21:35.000Z</published>
    <updated>2018-02-01T03:36:20.375Z</updated>
    
    <content type="html"><![CDATA[<h3 id="域名发散"><a href="#域名发散" class="headerlink" title="域名发散"></a>域名发散</h3><p>在PC时代，为了保护服务器，浏览器对最大并发数进行了限制，例如 Firefox46 中每一域名下最大连接数为6，IE10 下为8。为了突破这一限制，可以将静态资源置于多个域名下，来提高并发数，使得资源加载速度更快。<br>同时，可以将不需要发送 Cookie 的请求与主域区分开，可以有效减小请求头大小，使得传输速度更快。</p><h3 id="域名收敛"><a href="#域名收敛" class="headerlink" title="域名收敛"></a>域名收敛</h3><p>我们知道，在浏览器地址栏内输入一个域名并回车以后，第一步执行的就是DNS解析，将网址转换为IP地址，然后才能开始TCP握手。</p><p>DNS解析过程如下：</p><ol><li>查找浏览器DNS缓存，如果没有相应的记录， → 2</li><li>查找操作系统DNS缓存，如果没有， → 3</li><li>查找HOSTS文件，如果没有， → 4</li><li>请求ISP的DNS Server， → 5</li><li>从的根域名服务器开始，依次请求顶级域的权威服务器 、二级域服务器等，获取IP地址</li></ol><p>由于移动端存储空间成本、人为干扰等因素，缓存失效的可能性极高，而如果没有缓存，在移动网络等弱网环境下，DNS解析耗时将变得很长（通常要达到1秒以上）。对于静态资源域名，JS等资源的加载缓慢将直接阻塞页面渲染。所以，为了减少 DNS 请求的时间，收敛域名也成为了性能优化的重要方面，同时收敛域名也有利于更好的利用缓存。</p><h3 id="HTTP2"><a href="#HTTP2" class="headerlink" title="HTTP2"></a>HTTP2</h3><p>在域名收敛之后，为了解决浏览器同域名下的并发数量限制，可以启用HTTP2（由于新协议中的 Header 压缩等原因，速度要快于SPDY）。<br>在HTTP2中，提出了多路复用（MultiPlexing）的概念，即连接共享，即每一个request都是是用作连接共享机制的。一个request对应一个id，这样一个连接上可以有多个request，每个连接的request可以随机的混杂在一起，接收方可以根据request的 id将request再归属到各自不同的服务端请求里面，从而解决了浏览器的并发限制问题。</p><h3 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h3><p><a href="https://github.com/amfe/article/issues/1" target="_blank" rel="noopener">https://github.com/amfe/article/issues/1</a><br><a href="http://www.alloyteam.com/2016/07/httphttp2-0spdyhttps-reading-this-is-enough/" target="_blank" rel="noopener">http://www.alloyteam.com/2016/07/httphttp2-0spdyhttps-reading-this-is-enough/</a><br><a href="http://taobaofed.org/blog/2015/12/16/h5-performance-optimization-and-domain-convergence/?utm_source=tuicool&amp;utm_medium=referral" target="_blank" rel="noopener">http://taobaofed.org/blog/2015/12/16/h5-performance-optimization-and-domain-convergence/?utm_source=tuicool&amp;utm_medium=referral</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;在PC时代，为了保护服务器，浏览器对最大并发数进行了限制，例如 Firefox46 中每一域名下最大连接数为6，IE10 下
      
    
    </summary>
    
    
      <category term="性能优化" scheme="https://www.thjiang.com/tags/%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/"/>
    
      <category term="域名" scheme="https://www.thjiang.com/tags/%E5%9F%9F%E5%90%8D/"/>
    
      <category term="DNS" scheme="https://www.thjiang.com/tags/DNS/"/>
    
  </entry>
  
  <entry>
    <title>Webpack 性能优化</title>
    <link href="https://www.thjiang.com/2017/09/19/Webpack-%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/"/>
    <id>https://www.thjiang.com/2017/09/19/Webpack-性能优化/</id>
    <published>2017-09-19T12:29:51.000Z</published>
    <updated>2018-11-01T13:55:04.074Z</updated>
    
    <content type="html"><![CDATA[<hr><p>随着项目的发展，项目目录会越来越大，各种库也会越来越多，会直接导致Webpack的构建效率极低，比如下面的例子：<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><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">$  webpack</span><br><span class="line">Hash: 6aa4a418e100b6563347</span><br><span class="line">Version: webpack 3.5.5</span><br><span class="line">Time: 20199ms</span><br><span class="line">   Asset     Size  Chunks             Chunk Names</span><br><span class="line">index.js  4.27 MB       0  [emitted]  index  </span><br><span class="line"> + 395 hidden modules</span><br></pre></td></tr></table></figure><br>可以看到，在这个项目中有大量的模块，构建一次的时间长达20秒，这显然是不可接受的。</p><h2 id="慢在哪里"><a href="#慢在哪里" class="headerlink" title="慢在哪里"></a>慢在哪里</h2><p>有很多工具提供了可视化的分析，如<a href="https://www.npmjs.com/package/webpack-bundle-analyzer" target="_blank" rel="noopener">Webpack-bundle-analyzer</a>、<a href="http://alexkuz.github.io/webpack-chart/" target="_blank" rel="noopener">webpack-chart</a>、 <a href="http://webpack.github.io/analyse/" target="_blank" rel="noopener">webpack-analyse</a>。<br>以Webpack-bundle-analyzer为例，它提供了一个下图所示的图表，展示了引入的所有模块的大小、路径等信息，可以针对性的做出优化。</p><figure class="image-bubble">                <div class="img-lightbox">                    <div class="overlay"></div>                    <img src="https://c.usr.ink/webpack/webpack-bundle-analyzer.gif" alt="Webpack-bundle-analyzer" title="">                </div>                <div class="image-caption">Webpack-bundle-analyzer</div>            </figure><p>使用上也很简单：</p><figure class="highlight plain"><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">&#x2F;&#x2F; 全局安装：</span><br><span class="line">yarn global add webpack-bundle-analyzer</span><br></pre></td></tr></table></figure><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><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">&#x2F;&#x2F; webpack.config.js 配置</span><br><span class="line">const BundleAnalyzerPlugin &#x3D; require(&#39;webpack-bundle-analyzer&#39;).BundleAnalyzerPlugin;</span><br><span class="line"></span><br><span class="line">plugins: [</span><br><span class="line">    new BundleAnalyzerPlugin(&#123;</span><br><span class="line">    analyzerMode: &#39;server&#39;,</span><br><span class="line">    analyzerHost: &#39;127.0.0.1&#39;,</span><br><span class="line">    analyzerPort: 8888,</span><br><span class="line">    reportFilename: &#39;report.html&#39;,</span><br><span class="line">    defaultSizes: &#39;parsed&#39;,</span><br><span class="line">    openAnalyzer: true,</span><br><span class="line">    generateStatsFile: false,</span><br><span class="line">    statsFilename: &#39;stats.json&#39;,</span><br><span class="line">    logLevel: &#39;info&#39;</span><br><span class="line">    &#125;)</span><br><span class="line">]</span><br></pre></td></tr></table></figure><p>运行<code>webpack</code>命令，会自动在浏览器中打开<code>http://127.0.0.1:8888/</code>页面，展示可视化图表。</p><h2 id="具体优化"><a href="#具体优化" class="headerlink" title="具体优化"></a>具体优化</h2><h3 id="1-使用DllPlugin拆分模块"><a href="#1-使用DllPlugin拆分模块" class="headerlink" title="1. 使用DllPlugin拆分模块"></a>1. 使用<code>DllPlugin</code>拆分模块</h3><p>开发过程中，我们经常需要引入大量第三方库，这些库并不需要随时修改或调试，我们可以使用<code>DllPlugin</code>和<code>DllReferencePlugin</code>单独构建它们。<br>具体使用如下：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line">const HtmlWebpackPlugin &#x3D; require(&#39;html-webpack-plugin&#39;);</span><br><span class="line"></span><br><span class="line">module.exports &#x3D; &#123;</span><br><span class="line">entry: &#123;</span><br><span class="line">vendor: [</span><br><span class="line">    &#39;axios&#39;,</span><br><span class="line">    &#39;vue-i18n&#39;,</span><br><span class="line">    &#39;vue-router&#39;,</span><br><span class="line">    &#39;vuex&#39;</span><br><span class="line">]</span><br><span class="line">&#125;,</span><br><span class="line">output: &#123;</span><br><span class="line">path: path.resolve(__dirname, &#39;..&#x2F;static&#x2F;&#39;),</span><br><span class="line">filename: &#39;[name].dll.js&#39;,</span><br><span class="line">library: &#39;[name]_library&#39;</span><br><span class="line">&#125;,</span><br><span class="line">plugins: [</span><br><span class="line">    new webpack.DllPlugin(&#123;</span><br><span class="line">    path: path.join(__dirname, &#39;build&#39;, &#39;[name]-manifest.json&#39;),</span><br><span class="line">    name: &#39;[name]_library&#39;</span><br><span class="line">    &#125;)</span><br><span class="line">]</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>执行<code>webpack</code>命令，<code>build</code>目录下即可生成 <code>dll.js</code> 文件和对应的 <code>manifest</code> 文件，使用 <code>DLLReferencePlugin</code> 引入：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><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">plugins: [</span><br><span class="line">    new webpack.DllReferencePlugin(&#123;</span><br><span class="line">    context: __dirname,</span><br><span class="line">    manifest: require(&#39;.&#x2F;build&#x2F;vendor-manifest.json&#39;)</span><br><span class="line">    &#125;)</span><br><span class="line">]</span><br></pre></td></tr></table></figure><h3 id="2-使用-externals-通过CDN引入第三方库"><a href="#2-使用-externals-通过CDN引入第三方库" class="headerlink" title="2. 使用 externals 通过CDN引入第三方库"></a>2. 使用 <code>externals</code> 通过CDN引入第三方库</h3><p>Webpack提供了 <code>externals</code> 的方式来引入第三方库，我们可以在 HTML 文件中直接使用 script 标签的形式来引入：</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">"//cdn.bootcss.com/react.min.js"</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">"//cdn.bootcss.com/react-dom.js"</span>&gt;</span><span class="tag">&lt;/<span class="name">script</span>&gt;</span></span><br></pre></td></tr></table></figure><p>在 Webpack 中如下配置即可：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><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">externals: &#123;</span><br><span class="line">&#39;react&#39;: &#39;React&#39;,</span><br><span class="line">&#39;react-dom&#39;: &#39;ReactDOM&#39;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>和 dll 的打包方式相比，主要有以下几点区别：</p><blockquote><ol><li>并非所有依赖库都提供了打包好的生产环境的文件，对于这种只能通过npm来引入的库， externals 无能为力。</li><li>部分依赖库中会存在循环依赖的现象，在一些 React 相关的库中尤为明显，使用 externals 处理会造成路径混乱无法识别。</li><li>使用dll的方式打包好的静态文件在生产环境中需要额外处理，同步到build目录中。（可以使用 CopyWebpackPlugin 等插件）。</li></ol></blockquote><h3 id="3-使用-happypack-开启多线程构建"><a href="#3-使用-happypack-开启多线程构建" class="headerlink" title="3. 使用 happypack 开启多线程构建"></a>3. 使用 <code>happypack</code> 开启多线程构建</h3><p><a href="https://github.com/amireh/happypack" target="_blank" rel="noopener">HappyPack</a>可以将原有的 webpack 对 loader 的执行过程，从单一进程的形式扩展为多进程的模式，从而加速代码构建。使用方式如下：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><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">const HappyPack &#x3D; require(&#39;happypack&#39;);</span><br><span class="line">const happyThreadPool &#x3D; HappyPack.ThreadPool(&#123; size: os.cpus().length &#125;);</span><br><span class="line"></span><br><span class="line">module: &#123;</span><br><span class="line">loaders: [&#123;</span><br><span class="line">test: &#x2F;\.less$&#x2F;,</span><br><span class="line">loader: ExtractTextPlugin.extract(</span><br><span class="line">&#39;style&#39;, path.resolve(__dirname, &#39;.&#x2F;node_modules&#39;, &#39;happypack&#x2F;loader&#39;) + &#39;?id&#x3D;less&#39;</span><br><span class="line">)</span><br><span class="line">&#125;]</span><br><span class="line">&#125;,</span><br><span class="line">plugins: [</span><br><span class="line">new HappyPack(&#123;</span><br><span class="line">id: &#39;less&#39;,</span><br><span class="line">loaders: [&#39;css!less&#39;],</span><br><span class="line">threadPool: happyThreadPool,</span><br><span class="line">cache: true,</span><br><span class="line">verbose: true</span><br><span class="line">&#125;)</span><br><span class="line">]</span><br></pre></td></tr></table></figure><p>更多配置可以参考文档：<a href="https://github.com/amireh/happypack" target="_blank" rel="noopener">https://github.com/amireh/happypack</a></p><h3 id="4-增强代码压缩工具"><a href="#4-增强代码压缩工具" class="headerlink" title="4. 增强代码压缩工具"></a>4. 增强代码压缩工具</h3><p>Webpack默认提供的<code>UglifyJS</code>插件速度很慢，可以使用<code>webpack-parallel-uglify-plugin</code>替换。配置如下：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><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">const ParallelUglifyPlugin &#x3D; require(&#39;webpack-parallel-uglify-plugin&#39;);</span><br><span class="line">const os &#x3D; require(&#39;os&#39;);</span><br><span class="line"></span><br><span class="line">new ParallelUglifyPlugin(&#123;</span><br><span class="line">    workerCount: os.cpus().length,</span><br><span class="line">    cacheDir: &#39;.cache&#x2F;&#39;,</span><br><span class="line">    uglifyJS: &#123;</span><br><span class="line">        output: &#123;</span><br><span class="line">            comments: false</span><br><span class="line">        &#125;,</span><br><span class="line">        compress: &#123;</span><br><span class="line">            warnings: false,</span><br><span class="line">            drop_debugger: true,</span><br><span class="line">            drop_console: true</span><br><span class="line">        &#125;,</span><br><span class="line">        sourceMap: true,</span><br><span class="line">        mangle: true</span><br><span class="line">    &#125;</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><h3 id="5-缩小文件搜索范围"><a href="#5-缩小文件搜索范围" class="headerlink" title="5. 缩小文件搜索范围"></a>5. 缩小文件搜索范围</h3><p>Node.js的模块的载入及缓存机制如下：</p><ol><li>载入内置模块</li><li>载入文件模块</li><li>载入文件目录模块</li><li>载入node_modules里的模块</li><li>自动缓存已载入模块</li></ol><p>如果模块名不是路径，也不是内置模块，Node将试图去当前目录的node_modules文件夹里搜索。如果当前目录的node_modules里没有找到，Node会从父目录的node_modules里搜索，这样递归下去直到根目录。</p><p>我们可以对搜索过程进行一些优化，比如可以直接指定node_modules的路径：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><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">module.exports &#x3D; &#123;</span><br><span class="line">    resolve: &#123;</span><br><span class="line">        modules: [path.resolve(__dirname, &#39;node_modules&#39;)]</span><br><span class="line">    &#125;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><h3 id="6-Webpack3-新功能-Scope-Hoisting"><a href="#6-Webpack3-新功能-Scope-Hoisting" class="headerlink" title="6. Webpack3 新功能: Scope Hoisting"></a>6. Webpack3 新功能: Scope Hoisting</h3><p>在Webpack3.0 版本中，提供了一个新的功能：<code>Scope Hoisting</code>，又译作“作用域提升”。在Webpack2中，打包后的文件里每个模块都会被包装在一个单独的闭包中，这些闭包会导致JS执行速度变慢，Scope Hoisting则可以将所有模块打包进一个大的闭包中。只需在配置文件中添加一个新的插件，就可以让 Webpack 打包出来的代码文件更小、运行的更快：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><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">module.exports &#x3D; &#123;</span><br><span class="line">    plugins: [</span><br><span class="line">        new webpack.optimize.ModuleConcatenationPlugin()</span><br><span class="line">    ]</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="7-合理配置-babel-loader"><a href="#7-合理配置-babel-loader" class="headerlink" title="7. 合理配置 babel-loader"></a>7. 合理配置 babel-loader</h3><p>在balel的官方文档中有一句话：<code>babel-loader is slow!</code>。<br>我们在babel的配置中，也要尽量注意这一点。<br>比如，使用 <code>/\.js$/</code>来匹配文件的话，可能会将node_modules中的文件一起处理，我们需要使用exclude、include等尽可能准确的来指定需要转换的内容。<br>我们还可以开启babel的缓存配置（cacheDirectory）来提升一倍以上的效率，配置如下：</p><figure class="highlight plain"><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">test: &#x2F;\.js$&#x2F;,</span><br><span class="line">loader: &#39;babel-loader?cacheDirectory&#x3D;true&#39;,</span><br><span class="line">exclude: &#x2F;node_modules&#x2F;</span><br></pre></td></tr></table></figure>]]></content>
    
    <summary type="html">
    
      
      
        &lt;hr&gt;
&lt;p&gt;随着项目的发展，项目目录会越来越大，各种库也会越来越多，会直接导致Webpack的构建效率极低，比如下面的例子：&lt;br&gt;&lt;figure class=&quot;highlight plain&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre&gt;&lt;span
      
    
    </summary>
    
      <category term="性能优化" scheme="https://www.thjiang.com/categories/%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/"/>
    
    
      <category term="前端工程" scheme="https://www.thjiang.com/tags/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B/"/>
    
      <category term="性能优化" scheme="https://www.thjiang.com/tags/%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/"/>
    
  </entry>
  
</feed>
