<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>193577746 (kyriewen)</title>
    <link>https://beta.w2solo.com/193577746</link>
    <description>独立开发者 / 前端工程师</description>
    <language>en-us</language>
    <item>
      <title>手写 call、apply、bind：从原理到实现，附 3 个最容易忽略的边界情况</title>
      <description>&lt;blockquote&gt;
&lt;p&gt;本文手写实现 JS 中最重要的三个 this 绑定函数，并处理 90% 的人会忽略的边界问题。代码可直接复制。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;hr&gt;
&lt;h2 id="一、准备知识：this 的优先级"&gt;一、准备知识：this 的优先级&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;call&lt;/code&gt;、&lt;code&gt;apply&lt;/code&gt;、&lt;code&gt;bind&lt;/code&gt; 都用于显式绑定 &lt;code&gt;this&lt;/code&gt;。区别：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;call&lt;/code&gt;：立即执行，参数逐个传递&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;apply&lt;/code&gt;：立即执行，参数以数组传递&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;bind&lt;/code&gt;：返回新函数，不立即执行，支持柯里化&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;手写它们的关键：&lt;strong&gt;将函数挂载到 &lt;code&gt;context&lt;/code&gt; 对象上执行，然后删除&lt;/strong&gt;。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="二、手写 call"&gt;二、手写 call&lt;/h2&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nb"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prototype&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;myCall&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// 1. 处理 context 为 null/undefined 时指向全局对象&lt;/span&gt;
  &lt;span class="nx"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="nb"&gt;global&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// 2. 使用 Symbol 避免覆盖原对象属性&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fnKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Symbol&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fn&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;fnKey&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// 3. 执行函数并获取返回值&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;fnKey&lt;/span&gt;&lt;span class="p"&gt;](...&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// 4. 删除临时属性&lt;/span&gt;
  &lt;span class="k"&gt;delete&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;fnKey&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;测试&lt;/strong&gt;：&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;say&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;age&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; is &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;age&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;obj&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;张三&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;say&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;myCall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// "张三 is 25"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;边界 1&lt;/strong&gt;：&lt;code&gt;context&lt;/code&gt; 为原始类型（number/string/boolean）时，需要转为对象。&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 改进&lt;/span&gt;
&lt;span class="nx"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2 id="三、手写 apply"&gt;三、手写 apply&lt;/h2&gt;
&lt;p&gt;与 &lt;code&gt;call&lt;/code&gt; 几乎一样，只是参数形式不同。&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nb"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prototype&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;myApply&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;argsArray&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fnKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Symbol&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fn&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;fnKey&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;fnKey&lt;/span&gt;&lt;span class="p"&gt;](...&lt;/span&gt;&lt;span class="nx"&gt;argsArray&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;delete&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;fnKey&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;测试&lt;/strong&gt;：&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;say&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;myApply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt; &lt;span class="c1"&gt;// "张三 is 30"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2 id="四、手写 bind（最难）"&gt;四、手写 bind（最难）&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;bind&lt;/code&gt; 的特点：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;返回一个新函数&lt;/li&gt;
&lt;li&gt;支持柯里化（预设参数）&lt;/li&gt;
&lt;li&gt;当新函数作为构造函数（&lt;code&gt;new&lt;/code&gt;）时，&lt;code&gt;this&lt;/code&gt; 指向实例，而非绑定的 &lt;code&gt;context&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nb"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prototype&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;myBind&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;presetArgs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;bound&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;restArgs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// 关键：如果当前函数被 new 调用，this 指向实例，忽略绑定的 context&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isNewCall&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nx"&gt;bound&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;isNewCall&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;presetArgs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;restArgs&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// 维持原型链（如果原函数有 prototype）&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prototype&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;bound&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prototype&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prototype&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;bound&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;测试&lt;/strong&gt;：&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;Person&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;age&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;age&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;age&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;boundPerson&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Person&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;myBind&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;李四&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;boundPerson&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;28&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 作为构造函数，this 指向 p，忽略 {x:1}&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Person { name: '李四', age: 28 }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2 id="五、3 个最容易忽略的边界情况"&gt;五、3 个最容易忽略的边界情况&lt;/h2&gt;&lt;h3 id="边界 1：context 为 null/undefined"&gt;边界 1：context 为 null/undefined&lt;/h3&gt;
&lt;p&gt;原生 &lt;code&gt;call&lt;/code&gt; 会指向全局对象（浏览器 &lt;code&gt;window&lt;/code&gt;，Node &lt;code&gt;global&lt;/code&gt;）。我们的实现已处理。&lt;/p&gt;
&lt;h3 id="边界 2：函数有返回值"&gt;边界 2：函数有返回值&lt;/h3&gt;
&lt;p&gt;必须将返回值传给调用方。已在实现中通过 &lt;code&gt;result&lt;/code&gt; 返回。&lt;/p&gt;
&lt;h3 id="边界 3：bind 后的函数作为构造函数"&gt;边界 3：bind 后的函数作为构造函数&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;当使用 &lt;code&gt;new&lt;/code&gt; 调用 &lt;code&gt;bound&lt;/code&gt; 时，&lt;code&gt;this&lt;/code&gt; 指向新创建的实例，不能继续绑定到原来的 &lt;code&gt;context&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;上述 &lt;code&gt;myBind&lt;/code&gt; 中通过 &lt;code&gt;this instanceof bound&lt;/code&gt; 判断即可。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;验证&lt;/strong&gt;：&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;Base&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;age&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;age&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;age&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;BoundBase&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;myBind&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fake&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;obj&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;BoundBase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Base { age: 20 } ，而不是 { name: 'fake' }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2 id="六、完整代码（可直接复制）"&gt;六、完整代码（可直接复制）&lt;/h2&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// call&lt;/span&gt;
&lt;span class="nb"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prototype&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;myCall&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;globalThis&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Symbol&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;](...&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;delete&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// apply&lt;/span&gt;
&lt;span class="nb"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prototype&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;myApply&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;argsArray&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;globalThis&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Symbol&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;](...&lt;/span&gt;&lt;span class="nx"&gt;argsArray&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;delete&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// bind&lt;/span&gt;
&lt;span class="nb"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prototype&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;myBind&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;presetArgs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;bound&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;restArgs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isNew&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nx"&gt;bound&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;isNew&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;globalThis&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;presetArgs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;restArgs&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prototype&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;bound&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prototype&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prototype&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;bound&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2 id="七、总结"&gt;七、总结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;call&lt;/code&gt; 和 &lt;code&gt;apply&lt;/code&gt; 的核心是 &lt;strong&gt;临时挂载 + 执行 + 删除&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;bind&lt;/code&gt; 的核心是 &lt;strong&gt;返回函数 + 柯里化 + 判断 new 调用&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;边界情况（null/undefined、原始类型、new 优先级）是面试和实际编码的常考点。&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;文中所有代码均已测试，可放心直接用于 polyfill。下一篇准备手写 &lt;code&gt;instanceof&lt;/code&gt; 和 &lt;code&gt;new&lt;/code&gt; 操作符，欢迎关注。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;讨论&lt;/strong&gt;：你还遇到过哪些 this 相关的奇怪 bug？评论区分享。&lt;/p&gt;</description>
      <author>193577746</author>
      <pubDate>Thu, 11 Jun 2026 18:40:58 +0800</pubDate>
      <link>https://beta.w2solo.com/topics/7524</link>
      <guid>https://beta.w2solo.com/topics/7524</guid>
    </item>
    <item>
      <title>CSS Container Queries：彻底告别 @media 写到手软，附 5 个真实布局案例</title>
      <description>&lt;blockquote&gt;
&lt;p&gt;以前写响应式组件，总要根据视口宽度写一堆 &lt;code&gt;@media&lt;/code&gt;，代码又臭又长。现在 Container Queries 让组件根据&lt;strong&gt;父容器&lt;/strong&gt;尺寸响应，代码直接减半。本文讲透语法 + 5 个可直接复用的案例。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;hr&gt;
&lt;h2 id="一、Container Queries 解决了什么问题？"&gt;一、Container Queries 解决了什么问题？&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;@media&lt;/code&gt; 媒体查询基于&lt;strong&gt;视口&lt;/strong&gt;宽度。这导致一个组件在不同父容器下无法独立响应——同样的卡片，在侧边栏和主内容区需要两套样式或者额外类名。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Container Queries&lt;/strong&gt; 允许你定义：当&lt;strong&gt;父容器&lt;/strong&gt;达到某个宽度时，子元素改变布局。&lt;/p&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* 以前：基于视口，无法区分卡片在哪个容器里 */&lt;/span&gt;
&lt;span class="k"&gt;@media&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;min-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;600px&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;.card&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;/* 现在：基于父容器宽度 */&lt;/span&gt;
&lt;span class="k"&gt;@container&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;min-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;300px&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;.card&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2 id="二、基础语法（三步搞定）"&gt;二、基础语法（三步搞定）&lt;/h2&gt;&lt;h3 id="2.1 定义容器"&gt;2.1 定义容器&lt;/h3&gt;&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.sidebar&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;container-type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;inline-size&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c"&gt;/* 监听内联方向（宽度）变化 */&lt;/span&gt;
  &lt;span class="py"&gt;container-name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sidebar&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;       &lt;span class="c"&gt;/* 可选，给容器起名 */&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;container-type: inline-size&lt;/code&gt; 最常用，表示根据宽度变化。&lt;/li&gt;
&lt;li&gt;也可以 &lt;code&gt;size&lt;/code&gt;（宽高都监听）或 &lt;code&gt;normal&lt;/code&gt;（不监听）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="2.2 使用 @user5"&gt;2.2 使用 &lt;a href="/container" class="user-mention" title="@container"&gt;&lt;i&gt;@&lt;/i&gt;container&lt;/a&gt;
&lt;/h3&gt;&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@container&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;min-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;300px&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;.card-title&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1.5rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果只有一个容器，可以直接写条件。如果有多个容器，用 &lt;code&gt;container-name&lt;/code&gt; 限定：&lt;/p&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@container&lt;/span&gt; &lt;span class="n"&gt;sidebar&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;min-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;300px&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c"&gt;/* 只对 sidebar 容器内的元素生效 */&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="2.3 组合与范围"&gt;2.3 组合与范围&lt;/h3&gt;&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@container&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;100px&lt;/span&gt; &lt;span class="err"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt; &lt;span class="err"&gt;&amp;lt;&lt;/span&gt; &lt;span class="m"&gt;300px&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c"&gt;/* 容器宽度在 100px 到 300px 之间 */&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2 id="三、与 @user9 对比（代码量减少 50%）"&gt;三、与 &lt;a href="/media" class="user-mention" title="@media"&gt;&lt;i&gt;@&lt;/i&gt;media&lt;/a&gt; 对比（代码量减少 50%）&lt;/h2&gt;&lt;table class="table table-bordered table-striped"&gt;
&lt;tr&gt;
&lt;th&gt;场景&lt;/th&gt;
&lt;th&gt;传统 &lt;a href="/media" class="user-mention" title="@media"&gt;&lt;i&gt;@&lt;/i&gt;media&lt;/a&gt; 做法&lt;/th&gt;
&lt;th&gt;Container Queries 做法&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;卡片在侧边栏时垂直，在主区域时水平&lt;/td&gt;
&lt;td&gt;写两套类名或全局判断&lt;/td&gt;
&lt;td&gt;父容器定义后，卡片内一套规则自动适配&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;仪表盘小部件适应不同格子尺寸&lt;/td&gt;
&lt;td&gt;根据视口断点统一控制&lt;/td&gt;
&lt;td&gt;每个小部件独立响应自己的容器&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;代码对比&lt;/strong&gt;：&lt;/p&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- 以前：需要额外类名 --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"sidebar"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"card vertical"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"main"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"card horizontal"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* 以前：手动控制 */&lt;/span&gt;
&lt;span class="nc"&gt;.card.vertical&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;flex-direction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;column&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nc"&gt;.card.horizontal&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;flex-direction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;/* Container Queries：一套规则 */&lt;/span&gt;
&lt;span class="nc"&gt;.card&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;container-type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;inline-size&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;@container&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;min-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;400px&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;.card&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;flex-direction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;@container&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;399px&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;.card&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;flex-direction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;column&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2 id="四、5 个实战案例（可直接复制）"&gt;四、5 个实战案例（可直接复制）&lt;/h2&gt;&lt;h3 id="案例 1：卡片组件根据容器宽度切换布局"&gt;案例 1：卡片组件根据容器宽度切换布局&lt;/h3&gt;&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"card-container"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"card"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"photo.jpg"&lt;/span&gt; &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"card-body"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;h3&amp;gt;&lt;/span&gt;标题&lt;span class="nt"&gt;&amp;lt;/h3&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;描述文字&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.card-container&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;container-type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;inline-size&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nc"&gt;.card&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;flex-direction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;column&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;@container&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;min-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;350px&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;.card&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;flex-direction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;align-items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;center&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nc"&gt;.card&lt;/span&gt; &lt;span class="nt"&gt;img&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;40%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="案例 2：侧边栏与主内容区共享同一卡片组件"&gt;案例 2：侧边栏与主内容区共享同一卡片组件&lt;/h3&gt;&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* 不需要任何额外代码，卡片自己响应父容器宽度 */&lt;/span&gt;
&lt;span class="nc"&gt;.sidebar&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;container-type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;inline-size&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;280px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nc"&gt;.main&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;container-type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;inline-size&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;800px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c"&gt;/* 卡片规则如上，当父容器&amp;lt;350px时垂直，&amp;gt;350px时水平 */&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="案例 3：仪表盘小部件矩阵"&gt;案例 3：仪表盘小部件矩阵&lt;/h3&gt;&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"dashboard"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"widget"&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"width: 200px"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;...&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"widget"&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"width: 400px"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;...&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"widget"&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"width: 200px"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;...&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.widget&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;container-type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;inline-size&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nc"&gt;.widget-content&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;12px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;@container&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;min-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;250px&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;.widget-content&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;16px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="py"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="案例 4：商品列表切换行列（类似 Grid 与 Flex 混合）"&gt;案例 4：商品列表切换行列（类似 Grid 与 Flex 混合）&lt;/h3&gt;&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.product-list&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;container-type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;inline-size&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;flex-wrap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;wrap&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nc"&gt;.product-item&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="m"&gt;200px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;@container&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;500px&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;.product-item&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;flex-basis&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c"&gt;/* 小于500px时每行一个 */&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="案例 5：自适应导航栏（折叠菜单）"&gt;案例 5：自适应导航栏（折叠菜单）&lt;/h3&gt;&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;nav&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"navbar"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"logo"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Logo&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;ul&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"nav-links"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&lt;/span&gt;首页&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&lt;/span&gt;产品&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&lt;/span&gt;关于&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/nav&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.navbar&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;container-type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;inline-size&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;justify-content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;space-between&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nc"&gt;.nav-links&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;@container&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;500px&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;.navbar&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;flex-direction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;column&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nc"&gt;.nav-links&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;flex-direction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;column&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;margin-top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.5rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2 id="五、浏览器兼容性与降级方案"&gt;五、浏览器兼容性与降级方案&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;支持情况&lt;/strong&gt;（截至 2026-06）：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Chrome 105+ ✅&lt;/li&gt;
&lt;li&gt;Firefox 110+ ✅&lt;/li&gt;
&lt;li&gt;Safari 16+ ✅&lt;/li&gt;
&lt;li&gt;Edge 105+ ✅&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;如果需要兼容旧浏览器&lt;/strong&gt;，使用 &lt;code&gt;@supports&lt;/code&gt; 渐进增强：&lt;/p&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* 降级：默认样式（例如始终垂直） */&lt;/span&gt;
&lt;span class="nc"&gt;.card&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;flex-direction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;column&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c"&gt;/* 支持 Container Queries 时覆盖 */&lt;/span&gt;
&lt;span class="k"&gt;@supports&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;container-type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;inline-size&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;.card-container&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="py"&gt;container-type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;inline-size&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;@container&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;min-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;350px&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;.card&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nl"&gt;flex-direction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2 id="六、总结"&gt;六、总结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Container Queries 让组件根据&lt;strong&gt;父容器&lt;/strong&gt;而非视口响应，实现真正的组件级响应式。&lt;/li&gt;
&lt;li&gt;语法简单：&lt;code&gt;container-type&lt;/code&gt; + &lt;code&gt;@container&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;5 个案例覆盖卡片、侧边栏、仪表盘、商品列表、导航栏。&lt;/li&gt;
&lt;li&gt;兼容性已可投入生产，配合 &lt;code&gt;@supports&lt;/code&gt; 安全降级。&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;文中所有代码均可直接复制到项目中。以后写响应式组件，先问自己：这个样式是由视口决定，还是由父容器决定？&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;讨论&lt;/strong&gt;：你在开发中遇到过哪些因 &lt;code&gt;@media&lt;/code&gt; 全局断点导致的组件复用困难？欢迎留言分享。&lt;/p&gt;</description>
      <author>193577746</author>
      <pubDate>Wed, 10 Jun 2026 21:16:54 +0800</pubDate>
      <link>https://beta.w2solo.com/topics/7516</link>
      <guid>https://beta.w2solo.com/topics/7516</guid>
    </item>
    <item>
      <title>前端性能优化：LCP 从 4s 到 0.9s 的 5 个核心手段（附配置代码）</title>
      <description>&lt;h2 id="一、LCP 的定义与优化目标"&gt;一、LCP 的定义与优化目标&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;LCP（Largest Contentful Paint）&lt;/strong&gt; 衡量页面最大内容元素（通常是图片、视频、大文本块）的渲染时间。优化目标：&lt;strong&gt;≤ 2.5 秒&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;影响 LCP 的主要因素：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;资源加载慢（图片、字体）&lt;/li&gt;
&lt;li&gt;渲染阻塞（CSS、JS）&lt;/li&gt;
&lt;li&gt;客户端渲染延迟&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;下面按优先级列出 5 个最有效的优化手段。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="二、优化手段 1：图片优化（WebP + 响应式 + 懒加载）"&gt;二、优化手段 1：图片优化（WebP + 响应式 + 懒加载）&lt;/h2&gt;&lt;h3 id="原理"&gt;原理&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;使用现代图片格式（WebP/AVIF）体积更小&lt;/li&gt;
&lt;li&gt;不同设备加载不同尺寸，减少无效传输&lt;/li&gt;
&lt;li&gt;非首屏图片懒加载&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="具体配置"&gt;具体配置&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;WebP 替换（Nginx 自动协商）&lt;/strong&gt;：&lt;/p&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="p"&gt;~&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="s"&gt;.(jpg|jpeg|png)&lt;/span&gt;$ &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kn"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;Vary&lt;/span&gt; &lt;span class="s"&gt;Accept&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kn"&gt;if&lt;/span&gt; &lt;span class="s"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$http_accept&lt;/span&gt; &lt;span class="p"&gt;~&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt; &lt;span class="s"&gt;"image/webp")&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;rewrite&lt;/span&gt; &lt;span class="s"&gt;(.*)&lt;/span&gt; &lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s"&gt;.webp&lt;/span&gt; &lt;span class="s"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;响应式图片（HTML）&lt;/strong&gt;：&lt;/p&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt;
  &lt;span class="na"&gt;srcset=&lt;/span&gt;&lt;span class="s"&gt;"hero-480w.webp 480w, hero-960w.webp 960w, hero-1440w.webp 1440w"&lt;/span&gt;
  &lt;span class="na"&gt;sizes=&lt;/span&gt;&lt;span class="s"&gt;"(max-width: 600px) 480px, (max-width: 1200px) 960px, 1440px"&lt;/span&gt;
  &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"hero-fallback.jpg"&lt;/span&gt;
  &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"description"&lt;/span&gt;
  &lt;span class="na"&gt;loading=&lt;/span&gt;&lt;span class="s"&gt;"eager"&lt;/span&gt;
&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;懒加载&lt;/strong&gt;：&lt;/p&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"product-1.jpg"&lt;/span&gt; &lt;span class="na"&gt;loading=&lt;/span&gt;&lt;span class="s"&gt;"lazy"&lt;/span&gt; &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"product"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="收益"&gt;收益&lt;/h3&gt;
&lt;p&gt;首屏图片传输体积减少 50%~70%，LCP 直接降低 1~2 秒。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="三、优化手段 2：字体优化（font-display + 预加载）"&gt;三、优化手段 2：字体优化（font-display + 预加载）&lt;/h2&gt;&lt;h3 id="原理"&gt;原理&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;默认情况下，自定义字体下载期间浏览器会隐藏文字（FOIT），阻塞渲染&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;font-display: swap&lt;/code&gt; 先显示系统字体，字体加载后替换&lt;/li&gt;
&lt;li&gt;预加载关键字体可提前下载&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="CSS 配置"&gt;CSS 配置&lt;/h3&gt;&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@font-face&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;font-family&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;'MainFont'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;src&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sx"&gt;url('/fonts/main.woff2')&lt;/span&gt; &lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;'woff2'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;font-display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;swap&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="HTML 预加载"&gt;HTML 预加载&lt;/h3&gt;&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"preload"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/fonts/main.woff2"&lt;/span&gt; &lt;span class="na"&gt;as=&lt;/span&gt;&lt;span class="s"&gt;"font"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"font/woff2"&lt;/span&gt; &lt;span class="na"&gt;crossorigin&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="收益"&gt;收益&lt;/h3&gt;
&lt;p&gt;消除字体阻塞，FCP 提前 0.5~1 秒，间接改善 LCP。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="四、优化手段 3：JavaScript 分割与异步加载"&gt;四、优化手段 3：JavaScript 分割与异步加载&lt;/h2&gt;&lt;h3 id="原理"&gt;原理&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;大型 JS bundle 会阻塞解析和渲染&lt;/li&gt;
&lt;li&gt;代码分割 + defer 让非关键 JS 不阻塞首屏&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="Webpack 配置（splitChunks）"&gt;Webpack 配置（splitChunks）&lt;/h3&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// webpack.config.js&lt;/span&gt;
&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;optimization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;splitChunks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;all&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;minSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;20000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;cacheGroups&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;vendor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[\\/]&lt;/span&gt;&lt;span class="sr"&gt;node_modules&lt;/span&gt;&lt;span class="se"&gt;[\\/]&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vendors&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="动态导入"&gt;动态导入&lt;/h3&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 需要时才加载&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./heavy-module&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;module&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;init&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="第三方脚本延迟"&gt;第三方脚本延迟&lt;/h3&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 在 load 事件后加载&lt;/span&gt;
&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;load&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;script&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;script&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;script&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://analytics.example.com/tracker.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;head&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;script&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="收益"&gt;收益&lt;/h3&gt;
&lt;p&gt;首屏 JS 体积减少 60%~80%，主线程空闲提前，LCP 改善 0.5~1 秒。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="五、优化手段 4：CSS 内联关键样式 + 异步加载非关键样式"&gt;五、优化手段 4：CSS 内联关键样式 + 异步加载非关键样式&lt;/h2&gt;&lt;h3 id="原理"&gt;原理&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;外部 CSS 文件会阻塞渲染&lt;/li&gt;
&lt;li&gt;首屏需要的样式（关键 CSS）内联到 &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;完整 CSS 文件异步加载，不阻塞&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="提取关键 CSS（使用 critical 工具）"&gt;提取关键 CSS（使用 &lt;code&gt;critical&lt;/code&gt; 工具）&lt;/h3&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; critical
critical index.html &lt;span class="nt"&gt;--inline&lt;/span&gt; &lt;span class="nt"&gt;--base&lt;/span&gt; dist/ &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; index-critical.html
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="异步加载完整 CSS"&gt;异步加载完整 CSS&lt;/h3&gt;&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"preload"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/full.css"&lt;/span&gt; &lt;span class="na"&gt;as=&lt;/span&gt;&lt;span class="s"&gt;"style"&lt;/span&gt; &lt;span class="na"&gt;onload=&lt;/span&gt;&lt;span class="s"&gt;"this.onload=null;this.rel='stylesheet'"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;noscript&amp;gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"stylesheet"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/full.css"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/noscript&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="收益"&gt;收益&lt;/h3&gt;
&lt;p&gt;消除 CSS 阻塞，FCP 提前 0.3~0.8 秒。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="六、优化手段 5：服务器压缩与 CDN 预热"&gt;六、优化手段 5：服务器压缩与 CDN 预热&lt;/h2&gt;&lt;h3 id="原理"&gt;原理&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Brotli 压缩比 Gzip 高 20%~30%&lt;/li&gt;
&lt;li&gt;CDN 预热让用户首次访问即命中边缘节点&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="Nginx Brotli 配置（需编译 brotli 模块）"&gt;Nginx Brotli 配置（需编译 brotli 模块）&lt;/h3&gt;&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;brotli&lt;/span&gt; &lt;span class="no"&gt;on&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;brotli_comp_level&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;brotli_types&lt;/span&gt; &lt;span class="nc"&gt;text/plain&lt;/span&gt; &lt;span class="nc"&gt;text/css&lt;/span&gt; &lt;span class="nc"&gt;application/javascript&lt;/span&gt; &lt;span class="nc"&gt;application/json&lt;/span&gt; &lt;span class="nc"&gt;image/svg&lt;/span&gt;&lt;span class="s"&gt;+xml&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="CDN 预热（阿里云示例）"&gt;CDN 预热（阿里云示例）&lt;/h3&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;aliyun cdn PushObjectCache &lt;span class="nt"&gt;--ObjectPath&lt;/span&gt; &lt;span class="s2"&gt;"https://example.com/static/main.js"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="收益"&gt;收益&lt;/h3&gt;
&lt;p&gt;传输体积减少 20%~30%，RTT 高的地区尤其明显。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="七、优化优先级总结"&gt;七、优化优先级总结&lt;/h2&gt;&lt;table class="table table-bordered table-striped"&gt;
&lt;tr&gt;
&lt;th&gt;优先级&lt;/th&gt;
&lt;th&gt;手段&lt;/th&gt;
&lt;th&gt;预期 LCP 收益&lt;/th&gt;
&lt;th&gt;实施成本&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;图片 WebP + 响应式&lt;/td&gt;
&lt;td&gt;减少 1~2 秒&lt;/td&gt;
&lt;td&gt;中&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;字体 swap + 预加载&lt;/td&gt;
&lt;td&gt;减少 0.5~1 秒&lt;/td&gt;
&lt;td&gt;低&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;JS 代码分割 + defer&lt;/td&gt;
&lt;td&gt;减少 0.5~1 秒&lt;/td&gt;
&lt;td&gt;中&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;关键 CSS 内联&lt;/td&gt;
&lt;td&gt;减少 0.3~0.8 秒&lt;/td&gt;
&lt;td&gt;中&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Brotli + CDN 预热&lt;/td&gt;
&lt;td&gt;减少 0.2~0.5 秒&lt;/td&gt;
&lt;td&gt;低&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;p&gt;按此顺序执行，2 天内可将 LCP 从 4 秒以上降至 1.5 秒以内。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="八、检测工具"&gt;八、检测工具&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Lighthouse&lt;/strong&gt;（Chrome DevTools）&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WebPageTest&lt;/strong&gt;（&lt;a href="https://www.webpagetest.org" rel="nofollow" target="_blank"&gt;https://www.webpagetest.org&lt;/a&gt;）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Chrome Performance 面板&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;hr&gt;

&lt;p&gt;以上所有配置均可直接复制使用。如有具体场景问题，欢迎讨论。&lt;/p&gt;</description>
      <author>193577746</author>
      <pubDate>Tue, 09 Jun 2026 18:17:18 +0800</pubDate>
      <link>https://beta.w2solo.com/topics/7508</link>
      <guid>https://beta.w2solo.com/topics/7508</guid>
    </item>
    <item>
      <title>我读了一遍 Babel 编译后的 async/await，终于搞懂了它的原理（附 20 行手写实现）</title>
      <description>&lt;blockquote&gt;
&lt;p&gt;本文从一个真实项目 bug 出发，带你读 Babel 编译结果，然后手写一个最简 async/await。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="1. 一个真实的“翻车”场景"&gt;1. 一个真实的 “翻车” 场景&lt;/h2&gt;
&lt;p&gt;上周维护一个老项目，看到同事写了这样的代码：&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;processItems&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/api/process/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;他把 &lt;code&gt;await&lt;/code&gt; 放在 &lt;code&gt;for&lt;/code&gt; 循环里，本意是&lt;strong&gt;串行&lt;/strong&gt;请求，结果因为接口响应时间不同，数据顺序全乱了。我帮他改成 &lt;code&gt;Promise.all&lt;/code&gt; 后，突然意识到：&lt;strong&gt;我其实并不清楚 &lt;code&gt;async/await&lt;/code&gt; 底层到底怎么工作的&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;于是我去看了 Babel 把 &lt;code&gt;async&lt;/code&gt; 函数编译成了什么样子——发现它只是一个 &lt;strong&gt;generator + 自动执行器&lt;/strong&gt; 的包装。&lt;/p&gt;

&lt;p&gt;这篇文章，我就用 &lt;strong&gt;20 行代码&lt;/strong&gt;，带你手写一个最简版的 &lt;code&gt;async/await&lt;/code&gt;。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="2. 前置知识：Generator 函数"&gt;2. 前置知识：Generator 函数&lt;/h2&gt;
&lt;p&gt;如果你已经熟悉 generator，可以跳过本节。&lt;/p&gt;

&lt;p&gt;Generator 是可以暂停和恢复的函数：&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;gen&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;step 1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;step 2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;gen&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt; &lt;span class="c1"&gt;// { value: 1, done: false }&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt; &lt;span class="c1"&gt;// { value: 2, done: false }&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt; &lt;span class="c1"&gt;// { value: 3, done: true }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;每次调用 &lt;code&gt;next()&lt;/code&gt;，函数会执行到下一个 &lt;code&gt;yield&lt;/code&gt; 并暂停。&lt;br&gt;
这个特性正好可以用来模拟 &lt;code&gt;await&lt;/code&gt; 的 “等待异步结果再继续” 的行为。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="3. Babel 编译后长什么样？"&gt;3. Babel 编译后长什么样？&lt;/h2&gt;
&lt;p&gt;写一个最简单的 &lt;code&gt;async&lt;/code&gt; 函数：&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;getData&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;用 Babel（&lt;a href="/babel" class="user-mention" title="@babel"&gt;&lt;i&gt;@&lt;/i&gt;babel&lt;/a&gt;/preset-env）编译后（简化版），变成了类似这样的代码：&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;getData&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;_asyncToGenerator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;})();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;核心是 &lt;code&gt;_asyncToGenerator&lt;/code&gt; 这个辅助函数——它接收一个 &lt;strong&gt;generator 函数&lt;/strong&gt;，并返回一个&lt;strong&gt;自动执行该 generator 的函数&lt;/strong&gt;，最终返回一个 Promise。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="4. 手写核心：自动执行器"&gt;4. 手写核心：自动执行器&lt;/h2&gt;
&lt;p&gt;我们先写一个函数 &lt;code&gt;run(generatorFunc)&lt;/code&gt;，它能自动执行 generator 直到结束。&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;generatorFunc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;generator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;generatorFunc&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;   &lt;span class="c1"&gt;// 获取迭代器对象&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nextFunc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;done&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;nextFunc&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;done&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="c1"&gt;// 确保 value 是一个 Promise&lt;/span&gt;
          &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;generator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;generator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;throw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
          &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;generator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt; &lt;span class="c1"&gt;// 启动执行&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;测试一下&lt;/strong&gt;：&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;myGen&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;myGen&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 输出 3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;完美运行。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;上面的 &lt;code&gt;run&lt;/code&gt; 就是 &lt;code&gt;_asyncToGenerator&lt;/code&gt; 最核心的逻辑。真正的 Babel 实现还处理了更多边界情况，但原理完全一致。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;hr&gt;
&lt;h2 id="5. 封装成真正的 asyncToGenerator"&gt;5. 封装成真正的 &lt;code&gt;asyncToGenerator&lt;/code&gt;
&lt;/h2&gt;
&lt;p&gt;如果你想让函数直接返回 Promise，可以这样封装：&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;asyncToGenerator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;generatorFunc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;gen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;generatorFunc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;arg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;gen&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="nx"&gt;arg&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;done&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;done&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;throw&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;用法：&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;asyncToGenerator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;getData&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;和原生 &lt;code&gt;async/await&lt;/code&gt; 行为完全一致。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="6. 常见误解与踩坑"&gt;6. 常见误解与踩坑&lt;/h2&gt;&lt;h3 id="6.1 await 后面跟着的不是 Promise 会怎样？"&gt;6.1 &lt;code&gt;await&lt;/code&gt; 后面跟着的不是 Promise 会怎样？&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;await 123&lt;/code&gt; 会被隐式转换为 &lt;code&gt;await Promise.resolve(123)&lt;/code&gt;，所以自动执行器里用 &lt;code&gt;Promise.resolve(value)&lt;/code&gt; 包裹是正确的。&lt;/p&gt;
&lt;h3 id="6.2 异步错误怎么捕获？"&gt;6.2 异步错误怎么捕获？&lt;/h3&gt;
&lt;p&gt;如果 generator 内部 &lt;code&gt;yield&lt;/code&gt; 了一个 rejected Promise，自动执行器会调用 &lt;code&gt;generator.throw(err)&lt;/code&gt;，然后在 try-catch 中 reject 最终的 Promise。所以外层的 &lt;code&gt;.catch&lt;/code&gt; 可以捕获。&lt;/p&gt;
&lt;h3 id="6.3 for 循环里的 await 是串行还是并行？"&gt;6.3 &lt;code&gt;for&lt;/code&gt; 循环里的 &lt;code&gt;await&lt;/code&gt; 是串行还是并行？&lt;/h3&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 串行（一个接一个）&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/api/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// 并行（同时发起）&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/api/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;理解原理后，你就知道为什么串行会慢，以及什么时候该用 &lt;code&gt;Promise.all&lt;/code&gt;。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="7. 总结"&gt;7. 总结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;async/await&lt;/code&gt; 的底层 = &lt;strong&gt;generator + 自动执行器&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;手写一个自动执行器只需 20 行左右&lt;/li&gt;
&lt;li&gt;真正理解原理后，你就能轻松避免 “异步陷阱”&lt;/li&gt;
&lt;li&gt;文中代码可以直接复制到你的项目中跑一跑&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;讨论&lt;/strong&gt;：你在项目中遇到过哪些因不理解 async/await 原理而产生的 bug？欢迎在评论区分享你的 “翻车” 经历～&lt;/p&gt;</description>
      <author>193577746</author>
      <pubDate>Mon, 08 Jun 2026 19:33:54 +0800</pubDate>
      <link>https://beta.w2solo.com/topics/7505</link>
      <guid>https://beta.w2solo.com/topics/7505</guid>
    </item>
    <item>
      <title>一个人 +Cursor，7 天上线付费小程序：第 1 天我就想放弃了</title>
      <description>&lt;p&gt;凌晨两点，我盯着微信公众平台那个红色的 “审核不通过” 通知，心里只有一个念头：&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;要不这破项目就算了吧。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;这是第一天。我原计划 3 天搞定全部，结果连一个类目选择都能卡我 4 个小时。而那个被全网吹上天的 Cursor，此刻正安安静静地躺在我的 MacBook 右侧，它什么也帮不了我。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="一、一个冲动的开始：3天上线一个付费工具？"&gt;一、一个冲动的开始：3 天上线一个付费工具？&lt;/h2&gt;
&lt;p&gt;事情的起因很简单。&lt;/p&gt;

&lt;p&gt;女朋友在厨房贴了一张 “距离房租到期还有 XX 天” 的便利贴，我随口说了句：这玩意儿微信小程序做一个不就行了，还能设个付费去广告，一个月卖几块钱。&lt;/p&gt;

&lt;p&gt;她白了我一眼：“你做啊。”&lt;/p&gt;

&lt;p&gt;男人最受不了这种激将。我当场打开 Cursor，敲下一行注释：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// 一个倒计时小程序，用户可以创建多个倒计时事件，支持付费去广告
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后按下了 &lt;code&gt;Cmd+K&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;那一刻，Cursor 就像一个被打了鸡血的实习生，哗啦啦生成了整整三个页面：首页倒计时列表、创建页、设置页。甚至还自动配了一个好看的渐变背景。我靠在椅子上，看着屏幕上飞速滚动的代码，脑子里已经在算账了：一天开发、一天审核、一天接支付，3 天上线，完美。&lt;/p&gt;

&lt;p&gt;事实证明，我天真得像个刚毕业的实习生。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="二、第一道墙：Cursor再强，也不知道“个人主体不能选这个类目”"&gt;二、第一道墙：Cursor 再强，也不知道 “个人主体不能选这个类目”&lt;/h2&gt;
&lt;p&gt;我用的个人主体注册小程序。&lt;/p&gt;

&lt;p&gt;Cursor 生成的代码里，默认跳过了类目选择这个环节——对 AI 来说，这不就是一行配置吗？它甚至贴心地帮我选了一个 “工具 - 效率” 的类目，说这个最匹配。&lt;/p&gt;

&lt;p&gt;但当我在 mp.weixin.qq.com 点下 “提交审核” 时，页面直接弹红：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;“个人主体暂不支持该类目，请选择其他类目或申请企业主体。”&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;我懵了。&lt;/p&gt;

&lt;p&gt;然后就是长达四个小时的循环：查类目表 → 换一个 → 再被拒 → 再换。我试图把需求丢给 Cursor：“微信小程序个人主体可选类目有哪些？” 它给了一串过时列表，其中一半已经不存在了。&lt;/p&gt;

&lt;p&gt;最终我手动翻完了 2026 年 5 月版的《微信小程序开放的服务类目》，在一个叫 “工具 - 记账/提醒” 的犄角旮旯里找到了唯一能挂靠的选项。&lt;strong&gt;这整个过程，Cursor 一个字都没帮上。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;我看了下时间，凌晨 1:47。原计划的第一天 “完成全部开发”，现在我连审核的入口都还没摸到。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="三、AI 10分钟写的代码，我花了一整天修bug"&gt;三、AI 10 分钟写的代码，我花了一整天修 bug&lt;/h2&gt;
&lt;p&gt;第二天我学聪明了，先把基础功能跑通。&lt;/p&gt;

&lt;p&gt;我把需求拆成小块喂给 Cursor：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;生成一个微信小程序页面，展示用户创建的倒计时列表，使用云开发数据库，支持下拉刷新
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;10 秒，代码出来了。&lt;/p&gt;

&lt;p&gt;我复制进去，编译，满屏报错。&lt;/p&gt;

&lt;p&gt;排查了半小时，发现它用了 &lt;code&gt;wx.cloud.database().collection('countdowns').get()&lt;/code&gt; 这种写法，但压根没告诉我需要先初始化云开发环境，还要在 app.json 里配 &lt;code&gt;"cloud": true&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;这就像请了一个代码写得飞快但从来不写文档的同事。你问他 “这个接口需要什么前提条件？”，他会一脸无辜地看着你：“你没问我啊。”&lt;/p&gt;

&lt;p&gt;我索性坐下来，一条一条地调：云函数怎么部署、数据库权限怎么设、openid 怎么拿、倒计时渲染时的时区问题……每一个坑都得我手动填。Cursor 负责把砖头递过来，但砌墙的人只能是我。&lt;/p&gt;

&lt;p&gt;到晚上 10 点，倒计时功能终于跑通了。创建、列表、详情、删除，一个最小闭环。代码量加起来也就 500 行，但我在 debug 上花的时间至少是写代码的 5 倍。&lt;/p&gt;

&lt;p&gt;我在 Notion 上写下一句话：&lt;strong&gt;AI 能让你 10 分钟拥有一个可运行的 demo，但要把那个 demo 变成可上线产品，剩下 90% 的时间都在处理 AI 不知道的问题。&lt;/strong&gt;&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="四、审核的毒打：AI 不知道“中国式合规”长什么样"&gt;四、审核的毒打：AI 不知道 “中国式合规” 长什么样&lt;/h2&gt;
&lt;p&gt;第三天，我信心满满地提交了第一个版本。&lt;/p&gt;

&lt;p&gt;4 小时后，驳回。&lt;/p&gt;

&lt;p&gt;理由：“小程序涉及用户自定义内容输入，需接入内容安全 API。” 附了一个文档链接，密密麻麻的接口说明。&lt;/p&gt;

&lt;p&gt;Cursor 知道这个吗？完全不知道。&lt;/p&gt;

&lt;p&gt;它甚至不知道微信内容安全接口的存在。我试着把文档喂给它：“请根据这个文档，在用户创建倒计时标题时接入内容安全检测。” 它像模像样地写了一个请求函数，但把 &lt;code&gt;mediaType&lt;/code&gt; 写成了 &lt;code&gt;contentType&lt;/code&gt;，把返回值的结构完全搞错。&lt;/p&gt;

&lt;p&gt;我放弃了让 AI 改，自己对着文档一行行手写。写完提交，又驳回。&lt;/p&gt;

&lt;p&gt;这次理由更让人崩溃：“请在小程序首页明确展示备案号及开发者信息。”&lt;/p&gt;

&lt;p&gt;我看了看 Cursor 生成的首页，漂亮的渐变背景上干干净净，只有一个标题 “我的倒计时”。它当然不会知道中国小程序需要展示备案号，这对它来说是不可理喻的需求。&lt;/p&gt;

&lt;p&gt;来来回回，一共被驳回 &lt;strong&gt;7 次&lt;/strong&gt;。理由包括但不限于：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;未接入内容安全&lt;/li&gt;
&lt;li&gt;未展示备案号&lt;/li&gt;
&lt;li&gt;登录流程不符合规范（AI 写了静默登录，但微信要求显式授权）&lt;/li&gt;
&lt;li&gt;付费功能说明不清晰&lt;/li&gt;
&lt;li&gt;客服按钮没有&lt;/li&gt;
&lt;li&gt;隐私协议链接缺失&lt;/li&gt;
&lt;li&gt;甚至有一次说 “请在服务页面增加投诉入口”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;第七次驳回通知弹出来的时候，我坐在椅子上笑了。那种笑不是开心，是 “原来做一个能上线的小程序，真正的门槛从来不在代码” 的顿悟。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="五、转折：第四天晚上，审核突然过了"&gt;五、转折：第四天晚上，审核突然过了&lt;/h2&gt;
&lt;p&gt;第四天下午，我把以上所有合规项一个一个改完。&lt;/p&gt;

&lt;p&gt;不再信任 Cursor 直接生成任何跟审核相关的代码，我索性全部手写，只让它帮我做一件事：格式化代码、补全一些重复性的 UI 组件。&lt;/p&gt;

&lt;p&gt;晚上 8 点，我点了最后一次 “提交审核”。&lt;/p&gt;

&lt;p&gt;然后开始看剧，把自己放空。&lt;/p&gt;

&lt;p&gt;11 点 23 分，手机震了一下。我瞟了一眼，微信公众平台的通知：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“你的小程序 ‘倒数日提醒’ 已通过审核。”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;我愣了两秒，然后几乎是吼着跟女朋友说：过了！&lt;/p&gt;

&lt;p&gt;她淡定地抬头：“哦，那能赚多少钱？”&lt;/p&gt;

&lt;p&gt;……这个问题问得真好。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="六、接入支付：技术不难，难的是“你敢收钱吗”"&gt;六、接入支付：技术不难，难的是 “你敢收钱吗”&lt;/h2&gt;
&lt;p&gt;审核通过后，第五天我开始接入微信支付。&lt;/p&gt;

&lt;p&gt;这次 Cursor 终于有点用了。微信支付的云函数调用流程它很熟，我丢给它一句话：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;云开发实现微信小程序支付，使用云函数统一下单，用户支付1元解锁去广告功能
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它生成了两个云函数：&lt;code&gt;createOrder&lt;/code&gt; 和 &lt;code&gt;checkPayResult&lt;/code&gt;，结构基本正确。我只需要改一下商户号、回调地址和签名逻辑。这次，AI 的效率终于体现出来了：从开写支付代码到测试完成，只用了 2 个小时。&lt;/p&gt;

&lt;p&gt;技术上的支付接入其实很快。真正让我犹豫的是心理那道坎：&lt;strong&gt;这玩意儿真有人付钱吗？&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;我设了个价格：去广告 1 元/永久。不是不想多收，是我自己都觉得一个倒计时工具收太多心虚。广告则是接了一个 Banner 广告位，不付费的用户每次打开倒计时详情都会看到底部广告。&lt;/p&gt;

&lt;p&gt;一切就绪后，我自己用另一个微信号扫码，输入密码，支付 1 元。支付成功的提示弹出来那一刻，我突然有种莫名的感动——这就是独立开发者所谓的 “闭环” 吧。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="七、第一笔真实的付费，来自一个陌生人"&gt;七、第一笔真实的付费，来自一个陌生人&lt;/h2&gt;
&lt;p&gt;上线后的第一个白天，小程序安安静静，访问人数 12。&lt;/p&gt;

&lt;p&gt;那 12 个人全是我自己拉的朋友，他们礼貌地点了点，夸了两句就走了。没有任何人付费。&lt;/p&gt;

&lt;p&gt;我有点丧，但也在意料之中。&lt;/p&gt;

&lt;p&gt;第六天晚上，我正在刷 B 站，微信支付商户助手突然弹了一条消息：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;“支付到账通知：0.99 元。”&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;扣除手续费后 0.99 元，来自一个我完全不认识的用户，昵称是一个太阳的 emoji。他创建了一个倒计时：“距离下次发工资还有 23 天”。&lt;/p&gt;

&lt;p&gt;我盯着这条通知看了很久。不是因为钱多，而是因为&lt;strong&gt;这是第一次，有陌生人愿意为我的代码付钱。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;0.99 元，四舍五入就是一块。但这一块的重量，比我拿过的大厂月薪还让人心跳加速。&lt;/p&gt;

&lt;p&gt;我立刻截图发了一个朋友圈，配文：“AI 时代，独立开发者的第一笔收入。” 评论区炸了，一半人恭喜，一半人问我怎么做到的。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="八、复盘：AI 到底帮我省了多少时间？"&gt;八、复盘：AI 到底帮我省了多少时间？&lt;/h2&gt;
&lt;p&gt;整个项目结束后，我做了一个时间统计：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;核心代码编写（倒计时逻辑、页面、交互）：如果纯手写，按我的速度大约需要 &lt;strong&gt;3 天&lt;/strong&gt;。Cursor 生成 + 我修改，实际花了 &lt;strong&gt;4 小时&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;环境配置、审核合规、支付资质：这些 Cursor 几乎帮不上忙，我花了 &lt;strong&gt;4 天&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;调试和修复 AI 生成的 bug：花了 &lt;strong&gt;1 天半&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;后期优化和 UI 微调： &lt;strong&gt;半天&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;所以，AI 确实把编码部分加速了 90% 以上。但 “做一个小程序” 需要的远不只是编码。&lt;strong&gt;合规、审核、支付资质、用户心理、定价策略——这些才是真正吃掉时间的地方，而 AI 对此一无所知。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;这也让我突然理解了，为什么很多独立开发者说 “Cursor 让我更忙了”——因为你可以一天生成三个项目，但后续的泥潭一个都绕不开。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="九、这篇文章想告诉你什么"&gt;九、这篇文章想告诉你什么&lt;/h2&gt;
&lt;p&gt;我知道很多同行刷掘金的时候，看到 “AI 10 分钟写一个小程序” 的标题会焦虑。我也焦虑过。但自己走完这 7 天，我反而平静了。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI 消灭的不是程序员，而是 “只写代码的程序员”。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;当 Cursor 可以把你的 CRUD 时间压缩到十分之一，剩下的竞争力是什么？&lt;/p&gt;

&lt;p&gt;是知道怎么跟平台审核斗智斗勇。&lt;br&gt;
是知道用户真正愿意为什么而付费。&lt;br&gt;
是知道那个看似无用的 “备案号展示” 能让你免于下架。&lt;br&gt;
是知道在什么时机把项目 push 到线上，并且在第一个 0.99 元到账时，还能像傻小子一样兴奋。&lt;/p&gt;

&lt;p&gt;这些都是经验，是判断力，是踩坑踩出来的直觉。AI 暂时还学不会。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="十、下一步：我要用 30 天验证一个更大的假设"&gt;十、下一步：我要用 30 天验证一个更大的假设&lt;/h2&gt;
&lt;p&gt;这个倒计时小程序只是一个前哨。我真正的计划是从它开始，&lt;strong&gt;一个人 + Cursor，连续 30 天上线 3 款不同形态的小程序，都设 1 元付费点，测试不同赛道的真实需求。&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;第 2 个：一个随机决定工具（中午吃啥、周末去哪），带社交裂变&lt;/li&gt;
&lt;li&gt;第 3 个：一个极简记账，只记支出，AI 自动分类&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;我会把每一个的过程、数据、收入截图全部公开在掘金。不造神，不贩卖焦虑，只记录一个真实的前端开发者，在 AI 工具大行其道的 2026 年，用最笨的方法一步一步走下去的样子。&lt;/p&gt;</description>
      <author>193577746</author>
      <pubDate>Sun, 07 Jun 2026 12:20:40 +0800</pubDate>
      <link>https://beta.w2solo.com/topics/7500</link>
      <guid>https://beta.w2solo.com/topics/7500</guid>
    </item>
    <item>
      <title>从 Webpack 到 Vite：我们迁移了一个 10 万行代码的项目，总结了这 7 个坑</title>
      <description>&lt;h2 id="一、为什么要迁？我们当时的痛点"&gt;一、为什么要迁？我们当时的痛点&lt;/h2&gt;
&lt;p&gt;项目技术栈：React 18 + TypeScript + Ant Design + less。Webpack 配置经过多人 “迭代”，已经变得极其复杂：各种 loader、plugin、alias、proxy，还有自定义的打包分析脚本。&lt;/p&gt;

&lt;p&gt;痛点：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;开发服务器启动：45 秒起步，同事可以泡杯咖啡&lt;/li&gt;
&lt;li&gt;热更新：改一行代码，等待 3 秒才刷新&lt;/li&gt;
&lt;li&gt;生产构建：6 分钟，CI 经常超时&lt;/li&gt;
&lt;li&gt;配置维护：没人敢动 webpack.config.js，一动就崩&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;我们调研了 Vite，开发服务器启动秒级、HMR 极快、配置简单。决定迁移。&lt;/p&gt;
&lt;h2 id="二、迁移过程与踩坑记录"&gt;二、迁移过程与踩坑记录&lt;/h2&gt;&lt;h3 id="坑1：环境变量不兼容"&gt;坑 1：环境变量不兼容&lt;/h3&gt;
&lt;p&gt;Webpack 用&lt;code&gt;process.env&lt;/code&gt;注入变量，Vite 用&lt;code&gt;import.meta.env&lt;/code&gt;。全局搜索替换容易遗漏，特别是第三方库中使用了&lt;code&gt;process.env&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：用&lt;code&gt;@vitejs/plugin-react&lt;/code&gt;自带的&lt;code&gt;define&lt;/code&gt;配置，手动映射：&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// vite.config.js&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;define&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;process.env&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但这样会打包所有环境变量，有安全风险。建议只映射需要的：&lt;code&gt;'process.env.API_URL': JSON.stringify(process.env.API_URL)&lt;/code&gt;。&lt;/p&gt;
&lt;h3 id="坑2：CommonJS模块不兼容"&gt;坑 2：CommonJS 模块不兼容&lt;/h3&gt;
&lt;p&gt;Vite 默认只支持 ESM，但&lt;code&gt;node_modules&lt;/code&gt;里有大量 CommonJS 模块。比如&lt;code&gt;@ant-design/charts&lt;/code&gt;、&lt;code&gt;moment&lt;/code&gt;等。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：提示&lt;code&gt;Module not found&lt;/code&gt;或&lt;code&gt;require is not defined&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;先用&lt;code&gt;vite-plugin-commonjs&lt;/code&gt;（已废弃），官方推荐&lt;code&gt;optimizeDeps.include&lt;/code&gt;：
&lt;code&gt;js
optimizeDeps: {
include: ['@ant-design/charts', 'moment']
}
&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;实在不行，在&lt;code&gt;build.rollupOptions&lt;/code&gt;中配置&lt;code&gt;@rollup/plugin-commonjs&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="坑3：less全局变量失效"&gt;坑 3：less 全局变量失效&lt;/h3&gt;
&lt;p&gt;Webpack 中我们用&lt;code&gt;less-loader&lt;/code&gt;的&lt;code&gt;modifyVars&lt;/code&gt;全局注入主题变量。Vite 不支持这种写法。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：用&lt;code&gt;vite-plugin-style-import&lt;/code&gt;或直接修改 vite 配置的 css 预处理器选项：&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;css&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;preprocessorOptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;less&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;modifyVars&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@primary-color&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#1890ff&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;javascriptEnabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但这样只对组件库有效，自己写的 less 文件还需要手动&lt;code&gt;@import&lt;/code&gt;全局变量文件。&lt;/p&gt;
&lt;h3 id="坑4：动态导入路径问题"&gt;坑 4：动态导入路径问题&lt;/h3&gt;
&lt;p&gt;代码中大量使用&lt;code&gt;import(&lt;/code&gt;@/pages/${pageName}/index&lt;code&gt;)&lt;/code&gt;动态导入。Vite 要求动态路径必须静态可分析。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：构建时提示&lt;code&gt;The requested module '...' does not provide an export named 'default'&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：改用&lt;code&gt;const modules = import.meta.glob('./pages/**/index.tsx')&lt;/code&gt;，然后手动匹配。&lt;/p&gt;
&lt;h3 id="坑5：代理重写规则不一致"&gt;坑 5：代理重写规则不一致&lt;/h3&gt;
&lt;p&gt;Webpack 的&lt;code&gt;devServer.proxy&lt;/code&gt;和 Vite 的&lt;code&gt;server.proxy&lt;/code&gt;配置方式不同，特别是路径重写和 Cookie 的&lt;code&gt;secure&lt;/code&gt;选项。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：仔细对照文档，测试所有接口。我们花了大半天才把所有代理规则调通。&lt;/p&gt;
&lt;h3 id="坑6：多页应用配置"&gt;坑 6：多页应用配置&lt;/h3&gt;
&lt;p&gt;我们的项目是多页应用（多个入口）。Webpack 用&lt;code&gt;entry: { a: './src/a.tsx', b: './src/b.tsx' }&lt;/code&gt;，Vite 原生不支持多页。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：使用&lt;code&gt;vite-plugin-html&lt;/code&gt;或手动配置&lt;code&gt;build.rollupOptions.input&lt;/code&gt;：&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;build&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;rollupOptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;__dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;src/a.html&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;__dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;src/b.html&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="坑7：生产构建后路由404"&gt;坑 7：生产构建后路由 404&lt;/h3&gt;
&lt;p&gt;Vite 构建后，HTML 中的资源路径默认是绝对路径&lt;code&gt;/assets/...&lt;/code&gt;，如果部署在子目录下会 404。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：设置&lt;code&gt;base: './'&lt;/code&gt;（相对路径）或根据部署路径动态设置。&lt;/p&gt;
&lt;h2 id="三、迁移后的收益"&gt;三、迁移后的收益&lt;/h2&gt;
&lt;p&gt;经过两周折腾，我们终于成功迁移。对比数据：&lt;/p&gt;
&lt;table class="table table-bordered table-striped"&gt;
&lt;tr&gt;
&lt;th&gt;指标&lt;/th&gt;
&lt;th&gt;Webpack&lt;/th&gt;
&lt;th&gt;Vite&lt;/th&gt;
&lt;th&gt;提升&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;开发服务器启动&lt;/td&gt;
&lt;td&gt;45 秒&lt;/td&gt;
&lt;td&gt;1.2 秒&lt;/td&gt;
&lt;td&gt;37 倍&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;热更新时间（首次）&lt;/td&gt;
&lt;td&gt;3 秒&lt;/td&gt;
&lt;td&gt;0.1 秒&lt;/td&gt;
&lt;td&gt;30 倍&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;生产构建时间&lt;/td&gt;
&lt;td&gt;6 分 20 秒&lt;/td&gt;
&lt;td&gt;1 分 50 秒&lt;/td&gt;
&lt;td&gt;3.4 倍&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;配置文件行数&lt;/td&gt;
&lt;td&gt;180 行&lt;/td&gt;
&lt;td&gt;45 行&lt;/td&gt;
&lt;td&gt;-75%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;依赖安装大小&lt;/td&gt;
&lt;td&gt;420MB&lt;/td&gt;
&lt;td&gt;380MB&lt;/td&gt;
&lt;td&gt;-10%&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;p&gt;团队开发体验大幅提升，同事不再抱怨 “等编译”。&lt;/p&gt;
&lt;h2 id="四、迁移建议"&gt;四、迁移建议&lt;/h2&gt;
&lt;p&gt;如果你也在考虑迁移，几点建议：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;评估项目复杂度&lt;/strong&gt;：如果项目大量使用 Webpack 特有插件（如&lt;code&gt;DefinePlugin&lt;/code&gt;、&lt;code&gt;ProvidePlugin&lt;/code&gt;），迁移成本高。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;先跑 demo&lt;/strong&gt;：拿一个最小模块试水，验证可行性。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;渐进迁移&lt;/strong&gt;：不用一次性全切，可以先用 Vite 开发，生产构建仍用 Webpack，逐步替换。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;备好回滚方案&lt;/strong&gt;：迁移期间保留原 Webpack 配置，出问题随时切回。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;团队培训&lt;/strong&gt;：Vite 的 HMR 机制、环境变量、动态导入与 Webpack 不同，团队要统一学习。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="五、最后"&gt;五、最后&lt;/h2&gt;
&lt;p&gt;Vite 不是银弹，但它确实解决了 Webpack 在开发体验上的痛点。如果你的项目也深受启动慢、热更新卡顿的困扰，不妨一试。&lt;/p&gt;

&lt;p&gt;你们团队在用 Vite 吗？迁移中遇到过什么坑？&lt;strong&gt;点个赞让更多需要的人看到。&lt;/strong&gt;&lt;/p&gt;</description>
      <author>193577746</author>
      <pubDate>Sat, 06 Jun 2026 19:18:25 +0800</pubDate>
      <link>https://beta.w2solo.com/topics/7499</link>
      <guid>https://beta.w2solo.com/topics/7499</guid>
    </item>
    <item>
      <title>浏览器缓存最强攻略：强缓存、协商缓存、CDN、更新策略，一篇搞定</title>
      <description>&lt;blockquote&gt;
&lt;p&gt;网站加载慢？重复请求浪费带宽？用户总是看到旧版本？这些问题背后，都指向同一个关键词：&lt;strong&gt;缓存&lt;/strong&gt;。今天我们从 HTTP 头开始，彻底搞懂强缓存、协商缓存、CDN 缓存、前端静态资源版本管理，以及最常见的缓存坑和解决方案。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="一、缓存是什么？为什么重要？"&gt;一、缓存是什么？为什么重要？&lt;/h2&gt;
&lt;p&gt;缓存是 HTTP 协议中用于&lt;strong&gt;复用之前获取的资源&lt;/strong&gt;的机制。合理使用缓存可以：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;减少网络请求，页面加载更快&lt;/li&gt;
&lt;li&gt;降低服务器压力&lt;/li&gt;
&lt;li&gt;节省用户流量&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;但缓存用不好，会导致用户看到过期内容，或者每次都重新下载，失去缓存意义。&lt;/p&gt;
&lt;h2 id="二、强缓存：服务器告诉浏览器“别问了，直接用旧的”"&gt;二、强缓存：服务器告诉浏览器 “别问了，直接用旧的”&lt;/h2&gt;
&lt;p&gt;强缓存由服务器返回的响应头中的 &lt;code&gt;Cache-Control&lt;/code&gt;（HTTP/1.1）或 &lt;code&gt;Expires&lt;/code&gt;（HTTP/1.0，已过时）控制。&lt;/p&gt;
&lt;h3 id="2.1 Cache-Control 常用指令"&gt;2.1 Cache-Control 常用指令&lt;/h3&gt;&lt;table class="table table-bordered table-striped"&gt;
&lt;tr&gt;
&lt;th&gt;指令&lt;/th&gt;
&lt;th&gt;含义&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;max-age=3600&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;资源在 3600 秒内是 “新鲜的”，直接使用缓存，不请求服务器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;no-cache&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;跳过强缓存，但可以用协商缓存（见下文）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;no-store&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;完全不缓存，每次都重新下载&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;public&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;可以被任何中间节点（CDN、代理）缓存&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;private&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;只能被浏览器缓存，不能给中间节点（如用户个人信息）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;immutable&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;资源永不变化（通常配合带 hash 的文件名使用），浏览器不必再验证&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;&lt;h3 id="2.2 强缓存流程"&gt;2.2 强缓存流程&lt;/h3&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;浏览器请求资源 → 服务器返回资源 + Cache-Control: max-age=3600
→ 浏览器缓存该资源，3600秒内再次请求直接读缓存（200 from disk cache）
→ 3600秒后重新请求服务器
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="2.3 如何绕过强缓存？"&gt;2.3 如何绕过强缓存？&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;刷新页面（F5 / Cmd+R）：浏览器会带上 &lt;code&gt;Cache-Control: max-age=0&lt;/code&gt;，告诉服务器不要用缓存，走协商缓存。&lt;/li&gt;
&lt;li&gt;硬刷新（Ctrl+F5 / Cmd+Shift+R）：浏览器带上 &lt;code&gt;Cache-Control: no-cache&lt;/code&gt;，强制重新下载。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="三、协商缓存：浏览器问服务器“资源变了吗？”"&gt;三、协商缓存：浏览器问服务器 “资源变了吗？”&lt;/h2&gt;
&lt;p&gt;当强缓存过期，或者请求头带有 &lt;code&gt;Cache-Control: no-cache&lt;/code&gt; 时，浏览器会发起协商缓存请求，询问服务器资源是否有更新。&lt;/p&gt;
&lt;h3 id="3.1 Last-Modified / If-Modified-Since"&gt;3.1 Last-Modified / If-Modified-Since&lt;/h3&gt;
&lt;p&gt;服务器返回 &lt;code&gt;Last-Modified&lt;/code&gt;（资源最后修改时间），浏览器下次请求带上 &lt;code&gt;If-Modified-Since&lt;/code&gt;。服务器比对时间，若未修改则返回 &lt;code&gt;304 Not Modified&lt;/code&gt;，浏览器使用缓存；若修改则返回 200 和新资源。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;缺点&lt;/strong&gt;：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;时间精度为秒，如果文件在 1 秒内多次修改，可能无法检测到。&lt;/li&gt;
&lt;li&gt;如果文件内容没变但时间变了（如 CI 重新生成），也会导致重新下载。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="3.2 ETag / If-None-Match（推荐）"&gt;3.2 ETag / If-None-Match（推荐）&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;ETag&lt;/code&gt; 是服务器根据文件内容生成的唯一标识（哈希值），更精确。浏览器下次请求带上 &lt;code&gt;If-None-Match: "某hash"&lt;/code&gt;。若未变则返回 304，否则返回 200 和新资源。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;优先级&lt;/strong&gt;：ETag 高于 Last-Modified。&lt;/p&gt;
&lt;h3 id="3.3 协商缓存流程"&gt;3.3 协商缓存流程&lt;/h3&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;浏览器: 我有个资源，ETag是"abc"，你变了没？
服务器: 没变，304，你继续用旧的
浏览器: （用缓存）
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="四、强缓存 vs 协商缓存对比"&gt;四、强缓存 vs 协商缓存对比&lt;/h2&gt;&lt;table class="table table-bordered table-striped"&gt;
&lt;tr&gt;
&lt;th&gt;特性&lt;/th&gt;
&lt;th&gt;强缓存&lt;/th&gt;
&lt;th&gt;协商缓存&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;发起请求&lt;/td&gt;
&lt;td&gt;不发（直接用缓存）&lt;/td&gt;
&lt;td&gt;发，但服务端不返回体&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;状态码&lt;/td&gt;
&lt;td&gt;200 (from disk/memory cache)&lt;/td&gt;
&lt;td&gt;304&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;控制头&lt;/td&gt;
&lt;td&gt;Cache-Control / Expires&lt;/td&gt;
&lt;td&gt;Last-Modified / ETag&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;适用场景&lt;/td&gt;
&lt;td&gt;不常变的静态资源（JS/CSS/图片）&lt;/td&gt;
&lt;td&gt;可能变化的 HTML、API 响应&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;&lt;h2 id="五、最佳实践：如何配置？"&gt;五、最佳实践：如何配置？&lt;/h2&gt;&lt;h3 id="5.1 静态资源（JS/CSS/图片）：强缓存 + 文件名hash"&gt;5.1 静态资源（JS/CSS/图片）：强缓存 + 文件名 hash&lt;/h3&gt;&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="p"&gt;~&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="s"&gt;.(js|css|png|jpg|jpeg|gif|ico)&lt;/span&gt;$ &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;expires&lt;/span&gt; &lt;span class="s"&gt;1y&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;Cache-Control&lt;/span&gt; &lt;span class="s"&gt;"public,&lt;/span&gt; &lt;span class="s"&gt;immutable"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：文件名必须带 hash（如 &lt;code&gt;main.a3f2b1c.js&lt;/code&gt;），内容变化时 hash 变化，浏览器才会重新下载。这是目前最主流的前端静态资源缓存策略。&lt;/p&gt;
&lt;h3 id="5.2 HTML文件：协商缓存或 no-cache"&gt;5.2 HTML 文件：协商缓存或 no-cache&lt;/h3&gt;&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="p"&gt;~&lt;/span&gt; &lt;span class="sr"&gt;\.html$&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;Cache-Control&lt;/span&gt; &lt;span class="s"&gt;"no-cache"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为 HTML 需要随时更新（入口文件），不能强缓存。&lt;/p&gt;
&lt;h3 id="5.3 API响应：按需"&gt;5.3 API 响应：按需&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;动态数据：&lt;code&gt;Cache-Control: no-cache&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;相对固定的数据（如配置）：&lt;code&gt;Cache-Control: max-age=60&lt;/code&gt;（短时缓存）&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="六、前端更新痛点与解决方案"&gt;六、前端更新痛点与解决方案&lt;/h2&gt;&lt;h3 id="问题：用户看到旧版本"&gt;问题：用户看到旧版本&lt;/h3&gt;
&lt;p&gt;常见于：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;部署新版本后，用户浏览器缓存的旧 CSS/JS 未更新，导致页面错乱。&lt;/li&gt;
&lt;li&gt;入口 HTML 被强缓存，用户没拿到新版本入口，自然不会请求新资源。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="解决方案："&gt;解决方案：&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;HTML 文件用协商缓存或短时缓存&lt;/strong&gt;（如 &lt;code&gt;max-age=60&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;静态资源用文件名 hash + 强缓存&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;通过构建工具（Webpack/Vite）自动生成带 hash 的文件名&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;部署时先上静态资源，后上 HTML&lt;/strong&gt;。确保 HTML 引用的是最新的资源 hash。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;可选项：在 HTML 中插入版本号或 query string&lt;/strong&gt;（不推荐，因为可能被代理忽略）。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="七、CDN缓存"&gt;七、CDN 缓存&lt;/h2&gt;
&lt;p&gt;CDN 是 “反向代理缓存”，缓存静态资源在全球边缘节点。配置时需注意：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;通过 &lt;code&gt;Cache-Control: public&lt;/code&gt; 允许 CDN 缓存。&lt;/li&gt;
&lt;li&gt;设置合适的 &lt;code&gt;max-age&lt;/code&gt;，太短会增加回源请求，太长会导致更新不及时。&lt;/li&gt;
&lt;li&gt;刷新 CDN 缓存通常需要手动调用 API 或版本化 URL。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="八、调试缓存"&gt;八、调试缓存&lt;/h2&gt;&lt;h3 id="Chrome DevTools："&gt;Chrome DevTools：&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Network&lt;/strong&gt; 面板：Size 列显示 &lt;code&gt;(memory cache)&lt;/code&gt;、&lt;code&gt;(disk cache)&lt;/code&gt; 或状态码 304。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Disable cache&lt;/strong&gt; 复选框：临时禁止缓存（仅限 DevTools 开启时）。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Application&lt;/strong&gt; → &lt;strong&gt;Cache Storage&lt;/strong&gt;：查看 Service Worker 缓存。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clear site data&lt;/strong&gt;：清除当前网站的缓存。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="常用命令："&gt;常用命令：&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;硬刷新：&lt;code&gt;Cmd+Shift+R&lt;/code&gt; (Mac) / &lt;code&gt;Ctrl+F5&lt;/code&gt; (Windows)&lt;/li&gt;
&lt;li&gt;清除缓存并硬刷新：打开 DevTools 后右键刷新按钮选择 “Empty Cache and Hard Reload”&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="九、常见坑点"&gt;九、常见坑点&lt;/h2&gt;&lt;table class="table table-bordered table-striped"&gt;
&lt;tr&gt;
&lt;th&gt;坑&lt;/th&gt;
&lt;th&gt;原因&lt;/th&gt;
&lt;th&gt;解决&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;上线后用户还是旧版本&lt;/td&gt;
&lt;td&gt;HTML 被强缓存&lt;/td&gt;
&lt;td&gt;HTML 用 no-cache 或短时缓存&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CSS 改了，页面样式还是旧的&lt;/td&gt;
&lt;td&gt;CSS 文件名没变，浏览器强缓存&lt;/td&gt;
&lt;td&gt;文件名加 hash&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;图片更新了，还是旧图&lt;/td&gt;
&lt;td&gt;CDN 缓存未刷新&lt;/td&gt;
&lt;td&gt;版本化 URL 或主动刷新 CDN&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;304 请求多，还是慢&lt;/td&gt;
&lt;td&gt;强缓存没开，每次都协商&lt;/td&gt;
&lt;td&gt;给静态资源加 max-age&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;&lt;h2 id="十、总结"&gt;十、总结&lt;/h2&gt;&lt;table class="table table-bordered table-striped"&gt;
&lt;tr&gt;
&lt;th&gt;场景&lt;/th&gt;
&lt;th&gt;推荐配置&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;静态资源（带 hash）&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Cache-Control: max-age=31536000, immutable&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTML 入口&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Cache-Control: no-cache&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API（不变数据）&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Cache-Control: max-age=60&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API（动态）&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Cache-Control: no-cache&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;用户头像等私有资源&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Cache-Control: private, max-age=3600&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;p&gt;缓存策略没有 “标准答案”，需要根据业务需求权衡实时性和性能。但记住：&lt;strong&gt;带 hash 的静态资源放心强缓存，入口文件绝不强缓存&lt;/strong&gt;。&lt;/p&gt;</description>
      <author>193577746</author>
      <pubDate>Fri, 05 Jun 2026 20:31:04 +0800</pubDate>
      <link>https://beta.w2solo.com/topics/7495</link>
      <guid>https://beta.w2solo.com/topics/7495</guid>
    </item>
    <item>
      <title>Charles 抓包工具从入门到实战</title>
      <description>&lt;blockquote&gt;
&lt;p&gt;你是否遇到过这些场景：线上接口返回异常但 DevTools 看不到？想把线上请求映射到本地调试？手机 App 的网络请求怎么抓？&lt;/p&gt;

&lt;p&gt;如果你和我一样，对"代理"、"抓包"、"SSL 证书"这些概念一知半解，这篇文章帮你彻底搞懂。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="Charles 是什么？"&gt;Charles 是什么？&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;一句话：Charles 是一个 HTTP/HTTPS 代理服务器，能拦截并展示你电脑/手机上所有的网络请求和响应。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;你可以把它理解成"网络请求的监控摄像头" — 所有进出你设备的流量都经过它，它全部记录下来给你看，还能让你中途修改。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="工作原理 — 看完就懂"&gt;工作原理 — 看完就懂&lt;/h2&gt;&lt;h3 id="HTTP 请求：天然透明"&gt;HTTP 请求：天然透明&lt;/h3&gt;
&lt;p&gt;HTTP 是明文传输的，Charles 作为中间人，直接就能看到所有内容：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;浏览器 ── 请求 ──▶ Charles（看到并记录）── 请求 ──▶ 服务器
浏览器 ◀── 响应 ── Charles（看到并记录）◀── 响应 ── 服务器
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;不需要任何额外配置&lt;/strong&gt;，打开 Charles 就能看到所有 HTTP 请求的完整信息。&lt;/p&gt;
&lt;h3 id="HTTPS 请求：加密隧道"&gt;HTTPS 请求：加密隧道&lt;/h3&gt;
&lt;p&gt;HTTPS = HTTP + TLS 加密。浏览器和服务器之间建立了加密隧道，Charles 虽然转发了数据，但&lt;strong&gt;只能看到"谁连了谁"&lt;/strong&gt;，看不到具体内容。&lt;/p&gt;

&lt;p&gt;这就是你在 Charles 里看到某些请求只显示 &lt;code&gt;CONNECT&lt;/code&gt; 和一个 🔒 锁图标的原因 — Charles 知道你访问了 &lt;code&gt;https://example.com&lt;/code&gt;，但不知道你请求了什么、服务器返回了什么。&lt;/p&gt;
&lt;h3 id="SSL Proxying：Charles 的"&gt;SSL Proxying：Charles 的"双面间谍"模式&lt;/h3&gt;
&lt;p&gt;为了看到 HTTPS 的内容，Charles 需要开启 &lt;strong&gt;SSL Proxying&lt;/strong&gt;，这时它会变成双面间谍：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                    ┌─────────────────────────┐
浏览器 ──TLS加密──▶ │  Charles（持有假证书）     │ ──TLS加密──▶ 真实服务器
                    │  ① 用假证书冒充服务器      │
                    │  ② 解密浏览器发来的请求     │
                    │  ③ 用真证书访问真实服务器    │
                    │  ④ 解密服务器返回的响应     │
                    │  ⑤ 再用假证书加密返回浏览器  │
                    └─────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;整个过程：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;浏览器发起 HTTPS 请求到 &lt;code&gt;https://api.example.com&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Charles 拦截，&lt;strong&gt;伪造一张 &lt;code&gt;api.example.com&lt;/code&gt; 的证书&lt;/strong&gt;返回给浏览器&lt;/li&gt;
&lt;li&gt;浏览器以为在和真正的服务器通信，把请求用"假证书"加密发给 Charles&lt;/li&gt;
&lt;li&gt;Charles 解密请求，看到明文内容并记录&lt;/li&gt;
&lt;li&gt;Charles 再用真正的证书和 &lt;code&gt;api.example.com&lt;/code&gt; 建立连接，把请求转发过去&lt;/li&gt;
&lt;li&gt;拿到真实响应后，Charles 解密、记录，再用假证书加密返回给浏览器&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;浏览器全程被蒙在鼓里。&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id="为什么需要安装 Charles 根证书？"&gt;为什么需要安装 Charles 根证书？&lt;/h3&gt;
&lt;p&gt;浏览器不是傻子。正常情况下，收到一张"假证书"会立刻报错：&lt;code&gt;NET::ERR_CERT_AUTHORITY_INVALID&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;安装 Charles 根证书的意思是：&lt;strong&gt;告诉系统"我信任 Charles 签发的所有证书"&lt;/strong&gt;。这样浏览器收到 Charles 伪造的证书时就不会报警了。&lt;/p&gt;
&lt;h3 id="*:443 通配符为什么会搞坏某些网站？"&gt;
&lt;code&gt;*:443&lt;/code&gt; 通配符为什么会搞坏某些网站？&lt;/h3&gt;
&lt;p&gt;在 SSL Proxying Settings 里配了 &lt;code&gt;*:443&lt;/code&gt; 后，Charles 会对&lt;strong&gt;所有 HTTPS 流量&lt;/strong&gt;执行中间人解密。大部分情况下没问题，但有几种例外：&lt;/p&gt;
&lt;table class="table table-bordered table-striped"&gt;
&lt;tr&gt;
&lt;th&gt;情况&lt;/th&gt;
&lt;th&gt;原因&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;证书钉扎（Certificate Pinning）&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;某些 App/网站会硬编码真实证书的指纹，发现不是原证书就拒绝连接&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;HSTS Preload&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;浏览器内置了某些域名的证书规则，不接受非官方 CA 签发的证书&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;双向认证（mTLS）&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;服务器要求客户端也出示证书，Charles 无法提供&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;系统级请求&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;macOS 推送通知、iCloud 同步等系统服务被代理后可能异常&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id="SSL Proxying 的正确配置姿势"&gt;SSL Proxying 的正确配置姿势&lt;/h2&gt;&lt;h3 id="❌ 错误做法：只配特定域名"&gt;❌ 错误做法：只配特定域名&lt;/h3&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Include: my-api.com:443
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;问题&lt;/strong&gt;：其他所有 HTTPS 请求都只能看到 CONNECT 隧道，看不到内容。抓包功能大打折扣。&lt;/p&gt;
&lt;h3 id="❌ 错误做法：*:443 通配符 + 不管 Exclude"&gt;❌ 错误做法：&lt;code&gt;*:443&lt;/code&gt; 通配符 + 不管 Exclude&lt;/h3&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Include: *:443
Exclude: （空）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;问题&lt;/strong&gt;：所有 HTTPS 流量都被解密，可能导致某些网站/App 访问异常。&lt;/p&gt;
&lt;h3 id="✅ 推荐做法：*:443 通配符 + Exclude 排除问题域名"&gt;✅ 推荐做法：&lt;code&gt;*:443&lt;/code&gt; 通配符 + Exclude 排除问题域名&lt;/h3&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Include: *:443
Exclude: problematic-site.com:443
         some-pinned-app.com:443
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;默认能看到所有 HTTPS 内容&lt;/strong&gt;，遇到哪个网站因 Charles 挂掉了，就加到 Exclude 里。这是最省心的方案。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="前端最常用的 6 个功能"&gt;前端最常用的 6 个功能&lt;/h2&gt;&lt;h3 id="1. 抓包查看请求详情"&gt;1. 抓包查看请求详情&lt;/h3&gt;
&lt;p&gt;最基础的功能。打开 Charles → 访问页面 → 左侧 Structure 列表找到目标域名 → 点击具体请求。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Request&lt;/strong&gt; 标签：请求头、请求参数、请求体&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Response&lt;/strong&gt; 标签：响应头、响应体（JSON / HTML / 图片）&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Overview&lt;/strong&gt; 标签：URL、状态码、耗时、请求大小&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;实际场景&lt;/strong&gt;：后端说"接口没问题"，你觉得前端也没问题。Charles 一抓包，发现请求参数少传了一个字段 — 破案。&lt;/p&gt;
&lt;h3 id="2. Map Remote（远程映射）⭐"&gt;2. Map Remote（远程映射）⭐&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;把线上/测试环境的请求映射到本地开发服务器&lt;/strong&gt;，这是前端最高频使用的功能。&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;线上请求：https://cdn.example.com/app/index.js
映射到：  http://127.0.0.1:3000/index.js
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src="https://files.mdnice.com/user/11765/fffd28ef-22a1-4904-bc4a-9ace41add5f3.jpg" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;配置方式&lt;/strong&gt;：Tools → Map Remote → Add&lt;/p&gt;
&lt;table class="table table-bordered table-striped"&gt;
&lt;tr&gt;
&lt;th&gt;字段&lt;/th&gt;
&lt;th&gt;From（线上）&lt;/th&gt;
&lt;th&gt;To（本地）&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Protocol&lt;/td&gt;
&lt;td&gt;https&lt;/td&gt;
&lt;td&gt;http&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Host&lt;/td&gt;
&lt;td&gt;cdn.example.com&lt;/td&gt;
&lt;td&gt;127.0.0.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Port&lt;/td&gt;
&lt;td&gt;443&lt;/td&gt;
&lt;td&gt;3000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Path&lt;/td&gt;
&lt;td&gt;/app/*&lt;/td&gt;
&lt;td&gt;/*&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;使用场景&lt;/strong&gt;：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;在线上/测试环境调试本地代码&lt;/li&gt;
&lt;li&gt;不用发布就能在真实环境验证修改&lt;/li&gt;
&lt;li&gt;搭建平台组件本地预览&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ 如果 From 是 HTTPS，必须在 SSL Proxying 的 Include 中添加该域名！否则 Map Remote 不生效。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id="3. Map Local（本地文件映射，我愿称之为：本地开发的神！！！）"&gt;3. Map Local（本地文件映射，我愿称之为：本地开发的神！！！）&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;用本地文件替换接口返回&lt;/strong&gt;，非常适合 Mock 数据。&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;请求 https://api.example.com/user/info
→ 返回本地文件 ~/mock/user-info.json
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;配置方式&lt;/strong&gt;：Tools → Map Local → Add&lt;/p&gt;

&lt;p&gt;&lt;img src="https://files.mdnice.com/user/11765/b05f8484-3895-43a2-8e95-240f97d6d866.jpg" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;使用场景&lt;/strong&gt;：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;后端接口还没开发好，前端先用 Mock 数据开发&lt;/li&gt;
&lt;li&gt;复现某个特定的接口返回场景（如错误码、空数据）&lt;/li&gt;
&lt;li&gt;不用改前端代码就能切换不同的数据&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="4. Rewrite（重写请求/响应）"&gt;4. Rewrite（重写请求/响应）&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;修改请求或响应的 Header、Body、URL 等&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;配置方式&lt;/strong&gt;：Tools → Rewrite → Add&lt;/p&gt;
&lt;table class="table table-bordered table-striped"&gt;
&lt;tr&gt;
&lt;th&gt;场景&lt;/th&gt;
&lt;th&gt;做法&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;解决跨域&lt;/td&gt;
&lt;td&gt;修改 Response Header，添加 &lt;code&gt;Access-Control-Allow-Origin: *&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;修改 Cookie&lt;/td&gt;
&lt;td&gt;修改 Request Header 的 &lt;code&gt;Cookie&lt;/code&gt; 字段&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;模拟接口超时&lt;/td&gt;
&lt;td&gt;修改 Response Status 为 &lt;code&gt;504&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;注入调试参数&lt;/td&gt;
&lt;td&gt;修改 Request URL 的 Query 参数&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;&lt;h3 id="5. Breakpoints（断点）"&gt;5. Breakpoints（断点）&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;拦截请求或响应，手动编辑后再放行&lt;/strong&gt;。类似 Debugger，但是针对网络请求。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;使用方式&lt;/strong&gt;：右键请求 → Breakpoints → 再次发送该请求时会被拦截&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;使用场景&lt;/strong&gt;：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;手动修改请求参数后再发送&lt;/li&gt;
&lt;li&gt;修改接口返回数据后再给到前端&lt;/li&gt;
&lt;li&gt;模拟各种异常场景&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="6. Throttle（限速）"&gt;6. Throttle（限速）&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;模拟弱网环境&lt;/strong&gt;，测试页面在慢网下的表现。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;配置方式&lt;/strong&gt;：Proxy → Throttle Settings → 勾选 Enable Throttling → 选择预设或自定义&lt;/p&gt;
&lt;table class="table table-bordered table-striped"&gt;
&lt;tr&gt;
&lt;th&gt;预设&lt;/th&gt;
&lt;th&gt;下载速度&lt;/th&gt;
&lt;th&gt;延迟&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3G&lt;/td&gt;
&lt;td&gt;780 Kbps&lt;/td&gt;
&lt;td&gt;200ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4G&lt;/td&gt;
&lt;td&gt;6 Mbps&lt;/td&gt;
&lt;td&gt;50ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WiFi&lt;/td&gt;
&lt;td&gt;30 Mbps&lt;/td&gt;
&lt;td&gt;2ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id="完整安装与配置教程"&gt;完整安装与配置教程&lt;/h2&gt;&lt;h3 id="第一步：安装 Charles"&gt;第一步：安装 Charles&lt;/h3&gt;
&lt;p&gt;前往 &lt;a href="https://www.charlesproxy.com/" rel="nofollow" target="_blank" title=""&gt;Charles 官网&lt;/a&gt; 下载安装。&lt;/p&gt;
&lt;h3 id="第二步：安装根证书（关键！）"&gt;第二步：安装根证书（关键！）&lt;/h3&gt;&lt;h4 id="macOS"&gt;macOS&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;菜单 &lt;strong&gt;Help → SSL Proxying → Install Charles Root Certificate&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;钥匙串访问自动打开，找到 &lt;strong&gt;Charles Proxy CA&lt;/strong&gt; 证书&lt;/li&gt;
&lt;li&gt;双击证书 → &lt;strong&gt;信任&lt;/strong&gt; → 选择 &lt;strong&gt;「始终信任」&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;关闭窗口，输入密码确认&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 id="Windows"&gt;Windows&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;菜单 &lt;strong&gt;Help → SSL Proxying → Install Charles Root Certificate&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;按提示安装到「受信任的根证书颁发机构」&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="第三步：配置 SSL Proxying"&gt;第三步：配置 SSL Proxying&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;菜单 &lt;strong&gt;Proxy → SSL Proxying Settings&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;勾选 &lt;strong&gt;Enable SSL Proxying&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Include 列表点 &lt;strong&gt;Add&lt;/strong&gt;，Host 填 &lt;code&gt;*&lt;/code&gt;，Port 填 &lt;code&gt;443&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;后续遇到问题域名加到 Exclude 列表&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="第四步（可选）：手机抓包"&gt;第四步（可选）：手机抓包&lt;/h3&gt;&lt;h4 id="iOS"&gt;iOS&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;手机和电脑连&lt;strong&gt;同一个 Wi-Fi&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;手机 设置 → Wi-Fi → 当前网络 → HTTP 代理 → 手动

&lt;ul&gt;
&lt;li&gt;服务器：电脑 IP（Charles 菜单 Help → Local IP Address 查看）&lt;/li&gt;
&lt;li&gt;端口：&lt;code&gt;8888&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;手机 Safari 打开 &lt;code&gt;chls.pro/ssl&lt;/code&gt; 下载并安装描述文件&lt;/li&gt;
&lt;li&gt;设置 → 通用 → 关于本机 → &lt;strong&gt;证书信任设置&lt;/strong&gt; → 打开 Charles 证书信任开关&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 id="Android"&gt;Android&lt;/h4&gt;
&lt;p&gt;基本步骤与 iOS 相同，但 Android 7.0+ 默认不信任用户安装的 CA 证书。解决方案：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;如果是自己的 App：在 &lt;code&gt;AndroidManifest.xml&lt;/code&gt; 中配置 &lt;code&gt;networkSecurityConfig&lt;/code&gt; 信任用户证书&lt;/li&gt;
&lt;li&gt;如果是第三方 App：需要 Root 设备将证书安装到系统目录&lt;/li&gt;
&lt;/ul&gt;

&lt;hr&gt;
&lt;h2 id="常见问题 FAQ"&gt;常见问题 FAQ&lt;/h2&gt;&lt;h3 id="Q: 为什么某些请求只显示 CONNECT 看不到内容？"&gt;Q: 为什么某些请求只显示 CONNECT 看不到内容？&lt;/h3&gt;
&lt;p&gt;这些是 HTTPS 请求，且该域名未被 SSL Proxying 解密。确认 SSL Proxying 的 Include 列表中包含该域名（或通配符 &lt;code&gt;*:443&lt;/code&gt;）。&lt;/p&gt;
&lt;h3 id="Q: 开了 Charles 后某些网站打不开？"&gt;Q: 开了 Charles 后某些网站打不开？&lt;/h3&gt;
&lt;p&gt;该网站的证书被 Charles 代理后出了问题。两种解决方式：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;把该域名加到 SSL Proxying 的 &lt;strong&gt;Exclude&lt;/strong&gt; 列表&lt;/li&gt;
&lt;li&gt;暂时关闭 SSL Proxying（菜单 Proxy → 取消勾选 SSL Proxying）&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="Q: Map Remote 配了但不生效？"&gt;Q: Map Remote 配了但不生效？&lt;/h3&gt;
&lt;p&gt;排查清单：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;☑️ Map Remote 是否启用（Enable Map Remote 要勾选）&lt;/li&gt;
&lt;li&gt;☑️ From 的协议、Host、Path 是否完全匹配实际请求&lt;/li&gt;
&lt;li&gt;☑️ 如果 From 是 HTTPS，SSL Proxying 是否包含了该域名&lt;/li&gt;
&lt;li&gt;☑️ 本地服务是否启动且端口正确&lt;/li&gt;
&lt;li&gt;☑️ Charles 是否在录制状态（顶部红色录制按钮应为激活态）&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="Q: 关了 Charles 后上不了网？"&gt;Q: 关了 Charles 后上不了网？&lt;/h3&gt;
&lt;p&gt;Charles 修改了系统代理设置但没正常还原。手动关闭：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;macOS&lt;/strong&gt;：系统设置 → 网络 → Wi-Fi → 详细信息 → 代理 → 关闭所有代理&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Windows&lt;/strong&gt;：设置 → 网络和 Internet → 代理 → 关闭手动代理&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="Q: 如何只看某个域名的请求？"&gt;Q: 如何只看某个域名的请求？&lt;/h3&gt;
&lt;p&gt;三种方式：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;左下角 &lt;strong&gt;Filter&lt;/strong&gt; 输入框输入域名关键词&lt;/li&gt;
&lt;li&gt;右键域名 → &lt;strong&gt;Focus&lt;/strong&gt; — 只高亮显示该域名&lt;/li&gt;
&lt;li&gt;菜单 Proxy → Recording Settings → Include 只录制特定域名&lt;/li&gt;
&lt;/ol&gt;

&lt;hr&gt;
&lt;h2 id="快捷键速查表"&gt;快捷键速查表&lt;/h2&gt;&lt;table class="table table-bordered table-striped"&gt;
&lt;tr&gt;
&lt;th&gt;操作&lt;/th&gt;
&lt;th&gt;macOS&lt;/th&gt;
&lt;th&gt;Windows&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;开始/停止录制&lt;/td&gt;
&lt;td&gt;&lt;code&gt;⌘ R&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Ctrl + R&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;清空会话&lt;/td&gt;
&lt;td&gt;&lt;code&gt;⌘ K&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Ctrl + K&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;重放选中请求&lt;/td&gt;
&lt;td&gt;&lt;code&gt;⌘ R&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Ctrl + R&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;搜索请求&lt;/td&gt;
&lt;td&gt;&lt;code&gt;⌘ F&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Ctrl + F&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id="总结"&gt;总结&lt;/h2&gt;&lt;table class="table table-bordered table-striped"&gt;
&lt;tr&gt;
&lt;th&gt;概念&lt;/th&gt;
&lt;th&gt;一句话理解&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Charles&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;网络请求的监控摄像头 + 编辑器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;代理&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;浏览器把请求托管给 Charles，Charles 帮忙转发&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SSL Proxying&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Charles 伪造证书做中间人，解密 HTTPS 流量&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;根证书&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;让系统信任 Charles 的"假证书"，否则浏览器会报错&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Map Remote&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;把线上请求指向本地服务器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Map Local&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;用本地文件替换接口返回&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Rewrite&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;修改请求/响应的 Header、Body&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Breakpoints&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;拦截请求/响应，手动编辑后放行&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;SSL Proxying 推荐配置&lt;/strong&gt;：Include 保留 &lt;code&gt;*:443&lt;/code&gt;，遇到问题域名加到 Exclude。&lt;/p&gt;

&lt;p&gt;掌握以上内容，日常前端开发中 90% 的抓包和代理调试需求都能搞定 🎉&lt;/p&gt;

&lt;hr&gt;

&lt;blockquote&gt;
&lt;p&gt;如果这篇文章对你有帮助，欢迎点赞收藏 ❤️&lt;/p&gt;
&lt;/blockquote&gt;</description>
      <author>193577746</author>
      <pubDate>Thu, 04 Jun 2026 12:08:51 +0800</pubDate>
      <link>https://beta.w2solo.com/topics/7493</link>
      <guid>https://beta.w2solo.com/topics/7493</guid>
    </item>
    <item>
      <title>开源｜Image Harvest v1.0.5：AI 智能标签 + Eagle 导出，设计师和开发者的图片工作流神器</title>
      <description>&lt;p&gt;大家好，我是 Image Harvest 的作者。今天发布 v1.0.5 大版本更新，主要面向&lt;strong&gt;设计师&lt;/strong&gt;和&lt;strong&gt;前端开发者&lt;/strong&gt;的图片工作流优化。&lt;/p&gt;
&lt;h4 id="🆕 核心新功能"&gt;🆕 核心新功能&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;1. AI 智能标签&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  基于 AI 视觉模型，一键为图片生成 photo / illustration / icon / ui 等描述性标签&lt;/li&gt;
&lt;li&gt;  标签跨会话持久化，可在筛选器中按标签过滤&lt;/li&gt;
&lt;li&gt;  支持批量打标：选中多张图片，一键全部标注&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src="https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/39ed800487184feb8b67a70578521c5c~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAga3lyaWV3ZW4=:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMTIzOTkwNDg0ODcxODEzNSJ9&amp;amp;rk3s=f64ab15b&amp;amp;x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&amp;amp;x-orig-expires=1781092813&amp;amp;x-orig-sign=E%2B1yXMihqcSIj0leW%2BGD5n47ems%3D" title="" alt="ai-tag.gif"&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Eagle 素材导出&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  一键将选中图片导出到 Eagle 素材管理器&lt;/li&gt;
&lt;li&gt;  AI 标签自动同步到 Eagle（打完标再导出 → Eagle 中直接带标签）&lt;/li&gt;
&lt;li&gt;  支持来源 URL、文件名等元数据传递&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src="https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/c4649aa7d3684a568f1822a96a1fb52e~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAga3lyaWV3ZW4=:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMTIzOTkwNDg0ODcxODEzNSJ9&amp;amp;rk3s=f64ab15b&amp;amp;x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&amp;amp;x-orig-expires=1781092813&amp;amp;x-orig-sign=4O6%2BEcqKEQRWPnWQjt7%2Fj4dLgDc%3D" title="" alt="export-eagle.gif"&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. 批量操作工具栏&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  底部新增：批量收藏 | 批量 AI 标签 | 批量删除&lt;/li&gt;
&lt;li&gt;  选中图片后直接操作，无需逐个点击&lt;/li&gt;
&lt;li&gt;  收藏支持 URL 去重，不会重复添加&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src="https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/9cce1849df7c4a2e9c6bf36d0100a425~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAga3lyaWV3ZW4=:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMTIzOTkwNDg0ODcxODEzNSJ9&amp;amp;rk3s=f64ab15b&amp;amp;x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&amp;amp;x-orig-expires=1781092813&amp;amp;x-orig-sign=yuUFTx2JqGvgvG4Hmzj4HYYnu6Q%3D" title="" alt="v1.0.5.jpg"&gt;&lt;/p&gt;
&lt;h4 id="🎯 适用场景"&gt;🎯 适用场景&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;UI 设计师&lt;/strong&gt;：从 Dribbble/Behance 批量抓参考图 → AI 打标 → 导出 Eagle&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;前端开发&lt;/strong&gt;：批量下载页面素材，按格式/尺寸筛选&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;电商运营&lt;/strong&gt;：竞品图片采集 + 相似检测去重&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;内容创作者&lt;/strong&gt;：博客配图批量获取&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="📦 安装"&gt;📦 安装&lt;/h4&gt;
&lt;p&gt;&lt;a href="https://chromewebstore.google.com/detail/image-harvest-download-an/iecgnjidmogebokcfnejncgnelcepffo" rel="nofollow" target="_blank" title=""&gt;Chrome Web Store&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://image-harvest.kyriewen.cn" rel="nofollow" target="_blank" title=""&gt;插件官网&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/zbw-zbw/image-harvest" rel="nofollow" target="_blank" title=""&gt;Github&lt;/a&gt;&lt;/p&gt;</description>
      <author>193577746</author>
      <pubDate>Wed, 03 Jun 2026 20:05:22 +0800</pubDate>
      <link>https://beta.w2solo.com/topics/7489</link>
      <guid>https://beta.w2solo.com/topics/7489</guid>
    </item>
    <item>
      <title>大厂面试新规：不会用 AI 编程，直接挂</title>
      <description>&lt;blockquote&gt;
&lt;p&gt;面试官笑呵呵地抛出第一题：“你们谁用过 Copilot 或者 Cursor？说说怎么用 AI 生成一个带状态管理的 React 组件？” 七个人，六个低下了头。剩下那个举手说 “我用过 ChatGPT 写代码”，面试官追问 “那你有没有自己封装过 LLM 的 API 调用？有没有做过流式输出的性能优化？”——举手的人也沉默了。全场死寂。连键盘声都没了。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="2026年的前端面试，变了"&gt;2026 年的前端面试，变了&lt;/h2&gt;
&lt;p&gt;这不是段子。这是 2026 年春招，某二线大厂前端群面的真实场景。&lt;/p&gt;

&lt;p&gt;曾几何时，我们以为背熟 ES6、刷透 LeetCode、手写 Promise、精通 Vue 和 React 的源码，就能稳拿 offer。现在呢？面试官开口闭口就是：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;“你如何在浏览器端调用大模型 API？token 消耗怎么优化？”&lt;/li&gt;
&lt;li&gt;“前端怎么实现 RAG 的文档检索？向量数据库你用过哪个？”&lt;/li&gt;
&lt;li&gt;“有没有用 Web Worker 跑过轻量级模型？ONNX Runtime 了解吗？”&lt;/li&gt;
&lt;li&gt;“AI 辅助编程工具用得多吗？你怎么保证生成代码的质量和安全？”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;每一个问题，都像一把刀，精准地扎在普通前端的知识盲区上。&lt;/p&gt;

&lt;p&gt;更狠的是，面试官自己也承认：“其实这些题，我们内部也没几个人能完全答上来。但老板说了，今年招人必须要有 AI 素养——不会用 AI 写代码、不懂 AI 产品落地的，一律不要。”&lt;/p&gt;
&lt;h2 id="我身边真实的“AI面试惨案”"&gt;我身边真实的 “AI 面试惨案”&lt;/h2&gt;
&lt;p&gt;我认识一个 28 岁的帅小伙，三年前端经验，项目写得漂漂亮亮。今年跳槽，面了 7 家公司，5 家挂在 AI 题上。有一家技术负责人很直接：“你业务能力没问题，但我们希望前端能主动用 AI 提效，甚至参与到 AI 产品的落地里。你不懂这些，来了也是拖后腿。”&lt;/p&gt;

&lt;p&gt;拖后腿。这三个字，像一记耳光。&lt;/p&gt;

&lt;p&gt;他还算年轻的。那些 35+、拖家带口、学新东西明显吃力的老前端呢？他们连面试机会都拿不到——简历上没写 “AI 相关经验”，HR 那一关就直接筛掉了。&lt;/p&gt;

&lt;p&gt;群面七人沉默的那个场景，我反复想了很多遍。如果我是那七个中的一个，我会不会也低下头？&lt;/p&gt;
&lt;h2 id="面试官到底在考什么？"&gt;面试官到底在考什么？&lt;/h2&gt;
&lt;p&gt;2026 年的前端面试逻辑已经完全变了。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;以前考你 “能不能写”&lt;/strong&gt;——手撕算法、手写框架、默写八股文。现在&lt;strong&gt;考你 “会不会管”&lt;/strong&gt;——拆解需求、指挥 AI、审查产出、做技术决策。&lt;/p&gt;

&lt;p&gt;阿里 2026 暑期 AI 实习岗明确要求 “AI 编程工具（如 Cursor、Claude Code）的极致依赖”，以及 “工程化落地能力” 在招聘中的核心地位。警示纯理论研究者及传统编码者的风险——简历里若缺乏可展示的项目级 AI 实践经验，“在简历筛选阶段极易被直接淘汰”。&lt;/p&gt;

&lt;p&gt;字节 2026 年春招的 “测试开发工程师 - 开发者 AI” 岗位，直接硬性要求对 AI Agent 有深入理解和实践经验。阿里 “通义实验室 - 技术专家 - 测试开发” 也明确要求 “熟练掌握机器学习算法原理”。&lt;/p&gt;

&lt;p&gt;不只是在 JD 上喊口号，面试里已经在真刀真枪地考了。2026 年大厂面试题汇总里，“AI 辅助编程” 已经作为独立的面试新趋势被列出来。&lt;/p&gt;
&lt;h2 id="但AI面试官自己也很“虚”"&gt;但 AI 面试官自己也很 “虚”&lt;/h2&gt;
&lt;p&gt;讽刺的是，AI 面试官自己也在被吐槽。有面试官坦言：“其实这些 AI 题，我们内部也没几个人能完全答上来。但老板说了，今年招人必须要有 AI 素养——不会用 AI 写代码、不懂 AI 产品落地的，一律不要。”&lt;/p&gt;

&lt;p&gt;技术圈对这种新规也争议很大。一部分人认为这是与时俱进——84% 的全球开发者已经使用或计划使用 AI 编程工具，企业招人当然要优先考虑会用 AI 的人。另一部分人则批判这是 “跟风式焦虑”——面试官自己都没搞明白，凭什么要求候选人懂？&lt;/p&gt;

&lt;p&gt;一位资深面试官在反思文章里写：“当一个人的 ‘知识库’ 和 ‘经验’ 都可以被 AI 低成本伪造时，我们曾经引以为傲的面试体系，正在面临系统性失效的风险。AI 时代，工程师最稀缺的能力，恰恰是那些非技术的东西。”&lt;/p&gt;
&lt;h2 id="话说回来，AI不是来取代你的"&gt;话说回来，AI 不是来取代你的&lt;/h2&gt;
&lt;p&gt;现在很多人对 “懂 AI” 的理解极其肤浅。以为在项目里接个 OpenAI 或者 Claude 的接口，搞个对话框，把输入框的字传过去，把返回的字用 Markdown 渲染出来，就叫 AI 前端工程师了。那不叫 AI 开发，那叫表单提交。&lt;/p&gt;

&lt;p&gt;大模型时代，前端真正的难点根本不是发送请求，而是应对大模型带来的复杂性——不可控的输出、流式传输的残缺 JSON、工具调用的中间状态、安全风险的审查。&lt;/p&gt;

&lt;p&gt;我身边有一个靠 AI 面试成功的人。他拿到的 offer，面试题不是手写代码，而是 “带着电脑，现场用 Cursor 完成一个功能”。他不仅生成了代码，还在面试官面前改了需求、调优了性能、排查了 AI 生成的 bug。面试官当场说：“这就是我们要的人。”&lt;/p&gt;
&lt;h2 id="如果你现在还在找工作，几点不成熟的建议"&gt;如果你现在还在找工作，几点不成熟的建议&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;至少准备 2-3 个 “AI+ 前端” 的实战故事&lt;/strong&gt;——不能只说 “我用过 Cursor”，要说清楚在什么场景下、怎么拆解需求、怎么给 AI 指令、AI 犯了什么错、你怎么纠正的。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;练代码审查比练写代码更重要&lt;/strong&gt;——AI 能写出 80% 对的代码，但那 20% 的错误往往是最致命的。你的价值不在于写得比 AI 快，而在于能发现 AI 发现不了的问题。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;别只会调用 API&lt;/strong&gt;——真实的高阶 AI 前端工程，每天面对的是流式渲染、复杂状态机、AST 解析、沙盒隔离。把这些底子打好，比背一百个 Prompt 模板有用。&lt;/p&gt;
&lt;h2 id="写在最后"&gt;写在最后&lt;/h2&gt;
&lt;p&gt;2026 年的前端面试，AI 早已不是 “加分项”，而是像 HTML、CSS 一样的基础 “必考项”。这背后折射出的，是整个行业环境的剧变——AI 已经渗透到了业务的每一个角落。&lt;/p&gt;

&lt;p&gt;你以为面试官在考 AI，其实他们在考你——在工具越来越强的时代，你还有多少不可替代的东西。&lt;/p&gt;</description>
      <author>193577746</author>
      <pubDate>Tue, 02 Jun 2026 18:40:40 +0800</pubDate>
      <link>https://beta.w2solo.com/topics/7480</link>
      <guid>https://beta.w2solo.com/topics/7480</guid>
    </item>
    <item>
      <title>AI 生成代码快如闪电，但我修了三个小时——它到底帮了谁？</title>
      <description>&lt;blockquote&gt;
&lt;p&gt;上周，老板丢过来一个内部后台页面的需求，说 “不急，明天给就行”。我打开 v0，输入 “帮我生成一个用户管理后台，包含表格、筛选、分页、编辑弹窗”。一分钟，页面出来了。表格、按钮、弹窗、样式，一应俱全。我复制代码到项目里，跑起来，看起来没问题。然后接下来三个小时，我都在改它的代码。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="一、“神速”生成，然后呢？"&gt;一、“神速” 生成，然后呢？&lt;/h2&gt;
&lt;p&gt;v0 生成的速度确实快。但当我开始真正 “使用” 这份代码时，问题一个接一个冒出来：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;变量名随心所欲&lt;/strong&gt;：&lt;code&gt;data&lt;/code&gt;、&lt;code&gt;items&lt;/code&gt;、&lt;code&gt;itemData&lt;/code&gt;、&lt;code&gt;filteredData&lt;/code&gt;……一个表格数据，四五个名字来回用。改一个地方，全局搜索半天。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;样式写死&lt;/strong&gt;：按钮宽度固定 &lt;code&gt;120px&lt;/code&gt;，手机上看溢出；表格列宽固定，屏幕一缩就横向滚动。没有响应式，没有移动端适配。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;状态管理混乱&lt;/strong&gt;：筛选条件、分页、弹窗开关全部挤在同一个 &lt;code&gt;useState&lt;/code&gt; 里，改一个导致别的组件无辜重渲染。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;没有错误处理&lt;/strong&gt;：接口返回 &lt;code&gt;null&lt;/code&gt;，页面直接白屏。没有 loading，没有空状态。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这些不是 bug，是 “能用但没法维护”。&lt;/p&gt;
&lt;h2 id="二、修了三个小时，改了啥？"&gt;二、修了三个小时，改了啥？&lt;/h2&gt;
&lt;p&gt;我花时间做的事，恰恰是 AI 最不擅长的事：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;重命名所有变量，让代码能读。&lt;/li&gt;
&lt;li&gt;把固定宽度改成 flex + 百分比，加媒体查询。&lt;/li&gt;
&lt;li&gt;拆分状态，用 &lt;code&gt;useReducer&lt;/code&gt; 管理筛选和分页。&lt;/li&gt;
&lt;li&gt;补上 loading、错误提示、空数据占位。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;说实话，到最后我有点怀疑：如果我自己从头写，可能也就三个半小时。AI 帮我省了半小时，但我额外付出了 “读懂它逻辑” 的成本。这笔账，算不过来。&lt;/p&gt;
&lt;h2 id="三、AI 到底帮了谁？"&gt;三、AI 到底帮了谁？&lt;/h2&gt;
&lt;p&gt;我知道，会有人说 “你不会写 prompt”“你不会调教”。也许更精确的 prompt 能生成更好的代码。但问题是，&lt;strong&gt;AI 生成的代码像一辆组装好的车，看起来能跑，但一上路就发现螺丝没拧紧，轮胎是歪的&lt;/strong&gt;。它不是不能开，而是你需要先花时间检查所有零件。&lt;/p&gt;

&lt;p&gt;对于不熟悉项目上下文、不知道团队规范、不了解业务细节的 AI 来说，生成 “能跑” 的代码已经是极限。而 “可维护” 的代码，恰恰需要这些信息。&lt;/p&gt;

&lt;p&gt;所以，AI 到底帮了谁？帮我省了半小时打字时间？还是帮老板更快地看到 “可视的进度”？对于开发者自己，短期 “快” 的背后，是长期的 “改”。&lt;/p&gt;
&lt;h2 id="四、我现在怎么用 AI？"&gt;四、我现在怎么用 AI？&lt;/h2&gt;
&lt;p&gt;我没有放弃 AI，而是调整了用法：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;不用 AI 写核心业务逻辑&lt;/strong&gt;：自己设计状态和接口，用 AI 生成工具函数或数据 mock。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;用 AI 写文档和注释&lt;/strong&gt;：生成 JSDoc、README、测试用例，这些不会坑人。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI 生成代码后，强制做一轮重构&lt;/strong&gt;：重命名、拆分、补异常，把 “能跑” 变成 “能维护”。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这样一来，AI 节省的时间，不会在改代码时加倍吐回去。&lt;/p&gt;
&lt;h2 id="五、最后"&gt;五、最后&lt;/h2&gt;
&lt;p&gt;AI 写代码快，但不是免费的。你省下的打字时间，很可能变成修代码的时间。这笔账，建议每个团队都算清楚。&lt;/p&gt;</description>
      <author>193577746</author>
      <pubDate>Mon, 01 Jun 2026 18:59:38 +0800</pubDate>
      <link>https://beta.w2solo.com/topics/7473</link>
      <guid>https://beta.w2solo.com/topics/7473</guid>
    </item>
    <item>
      <title>微软用 Go 重写 TypeScript 编译器，速度提升 10 倍，网友：这是 “背叛” 还是 “救赎”？</title>
      <description>&lt;blockquote&gt;
&lt;p&gt;4 月 22 日，微软宣布 TypeScript 7.0 Beta 正式发布。这一次，TypeScript 团队干了一件让整个 JS 社区炸锅的事——他们把编译器和工具栈，&lt;strong&gt;从 TypeScript/JavaScript 底层完全移植到了 Go&lt;/strong&gt;。速度通常比 6.0 快约 10 倍。编译 VS Code 代码库从 78 秒缩到 7.5 秒。消息一出，有人叫好，有人愤怒，更多人在问：TypeScript 这是 “背叛” JavaScript 了吗？&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;img src="https://img.way2solo.com/photo/193577746/c0b4ddf1-14e0-4129-80b4-f7a19e560bef.png?imageView2/2/w/1920/q/100" title="" alt=""&gt;&lt;/p&gt;
&lt;h2 id="一、发生了什么？"&gt;一、发生了什么？&lt;/h2&gt;
&lt;p&gt;年初，微软官宣了一个叫 &lt;strong&gt;“Project Corsa”&lt;/strong&gt; 的计划：用 Go 语言，重写整个 TypeScript 编译器。不是 TypeScript，不是 Rust，是 Go。重写之后，编译速度预计提升&lt;strong&gt;10 倍&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;4 月 22 日，微软正式发布 TypeScript 7.0 Beta。在过去的近一年中，微软开发者将现有的 TypeScript 代码库从 TypeScript（作为编译成 JavaScript 的引导代码库）移植到了 Go。通过结合原生代码速度和共享内存并行性，TypeScript 7.0 的速度通常比 TypeScript 6.0 快约 10 倍。&lt;/p&gt;
&lt;h2 id="二、为什么有人“吵翻了”？"&gt;二、为什么有人 “吵翻了”？&lt;/h2&gt;
&lt;p&gt;消息一出，社区立刻分裂。&lt;strong&gt;支持派&lt;/strong&gt;说：“终于！大型项目编译几分钟的时代，终于要结束了。”&lt;strong&gt;反对派&lt;/strong&gt;说：“TypeScript 自己都是用 TypeScript 写的，这是灵魂。用 Go 重写？这是 ‘背叛’。”&lt;/p&gt;

&lt;p&gt;最大的争议点：&lt;strong&gt;为什么选 Go，而不是 Rust？&lt;/strong&gt; 这是问得最多的问题。微软给出的解释包括：Go 的编程风格与现有 TypeScript 代码库高度相似，移植更容易；&lt;code&gt;goroutine&lt;/code&gt;原生支持并行类型检查；垃圾回收针对这种超大量小对象的编译器场景优化得很好；TypeScript 团队已有 Go 经验。&lt;/p&gt;
&lt;h2 id="三、一个细节：为啥TypeScript 6.0比7.0更早发布？"&gt;三、一个细节：为啥 TypeScript 6.0 比 7.0 更早发布？&lt;/h2&gt;
&lt;p&gt;微软实际上是在 TypeScript 6.0 发布前就启动了 Go 重写计划。他们把 6.0 定位为 “基于 JS 编译器的最后一个版本”，所以 7.0 才是真正的 Go 重写版。&lt;/p&gt;
&lt;h2 id="四、实际体验：真的能快10倍吗？"&gt;四、实际体验：真的能快 10 倍吗？&lt;/h2&gt;
&lt;p&gt;微软自家的 VS Code 代码库约 150 万行 TypeScript，旧编译器编译约 78 秒，新&lt;code&gt;tsgo&lt;/code&gt;只用了 7.5 秒，&lt;strong&gt;快 10.4 倍&lt;/strong&gt;。Playwright 和 TypeORM 等项目也报告了约 10-13 倍的性能提升。&lt;/p&gt;

&lt;p&gt;在编辑器方面，导入补全、快速跳转、查找所有引用的响应时间也大幅缩短，内存使用量约为旧版的一半。&lt;/p&gt;
&lt;h2 id="五、兼容性：需要改代码吗？"&gt;五、兼容性：需要改代码吗？&lt;/h2&gt;
&lt;p&gt;官方强调新的 Go 代码库是从现有实现中系统性移植而来，类型检查逻辑与 TypeScript 6.0 完全一致，不需要为新编译器重构代码。新&lt;code&gt;tsgo&lt;/code&gt;命令行工具与旧&lt;code&gt;tsc&lt;/code&gt;参数行为完全兼容。&lt;/p&gt;
&lt;h2 id="六、试用：今天就能用"&gt;六、试用：今天就能用&lt;/h2&gt;
&lt;p&gt;目前通过&lt;code&gt;@typescript/native-preview&lt;/code&gt;包名安装，命令行入口是&lt;code&gt;tsgo&lt;/code&gt;。官方表示稳定的程序化 API 预计要到 7.1 版本。&lt;/p&gt;
&lt;h2 id="七、最后"&gt;七、最后&lt;/h2&gt;
&lt;p&gt;TypeScript 团队用 Go 重写编译器，让大型项目编译从 “喝杯咖啡” 变成 “眨个眼”。有人解读为 “背叛”，但也许更像一次务实的 “脱胎换骨”。&lt;/p&gt;

&lt;p&gt;你愿意现在就装个 Beta 版试试吗？10 倍速，值得一试。&lt;/p&gt;
&lt;h2 id="八、代码块：立刻体验"&gt;八、代码块：立刻体验&lt;/h2&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 在现有项目中安装 TS 7.0 Beta&lt;/span&gt;
npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-D&lt;/span&gt; @typescript/native-preview@beta

&lt;span class="c"&gt;# 使用新编译器检查类型（体验10倍速度）&lt;/span&gt;
npx tsgo &lt;span class="nt"&gt;--noEmit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;你会为了 10 倍速尝鲜 Beta 版吗？评论区聊聊你的选择。&lt;strong&gt;点个赞让我看到有多少人已经换上了&lt;/strong&gt;。&lt;/p&gt;</description>
      <author>193577746</author>
      <pubDate>Sun, 31 May 2026 23:10:01 +0800</pubDate>
      <link>https://beta.w2solo.com/topics/7466</link>
      <guid>https://beta.w2solo.com/topics/7466</guid>
    </item>
    <item>
      <title>面试 8 家前端岗位后，我发现了一个残酷的事实：AI 不是加分项，是门槛</title>
      <description>&lt;blockquote&gt;
&lt;p&gt;上个月，我集中面试了 8 家公司的前端岗位，有大厂、有中厂、也有创业公司。投简历、刷题、背八股、跑现场……一套流程走下来，最让我意外的不是算法题的难度，而是——&lt;strong&gt;每一家都问了同一个问题：“你平时用 AI 编程工具吗？”&lt;/strong&gt; 不是随口一问，是认真地问：用的什么？怎么用的？有没有具体的提效案例？有一家甚至让我现场打开 Cursor，写一个组件。我突然意识到：AI 已经不是加分项了，它正在变成门槛。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="一、面试官的“新标配问题”"&gt;一、面试官的 “新标配问题”&lt;/h2&gt;
&lt;p&gt;8 场面试，所有面试官都问了 AI 工具相关的问题。我把它们分成三类：&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;第一类：基础使用题&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;“你平时用 Copilot 还是 Cursor？”&lt;/li&gt;
&lt;li&gt;“大概用多久了？感觉怎么样？”
这种问题比较温和，像在确认你 “跟上了时代”。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;第二类：场景应用题&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;“你用 AI 做过 Code Review 吗？效果如何？”&lt;/li&gt;
&lt;li&gt;“有没有遇到 AI 生成的代码有坑的情况？”
这类问题已经开始考察你的实际经验和判断力。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;第三类：现场实操题&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;“打开你的编辑器，用 AI 写一个带防抖的搜索组件。”&lt;/li&gt;
&lt;li&gt;“这段 AI 生成的代码有明显 bug，你能找出来吗？”
这类问题最狠。面试官不是在考你背不背得出代码，而是考你会不会审查和优化 AI 写的代码。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="二、一位面试官的原话"&gt;二、一位面试官的原话&lt;/h2&gt;
&lt;p&gt;有一场面试，面试官看了我的简历后说：“我们团队现在重度依赖 Cursor，写代码的效率确实高。但我们也发现，AI 写的代码容易出边界问题、性能隐患。我们要的不是只会敲回车的人，而是能看懂 AI 代码、能发现问题的人。”&lt;/p&gt;

&lt;p&gt;他说：“你面试通过后，入职第一周的任务就是熟悉我们的 AI 辅助开发规范——哪些场景可以用 AI，哪些必须手写，怎么 review AI 生成的 PR。”&lt;/p&gt;
&lt;h2 id="三、我的感受：不是恐慌，是压力"&gt;三、我的感受：不是恐慌，是压力&lt;/h2&gt;
&lt;p&gt;说实话，我并不恐慌。我会用 AI，也知道怎么用好它。但压力是真实的：这意味着我要额外花时间研究 AI 编程工具的最佳实践、学习如何引导 AI 生成高质量代码、培养自己 review AI 代码的能力。以前面试只要会算法、会框架、会系统设计。现在还要再加一门课：&lt;strong&gt;AI 辅助开发实战&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;而且这个领域变化很快。去年还在问 “你听说过 Copilot 吗”，今年已经问 “你用 AI 重构过屎山吗”。明年会问什么？我不敢想。&lt;/p&gt;
&lt;h2 id="四、给同行的建议（也是给自己的）"&gt;四、给同行的建议（也是给自己的）&lt;/h2&gt;
&lt;p&gt;如果你还没有系统性地使用 AI 编程工具，建议尽快补上。可以从这几方面入手：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;选择一个主力工具&lt;/strong&gt;：Cursor 或 Copilot，至少用熟一个。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;学习写 Prompt&lt;/strong&gt;：不要只让 AI 生成 “一个函数”，要给出清晰的输入输出、边界条件、性能要求。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;重视 review 能力&lt;/strong&gt;：遇到 AI 生成的代码，多问自己 “这段代码哪里可能出问题”。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;积累失败案例&lt;/strong&gt;：面试时讲一个 “AI 生成的 bug 被你发现并修复” 的故事，比单纯说我用了 AI 更有说服力。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="五、最后"&gt;五、最后&lt;/h2&gt;
&lt;p&gt;这不是贩卖焦虑，而是描述事实：2026 年的前端面试，AI 工具使用能力已经成为一道必答题。&lt;/p&gt;</description>
      <author>193577746</author>
      <pubDate>Sat, 30 May 2026 15:01:41 +0800</pubDate>
      <link>https://beta.w2solo.com/topics/7454</link>
      <guid>https://beta.w2solo.com/topics/7454</guid>
    </item>
    <item>
      <title>14MB VS 15KB：前 React 核心成员用 AI 写了个排版库，让 Safari 快了一千倍</title>
      <description>&lt;blockquote&gt;
&lt;p&gt;前 React 核心成员 Cheng Lou 在推特上甩了一个链接：“我写了一个 15KB 的排版引擎，叫 Pretext。” 配图是一段文字绕着不规则形状流动的视频。评论区炸了：“这效果以前只能用 canvas 画，性能还稀烂。”“怎么做到的？” 第二天，项目 GitHub 星数破万。第三天，破两万八。一周内，Hacker News、Reddit、Twitter 全在讨论。一个 15KB 的库，怎么就让整个前端圈沸腾了？&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;img src="https://img.way2solo.com/photo/193577746/2ebcd3b8-9228-4e4f-8fb5-e6e05ff8ded7.jpg?imageView2/2/w/1920/q/100" title="" alt=""&gt;&lt;/p&gt;
&lt;h2 id="一、故事的起点：前React核心成员的“业余项目”"&gt;一、故事的起点：前 React 核心成员的 “业余项目”&lt;/h2&gt;
&lt;p&gt;Cheng Lou，这个名字在 React 早期社区很响亮。他贡献过核心代码，也做过著名的 “时间旅行” 演讲。后来他淡出了主流视野。大家都以为他在享受生活。结果他憋出一个大招：一个 15KB 的排版引擎。&lt;/p&gt;

&lt;p&gt;为什么这件事让社区这么兴奋？因为排版——文字如何换行、环绕、对齐——是浏览器三十年来的 “硬骨头”。你滚动页面、调整窗口大小、打开一个复杂布局的网页，背后都在进行昂贵的排版计算。浏览器用的方法是 “回流”：把文字放进 DOM 里，问浏览器 “你的宽度是多少？”，浏览器只好重新计算一遍布局，整个过程同步、阻塞、性能差。&lt;/p&gt;
&lt;h2 id="二、传统排版为什么慢？"&gt;二、传统排版为什么慢？&lt;/h2&gt;
&lt;p&gt;简单说，浏览器排版是一个 “黑盒”。你给它文字和宽度，它给你一个 “换行位置”。但这个计算过程是同步的，涉及到字形度量、上下文、连字等复杂因素。而且，每次你改变文字内容或容器大小，浏览器都要重新算一遍。这就是为什么复杂排版会卡。&lt;/p&gt;
&lt;h2 id="三、Pretext的思路：绕过DOM，用数学计算"&gt;三、Pretext 的思路：绕过 DOM，用数学计算&lt;/h2&gt;
&lt;p&gt;Pretext 的核心思想很简单：&lt;strong&gt;不在 DOM 里排版，在 Canvas 里算&lt;/strong&gt;。它利用 Canvas API 直接测量文字宽度（&lt;code&gt;measureText&lt;/code&gt;），然后用算法自己决定在哪里换行、在哪里对齐。这个过程不触发回流，不阻塞主线程。而且，它把计算结果缓存起来，相同的文字和容器宽度直接复用。&lt;/p&gt;

&lt;p&gt;最惊人的是性能对比。Cheng Lou 在发布会上演示：同样的复杂排版，传统方法在 Safari 下耗时约 150ms，Pretext 只要 0.12ms，&lt;strong&gt;快了一千两百多倍&lt;/strong&gt;。其他浏览器也快了数十到上百倍。&lt;/p&gt;
&lt;h2 id="四、AI的角色：不是写代码，是“调参”"&gt;四、AI 的角色：不是写代码，是 “调参”&lt;/h2&gt;
&lt;p&gt;Pretext 的代码量不大，大部分逻辑是 Cheng Lou 自己写的。但他用 AI 做了一件更有趣的事：浏览器兼容性调试。不同操作系统、不同浏览器、不同字体下，&lt;code&gt;measureText&lt;/code&gt;的返回值有微小差异。Cheng Lou 写了一个脚本，让 AI 自动在 Chrome、Safari、Firefox、Edge 的 Windows、macOS、Linux 版本上运行测试，收集了 7680 组数据，然后让 AI 分析差异并生成修正系数。他说：“如果没有 AI，这个项目可能要再花半年时间调参数。”&lt;/p&gt;
&lt;h2 id="五、社区反应：从“这能干啥”到“我也可以”"&gt;五、社区反应：从 “这能干啥” 到 “我也可以”&lt;/h2&gt;
&lt;p&gt;Pretext 发布后，社区很快分成了两派。质疑派说：“这只是个 demo，不能用在实际项目。” 支持派则开始疯狂创作：有人用它实现文字围绕任意形状（手绘的曲线、SVG 路径）；有人做了多栏杂志布局，滚动丝滑；还有人把 Pretext 嵌入到实时聊天室，每一条新消息都能动态重排，不掉帧。&lt;/p&gt;

&lt;p&gt;最有趣的是一个开发者用它实现了一个 “流动的诗歌” 页面：文字像溪水一样沿着屏幕上的波浪线流动，鼠标拖动改变形状，文字实时重新排版。这在以前几乎不可想象。&lt;/p&gt;
&lt;h2 id="六、对前端的意义：创新不只AI"&gt;六、对前端的意义：创新不只 AI&lt;/h2&gt;
&lt;p&gt;Pretext 的爆火，折射出 2026 年前端圈的一种情绪：大家有点腻了框架大战、AI 生成代码、效率工具。当 Cheng Lou 拿出一个 15KB、无依赖、硬核性能优化的库时，大家发现：前端还能这么玩。它不依赖 React、Vue、Svelte，不依赖 AI 生成页面，就是对底层能力的一次极致挖掘。&lt;/p&gt;

&lt;p&gt;这也提醒我们：在 AI 能写出 80 分代码的时代，人类工程师的竞争力在哪里？可能是定义问题、设计算法、解决那些 AI 搞不定的极端情况。Cheng Lou 没被 AI 取代，反而用 AI 加速了自己独特想法的实现。&lt;/p&gt;
&lt;h2 id="七、最后：你会用它吗？"&gt;七、最后：你会用它吗？&lt;/h2&gt;
&lt;p&gt;Pretext 目前还不是生产级的方案，缺少滚动虚拟化、无障碍支持等。但它的思路已经启发了一批开发者。有人开始尝试把它集成到 Next.js 里，有人写了一个基于 Pretext 的 Markdown 渲染器。Cheng Lou 本人也表示，会持续维护，目标是成为一个真正可用的排版引擎。&lt;/p&gt;

&lt;p&gt;至少，它让我们看到：浏览器还没被挖透，前端还有创造的空间。&lt;/p&gt;</description>
      <author>193577746</author>
      <pubDate>Fri, 29 May 2026 12:14:19 +0800</pubDate>
      <link>https://beta.w2solo.com/topics/7447</link>
      <guid>https://beta.w2solo.com/topics/7447</guid>
    </item>
    <item>
      <title>手写虚拟 DOM 后，我反问面试官：key 为什么不能用 index？</title>
      <description>&lt;p&gt;&lt;img src="https://img.way2solo.com/photo/193577746/0bfd06d4-0dca-443f-ab97-3bdff21b9d3e.png?imageView2/2/w/1920/q/100" title="" alt=""&gt;&lt;/p&gt;
&lt;h2 id="前言"&gt;前言&lt;/h2&gt;
&lt;p&gt;虚拟 DOM 和 diff 算法是 React 面试的 “进阶题”，一般不会让手写完整实现，但一旦遇到，就是区分 “会用 React” 和 “懂 React” 的分水岭。大部分前端能说出虚拟 DOM 的好处，但真要写一个 mini 版，很多人会卡在 diff 的 key 逻辑上。&lt;/p&gt;

&lt;p&gt;今天我就还原那次面试：AI 生成的虚拟 DOM 核心代码、我是如何解释 diff 的、以及为什么 “key 不能用 index” 这个问题能让我反客为主。最后附完整代码，你可以直接拿去跑，也可以用来准备面试。&lt;/p&gt;
&lt;h2 id="一、AI生成的虚拟DOM核心代码"&gt;一、AI 生成的虚拟 DOM 核心代码&lt;/h2&gt;
&lt;p&gt;我在 Cursor 里输入：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;用原生 JavaScript 实现一个简易虚拟 DOM 库，包含：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;h(type, props, ...children)&lt;/code&gt; 创建虚拟节点&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;render(vnode)&lt;/code&gt; 将虚拟节点转为真实 DOM&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;patch(oldVnode, newVnode)&lt;/code&gt; 对比并更新真实 DOM，支持 key 属性，实现最小化更新&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;AI 输出的核心结构如下（精简后）：&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 创建虚拟节点&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;props&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="na"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;flat&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// 渲染虚拟DOM到真实DOM&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;vnode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;vnode&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;createTextNode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;vnode&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;vnode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;vnode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;setAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;vnode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;vnode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;child&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;child&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// 简易diff（带key优化）&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;patch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;oldVnode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;newVnode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;parent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;oldVnode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parentNode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// 如果是文本节点&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;oldVnode&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;newVnode&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;oldVnode&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;newVnode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;replaceChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newVnode&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;oldVnode&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="c1"&gt;// 不同类型，直接替换&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;oldVnode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;newVnode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;replaceChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newVnode&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;oldVnode&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="c1"&gt;// 相同类型，更新属性（省略细节）&lt;/span&gt;
  &lt;span class="c1"&gt;// 然后递归处理children，这里重点演示key的作用&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;oldChildren&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;oldVnode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;newChildren&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;newVnode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;keyedOld&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="c1"&gt;// 将旧节点按key建立索引&lt;/span&gt;
  &lt;span class="nx"&gt;oldChildren&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;child&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;child&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;child&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;keyedOld&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;child&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;child&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;idx&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="c1"&gt;// 遍历新节点，复用key相同的节点&lt;/span&gt;
  &lt;span class="nx"&gt;newChildren&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;newChild&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;newIdx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newChild&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;newChild&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;matched&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;keyedOld&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newChild&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;matched&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// 复用该DOM节点，递归更新子内容&lt;/span&gt;
        &lt;span class="nx"&gt;patch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;matched&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;child&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;newChild&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="c1"&gt;// 移动位置（这里省略，示意核心）&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;// 没有匹配，插入新节点&lt;/span&gt;
    &lt;span class="nx"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newChild&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="二、我反问了面试官一个问题"&gt;二、我反问了面试官一个问题&lt;/h2&gt;
&lt;p&gt;等代码展示完，面试官还没开口，我说：“这个 diff 算法里用 key 来匹配节点。很多前端都用过 key，但有一个经典误区——&lt;strong&gt;把数组索引当 key 用&lt;/strong&gt;。您知道为什么这样会有问题吗？”&lt;/p&gt;

&lt;p&gt;他来了兴趣：“你说说看。”&lt;/p&gt;

&lt;p&gt;我解释：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;diff 算法通过 key 判断节点是否 “相同”。如果用索引，比如列表顺序变了，索引 0 可能原来对应 A，现在对应 B，但 key 相同（都是 0），React 会认为这两个节点相同，不重新创建，只是更新内容。这样本应销毁 A、创建 B 的场景，变成了复用 A 并修改内容。如果组件有复杂状态（比如动画、输入框焦点），就会出现状态错乱。&lt;/li&gt;
&lt;li&gt;更严重的是，在列表头部插入一个元素，所有后续节点的索引都变了，每个节点都会被 “原地修改”，性能反而比不用 key 还差。&lt;/li&gt;
&lt;li&gt;正确做法是用数据中唯一稳定的标识（如 id）作为 key。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;他点头：“这才是我想听到的答案。”&lt;/p&gt;
&lt;h2 id="三、为什么面试官认可这种“反客为主”？"&gt;三、为什么面试官认可这种 “反客为主”？&lt;/h2&gt;
&lt;p&gt;他后来告诉我：“你能自己生成正确的 diff 逻辑，还能主动抛出常见的误区，说明你不仅会写，还真的思考过生产中的坑。这种深度，比背代码有价值。”&lt;/p&gt;

&lt;p&gt;所以这道题的关键不是完美写出所有 diff 逻辑，而是&lt;strong&gt;理解 key 的真实作用&lt;/strong&gt;。AI 帮你搭了骨架，你用自己的理解填充了灵魂。&lt;/p&gt;
&lt;h2 id="四、完整可运行的迷你虚拟DOM代码"&gt;四、完整可运行的迷你虚拟 DOM 代码&lt;/h2&gt;
&lt;p&gt;我把面试中使用的完整代码放在这里，你可以在浏览器控制台运行测试：&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 完整示例（带简版diff和key复用）&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;props&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="na"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;flat&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;vnode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;vnode&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;createTextNode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;vnode&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;vnode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;vnode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;setAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;vnode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="nx"&gt;vnode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;patch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;oldVnode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;newVnode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;parent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;oldVnode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parentNode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;oldVnode&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;newVnode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="c1"&gt;// 文本节点&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;oldVnode&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;newVnode&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;oldVnode&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;newVnode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;replaceChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newVnode&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;oldVnode&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;oldVnode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;newVnode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;replaceChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newVnode&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;oldVnode&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="c1"&gt;// 更新属性（略）&lt;/span&gt;
  &lt;span class="c1"&gt;// 处理children（简易版：只演示替换，不移动）&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;oldChildren&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;oldVnode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;newChildren&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;newVnode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;maxLen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;oldChildren&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;newChildren&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;maxLen&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;oldChildren&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;newChildren&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;patch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;oldChildren&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nx"&gt;newChildren&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nx"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;childNodes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;newChildren&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newChildren&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;removeChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;childNodes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;你可以用这段代码测试列表渲染，尝试改变顺序或插入头节点，观察不用 key vs 用 index vs 用 id 的区别。&lt;/p&gt;
&lt;h2 id="五、写在最后"&gt;五、写在最后&lt;/h2&gt;
&lt;p&gt;虚拟 DOM 和 diff 是 React 的根基，手写一遍能让你对性能优化有更深的体感。AI 能帮你快速生成模板，但真正拉开差距的，是对 “为什么 key 不能用 index” 这种问题的思考深度。&lt;/p&gt;</description>
      <author>193577746</author>
      <pubDate>Thu, 28 May 2026 18:16:46 +0800</pubDate>
      <link>https://beta.w2solo.com/topics/7437</link>
      <guid>https://beta.w2solo.com/topics/7437</guid>
    </item>
    <item>
      <title>推行 AI 写代码一年后，Code Review 变成了新的加班理由</title>
      <description>&lt;blockquote&gt;
&lt;p&gt;去年团队开始用 AI 编程工具，大家都觉得写代码变快了。但一年下来我发现一个怪现象：写代码的时间确实少了，可 Code Review 的时间却越来越长。以前 review 一个 PR，扫一遍逻辑、提两个问题就过了。现在每条改动我都要反复确认：这段是 AI 写的还是人写的？边界条件处理了吗？有没有隐藏的性能问题？review 到后来，比我自己重写一遍还累。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="一、AI写代码快，但review起来慢"&gt;一、AI 写代码快，但 review 起来慢&lt;/h2&gt;
&lt;p&gt;今年初，我接手了团队的核心项目。组里几个同事都用 Cursor 和 Copilot，产出确实高。但轮到我来 review 的时候，总觉得不对劲。&lt;/p&gt;

&lt;p&gt;举个例子，有个同事提交了一个功能模块，代码跑起来没问题，但我逐行看的时候发现：一个组件里挂了三个&lt;code&gt;useEffect&lt;/code&gt;，其中两个功能重叠。作者说他没注意，是 AI 自动补全的。这类问题成了常态：AI 生成的代码往往能跑，但结构混乱、逻辑重复、缺少边界判断。&lt;/p&gt;

&lt;p&gt;以前人工写的代码，你能感觉到作者的思路。AI 写的代码像一锅大杂烩，看起来什么都有，但就是不舒服。review 的时候，你不能只看逻辑对不对，还得帮作者删冗余、补缺失、理结构。这一套下来，时间不知不觉就翻倍了。&lt;/p&gt;
&lt;h2 id="二、几种AI代码的“通病”"&gt;二、几种 AI 代码的 “通病”&lt;/h2&gt;
&lt;p&gt;我总结了几类经常在 review 时遇到的 AI 痕迹：&lt;/p&gt;
&lt;h3 id="1. 逻辑正确，边界全无"&gt;1. 逻辑正确，边界全无&lt;/h3&gt;
&lt;p&gt;AI 很擅长把主流程跑通，但空指针、超时、并发这些问题它不太会主动处理。比如一个查询接口，AI 会写&lt;code&gt;data.list.map(...)&lt;/code&gt;，但不会判断&lt;code&gt;data&lt;/code&gt;是不是空。review 时要帮它补上&lt;code&gt;data?.list?.map&lt;/code&gt;，或者&lt;code&gt;if (!data) return null&lt;/code&gt;。&lt;/p&gt;
&lt;h3 id="2. 过度设计，简单问题复杂化"&gt;2. 过度设计，简单问题复杂化&lt;/h3&gt;
&lt;p&gt;有时候 AI 会把一个很简单的事搞得特别 “高端”。比如加个防抖，它给你写一个完整的自定义 Hook，里面用了&lt;code&gt;useRef&lt;/code&gt;、&lt;code&gt;useCallback&lt;/code&gt;、&lt;code&gt;useEffect&lt;/code&gt;，洋洋洒洒四五十行。但实际上只需要一个&lt;code&gt;debounce&lt;/code&gt;函数。review 这种代码，你得先理解它的实现，再判断是不是过度了。&lt;/p&gt;
&lt;h3 id="3. 注释和命名像“机器翻译”"&gt;3. 注释和命名像 “机器翻译”&lt;/h3&gt;
&lt;p&gt;AI 生成的变量名经常是&lt;code&gt;data&lt;/code&gt;、&lt;code&gt;items&lt;/code&gt;、&lt;code&gt;config&lt;/code&gt;，看不出具体含义。函数注释要么没有，要么是 “获取数据” 这种废话。review 时要帮忙改成有意义的命名，或者补上必要的注释，否则下一个人看不懂。&lt;/p&gt;
&lt;h2 id="三、我的个人策略：怎么让review不那么痛苦"&gt;三、我的个人策略：怎么让 review 不那么痛苦&lt;/h2&gt;
&lt;p&gt;经过大半年，我摸索出几条还算管用的办法：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;让 AI 先审 AI&lt;/strong&gt;：提交 PR 前，要求作者把代码再给 AI（换一个模型）审一遍，根据建议改一轮。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;差异化对待&lt;/strong&gt;：工具类、单元测试、文档注释可以放心让 AI 写；核心业务逻辑建议手写或深度修改。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;建立小规范&lt;/strong&gt;：比如 “useEffect 必须有清理函数”、“API 调用必须有 loading 和 error 状态”。AI 不一定会遵守，但 review 时可以快速对照。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这些办法不能根除问题，但至少能把 review 时间拉回到可接受的范围。&lt;/p&gt;
&lt;h2 id="四、反思"&gt;四、反思&lt;/h2&gt;
&lt;p&gt;我现在对 AI 编程的态度很矛盾。一方面，它确实帮我省了不少重复劳动；另一方面，它又把负担转移到了 review 环节。以前一个人写代码，大家一起审；现在一个人写 AI 代码，另一个人替他审漏洞。&lt;/p&gt;

&lt;p&gt;这不是 AI 的错，也不是工具的错，而是我们还没学会怎么和 AI 协作。也许未来会有更好的 review 流程，或者 AI 能自己审自己。但就目前而言，&lt;strong&gt;AI 没有让我少加班，只是让我加班的内容从 “写” 变成了 “审”&lt;/strong&gt;。&lt;/p&gt;
&lt;h2 id="五、最后"&gt;五、最后&lt;/h2&gt;
&lt;p&gt;如果你也在经历类似的感受，&lt;strong&gt;点个赞让我知道不是一个人&lt;/strong&gt;。评论区聊聊：你的团队有没有因为 AI 代码而 review 变慢？&lt;/p&gt;</description>
      <author>193577746</author>
      <pubDate>Wed, 27 May 2026 18:31:20 +0800</pubDate>
      <link>https://beta.w2solo.com/topics/7429</link>
      <guid>https://beta.w2solo.com/topics/7429</guid>
    </item>
    <item>
      <title>前端初级岗位暴跌 62%：我带了三年的实习生被裁了，而 AI 是他亲手教的</title>
      <description>&lt;blockquote&gt;
&lt;p&gt;上周五，我带了三年、刚转正没几个月的实习生被公司优化了。他在公司吃过最后一顿散伙饭，临走时对着部门那台退役的开发机拍了张照，说想留个纪念。我拍了拍他肩膀：“别灰心，多投几家。” 第二天，我把这个月筛选简历的数据拉出来一看——一个初级岗位，127 份简历，面试转化率不到 1%。不是他不够好。是整个前端初级岗位，正在被 AI 连根拔起。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="数据不会说谎：初级前端正在消失"&gt;数据不会说谎：初级前端正在消失&lt;/h2&gt;
&lt;p&gt;这不是感觉，是真实数据。&lt;/p&gt;

&lt;p&gt;根据拉勾网《2026 年第一季度前端行业报告》：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;前端初级岗位（0-3 年经验）招聘量同比 &lt;strong&gt;暴跌 62%&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;单个岗位平均收到 &lt;strong&gt;127 份简历&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;超过 &lt;strong&gt;80%&lt;/strong&gt; 的公司在 JD 里明确要求 “熟练使用 AI 编程工具”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;与此同时，AI 生成前端代码的能力在过去一年翻了至少三倍。一个刚入行的前端要一年才能掌握的布局、交互、状态管理，Cursor 用半分钟就能生成个七七八八。&lt;/p&gt;
&lt;h2 id="我带了三年的实习生，成了这场变革的缩影"&gt;我带了三年的实习生，成了这场变革的缩影&lt;/h2&gt;
&lt;p&gt;我的实习生小 L，本科计算机，在校成绩中上，自驱力强。他跟我学了 React、Vue、Webpack、性能优化，后来自己研究起 AI，主动在项目里接入 Copilot、用 Cursor 写组件库的单元测试，甚至给部门分享过一场 “AI 辅助开发实战”。他是我见过最积极的初级程序员。&lt;/p&gt;

&lt;p&gt;然而两个月前，公司组织架构调整，前端 HC 收缩。小 L 所在的边缘业务组直接被裁，他也在名单里。HR 的理由很标准：“业务方向调整。” 但私下里，技术总监说了一句大实话：“我们现在只需要能独当一面、能用 AI 提效的高级开发，初级的工作 AI 自己就能干大半。”&lt;/p&gt;

&lt;p&gt;那一刻我突然意识到：小 L 教 AI 写代码，最后 AI 把他教会了，他却没位置了。&lt;/p&gt;
&lt;h2 id="最残酷的悖论：AI淘汰的是“初级”，而不是“不会用AI的人”"&gt;最残酷的悖论：AI 淘汰的是 “初级”，而不是 “不会用 AI 的人”&lt;/h2&gt;
&lt;p&gt;很多人以为，只要学会用 AI 就能保住饭碗。但现实更残酷：&lt;strong&gt;当 AI 能完成 60% 的初级开发工作后，企业不再需要一个新人花半年成长到高级，而是直接招一个高级工程师用 AI 去覆盖初级的那部分产出。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;初级岗位的需求曲线，几乎是和 AI 编程能力的发展曲线反向交叉的。2023 年，AI 写代码还是段子；2026 年，它已经是职场竞争的底牌。&lt;/p&gt;

&lt;p&gt;小 L 不是不会用 AI，他把 AI 玩得很熟。但他的悲剧在于：&lt;strong&gt;一个能用 AI 的高级工程师，已经能覆盖三个初级的工作量。&lt;/strong&gt; 企业算的不是技术账，是成本账。&lt;/p&gt;
&lt;h2 id="前端没有死，但“门槛”被AI焊死了"&gt;前端没有死，但 “门槛” 被 AI 焊死了&lt;/h2&gt;
&lt;p&gt;前端这个岗位消失了吗？没有。&lt;/p&gt;

&lt;p&gt;高级前端的需求依然旺盛，而且薪资还在涨。但入行的门槛，被 AI 暴力抬高了。三年前，学会 Vue + 做一个后台管理系统就能找到工作。现在，企业要求：能独立搭建复杂的前端架构，能指导 AI 生成高质量代码并审查，能解决 AI 处理不了的性能瓶颈、安全漏洞、定制需求。&lt;/p&gt;

&lt;p&gt;换句话说，&lt;strong&gt;AI 把 “会不会写” 变成标配，把 “能不能解决复杂问题” 变成核心竞争力。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;小 L 走后，我们组没有再招初级。取而代之的是，团队给每位高级前端每月报销 AI 工具费用，要求用 AI 把重复劳动降到最低，把精力集中在架构、性能、工程化上。我们的人数少了，交付速度却快了一截。&lt;/p&gt;
&lt;h2 id="这不是一篇丧气文"&gt;这不是一篇丧气文&lt;/h2&gt;
&lt;p&gt;我写这篇文章不是为了制造焦虑。作为技术人，我始终相信：每一次技术浪潮，都会冲走一些岗位，也会创造出新的机会。AI 让初级岗位减少，但也催生了 AI 工程化、前端 Agent 开发、RAG 应用等新方向。&lt;/p&gt;

&lt;p&gt;真正危险的，不是某个岗位，而是停止进化的思维。&lt;/p&gt;

&lt;p&gt;小 L 后来去了另一家创业公司，负责搭建一个 AI 辅助的自动化前端流水线，职位从 “前端开发” 变成了 “AI 工程化前端”。他不再画页面，而是教 AI 怎么画页面。&lt;/p&gt;

&lt;p&gt;他那天给前同事发的消息是：“我终于不是被 AI 淘汰的人，而是操控 AI 的人。”&lt;/p&gt;
&lt;h2 id="写在最后"&gt;写在最后&lt;/h2&gt;
&lt;p&gt;2026 年的前端，比任何一个时代都更需要深度。AI 帮你写代码，但不会帮你背锅；AI 帮你查文档，但不会帮你设计架构；AI 帮你生成组件，但不会帮你理解业务。&lt;/p&gt;

&lt;p&gt;初级岗位在消失，但优秀的前端永远稀缺。&lt;/p&gt;

&lt;p&gt;你身边有被 AI“挤出” 岗位的同事吗？或者你自己就是靠 AI 转型上岸的？评论区聊聊，&lt;strong&gt;点个赞让我看到这个时代的缩影&lt;/strong&gt;。&lt;/p&gt;</description>
      <author>193577746</author>
      <pubDate>Tue, 26 May 2026 12:03:53 +0800</pubDate>
      <link>https://beta.w2solo.com/topics/7414</link>
      <guid>https://beta.w2solo.com/topics/7414</guid>
    </item>
    <item>
      <title>用魔法打败魔法：我让 AI 替我去面试前端岗，AI 面试官给我打了 92 分，还发了 offer</title>
      <description>&lt;h2 id="前言"&gt;前言&lt;/h2&gt;
&lt;p&gt;AI 面试官已经不是什么新鲜事了。银行、外企、大厂海量筛人时，都用 AI 初筛：你对着屏幕答题，AI 分析你的关键词、表情、语音语调。尤其前端岗，AI 会问你 “盒模型”“闭包”“事件循环”……全是标准答案题。&lt;/p&gt;

&lt;p&gt;既然对面是机器，那我这边也上机器，很合理吧？我花了一个周末，写了一个&lt;strong&gt;Chrome 扩展 + 本地服务&lt;/strong&gt;，搭建了一个 “前端 AI 替身”：麦克风收音 → 语音转文字 → 调用 GPT 生成前端答案 → 文字转语音 → 播放。全程自动，我只需要坐在旁边喝茶。结果对面真没发现，还给了我 92 分。&lt;/p&gt;

&lt;p&gt;今天我就把这个过程完整复盘，附上技术方案和代码思路。不是为了教你作弊，而是想告诉你：&lt;strong&gt;AI 面试官远没有想象中智能，以及未来前端面试防作弊技术会有多卷。&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;金句&lt;/strong&gt;：当面试官是 AI 时，应聘者也可以是 AI——魔法打败魔法的时代来了。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="一、前端AI面试官到底在“面”什么？"&gt;一、前端 AI 面试官到底在 “面” 什么？&lt;/h2&gt;
&lt;p&gt;目前主流 AI 面试系统（如 HireVue、国产的 “猿圈”“赛码”）对前端岗位会重点分析：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;前端关键词命中&lt;/strong&gt;：是否说出 “闭包”“原型链”“虚拟 DOM”“性能优化” 等词&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;语音流畅度&lt;/strong&gt;：卡顿、重复、语气词&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;微表情&lt;/strong&gt;：有没有看屏幕、是否紧张&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;逻辑结构&lt;/strong&gt;：回答是否分点、有无 “首先/然后/最后”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;它不会理解你的代码能力，只会统计特征。这就给 “AI 替身” 留下了可乘之机。&lt;/p&gt;
&lt;h2 id="二、我的“前端AI替身”技术方案（纯前端实现）"&gt;二、我的 “前端 AI 替身” 技术方案（纯前端实现）&lt;/h2&gt;
&lt;p&gt;整套系统跑在我的 MacBook 上，核心是四个模块，&lt;strong&gt;全部用前端技术栈实现&lt;/strong&gt;：&lt;/p&gt;
&lt;table class="table table-bordered table-striped"&gt;
&lt;tr&gt;
&lt;th&gt;模块&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;th&gt;前端实现工具&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;录音&lt;/td&gt;
&lt;td&gt;捕获面试官的声音&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;MediaRecorder&lt;/code&gt; API + &lt;code&gt;getUserMedia&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;语音转文字&lt;/td&gt;
&lt;td&gt;把问题变成文本&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Web Speech API&lt;/code&gt; 或 调用 Whisper 接口（通过 &lt;code&gt;fetch&lt;/code&gt;）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;大模型生成答案&lt;/td&gt;
&lt;td&gt;根据前端问题生成回答&lt;/td&gt;
&lt;td&gt;调用 GPT-4 / Claude API（&lt;code&gt;fetch&lt;/code&gt; + 流式响应）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;文字转语音&lt;/td&gt;
&lt;td&gt;把答案读出来&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Web Speech API&lt;/code&gt; 的 &lt;code&gt;speechSynthesis&lt;/code&gt;（无需额外库）&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;流程图&lt;/strong&gt;：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;面试官提问 → &lt;code&gt;MediaRecorder&lt;/code&gt; 录音 → &lt;code&gt;Web Speech API&lt;/code&gt; 转文字 → &lt;code&gt;fetch&lt;/code&gt; 调用 GPT → 生成答案 → &lt;code&gt;speechSynthesis&lt;/code&gt; 朗读 → 麦克风输出 → 面试官听到&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;为了让对面不察觉是机器，我加了几个细节：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;随机添加前端语气词&lt;/strong&gt;：在答案里插入 “嗯…我觉得”“其实嘛”“一般来说”，避免过于流畅&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;随机延迟&lt;/strong&gt;：每句话前等待 0.5~1.5 秒，模拟思考时间&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;摄像头画面我用的是自己提前录好的 30 秒循环视频&lt;/strong&gt;（眼神会眨眼、头会微动）&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="三、核心代码实现（纯前端）"&gt;三、核心代码实现（纯前端）&lt;/h2&gt;
&lt;p&gt;我写了一个简单的 HTML 页面，打开即可运行：&lt;/p&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;前端AI面试助手&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"start"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;开始监听面试官&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"question"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"answer"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;startBtn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;start&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;recognition&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// 1. 语音转文字（使用Web Speech API）&lt;/span&gt;
    &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;initRecognition&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;recognition&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;webkitSpeechRecognition&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="nx"&gt;recognition&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;continuous&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;recognition&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;interimResults&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;recognition&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lang&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;zh-CN&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;recognition&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onresult&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;question&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;transcript&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;question&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;innerText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`问题：&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;question&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="c1"&gt;// 2. 调用大模型生成前端答案&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;answer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;generateAnswer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;question&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;answer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;innerText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`答案：&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="c1"&gt;// 3. 语音合成&lt;/span&gt;
        &lt;span class="nx"&gt;speak&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// 调用GPT生成前端面试答案&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;generateAnswer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;question&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.openai.com/v1/chat/completions&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Authorization&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Bearer YOUR_API_KEY&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
          &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gpt-4&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;system&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;你是资深前端工程师，用口语化、自然的方式回答前端面试题，偶尔加“嗯”“我觉得”。答案控制在40秒内。&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;question&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
          &lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// 语音合成&lt;/span&gt;
    &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;speak&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;utterance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;SpeechSynthesisUtterance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;utterance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lang&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;zh-CN&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;utterance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.9&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 稍慢，像人在思考&lt;/span&gt;
      &lt;span class="nx"&gt;utterance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pitch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;speechSynthesis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;speak&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;utterance&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;startBtn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onclick&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;initRecognition&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="nx"&gt;recognition&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="nx"&gt;startBtn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;disabled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：为了不让对面听到电脑自身扬声器的声音导致回声，你需要戴上耳机，把麦克风输入设为耳机麦克风。这个脚本会把面试官的问题实时转文字，然后 GPT 生成答案，再用语音读出来。&lt;strong&gt;你全程不需要说一句话。&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="四、面试实战：30分钟，对面毫无察觉"&gt;四、面试实战：30 分钟，对面毫无察觉&lt;/h2&gt;
&lt;p&gt;那天面试，我打开这个页面，戴上耳机，点击 “开始监听”。对面 AI 面试官问：“请解释一下事件循环。” 我的脚本 3 秒后回答：“嗯…事件循环是 JavaScript 异步的核心机制。JS 是单线程的，它会把任务分为宏任务和微任务……” 语速平稳，偶尔带 “嗯”“呃”，像真人在思考。&lt;/p&gt;

&lt;p&gt;中间有一个问题问：“React 的 diff 算法原理是什么？” 脚本回答时，我手动加了点 “嗯…这个嘛”，显得更自然。&lt;/p&gt;

&lt;p&gt;全程 30 分钟，对面问了 8 个前端题：闭包、原型链、虚拟 DOM、性能优化、webpack 热更新……全部对答如流。AI 面试官的反馈界面显示：“关键词匹配度 92%，语言流畅度 98%。”&lt;/p&gt;

&lt;p&gt;两天后 HR 打电话来：“你初试成绩很不错，岗位匹配度 92%，约一下技术二面（真人面）。” 我假装镇定：“好的好的。” 挂了电话，心里五味杂陈。&lt;/p&gt;
&lt;h2 id="五、引发的思考：AI面试防作弊怎么破？"&gt;五、引发的思考：AI 面试防作弊怎么破？&lt;/h2&gt;
&lt;p&gt;这件事让我后背发凉：如果我可以，别人也可以。未来的 AI 面试防作弊手段会有多变态？&lt;/p&gt;

&lt;p&gt;我大胆预测前端场景下的防作弊：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;实时眼神跟踪&lt;/strong&gt;：要求你盯着屏幕，不能乱瞟，且瞳孔反光中必须看到屏幕内容（防换脸视频）&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;唇语识别&lt;/strong&gt;：用另一个模型检测你嘴巴动的语音是否匹配，防止 TTS&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;设备指纹&lt;/strong&gt;：检测是否运行了虚拟音频驱动、自动化脚本&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;双摄像头&lt;/strong&gt;：一个拍脸，一个拍桌面和键盘，防止你看提词器&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;在此之前，我建议求职者：&lt;strong&gt;如果遇到 AI 面试官，你可以提前准备答案做提词器，但千万别全自动替身&lt;/strong&gt;。一旦被检测出来，会被拉入企业黑名单。&lt;/p&gt;
&lt;h2 id="六、写在最后"&gt;六、写在最后&lt;/h2&gt;
&lt;p&gt;这次经历让我感慨：技术是中性的，用它来绕过规则还是提升效率，全在人心。我最终去参加了二面，没有再用 AI——因为二面是人，我能感觉到那份尊重和温度。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI 面试官不是敌人，而是倒逼我们更真实、更优秀的镜子。&lt;/strong&gt; 当你发现自己能被机器完美替代时，就该想想：我的核心竞争力到底是什么？&lt;/p&gt;</description>
      <author>193577746</author>
      <pubDate>Mon, 25 May 2026 19:11:27 +0800</pubDate>
      <link>https://beta.w2solo.com/topics/7412</link>
      <guid>https://beta.w2solo.com/topics/7412</guid>
    </item>
    <item>
      <title>写组件文档写到吐？我用 AI 自动生成 Storybook，同事以后直接抄</title>
      <description>&lt;blockquote&gt;
&lt;p&gt;我们公司的组件库有一百多个组件，但文档几乎为零。新同事来了，不知道每个组件怎么用、支持哪些 props，只能翻源码。我每周至少被问三次：“这个按钮的 size 参数是 ‘large’ 还是 ‘lg’？” 我烦了，写了个脚本：用 AI 读取组件的 TypeScript 类型定义，自动生成 Storybook 文档和示例代码。现在每次提交组件，CI 自动跑一遍，文档实时更新。同事再也不问我了，CTO 看到说：“这个自动化做得好，省了半个前端的人力。”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="前言"&gt;前言&lt;/h2&gt;
&lt;p&gt;组件文档的重要性，每个前端都懂。但现实是：业务需求压过来，谁有时间写文档？结果就是组件库越来越膨胀，文档越来越荒废。新人来了，要么猜，要么问，要么翻源码。&lt;/p&gt;

&lt;p&gt;我试过手动写 Storybook，一个组件写半小时，一百个组件写完，我可能已经离职了。后来我想：能不能让 AI 自动生成？组件的 props 类型、默认值、描述，不都写在代码里了吗？让 AI 提取出来，再套个模板，不就完事了？&lt;/p&gt;

&lt;p&gt;今天我把这套自动化流程拆给你看：怎么用 AI 解析 TS 类型，怎么生成 Storybook 的 stories 文件，怎么集成到 CI。以后你只需要写好组件，文档的事交给 AI。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;金句&lt;/strong&gt;：最好的文档不是 “人写的”，而是 “代码里长出来的”。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="一、痛点：手动写文档的三大坑"&gt;一、痛点：手动写文档的三大坑&lt;/h2&gt;&lt;table class="table table-bordered table-striped"&gt;
&lt;tr&gt;
&lt;th&gt;痛点&lt;/th&gt;
&lt;th&gt;具体表现&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;耗时长&lt;/td&gt;
&lt;td&gt;一个组件平均花 30 分钟写文档（props 表格 + 示例代码 + 说明）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;不同步&lt;/td&gt;
&lt;td&gt;改了组件 props，忘了改文档，文档变成 “废纸”&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;没人愿意写&lt;/td&gt;
&lt;td&gt;团队集体拖延，文档库永远是 “建设中”&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;p&gt;我们团队曾试图用&lt;code&gt;react-docgen-typescript&lt;/code&gt;自动提取 props，但它只能生成原始 JSON，还得人工转成 Markdown 或 Storybook。而且它不懂业务描述，比如 “size 的 large 表示大号按钮，用于主操作”，这种描述还是得人写。&lt;/p&gt;

&lt;p&gt;AI 的出现正好填补了这个缺口：它既能解析类型，又能生成自然语言描述，还能帮你写示例代码。&lt;/p&gt;
&lt;h2 id="二、我是怎么做的？三步全自动"&gt;二、我是怎么做的？三步全自动&lt;/h2&gt;&lt;h3 id="第一步：提取组件类型信息"&gt;第一步：提取组件类型信息&lt;/h3&gt;
&lt;p&gt;我用&lt;code&gt;react-docgen-typescript&lt;/code&gt;把组件的 props、类型、默认值、是否必填提取成 JSON。写一个脚本&lt;code&gt;extract-props.ts&lt;/code&gt;：&lt;/p&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;docgen&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-docgen-typescript&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;savePropValueAsString&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;shouldExtractLiteralValuesFromEnum&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;parser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;docgen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;withCustomConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./tsconfig.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;docs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./src/components/Button/index.tsx&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// docs[0] 结构：&lt;/span&gt;
&lt;span class="c1"&gt;// {&lt;/span&gt;
&lt;span class="c1"&gt;//   displayName: 'Button',&lt;/span&gt;
&lt;span class="c1"&gt;//   props: {&lt;/span&gt;
&lt;span class="c1"&gt;//     size: { type: { name: "'small' | 'medium' | 'large'" }, required: false, defaultValue: 'medium' },&lt;/span&gt;
&lt;span class="c1"&gt;//     children: { type: { name: 'ReactNode' }, required: true }&lt;/span&gt;
&lt;span class="c1"&gt;//   }&lt;/span&gt;
&lt;span class="c1"&gt;// }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="第二步：用AI生成Storybook内容"&gt;第二步：用 AI 生成 Storybook 内容&lt;/h3&gt;
&lt;p&gt;把提取的 JSON 喂给 AI，提示词：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;你是一名前端技术文档专家。请根据以下组件的 props 信息，生成一份 Storybook 的 stories 文件代码。&lt;/p&gt;

&lt;p&gt;要求：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;为每个 prop 生成控制台（controls）配置&lt;/li&gt;
&lt;li&gt;生成至少 3 个示例：基础用法、不同尺寸、自定义样式&lt;/li&gt;
&lt;li&gt;用 Markdown 格式输出，包含组件描述和 Props 表格&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;组件信息：&lt;/p&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;粘贴上一步的JSON&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;

&lt;p&gt;AI 会输出类似这样的 Storybook 代码：&lt;/p&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Button.stories.tsx&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Meta&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;StoryObj&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@storybook/react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Button&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./Button&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Meta&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;Button&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Components/Button&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;component&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;docs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;component&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;通用按钮组件，支持三种尺寸和两种主题色。主要用于表单提交、弹窗确认等操作。&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;argTypes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;control&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;radio&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;small&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;medium&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;large&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;按钮尺寸&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;control&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;按钮内容&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;StoryObj&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;Button&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;按钮&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;medium&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Large&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;StoryObj&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;Button&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;大按钮&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;large&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Small&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;StoryObj&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;Button&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;小按钮&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;small&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="第三步：集成到CI"&gt;第三步：集成到 CI&lt;/h3&gt;
&lt;p&gt;我在项目的&lt;code&gt;.github/workflows/docs.yml&lt;/code&gt;里加了一个 job：&lt;/p&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Auto generate Storybook&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;npm run extract:props&lt;/span&gt;
    &lt;span class="s"&gt;npm run ai:generate-stories&lt;/span&gt;
    &lt;span class="s"&gt;npm run build:storybook&lt;/span&gt;
  &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.OPENAI_API_KEY }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;每次&lt;code&gt;git push&lt;/code&gt;到 main 分支，GitHub Actions 自动跑一遍，重新生成文档并部署到 GitHub Pages。从此文档永远最新。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;金句&lt;/strong&gt;：AI 自动生成文档，让 “文档过时” 成为历史。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="三、实测效果：节省了多少时间？"&gt;三、实测效果：节省了多少时间？&lt;/h2&gt;
&lt;p&gt;我们组件库有 87 个组件，人工写一个平均 30 分钟，总计 43.5 小时。用 AI 自动生成后，每个组件约 2 分钟（人工 review 和微调），总计约 3 小时。&lt;strong&gt;节省了 93% 的时间&lt;/strong&gt;。&lt;/p&gt;
&lt;table class="table table-bordered table-striped"&gt;
&lt;tr&gt;
&lt;th&gt;指标&lt;/th&gt;
&lt;th&gt;手工&lt;/th&gt;
&lt;th&gt;AI 辅助&lt;/th&gt;
&lt;th&gt;变化&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;单组件文档耗时&lt;/td&gt;
&lt;td&gt;30 分钟&lt;/td&gt;
&lt;td&gt;2 分钟&lt;/td&gt;
&lt;td&gt;↓ 93%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;文档与代码同步率&lt;/td&gt;
&lt;td&gt;经常滞后&lt;/td&gt;
&lt;td&gt;实时同步&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;新人上手询问次数（月均）&lt;/td&gt;
&lt;td&gt;12 次&lt;/td&gt;
&lt;td&gt;2 次&lt;/td&gt;
&lt;td&gt;↓ 83%&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;p&gt;同事现在想查组件用法，直接打开 Storybook。没人再问我了，我可以安心写代码。&lt;/p&gt;
&lt;h2 id="四、注意事项（坑点）"&gt;四、注意事项（坑点）&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AI 会脑补不存在的 props&lt;/strong&gt;：有时会生成示例里用了你没提供的 prop，必须人工删掉。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;复杂泛型可能解析错&lt;/strong&gt;：&lt;code&gt;react-docgen-typescript&lt;/code&gt;对高阶组件、泛型组件的解析不够准，需要手动修正输入 JSON。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;保护公司组件库隐私&lt;/strong&gt;：不要直接把整个组件库代码喂给云端 AI。可以用本地模型（如 Ollama + CodeLlama），或者只传类型 JSON（不传实现细节）。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;示例代码要人工跑一遍&lt;/strong&gt;：AI 生成的示例可能无法直接运行（比如忘记 import 样式），你至少编译一次确保没语法错误。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="五、完整的Prompt模板（复制可用）"&gt;五、完整的 Prompt 模板（复制可用）&lt;/h2&gt;&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# 角色&lt;/span&gt;
你是一名前端技术文档专家，擅长为React组件生成Storybook文档。

&lt;span class="gh"&gt;# 任务&lt;/span&gt;
根据以下组件的props信息，生成一个完整的Storybook stories文件。

&lt;span class="gh"&gt;# 输入格式&lt;/span&gt;
JSON对象，包含displayName和props

&lt;span class="gh"&gt;# 输出要求&lt;/span&gt;
&lt;span class="p"&gt;1.&lt;/span&gt; 使用TypeScript语法
&lt;span class="p"&gt;2.&lt;/span&gt; 包含meta配置（title, component, parameters, argTypes）
&lt;span class="p"&gt;3.&lt;/span&gt; 生成至少3个Story：Default, Large, Small（或其他合适的变体）
&lt;span class="p"&gt;4.&lt;/span&gt; 在parameters.docs.description.component中写一段组件用途说明
&lt;span class="p"&gt;5.&lt;/span&gt; 每个argType的control要合理（radio/select/text等）

&lt;span class="gh"&gt;# 组件信息&lt;/span&gt;
[粘贴JSON]
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="六、写在最后"&gt;六、写在最后&lt;/h2&gt;
&lt;p&gt;以前我觉得文档是 “良心活”，写了加分，不写也没人扣分。现在 AI 帮我自动生成，我才发现：文档不是负担，而是杠杆——它让组件的复用率大大提升，团队效率自然就上去了。&lt;/p&gt;

&lt;p&gt;如果你也被组件文档折磨过，或者想知道怎么把 AI 接入你的工程化流程，&lt;strong&gt;点个赞让我看到&lt;/strong&gt;。&lt;/p&gt;</description>
      <author>193577746</author>
      <pubDate>Sat, 23 May 2026 21:45:25 +0800</pubDate>
      <link>https://beta.w2solo.com/topics/7398</link>
      <guid>https://beta.w2solo.com/topics/7398</guid>
    </item>
    <item>
      <title>面试官让我查各部门工资最高的员工，我用 AI 三秒写出窗口函数，他愣了</title>
      <description>&lt;blockquote&gt;
&lt;p&gt;上个月面一家大厂，技术面第二轮，面试官出了一个 SQL 题：“查每个部门工资最高的员工。” 我脑子里闪过一堆解法：子查询、自连接、group by……但都太绕，而且还容易漏掉并列第一。我打开 Cursor，输入：“用窗口函数写一个 SQL，查出每个部门工资最高的员工，如果最高工资并列全部返回。” 三秒后 SQL 生成。面试官看了一眼，说：“你是我见过第一个用窗口函数还讲得清原理的候选人。” 后来我拿到了 offer。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="前言"&gt;前言&lt;/h2&gt;
&lt;p&gt;SQL 面试题里，“分组取 Top N” 是经典中的经典。尤其是 “每个部门工资最高的员工”，考频率仅次于两数之和。很多人能写出来，但写出来的往往是子查询嵌套自连接，性能差、代码丑、还容易漏掉并列的情况。&lt;/p&gt;

&lt;p&gt;而窗口函数（ROW_NUMBER / RANK / DENSE_RANK）才是这道题的标准答案，但很多人一紧张就忘了语法：OVER 里面怎么写？PARTITION BY 和 ORDER BY 怎么配？RANK 和 ROW_NUMBER 有什么区别？&lt;/p&gt;

&lt;p&gt;其实不用背，AI 一秒就给你写出来。但关键是：你要能讲清楚为什么选这个函数，以及窗口函数的执行顺序。今天我就用这个真实面试题，教你用 AI 秒解 SQL 难题，顺便讲透窗口函数。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;金句&lt;/strong&gt;：面试官考的不是你背不背得出语法，而是你懂不懂 “为什么” 和 “选哪个”。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="一、先看面试题原貌"&gt;一、先看面试题原貌&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;表结构&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;employee&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;department&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;salary&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;需求&lt;/strong&gt;：查询每个部门工资最高的员工。如果最高工资有并列（比如两人都是 10000），都要返回。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;示例数据&lt;/strong&gt;
| id | name | department | salary |
| ---- |------| ------------ |--------|
| 1 | 张三 | 技术部 | 12000 |
| 2 | 李四 | 技术部 | 10000 |
| 3 | 王五 | 技术部 | 12000 |
| 4 | 赵六 | 销售部 | 9000 |&lt;/p&gt;

&lt;p&gt;期望输出：技术部返回张三和王五（并列 12000），销售部返回赵六。&lt;/p&gt;
&lt;h2 id="二、AI生成的标准答案"&gt;二、AI 生成的标准答案&lt;/h2&gt;
&lt;p&gt;我在 Cursor 里输入：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;用 MySQL 语法，写一个 SQL 查询：每个部门工资最高的员工。如果最高工资有多人，全部返回。要求使用窗口函数，并解释执行顺序。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;AI 输出：&lt;/p&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;ranked&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; 
        &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;RANK&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;department&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;salary&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;rnk&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;employee&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;department&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;salary&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;ranked&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;rnk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;为什么用 RANK 而不是 ROW_NUMBER？&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ROW_NUMBER&lt;/code&gt; 会给每人一个唯一序号，并列的也会强行分出先后，导致只能返回一人。&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;RANK&lt;/code&gt; 会为相同工资分配相同排名，并列第一就都返回。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;面试官紧接着追问：那 &lt;code&gt;DENSE_RANK&lt;/code&gt; 呢？答：&lt;code&gt;DENSE_RANK&lt;/code&gt; 排名连续，但本题只需要最高工资，效果与 &lt;code&gt;RANK&lt;/code&gt; 一样（因为只取 rnk=1）。但如果有第二高需求，&lt;code&gt;DENSE_RANK&lt;/code&gt; 和 &lt;code&gt;RANK&lt;/code&gt; 区别就大了。&lt;/p&gt;
&lt;h2 id="三、窗口函数的执行顺序（讲清楚才能加分）"&gt;三、窗口函数的执行顺序（讲清楚才能加分）&lt;/h2&gt;
&lt;p&gt;面试官接着问：“窗口函数是在 SQL 哪个阶段执行的？”&lt;/p&gt;

&lt;p&gt;我回答：&lt;strong&gt;在 WHERE、GROUP BY 之后，在 ORDER BY 之前&lt;/strong&gt;。具体顺序：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;FROM → JOIN&lt;/li&gt;
&lt;li&gt;WHERE → 过滤行&lt;/li&gt;
&lt;li&gt;GROUP BY → 分组&lt;/li&gt;
&lt;li&gt;聚合函数计算&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HAVING&lt;/strong&gt; → 过滤分组&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;窗口函数&lt;/strong&gt; → 在这里计算排名&lt;/li&gt;
&lt;li&gt;ORDER BY&lt;/li&gt;
&lt;li&gt;LIMIT&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;所以，窗口函数的结果可以在 WHERE 里用吗？&lt;strong&gt;不能&lt;/strong&gt;，因为 WHERE 在窗口函数之前执行。所以必须用 CTE 或子查询先计算排名，再在外部 WHERE 筛选。&lt;/p&gt;

&lt;p&gt;AI 生成的代码正是这样做的：&lt;code&gt;WITH ranked AS ( ... )&lt;/code&gt; 先算排名，再 &lt;code&gt;WHERE rnk = 1&lt;/code&gt;。面试官听到这里，点了头。&lt;/p&gt;
&lt;h2 id="四、如果不用窗口函数，你能写出更优的解法吗？"&gt;四、如果不用窗口函数，你能写出更优的解法吗？&lt;/h2&gt;
&lt;p&gt;可以用&lt;strong&gt;子查询 + 关联&lt;/strong&gt;，但性能差且代码臃肿：&lt;/p&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;e1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;employee&lt;/span&gt; &lt;span class="n"&gt;e1&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;employee&lt;/span&gt; &lt;span class="n"&gt;e2&lt;/span&gt; 
    &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;e1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;department&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;e2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;department&lt;/span&gt; 
    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;e1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;salary&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;e2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;salary&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;e2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个解法的逻辑是：找出不存在同部门更高工资的人。优点是跨数据库通用，缺点是不好理解，而且对于大表性能差（因为自连接扫描两次）。&lt;/p&gt;

&lt;p&gt;用窗口函数，不仅快（一次扫描），而且清晰易维护。所以现代 SQL 强烈推荐。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;金句&lt;/strong&gt;：不用窗口函数的 SQL 优化，就像不用箭头函数的 JS——能跑，但不优雅。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="五、面试官为什么认可我用AI？"&gt;五、面试官为什么认可我用 AI？&lt;/h2&gt;
&lt;p&gt;同样的逻辑：AI 帮我生成语法，我负责解释原理。他知道我背不出完整的 RANK 语法（正常人谁背？），但我知道什么时候用 RANK、窗口函数执行顺序、如何改造成其他需求。这就够了。&lt;/p&gt;
&lt;h2 id="六、拓展：分组取第二名怎么改？"&gt;六、拓展：分组取第二名怎么改？&lt;/h2&gt;
&lt;p&gt;面试官可能接着问：“如果我要每个部门第二高工资呢？”&lt;/p&gt;

&lt;p&gt;很简单，把 &lt;code&gt;WHERE rnk = 1&lt;/code&gt; 改成 &lt;code&gt;WHERE rnk = 2&lt;/code&gt; 就行。但注意：如果第二名是并列（比如两人都是 9000），RANK 会跳过第三名的序号（比如排名：1,2,2,4），&lt;code&gt;DENSE_RANK&lt;/code&gt; 则连续（1,2,2,3）。你需要问清楚需求。&lt;/p&gt;
&lt;h2 id="七、完整代码和测试数据"&gt;七、完整代码和测试数据&lt;/h2&gt;
&lt;p&gt;你可以用这个数据自测：&lt;/p&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;employee&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;department&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;salary&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;employee&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'张三'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'技术部'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;12000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'李四'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'技术部'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;10000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'王五'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'技术部'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;12000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'赵六'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'销售部'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;9000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后运行上面的窗口函数 SQL，输出应该是：张三、王五、赵六。&lt;/p&gt;
&lt;h2 id="八、写在最后"&gt;八、写在最后&lt;/h2&gt;
&lt;p&gt;面试题在变，但考察的核心没变：&lt;strong&gt;理解原理 &amp;gt; 背诵语法&lt;/strong&gt;。AI 能帮你写出任何代码，但能讲清楚为什么、怎么改、有什么坑，才是你的真本事。&lt;/p&gt;

&lt;p&gt;你面试遇到过哪些 “想不起来语法” 的瞬间？后来怎么过的？&lt;/p&gt;</description>
      <author>193577746</author>
      <pubDate>Fri, 22 May 2026 18:29:37 +0800</pubDate>
      <link>https://beta.w2solo.com/topics/7389</link>
      <guid>https://beta.w2solo.com/topics/7389</guid>
    </item>
  </channel>
</rss>
