<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Pattern on Tarragon</title><link>https://tarrragon.github.io/blog/tags/pattern/</link><description>Recent content in Pattern on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Sun, 26 Apr 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/pattern/index.xml" rel="self" type="application/rss+xml"/><item><title>Pattern：Document 全文件 query</title><link>https://tarrragon.github.io/blog/report/pattern-document-query/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/pattern-document-query/</guid><description>&lt;h2 id="核心做法">核心做法&lt;/h2>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.target&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelectorAll&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.target&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>從整個頁面找元素、不指定 ancestor scope。&lt;/p>
&lt;hr>
&lt;h2 id="這個做法存在的價值">這個做法存在的價值&lt;/h2>
&lt;p>簡潔。一行就能取到目標、不需要先建立元件根變數。在「我只想快速確認某個元素在不在 / 取它的某個屬性」這類情境下、寫一個 import shell 變數 + null check 是過度工程。&lt;/p>
&lt;hr>
&lt;h2 id="適合的情境">適合的情境&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>情境&lt;/th>
 &lt;th>為什麼合理&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Devtools console 一行查詢&lt;/td>
 &lt;td>沒有「未來會壞」的問題、用完就丟&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>原型 / spike 階段程式碼&lt;/td>
 &lt;td>預期會被丟棄重寫、不需要長期維護考量&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>確定全頁唯一的單例（&lt;code>document.body&lt;/code>、&lt;code>&amp;lt;html&amp;gt;&lt;/code>）&lt;/td>
 &lt;td>從定義上不會多個、也不會被誤命中&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Build-time script、不會在 runtime 跑&lt;/td>
 &lt;td>沒有「同頁多元件」的可能性&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>核心特徵：&lt;strong>這段程式不會在多元件 / 動態 DOM 環境長期存活&lt;/strong>。&lt;/p>
&lt;hr>
&lt;h2 id="不適合的情境">不適合的情境&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>情境&lt;/th>
 &lt;th>失敗模式&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Production 客製、預期長期存活&lt;/td>
 &lt;td>未來頁面結構變動、誤命中或漏命中&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>同頁可能有多個同類元件&lt;/td>
 &lt;td>只取第一個、其他被忽略且不報錯&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>元件可能在 SPA 路由中動態增減&lt;/td>
 &lt;td>query 時機跟元件 mount 時機不對齊&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>寫入第三方函式庫&lt;/td>
 &lt;td>使用者頁面的其他 class 可能跟你的 selector 撞&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>安靜失敗是最危險的特徵&lt;/strong> — 不報錯、操作了錯元素、bug 表現遠離 root cause。&lt;/p>
&lt;hr>
&lt;h2 id="跟其他起點做法的關係">跟其他起點做法的關係&lt;/h2>
&lt;p>&lt;a href="../dom-selector-precision/">#14 Selector 精準度&lt;/a> 的「起點」維度有四種做法、document query 是其中之一：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>做法&lt;/th>
 &lt;th>比較&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>本卡片：document query&lt;/td>
 &lt;td>簡潔但不防護未來變動&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="../pattern-component-root/">元件根變數&lt;/a>&lt;/td>
 &lt;td>多一行 setup、換到「shell 內隔離」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="../pattern-root-as-parameter/">起點當參數&lt;/a>&lt;/td>
 &lt;td>多實例支援、適合可能擴展的客製&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="../pattern-closest-lookup/">closest 反向找根&lt;/a>&lt;/td>
 &lt;td>事件委派情境、動態元件&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>選擇順序：production 客製預設用「元件根變數」、原型 / 探索 / 一次性才用 document query。&lt;/p>
&lt;hr>
&lt;h2 id="邊界什麼時候-document-query-在-production-也合理">邊界：什麼時候 document query 在 production 也合理&lt;/h2>
&lt;p>幾個常見的 production 例外：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1">// 例外 1：操作的目標就是「全頁面唯一單例」
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">body&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">classList&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">add&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;page-search&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">documentElement&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">setAttribute&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;data-theme&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;dark&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="c1">// 例外 2：跨元件邊界的元素（不在任何元件內）
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kd">var&lt;/span> &lt;span class="nx">slot&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.search-filter-slot&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="c1">// (slot 是 main 的子節點、不在 search-shell 內、不能從 shell 找)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="c1">// 例外 3：頁面層級的 meta 元素
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;meta[name=&amp;#34;description&amp;#34;]&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>例外都共享一個特徵：&lt;strong>目標元素本質上就在「頁面層級」、不是任何元件的內部&lt;/strong>。&lt;/p>
&lt;p>不是例外的場景、即使「當前頁面只有一個」、也用元件根變數 — 預防未來擴展。&lt;/p>
&lt;hr>
&lt;h2 id="判讀徵兆">判讀徵兆&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>訊號&lt;/th>
 &lt;th>該換做法嗎？&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>「現在只有一個、之後再想」&lt;/td>
 &lt;td>是 — 換元件根變數&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>同檔案多處 &lt;code>document.querySelector('.x')&lt;/code>&lt;/td>
 &lt;td>是 — 至少改成存變數重用&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>寫第三方 library 用 document query&lt;/td>
 &lt;td>是 — 改用根參數 pattern&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>操作 &lt;code>document.body&lt;/code> / &lt;code>&amp;lt;html&amp;gt;&lt;/code>&lt;/td>
 &lt;td>否 — 這就是合理場景&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>程式跑一次後丟棄（migration script）&lt;/td>
 &lt;td>否 — 簡潔優先&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>核心原則&lt;/strong>：document query 不是反模式、是有適用範圍的工具。判斷「這段程式預期活多久」 — 短命用 document、長命用元件根。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心做法">核心做法</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.target&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nb">document</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span><span class="s1">&#39;.target&#39;</span><span class="p">);</span></span></span></code></pre></div><p>從整個頁面找元素、不指定 ancestor scope。</p>
<hr>
<h2 id="這個做法存在的價值">這個做法存在的價值</h2>
<p>簡潔。一行就能取到目標、不需要先建立元件根變數。在「我只想快速確認某個元素在不在 / 取它的某個屬性」這類情境下、寫一個 import shell 變數 + null check 是過度工程。</p>
<hr>
<h2 id="適合的情境">適合的情境</h2>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>為什麼合理</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Devtools console 一行查詢</td>
          <td>沒有「未來會壞」的問題、用完就丟</td>
      </tr>
      <tr>
          <td>原型 / spike 階段程式碼</td>
          <td>預期會被丟棄重寫、不需要長期維護考量</td>
      </tr>
      <tr>
          <td>確定全頁唯一的單例（<code>document.body</code>、<code>&lt;html&gt;</code>）</td>
          <td>從定義上不會多個、也不會被誤命中</td>
      </tr>
      <tr>
          <td>Build-time script、不會在 runtime 跑</td>
          <td>沒有「同頁多元件」的可能性</td>
      </tr>
  </tbody>
</table>
<p>核心特徵：<strong>這段程式不會在多元件 / 動態 DOM 環境長期存活</strong>。</p>
<hr>
<h2 id="不適合的情境">不適合的情境</h2>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>失敗模式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Production 客製、預期長期存活</td>
          <td>未來頁面結構變動、誤命中或漏命中</td>
      </tr>
      <tr>
          <td>同頁可能有多個同類元件</td>
          <td>只取第一個、其他被忽略且不報錯</td>
      </tr>
      <tr>
          <td>元件可能在 SPA 路由中動態增減</td>
          <td>query 時機跟元件 mount 時機不對齊</td>
      </tr>
      <tr>
          <td>寫入第三方函式庫</td>
          <td>使用者頁面的其他 class 可能跟你的 selector 撞</td>
      </tr>
  </tbody>
</table>
<p><strong>安靜失敗是最危險的特徵</strong> — 不報錯、操作了錯元素、bug 表現遠離 root cause。</p>
<hr>
<h2 id="跟其他起點做法的關係">跟其他起點做法的關係</h2>
<p><a href="../dom-selector-precision/">#14 Selector 精準度</a> 的「起點」維度有四種做法、document query 是其中之一：</p>
<table>
  <thead>
      <tr>
          <th>做法</th>
          <th>比較</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>本卡片：document query</td>
          <td>簡潔但不防護未來變動</td>
      </tr>
      <tr>
          <td><a href="../pattern-component-root/">元件根變數</a></td>
          <td>多一行 setup、換到「shell 內隔離」</td>
      </tr>
      <tr>
          <td><a href="../pattern-root-as-parameter/">起點當參數</a></td>
          <td>多實例支援、適合可能擴展的客製</td>
      </tr>
      <tr>
          <td><a href="../pattern-closest-lookup/">closest 反向找根</a></td>
          <td>事件委派情境、動態元件</td>
      </tr>
  </tbody>
</table>
<p>選擇順序：production 客製預設用「元件根變數」、原型 / 探索 / 一次性才用 document query。</p>
<hr>
<h2 id="邊界什麼時候-document-query-在-production-也合理">邊界：什麼時候 document query 在 production 也合理</h2>
<p>幾個常見的 production 例外：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">// 例外 1：操作的目標就是「全頁面唯一單例」
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="nb">document</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nx">classList</span><span class="p">.</span><span class="nx">add</span><span class="p">(</span><span class="s1">&#39;page-search&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="nb">document</span><span class="p">.</span><span class="nx">documentElement</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="s1">&#39;data-theme&#39;</span><span class="p">,</span> <span class="s1">&#39;dark&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1">// 例外 2：跨元件邊界的元素（不在任何元件內）
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"></span><span class="kd">var</span> <span class="nx">slot</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.search-filter-slot&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1">// (slot 是 main 的子節點、不在 search-shell 內、不能從 shell 找)
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"></span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1">// 例外 3：頁面層級的 meta 元素
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"></span><span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;meta[name=&#34;description&#34;]&#39;</span><span class="p">);</span></span></span></code></pre></div><p>例外都共享一個特徵：<strong>目標元素本質上就在「頁面層級」、不是任何元件的內部</strong>。</p>
<p>不是例外的場景、即使「當前頁面只有一個」、也用元件根變數 — 預防未來擴展。</p>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該換做法嗎？</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>「現在只有一個、之後再想」</td>
          <td>是 — 換元件根變數</td>
      </tr>
      <tr>
          <td>同檔案多處 <code>document.querySelector('.x')</code></td>
          <td>是 — 至少改成存變數重用</td>
      </tr>
      <tr>
          <td>寫第三方 library 用 document query</td>
          <td>是 — 改用根參數 pattern</td>
      </tr>
      <tr>
          <td>操作 <code>document.body</code> / <code>&lt;html&gt;</code></td>
          <td>否 — 這就是合理場景</td>
      </tr>
      <tr>
          <td>程式跑一次後丟棄（migration script）</td>
          <td>否 — 簡潔優先</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：document query 不是反模式、是有適用範圍的工具。判斷「這段程式預期活多久」 — 短命用 document、長命用元件根。</p>
]]></content:encoded></item><item><title>Pattern：元件根變數 query</title><link>https://tarrragon.github.io/blog/report/pattern-component-root/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/pattern-component-root/</guid><description>&lt;h2 id="核心做法">核心做法&lt;/h2>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kd">var&lt;/span> &lt;span class="nx">shell&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.search-shell&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="o">!&lt;/span>&lt;span class="nx">shell&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="k">return&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="kd">var&lt;/span> &lt;span class="nx">input&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">shell&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.pagefind-ui__search-input&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="kd">var&lt;/span> &lt;span class="nx">drawer&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">shell&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.pagefind-ui__drawer&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="c1">// ... 之後所有 query 都從 shell 開始
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>把元件根 query 一次存變數、所有後續 query 都從這個變數開始。&lt;/p>
&lt;hr>
&lt;h2 id="這個做法存在的價值">這個做法存在的價值&lt;/h2>
&lt;p>把 selector 的作用範圍從「全頁面」收斂到「元件內部」。即使未來頁面其他地方出現同名元素、跟我無關。成本只多一行 query + 一個 null check、防護收益大。&lt;/p>
&lt;hr>
&lt;h2 id="適合的情境">適合的情境&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>情境&lt;/th>
 &lt;th>為什麼合理&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Production 客製、預期長期存活&lt;/td>
 &lt;td>未來頁面結構可能變動、需要隔離&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>當前只有一個元件實例、未來可能加&lt;/td>
 &lt;td>提早預防、改造成本最低&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>元件根 mount 後不會被移除&lt;/td>
 &lt;td>變數生命週期跟元件一致&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>程式跑在頁面 mount 後（DOMContentLoaded 後）&lt;/td>
 &lt;td>shell 可被找到&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>核心特徵&lt;/strong>：寫的時候只有一個元件、但希望程式碼能容忍未來頁面結構變動。&lt;/p>
&lt;hr>
&lt;h2 id="不適合的情境">不適合的情境&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>情境&lt;/th>
 &lt;th>為什麼不夠&lt;/th>
 &lt;th>改用&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>同頁同時有多個元件實例&lt;/td>
 &lt;td>變數只存第一個 shell、其他被忽略&lt;/td>
 &lt;td>&lt;a href="../pattern-root-as-parameter/">起點當參數&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>元件動態增減（SPA 路由切換）&lt;/td>
 &lt;td>變數指向 stale DOM&lt;/td>
 &lt;td>&lt;a href="../pattern-closest-lookup/">closest 反向找根&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>一次性 / 探索期程式&lt;/td>
 &lt;td>過度工程&lt;/td>
 &lt;td>&lt;a href="../pattern-document-query/">document query&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="設計細節">設計細節&lt;/h2>
&lt;h3 id="null-check-的時機">Null check 的時機&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kd">var&lt;/span> &lt;span class="nx">shell&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.search-shell&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="o">!&lt;/span>&lt;span class="nx">shell&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="k">return&lt;/span>&lt;span class="p">;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>頁面可能沒有 shell（不是搜尋頁），所有後續 query 都會 null pointer。提早 return 比後續一連串 &lt;code>if (drawer)&lt;/code> 乾淨。&lt;/p>
&lt;p>&lt;strong>等同於宣告&lt;/strong>：「這段程式只在有 shell 的頁面執行」。&lt;/p>
&lt;h3 id="變數的宣告位置">變數的宣告位置&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>位置&lt;/th>
 &lt;th>適合&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>函式內 local 變數&lt;/td>
 &lt;td>預設、scope 最小&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Module scope（IIFE 內）&lt;/td>
 &lt;td>多函式共用同一 shell&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Class instance property&lt;/td>
 &lt;td>元件本身用 class 包裝時&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>避免全域變數 — &lt;code>window.shell&lt;/code> 容易跟其他 script 撞。&lt;/p>
&lt;h3 id="等待-shell-mount-的處理">等待 shell mount 的處理&lt;/h3>
&lt;p>如果 script 跑得太早（shell 還沒 mount），shell 會是 null：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1">// 解法 1：等 DOMContentLoaded
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">addEventListener&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;DOMContentLoaded&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kd">function&lt;/span> &lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">shell&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.search-shell&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="o">!&lt;/span>&lt;span class="nx">shell&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="k">return&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="c1">// ...
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="c1">// 解法 2：MutationObserver 等 mount
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kd">var&lt;/span> &lt;span class="nx">bootstrap&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">MutationObserver&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kd">function&lt;/span> &lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">shell&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.search-shell&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="o">!&lt;/span>&lt;span class="nx">shell&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="k">return&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl"> &lt;span class="nx">bootstrap&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">disconnect&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl"> &lt;span class="c1">// ...
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">&lt;span class="nx">bootstrap&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">observe&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">body&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">childList&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">subtree&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="kc">true&lt;/span> &lt;span class="p">});&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>選擇取決於 shell 是 server-render 還是 client-render&lt;/strong>：server-render 用 DOMContentLoaded、client-render 用 observer。&lt;/p>
&lt;hr>
&lt;h2 id="跟其他起點做法的關係">跟其他起點做法的關係&lt;/h2>
&lt;p>&lt;a href="../dom-selector-precision/">#14 Selector 精準度&lt;/a> 的「起點」維度有四種做法：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>做法&lt;/th>
 &lt;th>比較&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="../pattern-document-query/">document query&lt;/a>&lt;/td>
 &lt;td>比本卡片簡潔、不防護未來變動&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>本卡片：元件根變數&lt;/td>
 &lt;td>多一行設定、隔離未來頁面變動&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="../pattern-root-as-parameter/">起點當參數&lt;/a>&lt;/td>
 &lt;td>比本卡片多支援多實例、設計成本前移&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="../pattern-closest-lookup/">closest 反向找根&lt;/a>&lt;/td>
 &lt;td>適合動態元件、不依賴變數綁定的時間&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>預設用本卡片、需要多實例升級到「起點當參數」、需要動態升級到「closest」。&lt;/p>
&lt;hr>
&lt;h2 id="應用範例完整-setup">應用範例：完整 setup&lt;/h2>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="kd">function&lt;/span> &lt;span class="nx">init&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">shell&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.search-shell&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="o">!&lt;/span>&lt;span class="nx">shell&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="k">return&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">ui&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">shell&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.pagefind-ui&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">input&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">shell&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.pagefind-ui__search-input&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">drawer&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">shell&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.pagefind-ui__drawer&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="o">!&lt;/span>&lt;span class="nx">input&lt;/span> &lt;span class="o">||&lt;/span> &lt;span class="o">!&lt;/span>&lt;span class="nx">drawer&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="k">return&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// 元件未完整 mount
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl"> &lt;span class="nx">syncScopeHeight&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">shell&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.search-scope&amp;#39;&lt;/span>&lt;span class="p">));&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl"> &lt;span class="nx">setupFilterSlotSwap&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">shell&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">drawer&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl"> &lt;span class="nx">setupScopeFilter&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">shell&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">input&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl">&lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">addEventListener&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;DOMContentLoaded&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">init&lt;/span>&lt;span class="p">);&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>shell 取一次、各 setup 函式從 shell 派生需要的子節點。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心做法">核心做法</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">var</span> <span class="nx">shell</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.search-shell&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">shell</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="kd">var</span> <span class="nx">input</span>  <span class="o">=</span> <span class="nx">shell</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui__search-input&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="kd">var</span> <span class="nx">drawer</span> <span class="o">=</span> <span class="nx">shell</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui__drawer&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1">// ... 之後所有 query 都從 shell 開始
</span></span></span></code></pre></div><p>把元件根 query 一次存變數、所有後續 query 都從這個變數開始。</p>
<hr>
<h2 id="這個做法存在的價值">這個做法存在的價值</h2>
<p>把 selector 的作用範圍從「全頁面」收斂到「元件內部」。即使未來頁面其他地方出現同名元素、跟我無關。成本只多一行 query + 一個 null check、防護收益大。</p>
<hr>
<h2 id="適合的情境">適合的情境</h2>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>為什麼合理</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Production 客製、預期長期存活</td>
          <td>未來頁面結構可能變動、需要隔離</td>
      </tr>
      <tr>
          <td>當前只有一個元件實例、未來可能加</td>
          <td>提早預防、改造成本最低</td>
      </tr>
      <tr>
          <td>元件根 mount 後不會被移除</td>
          <td>變數生命週期跟元件一致</td>
      </tr>
      <tr>
          <td>程式跑在頁面 mount 後（DOMContentLoaded 後）</td>
          <td>shell 可被找到</td>
      </tr>
  </tbody>
</table>
<p><strong>核心特徵</strong>：寫的時候只有一個元件、但希望程式碼能容忍未來頁面結構變動。</p>
<hr>
<h2 id="不適合的情境">不適合的情境</h2>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>為什麼不夠</th>
          <th>改用</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>同頁同時有多個元件實例</td>
          <td>變數只存第一個 shell、其他被忽略</td>
          <td><a href="../pattern-root-as-parameter/">起點當參數</a></td>
      </tr>
      <tr>
          <td>元件動態增減（SPA 路由切換）</td>
          <td>變數指向 stale DOM</td>
          <td><a href="../pattern-closest-lookup/">closest 反向找根</a></td>
      </tr>
      <tr>
          <td>一次性 / 探索期程式</td>
          <td>過度工程</td>
          <td><a href="../pattern-document-query/">document query</a></td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="設計細節">設計細節</h2>
<h3 id="null-check-的時機">Null check 的時機</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">var</span> <span class="nx">shell</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.search-shell&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">shell</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span></span></span></code></pre></div><p>頁面可能沒有 shell（不是搜尋頁），所有後續 query 都會 null pointer。提早 return 比後續一連串 <code>if (drawer)</code> 乾淨。</p>
<p><strong>等同於宣告</strong>：「這段程式只在有 shell 的頁面執行」。</p>
<h3 id="變數的宣告位置">變數的宣告位置</h3>
<table>
  <thead>
      <tr>
          <th>位置</th>
          <th>適合</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>函式內 local 變數</td>
          <td>預設、scope 最小</td>
      </tr>
      <tr>
          <td>Module scope（IIFE 內）</td>
          <td>多函式共用同一 shell</td>
      </tr>
      <tr>
          <td>Class instance property</td>
          <td>元件本身用 class 包裝時</td>
      </tr>
  </tbody>
</table>
<p>避免全域變數 — <code>window.shell</code> 容易跟其他 script 撞。</p>
<h3 id="等待-shell-mount-的處理">等待 shell mount 的處理</h3>
<p>如果 script 跑得太早（shell 還沒 mount），shell 會是 null：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">// 解法 1：等 DOMContentLoaded
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="nb">document</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">&#39;DOMContentLoaded&#39;</span><span class="p">,</span> <span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="kd">var</span> <span class="nx">shell</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.search-shell&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">shell</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  <span class="c1">// ...
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"></span><span class="p">});</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1">// 解法 2：MutationObserver 等 mount
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"></span><span class="kd">var</span> <span class="nx">bootstrap</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">MutationObserver</span><span class="p">(</span><span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">  <span class="kd">var</span> <span class="nx">shell</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.search-shell&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">shell</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">  <span class="nx">bootstrap</span><span class="p">.</span><span class="nx">disconnect</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">  <span class="c1">// ...
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="c1"></span><span class="p">});</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="nx">bootstrap</span><span class="p">.</span><span class="nx">observe</span><span class="p">(</span><span class="nb">document</span><span class="p">.</span><span class="nx">body</span><span class="p">,</span> <span class="p">{</span> <span class="nx">childList</span><span class="o">:</span> <span class="kc">true</span><span class="p">,</span> <span class="nx">subtree</span><span class="o">:</span> <span class="kc">true</span> <span class="p">});</span></span></span></code></pre></div><p><strong>選擇取決於 shell 是 server-render 還是 client-render</strong>：server-render 用 DOMContentLoaded、client-render 用 observer。</p>
<hr>
<h2 id="跟其他起點做法的關係">跟其他起點做法的關係</h2>
<p><a href="../dom-selector-precision/">#14 Selector 精準度</a> 的「起點」維度有四種做法：</p>
<table>
  <thead>
      <tr>
          <th>做法</th>
          <th>比較</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../pattern-document-query/">document query</a></td>
          <td>比本卡片簡潔、不防護未來變動</td>
      </tr>
      <tr>
          <td>本卡片：元件根變數</td>
          <td>多一行設定、隔離未來頁面變動</td>
      </tr>
      <tr>
          <td><a href="../pattern-root-as-parameter/">起點當參數</a></td>
          <td>比本卡片多支援多實例、設計成本前移</td>
      </tr>
      <tr>
          <td><a href="../pattern-closest-lookup/">closest 反向找根</a></td>
          <td>適合動態元件、不依賴變數綁定的時間</td>
      </tr>
  </tbody>
</table>
<p>預設用本卡片、需要多實例升級到「起點當參數」、需要動態升級到「closest」。</p>
<hr>
<h2 id="應用範例完整-setup">應用範例：完整 setup</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">function</span> <span class="nx">init</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="kd">var</span> <span class="nx">shell</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.search-shell&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">shell</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  <span class="kd">var</span> <span class="nx">ui</span>     <span class="o">=</span> <span class="nx">shell</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">  <span class="kd">var</span> <span class="nx">input</span>  <span class="o">=</span> <span class="nx">shell</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui__search-input&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  <span class="kd">var</span> <span class="nx">drawer</span> <span class="o">=</span> <span class="nx">shell</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui__drawer&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">input</span> <span class="o">||</span> <span class="o">!</span><span class="nx">drawer</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>  <span class="c1">// 元件未完整 mount
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"></span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">  <span class="nx">syncScopeHeight</span><span class="p">(</span><span class="nx">shell</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.search-scope&#39;</span><span class="p">));</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">  <span class="nx">setupFilterSlotSwap</span><span class="p">(</span><span class="nx">shell</span><span class="p">,</span> <span class="nx">drawer</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">  <span class="nx">setupScopeFilter</span><span class="p">(</span><span class="nx">shell</span><span class="p">,</span> <span class="nx">input</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="nb">document</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">&#39;DOMContentLoaded&#39;</span><span class="p">,</span> <span class="nx">init</span><span class="p">);</span></span></span></code></pre></div><p>shell 取一次、各 setup 函式從 shell 派生需要的子節點。</p>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該換做法嗎？</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>多函式共用同一 shell、各自重 query</td>
          <td>否 — 把 shell 提到 module scope 共用</td>
      </tr>
      <tr>
          <td>同頁面要支援多個 shell 實例</td>
          <td>是 — 升級到「起點當參數」</td>
      </tr>
      <tr>
          <td>元件可能在 runtime 動態出現 / 消失</td>
          <td>是 — 升級到「closest 反向」</td>
      </tr>
      <tr>
          <td>Shell 偶爾找不到（時序問題）</td>
          <td>否 — 加 MutationObserver bootstrap、做法不變</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：本 pattern 是 production 客製的預設、不是極致最佳化。當當前情境不複雜（一個元件、靜態 mount）、用本 pattern 即可；情境變複雜時再升級到對應做法。</p>
]]></content:encoded></item><item><title>Pattern：起點當函式參數</title><link>https://tarrragon.github.io/blog/report/pattern-root-as-parameter/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/pattern-root-as-parameter/</guid><description>&lt;h2 id="核心做法">核心做法&lt;/h2>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kd">function&lt;/span> &lt;span class="nx">setupSearchShell&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">shell&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">ui&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">shell&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.pagefind-ui&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">input&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">shell&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.pagefind-ui__search-input&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">drawer&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">shell&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.pagefind-ui__drawer&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> &lt;span class="c1">// ... 所有 query 從參數 shell 開始
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelectorAll&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.search-shell&amp;#39;&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">forEach&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">setupSearchShell&lt;/span>&lt;span class="p">);&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>元件根不在函式內 query、由呼叫者傳入。函式支援任意數量的元件實例。&lt;/p>
&lt;hr>
&lt;h2 id="這個做法存在的價值">這個做法存在的價值&lt;/h2>
&lt;p>兩件事：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>多實例支援免費&lt;/strong>：&lt;code>forEach(setup)&lt;/code> 自動處理多個 shell&lt;/li>
&lt;li>&lt;strong>純函式特性&lt;/strong>：函式行為只依賴參數、不依賴外部狀態 — 可單獨測試、可重用、副作用集中&lt;/li>
&lt;/ol>
&lt;p>跟&lt;a href="../pattern-component-root/">元件根變數&lt;/a>的關鍵差異：那個 pattern 假設「shell 唯一」、本 pattern 把這個假設外移到呼叫端、函式本身不假設。&lt;/p>
&lt;hr>
&lt;h2 id="適合的情境">適合的情境&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>情境&lt;/th>
 &lt;th>為什麼合理&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>同頁面有多個元件實例（多語切換、相關搜尋）&lt;/td>
 &lt;td>&lt;code>forEach&lt;/code> 自動覆蓋全部&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>元件設計成可被重用到其他頁面&lt;/td>
 &lt;td>沒有 hardcoded 依賴、容易移植&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>寫成函式庫 / 第三方 component&lt;/td>
 &lt;td>使用者可以對任意根節點呼叫&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>想單元測試函式行為&lt;/td>
 &lt;td>傳入 mock root 即可測試&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>核心特徵&lt;/strong>：把「shell 從哪來」這個責任明確交給呼叫端、函式自己不關心。&lt;/p>
&lt;hr>
&lt;h2 id="不適合的情境">不適合的情境&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>情境&lt;/th>
 &lt;th>為什麼過度工程&lt;/th>
 &lt;th>改用&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>確定全站只有一個元件實例&lt;/td>
 &lt;td>每函式多一個參數、收益不明顯&lt;/td>
 &lt;td>&lt;a href="../pattern-component-root/">元件根變數&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>元件動態增減、生命週期不可預測&lt;/td>
 &lt;td>forEach 只跑一次、無法捕捉後加的元件&lt;/td>
 &lt;td>&lt;a href="../pattern-closest-lookup/">closest 反向找根&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>一次性探索程式碼&lt;/td>
 &lt;td>純函式設計成本不值得&lt;/td>
 &lt;td>&lt;a href="../pattern-document-query/">document query&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="設計細節">設計細節&lt;/h2>
&lt;h3 id="函式簽名的設計">函式簽名的設計&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1">// 好：shell 是必填參數
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kd">function&lt;/span> &lt;span class="nx">setupSearchShell&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">shell&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="p">...&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="c1">// 較差：依賴外部變數
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kd">var&lt;/span> &lt;span class="nx">shell&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// module scope
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kd">function&lt;/span> &lt;span class="nx">setupSearchShell&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="c1">// 用了外部 shell
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="c1">// 更差：mega object
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kd">function&lt;/span> &lt;span class="nx">setupSearchShell&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">allElements&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">shell&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">allElements&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">shell&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// 不知道實際依賴什麼
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="c1">// ...
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>明確參數 = 明確依賴 = 容易測試、容易讀。&lt;/p>
&lt;h3 id="內部子函式也接受-shell">內部子函式也接受 shell&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="kd">function&lt;/span> &lt;span class="nx">setupSearchShell&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">shell&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl"> &lt;span class="nx">syncScopeHeight&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">shell&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &lt;span class="nx">setupFilterSlot&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">shell&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> &lt;span class="nx">setupScopeFilter&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">shell&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="kd">function&lt;/span> &lt;span class="nx">syncScopeHeight&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">shell&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">scope&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">shell&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.search-scope&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="c1">// ...
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>每層都明確接受 shell — 不依賴外層 closure。整套函式族都是純函式。&lt;/p>
&lt;h3 id="預先抓子節點-vs-每次重-query">預先抓子節點 vs 每次重 query&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1">// 方式 A：函式入口抓所有子節點
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kd">function&lt;/span> &lt;span class="nx">setupSearchShell&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">shell&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">els&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> &lt;span class="nx">ui&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">shell&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.pagefind-ui&amp;#39;&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="nx">input&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">shell&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.pagefind-ui__search-input&amp;#39;&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> &lt;span class="nx">drawer&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">shell&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.pagefind-ui__drawer&amp;#39;&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="p">};&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="c1">// 後續用 els.ui / els.input / els.drawer
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="c1">// 方式 B：各子函式自己 query
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kd">function&lt;/span> &lt;span class="nx">setupSearchShell&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">shell&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl"> &lt;span class="nx">syncScopeHeight&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">shell&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="c1">// 內部自己 querySelector
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="nx">setupFilterSlot&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">shell&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>A 比較有效率（只 query 一次）、B 比較解耦（子函式自包含）。&lt;strong>選 B 為預設、效能瓶頸時才考慮 A&lt;/strong>。&lt;/p>
&lt;hr>
&lt;h2 id="跟其他起點做法的關係">跟其他起點做法的關係&lt;/h2>
&lt;p>&lt;a href="../dom-selector-precision/">#14 Selector 精準度&lt;/a> 的「起點」維度有四種做法：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>做法&lt;/th>
 &lt;th>比較&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="../pattern-document-query/">document query&lt;/a>&lt;/td>
 &lt;td>比本卡片簡潔、無多實例支援&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="../pattern-component-root/">元件根變數&lt;/a>&lt;/td>
 &lt;td>比本卡片少一個參數、無多實例支援&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>本卡片：起點當參數&lt;/td>
 &lt;td>多實例支援、純函式、設計成本前移&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="../pattern-closest-lookup/">closest 反向找根&lt;/a>&lt;/td>
 &lt;td>比本卡片更動態、不依賴 forEach 時機&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>升級階梯：document → 元件根變數 → 起點當參數 → closest。複雜度遞增、能處理的情境也遞增。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心做法">核心做法</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">function</span> <span class="nx">setupSearchShell</span><span class="p">(</span><span class="nx">shell</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="kd">var</span> <span class="nx">ui</span>     <span class="o">=</span> <span class="nx">shell</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="kd">var</span> <span class="nx">input</span>  <span class="o">=</span> <span class="nx">shell</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui__search-input&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="kd">var</span> <span class="nx">drawer</span> <span class="o">=</span> <span class="nx">shell</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui__drawer&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">  <span class="c1">// ... 所有 query 從參數 shell 開始
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"></span><span class="p">}</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="nb">document</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span><span class="s1">&#39;.search-shell&#39;</span><span class="p">).</span><span class="nx">forEach</span><span class="p">(</span><span class="nx">setupSearchShell</span><span class="p">);</span></span></span></code></pre></div><p>元件根不在函式內 query、由呼叫者傳入。函式支援任意數量的元件實例。</p>
<hr>
<h2 id="這個做法存在的價值">這個做法存在的價值</h2>
<p>兩件事：</p>
<ol>
<li><strong>多實例支援免費</strong>：<code>forEach(setup)</code> 自動處理多個 shell</li>
<li><strong>純函式特性</strong>：函式行為只依賴參數、不依賴外部狀態 — 可單獨測試、可重用、副作用集中</li>
</ol>
<p>跟<a href="../pattern-component-root/">元件根變數</a>的關鍵差異：那個 pattern 假設「shell 唯一」、本 pattern 把這個假設外移到呼叫端、函式本身不假設。</p>
<hr>
<h2 id="適合的情境">適合的情境</h2>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>為什麼合理</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>同頁面有多個元件實例（多語切換、相關搜尋）</td>
          <td><code>forEach</code> 自動覆蓋全部</td>
      </tr>
      <tr>
          <td>元件設計成可被重用到其他頁面</td>
          <td>沒有 hardcoded 依賴、容易移植</td>
      </tr>
      <tr>
          <td>寫成函式庫 / 第三方 component</td>
          <td>使用者可以對任意根節點呼叫</td>
      </tr>
      <tr>
          <td>想單元測試函式行為</td>
          <td>傳入 mock root 即可測試</td>
      </tr>
  </tbody>
</table>
<p><strong>核心特徵</strong>：把「shell 從哪來」這個責任明確交給呼叫端、函式自己不關心。</p>
<hr>
<h2 id="不適合的情境">不適合的情境</h2>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>為什麼過度工程</th>
          <th>改用</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>確定全站只有一個元件實例</td>
          <td>每函式多一個參數、收益不明顯</td>
          <td><a href="../pattern-component-root/">元件根變數</a></td>
      </tr>
      <tr>
          <td>元件動態增減、生命週期不可預測</td>
          <td>forEach 只跑一次、無法捕捉後加的元件</td>
          <td><a href="../pattern-closest-lookup/">closest 反向找根</a></td>
      </tr>
      <tr>
          <td>一次性探索程式碼</td>
          <td>純函式設計成本不值得</td>
          <td><a href="../pattern-document-query/">document query</a></td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="設計細節">設計細節</h2>
<h3 id="函式簽名的設計">函式簽名的設計</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">// 好：shell 是必填參數
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="kd">function</span> <span class="nx">setupSearchShell</span><span class="p">(</span><span class="nx">shell</span><span class="p">)</span> <span class="p">{</span> <span class="p">...</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1">// 較差：依賴外部變數
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"></span><span class="kd">var</span> <span class="nx">shell</span><span class="p">;</span>  <span class="c1">// module scope
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"></span><span class="kd">function</span> <span class="nx">setupSearchShell</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  <span class="c1">// 用了外部 shell
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"></span><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1">// 更差：mega object
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"></span><span class="kd">function</span> <span class="nx">setupSearchShell</span><span class="p">(</span><span class="nx">allElements</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">  <span class="kd">var</span> <span class="nx">shell</span> <span class="o">=</span> <span class="nx">allElements</span><span class="p">.</span><span class="nx">shell</span><span class="p">;</span>  <span class="c1">// 不知道實際依賴什麼
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1"></span>  <span class="c1">// ...
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="c1"></span><span class="p">}</span></span></span></code></pre></div><p>明確參數 = 明確依賴 = 容易測試、容易讀。</p>
<h3 id="內部子函式也接受-shell">內部子函式也接受 shell</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">function</span> <span class="nx">setupSearchShell</span><span class="p">(</span><span class="nx">shell</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="nx">syncScopeHeight</span><span class="p">(</span><span class="nx">shell</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="nx">setupFilterSlot</span><span class="p">(</span><span class="nx">shell</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="nx">setupScopeFilter</span><span class="p">(</span><span class="nx">shell</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="kd">function</span> <span class="nx">syncScopeHeight</span><span class="p">(</span><span class="nx">shell</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  <span class="kd">var</span> <span class="nx">scope</span> <span class="o">=</span> <span class="nx">shell</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.search-scope&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  <span class="c1">// ...
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"></span><span class="p">}</span></span></span></code></pre></div><p>每層都明確接受 shell — 不依賴外層 closure。整套函式族都是純函式。</p>
<h3 id="預先抓子節點-vs-每次重-query">預先抓子節點 vs 每次重 query</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">// 方式 A：函式入口抓所有子節點
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="kd">function</span> <span class="nx">setupSearchShell</span><span class="p">(</span><span class="nx">shell</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="kd">var</span> <span class="nx">els</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="nx">ui</span><span class="o">:</span>     <span class="nx">shell</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui&#39;</span><span class="p">),</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="nx">input</span><span class="o">:</span>  <span class="nx">shell</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui__search-input&#39;</span><span class="p">),</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="nx">drawer</span><span class="o">:</span> <span class="nx">shell</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui__drawer&#39;</span><span class="p">),</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  <span class="p">};</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  <span class="c1">// 後續用 els.ui / els.input / els.drawer
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"></span><span class="p">}</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1">// 方式 B：各子函式自己 query
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1"></span><span class="kd">function</span> <span class="nx">setupSearchShell</span><span class="p">(</span><span class="nx">shell</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">  <span class="nx">syncScopeHeight</span><span class="p">(</span><span class="nx">shell</span><span class="p">);</span>  <span class="c1">// 內部自己 querySelector
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="c1"></span>  <span class="nx">setupFilterSlot</span><span class="p">(</span><span class="nx">shell</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>A 比較有效率（只 query 一次）、B 比較解耦（子函式自包含）。<strong>選 B 為預設、效能瓶頸時才考慮 A</strong>。</p>
<hr>
<h2 id="跟其他起點做法的關係">跟其他起點做法的關係</h2>
<p><a href="../dom-selector-precision/">#14 Selector 精準度</a> 的「起點」維度有四種做法：</p>
<table>
  <thead>
      <tr>
          <th>做法</th>
          <th>比較</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../pattern-document-query/">document query</a></td>
          <td>比本卡片簡潔、無多實例支援</td>
      </tr>
      <tr>
          <td><a href="../pattern-component-root/">元件根變數</a></td>
          <td>比本卡片少一個參數、無多實例支援</td>
      </tr>
      <tr>
          <td>本卡片：起點當參數</td>
          <td>多實例支援、純函式、設計成本前移</td>
      </tr>
      <tr>
          <td><a href="../pattern-closest-lookup/">closest 反向找根</a></td>
          <td>比本卡片更動態、不依賴 forEach 時機</td>
      </tr>
  </tbody>
</table>
<p>升級階梯：document → 元件根變數 → 起點當參數 → closest。複雜度遞增、能處理的情境也遞增。</p>
<hr>
<h2 id="應用範例多實例-setup">應用範例：多實例 setup</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// 頁面有 N 個 search-shell（例如多語版面切換）
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="nb">document</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span><span class="s1">&#39;.search-shell&#39;</span><span class="p">).</span><span class="nx">forEach</span><span class="p">(</span><span class="nx">setupSearchShell</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1">// 跑完之後：每個 shell 各自獨立 setup、互不干擾
</span></span></span></code></pre></div><p>當前頁只一個 shell、上面這行也適用 —<code>forEach</code> 對 1 個元素跑一次、跟 hardcode 單例沒差。<strong>做了多實例設計、未來不需要重寫</strong>。</p>
<hr>
<h2 id="應用範例單元測試">應用範例：單元測試</h2>
<p>純函式可以對 mock DOM 測試：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="nx">test</span><span class="p">(</span><span class="s1">&#39;setupSearchShell 把 filter 移到 sidebar&#39;</span><span class="p">,</span> <span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="kd">var</span> <span class="nx">shell</span> <span class="o">=</span> <span class="nx">createMockShell</span><span class="p">();</span>  <span class="c1">// 建立測試用 DOM
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span>  <span class="nx">setupSearchShell</span><span class="p">(</span><span class="nx">shell</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</span></span><span class="line"><span class="ln">5</span><span class="cl">  <span class="nx">expect</span><span class="p">(</span><span class="nx">shell</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.search-filter-slot&#39;</span><span class="p">).</span><span class="nx">children</span><span class="p">.</span><span class="nx">length</span><span class="p">).</span><span class="nx">toBe</span><span class="p">(</span><span class="mi">1</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="p">});</span></span></span></code></pre></div><p>不需要全頁面 mount、只需要 mock 一個 shell — 測試成本低。</p>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該套用本 pattern 嗎？</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>同頁要支援多個元件實例</td>
          <td>是 — 直接的好處</td>
      </tr>
      <tr>
          <td>想對函式寫單元測試</td>
          <td>是 — 純函式才好測</td>
      </tr>
      <tr>
          <td>函式內讀 module scope 變數</td>
          <td>是 — 改成參數讓依賴顯式</td>
      </tr>
      <tr>
          <td>確定永遠只一個實例、且不寫測試</td>
          <td>否 — <a href="../pattern-component-root/">元件根變數</a> 已夠</td>
      </tr>
      <tr>
          <td>元件實例 runtime 動態增減</td>
          <td>否 — 升級到 <a href="../pattern-closest-lookup/">closest</a></td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：本 pattern 把「我從哪取得 shell」的答案從函式內搬到呼叫端 — 換到「函式可重用」+「測試容易」+「多實例免費」三個收益、代價是函式簽名多一個參數。當前情境只一個實例也適用、未來擴展不需重寫。</p>
]]></content:encoded></item><item><title>Pattern：closest 反向找根</title><link>https://tarrragon.github.io/blog/report/pattern-closest-lookup/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/pattern-closest-lookup/</guid><description>&lt;h2 id="核心做法">核心做法&lt;/h2>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">addEventListener&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;click&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kd">function&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">e&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">shell&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">e&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">target&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">closest&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.search-shell&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="o">!&lt;/span>&lt;span class="nx">shell&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="k">return&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> &lt;span class="c1">// 在這個 shell 內處理
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="nx">handleSearchClick&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">shell&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">e&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="p">});&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>不在初始化時綁定 listener、而是頁面層級委派事件、事件處理時從 &lt;code>e.target&lt;/code> 反向找元件根。&lt;/p>
&lt;hr>
&lt;h2 id="這個做法存在的價值">這個做法存在的價值&lt;/h2>
&lt;p>把「找元件根」從「初始化時綁定」延後到「事件發生時動態判斷」 — 換到三個能力：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>元件動態增減免處理&lt;/strong>：新加的元件不需要重新綁 listener&lt;/li>
&lt;li>&lt;strong>多實例不需要 forEach setup&lt;/strong>：所有實例共用一個 listener&lt;/li>
&lt;li>&lt;strong>記憶體效率&lt;/strong>：N 個元件只綁 1 個 listener、不是 N 個&lt;/li>
&lt;/ol>
&lt;p>代價是事件處理邏輯多一層（每次都要 closest 反向找）。&lt;/p>
&lt;hr>
&lt;h2 id="適合的情境">適合的情境&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>情境&lt;/th>
 &lt;th>為什麼合理&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>SPA 路由切換、元件動態 mount/unmount&lt;/td>
 &lt;td>不需要在 mount 時重綁 listener&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>元件數量大（&amp;gt;10 個實例）&lt;/td>
 &lt;td>事件委派比每實例綁 listener 省記憶體&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>元件透過 AJAX 動態注入&lt;/td>
 &lt;td>注入後不需要任何 setup 動作&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>第三方 widget、不能控制元件生命週期&lt;/td>
 &lt;td>listener 綁在 document、跟 widget 解耦&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>核心特徵&lt;/strong>：元件的 mount 時機 / 數量 runtime 才知道、不是初始化時固定。&lt;/p>
&lt;hr>
&lt;h2 id="不適合的情境">不適合的情境&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>情境&lt;/th>
 &lt;th>為什麼過度工程&lt;/th>
 &lt;th>改用&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>元件靜態 mount、生命週期跟頁面一樣&lt;/td>
 &lt;td>委派多一層、收益不明顯&lt;/td>
 &lt;td>&lt;a href="../pattern-root-as-parameter/">起點當參數&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>一個元件實例、永不變動&lt;/td>
 &lt;td>完全沒必要&lt;/td>
 &lt;td>&lt;a href="../pattern-component-root/">元件根變數&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>需要在元件 mount 時就跑邏輯（不只回應事件）&lt;/td>
 &lt;td>closest 只在事件發生時跑、無法當 init hook&lt;/td>
 &lt;td>&lt;a href="../pattern-root-as-parameter/">起點當參數&lt;/a> + MutationObserver&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="設計細節">設計細節&lt;/h2>
&lt;h3 id="closest-失敗的處理">Closest 失敗的處理&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">addEventListener&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;click&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kd">function&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">e&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">shell&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">e&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">target&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">closest&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.search-shell&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="o">!&lt;/span>&lt;span class="nx">shell&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="k">return&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// 點擊不在任何 shell 內
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="c1">// ...
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="p">});&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>closest&lt;/code> 找不到時回 &lt;code>null&lt;/code>、提早 return 是必要防護。&lt;strong>沒這個 check 會在頁面其他地方點擊時報錯&lt;/strong>。&lt;/p>
&lt;h3 id="從-closest-結果再往下-query">從 closest 結果再往下 query&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kd">var&lt;/span> &lt;span class="nx">shell&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">e&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">target&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">closest&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.search-shell&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="kd">var&lt;/span> &lt;span class="nx">input&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">shell&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.pagefind-ui__search-input&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>closest&lt;/code> 找到 shell 後、可以從 shell 往下 query 同元件內的其他元素 — 這是「事件 + closest + 局部 query」的組合。&lt;/p>
&lt;h3 id="事件類型的選擇">事件類型的選擇&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>事件&lt;/th>
 &lt;th>適合&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;code>click&lt;/code>&lt;/td>
 &lt;td>點擊互動&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>input&lt;/code>&lt;/td>
 &lt;td>輸入框文字變動（需要 bubble）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>change&lt;/code>&lt;/td>
 &lt;td>選項變動（select / radio / checkbox）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>keydown&lt;/code>&lt;/td>
 &lt;td>鍵盤快捷鍵&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>focus&lt;/code> / &lt;code>blur&lt;/code>&lt;/td>
 &lt;td>焦點移動（不 bubble、要用 &lt;code>focusin&lt;/code> / &lt;code>focusout&lt;/code>）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>注意 &lt;code>focus&lt;/code> / &lt;code>blur&lt;/code> 不會 bubble — 事件委派要用 &lt;code>focusin&lt;/code> / &lt;code>focusout&lt;/code>。&lt;/p>
&lt;h3 id="委派的根節點選擇">委派的根節點選擇&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1">// 選項 1：document（最寬）
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">addEventListener&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;click&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">handler&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1">// 選項 2：特定容器（縮範圍）
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kd">var&lt;/span> &lt;span class="nx">pageContainer&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;main&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="nx">pageContainer&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">addEventListener&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;click&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">handler&lt;/span>&lt;span class="p">);&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>縮範圍的好處是「跟其他頁面區域的 listener 不互相干擾」。預設用 document、有干擾風險才縮。&lt;/p>
&lt;hr>
&lt;h2 id="跟其他起點做法的關係">跟其他起點做法的關係&lt;/h2>
&lt;p>&lt;a href="../dom-selector-precision/">#14 Selector 精準度&lt;/a> 的「起點」維度有四種做法：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>做法&lt;/th>
 &lt;th>比較&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="../pattern-document-query/">document query&lt;/a>&lt;/td>
 &lt;td>靜態、簡潔、無多實例支援&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="../pattern-component-root/">元件根變數&lt;/a>&lt;/td>
 &lt;td>靜態、shell 唯一假設&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="../pattern-root-as-parameter/">起點當參數&lt;/a>&lt;/td>
 &lt;td>靜態多實例、forEach 一次設定&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>本卡片：closest 反向找根&lt;/td>
 &lt;td>動態、事件驅動、無 init 時機綁定&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>複雜度遞增、能處理的動態程度也遞增。最動態的場景才用本 pattern。&lt;/p>
&lt;hr>
&lt;h2 id="應用範例跨多-shell-的-scope-filter">應用範例：跨多 shell 的 scope filter&lt;/h2>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="kd">function&lt;/span> &lt;span class="nx">setupGlobalScopeFilter&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl"> &lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">addEventListener&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;change&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kd">function&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">e&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">shell&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">e&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">target&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">closest&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.search-shell&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="o">!&lt;/span>&lt;span class="nx">shell&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="k">return&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">scope&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">e&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">target&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">closest&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.search-scope&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="o">!&lt;/span>&lt;span class="nx">scope&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="k">return&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// 不是 scope 控制的 change
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="nx">applyScope&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">shell&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">scope&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="nx">setupGlobalScopeFilter&lt;/span>&lt;span class="p">();&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>一個 listener 處理所有 shell 的 scope 變動 — 不論 shell 是初始 mount 的、還是 runtime 注入的。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心做法">核心做法</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="nb">document</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">&#39;click&#39;</span><span class="p">,</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="kd">var</span> <span class="nx">shell</span> <span class="o">=</span> <span class="nx">e</span><span class="p">.</span><span class="nx">target</span><span class="p">.</span><span class="nx">closest</span><span class="p">(</span><span class="s1">&#39;.search-shell&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">shell</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="c1">// 在這個 shell 內處理
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span>  <span class="nx">handleSearchClick</span><span class="p">(</span><span class="nx">shell</span><span class="p">,</span> <span class="nx">e</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="p">});</span></span></span></code></pre></div><p>不在初始化時綁定 listener、而是頁面層級委派事件、事件處理時從 <code>e.target</code> 反向找元件根。</p>
<hr>
<h2 id="這個做法存在的價值">這個做法存在的價值</h2>
<p>把「找元件根」從「初始化時綁定」延後到「事件發生時動態判斷」 — 換到三個能力：</p>
<ol>
<li><strong>元件動態增減免處理</strong>：新加的元件不需要重新綁 listener</li>
<li><strong>多實例不需要 forEach setup</strong>：所有實例共用一個 listener</li>
<li><strong>記憶體效率</strong>：N 個元件只綁 1 個 listener、不是 N 個</li>
</ol>
<p>代價是事件處理邏輯多一層（每次都要 closest 反向找）。</p>
<hr>
<h2 id="適合的情境">適合的情境</h2>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>為什麼合理</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>SPA 路由切換、元件動態 mount/unmount</td>
          <td>不需要在 mount 時重綁 listener</td>
      </tr>
      <tr>
          <td>元件數量大（&gt;10 個實例）</td>
          <td>事件委派比每實例綁 listener 省記憶體</td>
      </tr>
      <tr>
          <td>元件透過 AJAX 動態注入</td>
          <td>注入後不需要任何 setup 動作</td>
      </tr>
      <tr>
          <td>第三方 widget、不能控制元件生命週期</td>
          <td>listener 綁在 document、跟 widget 解耦</td>
      </tr>
  </tbody>
</table>
<p><strong>核心特徵</strong>：元件的 mount 時機 / 數量 runtime 才知道、不是初始化時固定。</p>
<hr>
<h2 id="不適合的情境">不適合的情境</h2>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>為什麼過度工程</th>
          <th>改用</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>元件靜態 mount、生命週期跟頁面一樣</td>
          <td>委派多一層、收益不明顯</td>
          <td><a href="../pattern-root-as-parameter/">起點當參數</a></td>
      </tr>
      <tr>
          <td>一個元件實例、永不變動</td>
          <td>完全沒必要</td>
          <td><a href="../pattern-component-root/">元件根變數</a></td>
      </tr>
      <tr>
          <td>需要在元件 mount 時就跑邏輯（不只回應事件）</td>
          <td>closest 只在事件發生時跑、無法當 init hook</td>
          <td><a href="../pattern-root-as-parameter/">起點當參數</a> + MutationObserver</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="設計細節">設計細節</h2>
<h3 id="closest-失敗的處理">Closest 失敗的處理</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="nb">document</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">&#39;click&#39;</span><span class="p">,</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="kd">var</span> <span class="nx">shell</span> <span class="o">=</span> <span class="nx">e</span><span class="p">.</span><span class="nx">target</span><span class="p">.</span><span class="nx">closest</span><span class="p">(</span><span class="s1">&#39;.search-shell&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">shell</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>  <span class="c1">// 點擊不在任何 shell 內
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"></span>  <span class="c1">// ...
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span><span class="p">});</span></span></span></code></pre></div><p><code>closest</code> 找不到時回 <code>null</code>、提早 return 是必要防護。<strong>沒這個 check 會在頁面其他地方點擊時報錯</strong>。</p>
<h3 id="從-closest-結果再往下-query">從 closest 結果再往下 query</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">var</span> <span class="nx">shell</span> <span class="o">=</span> <span class="nx">e</span><span class="p">.</span><span class="nx">target</span><span class="p">.</span><span class="nx">closest</span><span class="p">(</span><span class="s1">&#39;.search-shell&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="kd">var</span> <span class="nx">input</span> <span class="o">=</span> <span class="nx">shell</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui__search-input&#39;</span><span class="p">);</span></span></span></code></pre></div><p><code>closest</code> 找到 shell 後、可以從 shell 往下 query 同元件內的其他元素 — 這是「事件 + closest + 局部 query」的組合。</p>
<h3 id="事件類型的選擇">事件類型的選擇</h3>
<table>
  <thead>
      <tr>
          <th>事件</th>
          <th>適合</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>click</code></td>
          <td>點擊互動</td>
      </tr>
      <tr>
          <td><code>input</code></td>
          <td>輸入框文字變動（需要 bubble）</td>
      </tr>
      <tr>
          <td><code>change</code></td>
          <td>選項變動（select / radio / checkbox）</td>
      </tr>
      <tr>
          <td><code>keydown</code></td>
          <td>鍵盤快捷鍵</td>
      </tr>
      <tr>
          <td><code>focus</code> / <code>blur</code></td>
          <td>焦點移動（不 bubble、要用 <code>focusin</code> / <code>focusout</code>）</td>
      </tr>
  </tbody>
</table>
<p>注意 <code>focus</code> / <code>blur</code> 不會 bubble — 事件委派要用 <code>focusin</code> / <code>focusout</code>。</p>
<h3 id="委派的根節點選擇">委派的根節點選擇</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// 選項 1：document（最寬）
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="nb">document</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">&#39;click&#39;</span><span class="p">,</span> <span class="nx">handler</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1">// 選項 2：特定容器（縮範圍）
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span><span class="kd">var</span> <span class="nx">pageContainer</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;main&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="nx">pageContainer</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">&#39;click&#39;</span><span class="p">,</span> <span class="nx">handler</span><span class="p">);</span></span></span></code></pre></div><p>縮範圍的好處是「跟其他頁面區域的 listener 不互相干擾」。預設用 document、有干擾風險才縮。</p>
<hr>
<h2 id="跟其他起點做法的關係">跟其他起點做法的關係</h2>
<p><a href="../dom-selector-precision/">#14 Selector 精準度</a> 的「起點」維度有四種做法：</p>
<table>
  <thead>
      <tr>
          <th>做法</th>
          <th>比較</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../pattern-document-query/">document query</a></td>
          <td>靜態、簡潔、無多實例支援</td>
      </tr>
      <tr>
          <td><a href="../pattern-component-root/">元件根變數</a></td>
          <td>靜態、shell 唯一假設</td>
      </tr>
      <tr>
          <td><a href="../pattern-root-as-parameter/">起點當參數</a></td>
          <td>靜態多實例、forEach 一次設定</td>
      </tr>
      <tr>
          <td>本卡片：closest 反向找根</td>
          <td>動態、事件驅動、無 init 時機綁定</td>
      </tr>
  </tbody>
</table>
<p>複雜度遞增、能處理的動態程度也遞增。最動態的場景才用本 pattern。</p>
<hr>
<h2 id="應用範例跨多-shell-的-scope-filter">應用範例：跨多 shell 的 scope filter</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">function</span> <span class="nx">setupGlobalScopeFilter</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="nb">document</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">&#39;change&#39;</span><span class="p">,</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="kd">var</span> <span class="nx">shell</span> <span class="o">=</span> <span class="nx">e</span><span class="p">.</span><span class="nx">target</span><span class="p">.</span><span class="nx">closest</span><span class="p">(</span><span class="s1">&#39;.search-shell&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">shell</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="kd">var</span> <span class="nx">scope</span> <span class="o">=</span> <span class="nx">e</span><span class="p">.</span><span class="nx">target</span><span class="p">.</span><span class="nx">closest</span><span class="p">(</span><span class="s1">&#39;.search-scope&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">scope</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>  <span class="c1">// 不是 scope 控制的 change
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"></span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="nx">applyScope</span><span class="p">(</span><span class="nx">shell</span><span class="p">,</span> <span class="nx">scope</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">  <span class="p">});</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="nx">setupGlobalScopeFilter</span><span class="p">();</span></span></span></code></pre></div><p>一個 listener 處理所有 shell 的 scope 變動 — 不論 shell 是初始 mount 的、還是 runtime 注入的。</p>
<hr>
<h2 id="應用範例與-起點當參數-組合">應用範例：與 <a href="../pattern-root-as-parameter/">起點當參數</a> 組合</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">// 初始化階段：對已存在的 shell 做 setup
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="nb">document</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span><span class="s1">&#39;.search-shell&#39;</span><span class="p">).</span><span class="nx">forEach</span><span class="p">(</span><span class="nx">setupSearchShell</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1">// 事件階段：用 closest 處理可能新加的 shell
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"></span><span class="nb">document</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">&#39;click&#39;</span><span class="p">,</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">  <span class="kd">var</span> <span class="nx">shell</span> <span class="o">=</span> <span class="nx">e</span><span class="p">.</span><span class="nx">target</span><span class="p">.</span><span class="nx">closest</span><span class="p">(</span><span class="s1">&#39;.search-shell&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">shell</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  <span class="c1">// 處理事件、不論 shell 是初始的還是後加的
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"></span><span class="p">});</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1">// MutationObserver：捕捉新加的 shell 做 setup
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1"></span><span class="k">new</span> <span class="nx">MutationObserver</span><span class="p">(</span><span class="kd">function</span> <span class="p">(</span><span class="nx">mutations</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">  <span class="nx">mutations</span><span class="p">.</span><span class="nx">forEach</span><span class="p">(</span><span class="kd">function</span> <span class="p">(</span><span class="nx">m</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="nx">m</span><span class="p">.</span><span class="nx">addedNodes</span><span class="p">.</span><span class="nx">forEach</span><span class="p">(</span><span class="kd">function</span> <span class="p">(</span><span class="nx">node</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">      <span class="k">if</span> <span class="p">(</span><span class="nx">node</span><span class="p">.</span><span class="nx">matches</span> <span class="o">&amp;&amp;</span> <span class="nx">node</span><span class="p">.</span><span class="nx">matches</span><span class="p">(</span><span class="s1">&#39;.search-shell&#39;</span><span class="p">))</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">        <span class="nx">setupSearchShell</span><span class="p">(</span><span class="nx">node</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">      <span class="p">}</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">    <span class="p">});</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">  <span class="p">});</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="p">}).</span><span class="nx">observe</span><span class="p">(</span><span class="nb">document</span><span class="p">.</span><span class="nx">body</span><span class="p">,</span> <span class="p">{</span> <span class="nx">childList</span><span class="o">:</span> <span class="kc">true</span><span class="p">,</span> <span class="nx">subtree</span><span class="o">:</span> <span class="kc">true</span> <span class="p">});</span></span></span></code></pre></div><p>三個 pattern 組合：「靜態 setup」+「事件動態」+「mount 時 setup」 — 各 pattern 補不同時間點的需求。</p>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該套用本 pattern 嗎？</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>元件 SPA 路由動態切換</td>
          <td>是 — 直接對應使用情境</td>
      </tr>
      <tr>
          <td>元件數量大、每實例都要綁 listener</td>
          <td>是 — 委派省記憶體</td>
      </tr>
      <tr>
          <td>AJAX / Web Component runtime 注入</td>
          <td>是 — 不需要重綁</td>
      </tr>
      <tr>
          <td>確定元件靜態、生命週期固定</td>
          <td>否 — <a href="../pattern-root-as-parameter/">起點當參數</a> 已夠</td>
      </tr>
      <tr>
          <td>邏輯不是事件驅動（init 時就要跑）</td>
          <td>否 — closest 只在事件發生時跑</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：closest 反向找根把「定位元件」從綁定時延後到事件發生時 — 換到動態能力、付出的是事件處理多一層判斷。靜態場景用更簡單的做法、動態場景才升級到本 pattern。</p>
]]></content:encoded></item><item><title>Pattern：DOM attribute idempotency 標記</title><link>https://tarrragon.github.io/blog/report/pattern-attribute-idempotency-marker/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/pattern-attribute-idempotency-marker/</guid><description>&lt;h2 id="核心做法">核心做法&lt;/h2>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="nx">shell&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelectorAll&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.pagefind-ui__result:not([data-scoped])&amp;#39;&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">forEach&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kd">function&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">el&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="c1">// ... 處理
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="nx">el&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">setAttribute&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;data-scoped&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;true&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="p">});&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>apply 函式入口用 &lt;code>:not([data-x])&lt;/code> 過濾掉已處理元素、處理完後設 attribute 標記。下次 apply 被觸發時、已處理的元素不會被命中。&lt;/p>
&lt;hr>
&lt;h2 id="這個做法存在的價值">這個做法存在的價值&lt;/h2>
&lt;p>把「保證只處理一次」的責任從&lt;strong>呼叫端&lt;/strong>（要記得只呼叫一次）轉到&lt;strong>元素本身&lt;/strong>（看自己有沒有被處理過）。&lt;/p>
&lt;p>apply 函式可能被多個源觸發：&lt;/p>
&lt;ul>
&lt;li>初始化時呼叫&lt;/li>
&lt;li>MutationObserver 偵測到變動觸發&lt;/li>
&lt;li>使用者事件觸發&lt;/li>
&lt;li>Framework 重繪後重新呼叫&lt;/li>
&lt;/ul>
&lt;p>任一個源多呼叫就重複處理 — 無法靠呼叫端紀律避免。Idempotency 標記讓 apply 自己防護。&lt;/p>
&lt;hr>
&lt;h2 id="適合的情境">適合的情境&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>情境&lt;/th>
 &lt;th>為什麼合理&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Production apply 函式、可能被多源觸發&lt;/td>
 &lt;td>標記在元素上、不依賴呼叫紀律&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>處理動作有副作用（綁 listener、改 class）&lt;/td>
 &lt;td>重複觸發會疊加副作用&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>元素生命週期跟 attribute 同步（不會被 reset）&lt;/td>
 &lt;td>標記跟著元素走、自然清理&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Devtools debug 友善&lt;/td>
 &lt;td>attribute 在 inspector 可見&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>核心特徵&lt;/strong>：元素的 attribute 跟著元素 DOM 生命週期、元素移除時標記自動消失。&lt;/p>
&lt;hr>
&lt;h2 id="不適合的情境">不適合的情境&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>情境&lt;/th>
 &lt;th>為什麼不夠&lt;/th>
 &lt;th>改用&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>寫第三方 library&lt;/td>
 &lt;td>在使用者 DOM 加自家 attribute、有命名衝突風險&lt;/td>
 &lt;td>&lt;a href="../pattern-weakmap-idempotency-record/">WeakMap 紀錄&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Framework 重繪會清掉 attribute&lt;/td>
 &lt;td>標記消失、防護失效&lt;/td>
 &lt;td>配合 disconnect/observe 或改 WeakMap&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>需要週期性 reset 標記&lt;/td>
 &lt;td>attribute 改回需要遍歷所有元素&lt;/td>
 &lt;td>WeakMap 可整批 &lt;code>new WeakMap()&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>多種獨立的 idempotency 維度&lt;/td>
 &lt;td>DOM 上多 attribute 互相干擾&lt;/td>
 &lt;td>WeakMap 各別管理&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="設計細節">設計細節&lt;/h2>
&lt;h3 id="attribute-命名規範">Attribute 命名規範&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1">// 好：明確 namespace + 用途
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="nx">el&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">setAttribute&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;data-search-scoped&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;true&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="nx">el&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">setAttribute&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;data-myapp-processed&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;true&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1">// 較差：通用名、容易跟其他程式撞
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="nx">el&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">setAttribute&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;data-processed&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;true&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="nx">el&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">setAttribute&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;processed&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;true&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="c1">// 不是 data-* 開頭、可能不被 HTML spec 接受
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>預設用 &lt;code>data-{appname}-{purpose}&lt;/code> 格式 — 即使引入第三方 library 加 attribute、也不會撞名。&lt;/p>
&lt;h3 id="attribute-值的選擇">Attribute 值的選擇&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1">// 用法 1：固定 &amp;#39;true&amp;#39;（最簡）
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="nx">el&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">setAttribute&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;data-scoped&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;true&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="c1">// 用法 2：紀錄處理時間 / 版本（debug 友善）
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="nx">el&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">setAttribute&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;data-scoped&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nb">String&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nb">Date&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">now&lt;/span>&lt;span class="p">()));&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="nx">el&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">setAttribute&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;data-scoped&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;v2&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="c1">// 用法 3：boolean attribute（無值）
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="nx">el&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">setAttribute&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;data-scoped&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="c1">// CSS 用 [data-scoped] 即可選中
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>預設用 &lt;code>'true'&lt;/code>、debug 困難時改 timestamp 看處理順序。&lt;/p>
&lt;h3 id="跟-framework-重繪共處">跟 framework 重繪共處&lt;/h3>
&lt;p>Svelte / React / Vue 重繪元素時、&lt;strong>自家 attribute 通常會被保留&lt;/strong>（framework 只管自己的 attribute）— 但有例外：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>情境&lt;/th>
 &lt;th>行為&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Framework re-render 整段 DOM&lt;/td>
 &lt;td>元素被替換、新元素沒標記 → apply 重跑、合理&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Framework patch 既有元素 attribute&lt;/td>
 &lt;td>自家 attribute 保留&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Framework &lt;code>replaceWith&lt;/code> / &lt;code>innerHTML&lt;/code> 重設&lt;/td>
 &lt;td>元素被替換 → 標記消失、apply 重跑、合理&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>核心觀察&lt;/strong>：自家 attribute 跟著元素走 — 元素還在就有、元素被換就沒。這是「正確」行為、不是 bug。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心做法">核心做法</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="nx">shell</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui__result:not([data-scoped])&#39;</span><span class="p">).</span><span class="nx">forEach</span><span class="p">(</span><span class="kd">function</span> <span class="p">(</span><span class="nx">el</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="c1">// ... 處理
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span>  <span class="nx">el</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="s1">&#39;data-scoped&#39;</span><span class="p">,</span> <span class="s1">&#39;true&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="p">});</span></span></span></code></pre></div><p>apply 函式入口用 <code>:not([data-x])</code> 過濾掉已處理元素、處理完後設 attribute 標記。下次 apply 被觸發時、已處理的元素不會被命中。</p>
<hr>
<h2 id="這個做法存在的價值">這個做法存在的價值</h2>
<p>把「保證只處理一次」的責任從<strong>呼叫端</strong>（要記得只呼叫一次）轉到<strong>元素本身</strong>（看自己有沒有被處理過）。</p>
<p>apply 函式可能被多個源觸發：</p>
<ul>
<li>初始化時呼叫</li>
<li>MutationObserver 偵測到變動觸發</li>
<li>使用者事件觸發</li>
<li>Framework 重繪後重新呼叫</li>
</ul>
<p>任一個源多呼叫就重複處理 — 無法靠呼叫端紀律避免。Idempotency 標記讓 apply 自己防護。</p>
<hr>
<h2 id="適合的情境">適合的情境</h2>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>為什麼合理</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Production apply 函式、可能被多源觸發</td>
          <td>標記在元素上、不依賴呼叫紀律</td>
      </tr>
      <tr>
          <td>處理動作有副作用（綁 listener、改 class）</td>
          <td>重複觸發會疊加副作用</td>
      </tr>
      <tr>
          <td>元素生命週期跟 attribute 同步（不會被 reset）</td>
          <td>標記跟著元素走、自然清理</td>
      </tr>
      <tr>
          <td>Devtools debug 友善</td>
          <td>attribute 在 inspector 可見</td>
      </tr>
  </tbody>
</table>
<p><strong>核心特徵</strong>：元素的 attribute 跟著元素 DOM 生命週期、元素移除時標記自動消失。</p>
<hr>
<h2 id="不適合的情境">不適合的情境</h2>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>為什麼不夠</th>
          <th>改用</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>寫第三方 library</td>
          <td>在使用者 DOM 加自家 attribute、有命名衝突風險</td>
          <td><a href="../pattern-weakmap-idempotency-record/">WeakMap 紀錄</a></td>
      </tr>
      <tr>
          <td>Framework 重繪會清掉 attribute</td>
          <td>標記消失、防護失效</td>
          <td>配合 disconnect/observe 或改 WeakMap</td>
      </tr>
      <tr>
          <td>需要週期性 reset 標記</td>
          <td>attribute 改回需要遍歷所有元素</td>
          <td>WeakMap 可整批 <code>new WeakMap()</code></td>
      </tr>
      <tr>
          <td>多種獨立的 idempotency 維度</td>
          <td>DOM 上多 attribute 互相干擾</td>
          <td>WeakMap 各別管理</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="設計細節">設計細節</h2>
<h3 id="attribute-命名規範">Attribute 命名規範</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// 好：明確 namespace + 用途
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="nx">el</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="s1">&#39;data-search-scoped&#39;</span><span class="p">,</span> <span class="s1">&#39;true&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nx">el</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="s1">&#39;data-myapp-processed&#39;</span><span class="p">,</span> <span class="s1">&#39;true&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1">// 較差：通用名、容易跟其他程式撞
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"></span><span class="nx">el</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="s1">&#39;data-processed&#39;</span><span class="p">,</span> <span class="s1">&#39;true&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="nx">el</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="s1">&#39;processed&#39;</span><span class="p">,</span> <span class="s1">&#39;true&#39;</span><span class="p">);</span>  <span class="c1">// 不是 data-* 開頭、可能不被 HTML spec 接受
</span></span></span></code></pre></div><p>預設用 <code>data-{appname}-{purpose}</code> 格式 — 即使引入第三方 library 加 attribute、也不會撞名。</p>
<h3 id="attribute-值的選擇">Attribute 值的選擇</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">// 用法 1：固定 &#39;true&#39;（最簡）
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="nx">el</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="s1">&#39;data-scoped&#39;</span><span class="p">,</span> <span class="s1">&#39;true&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1">// 用法 2：紀錄處理時間 / 版本（debug 友善）
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"></span><span class="nx">el</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="s1">&#39;data-scoped&#39;</span><span class="p">,</span> <span class="nb">String</span><span class="p">(</span><span class="nb">Date</span><span class="p">.</span><span class="nx">now</span><span class="p">()));</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="nx">el</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="s1">&#39;data-scoped&#39;</span><span class="p">,</span> <span class="s1">&#39;v2&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1">// 用法 3：boolean attribute（無值）
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"></span><span class="nx">el</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="s1">&#39;data-scoped&#39;</span><span class="p">,</span> <span class="s1">&#39;&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1">// CSS 用 [data-scoped] 即可選中
</span></span></span></code></pre></div><p>預設用 <code>'true'</code>、debug 困難時改 timestamp 看處理順序。</p>
<h3 id="跟-framework-重繪共處">跟 framework 重繪共處</h3>
<p>Svelte / React / Vue 重繪元素時、<strong>自家 attribute 通常會被保留</strong>（framework 只管自己的 attribute）— 但有例外：</p>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>行為</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Framework re-render 整段 DOM</td>
          <td>元素被替換、新元素沒標記 → apply 重跑、合理</td>
      </tr>
      <tr>
          <td>Framework patch 既有元素 attribute</td>
          <td>自家 attribute 保留</td>
      </tr>
      <tr>
          <td>Framework <code>replaceWith</code> / <code>innerHTML</code> 重設</td>
          <td>元素被替換 → 標記消失、apply 重跑、合理</td>
      </tr>
  </tbody>
</table>
<p><strong>核心觀察</strong>：自家 attribute 跟著元素走 — 元素還在就有、元素被換就沒。這是「正確」行為、不是 bug。</p>
<h3 id="例外framework-主動清自家-attribute">例外：framework 主動清自家 attribute</h3>
<p>少數 framework 會 strict 清非預期的 attribute（例如某些 Web Component lib）。檢查方式：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="nx">el</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="s1">&#39;data-scoped&#39;</span><span class="p">,</span> <span class="s1">&#39;true&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1">// ... 等 framework patch 一次後
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span><span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">el</span><span class="p">.</span><span class="nx">getAttribute</span><span class="p">(</span><span class="s1">&#39;data-scoped&#39;</span><span class="p">));</span>  <span class="c1">// 還在嗎？
</span></span></span></code></pre></div><p>如果消失、改用 <a href="../pattern-weakmap-idempotency-record/">WeakMap 紀錄</a>。</p>
<hr>
<h2 id="跟其他-idempotency-做法的關係">跟其他 idempotency 做法的關係</h2>
<p><a href="../dom-selector-precision/">#14 Selector 精準度</a> 的「過濾」維度有三種做法：</p>
<table>
  <thead>
      <tr>
          <th>做法</th>
          <th>比較</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>本卡片：DOM attribute 標記</td>
          <td>production 預設、devtools 可見、有命名衝突風險</td>
      </tr>
      <tr>
          <td><a href="../pattern-weakmap-idempotency-record/">WeakMap 紀錄</a></td>
          <td>不污染 DOM、適合 library、debug 不便</td>
      </tr>
      <tr>
          <td>依賴外部呼叫者保證</td>
          <td>反模式、無防護、不可靠</td>
      </tr>
  </tbody>
</table>
<p>預設用本卡片、第三方 library / framework 衝突情境升級到 WeakMap。</p>
<hr>
<h2 id="應用範例完整-apply">應用範例：完整 apply</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">function</span> <span class="nx">apply</span><span class="p">(</span><span class="nx">shell</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="kd">var</span> <span class="nx">newResults</span> <span class="o">=</span> <span class="nx">shell</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="s1">&#39;.pagefind-ui__result:not([data-search-scoped])&#39;</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="p">);</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">  <span class="nx">newResults</span><span class="p">.</span><span class="nx">forEach</span><span class="p">(</span><span class="kd">function</span> <span class="p">(</span><span class="nx">el</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="nx">bindClickHandler</span><span class="p">(</span><span class="nx">el</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="nx">addCustomBadge</span><span class="p">(</span><span class="nx">el</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="nx">el</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="s1">&#39;data-search-scoped&#39;</span><span class="p">,</span> <span class="s1">&#39;true&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">  <span class="p">});</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1">// 多源觸發都安全
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="c1"></span><span class="nx">init</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">&#39;click&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">=&gt;</span> <span class="nx">apply</span><span class="p">(</span><span class="nx">shell</span><span class="p">));</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="nx">observer</span><span class="p">.</span><span class="nx">observe</span><span class="p">(</span><span class="nx">shell</span><span class="p">,</span> <span class="p">...);</span>  <span class="c1">// 觀察到變動觸發 apply
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="c1"></span><span class="nx">apply</span><span class="p">(</span><span class="nx">shell</span><span class="p">);</span>  <span class="c1">// 初始化時跑一次
</span></span></span></code></pre></div><p>三個觸發點任一個多跑、<code>:not([data-search-scoped])</code> 都會過濾掉已處理元素。</p>
<hr>
<h2 id="應用範例多維度標記">應用範例：多維度標記</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">// 三個獨立 idempotency 維度、各自 attribute
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="nx">el</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="s1">&#39;data-search-scoped&#39;</span><span class="p">,</span> <span class="s1">&#39;true&#39;</span><span class="p">);</span>     <span class="c1">// scope filter 處理過
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1"></span><span class="nx">el</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="s1">&#39;data-search-bound&#39;</span><span class="p">,</span> <span class="s1">&#39;true&#39;</span><span class="p">);</span>      <span class="c1">// event listener 綁過
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1"></span><span class="nx">el</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="s1">&#39;data-search-decorated&#39;</span><span class="p">,</span> <span class="s1">&#39;true&#39;</span><span class="p">);</span>  <span class="c1">// 視覺裝飾加過
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"></span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1">// 各 apply 函式只看自己的 attribute
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"></span><span class="kd">function</span> <span class="nx">applyScope</span><span class="p">(</span><span class="nx">shell</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  <span class="nx">shell</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span><span class="s1">&#39;.x:not([data-search-scoped])&#39;</span><span class="p">).</span><span class="nx">forEach</span><span class="p">(...)</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="kd">function</span> <span class="nx">applyBindings</span><span class="p">(</span><span class="nx">shell</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">  <span class="nx">shell</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span><span class="s1">&#39;.x:not([data-search-bound])&#39;</span><span class="p">).</span><span class="nx">forEach</span><span class="p">(...)</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>每個 idempotency 維度獨立 — 互相不干擾。</p>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該套用本 pattern 嗎？</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Apply 被多源觸發、產生重複處理 bug</td>
          <td>是 — 直接對應使用情境</td>
      </tr>
      <tr>
          <td>寫第三方 library / 不能污染 DOM</td>
          <td>否 — 改 <a href="../pattern-weakmap-idempotency-record/">WeakMap</a></td>
      </tr>
      <tr>
          <td>Framework 會清自家 attribute</td>
          <td>否 — 改 WeakMap</td>
      </tr>
      <tr>
          <td>想在 devtools inspector 直接看處理狀態</td>
          <td>是 — attribute 可見性是優點</td>
      </tr>
      <tr>
          <td>同元素多種 idempotency 維度</td>
          <td>是 — 多 attribute 各自管理</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：把 idempotency 責任從呼叫端搬到元素本身、attribute 是「便宜可見的旗標」。Production apply 預設用本 pattern、特殊情境（library / framework 衝突）才升級到 WeakMap。</p>
]]></content:encoded></item><item><title>Pattern：WeakMap idempotency 紀錄</title><link>https://tarrragon.github.io/blog/report/pattern-weakmap-idempotency-record/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/pattern-weakmap-idempotency-record/</guid><description>&lt;h2 id="核心做法">核心做法&lt;/h2>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kd">var&lt;/span> &lt;span class="nx">processed&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">WeakMap&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="nx">shell&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelectorAll&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.pagefind-ui__result&amp;#39;&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">forEach&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kd">function&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">el&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">processed&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">has&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">el&lt;/span>&lt;span class="p">))&lt;/span> &lt;span class="k">return&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> &lt;span class="c1">// ... 處理
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="nx">processed&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">set&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">el&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="p">});&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>把「已處理」狀態紀錄在 JS 的 WeakMap 裡、不寫到 DOM 上。WeakMap key 是元素本身、元素被 GC 時自動清理。&lt;/p>
&lt;hr>
&lt;h2 id="這個做法存在的價值">這個做法存在的價值&lt;/h2>
&lt;p>兩件事 &lt;a href="../pattern-attribute-idempotency-marker/">DOM attribute 標記&lt;/a> 做不到：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>不污染 DOM&lt;/strong>：使用者 DOM 不會被加自家 attribute、適合第三方 library&lt;/li>
&lt;li>&lt;strong>跟 framework 完全解耦&lt;/strong>：framework 怎麼操作 DOM 都不影響 WeakMap 紀錄&lt;/li>
&lt;/ol>
&lt;p>代價是 debug 不便（看不到狀態）、紀錄跟 JS context 綁定（換頁就消失）。&lt;/p>
&lt;hr>
&lt;h2 id="適合的情境">適合的情境&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>情境&lt;/th>
 &lt;th>為什麼合理&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>寫第三方 library / npm package&lt;/td>
 &lt;td>不在使用者 DOM 加 attribute、避免命名衝突&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Framework 會清非預期的 attribute&lt;/td>
 &lt;td>WeakMap 不在 DOM、framework 動不到&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>需要週期性 reset 紀錄&lt;/td>
 &lt;td>&lt;code>processed = new WeakMap()&lt;/code> 一行重置全部&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>紀錄複雜資料、不只是 boolean&lt;/td>
 &lt;td>WeakMap value 可以是任何物件&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>核心特徵&lt;/strong>：紀錄獨立於 DOM 之外、跟 JS 物件 lifetime 綁定。&lt;/p>
&lt;hr>
&lt;h2 id="不適合的情境">不適合的情境&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>情境&lt;/th>
 &lt;th>為什麼不夠&lt;/th>
 &lt;th>改用&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>自家 application、devtools debug 重要&lt;/td>
 &lt;td>看不到狀態、debug 困難&lt;/td>
 &lt;td>&lt;a href="../pattern-attribute-idempotency-marker/">DOM attribute 標記&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>跨頁面 / 跨 session 的 idempotency&lt;/td>
 &lt;td>WeakMap 在 JS context 內、換頁就消失&lt;/td>
 &lt;td>LocalStorage / 後端紀錄&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>元素生命週期短、頻繁 GC&lt;/td>
 &lt;td>WeakMap 自動清理可能比預期早&lt;/td>
 &lt;td>改用 Map（但要手動清理）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>紀錄要跟 SSR 同步&lt;/td>
 &lt;td>WeakMap 只活在 client&lt;/td>
 &lt;td>結合 attribute（SSR 階段標記）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="設計細節">設計細節&lt;/h2>
&lt;h3 id="為什麼用-weakmap-不用-map--set">為什麼用 WeakMap 不用 Map / Set&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1">// WeakMap：key 是元素、元素被 GC 時 entry 自動消失
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kd">var&lt;/span> &lt;span class="nx">processedW&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">WeakMap&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="nx">processedW&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">set&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">el&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1">// el 從 DOM 移除 + 沒其他 reference → GC → WeakMap entry 消失
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="c1">// Map / Set：強引用、阻止 GC
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kd">var&lt;/span> &lt;span class="nx">processedS&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">Set&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="nx">processedS&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">add&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">el&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">9&lt;/span>&lt;span class="cl">&lt;span class="c1">// el 從 DOM 移除、但 Set 還抓著 → 永久 leak
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>DOM 元素可能動態移除（filter、SPA 路由切換、framework 重繪）— Map / Set 會造成 memory leak。&lt;strong>處理 DOM 元素 idempotency 預設用 WeakMap&lt;/strong>。&lt;/p>
&lt;h3 id="value-的設計">Value 的設計&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1">// 用法 1：純 boolean（最簡）
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="nx">processed&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">set&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">el&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="c1">// 用法 2：紀錄處理版本（升級時偵測 stale 紀錄）
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="nx">processed&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">set&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">el&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">version&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mi">2&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">time&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nb">Date&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">now&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">processed&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">has&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">el&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="nx">processed&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">el&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">version&lt;/span> &lt;span class="o">===&lt;/span> &lt;span class="nx">currentVersion&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="k">return&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="c1">// 用法 3：紀錄相關 metadata（避免重複查詢）
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="nx">processed&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">set&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">el&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> &lt;span class="nx">bindingsId&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">registerListener&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">el&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl"> &lt;span class="nx">initialClass&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">el&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">className&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="p">});&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>WeakMap value 可以儲任何資料 — 比 attribute（只能存字串）更彈性。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心做法">核心做法</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">var</span> <span class="nx">processed</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">WeakMap</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nx">shell</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui__result&#39;</span><span class="p">).</span><span class="nx">forEach</span><span class="p">(</span><span class="kd">function</span> <span class="p">(</span><span class="nx">el</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="nx">processed</span><span class="p">.</span><span class="nx">has</span><span class="p">(</span><span class="nx">el</span><span class="p">))</span> <span class="k">return</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">  <span class="c1">// ... 處理
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"></span>  <span class="nx">processed</span><span class="p">.</span><span class="nx">set</span><span class="p">(</span><span class="nx">el</span><span class="p">,</span> <span class="kc">true</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="p">});</span></span></span></code></pre></div><p>把「已處理」狀態紀錄在 JS 的 WeakMap 裡、不寫到 DOM 上。WeakMap key 是元素本身、元素被 GC 時自動清理。</p>
<hr>
<h2 id="這個做法存在的價值">這個做法存在的價值</h2>
<p>兩件事 <a href="../pattern-attribute-idempotency-marker/">DOM attribute 標記</a> 做不到：</p>
<ol>
<li><strong>不污染 DOM</strong>：使用者 DOM 不會被加自家 attribute、適合第三方 library</li>
<li><strong>跟 framework 完全解耦</strong>：framework 怎麼操作 DOM 都不影響 WeakMap 紀錄</li>
</ol>
<p>代價是 debug 不便（看不到狀態）、紀錄跟 JS context 綁定（換頁就消失）。</p>
<hr>
<h2 id="適合的情境">適合的情境</h2>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>為什麼合理</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>寫第三方 library / npm package</td>
          <td>不在使用者 DOM 加 attribute、避免命名衝突</td>
      </tr>
      <tr>
          <td>Framework 會清非預期的 attribute</td>
          <td>WeakMap 不在 DOM、framework 動不到</td>
      </tr>
      <tr>
          <td>需要週期性 reset 紀錄</td>
          <td><code>processed = new WeakMap()</code> 一行重置全部</td>
      </tr>
      <tr>
          <td>紀錄複雜資料、不只是 boolean</td>
          <td>WeakMap value 可以是任何物件</td>
      </tr>
  </tbody>
</table>
<p><strong>核心特徵</strong>：紀錄獨立於 DOM 之外、跟 JS 物件 lifetime 綁定。</p>
<hr>
<h2 id="不適合的情境">不適合的情境</h2>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>為什麼不夠</th>
          <th>改用</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>自家 application、devtools debug 重要</td>
          <td>看不到狀態、debug 困難</td>
          <td><a href="../pattern-attribute-idempotency-marker/">DOM attribute 標記</a></td>
      </tr>
      <tr>
          <td>跨頁面 / 跨 session 的 idempotency</td>
          <td>WeakMap 在 JS context 內、換頁就消失</td>
          <td>LocalStorage / 後端紀錄</td>
      </tr>
      <tr>
          <td>元素生命週期短、頻繁 GC</td>
          <td>WeakMap 自動清理可能比預期早</td>
          <td>改用 Map（但要手動清理）</td>
      </tr>
      <tr>
          <td>紀錄要跟 SSR 同步</td>
          <td>WeakMap 只活在 client</td>
          <td>結合 attribute（SSR 階段標記）</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="設計細節">設計細節</h2>
<h3 id="為什麼用-weakmap-不用-map--set">為什麼用 WeakMap 不用 Map / Set</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// WeakMap：key 是元素、元素被 GC 時 entry 自動消失
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="kd">var</span> <span class="nx">processedW</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">WeakMap</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nx">processedW</span><span class="p">.</span><span class="nx">set</span><span class="p">(</span><span class="nx">el</span><span class="p">,</span> <span class="kc">true</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1">// el 從 DOM 移除 + 沒其他 reference → GC → WeakMap entry 消失
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1">// Map / Set：強引用、阻止 GC
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1"></span><span class="kd">var</span> <span class="nx">processedS</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Set</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="nx">processedS</span><span class="p">.</span><span class="nx">add</span><span class="p">(</span><span class="nx">el</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="c1">// el 從 DOM 移除、但 Set 還抓著 → 永久 leak
</span></span></span></code></pre></div><p>DOM 元素可能動態移除（filter、SPA 路由切換、framework 重繪）— Map / Set 會造成 memory leak。<strong>處理 DOM 元素 idempotency 預設用 WeakMap</strong>。</p>
<h3 id="value-的設計">Value 的設計</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">// 用法 1：純 boolean（最簡）
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="nx">processed</span><span class="p">.</span><span class="nx">set</span><span class="p">(</span><span class="nx">el</span><span class="p">,</span> <span class="kc">true</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1">// 用法 2：紀錄處理版本（升級時偵測 stale 紀錄）
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"></span><span class="nx">processed</span><span class="p">.</span><span class="nx">set</span><span class="p">(</span><span class="nx">el</span><span class="p">,</span> <span class="p">{</span> <span class="nx">version</span><span class="o">:</span> <span class="mi">2</span><span class="p">,</span> <span class="nx">time</span><span class="o">:</span> <span class="nb">Date</span><span class="p">.</span><span class="nx">now</span><span class="p">()</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="k">if</span> <span class="p">(</span><span class="nx">processed</span><span class="p">.</span><span class="nx">has</span><span class="p">(</span><span class="nx">el</span><span class="p">)</span> <span class="o">&amp;&amp;</span> <span class="nx">processed</span><span class="p">.</span><span class="nx">get</span><span class="p">(</span><span class="nx">el</span><span class="p">).</span><span class="nx">version</span> <span class="o">===</span> <span class="nx">currentVersion</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1">// 用法 3：紀錄相關 metadata（避免重複查詢）
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"></span><span class="nx">processed</span><span class="p">.</span><span class="nx">set</span><span class="p">(</span><span class="nx">el</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">  <span class="nx">bindingsId</span><span class="o">:</span> <span class="nx">registerListener</span><span class="p">(</span><span class="nx">el</span><span class="p">),</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">  <span class="nx">initialClass</span><span class="o">:</span> <span class="nx">el</span><span class="p">.</span><span class="nx">className</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="p">});</span></span></span></code></pre></div><p>WeakMap value 可以儲任何資料 — 比 attribute（只能存字串）更彈性。</p>
<h3 id="debug-替代方案">Debug 替代方案</h3>
<p>attribute 標記可以在 devtools inspector 直接看；WeakMap 看不到。debug 時的替代：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// 開發模式同步寫一份 attribute（production build 時拿掉）
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="kd">function</span> <span class="nx">markProcessed</span><span class="p">(</span><span class="nx">el</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="nx">processed</span><span class="p">.</span><span class="nx">set</span><span class="p">(</span><span class="nx">el</span><span class="p">,</span> <span class="kc">true</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="nx">DEV_MODE</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="nx">el</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="s1">&#39;data-debug-processed&#39;</span><span class="p">,</span> <span class="s1">&#39;true&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>或暴露到 console：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="nb">window</span><span class="p">.</span><span class="nx">__debug_processed</span> <span class="o">=</span> <span class="nx">processed</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1">// console: __debug_processed.has($0)  // 檢查當前選中元素
</span></span></span></code></pre></div><p>這些都是 workaround、不如 attribute 標記直觀。<strong>選 WeakMap 的人通常已經接受這個 debug 成本</strong>。</p>
<h3 id="reset-紀錄">Reset 紀錄</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// WeakMap 整批 reset
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="nx">processed</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">WeakMap</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1">// 對比 attribute 整批 reset 要遍歷
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span><span class="nx">shell</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span><span class="s1">&#39;[data-scoped]&#39;</span><span class="p">).</span><span class="nx">forEach</span><span class="p">(</span><span class="nx">el</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">  <span class="nx">el</span><span class="p">.</span><span class="nx">removeAttribute</span><span class="p">(</span><span class="s1">&#39;data-scoped&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="p">});</span></span></span></code></pre></div><p>需要週期性 reset（例如 user 切換 mode、所有元素該重新處理）— WeakMap 一行解決、attribute 要遍歷。</p>
<hr>
<h2 id="跟其他-idempotency-做法的關係">跟其他 idempotency 做法的關係</h2>
<p><a href="../dom-selector-precision/">#14 Selector 精準度</a> 的「過濾」維度有三種做法：</p>
<table>
  <thead>
      <tr>
          <th>做法</th>
          <th>比較</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../pattern-attribute-idempotency-marker/">DOM attribute 標記</a></td>
          <td>production 預設、devtools 可見、有命名衝突風險</td>
      </tr>
      <tr>
          <td>本卡片：WeakMap 紀錄</td>
          <td>不污染 DOM、適合 library、debug 不便</td>
      </tr>
      <tr>
          <td>依賴外部呼叫者保證</td>
          <td>反模式、無防護</td>
      </tr>
  </tbody>
</table>
<p>選擇順序：<strong>自家 application</strong> → attribute；<strong>library / framework 衝突</strong> → WeakMap；<strong>反模式不選</strong>。</p>
<hr>
<h2 id="應用範例library-設計">應用範例：library 設計</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">// 第三方 library export 的 init 函式
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="kd">function</span> <span class="nx">initSearchEnhancement</span><span class="p">(</span><span class="nx">shell</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="kd">var</span> <span class="nx">processed</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">WeakMap</span><span class="p">();</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  <span class="kd">function</span> <span class="nx">apply</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="nx">shell</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span><span class="s1">&#39;.search-result&#39;</span><span class="p">).</span><span class="nx">forEach</span><span class="p">(</span><span class="kd">function</span> <span class="p">(</span><span class="nx">el</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">      <span class="k">if</span> <span class="p">(</span><span class="nx">processed</span><span class="p">.</span><span class="nx">has</span><span class="p">(</span><span class="nx">el</span><span class="p">))</span> <span class="k">return</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">      <span class="nx">enhanceResult</span><span class="p">(</span><span class="nx">el</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">      <span class="nx">processed</span><span class="p">.</span><span class="nx">set</span><span class="p">(</span><span class="nx">el</span><span class="p">,</span> <span class="kc">true</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="p">});</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">
</span></span><span class="line"><span class="ln">13</span><span class="cl">  <span class="nx">apply</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">  <span class="k">new</span> <span class="nx">MutationObserver</span><span class="p">(</span><span class="nx">apply</span><span class="p">).</span><span class="nx">observe</span><span class="p">(</span><span class="nx">shell</span><span class="p">,</span> <span class="p">{</span> <span class="nx">childList</span><span class="o">:</span> <span class="kc">true</span><span class="p">,</span> <span class="nx">subtree</span><span class="o">:</span> <span class="kc">true</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">
</span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="c1">// 使用者：
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="c1"></span><span class="nx">initSearchEnhancement</span><span class="p">(</span><span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.my-search&#39;</span><span class="p">));</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="c1">// 不會在使用者 DOM 上加任何 data-* attribute
</span></span></span></code></pre></div><p>使用者 DOM 完全乾淨、library 行為內聚。</p>
<hr>
<h2 id="應用範例版本化處理">應用範例：版本化處理</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">var</span> <span class="nx">processed</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">WeakMap</span><span class="p">();</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="kd">var</span> <span class="nx">CURRENT_VERSION</span> <span class="o">=</span> <span class="mi">3</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="kd">function</span> <span class="nx">apply</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  <span class="nx">shell</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span><span class="s1">&#39;.x&#39;</span><span class="p">).</span><span class="nx">forEach</span><span class="p">(</span><span class="kd">function</span> <span class="p">(</span><span class="nx">el</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="kd">var</span> <span class="nx">record</span> <span class="o">=</span> <span class="nx">processed</span><span class="p">.</span><span class="nx">get</span><span class="p">(</span><span class="nx">el</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="nx">record</span> <span class="o">&amp;&amp;</span> <span class="nx">record</span><span class="p">.</span><span class="nx">version</span> <span class="o">===</span> <span class="nx">CURRENT_VERSION</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="c1">// 升級到新版本（可能需要清舊綁定）
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"></span>    <span class="k">if</span> <span class="p">(</span><span class="nx">record</span><span class="p">)</span> <span class="nx">cleanup</span><span class="p">(</span><span class="nx">el</span><span class="p">,</span> <span class="nx">record</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="nx">enhance</span><span class="p">(</span><span class="nx">el</span><span class="p">,</span> <span class="nx">CURRENT_VERSION</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="nx">processed</span><span class="p">.</span><span class="nx">set</span><span class="p">(</span><span class="nx">el</span><span class="p">,</span> <span class="p">{</span> <span class="nx">version</span><span class="o">:</span> <span class="nx">CURRENT_VERSION</span><span class="p">,</span> <span class="nx">time</span><span class="o">:</span> <span class="nb">Date</span><span class="p">.</span><span class="nx">now</span><span class="p">()</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">  <span class="p">});</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>版本變動時 — 不需要遍歷 DOM 清舊 attribute、直接用 WeakMap value 比對。</p>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該套用本 pattern 嗎？</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>寫第三方 library / npm package</td>
          <td>是 — 不污染使用者 DOM</td>
      </tr>
      <tr>
          <td>Framework 會 strict 清自家 attribute</td>
          <td>是 — WeakMap 跟 framework 解耦</td>
      </tr>
      <tr>
          <td>紀錄需要儲複雜資料（不只 boolean）</td>
          <td>是 — WeakMap value 可任意</td>
      </tr>
      <tr>
          <td>自家 application、debug 重要</td>
          <td>否 — <a href="../pattern-attribute-idempotency-marker/">attribute 標記</a> 在 inspector 可見</td>
      </tr>
      <tr>
          <td>紀錄要跨頁面持久化</td>
          <td>否 — 改用 storage / 後端</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：WeakMap idempotency 是 attribute 標記的「不污染 DOM 替代品」 — 在 library / framework 衝突情境必要、在自家 application 通常用 attribute 即可。GC 自動清理是 WeakMap 的特性、預設不用 Map / Set 是因為它們會 memory leak。</p>
]]></content:encoded></item><item><title>Pattern：跨 slot 同節點搬遷</title><link>https://tarrragon.github.io/blog/report/pattern-cross-slot-node-relocation/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/pattern-cross-slot-node-relocation/</guid><description>&lt;h2 id="核心做法">核心做法&lt;/h2>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="kd">var&lt;/span> &lt;span class="nx">mql&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">window&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">matchMedia&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;(min-width: 1400px)&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="kd">function&lt;/span> &lt;span class="nx">place&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">mql&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">matches&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> &lt;span class="nx">desktopSlot&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">appendChild&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">filter&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span> &lt;span class="k">else&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> &lt;span class="nx">drawer&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">insertBefore&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">filter&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">drawer&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">firstChild&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="nx">mql&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">addEventListener&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;change&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">place&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="nx">place&lt;/span>&lt;span class="p">();&lt;/span> &lt;span class="c1">// 初始化
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>同一個 DOM 節點在兩個 slot 之間搬移、不複製成兩份。&lt;/p>
&lt;hr>
&lt;h2 id="這個做法存在的價值">這個做法存在的價值&lt;/h2>
&lt;p>Stateful UI（內含 checkbox 勾選、表單值、scroll 位置等 state）跨兩個顯示位置切換時、複製兩份會造成 state 分歧 — 使用者在 desktop 勾的 filter、切到 mobile 看不到勾選狀態。&lt;/p>
&lt;p>搬同一份節點 = state 永遠跟著節點走 = 切換無感。&lt;/p>
&lt;hr>
&lt;h2 id="適合的情境">適合的情境&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>情境&lt;/th>
 &lt;th>為什麼合理&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Filter UI 跨 viewport 切換顯示位置&lt;/td>
 &lt;td>checkbox state 跟著節點&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Modal 內容 vs 側邊抽屜&lt;/td>
 &lt;td>同一份表單在兩種展示方式間&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Tab UI 跨 desktop / mobile 重新組織&lt;/td>
 &lt;td>各 tab 內 state 不重置&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>任何「同 UI、不同位置」的 responsive 切換&lt;/td>
 &lt;td>不需要 state 同步邏輯&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>核心特徵&lt;/strong>：UI 內含 state、兩個位置展示的是「同一個邏輯單位」、不是「兩個獨立元件」。&lt;/p>
&lt;hr>
&lt;h2 id="不適合的情境">不適合的情境&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>情境&lt;/th>
 &lt;th>為什麼不夠&lt;/th>
 &lt;th>改用&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>兩個位置展示的是不同元件（雖然視覺類似）&lt;/td>
 &lt;td>搬遷會把錯誤元件搬到錯位置&lt;/td>
 &lt;td>各自獨立掛載、不搬&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>UI 純 stateless（純圖示、純文字）&lt;/td>
 &lt;td>複製兩份成本低、無 state 風險&lt;/td>
 &lt;td>CSS-only 雙顯示 + display 切換&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Framework 管的節點&lt;/td>
 &lt;td>整節點搬安全、但複製不安全（id duplicate / framework 困惑）&lt;/td>
 &lt;td>必須搬整節點、不複製&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>兩個位置視覺差異大&lt;/td>
 &lt;td>搬遷後 UI 不適配新位置&lt;/td>
 &lt;td>各自獨立元件&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="設計細節">設計細節&lt;/h2>
&lt;h3 id="appendchild-是搬遷不是複製">&lt;code>appendChild&lt;/code> 是搬遷、不是複製&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="nx">parentA&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">appendChild&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">node&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="c1">// node 從原位置消失、出現在 parentA
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>DOM API 的 &lt;code>appendChild&lt;/code> / &lt;code>insertBefore&lt;/code> 是 move、不是 copy — 同一個節點不能同時存在於多個位置。這個特性正是搬遷 pattern 的基礎。&lt;/p>
&lt;h3 id="初始放在哪">初始放在哪&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c">&amp;lt;!-- 預設位置（mobile / fallback）--&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">div&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;pagefind-ui&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">div&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;drawer&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">div&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;filter-panel&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>...&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">div&lt;/span>&lt;span class="p">&amp;gt;&lt;/span> &lt;span class="c">&amp;lt;!-- 初始在這 --&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> &lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">div&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">div&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="c">&amp;lt;!-- 桌面 slot（空、等待搬入）--&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">9&lt;/span>&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">aside&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;desktop-filter-slot&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&amp;lt;/&lt;/span>&lt;span class="nt">aside&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>預設放在 fallback 位置 — 當 JS 失敗時仍可見。&lt;/p>
&lt;h3 id="跨-slot-切換的時機">跨 slot 切換的時機&lt;/h3>
&lt;p>&lt;code>matchMedia&lt;/code> event 是 viewport 跨過 breakpoint 的瞬間：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kd">var&lt;/span> &lt;span class="nx">mql&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">window&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">matchMedia&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;(min-width: 1400px)&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="nx">mql&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">addEventListener&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;change&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">place&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="nx">place&lt;/span>&lt;span class="p">();&lt;/span> &lt;span class="c1">// 初始也跑一次
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>不要用 resize event — 太頻繁、會在 breakpoint 邊界震盪。&lt;code>matchMedia&lt;/code> 只在 cross 的瞬間觸發。&lt;/p>
&lt;h3 id="搬遷時-framework-的-reactivity">搬遷時 framework 的 reactivity&lt;/h3>
&lt;p>如果搬遷的節點是 framework 管的（如 Pagefind 的 svelte 元件）— 整節點搬通常安全、framework 在下次 patch 時看到節點還在、繼續更新內部。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心做法">核心做法</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">var</span> <span class="nx">mql</span> <span class="o">=</span> <span class="nb">window</span><span class="p">.</span><span class="nx">matchMedia</span><span class="p">(</span><span class="s1">&#39;(min-width: 1400px)&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="kd">function</span> <span class="nx">place</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="nx">mql</span><span class="p">.</span><span class="nx">matches</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="nx">desktopSlot</span><span class="p">.</span><span class="nx">appendChild</span><span class="p">(</span><span class="nx">filter</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="nx">drawer</span><span class="p">.</span><span class="nx">insertBefore</span><span class="p">(</span><span class="nx">filter</span><span class="p">,</span> <span class="nx">drawer</span><span class="p">.</span><span class="nx">firstChild</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="nx">mql</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">&#39;change&#39;</span><span class="p">,</span> <span class="nx">place</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="nx">place</span><span class="p">();</span>  <span class="c1">// 初始化
</span></span></span></code></pre></div><p>同一個 DOM 節點在兩個 slot 之間搬移、不複製成兩份。</p>
<hr>
<h2 id="這個做法存在的價值">這個做法存在的價值</h2>
<p>Stateful UI（內含 checkbox 勾選、表單值、scroll 位置等 state）跨兩個顯示位置切換時、複製兩份會造成 state 分歧 — 使用者在 desktop 勾的 filter、切到 mobile 看不到勾選狀態。</p>
<p>搬同一份節點 = state 永遠跟著節點走 = 切換無感。</p>
<hr>
<h2 id="適合的情境">適合的情境</h2>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>為什麼合理</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Filter UI 跨 viewport 切換顯示位置</td>
          <td>checkbox state 跟著節點</td>
      </tr>
      <tr>
          <td>Modal 內容 vs 側邊抽屜</td>
          <td>同一份表單在兩種展示方式間</td>
      </tr>
      <tr>
          <td>Tab UI 跨 desktop / mobile 重新組織</td>
          <td>各 tab 內 state 不重置</td>
      </tr>
      <tr>
          <td>任何「同 UI、不同位置」的 responsive 切換</td>
          <td>不需要 state 同步邏輯</td>
      </tr>
  </tbody>
</table>
<p><strong>核心特徵</strong>：UI 內含 state、兩個位置展示的是「同一個邏輯單位」、不是「兩個獨立元件」。</p>
<hr>
<h2 id="不適合的情境">不適合的情境</h2>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>為什麼不夠</th>
          <th>改用</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>兩個位置展示的是不同元件（雖然視覺類似）</td>
          <td>搬遷會把錯誤元件搬到錯位置</td>
          <td>各自獨立掛載、不搬</td>
      </tr>
      <tr>
          <td>UI 純 stateless（純圖示、純文字）</td>
          <td>複製兩份成本低、無 state 風險</td>
          <td>CSS-only 雙顯示 + display 切換</td>
      </tr>
      <tr>
          <td>Framework 管的節點</td>
          <td>整節點搬安全、但複製不安全（id duplicate / framework 困惑）</td>
          <td>必須搬整節點、不複製</td>
      </tr>
      <tr>
          <td>兩個位置視覺差異大</td>
          <td>搬遷後 UI 不適配新位置</td>
          <td>各自獨立元件</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="設計細節">設計細節</h2>
<h3 id="appendchild-是搬遷不是複製"><code>appendChild</code> 是搬遷、不是複製</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="nx">parentA</span><span class="p">.</span><span class="nx">appendChild</span><span class="p">(</span><span class="nx">node</span><span class="p">);</span>  <span class="c1">// node 從原位置消失、出現在 parentA
</span></span></span></code></pre></div><p>DOM API 的 <code>appendChild</code> / <code>insertBefore</code> 是 move、不是 copy — 同一個節點不能同時存在於多個位置。這個特性正是搬遷 pattern 的基礎。</p>
<h3 id="初始放在哪">初始放在哪</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="ln">1</span><span class="cl"><span class="c">&lt;!-- 預設位置（mobile / fallback）--&gt;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="p">&lt;</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;pagefind-ui&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="p">&lt;</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;drawer&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="p">&lt;</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;filter-panel&#34;</span><span class="p">&gt;</span>...<span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>  <span class="c">&lt;!-- 初始在這 --&gt;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">  <span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c">&lt;!-- 桌面 slot（空、等待搬入）--&gt;</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="p">&lt;</span><span class="nt">aside</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;desktop-filter-slot&#34;</span><span class="p">&gt;&lt;/</span><span class="nt">aside</span><span class="p">&gt;</span></span></span></code></pre></div><p>預設放在 fallback 位置 — 當 JS 失敗時仍可見。</p>
<h3 id="跨-slot-切換的時機">跨 slot 切換的時機</h3>
<p><code>matchMedia</code> event 是 viewport 跨過 breakpoint 的瞬間：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">var</span> <span class="nx">mql</span> <span class="o">=</span> <span class="nb">window</span><span class="p">.</span><span class="nx">matchMedia</span><span class="p">(</span><span class="s1">&#39;(min-width: 1400px)&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nx">mql</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">&#39;change&#39;</span><span class="p">,</span> <span class="nx">place</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nx">place</span><span class="p">();</span>  <span class="c1">// 初始也跑一次
</span></span></span></code></pre></div><p>不要用 resize event — 太頻繁、會在 breakpoint 邊界震盪。<code>matchMedia</code> 只在 cross 的瞬間觸發。</p>
<h3 id="搬遷時-framework-的-reactivity">搬遷時 framework 的 reactivity</h3>
<p>如果搬遷的節點是 framework 管的（如 Pagefind 的 svelte 元件）— 整節點搬通常安全、framework 在下次 patch 時看到節點還在、繼續更新內部。</p>
<p>詳細安全規則由 <a href="../component-boundary-and-js-impact/">#13 JS 操作 framework 元件：邊界辨識與安全規則</a> 處理。</p>
<h3 id="focus-跟著搬">Focus 跟著搬</h3>
<p>搬遷可能讓鍵盤 focus 暫時失去（視瀏覽器）— 加 save/restore：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">function</span> <span class="nx">place</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="kd">var</span> <span class="nx">activeBefore</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">activeElement</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="nx">mql</span><span class="p">.</span><span class="nx">matches</span><span class="p">)</span> <span class="nx">desktopSlot</span><span class="p">.</span><span class="nx">appendChild</span><span class="p">(</span><span class="nx">filter</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="k">else</span> <span class="nx">drawer</span><span class="p">.</span><span class="nx">insertBefore</span><span class="p">(</span><span class="nx">filter</span><span class="p">,</span> <span class="nx">drawer</span><span class="p">.</span><span class="nx">firstChild</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="nx">activeBefore</span> <span class="o">&amp;&amp;</span> <span class="nx">filter</span><span class="p">.</span><span class="nx">contains</span><span class="p">(</span><span class="nx">activeBefore</span><span class="p">))</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="nx">activeBefore</span><span class="p">.</span><span class="nx">focus</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>詳細處理由 <a href="../focus-management-on-dom-move/">#37 動態 DOM 移動時的 focus 管理</a> 處理。</p>
<hr>
<h2 id="設計取捨兩個-slot-的-stateful-ui-共用">設計取捨：兩個 slot 的 stateful UI 共用</h2>
<p>四種做法、各自機會成本不同。預設選 A（搬同節點）、其他做法在特定情境合理。</p>
<h3 id="a搬同一節點這個專案的預設">A：搬同一節點（這個專案的預設）</h3>
<ul>
<li><strong>機制</strong>：<code>matchMedia + appendChild</code> 在兩 slot 間搬同一份節點</li>
<li><strong>選 A 的理由</strong>：state 跟著節點、切換無感、不需要 sync 邏輯</li>
<li><strong>適合</strong>：stateful UI、需要在兩個位置展示同樣內容</li>
<li><strong>代價</strong>：搬遷 callback 在 viewport 跨 breakpoint 時觸發、需要處理 focus / 動畫</li>
<li><strong>詳細</strong>：本卡片</li>
</ul>
<h3 id="bcss-only-雙顯示--display-切換">B：CSS-only 雙顯示 + display 切換</h3>
<ul>
<li><strong>機制</strong>：兩個位置都放同一份節點 (寫兩遍 HTML)、用 <code>@media + display: none</code> 切換顯示</li>
<li><strong>跟 A 的取捨</strong>：B 純 CSS 簡單、A 需要 JS；但 B 對 stateful UI 失敗（兩份 state 各自獨立）</li>
<li><strong>B 比 A 好的情境</strong>：UI 純 stateless（純圖示）、純 CSS 解就夠</li>
</ul>
<h3 id="ccss-only--js-同步-state">C：CSS-only + JS 同步 state</h3>
<ul>
<li><strong>機制</strong>：兩份節點 + JS 監聽 state 變動同步</li>
<li><strong>跟 A 的取捨</strong>：C 比 B 解 state 問題、但同步邏輯複雜（雙向更新、避免循環）</li>
<li><strong>C 比 A 好的情境</strong>：兩個位置的 UI 視覺需要差異（不只是位置不同）</li>
</ul>
<h3 id="djs-完全重建-ui">D：JS 完全重建 UI</h3>
<ul>
<li><strong>機制</strong>：viewport 變動時拆掉舊 UI、在新位置重建一份</li>
<li><strong>成本特別高的原因</strong>：state 在重建時遺失、UI 閃爍、輸入中斷</li>
<li><strong>D 才合理的情境</strong>：UI 是 stateless 的、且重建成本低</li>
</ul>
<hr>
<h2 id="跟其他-pattern-的關係">跟其他 pattern 的關係</h2>
<p><a href="../dom-selector-precision/">#14 Selector 精準度</a> 的「起點」維度有四種做法、本卡片是「跨 slot 搬遷」這個專門情境的補充：</p>
<table>
  <thead>
      <tr>
          <th>議題</th>
          <th>對應 pattern</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Query 的起點</td>
          <td><a href="../pattern-document-query/">#46 document</a> / <a href="../pattern-component-root/">#47 元件根變數</a> / <a href="../pattern-root-as-parameter/">#48 起點當參數</a> / <a href="../pattern-closest-lookup/">#49 closest 反向</a></td>
      </tr>
      <tr>
          <td>Idempotency 過濾</td>
          <td><a href="../pattern-attribute-idempotency-marker/">#50 attribute 標記</a> / <a href="../pattern-weakmap-idempotency-record/">#51 WeakMap</a></td>
      </tr>
      <tr>
          <td>跨 slot 搬遷（本卡片）</td>
          <td>同節點 vs 雙節點 + state 同步</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="應用範例跨-viewport-filter-切換">應用範例：跨 viewport filter 切換</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">function</span> <span class="nx">setupResponsiveFilter</span><span class="p">(</span><span class="nx">shell</span><span class="p">,</span> <span class="nx">breakpoint</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="kd">var</span> <span class="nx">filter</span> <span class="o">=</span> <span class="nx">shell</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui__filter-panel&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="kd">var</span> <span class="nx">drawer</span> <span class="o">=</span> <span class="nx">shell</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui__drawer&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="kd">var</span> <span class="nx">desktopSlot</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.search-filter-slot&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">filter</span> <span class="o">||</span> <span class="o">!</span><span class="nx">drawer</span> <span class="o">||</span> <span class="o">!</span><span class="nx">desktopSlot</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  <span class="kd">var</span> <span class="nx">mql</span> <span class="o">=</span> <span class="nb">window</span><span class="p">.</span><span class="nx">matchMedia</span><span class="p">(</span><span class="s1">&#39;(min-width: &#39;</span> <span class="o">+</span> <span class="nx">breakpoint</span> <span class="o">+</span> <span class="s1">&#39;px)&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl">  <span class="kd">function</span> <span class="nx">place</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="kd">var</span> <span class="nx">activeBefore</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">activeElement</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="nx">mql</span><span class="p">.</span><span class="nx">matches</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">      <span class="nx">desktopSlot</span><span class="p">.</span><span class="nx">appendChild</span><span class="p">(</span><span class="nx">filter</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">    <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">      <span class="nx">drawer</span><span class="p">.</span><span class="nx">insertBefore</span><span class="p">(</span><span class="nx">filter</span><span class="p">,</span> <span class="nx">drawer</span><span class="p">.</span><span class="nx">firstChild</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">
</span></span><span class="line"><span class="ln">19</span><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="nx">activeBefore</span> <span class="o">&amp;&amp;</span> <span class="nx">filter</span><span class="p">.</span><span class="nx">contains</span><span class="p">(</span><span class="nx">activeBefore</span><span class="p">))</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">      <span class="nx">activeBefore</span><span class="p">.</span><span class="nx">focus</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">
</span></span><span class="line"><span class="ln">24</span><span class="cl">  <span class="nx">place</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">  <span class="nx">mql</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">&#39;change&#39;</span><span class="p">,</span> <span class="nx">place</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>完整 pattern：取元件根 + matchMedia + 搬遷 + focus 處理。</p>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該套用本 pattern 嗎？</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>兩份節點各自 state、用 sync 邏輯保持一致</td>
          <td>是 — 改成搬同節點、移除 sync</td>
      </tr>
      <tr>
          <td>Stateful UI 在 mobile / desktop 兩種 layout 間</td>
          <td>是 — 直接的應用</td>
      </tr>
      <tr>
          <td>切換 viewport 時 UI 閃爍 / 重建</td>
          <td>是 — 改成搬而非重建</td>
      </tr>
      <tr>
          <td>兩個位置展示完全不同的 UI（不是同邏輯）</td>
          <td>否 — 各自獨立元件</td>
      </tr>
      <tr>
          <td>Framework 管的節點</td>
          <td>是 — 整節點搬安全、但要遵守 <a href="../component-boundary-and-js-impact/">#13</a> 的規則</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：Stateful UI 的兩個展示位置共用同一份節點、state 自然跟著走 — 比「兩份節點 + sync 邏輯」乾淨。複製兩份是「state 來源從一變二」的隱形多源（違反 <a href="../single-source-of-truth/">#44 SSoT</a>）。</p>
]]></content:encoded></item><item><title>Pattern：自動續抓直到湊滿 quota</title><link>https://tarrragon.github.io/blog/report/pattern-fetch-until-quota/</link><pubDate>Sun, 26 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/pattern-fetch-until-quota/</guid><description>&lt;h2 id="pattern-一句話">Pattern 一句話&lt;/h2>
&lt;p>抓一批 → filter → 不夠就再抓 → 湊滿 N 個 match 或 source 結束。&lt;/p>
&lt;p>對應 #59 &lt;a href="../filter-source-composition-strategies/">Filter × Source 合成策略&lt;/a> 的策略 B。&lt;/p>
&lt;hr>
&lt;h2 id="何時用何時不用">何時用、何時不用&lt;/h2>
&lt;h3 id="用">用&lt;/h3>
&lt;ul>
&lt;li>Source 不支援 server-side filter（不能用策略 A）&lt;/li>
&lt;li>不能控 build pipeline 重 index（不能用策略 C）&lt;/li>
&lt;li>Match 密度可預期、不會稀疏到要拉光整個 dataset&lt;/li>
&lt;li>使用者期望「filter 後自動湊夠 N 個」、不要手動續抓&lt;/li>
&lt;/ul>
&lt;h3 id="不用">不用&lt;/h3>
&lt;ul>
&lt;li>Source 支援 server-side filter（直接用策略 A）&lt;/li>
&lt;li>Match 稀疏、可能拉光整個 dataset 才湊到 N（換 D 誠實 UX）&lt;/li>
&lt;li>Source cardinality 大（10 萬筆）、不能拉太多次&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="必要元件">必要元件&lt;/h2>
&lt;h3 id="元件-1quota-跟上限">元件 1：Quota 跟上限&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kr">const&lt;/span> &lt;span class="nx">TARGET&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">10&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// 期望湊滿的 match 數
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kr">const&lt;/span> &lt;span class="nx">MAX_BATCHES&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">20&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// 最多續抓次數（保護）
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kr">const&lt;/span> &lt;span class="nx">MAX_TIME_MS&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">5000&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// 最大時間（保護）
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>沒有上限 = 稀疏時拉爆&lt;/strong>。兩個上限缺一不可。&lt;/p>
&lt;h3 id="元件-2loop-with-break-conditions">元件 2：Loop with break conditions&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="kr">async&lt;/span> &lt;span class="kd">function&lt;/span> &lt;span class="nx">fetchUntilQuota&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">matches&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">target&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">TARGET&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="nx">collected&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[];&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="nx">start&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">Date&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">now&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> &lt;span class="kd">let&lt;/span> &lt;span class="nx">batchCount&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> &lt;span class="k">while&lt;/span> &lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="nx">collected&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">length&lt;/span> &lt;span class="o">&amp;lt;&lt;/span> &lt;span class="nx">target&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="nx">hasMore&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="nx">batchCount&lt;/span> &lt;span class="o">&amp;lt;&lt;/span> &lt;span class="nx">MAX_BATCHES&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> &lt;span class="nb">Date&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">now&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="nx">start&lt;/span> &lt;span class="o">&amp;lt;&lt;/span> &lt;span class="nx">MAX_TIME_MS&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl"> &lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="nx">batch&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kr">await&lt;/span> &lt;span class="nx">fetchNext&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl"> &lt;span class="nx">collected&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">push&lt;/span>&lt;span class="p">(...&lt;/span>&lt;span class="nx">batch&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">filter&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">matches&lt;/span>&lt;span class="p">));&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl"> &lt;span class="nx">batchCount&lt;/span>&lt;span class="o">++&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl"> &lt;span class="nx">collected&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&lt;/span>&lt;span class="cl"> &lt;span class="nx">reachedQuota&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">collected&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">length&lt;/span> &lt;span class="o">&amp;gt;=&lt;/span> &lt;span class="nx">target&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">20&lt;/span>&lt;span class="cl"> &lt;span class="nx">exhaustedSource&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="o">!&lt;/span>&lt;span class="nx">hasMore&lt;/span>&lt;span class="p">(),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">21&lt;/span>&lt;span class="cl"> &lt;span class="nx">hitLimit&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">batchCount&lt;/span> &lt;span class="o">&amp;gt;=&lt;/span> &lt;span class="nx">MAX_BATCHES&lt;/span> &lt;span class="o">||&lt;/span> &lt;span class="nb">Date&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">now&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="nx">start&lt;/span> &lt;span class="o">&amp;gt;=&lt;/span> &lt;span class="nx">MAX_TIME_MS&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">22&lt;/span>&lt;span class="cl"> &lt;span class="p">};&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">23&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>返回值含三個 flag、UI 用來判斷該顯示哪個狀態（湊滿 / 抓完無更多 / 撞到上限）。&lt;/p>
&lt;h3 id="元件-3可中斷">元件 3：可中斷&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="kr">async&lt;/span> &lt;span class="kd">function&lt;/span> &lt;span class="nx">fetchUntilQuota&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">matches&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">target&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">signal&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl"> &lt;span class="c1">// ...
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="k">while&lt;/span> &lt;span class="p">(...)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">signal&lt;/span>&lt;span class="o">?&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">aborted&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="k">throw&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">DOMException&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;aborted&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;AbortError&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="nx">batch&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kr">await&lt;/span> &lt;span class="nx">fetchNext&lt;/span>&lt;span class="p">({&lt;/span> &lt;span class="nx">signal&lt;/span> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> &lt;span class="c1">// ...
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="c1">// 使用
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kr">const&lt;/span> &lt;span class="nx">ctrl&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">AbortController&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="nx">input&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">addEventListener&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;input&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">()&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl"> &lt;span class="nx">ctrl&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">abort&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl"> &lt;span class="nx">ctrl&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">AbortController&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl"> &lt;span class="nx">fetchUntilQuota&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">matches&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">10&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">ctrl&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">signal&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl">&lt;span class="p">});&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>使用者改 query / filter 時能立刻取消舊的續抓。&lt;strong>沒有可中斷 = 競態 bug&lt;/strong>（舊 query 的結果晚到、覆蓋新 query 的）。&lt;/p>
&lt;hr>
&lt;h2 id="ux-配套">UX 配套&lt;/h2>
&lt;h3 id="載入中顯示進度">載入中顯示進度&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">div&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;loading&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>已掃 &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">strong&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>24&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">strong&lt;/span>&lt;span class="p">&amp;gt;&lt;/span> 筆 / 已命中 &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">strong&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>3 / 10&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">strong&lt;/span>&lt;span class="p">&amp;gt;&amp;lt;/&lt;/span>&lt;span class="nt">div&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>不顯示進度 = 使用者不知道是在等還是卡住。&lt;/p>
&lt;h3 id="結束時顯示原因">結束時顯示原因&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>結束原因&lt;/th>
 &lt;th>顯示&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;code>reachedQuota&lt;/code>&lt;/td>
 &lt;td>「找到 10 個結果」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>exhaustedSource&lt;/code>&lt;/td>
 &lt;td>「全部掃完、共找到 K 個」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>hitLimit&lt;/code>&lt;/td>
 &lt;td>「已掃 N 筆、找到 K 個。要繼續找嗎？」&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>不區分原因 = 使用者不知道為什麼停（同 #57 三狀態問題）。&lt;/p>
&lt;hr>
&lt;h2 id="反例">反例&lt;/h2>
&lt;h3 id="反例-1沒上限">反例 1：沒上限&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="k">while&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">collected&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">length&lt;/span> &lt;span class="o">&amp;lt;&lt;/span> &lt;span class="nx">target&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="nx">hasMore&lt;/span>&lt;span class="p">())&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="nx">collected&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">push&lt;/span>&lt;span class="p">(...(&lt;/span>&lt;span class="kr">await&lt;/span> &lt;span class="nx">fetchNext&lt;/span>&lt;span class="p">()).&lt;/span>&lt;span class="nx">filter&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">matches&lt;/span>&lt;span class="p">));&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1">// 稀疏 match → 拉光整個 source
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="反例-2沒-abort-signal">反例 2：沒 abort signal&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="nx">input&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">addEventListener&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;input&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kr">async&lt;/span> &lt;span class="p">()&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="nx">r&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kr">await&lt;/span> &lt;span class="nx">fetchUntilQuota&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">matches&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="nx">render&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">r&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="c1">// 舊 query 的結果可能覆蓋新 query
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="p">});&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="反例-3每批序列化等">反例 3：每批序列化等&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="k">for&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="kd">let&lt;/span> &lt;span class="nx">i&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="nx">i&lt;/span> &lt;span class="o">&amp;lt;&lt;/span> &lt;span class="nx">MAX&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="nx">i&lt;/span>&lt;span class="o">++&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="nx">batch&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kr">await&lt;/span> &lt;span class="nx">fetchNext&lt;/span>&lt;span class="p">();&lt;/span> &lt;span class="c1">// 序列、慢
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="c1">// ...
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>如果 source 支援平行 fetch（多個 page 同時抓） → 改成平行更快：&lt;/p></description><content:encoded><![CDATA[<h2 id="pattern-一句話">Pattern 一句話</h2>
<p>抓一批 → filter → 不夠就再抓 → 湊滿 N 個 match 或 source 結束。</p>
<p>對應 #59 <a href="../filter-source-composition-strategies/">Filter × Source 合成策略</a> 的策略 B。</p>
<hr>
<h2 id="何時用何時不用">何時用、何時不用</h2>
<h3 id="用">用</h3>
<ul>
<li>Source 不支援 server-side filter（不能用策略 A）</li>
<li>不能控 build pipeline 重 index（不能用策略 C）</li>
<li>Match 密度可預期、不會稀疏到要拉光整個 dataset</li>
<li>使用者期望「filter 後自動湊夠 N 個」、不要手動續抓</li>
</ul>
<h3 id="不用">不用</h3>
<ul>
<li>Source 支援 server-side filter（直接用策略 A）</li>
<li>Match 稀疏、可能拉光整個 dataset 才湊到 N（換 D 誠實 UX）</li>
<li>Source cardinality 大（10 萬筆）、不能拉太多次</li>
</ul>
<hr>
<h2 id="必要元件">必要元件</h2>
<h3 id="元件-1quota-跟上限">元件 1：Quota 跟上限</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="kr">const</span> <span class="nx">TARGET</span> <span class="o">=</span> <span class="mi">10</span><span class="p">;</span>       <span class="c1">// 期望湊滿的 match 數
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="kr">const</span> <span class="nx">MAX_BATCHES</span> <span class="o">=</span> <span class="mi">20</span><span class="p">;</span>  <span class="c1">// 最多續抓次數（保護）
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span><span class="kr">const</span> <span class="nx">MAX_TIME_MS</span> <span class="o">=</span> <span class="mi">5000</span><span class="p">;</span> <span class="c1">// 最大時間（保護）
</span></span></span></code></pre></div><p><strong>沒有上限 = 稀疏時拉爆</strong>。兩個上限缺一不可。</p>
<h3 id="元件-2loop-with-break-conditions">元件 2：Loop with break conditions</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kr">async</span> <span class="kd">function</span> <span class="nx">fetchUntilQuota</span><span class="p">(</span><span class="nx">matches</span><span class="p">,</span> <span class="nx">target</span> <span class="o">=</span> <span class="nx">TARGET</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="kr">const</span> <span class="nx">collected</span> <span class="o">=</span> <span class="p">[];</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="kr">const</span> <span class="nx">start</span> <span class="o">=</span> <span class="nb">Date</span><span class="p">.</span><span class="nx">now</span><span class="p">();</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="kd">let</span> <span class="nx">batchCount</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">  <span class="k">while</span> <span class="p">(</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="nx">collected</span><span class="p">.</span><span class="nx">length</span> <span class="o">&lt;</span> <span class="nx">target</span> <span class="o">&amp;&amp;</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="nx">hasMore</span><span class="p">()</span> <span class="o">&amp;&amp;</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="nx">batchCount</span> <span class="o">&lt;</span> <span class="nx">MAX_BATCHES</span> <span class="o">&amp;&amp;</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="nb">Date</span><span class="p">.</span><span class="nx">now</span><span class="p">()</span> <span class="o">-</span> <span class="nx">start</span> <span class="o">&lt;</span> <span class="nx">MAX_TIME_MS</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">  <span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="kr">const</span> <span class="nx">batch</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">fetchNext</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="nx">collected</span><span class="p">.</span><span class="nx">push</span><span class="p">(...</span><span class="nx">batch</span><span class="p">.</span><span class="nx">filter</span><span class="p">(</span><span class="nx">matches</span><span class="p">));</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="nx">batchCount</span><span class="o">++</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">
</span></span><span class="line"><span class="ln">17</span><span class="cl">  <span class="k">return</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">    <span class="nx">collected</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">    <span class="nx">reachedQuota</span><span class="o">:</span> <span class="nx">collected</span><span class="p">.</span><span class="nx">length</span> <span class="o">&gt;=</span> <span class="nx">target</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">    <span class="nx">exhaustedSource</span><span class="o">:</span> <span class="o">!</span><span class="nx">hasMore</span><span class="p">(),</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">    <span class="nx">hitLimit</span><span class="o">:</span> <span class="nx">batchCount</span> <span class="o">&gt;=</span> <span class="nx">MAX_BATCHES</span> <span class="o">||</span> <span class="nb">Date</span><span class="p">.</span><span class="nx">now</span><span class="p">()</span> <span class="o">-</span> <span class="nx">start</span> <span class="o">&gt;=</span> <span class="nx">MAX_TIME_MS</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">  <span class="p">};</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>返回值含三個 flag、UI 用來判斷該顯示哪個狀態（湊滿 / 抓完無更多 / 撞到上限）。</p>
<h3 id="元件-3可中斷">元件 3：可中斷</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kr">async</span> <span class="kd">function</span> <span class="nx">fetchUntilQuota</span><span class="p">(</span><span class="nx">matches</span><span class="p">,</span> <span class="nx">target</span><span class="p">,</span> <span class="nx">signal</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="c1">// ...
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1"></span>  <span class="k">while</span> <span class="p">(...)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="nx">signal</span><span class="o">?</span><span class="p">.</span><span class="nx">aborted</span><span class="p">)</span> <span class="k">throw</span> <span class="k">new</span> <span class="nx">DOMException</span><span class="p">(</span><span class="s1">&#39;aborted&#39;</span><span class="p">,</span> <span class="s1">&#39;AbortError&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="kr">const</span> <span class="nx">batch</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">fetchNext</span><span class="p">({</span> <span class="nx">signal</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="c1">// ...
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"></span>  <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1">// 使用
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"></span><span class="kr">const</span> <span class="nx">ctrl</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">AbortController</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="nx">input</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">&#39;input&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">  <span class="nx">ctrl</span><span class="p">.</span><span class="nx">abort</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">  <span class="nx">ctrl</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">AbortController</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">  <span class="nx">fetchUntilQuota</span><span class="p">(</span><span class="nx">matches</span><span class="p">,</span> <span class="mi">10</span><span class="p">,</span> <span class="nx">ctrl</span><span class="p">.</span><span class="nx">signal</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="p">});</span></span></span></code></pre></div><p>使用者改 query / filter 時能立刻取消舊的續抓。<strong>沒有可中斷 = 競態 bug</strong>（舊 query 的結果晚到、覆蓋新 query 的）。</p>
<hr>
<h2 id="ux-配套">UX 配套</h2>
<h3 id="載入中顯示進度">載入中顯示進度</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="ln">1</span><span class="cl"><span class="p">&lt;</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;loading&#34;</span><span class="p">&gt;</span>已掃 <span class="p">&lt;</span><span class="nt">strong</span><span class="p">&gt;</span>24<span class="p">&lt;/</span><span class="nt">strong</span><span class="p">&gt;</span> 筆 / 已命中 <span class="p">&lt;</span><span class="nt">strong</span><span class="p">&gt;</span>3 / 10<span class="p">&lt;/</span><span class="nt">strong</span><span class="p">&gt;&lt;/</span><span class="nt">div</span><span class="p">&gt;</span></span></span></code></pre></div><p>不顯示進度 = 使用者不知道是在等還是卡住。</p>
<h3 id="結束時顯示原因">結束時顯示原因</h3>
<table>
  <thead>
      <tr>
          <th>結束原因</th>
          <th>顯示</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>reachedQuota</code></td>
          <td>「找到 10 個結果」</td>
      </tr>
      <tr>
          <td><code>exhaustedSource</code></td>
          <td>「全部掃完、共找到 K 個」</td>
      </tr>
      <tr>
          <td><code>hitLimit</code></td>
          <td>「已掃 N 筆、找到 K 個。要繼續找嗎？」</td>
      </tr>
  </tbody>
</table>
<p>不區分原因 = 使用者不知道為什麼停（同 #57 三狀態問題）。</p>
<hr>
<h2 id="反例">反例</h2>
<h3 id="反例-1沒上限">反例 1：沒上限</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">while</span> <span class="p">(</span><span class="nx">collected</span><span class="p">.</span><span class="nx">length</span> <span class="o">&lt;</span> <span class="nx">target</span> <span class="o">&amp;&amp;</span> <span class="nx">hasMore</span><span class="p">())</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="nx">collected</span><span class="p">.</span><span class="nx">push</span><span class="p">(...(</span><span class="kr">await</span> <span class="nx">fetchNext</span><span class="p">()).</span><span class="nx">filter</span><span class="p">(</span><span class="nx">matches</span><span class="p">));</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1">// 稀疏 match → 拉光整個 source
</span></span></span></code></pre></div><h3 id="反例-2沒-abort-signal">反例 2：沒 abort signal</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="nx">input</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">&#39;input&#39;</span><span class="p">,</span> <span class="kr">async</span> <span class="p">()</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="kr">const</span> <span class="nx">r</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">fetchUntilQuota</span><span class="p">(</span><span class="nx">matches</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="nx">render</span><span class="p">(</span><span class="nx">r</span><span class="p">);</span>  <span class="c1">// 舊 query 的結果可能覆蓋新 query
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"></span><span class="p">});</span></span></span></code></pre></div><h3 id="反例-3每批序列化等">反例 3：每批序列化等</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">for</span> <span class="p">(</span><span class="kd">let</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="nx">MAX</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="kr">const</span> <span class="nx">batch</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">fetchNext</span><span class="p">();</span>  <span class="c1">// 序列、慢
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span>  <span class="c1">// ...
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"></span><span class="p">}</span></span></span></code></pre></div><p>如果 source 支援平行 fetch（多個 page 同時抓） → 改成平行更快：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="kr">const</span> <span class="nx">batches</span> <span class="o">=</span> <span class="kr">await</span> <span class="nb">Promise</span><span class="p">.</span><span class="nx">all</span><span class="p">([</span><span class="nx">fetch</span><span class="p">(</span><span class="mi">0</span><span class="p">),</span> <span class="nx">fetch</span><span class="p">(</span><span class="mi">1</span><span class="p">),</span> <span class="nx">fetch</span><span class="p">(</span><span class="mi">2</span><span class="p">)]);</span></span></span></code></pre></div><p>但平行有 over-fetch 風險（湊滿後其他批白抓） — 適合 match 密度高的情境。</p>
<hr>
<h2 id="跟其他-pattern-的關係">跟其他 Pattern 的關係</h2>
<ul>
<li>跟 #61 <a href="../pattern-query-side-pushdown/">Pattern：推進 query</a>（待補）：A 是最優、B 是 source 不支援時的退路</li>
<li>跟 #62 <a href="../pattern-honest-progress-ui/">Pattern：誠實進度 UX</a>（待補）：B 撞到上限後 fallback 到誠實 UX</li>
</ul>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Source 不支援 filter、要湊滿 N 個結果</td>
          <td>用本 pattern</td>
      </tr>
      <tr>
          <td>寫了 while loop 但沒上限</td>
          <td>補 MAX_BATCHES + MAX_TIME_MS</td>
      </tr>
      <tr>
          <td>Input 改變時舊的續抓還在跑</td>
          <td>補 AbortController</td>
      </tr>
      <tr>
          <td>結束時不知道是「湊滿」「掃完」「撞上限」</td>
          <td>補三個 flag、UI 分支顯示</td>
      </tr>
      <tr>
          <td>Match 稀疏、續抓 50 次才湊到 1 個</td>
          <td>換策略 — B 不適合稀疏 case</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：自動續抓的價值在「使用者透明」、但成本是「上限保護必要」。沒上限的 B 比 silent post-filter 更糟（會拉爆）。</p>
]]></content:encoded></item><item><title>Pattern：把 filter 推進 query 引擎</title><link>https://tarrragon.github.io/blog/report/pattern-query-side-pushdown/</link><pubDate>Sun, 26 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/pattern-query-side-pushdown/</guid><description>&lt;h2 id="pattern-一句話">Pattern 一句話&lt;/h2>
&lt;p>把 filter 變成 source 的 query 參數、source 端就回符合的、client 不 post-filter。&lt;/p>
&lt;p>對應 #59 &lt;a href="../filter-source-composition-strategies/">Filter × Source 合成策略&lt;/a> 的策略 A。&lt;/p>
&lt;hr>
&lt;h2 id="何時用何時不用">何時用、何時不用&lt;/h2>
&lt;h3 id="用">用&lt;/h3>
&lt;ul>
&lt;li>Source 支援該 filter 條件（已索引、能在 query 表達）&lt;/li>
&lt;li>想避免任何 client-side post-filter&lt;/li>
&lt;li>想避免層錯位（見 #55）&lt;/li>
&lt;/ul>
&lt;h3 id="不用">不用&lt;/h3>
&lt;ul>
&lt;li>Source 不支援（pagefind 對 title-only 沒 native 支援）&lt;/li>
&lt;li>條件需要 client-side 計算（依 viewport / 隨機抽樣）&lt;/li>
&lt;li>推進 query 後 cardinality 仍大、還是要 paginate（這時 A + B 並用）&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="推進的層次">推進的層次&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>層次&lt;/th>
 &lt;th>範例&lt;/th>
 &lt;th>成本&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Query 參數&lt;/td>
 &lt;td>&lt;code>?type=post&amp;amp;tag=js&lt;/code>&lt;/td>
 &lt;td>最低、改 URL&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Filter API&lt;/td>
 &lt;td>&lt;code>pagefind.search(q, { filters: { type: 'post' } })&lt;/code>&lt;/td>
 &lt;td>低、用 SDK&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Re-query&lt;/td>
 &lt;td>重新呼叫 search、不是同個 result 集再過濾&lt;/td>
 &lt;td>低&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Index 重建&lt;/td>
 &lt;td>Build 時加新欄位 / 新 index&lt;/td>
 &lt;td>中-高、要 build&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Schema 修改&lt;/td>
 &lt;td>改 DB schema、加欄位、reindex&lt;/td>
 &lt;td>高&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>選哪一層 = source 的 capabilities 決定。&lt;/p>
&lt;hr>
&lt;h2 id="評估-source-capabilities">評估 Source Capabilities&lt;/h2>
&lt;p>寫之前讀 source docs / API spec、列出：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>問題&lt;/th>
 &lt;th>答案範例&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Source 接受哪些 filter 條件？&lt;/td>
 &lt;td>&lt;code>=&lt;/code>, &lt;code>IN&lt;/code>, &lt;code>BETWEEN&lt;/code>, full-text, &amp;hellip;&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>哪些欄位已索引？&lt;/td>
 &lt;td>&lt;code>type&lt;/code>, &lt;code>tag&lt;/code>, &lt;code>date&lt;/code> (not &lt;code>title&lt;/code>)&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>哪些 filter 不支援、需要重 index？&lt;/td>
 &lt;td>&lt;code>title contains&lt;/code>（需 full-text title）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Filter 有沒有 cost cap（rate limit）？&lt;/td>
 &lt;td>100 query / sec&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>不評估就寫 = 寫到一半發現 source 不支援、回頭走策略 B 或 C。&lt;/p>
&lt;hr>
&lt;h2 id="範例pagefind">範例：Pagefind&lt;/h2>
&lt;h3 id="支援的-filter">支援的 filter&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1">// pagefind 已支援 filter（透過 _pagefind/filter.json）
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kr">const&lt;/span> &lt;span class="nx">r&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kr">await&lt;/span> &lt;span class="nx">pagefind&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">search&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;keyword&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="nx">filters&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> &lt;span class="nx">type&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;post&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// 支援
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="nx">tag&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">any&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;js&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;css&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="p">},&lt;/span> &lt;span class="c1">// 支援多選
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="p">});&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="不支援的-filter">不支援的 filter&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1">// pagefind 不支援「只搜 title」
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1">// 因為 pagefind 的 search 對 full-text、不分區
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kr">const&lt;/span> &lt;span class="nx">r&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kr">await&lt;/span> &lt;span class="nx">pagefind&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">search&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;keyword&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> &lt;span class="nx">scope&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;title-only&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// 不存在（不支援）
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="p">});&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>要解決：&lt;/p>
&lt;ul>
&lt;li>方案 1：build 時用 &lt;code>data-pagefind-body&lt;/code> 把 title 標成獨立 region、用 &lt;code>body&lt;/code> filter（pagefind v1.1+）&lt;/li>
&lt;li>方案 2：建兩個獨立 index（一個只 index title、一個只 index content） — 走策略 C&lt;/li>
&lt;li>方案 3：放棄推進 query、用策略 B 自動續抓 + post-filter&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="跟原本-query-邏輯的並用">跟原本 query 邏輯的並用&lt;/h2>
&lt;p>推進 filter 通常不取代原本 query、是「補上條件」：&lt;/p></description><content:encoded><![CDATA[<h2 id="pattern-一句話">Pattern 一句話</h2>
<p>把 filter 變成 source 的 query 參數、source 端就回符合的、client 不 post-filter。</p>
<p>對應 #59 <a href="../filter-source-composition-strategies/">Filter × Source 合成策略</a> 的策略 A。</p>
<hr>
<h2 id="何時用何時不用">何時用、何時不用</h2>
<h3 id="用">用</h3>
<ul>
<li>Source 支援該 filter 條件（已索引、能在 query 表達）</li>
<li>想避免任何 client-side post-filter</li>
<li>想避免層錯位（見 #55）</li>
</ul>
<h3 id="不用">不用</h3>
<ul>
<li>Source 不支援（pagefind 對 title-only 沒 native 支援）</li>
<li>條件需要 client-side 計算（依 viewport / 隨機抽樣）</li>
<li>推進 query 後 cardinality 仍大、還是要 paginate（這時 A + B 並用）</li>
</ul>
<hr>
<h2 id="推進的層次">推進的層次</h2>
<table>
  <thead>
      <tr>
          <th>層次</th>
          <th>範例</th>
          <th>成本</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Query 參數</td>
          <td><code>?type=post&amp;tag=js</code></td>
          <td>最低、改 URL</td>
      </tr>
      <tr>
          <td>Filter API</td>
          <td><code>pagefind.search(q, { filters: { type: 'post' } })</code></td>
          <td>低、用 SDK</td>
      </tr>
      <tr>
          <td>Re-query</td>
          <td>重新呼叫 search、不是同個 result 集再過濾</td>
          <td>低</td>
      </tr>
      <tr>
          <td>Index 重建</td>
          <td>Build 時加新欄位 / 新 index</td>
          <td>中-高、要 build</td>
      </tr>
      <tr>
          <td>Schema 修改</td>
          <td>改 DB schema、加欄位、reindex</td>
          <td>高</td>
      </tr>
  </tbody>
</table>
<p>選哪一層 = source 的 capabilities 決定。</p>
<hr>
<h2 id="評估-source-capabilities">評估 Source Capabilities</h2>
<p>寫之前讀 source docs / API spec、列出：</p>
<table>
  <thead>
      <tr>
          <th>問題</th>
          <th>答案範例</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Source 接受哪些 filter 條件？</td>
          <td><code>=</code>, <code>IN</code>, <code>BETWEEN</code>, full-text, &hellip;</td>
      </tr>
      <tr>
          <td>哪些欄位已索引？</td>
          <td><code>type</code>, <code>tag</code>, <code>date</code> (not <code>title</code>)</td>
      </tr>
      <tr>
          <td>哪些 filter 不支援、需要重 index？</td>
          <td><code>title contains</code>（需 full-text title）</td>
      </tr>
      <tr>
          <td>Filter 有沒有 cost cap（rate limit）？</td>
          <td>100 query / sec</td>
      </tr>
  </tbody>
</table>
<p>不評估就寫 = 寫到一半發現 source 不支援、回頭走策略 B 或 C。</p>
<hr>
<h2 id="範例pagefind">範例：Pagefind</h2>
<h3 id="支援的-filter">支援的 filter</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// pagefind 已支援 filter（透過 _pagefind/filter.json）
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="kr">const</span> <span class="nx">r</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">pagefind</span><span class="p">.</span><span class="nx">search</span><span class="p">(</span><span class="s1">&#39;keyword&#39;</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="nx">filters</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="nx">type</span><span class="o">:</span> <span class="s1">&#39;post&#39;</span><span class="p">,</span>           <span class="c1">// 支援
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span>    <span class="nx">tag</span><span class="o">:</span> <span class="p">{</span> <span class="nx">any</span><span class="o">:</span> <span class="p">[</span><span class="s1">&#39;js&#39;</span><span class="p">,</span> <span class="s1">&#39;css&#39;</span><span class="p">]</span> <span class="p">},</span> <span class="c1">// 支援多選
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"></span>  <span class="p">},</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="p">});</span></span></span></code></pre></div><h3 id="不支援的-filter">不支援的 filter</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// pagefind 不支援「只搜 title」
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1">// 因為 pagefind 的 search 對 full-text、不分區
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span><span class="kr">const</span> <span class="nx">r</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">pagefind</span><span class="p">.</span><span class="nx">search</span><span class="p">(</span><span class="s1">&#39;keyword&#39;</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="nx">scope</span><span class="o">:</span> <span class="s1">&#39;title-only&#39;</span><span class="p">,</span>  <span class="c1">// 不存在（不支援）
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span><span class="p">});</span></span></span></code></pre></div><p>要解決：</p>
<ul>
<li>方案 1：build 時用 <code>data-pagefind-body</code> 把 title 標成獨立 region、用 <code>body</code> filter（pagefind v1.1+）</li>
<li>方案 2：建兩個獨立 index（一個只 index title、一個只 index content） — 走策略 C</li>
<li>方案 3：放棄推進 query、用策略 B 自動續抓 + post-filter</li>
</ul>
<hr>
<h2 id="跟原本-query-邏輯的並用">跟原本 query 邏輯的並用</h2>
<p>推進 filter 通常不取代原本 query、是「補上條件」：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// 使用者輸入 query &#34;css&#34;、選 type=post
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="kr">const</span> <span class="nx">r</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">pagefind</span><span class="p">.</span><span class="nx">search</span><span class="p">(</span><span class="s1">&#39;css&#39;</span><span class="p">,</span> <span class="p">{</span>  <span class="c1">// query
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span>  <span class="nx">filters</span><span class="o">:</span> <span class="p">{</span> <span class="nx">type</span><span class="o">:</span> <span class="s1">&#39;post&#39;</span> <span class="p">},</span>              <span class="c1">// filter
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"></span><span class="p">});</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1">// 兩個都進 source、source 算交集
</span></span></span></code></pre></div><p>Filter 跟 query 是不同維度：query 是「找什麼」、filter 是「在哪些範圍找」。</p>
<hr>
<h2 id="反例">反例</h2>
<h3 id="反例-1推進不完全留-client-side-post-filter-補">反例 1：推進不完全、留 client-side post-filter 補</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="kr">const</span> <span class="nx">r</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">pagefind</span><span class="p">.</span><span class="nx">search</span><span class="p">(</span><span class="nx">q</span><span class="p">,</span> <span class="p">{</span> <span class="nx">filters</span><span class="o">:</span> <span class="p">{</span> <span class="nx">type</span><span class="o">:</span> <span class="s1">&#39;post&#39;</span> <span class="p">}</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="kr">const</span> <span class="nx">filtered</span> <span class="o">=</span> <span class="nx">r</span><span class="p">.</span><span class="nx">results</span><span class="p">.</span><span class="nx">filter</span><span class="p">(</span><span class="nx">x</span> <span class="p">=&gt;</span> <span class="nx">x</span><span class="p">.</span><span class="nx">title</span><span class="p">.</span><span class="nx">includes</span><span class="p">(</span><span class="nx">q</span><span class="p">));</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1">// ↑ 這行還是 #55 層錯位
</span></span></span></code></pre></div><p>如果 source 不支援 title-filter、不要用「半推進」 — 直接走策略 C 或 B。</p>
<h3 id="反例-2忽略-cost-cap">反例 2：忽略 cost cap</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="nx">input</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">&#39;input&#39;</span><span class="p">,</span> <span class="kr">async</span> <span class="p">()</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="c1">// 每個鍵盤事件 fire 一個 search query
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span>  <span class="kr">const</span> <span class="nx">r</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">pagefind</span><span class="p">.</span><span class="nx">search</span><span class="p">(</span><span class="nx">input</span><span class="p">.</span><span class="nx">value</span><span class="p">,</span> <span class="p">{</span> <span class="nx">filters</span><span class="o">:</span> <span class="p">...</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="p">});</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1">// → query rate 100+/秒、撞 rate limit
</span></span></span></code></pre></div><p>加 debounce：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">let</span> <span class="nx">timer</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nx">input</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">&#39;input&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="nx">clearTimeout</span><span class="p">(</span><span class="nx">timer</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="nx">timer</span> <span class="o">=</span> <span class="nx">setTimeout</span><span class="p">(()</span> <span class="p">=&gt;</span> <span class="nx">pagefind</span><span class="p">.</span><span class="nx">search</span><span class="p">(</span><span class="nx">input</span><span class="p">.</span><span class="nx">value</span><span class="p">,</span> <span class="p">...),</span> <span class="mi">200</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="p">});</span></span></span></code></pre></div><h3 id="反例-3客製欄位沒進-index寫了-query-失效">反例 3：客製欄位沒進 index、寫了 query 失效</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// 期望 filter 「閱讀時間 &gt; 5 分鐘」
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="kr">const</span> <span class="nx">r</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">pagefind</span><span class="p">.</span><span class="nx">search</span><span class="p">(</span><span class="nx">q</span><span class="p">,</span> <span class="p">{</span> <span class="nx">filters</span><span class="o">:</span> <span class="p">{</span> <span class="nx">readingTime</span><span class="o">:</span> <span class="p">{</span> <span class="nx">gt</span><span class="o">:</span> <span class="mi">5</span> <span class="p">}</span> <span class="p">}</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1">// → 但 build 時沒把 readingTime 進 filter index → filter 被忽略
</span></span></span></code></pre></div><p>預期 source 不支援 → 評估「是否值得加進 index」（成本 vs 使用率）。</p>
<hr>
<h2 id="跟其他-pattern-的關係">跟其他 Pattern 的關係</h2>
<ul>
<li>A 是最優 — 在 source capabilities 範圍內優先選</li>
<li>A 不可行 → 評估 C（建獨立 index）</li>
<li>C 也不可行 → 退到 B（自動續抓）</li>
<li>都不可行 → D（誠實 UX）</li>
</ul>
<p><strong>選擇順序：A → C → B → D</strong>。</p>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Filter 條件能在 source 端表達</td>
          <td>用本 pattern</td>
      </tr>
      <tr>
          <td>Source 不支援、考慮要不要重 index</td>
          <td>評估 C 的成本</td>
      </tr>
      <tr>
          <td>用了 filter 還寫 client-side post-filter</td>
          <td>半推進是反模式、要嘛全推進、要嘛換策略</td>
      </tr>
      <tr>
          <td>Filter 觸發 query rate 高</td>
          <td>加 debounce / throttle</td>
      </tr>
      <tr>
          <td>Query 跟 filter 概念混淆</td>
          <td>區分：query = 「找什麼」、filter = 「範圍」</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：能推進 query 就推 — 沒層錯位、沒 silent 失敗、跟使用者意圖最近。但前提是 source 支援；不支援就要退到 B / C / D、不要做半推進。</p>
]]></content:encoded></item><item><title>Pattern：誠實進度 UX（已掃 N / 命中 K / 共 M）</title><link>https://tarrragon.github.io/blog/report/pattern-honest-progress-ui/</link><pubDate>Sun, 26 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/pattern-honest-progress-ui/</guid><description>&lt;h2 id="pattern-一句話">Pattern 一句話&lt;/h2>
&lt;p>當 filter 必然有層錯位、用「已掃 N / 命中 K / 共 M」三數字 + 「再掃一批」按鈕讓使用者看見掃描範圍、自己決定要不要續抓。&lt;/p>
&lt;p>對應 #59 &lt;a href="../filter-source-composition-strategies/">Filter × Source 合成策略&lt;/a> 的策略 D。&lt;/p>
&lt;hr>
&lt;h2 id="何時用何時不用">何時用、何時不用&lt;/h2>
&lt;h3 id="用">用&lt;/h3>
&lt;ul>
&lt;li>Source 不支援 server-side filter（A 不可行）&lt;/li>
&lt;li>不能或不值得重 index（C 不可行）&lt;/li>
&lt;li>Match 稀疏或不可預期、自動續抓（B）會拉爆&lt;/li>
&lt;li>工程量限制、原型期 / MVP&lt;/li>
&lt;/ul>
&lt;h3 id="不用">不用&lt;/h3>
&lt;ul>
&lt;li>Filter 是主要互動模式（使用者預期「自動全找完」）&lt;/li>
&lt;li>三數字會讓 UI 太複雜&lt;/li>
&lt;li>使用者完全不在意「掃描範圍」&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="三數字的語意">三數字的語意&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>數字&lt;/th>
 &lt;th>意思&lt;/th>
 &lt;th>來源&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>已掃 N&lt;/td>
 &lt;td>已從 source 載入並 filter 過的筆數&lt;/td>
 &lt;td>client 累計&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>命中 K&lt;/td>
 &lt;td>已掃 N 筆中、符合 filter 的筆數&lt;/td>
 &lt;td>client 累計&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>共 M&lt;/td>
 &lt;td>Source 總筆數（如果 source 知道）&lt;/td>
 &lt;td>source meta（可選）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>最少要顯示「已掃 N / 命中 K」 — 沒有 N 使用者不知道掃描範圍、沒有 K 使用者不知道有沒有命中。&lt;/p>
&lt;p>「共 M」可選 — 有的 source（pagefind）會給 total count、有的（streaming）不會。&lt;/p>
&lt;hr>
&lt;h2 id="ui-模板">UI 模板&lt;/h2>
&lt;h3 id="基本版">基本版&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">div&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;filter-status&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> 已掃 &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">strong&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>24&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">strong&lt;/span>&lt;span class="p">&amp;gt;&lt;/span> 筆 / 命中 &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">strong&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>3&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">strong&lt;/span>&lt;span class="p">&amp;gt;&lt;/span> 筆
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">button&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>再掃一批&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">button&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">div&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="含總數">含總數&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">div&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;filter-status&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> 已掃 &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">strong&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>24&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">strong&lt;/span>&lt;span class="p">&amp;gt;&lt;/span> / &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">strong&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>~150&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">strong&lt;/span>&lt;span class="p">&amp;gt;&lt;/span> 筆 — 命中 &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">strong&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>3&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">strong&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">button&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>再掃一批&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">button&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">div&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="含結束狀態呼應-57-三狀態">含結束狀態（呼應 #57 三狀態）&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c">&amp;lt;!-- Loading --&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">div&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;filter-status&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>掃描中... 已掃 &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">strong&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>24&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">strong&lt;/span>&lt;span class="p">&amp;gt;&lt;/span> / 命中 &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">strong&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>3&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">strong&lt;/span>&lt;span class="p">&amp;gt;&amp;lt;/&lt;/span>&lt;span class="nt">div&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="c">&amp;lt;!-- Partial（還可續） --&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">div&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;filter-status&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>已掃 &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">strong&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>24&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">strong&lt;/span>&lt;span class="p">&amp;gt;&lt;/span> / 命中 &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">strong&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>3&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">strong&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">button&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>再掃一批&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">button&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">div&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="c">&amp;lt;!-- End（掃完） --&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">div&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;filter-status&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>已全部掃完、共命中 &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">strong&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>12&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">strong&lt;/span>&lt;span class="p">&amp;gt;&lt;/span> 筆&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">div&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="c">&amp;lt;!-- Empty (filter) --&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">div&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;filter-status&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>已掃 &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">strong&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>24&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">strong&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>、沒有命中
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl"> &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">button&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>再掃一批&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">button&lt;/span>&lt;span class="p">&amp;gt;&lt;/span> 或 &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">a&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>清除 filter&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">a&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">div&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h2 id="進度更新時機">進度更新時機&lt;/h2>
&lt;h3 id="即時更新每筆">即時更新（每筆）&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="k">for&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="kr">const&lt;/span> &lt;span class="nx">item&lt;/span> &lt;span class="k">of&lt;/span> &lt;span class="nx">stream&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="nx">scanned&lt;/span>&lt;span class="o">++&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">matches&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">item&lt;/span>&lt;span class="p">))&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> &lt;span class="nx">matched&lt;/span>&lt;span class="o">++&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> &lt;span class="nx">appendResult&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">item&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl"> &lt;span class="nx">updateUI&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">scanned&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">matched&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="c1">// 每筆更新
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>UX 順、但 DOM 操作頻繁、可能 jank。&lt;/p>
&lt;h3 id="批次更新每批">批次更新（每批）&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kr">const&lt;/span> &lt;span class="nx">batch&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kr">await&lt;/span> &lt;span class="nx">fetchNext&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="nx">scanned&lt;/span> &lt;span class="o">+=&lt;/span> &lt;span class="nx">batch&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">length&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="kr">const&lt;/span> &lt;span class="nx">m&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">batch&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">filter&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">matches&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="nx">matched&lt;/span> &lt;span class="o">+=&lt;/span> &lt;span class="nx">m&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">length&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="nx">appendResults&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">m&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="nx">updateUI&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">scanned&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">matched&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="c1">// 每批一次
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>DOM 操作少、但 UX 不夠順（一段時間沒動）。&lt;/p>
&lt;h3 id="推薦每批--載入中-spinner">推薦：每批 + 載入中 spinner&lt;/h3>
&lt;p>批次後更新數字、批次間顯示 spinner。最平衡。&lt;/p>
&lt;hr>
&lt;h2 id="跟自動續抓b的混合">跟自動續抓（B）的混合&lt;/h2>
&lt;p>可以做成「初始自動續抓 N 批、之後切誠實 UX」：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kr">async&lt;/span> &lt;span class="kd">function&lt;/span> &lt;span class="nx">searchWithFilter&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">query&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="c1">// 初始自動續抓 3 批（湊一些結果）
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="kr">await&lt;/span> &lt;span class="nx">fetchUntilQuota&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">3&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">autoBatches&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mi">3&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> &lt;span class="c1">// 之後使用者手動點「再掃一批」
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="nx">showHonestProgressUI&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>混合的好處：使用者一進來就有結果（不是空畫面）、之後續抓由使用者決定。&lt;/p></description><content:encoded><![CDATA[<h2 id="pattern-一句話">Pattern 一句話</h2>
<p>當 filter 必然有層錯位、用「已掃 N / 命中 K / 共 M」三數字 + 「再掃一批」按鈕讓使用者看見掃描範圍、自己決定要不要續抓。</p>
<p>對應 #59 <a href="../filter-source-composition-strategies/">Filter × Source 合成策略</a> 的策略 D。</p>
<hr>
<h2 id="何時用何時不用">何時用、何時不用</h2>
<h3 id="用">用</h3>
<ul>
<li>Source 不支援 server-side filter（A 不可行）</li>
<li>不能或不值得重 index（C 不可行）</li>
<li>Match 稀疏或不可預期、自動續抓（B）會拉爆</li>
<li>工程量限制、原型期 / MVP</li>
</ul>
<h3 id="不用">不用</h3>
<ul>
<li>Filter 是主要互動模式（使用者預期「自動全找完」）</li>
<li>三數字會讓 UI 太複雜</li>
<li>使用者完全不在意「掃描範圍」</li>
</ul>
<hr>
<h2 id="三數字的語意">三數字的語意</h2>
<table>
  <thead>
      <tr>
          <th>數字</th>
          <th>意思</th>
          <th>來源</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>已掃 N</td>
          <td>已從 source 載入並 filter 過的筆數</td>
          <td>client 累計</td>
      </tr>
      <tr>
          <td>命中 K</td>
          <td>已掃 N 筆中、符合 filter 的筆數</td>
          <td>client 累計</td>
      </tr>
      <tr>
          <td>共 M</td>
          <td>Source 總筆數（如果 source 知道）</td>
          <td>source meta（可選）</td>
      </tr>
  </tbody>
</table>
<p>最少要顯示「已掃 N / 命中 K」 — 沒有 N 使用者不知道掃描範圍、沒有 K 使用者不知道有沒有命中。</p>
<p>「共 M」可選 — 有的 source（pagefind）會給 total count、有的（streaming）不會。</p>
<hr>
<h2 id="ui-模板">UI 模板</h2>
<h3 id="基本版">基本版</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="ln">1</span><span class="cl"><span class="p">&lt;</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;filter-status&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  已掃 <span class="p">&lt;</span><span class="nt">strong</span><span class="p">&gt;</span>24<span class="p">&lt;/</span><span class="nt">strong</span><span class="p">&gt;</span> 筆 / 命中 <span class="p">&lt;</span><span class="nt">strong</span><span class="p">&gt;</span>3<span class="p">&lt;/</span><span class="nt">strong</span><span class="p">&gt;</span> 筆
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="p">&lt;</span><span class="nt">button</span><span class="p">&gt;</span>再掃一批<span class="p">&lt;/</span><span class="nt">button</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span></span></span></code></pre></div><h3 id="含總數">含總數</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="ln">1</span><span class="cl"><span class="p">&lt;</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;filter-status&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  已掃 <span class="p">&lt;</span><span class="nt">strong</span><span class="p">&gt;</span>24<span class="p">&lt;/</span><span class="nt">strong</span><span class="p">&gt;</span> / <span class="p">&lt;</span><span class="nt">strong</span><span class="p">&gt;</span>~150<span class="p">&lt;/</span><span class="nt">strong</span><span class="p">&gt;</span> 筆 — 命中 <span class="p">&lt;</span><span class="nt">strong</span><span class="p">&gt;</span>3<span class="p">&lt;/</span><span class="nt">strong</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="p">&lt;</span><span class="nt">button</span><span class="p">&gt;</span>再掃一批<span class="p">&lt;/</span><span class="nt">button</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span></span></span></code></pre></div><h3 id="含結束狀態呼應-57-三狀態">含結束狀態（呼應 #57 三狀態）</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c">&lt;!-- Loading --&gt;</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="p">&lt;</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;filter-status&#34;</span><span class="p">&gt;</span>掃描中... 已掃 <span class="p">&lt;</span><span class="nt">strong</span><span class="p">&gt;</span>24<span class="p">&lt;/</span><span class="nt">strong</span><span class="p">&gt;</span> / 命中 <span class="p">&lt;</span><span class="nt">strong</span><span class="p">&gt;</span>3<span class="p">&lt;/</span><span class="nt">strong</span><span class="p">&gt;&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c">&lt;!-- Partial（還可續） --&gt;</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="p">&lt;</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;filter-status&#34;</span><span class="p">&gt;</span>已掃 <span class="p">&lt;</span><span class="nt">strong</span><span class="p">&gt;</span>24<span class="p">&lt;/</span><span class="nt">strong</span><span class="p">&gt;</span> / 命中 <span class="p">&lt;</span><span class="nt">strong</span><span class="p">&gt;</span>3<span class="p">&lt;/</span><span class="nt">strong</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">  <span class="p">&lt;</span><span class="nt">button</span><span class="p">&gt;</span>再掃一批<span class="p">&lt;/</span><span class="nt">button</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c">&lt;!-- End（掃完） --&gt;</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="p">&lt;</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;filter-status&#34;</span><span class="p">&gt;</span>已全部掃完、共命中 <span class="p">&lt;</span><span class="nt">strong</span><span class="p">&gt;</span>12<span class="p">&lt;/</span><span class="nt">strong</span><span class="p">&gt;</span> 筆<span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c">&lt;!-- Empty (filter) --&gt;</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="p">&lt;</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;filter-status&#34;</span><span class="p">&gt;</span>已掃 <span class="p">&lt;</span><span class="nt">strong</span><span class="p">&gt;</span>24<span class="p">&lt;/</span><span class="nt">strong</span><span class="p">&gt;</span>、沒有命中
</span></span><span class="line"><span class="ln">14</span><span class="cl">  <span class="p">&lt;</span><span class="nt">button</span><span class="p">&gt;</span>再掃一批<span class="p">&lt;/</span><span class="nt">button</span><span class="p">&gt;</span> 或 <span class="p">&lt;</span><span class="nt">a</span><span class="p">&gt;</span>清除 filter<span class="p">&lt;/</span><span class="nt">a</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span></span></span></code></pre></div><hr>
<h2 id="進度更新時機">進度更新時機</h2>
<h3 id="即時更新每筆">即時更新（每筆）</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">for</span> <span class="p">(</span><span class="kr">const</span> <span class="nx">item</span> <span class="k">of</span> <span class="nx">stream</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="nx">scanned</span><span class="o">++</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="nx">matches</span><span class="p">(</span><span class="nx">item</span><span class="p">))</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="nx">matched</span><span class="o">++</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="nx">appendResult</span><span class="p">(</span><span class="nx">item</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">  <span class="nx">updateUI</span><span class="p">(</span><span class="nx">scanned</span><span class="p">,</span> <span class="nx">matched</span><span class="p">);</span>  <span class="c1">// 每筆更新
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c1"></span><span class="p">}</span></span></span></code></pre></div><p>UX 順、但 DOM 操作頻繁、可能 jank。</p>
<h3 id="批次更新每批">批次更新（每批）</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="kr">const</span> <span class="nx">batch</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">fetchNext</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nx">scanned</span> <span class="o">+=</span> <span class="nx">batch</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="kr">const</span> <span class="nx">m</span> <span class="o">=</span> <span class="nx">batch</span><span class="p">.</span><span class="nx">filter</span><span class="p">(</span><span class="nx">matches</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="nx">matched</span> <span class="o">+=</span> <span class="nx">m</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="nx">appendResults</span><span class="p">(</span><span class="nx">m</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="nx">updateUI</span><span class="p">(</span><span class="nx">scanned</span><span class="p">,</span> <span class="nx">matched</span><span class="p">);</span>  <span class="c1">// 每批一次
</span></span></span></code></pre></div><p>DOM 操作少、但 UX 不夠順（一段時間沒動）。</p>
<h3 id="推薦每批--載入中-spinner">推薦：每批 + 載入中 spinner</h3>
<p>批次後更新數字、批次間顯示 spinner。最平衡。</p>
<hr>
<h2 id="跟自動續抓b的混合">跟自動續抓（B）的混合</h2>
<p>可以做成「初始自動續抓 N 批、之後切誠實 UX」：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="kr">async</span> <span class="kd">function</span> <span class="nx">searchWithFilter</span><span class="p">(</span><span class="nx">query</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="c1">// 初始自動續抓 3 批（湊一些結果）
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span>  <span class="kr">await</span> <span class="nx">fetchUntilQuota</span><span class="p">(</span><span class="mi">3</span><span class="p">,</span> <span class="nx">autoBatches</span><span class="o">:</span> <span class="mi">3</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</span></span><span class="line"><span class="ln">5</span><span class="cl">  <span class="c1">// 之後使用者手動點「再掃一批」
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"></span>  <span class="nx">showHonestProgressUI</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>混合的好處：使用者一進來就有結果（不是空畫面）、之後續抓由使用者決定。</p>
<hr>
<h2 id="反例">反例</h2>
<h3 id="反例-1只顯示命中-k不顯示已掃-n">反例 1：只顯示「命中 K」、不顯示「已掃 N」</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="ln">1</span><span class="cl"><span class="p">&lt;</span><span class="nt">div</span><span class="p">&gt;</span>找到 3 筆結果<span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span></span></span></code></pre></div><p>使用者不知道是從多少筆裡找的、不知道「再掃會不會有」。</p>
<h3 id="反例-2只顯示共-m--n進度條沒分已掃命中">反例 2：只顯示「共 M / N」進度條、沒分「已掃」「命中」</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="ln">1</span><span class="cl"><span class="p">&lt;</span><span class="nt">progress</span> <span class="na">value</span><span class="o">=</span><span class="s">&#34;24&#34;</span> <span class="na">max</span><span class="o">=</span><span class="s">&#34;150&#34;</span><span class="p">&gt;&lt;/</span><span class="nt">progress</span><span class="p">&gt;</span></span></span></code></pre></div><p>進度條告訴使用者「load 進度」、但「load 進度 ≠ filter 進度」。沒命中時使用者不知道為什麼進度走了 24% 但畫面沒結果。</p>
<h3 id="反例-3再掃一批沒做">反例 3：「再掃一批」沒做</h3>
<p>只顯示三數字、沒提供續抓 button — 使用者看到「已掃 24 沒命中」、不知道下一步。</p>
<hr>
<h2 id="跟-57-三狀態的關係">跟 #57 三狀態的關係</h2>
<p>誠實進度 UX 是 #57 <a href="../loading-empty-end-state-distinction/">Loading / Empty / End 三狀態的區分</a> 在「filter + 分批」情境下的具體實作。三數字提供區分三狀態的訊號：</p>
<table>
  <thead>
      <tr>
          <th>#57 狀態</th>
          <th>對應的三數字組合</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Loading</td>
          <td>已掃增加中、N 還在跑</td>
      </tr>
      <tr>
          <td>Empty (filter)</td>
          <td>已掃 = 24、命中 = 0、還有 → 「再掃」</td>
      </tr>
      <tr>
          <td>End</td>
          <td>已掃 = M、命中 = K（K 可能 0）</td>
      </tr>
      <tr>
          <td>Partial</td>
          <td>已掃 &lt; M、命中 ≥ 1、還有 → 「再掃」</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Filter 後可能 0 筆、source 還有未載入</td>
          <td>用本 pattern</td>
      </tr>
      <tr>
          <td>UI 上只有「找到 K 筆」、沒有「已掃 N」</td>
          <td>補 N — 否則使用者無法判斷</td>
      </tr>
      <tr>
          <td>沒有「再掃一批」按鈕</td>
          <td>補 — 給使用者下一步行動</td>
      </tr>
      <tr>
          <td>工程量允許做策略 A / C</td>
          <td>用 A / C、誠實 UX 是退路</td>
      </tr>
      <tr>
          <td>Match 密集、自動續抓不會爆</td>
          <td>用策略 B、誠實 UX 太顯眼</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：誠實 UX 不是「lazy 解法」、是「sourcing 限制下的合理透明度」。給使用者三數字 + 行動選項、比假裝完美但 silent 失敗好。</p>
<p>跟 <a href="../override-depth-cost-report/">#19 覆寫深度的成本告知</a> 同源：兩者都是「把實作的限制 / 代價攤給使用者、讓使用者參與決策」。差別在 #19 是「實作前告知工程成本」、本卡是「runtime 持續顯示掃描成本」 — 攤出來的位置不同、原則一致：silent 累積負擔是反模式。</p>
]]></content:encoded></item><item><title>Pattern：預先建獨立 index（每種 mode 一份）</title><link>https://tarrragon.github.io/blog/report/pattern-multiple-indexes/</link><pubDate>Sun, 26 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/pattern-multiple-indexes/</guid><description>&lt;h2 id="pattern-一句話">Pattern 一句話&lt;/h2>
&lt;p>Build time 為每種 filter mode 各建一份獨立 source / index、runtime 切換 mode 等於切 source。&lt;/p>
&lt;p>對應 #59 &lt;a href="../filter-source-composition-strategies/">Filter × Source 合成策略&lt;/a> 的策略 C。&lt;/p>
&lt;hr>
&lt;h2 id="何時用何時不用">何時用、何時不用&lt;/h2>
&lt;h3 id="用">用&lt;/h3>
&lt;ul>
&lt;li>能控 source 的 build pipeline（自家 build、不是第三方 API）&lt;/li>
&lt;li>Filter mode 數量有限且穩定（&amp;lt; 5 個、不會爆炸組合）&lt;/li>
&lt;li>兩個（含以上）mode 都重要、流量大、值得獨立 index&lt;/li>
&lt;li>Source 的 query 引擎不支援該 filter（不能用 #61 推進 query）&lt;/li>
&lt;/ul>
&lt;h3 id="不用">不用&lt;/h3>
&lt;ul>
&lt;li>Filter 維度多、組合會爆炸（5 維 × 各 5 選項 = 3125 種 index）&lt;/li>
&lt;li>Index 大小敏感（每份 index 都重複占空間）&lt;/li>
&lt;li>Build pipeline 無法控（外部 API、vendor service）&lt;/li>
&lt;li>Mode 不穩定、常常增刪&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="結構">結構&lt;/h2>
&lt;h3 id="build-pipeline">Build pipeline&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># Pagefind 範例：兩份 index 各自掃不同 region&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">pagefind --site public --output-subdir _pagefind-all
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">pagefind --site public/title-only --output-subdir _pagefind-title
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1"># 或用 --root-selector 限定 source 範圍&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">pagefind --site public --root-selector &lt;span class="s2">&amp;#34;.post-title&amp;#34;&lt;/span> --output-subdir _pagefind-title&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="runtime-切換">Runtime 切換&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kr">const&lt;/span> &lt;span class="nx">indexes&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="nx">all&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="kr">await&lt;/span> &lt;span class="kr">import&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;/_pagefind-all/pagefind.js&amp;#39;&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="nx">title&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="kr">await&lt;/span> &lt;span class="kr">import&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;/_pagefind-title/pagefind.js&amp;#39;&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="p">};&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="kd">function&lt;/span> &lt;span class="nx">search&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">query&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">mode&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">indexes&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="nx">mode&lt;/span>&lt;span class="p">].&lt;/span>&lt;span class="nx">search&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">query&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>每個 mode 對應一份完整 index、search 結果直接是該 mode 的全集。&lt;strong>沒有 post-filter、沒有層錯位&lt;/strong>。&lt;/p>
&lt;hr>
&lt;h2 id="多-index-的成本">多 index 的成本&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>成本面&lt;/th>
 &lt;th>影響&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Build 時間&lt;/td>
 &lt;td>每份 index 各建、線性增加&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>儲存空間&lt;/td>
 &lt;td>每份各自占用（pagefind 約 site 大小 2-5%）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>載入頻寬&lt;/td>
 &lt;td>runtime 載入哪份 = 該 mode 的 size&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>維護&lt;/td>
 &lt;td>改 source / schema 時、所有 index 都要重 build&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>通常 build / 儲存的成本 &amp;lt; 在 runtime 自動續抓（B）的累積請求成本。&lt;/p>
&lt;hr>
&lt;h2 id="跟-59-策略並用">跟 #59 策略並用&lt;/h2>
&lt;p>C 通常跟其他策略並用：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>C + 推進 query (A)&lt;/strong>：在每份 index 內、再用 query filter 細分（如 &lt;code>_pagefind-post.search('css', { filters: { tag: 'js' } })&lt;/code>）&lt;/li>
&lt;li>&lt;strong>C 切 mode + B 自動續抓&lt;/strong>：mode 切換無感、mode 內續抓也無感&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="反例">反例&lt;/h2>
&lt;h3 id="反例-1mode-組合爆炸">反例 1：Mode 組合爆炸&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 5 維 × 各 5 選項 = 3125 份 index&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="k">for&lt;/span> &lt;span class="nb">type&lt;/span> in post page tutorial faq doc&lt;span class="p">;&lt;/span> &lt;span class="k">do&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="k">for&lt;/span> tag in js css html ts py&lt;span class="p">;&lt;/span> &lt;span class="k">do&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> &lt;span class="k">for&lt;/span> date in &lt;span class="m">2020&lt;/span> &lt;span class="m">2021&lt;/span> &lt;span class="m">2022&lt;/span> &lt;span class="m">2023&lt;/span> 2024&lt;span class="p">;&lt;/span> &lt;span class="k">do&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> pagefind --filter &lt;span class="s2">&amp;#34;type=&lt;/span>&lt;span class="nv">$type&lt;/span>&lt;span class="s2"> tag=&lt;/span>&lt;span class="nv">$tag&lt;/span>&lt;span class="s2"> date=&lt;/span>&lt;span class="nv">$date&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> --output-subdir _pagefind-&lt;span class="nv">$type&lt;/span>-&lt;span class="nv">$tag&lt;/span>-&lt;span class="nv">$date&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> &lt;span class="k">done&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl"> &lt;span class="k">done&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="k">done&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>組合爆炸時不能用 C — 改用 A（推進 query）讓 source 一份就好。&lt;/p></description><content:encoded><![CDATA[<h2 id="pattern-一句話">Pattern 一句話</h2>
<p>Build time 為每種 filter mode 各建一份獨立 source / index、runtime 切換 mode 等於切 source。</p>
<p>對應 #59 <a href="../filter-source-composition-strategies/">Filter × Source 合成策略</a> 的策略 C。</p>
<hr>
<h2 id="何時用何時不用">何時用、何時不用</h2>
<h3 id="用">用</h3>
<ul>
<li>能控 source 的 build pipeline（自家 build、不是第三方 API）</li>
<li>Filter mode 數量有限且穩定（&lt; 5 個、不會爆炸組合）</li>
<li>兩個（含以上）mode 都重要、流量大、值得獨立 index</li>
<li>Source 的 query 引擎不支援該 filter（不能用 #61 推進 query）</li>
</ul>
<h3 id="不用">不用</h3>
<ul>
<li>Filter 維度多、組合會爆炸（5 維 × 各 5 選項 = 3125 種 index）</li>
<li>Index 大小敏感（每份 index 都重複占空間）</li>
<li>Build pipeline 無法控（外部 API、vendor service）</li>
<li>Mode 不穩定、常常增刪</li>
</ul>
<hr>
<h2 id="結構">結構</h2>
<h3 id="build-pipeline">Build pipeline</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># Pagefind 範例：兩份 index 各自掃不同 region</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">pagefind --site public --output-subdir _pagefind-all
</span></span><span class="line"><span class="ln">3</span><span class="cl">pagefind --site public/title-only --output-subdir _pagefind-title
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># 或用 --root-selector 限定 source 範圍</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">pagefind --site public --root-selector <span class="s2">&#34;.post-title&#34;</span> --output-subdir _pagefind-title</span></span></code></pre></div><h3 id="runtime-切換">Runtime 切換</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="kr">const</span> <span class="nx">indexes</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="nx">all</span><span class="o">:</span> <span class="kr">await</span> <span class="kr">import</span><span class="p">(</span><span class="s1">&#39;/_pagefind-all/pagefind.js&#39;</span><span class="p">),</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="nx">title</span><span class="o">:</span> <span class="kr">await</span> <span class="kr">import</span><span class="p">(</span><span class="s1">&#39;/_pagefind-title/pagefind.js&#39;</span><span class="p">),</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="p">};</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="kd">function</span> <span class="nx">search</span><span class="p">(</span><span class="nx">query</span><span class="p">,</span> <span class="nx">mode</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">  <span class="k">return</span> <span class="nx">indexes</span><span class="p">[</span><span class="nx">mode</span><span class="p">].</span><span class="nx">search</span><span class="p">(</span><span class="nx">query</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>每個 mode 對應一份完整 index、search 結果直接是該 mode 的全集。<strong>沒有 post-filter、沒有層錯位</strong>。</p>
<hr>
<h2 id="多-index-的成本">多 index 的成本</h2>
<table>
  <thead>
      <tr>
          <th>成本面</th>
          <th>影響</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Build 時間</td>
          <td>每份 index 各建、線性增加</td>
      </tr>
      <tr>
          <td>儲存空間</td>
          <td>每份各自占用（pagefind 約 site 大小 2-5%）</td>
      </tr>
      <tr>
          <td>載入頻寬</td>
          <td>runtime 載入哪份 = 該 mode 的 size</td>
      </tr>
      <tr>
          <td>維護</td>
          <td>改 source / schema 時、所有 index 都要重 build</td>
      </tr>
  </tbody>
</table>
<p>通常 build / 儲存的成本 &lt; 在 runtime 自動續抓（B）的累積請求成本。</p>
<hr>
<h2 id="跟-59-策略並用">跟 #59 策略並用</h2>
<p>C 通常跟其他策略並用：</p>
<ul>
<li><strong>C + 推進 query (A)</strong>：在每份 index 內、再用 query filter 細分（如 <code>_pagefind-post.search('css', { filters: { tag: 'js' } })</code>）</li>
<li><strong>C 切 mode + B 自動續抓</strong>：mode 切換無感、mode 內續抓也無感</li>
</ul>
<hr>
<h2 id="反例">反例</h2>
<h3 id="反例-1mode-組合爆炸">反例 1：Mode 組合爆炸</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 5 維 × 各 5 選項 = 3125 份 index</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">for</span> <span class="nb">type</span> in post page tutorial faq doc<span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="k">for</span> tag in js css html ts py<span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="k">for</span> date in <span class="m">2020</span> <span class="m">2021</span> <span class="m">2022</span> <span class="m">2023</span> 2024<span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">      pagefind --filter <span class="s2">&#34;type=</span><span class="nv">$type</span><span class="s2"> tag=</span><span class="nv">$tag</span><span class="s2"> date=</span><span class="nv">$date</span><span class="s2">&#34;</span> --output-subdir _pagefind-<span class="nv">$type</span>-<span class="nv">$tag</span>-<span class="nv">$date</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="k">done</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">  <span class="k">done</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="k">done</span></span></span></code></pre></div><p>組合爆炸時不能用 C — 改用 A（推進 query）讓 source 一份就好。</p>
<h3 id="反例-2mode-不穩定常常增減">反例 2：Mode 不穩定、常常增減</h3>
<p>每加一個 mode、build pipeline 多一份、deploy 多一份。如果 mode 半年內會大改、不適合 C。</p>
<h3 id="反例-3index-沒對齊-mode">反例 3：Index 沒對齊 mode</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">pagefind --site public --output-subdir _pagefind-title
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># build 完後沒過濾、其實 index 了 title + content 全部</span></span></span></code></pre></div><p>如果只是改 output 路徑、沒改 index 範圍 → 兩份 index 內容一樣、白做。要用 <code>--root-selector</code> 或 <code>data-pagefind-body</code> 標記正確範圍。</p>
<hr>
<h2 id="跟其他-pattern-的關係">跟其他 Pattern 的關係</h2>
<p>選擇順序：A → C → B → D（見 #61）：</p>
<ul>
<li>A 不行（source 不支援該 filter） → 評估 C</li>
<li>C 不行（mode 爆炸 / 不能控 build） → 退到 B</li>
<li>B 不行（match 稀疏會爆） → 退到 D</li>
</ul>
<p><strong>C 是 A 的 build-time 模擬</strong> — 用 build 時間換 runtime 體驗、跟使用者意圖完全對齊（每份 index = 該 mode 的全集）。</p>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Source 不支援該 filter、想用 A 但做不到</td>
          <td>評估能不能控 build → 是 → C</td>
      </tr>
      <tr>
          <td>Mode 數量 &lt; 5、stable、能控 build</td>
          <td>用本 pattern</td>
      </tr>
      <tr>
          <td>Mode 組合會爆炸（多維 × 多選）</td>
          <td>不要用 C、考慮 A 或重新思考 mode 設計</td>
      </tr>
      <tr>
          <td>兩份 index 內容一樣（沒對齊 mode）</td>
          <td>Build pipeline 出錯、檢查 source 過濾</td>
      </tr>
      <tr>
          <td>Build 時間翻倍但 runtime 體驗沒改善</td>
          <td>重評估：是否值得多份 index</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：C 用 build-time 換 runtime 體驗。前提是 mode 有限、可控、值得 — 否則退到 A 或 B。</p>
]]></content:encoded></item><item><title>Pattern：明示語意縮小（不承諾全集）</title><link>https://tarrragon.github.io/blog/report/pattern-explicit-semantic-narrowing/</link><pubDate>Sun, 26 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/pattern-explicit-semantic-narrowing/</guid><description>&lt;h2 id="pattern-一句話">Pattern 一句話&lt;/h2>
&lt;p>當 filter 必然只在已載入子集上運作、用 UI 文字 / API contract / docstring 明確告訴呼叫者「範圍 = 已載入、不承諾全集」 — 不假裝是全集 filter。&lt;/p>
&lt;p>對應 #59 &lt;a href="../filter-source-composition-strategies/">Filter × Source 合成策略&lt;/a> 的策略 E。&lt;/p>
&lt;hr>
&lt;h2 id="何時用何時不用">何時用、何時不用&lt;/h2>
&lt;h3 id="用">用&lt;/h3>
&lt;ul>
&lt;li>Source 不支援推進 query (A 不可行)&lt;/li>
&lt;li>不能控 build pipeline (C 不可行)&lt;/li>
&lt;li>Match 稀疏、自動續抓會拉爆 (B 不可行)&lt;/li>
&lt;li>工程量限制、做不了 #62 誠實 UX 的三數字&lt;/li>
&lt;li>能接受「filter 範圍 = subset」這個語意縮小、但要使用者知道&lt;/li>
&lt;/ul>
&lt;h3 id="不用">不用&lt;/h3>
&lt;ul>
&lt;li>Source 一次給完整 dataset（沒有 subset、不需要縮小）&lt;/li>
&lt;li>使用者預期 filter 是「全集」、無法接受縮小&lt;/li>
&lt;li>應用情境影響重大決策（finance、medical 等不能接受 silent 範圍縮小）&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="跟策略-d誠實-ux的差別">跟策略 D（誠實 UX）的差別&lt;/h2>
&lt;p>D 跟 E 都是「在 subset 上 filter」、差別在「怎麼告訴使用者」：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>面向&lt;/th>
 &lt;th>D（誠實 UX）&lt;/th>
 &lt;th>E（明示語意縮小）&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>範圍訊號&lt;/td>
 &lt;td>即時數字（已掃 N / 命中 K）&lt;/td>
 &lt;td>文字描述（一次性告知）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>UI 顯眼度&lt;/td>
 &lt;td>高 — 每次都看得到&lt;/td>
 &lt;td>低 — 看一次就過&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>工程量&lt;/td>
 &lt;td>中 — 要實作三數字&lt;/td>
 &lt;td>低 — 改文字 / 加 docstring&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>使用者參與&lt;/td>
 &lt;td>點「再掃一批」續抓&lt;/td>
 &lt;td>不續抓、自己判斷要不要 load more&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>適合&lt;/td>
 &lt;td>filter 是主要互動模式&lt;/td>
 &lt;td>filter 是次要功能、原型期&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>簡言之：D 是「持續顯示掃描範圍」、E 是「告訴一次、之後不再提」。&lt;/p>
&lt;hr>
&lt;h2 id="明示的具體做法">「明示」的具體做法&lt;/h2>
&lt;h3 id="ui-明示">UI 明示&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">input&lt;/span> &lt;span class="na">type&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;search&amp;#34;&lt;/span> &lt;span class="na">placeholder&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;Filter loaded results...&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">small&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;hint&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>只在已載入的結果裡篩選。要看更多請先載入更多。&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">small&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>「Filter loaded results」、「已載入的結果裡」、「載入更多」 — 三個 cue 讓使用者知道範圍。&lt;/p>
&lt;h3 id="api-contract-明示">API contract 明示&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-ts" data-lang="ts">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="cm">/**
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="cm"> * Filter loaded results by predicate.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="cm"> *
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="cm"> * NOTE: Operates on currently loaded subset only.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="cm"> * Does NOT trigger fetch of un-loaded items. To filter the full
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="cm"> * dataset, use {@link searchAll} instead.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="cm"> */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="kd">function&lt;/span> &lt;span class="nx">filterLoaded&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">predicate&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">item&lt;/span>: &lt;span class="kt">Item&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="kr">boolean&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">Item&lt;/span>&lt;span class="p">[];&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>JSDoc / TSDoc 把語意寫進 API、IDE 提示能看到。&lt;/p>
&lt;h3 id="docstring--readme-明示">Docstring / README 明示&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-markdown" data-lang="markdown">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="gu">## Filter behavior
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="gu">&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="sb">`filter()`&lt;/span> only operates on results currently loaded in client.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">If the source uses pagination, items not yet loaded are NOT included.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">For full-dataset filtering, the source must support server-side filter.&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>文件級的明示、給開發者讀。&lt;/p></description><content:encoded><![CDATA[<h2 id="pattern-一句話">Pattern 一句話</h2>
<p>當 filter 必然只在已載入子集上運作、用 UI 文字 / API contract / docstring 明確告訴呼叫者「範圍 = 已載入、不承諾全集」 — 不假裝是全集 filter。</p>
<p>對應 #59 <a href="../filter-source-composition-strategies/">Filter × Source 合成策略</a> 的策略 E。</p>
<hr>
<h2 id="何時用何時不用">何時用、何時不用</h2>
<h3 id="用">用</h3>
<ul>
<li>Source 不支援推進 query (A 不可行)</li>
<li>不能控 build pipeline (C 不可行)</li>
<li>Match 稀疏、自動續抓會拉爆 (B 不可行)</li>
<li>工程量限制、做不了 #62 誠實 UX 的三數字</li>
<li>能接受「filter 範圍 = subset」這個語意縮小、但要使用者知道</li>
</ul>
<h3 id="不用">不用</h3>
<ul>
<li>Source 一次給完整 dataset（沒有 subset、不需要縮小）</li>
<li>使用者預期 filter 是「全集」、無法接受縮小</li>
<li>應用情境影響重大決策（finance、medical 等不能接受 silent 範圍縮小）</li>
</ul>
<hr>
<h2 id="跟策略-d誠實-ux的差別">跟策略 D（誠實 UX）的差別</h2>
<p>D 跟 E 都是「在 subset 上 filter」、差別在「怎麼告訴使用者」：</p>
<table>
  <thead>
      <tr>
          <th>面向</th>
          <th>D（誠實 UX）</th>
          <th>E（明示語意縮小）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>範圍訊號</td>
          <td>即時數字（已掃 N / 命中 K）</td>
          <td>文字描述（一次性告知）</td>
      </tr>
      <tr>
          <td>UI 顯眼度</td>
          <td>高 — 每次都看得到</td>
          <td>低 — 看一次就過</td>
      </tr>
      <tr>
          <td>工程量</td>
          <td>中 — 要實作三數字</td>
          <td>低 — 改文字 / 加 docstring</td>
      </tr>
      <tr>
          <td>使用者參與</td>
          <td>點「再掃一批」續抓</td>
          <td>不續抓、自己判斷要不要 load more</td>
      </tr>
      <tr>
          <td>適合</td>
          <td>filter 是主要互動模式</td>
          <td>filter 是次要功能、原型期</td>
      </tr>
  </tbody>
</table>
<p>簡言之：D 是「持續顯示掃描範圍」、E 是「告訴一次、之後不再提」。</p>
<hr>
<h2 id="明示的具體做法">「明示」的具體做法</h2>
<h3 id="ui-明示">UI 明示</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="ln">1</span><span class="cl"><span class="p">&lt;</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;search&#34;</span> <span class="na">placeholder</span><span class="o">=</span><span class="s">&#34;Filter loaded results...&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="p">&lt;</span><span class="nt">small</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;hint&#34;</span><span class="p">&gt;</span>只在已載入的結果裡篩選。要看更多請先載入更多。<span class="p">&lt;/</span><span class="nt">small</span><span class="p">&gt;</span></span></span></code></pre></div><p>「Filter loaded results」、「已載入的結果裡」、「載入更多」 — 三個 cue 讓使用者知道範圍。</p>
<h3 id="api-contract-明示">API contract 明示</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ts" data-lang="ts"><span class="line"><span class="ln">1</span><span class="cl"><span class="cm">/**
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="cm"> * Filter loaded results by predicate.
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="cm"> *
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="cm"> * NOTE: Operates on currently loaded subset only.
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="cm"> * Does NOT trigger fetch of un-loaded items. To filter the full
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="cm"> * dataset, use {@link searchAll} instead.
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="cm"> */</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="kd">function</span> <span class="nx">filterLoaded</span><span class="p">(</span><span class="nx">predicate</span><span class="o">:</span> <span class="p">(</span><span class="nx">item</span>: <span class="kt">Item</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="kr">boolean</span><span class="p">)</span><span class="o">:</span> <span class="nx">Item</span><span class="p">[];</span></span></span></code></pre></div><p>JSDoc / TSDoc 把語意寫進 API、IDE 提示能看到。</p>
<h3 id="docstring--readme-明示">Docstring / README 明示</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="ln">1</span><span class="cl"><span class="gu">## Filter behavior
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="gu"></span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="sb">`filter()`</span> only operates on results currently loaded in client.
</span></span><span class="line"><span class="ln">4</span><span class="cl">If the source uses pagination, items not yet loaded are NOT included.
</span></span><span class="line"><span class="ln">5</span><span class="cl">For full-dataset filtering, the source must support server-side filter.</span></span></code></pre></div><p>文件級的明示、給開發者讀。</p>
<hr>
<h2 id="反例">反例</h2>
<h3 id="反例-1silent-縮小不告訴">反例 1：Silent 縮小（不告訴）</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="ln">1</span><span class="cl"><span class="p">&lt;</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;search&#34;</span> <span class="na">placeholder</span><span class="o">=</span><span class="s">&#34;Filter results...&#34;</span><span class="p">&gt;</span></span></span></code></pre></div><p>「Filter results」沒指明「only loaded」 — 使用者預設是全集 filter、實際是 subset → 撞回 #55 <a href="../view-layer-filter-vs-source-layer/">層錯位</a> 的語意縫。</p>
<h3 id="反例-2明示位置使用者看不到">反例 2：明示位置使用者看不到</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ts" data-lang="ts"><span class="line"><span class="ln">1</span><span class="cl"><span class="cm">/**
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="cm"> * Filter results.
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="cm"> * Note: subset only.
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="cm"> */</span></span></span></code></pre></div><p>使用者只看 UI、不讀 docstring — 「明示」要在使用者會看到的位置（UI hint、tooltip、行為描述）。</p>
<h3 id="反例-3明示但不清楚">反例 3：明示但不清楚</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="ln">1</span><span class="cl"><span class="p">&lt;</span><span class="nt">small</span><span class="p">&gt;</span>限定範圍篩選<span class="p">&lt;/</span><span class="nt">small</span><span class="p">&gt;</span></span></span></code></pre></div><p>「限定範圍」太抽象、沒說明是什麼範圍。要寫具體：「已載入的 N 筆內」「不包含尚未載入的」。</p>
<hr>
<h2 id="何時-e-升級到-d">何時 E 升級到 D</h2>
<p>當以下任一觸發、把 E 升級到 D（誠實 UX 三數字）：</p>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>行動</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>使用者依然誤以為是全集 filter</td>
          <td>升 D — 文字明示不夠</td>
      </tr>
      <tr>
          <td>Filter 後 0 筆的情境變常見</td>
          <td>升 D — 三數字能 disambiguate</td>
      </tr>
      <tr>
          <td>Filter 變主要互動模式（不再是次要功能）</td>
          <td>升 D — 顯眼度需要拉高</td>
      </tr>
      <tr>
          <td>Match 密度高、續抓 ROI 變正</td>
          <td>升 B（自動續抓）</td>
      </tr>
  </tbody>
</table>
<p>E 是「成本低的退路」、不是長期解。當需求成熟、應該升級到 D / A / C。</p>
<hr>
<h2 id="跟其他-pattern-的關係">跟其他 Pattern 的關係</h2>
<ul>
<li>E 是策略順序 A → C → B → D 之外的「最後退路」</li>
<li>E 跟 D 都是「在 subset 上做」、差別在告知方式</li>
<li>E 跟 #55 silent 反模式的差別：<strong>E 是 explicit 縮小、silent 是 implicit 縮小</strong></li>
</ul>
<p>選擇順序（重申）：<strong>A 推進 → C 多 index → B 自動續抓 → D 誠實 UX → E 明示縮小 → silent（反模式）</strong></p>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Source 不支援、工程量做不了 D</td>
          <td>用本 pattern</td>
      </tr>
      <tr>
          <td>Filter 行為已決定是 subset、但 UI 沒寫</td>
          <td>補 UI hint</td>
      </tr>
      <tr>
          <td>API 沒 docstring 說明 filter 範圍</td>
          <td>補 docstring</td>
      </tr>
      <tr>
          <td>使用者反映「filter 結果跟我想的不一樣」</td>
          <td>E 沒成功、升級到 D 或 A</td>
      </tr>
      <tr>
          <td>內心 OS：「反正 subset 就是 subset、寫了也沒人看」</td>
          <td>停 — silent 縮小是 #55 反模式</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：能接受語意縮小是可以、但必須明示。Silent 縮小（沒告知就 subset）等於 #55 層錯位、是反模式。E 的價值在「明示」這個動作、不在「subset」這個事實。</p>
]]></content:encoded></item><item><title>決策呈現：選項 + 推薦 + 開放修改</title><link>https://tarrragon.github.io/blog/report/decision-presentation-options-recommendation/</link><pubDate>Sun, 26 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/decision-presentation-options-recommendation/</guid><description>&lt;h2 id="結論">結論&lt;/h2>
&lt;p>把決策交給使用者時、用三層格式：&lt;strong>選項列表 + 每選項適配性 + 標出推薦 + 「想改？」開放&lt;/strong>。不要用「你覺得呢？」「你想怎麼做？」這類開放問。&lt;/p>
&lt;p>開放問看似尊重、實際把「整理問題」的成本完全丟給使用者；推薦看似越權、實際讓使用者用「同意 / 反對」這個低成本動作完成決策。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼開放問是反模式">為什麼開放問是反模式&lt;/h2>
&lt;p>開放問（&amp;ldquo;你想怎麼做？&amp;quot;）預設使用者&lt;strong>已經完成&lt;/strong>這幾步：&lt;/p>
&lt;ol>
&lt;li>知道有哪些選項&lt;/li>
&lt;li>每個選項的成本與風險&lt;/li>
&lt;li>哪個選項最適合這個情境&lt;/li>
&lt;li>願意在當下重新整理思緒、把答案寫成完整指令&lt;/li>
&lt;/ol>
&lt;p>如果使用者已經做完這四步、他不會找你來問 — 他會直接下指令。會走到「需要決策」這個動作、通常是因為&lt;strong>手上沒有完整 1-3&lt;/strong>、需要對方協助整理。&lt;/p>
&lt;p>開放問 = 把「整理選項」這件事再丟回去、讓使用者重新做一遍你已經做過的功課。&lt;/p>
&lt;hr>
&lt;h2 id="三層格式的展開">三層格式的展開&lt;/h2>
&lt;h3 id="layer-1選項列表">Layer 1：選項列表&lt;/h3>
&lt;p>把所有合理選項列出（包含「不做」這個選項、如果合理）。每個選項一行、不超過 6 個 — 超過代表還沒篩選完、不該丟給使用者。&lt;/p>
&lt;h3 id="layer-2適配性--取捨">Layer 2：適配性 / 取捨&lt;/h3>
&lt;p>每個選項配一句「為什麼適合 / 不適合」 — 維度可以是成本、風險、相容性、複雜度等、依任務選 1-2 個維度。&lt;/p>
&lt;h3 id="layer-3標推薦--開放修改">Layer 3：標推薦 + 開放修改&lt;/h3>
&lt;p>明確說「我推薦 X、因為 Y」、然後加一句「想改成其他選項或調整、跟我說」。推薦不是越權、是&lt;strong>把判斷攤開供質疑&lt;/strong> — 使用者反對時知道反對什麼、同意時知道同意了什麼。&lt;/p>
&lt;hr>
&lt;h2 id="完整模板">完整模板&lt;/h2>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">我看到三個方向：
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">| 選項 | 適配性 | 取捨 |
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">|---|---|---|
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">| A 用 X 庫 | 既有依賴、易維護 | 功能受限、要小妥協 |
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">| B 自己寫 | 完全控制 | 維護成本、邊界 case 要寫測試 |
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">| C 不做 | 0 成本 | 使用者繼續手動 |
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">9&lt;/span>&lt;span class="cl">我會選 **A**、因為這個 feature 不是核心、用既有依賴的維護負擔最低。想改成 B 或補充 C 沒問題、跟我說。&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>關鍵：使用者只需要回 &amp;ldquo;好&amp;rdquo;（同意）或 &amp;ldquo;改 B、原因是 ⋯⋯&amp;quot;（反對 + 提供新訊息）— 不必重新整理整個問題空間。&lt;/p>
&lt;hr>
&lt;h2 id="反模式對照">反模式對照&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>反模式&lt;/th>
 &lt;th>為什麼不好&lt;/th>
 &lt;th>修法&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&amp;ldquo;你想怎麼做？&amp;rdquo;&lt;/td>
 &lt;td>把整個決策空間丟回去&lt;/td>
 &lt;td>列選項、給推薦&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&amp;ldquo;我可以選 A 或 B、你選哪個？&amp;rdquo;&lt;/td>
 &lt;td>沒講 A/B 差異、使用者要自己 reverse engineer&lt;/td>
 &lt;td>補上每選項的適配性&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&amp;ldquo;我建議 A&amp;rdquo;（沒講為什麼）&lt;/td>
 &lt;td>推薦不可質疑、使用者只能盲信或盲拒&lt;/td>
 &lt;td>補 reason、讓推薦可質疑&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&amp;ldquo;我覺得 A 比較好、不過 B 也行&amp;rdquo;&lt;/td>
 &lt;td>推薦不夠明確、使用者不知道你真的傾向哪邊&lt;/td>
 &lt;td>標明推薦、別騎牆&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>給 10+ 選項&lt;/td>
 &lt;td>篩選沒做完、認知超載&lt;/td>
 &lt;td>自己先篩到 ≤ 5、剩下歸為「其他可討論」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&amp;ldquo;你授權我做嗎？&amp;rdquo;&lt;/td>
 &lt;td>Yes/No 二選、缺中間態&lt;/td>
 &lt;td>給選項表、讓使用者選擇執行範圍&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="何時可以例外純開放問是合理的">何時可以例外（純開放問是合理的）&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>情境&lt;/th>
 &lt;th>為什麼開放問合理&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>探索期、選項本身還沒成形&lt;/td>
 &lt;td>還沒到「該選」的階段、需要先 brainstorm&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>使用者明確說「我來決定方向、你別給意見」&lt;/td>
 &lt;td>推薦會干擾&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>主觀偏好題（命名、配色、文字風格）&lt;/td>
 &lt;td>沒「客觀適配性」可比較&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>使用者已給出完整偏好、執行細節純客戶化&lt;/td>
 &lt;td>不需要推薦、純執行&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>四類共同點：&lt;strong>整理選項這個工作不需要做、或不該由你做&lt;/strong>。其他情境都該套三層格式。&lt;/p>
&lt;hr>
&lt;h2 id="跟其他卡的關係">跟其他卡的關係&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>卡&lt;/th>
 &lt;th>關係&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="../filter-instruction-clarification/">#58 模糊指令的篩選三問&lt;/a>&lt;/td>
 &lt;td>#58 是「使用者下了模糊指令、你列三問澄清」、本卡是「你列了選項、用三層格式呈現」— 同一條協議的兩端&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="../filter-source-composition-strategies/">#59 五策略選擇矩陣&lt;/a>&lt;/td>
 &lt;td>#59 的「五策略 × 適配性表」就是本卡 Layer 1+2 的具體展現&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="../ease-of-writing-vs-intent-alignment/">#67 寫作便利度跟意圖對齊反相關&lt;/a>&lt;/td>
 &lt;td>開放問是「容易寫」的格式（少打字）、跟使用者意圖對齊（不重做功課）反相關&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="../external-trigger-for-high-roi-work/">#72 高 ROI 無外部觸發&lt;/a>&lt;/td>
 &lt;td>「列選項 + 標推薦」是高 ROI 但無觸發的工作（多打字、慢）、需要協議結構強制&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="../decision-dialogue-dimensions/">#79 決策對話的五維度&lt;/a>&lt;/td>
 &lt;td>本卡是 #79「呈現格式」維度的展開 — 開放問 vs 結構表 + 推薦&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="判讀徵兆">判讀徵兆&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>訊號&lt;/th>
 &lt;th>該做的事&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>寫到「你想怎麼做？」&lt;/td>
 &lt;td>改成三層格式、把選項與推薦攤開&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>推薦的 reason 寫成「我覺得 A 比較好」&lt;/td>
 &lt;td>補實質判準（成本 / 風險 / 相容性）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>一次列 7+ 選項&lt;/td>
 &lt;td>自己沒篩夠、再過濾一遍&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>使用者回 &amp;ldquo;都可以、你決定&amp;rdquo;&lt;/td>
 &lt;td>推薦不夠明確、改成「我做 X、除非你反對」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>推薦後使用者每次都同意&lt;/td>
 &lt;td>推薦變單純的「你決定」、檢查使用者是否還有實質參與空間&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>使用者回「為什麼？」追問 reason&lt;/td>
 &lt;td>reason 寫不夠清楚、補強&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>核心&lt;/strong>：決策呈現的目標不是「讓使用者參與」、是「讓使用者用最低成本參與」。低成本來自選項已經整理好、推薦已經攤開、只需要說「同意」或「改 X」。&lt;/p></description><content:encoded><![CDATA[<h2 id="結論">結論</h2>
<p>把決策交給使用者時、用三層格式：<strong>選項列表 + 每選項適配性 + 標出推薦 + 「想改？」開放</strong>。不要用「你覺得呢？」「你想怎麼做？」這類開放問。</p>
<p>開放問看似尊重、實際把「整理問題」的成本完全丟給使用者；推薦看似越權、實際讓使用者用「同意 / 反對」這個低成本動作完成決策。</p>
<hr>
<h2 id="為什麼開放問是反模式">為什麼開放問是反模式</h2>
<p>開放問（&ldquo;你想怎麼做？&quot;）預設使用者<strong>已經完成</strong>這幾步：</p>
<ol>
<li>知道有哪些選項</li>
<li>每個選項的成本與風險</li>
<li>哪個選項最適合這個情境</li>
<li>願意在當下重新整理思緒、把答案寫成完整指令</li>
</ol>
<p>如果使用者已經做完這四步、他不會找你來問 — 他會直接下指令。會走到「需要決策」這個動作、通常是因為<strong>手上沒有完整 1-3</strong>、需要對方協助整理。</p>
<p>開放問 = 把「整理選項」這件事再丟回去、讓使用者重新做一遍你已經做過的功課。</p>
<hr>
<h2 id="三層格式的展開">三層格式的展開</h2>
<h3 id="layer-1選項列表">Layer 1：選項列表</h3>
<p>把所有合理選項列出（包含「不做」這個選項、如果合理）。每個選項一行、不超過 6 個 — 超過代表還沒篩選完、不該丟給使用者。</p>
<h3 id="layer-2適配性--取捨">Layer 2：適配性 / 取捨</h3>
<p>每個選項配一句「為什麼適合 / 不適合」 — 維度可以是成本、風險、相容性、複雜度等、依任務選 1-2 個維度。</p>
<h3 id="layer-3標推薦--開放修改">Layer 3：標推薦 + 開放修改</h3>
<p>明確說「我推薦 X、因為 Y」、然後加一句「想改成其他選項或調整、跟我說」。推薦不是越權、是<strong>把判斷攤開供質疑</strong> — 使用者反對時知道反對什麼、同意時知道同意了什麼。</p>
<hr>
<h2 id="完整模板">完整模板</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">我看到三個方向：
</span></span><span class="line"><span class="ln">2</span><span class="cl">
</span></span><span class="line"><span class="ln">3</span><span class="cl">| 選項 | 適配性 | 取捨 |
</span></span><span class="line"><span class="ln">4</span><span class="cl">|---|---|---|
</span></span><span class="line"><span class="ln">5</span><span class="cl">| A 用 X 庫 | 既有依賴、易維護 | 功能受限、要小妥協 |
</span></span><span class="line"><span class="ln">6</span><span class="cl">| B 自己寫 | 完全控制 | 維護成本、邊界 case 要寫測試 |
</span></span><span class="line"><span class="ln">7</span><span class="cl">| C 不做 | 0 成本 | 使用者繼續手動 |
</span></span><span class="line"><span class="ln">8</span><span class="cl">
</span></span><span class="line"><span class="ln">9</span><span class="cl">我會選 **A**、因為這個 feature 不是核心、用既有依賴的維護負擔最低。想改成 B 或補充 C 沒問題、跟我說。</span></span></code></pre></div><p>關鍵：使用者只需要回 &ldquo;好&rdquo;（同意）或 &ldquo;改 B、原因是 ⋯⋯&quot;（反對 + 提供新訊息）— 不必重新整理整個問題空間。</p>
<hr>
<h2 id="反模式對照">反模式對照</h2>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>為什麼不好</th>
          <th>修法</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>&ldquo;你想怎麼做？&rdquo;</td>
          <td>把整個決策空間丟回去</td>
          <td>列選項、給推薦</td>
      </tr>
      <tr>
          <td>&ldquo;我可以選 A 或 B、你選哪個？&rdquo;</td>
          <td>沒講 A/B 差異、使用者要自己 reverse engineer</td>
          <td>補上每選項的適配性</td>
      </tr>
      <tr>
          <td>&ldquo;我建議 A&rdquo;（沒講為什麼）</td>
          <td>推薦不可質疑、使用者只能盲信或盲拒</td>
          <td>補 reason、讓推薦可質疑</td>
      </tr>
      <tr>
          <td>&ldquo;我覺得 A 比較好、不過 B 也行&rdquo;</td>
          <td>推薦不夠明確、使用者不知道你真的傾向哪邊</td>
          <td>標明推薦、別騎牆</td>
      </tr>
      <tr>
          <td>給 10+ 選項</td>
          <td>篩選沒做完、認知超載</td>
          <td>自己先篩到 ≤ 5、剩下歸為「其他可討論」</td>
      </tr>
      <tr>
          <td>&ldquo;你授權我做嗎？&rdquo;</td>
          <td>Yes/No 二選、缺中間態</td>
          <td>給選項表、讓使用者選擇執行範圍</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="何時可以例外純開放問是合理的">何時可以例外（純開放問是合理的）</h2>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>為什麼開放問合理</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>探索期、選項本身還沒成形</td>
          <td>還沒到「該選」的階段、需要先 brainstorm</td>
      </tr>
      <tr>
          <td>使用者明確說「我來決定方向、你別給意見」</td>
          <td>推薦會干擾</td>
      </tr>
      <tr>
          <td>主觀偏好題（命名、配色、文字風格）</td>
          <td>沒「客觀適配性」可比較</td>
      </tr>
      <tr>
          <td>使用者已給出完整偏好、執行細節純客戶化</td>
          <td>不需要推薦、純執行</td>
      </tr>
  </tbody>
</table>
<p>四類共同點：<strong>整理選項這個工作不需要做、或不該由你做</strong>。其他情境都該套三層格式。</p>
<hr>
<h2 id="跟其他卡的關係">跟其他卡的關係</h2>
<table>
  <thead>
      <tr>
          <th>卡</th>
          <th>關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../filter-instruction-clarification/">#58 模糊指令的篩選三問</a></td>
          <td>#58 是「使用者下了模糊指令、你列三問澄清」、本卡是「你列了選項、用三層格式呈現」— 同一條協議的兩端</td>
      </tr>
      <tr>
          <td><a href="../filter-source-composition-strategies/">#59 五策略選擇矩陣</a></td>
          <td>#59 的「五策略 × 適配性表」就是本卡 Layer 1+2 的具體展現</td>
      </tr>
      <tr>
          <td><a href="../ease-of-writing-vs-intent-alignment/">#67 寫作便利度跟意圖對齊反相關</a></td>
          <td>開放問是「容易寫」的格式（少打字）、跟使用者意圖對齊（不重做功課）反相關</td>
      </tr>
      <tr>
          <td><a href="../external-trigger-for-high-roi-work/">#72 高 ROI 無外部觸發</a></td>
          <td>「列選項 + 標推薦」是高 ROI 但無觸發的工作（多打字、慢）、需要協議結構強制</td>
      </tr>
      <tr>
          <td><a href="../decision-dialogue-dimensions/">#79 決策對話的五維度</a></td>
          <td>本卡是 #79「呈現格式」維度的展開 — 開放問 vs 結構表 + 推薦</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>寫到「你想怎麼做？」</td>
          <td>改成三層格式、把選項與推薦攤開</td>
      </tr>
      <tr>
          <td>推薦的 reason 寫成「我覺得 A 比較好」</td>
          <td>補實質判準（成本 / 風險 / 相容性）</td>
      </tr>
      <tr>
          <td>一次列 7+ 選項</td>
          <td>自己沒篩夠、再過濾一遍</td>
      </tr>
      <tr>
          <td>使用者回 &ldquo;都可以、你決定&rdquo;</td>
          <td>推薦不夠明確、改成「我做 X、除非你反對」</td>
      </tr>
      <tr>
          <td>推薦後使用者每次都同意</td>
          <td>推薦變單純的「你決定」、檢查使用者是否還有實質參與空間</td>
      </tr>
      <tr>
          <td>使用者回「為什麼？」追問 reason</td>
          <td>reason 寫不夠清楚、補強</td>
      </tr>
  </tbody>
</table>
<p><strong>核心</strong>：決策呈現的目標不是「讓使用者參與」、是「讓使用者用最低成本參與」。低成本來自選項已經整理好、推薦已經攤開、只需要說「同意」或「改 X」。</p>
]]></content:encoded></item><item><title>反省任務預設複選：互斥要證明、不互斥是預設</title><link>https://tarrragon.github.io/blog/report/retrospective-multi-select-default/</link><pubDate>Sun, 26 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/retrospective-multi-select-default/</guid><description>&lt;h2 id="結論">結論&lt;/h2>
&lt;p>寫到「我們還可以做什麼？」「下一步該往哪走？」「這次反省我們學到 X、Y、Z 哪個重要？」這類&lt;strong>反省 / 改進方向&lt;/strong>問題、預設給&lt;strong>複選&lt;/strong>（checkbox semantics、可選多項）。&lt;/p>
&lt;p>執行類決策多半互斥（用 A 工具 vs B 工具）、反省類決策多半不互斥（思考 X + 思考 Y + 思考 Z 都做、互不干擾、合在一起反省深度更高）。把反省題用單選格式呈現 = 強迫使用者排序「思考的優先級」、結果通常 1 被選、2-N 被丟。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼反省題不該單選">為什麼反省題不該單選&lt;/h2>
&lt;p>反省任務的 output 通常是「對問題的多面向理解 / 多條後續動作」、不是「一個結論」。多選項共存時：&lt;/p>
&lt;ul>
&lt;li>思考 A 跟思考 B 互不干擾（同一個事件可從多角度反省）&lt;/li>
&lt;li>動作 X 跟動作 Y 互補（補卡片 + 寫測試 + 改流程）&lt;/li>
&lt;li>多角度疊加 → 反省深度更高、不是「一條最佳」就夠&lt;/li>
&lt;/ul>
&lt;p>把這種題目強制成單選、暗示「只能挑一個」、使用者就只挑一個。其他 N-1 個本來該做的、被結構性跳過 — 不是因為不重要、是因為&lt;strong>呈現格式叫他這樣選&lt;/strong>。&lt;/p>
&lt;hr>
&lt;h2 id="反省-vs-執行呈現格式的差異">反省 vs 執行：呈現格式的差異&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>維度&lt;/th>
 &lt;th>執行類決策&lt;/th>
 &lt;th>反省類決策&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>互斥性&lt;/td>
 &lt;td>通常互斥（slot 只能放一個）&lt;/td>
 &lt;td>通常不互斥（多角度共存）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>預設格式&lt;/td>
 &lt;td>單選 + 推薦（&lt;a href="../decision-presentation-options-recommendation/">#74&lt;/a>）&lt;/td>
 &lt;td>複選 + 全列&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>推薦語氣&lt;/td>
 &lt;td>「我建議 A」&lt;/td>
 &lt;td>「都該做、優先 X」或「全做」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>使用者回應&lt;/td>
 &lt;td>同意 / 改 B&lt;/td>
 &lt;td>「全做」/「先 1+2、3 下輪」/「跳過 X」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>「全做」是合法回應&lt;/td>
 &lt;td>通常不（資源有限 / 互斥）&lt;/td>
 &lt;td>通常是&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>關鍵：&lt;strong>識別這個決策是「執行」還是「反省」&lt;/strong>、決定用哪種格式。&lt;/p>
&lt;hr>
&lt;h2 id="完整模板反省題">完整模板（反省題）&lt;/h2>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">這次經歷我們可以反省幾個方向：
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">1. 補卡片做分析（沉澱知識）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">2. 補測試固化（避免回退）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">3. 改流程 / 工具觸發（結構性對策）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">4. 整理 case study 文章（對外輸出）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">5. 重做某個決策（如果發現方向錯）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">9&lt;/span>&lt;span class="cl">這些**互不衝突、可以全做**。如果時間有限、我建議優先 1+2（沉澱 + 固化），3+4 下輪。要全做、跳過某幾個、或調整順序、跟我說。&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>關鍵差異 vs 執行類決策：&lt;/p>
&lt;ul>
&lt;li>明示「互不衝突、可以全做」&lt;/li>
&lt;li>推薦是「優先級」而非「選一個」&lt;/li>
&lt;li>「全做」是合法選項 + 「跳過某幾個」也是合法選項&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="反模式">反模式&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>反模式&lt;/th>
 &lt;th>為什麼不好&lt;/th>
 &lt;th>修法&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>反省題用 radio 格式（&amp;ldquo;你想做哪一個？&amp;quot;）&lt;/td>
 &lt;td>強迫單選、丟失多面向&lt;/td>
 &lt;td>改 checkbox 格式&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>不講「可以全選」、使用者預設選一個&lt;/td>
 &lt;td>隱式單選&lt;/td>
 &lt;td>主動標「互不衝突、全做也可以」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>列 5 個方向、第一個是顯然要做的&lt;/td>
 &lt;td>排序暗示 = 隱式推薦、其他被忽略&lt;/td>
 &lt;td>區分「全選 vs 優先選」、明示順序&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>「主要是 1、2-5 也可以做」騎牆&lt;/td>
 &lt;td>推薦不夠明確&lt;/td>
 &lt;td>標清楚 + 講優先級&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>一次列 10+ 個方向&lt;/td>
 &lt;td>認知超載、變成「都不選」&lt;/td>
 &lt;td>自己先聚類、≤ 7 項&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>反省結論寫成「我們學到 X」（單一）&lt;/td>
 &lt;td>多面向 collapse 成單點&lt;/td>
 &lt;td>列出所有面向、再標核心&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="何時反省題該回退到單選">何時反省題該回退到單選&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>情境&lt;/th>
 &lt;th>為什麼&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>多選項真的互斥（不同優先級會排擠）&lt;/td>
 &lt;td>例：「先做 X 還是先做 Y、人力只夠一個」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>反省結論本身互斥（A 對 = B 錯）&lt;/td>
 &lt;td>例：「root cause 是 P 還是 Q」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>使用者明確要單選&lt;/td>
 &lt;td>尊重使用者的決策框架&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>二選一的對比題&lt;/td>
 &lt;td>例：「這次失誤是設計問題還是執行問題」&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>四類共通：&lt;strong>互斥性是真實的、不是格式造成的&lt;/strong>。其他情境預設複選。&lt;/p>
&lt;hr>
&lt;h2 id="跟其他卡的關係">跟其他卡的關係&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>卡&lt;/th>
 &lt;th>關係&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="../decision-presentation-options-recommendation/">#74 決策呈現格式&lt;/a>&lt;/td>
 &lt;td>#74 是執行類決策的格式、本卡是反省類決策的特化 — 兩卡互補&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="../decide-later-as-valid-option/">#77 「現在不決定」是合法選項&lt;/a>&lt;/td>
 &lt;td>反省題的「跳過某幾個」常是隱式延後、本卡跟 #77 對應&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="../verification-timeline-checkpoints/">#68 驗收的時間軸&lt;/a>&lt;/td>
 &lt;td>Ship 後反省 = 多面向、適用本卡&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="../ease-of-writing-vs-intent-alignment/">#67 寫作便利度跟意圖對齊反相關&lt;/a>&lt;/td>
 &lt;td>「列複選 + 標優先級」比「列一個推薦」難寫、容易被簡化成單選&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="../decision-dialogue-dimensions/">#79 決策對話的五維度&lt;/a>&lt;/td>
 &lt;td>本卡是 #79「選項類型」維度的展開 — 單選 radio vs 複選 checkbox&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="判讀徵兆">判讀徵兆&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>訊號&lt;/th>
 &lt;th>該做的事&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>反省題只列「最該做的一個」&lt;/td>
 &lt;td>補上其他面向、改複選&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>使用者回應 &amp;ldquo;1+2&amp;rdquo;、&amp;ldquo;全做&amp;rdquo;、&amp;ldquo;1234&amp;rdquo;&lt;/td>
 &lt;td>確認你給了複選格式、否則使用者得自己 reverse&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>使用者只回 &amp;ldquo;1&amp;rdquo;、然後其他面向沒做到&lt;/td>
 &lt;td>檢查格式是否暗示了單選&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>反省結論 collapse 成一句話&lt;/td>
 &lt;td>退一步、列出多面向再寫總結&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>某面向「明顯該做」、自己已替使用者選&lt;/td>
 &lt;td>取消代選、明示「都該做、想跳過告訴我」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>「先 X、Y 之後再做」每次 Y 都不做&lt;/td>
 &lt;td>跟 &lt;a href="../external-trigger-for-high-roi-work/">#72&lt;/a> 一樣、補 trigger&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>列 10+ 個面向&lt;/td>
 &lt;td>自己聚類、別丟給使用者&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>核心&lt;/strong>：執行決定是「選一個 path」、反省決定是「攤開多面向 + 標優先級」。預設互斥的格式（radio）套用到不互斥的場景（反省）= 結構性把多面向 collapse 成單點、丟失反省應有的深度。&lt;/p></description><content:encoded><![CDATA[<h2 id="結論">結論</h2>
<p>寫到「我們還可以做什麼？」「下一步該往哪走？」「這次反省我們學到 X、Y、Z 哪個重要？」這類<strong>反省 / 改進方向</strong>問題、預設給<strong>複選</strong>（checkbox semantics、可選多項）。</p>
<p>執行類決策多半互斥（用 A 工具 vs B 工具）、反省類決策多半不互斥（思考 X + 思考 Y + 思考 Z 都做、互不干擾、合在一起反省深度更高）。把反省題用單選格式呈現 = 強迫使用者排序「思考的優先級」、結果通常 1 被選、2-N 被丟。</p>
<hr>
<h2 id="為什麼反省題不該單選">為什麼反省題不該單選</h2>
<p>反省任務的 output 通常是「對問題的多面向理解 / 多條後續動作」、不是「一個結論」。多選項共存時：</p>
<ul>
<li>思考 A 跟思考 B 互不干擾（同一個事件可從多角度反省）</li>
<li>動作 X 跟動作 Y 互補（補卡片 + 寫測試 + 改流程）</li>
<li>多角度疊加 → 反省深度更高、不是「一條最佳」就夠</li>
</ul>
<p>把這種題目強制成單選、暗示「只能挑一個」、使用者就只挑一個。其他 N-1 個本來該做的、被結構性跳過 — 不是因為不重要、是因為<strong>呈現格式叫他這樣選</strong>。</p>
<hr>
<h2 id="反省-vs-執行呈現格式的差異">反省 vs 執行：呈現格式的差異</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>執行類決策</th>
          <th>反省類決策</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>互斥性</td>
          <td>通常互斥（slot 只能放一個）</td>
          <td>通常不互斥（多角度共存）</td>
      </tr>
      <tr>
          <td>預設格式</td>
          <td>單選 + 推薦（<a href="../decision-presentation-options-recommendation/">#74</a>）</td>
          <td>複選 + 全列</td>
      </tr>
      <tr>
          <td>推薦語氣</td>
          <td>「我建議 A」</td>
          <td>「都該做、優先 X」或「全做」</td>
      </tr>
      <tr>
          <td>使用者回應</td>
          <td>同意 / 改 B</td>
          <td>「全做」/「先 1+2、3 下輪」/「跳過 X」</td>
      </tr>
      <tr>
          <td>「全做」是合法回應</td>
          <td>通常不（資源有限 / 互斥）</td>
          <td>通常是</td>
      </tr>
  </tbody>
</table>
<p>關鍵：<strong>識別這個決策是「執行」還是「反省」</strong>、決定用哪種格式。</p>
<hr>
<h2 id="完整模板反省題">完整模板（反省題）</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">這次經歷我們可以反省幾個方向：
</span></span><span class="line"><span class="ln">2</span><span class="cl">
</span></span><span class="line"><span class="ln">3</span><span class="cl">1. 補卡片做分析（沉澱知識）
</span></span><span class="line"><span class="ln">4</span><span class="cl">2. 補測試固化（避免回退）
</span></span><span class="line"><span class="ln">5</span><span class="cl">3. 改流程 / 工具觸發（結構性對策）
</span></span><span class="line"><span class="ln">6</span><span class="cl">4. 整理 case study 文章（對外輸出）
</span></span><span class="line"><span class="ln">7</span><span class="cl">5. 重做某個決策（如果發現方向錯）
</span></span><span class="line"><span class="ln">8</span><span class="cl">
</span></span><span class="line"><span class="ln">9</span><span class="cl">這些**互不衝突、可以全做**。如果時間有限、我建議優先 1+2（沉澱 + 固化），3+4 下輪。要全做、跳過某幾個、或調整順序、跟我說。</span></span></code></pre></div><p>關鍵差異 vs 執行類決策：</p>
<ul>
<li>明示「互不衝突、可以全做」</li>
<li>推薦是「優先級」而非「選一個」</li>
<li>「全做」是合法選項 + 「跳過某幾個」也是合法選項</li>
</ul>
<hr>
<h2 id="反模式">反模式</h2>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>為什麼不好</th>
          <th>修法</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>反省題用 radio 格式（&ldquo;你想做哪一個？&quot;）</td>
          <td>強迫單選、丟失多面向</td>
          <td>改 checkbox 格式</td>
      </tr>
      <tr>
          <td>不講「可以全選」、使用者預設選一個</td>
          <td>隱式單選</td>
          <td>主動標「互不衝突、全做也可以」</td>
      </tr>
      <tr>
          <td>列 5 個方向、第一個是顯然要做的</td>
          <td>排序暗示 = 隱式推薦、其他被忽略</td>
          <td>區分「全選 vs 優先選」、明示順序</td>
      </tr>
      <tr>
          <td>「主要是 1、2-5 也可以做」騎牆</td>
          <td>推薦不夠明確</td>
          <td>標清楚 + 講優先級</td>
      </tr>
      <tr>
          <td>一次列 10+ 個方向</td>
          <td>認知超載、變成「都不選」</td>
          <td>自己先聚類、≤ 7 項</td>
      </tr>
      <tr>
          <td>反省結論寫成「我們學到 X」（單一）</td>
          <td>多面向 collapse 成單點</td>
          <td>列出所有面向、再標核心</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="何時反省題該回退到單選">何時反省題該回退到單選</h2>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>為什麼</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>多選項真的互斥（不同優先級會排擠）</td>
          <td>例：「先做 X 還是先做 Y、人力只夠一個」</td>
      </tr>
      <tr>
          <td>反省結論本身互斥（A 對 = B 錯）</td>
          <td>例：「root cause 是 P 還是 Q」</td>
      </tr>
      <tr>
          <td>使用者明確要單選</td>
          <td>尊重使用者的決策框架</td>
      </tr>
      <tr>
          <td>二選一的對比題</td>
          <td>例：「這次失誤是設計問題還是執行問題」</td>
      </tr>
  </tbody>
</table>
<p>四類共通：<strong>互斥性是真實的、不是格式造成的</strong>。其他情境預設複選。</p>
<hr>
<h2 id="跟其他卡的關係">跟其他卡的關係</h2>
<table>
  <thead>
      <tr>
          <th>卡</th>
          <th>關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../decision-presentation-options-recommendation/">#74 決策呈現格式</a></td>
          <td>#74 是執行類決策的格式、本卡是反省類決策的特化 — 兩卡互補</td>
      </tr>
      <tr>
          <td><a href="../decide-later-as-valid-option/">#77 「現在不決定」是合法選項</a></td>
          <td>反省題的「跳過某幾個」常是隱式延後、本卡跟 #77 對應</td>
      </tr>
      <tr>
          <td><a href="../verification-timeline-checkpoints/">#68 驗收的時間軸</a></td>
          <td>Ship 後反省 = 多面向、適用本卡</td>
      </tr>
      <tr>
          <td><a href="../ease-of-writing-vs-intent-alignment/">#67 寫作便利度跟意圖對齊反相關</a></td>
          <td>「列複選 + 標優先級」比「列一個推薦」難寫、容易被簡化成單選</td>
      </tr>
      <tr>
          <td><a href="../decision-dialogue-dimensions/">#79 決策對話的五維度</a></td>
          <td>本卡是 #79「選項類型」維度的展開 — 單選 radio vs 複選 checkbox</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>反省題只列「最該做的一個」</td>
          <td>補上其他面向、改複選</td>
      </tr>
      <tr>
          <td>使用者回應 &ldquo;1+2&rdquo;、&ldquo;全做&rdquo;、&ldquo;1234&rdquo;</td>
          <td>確認你給了複選格式、否則使用者得自己 reverse</td>
      </tr>
      <tr>
          <td>使用者只回 &ldquo;1&rdquo;、然後其他面向沒做到</td>
          <td>檢查格式是否暗示了單選</td>
      </tr>
      <tr>
          <td>反省結論 collapse 成一句話</td>
          <td>退一步、列出多面向再寫總結</td>
      </tr>
      <tr>
          <td>某面向「明顯該做」、自己已替使用者選</td>
          <td>取消代選、明示「都該做、想跳過告訴我」</td>
      </tr>
      <tr>
          <td>「先 X、Y 之後再做」每次 Y 都不做</td>
          <td>跟 <a href="../external-trigger-for-high-roi-work/">#72</a> 一樣、補 trigger</td>
      </tr>
      <tr>
          <td>列 10+ 個面向</td>
          <td>自己聚類、別丟給使用者</td>
      </tr>
  </tbody>
</table>
<p><strong>核心</strong>：執行決定是「選一個 path」、反省決定是「攤開多面向 + 標優先級」。預設互斥的格式（radio）套用到不互斥的場景（反省）= 結構性把多面向 collapse 成單點、丟失反省應有的深度。</p>
]]></content:encoded></item><item><title>Yes/No 二選是隱式 collapse：把多選空間壓成 1 bit</title><link>https://tarrragon.github.io/blog/report/yes-no-binary-collapse/</link><pubDate>Sun, 26 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/yes-no-binary-collapse/</guid><description>&lt;h2 id="結論">結論&lt;/h2>
&lt;p>把決策呈現成 yes/no 問句（&amp;ldquo;需要我繼續嗎？&amp;quot;、&amp;ldquo;要做嗎？&amp;quot;、&amp;ldquo;確認嗎？&amp;quot;）= 把使用者的回應空間壓縮到 1 bit。&lt;strong>1 bit 不夠表達決策&lt;/strong>：使用者實際上有「改方向 / 延後 / 疊加 / 分批 / 反問」等合法選項、被 yes/no 結構藏起來。&lt;/p>
&lt;p>這是 &lt;a href="../decision-dialogue-dimensions/">#79 五維度&lt;/a> collapse 的極致形態 — 預設窄格 + 預設立刻 + 預設單選 + 預設一次完成 + 預設單策略 + &lt;strong>再加一層 reduce 到 binary&lt;/strong>。最容易寫、最隱形、最常見。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼-yesno-看起來合理但其實不夠">為什麼 Yes/No 看起來合理但其實不夠&lt;/h2>
&lt;p>Yes/No 問句的隱含預設：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>使用者已經同意「我提出的方向」是對的&lt;/strong> — 只剩執行 / 不執行&lt;/li>
&lt;li>&lt;strong>沒有「微調」的中間態&lt;/strong> — 想改一點點、要重起一輪 yes/no&lt;/li>
&lt;li>&lt;strong>沒有「再多想想」&lt;/strong> — 沒有 yes 也沒有 no 的時候被迫硬選&lt;/li>
&lt;li>&lt;strong>沒有「順便加 X」&lt;/strong> — 想疊加策略要破格自己提&lt;/li>
&lt;li>&lt;strong>沒有「先做一半」&lt;/strong> — 全做 vs 不做、沒有分批選項&lt;/li>
&lt;/ol>
&lt;p>實際上多數決策不是 1 bit 寬。當 agent 寫「要繼續嗎？」、使用者經常回答「等等、我想先 X」、「順便也做 Y」、「先做 A 不做 B」 — 這些回應全部都不是 yes 也不是 no、是&lt;strong>對話越過 yes/no 結構自我修正&lt;/strong>。&lt;/p>
&lt;p>修正成本由使用者承擔 = 反過來 #74 的問題（把整理成本丟回使用者）。&lt;/p>
&lt;hr>
&lt;h2 id="yesno-的常見變種">Yes/No 的常見變種&lt;/h2>
&lt;p>agent 容易寫的 yes/no 變種：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>變種&lt;/th>
 &lt;th>隱含預設&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&amp;ldquo;需要我 X 嗎？&amp;rdquo;&lt;/td>
 &lt;td>我已經想好要 X、你只要批准&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&amp;ldquo;要繼續嗎？&amp;rdquo;&lt;/td>
 &lt;td>流程往下走、你只要不阻止&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&amp;ldquo;OK 嗎？&amp;rdquo;&lt;/td>
 &lt;td>我做了 X、你只要不反對&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&amp;ldquo;我先做 X、有問題嗎？&amp;rdquo;&lt;/td>
 &lt;td>我打算開幹、你只要沒異議&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&amp;ldquo;確認一下、是這樣嗎？&amp;rdquo;&lt;/td>
 &lt;td>你之前說的我理解對嗎、回 yes/no&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&amp;ldquo;如果 OK 我繼續做&amp;rdquo;&lt;/td>
 &lt;td>同意 → 繼續、不同意 → 卡住&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>每個變種都把使用者推進「同意 / 不同意」的二元框、其他合法回應（改 / 延後 / 疊加 / 分批 / 反問澄清）需要使用者自己破格。&lt;/p>
&lt;hr>
&lt;h2 id="修法把-yesno-翻成多維展開">修法：把 Yes/No 翻成多維展開&lt;/h2>
&lt;p>把 binary 拆回多維：&lt;/p>
&lt;h3 id="beforeyesno">Before（yes/no）&lt;/h3>
&lt;blockquote>
&lt;p>&amp;ldquo;我打算 ship D（UX hint）、需要我繼續嗎？&amp;rdquo;&lt;/p>&lt;/blockquote>
&lt;h3 id="after五維展開">After（五維展開）&lt;/h3>
&lt;blockquote>
&lt;p>接下來的選項：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>選項&lt;/th>
 &lt;th>適配性&lt;/th>
 &lt;th>取捨&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>立刻 ship D&lt;/td>
 &lt;td>解眼前痛&lt;/td>
 &lt;td>不解根因&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>延後 D、先補 telemetry&lt;/td>
 &lt;td>等資料&lt;/td>
 &lt;td>推遲 1 週&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>跳過 D、做 B/C&lt;/td>
 &lt;td>結構 fix&lt;/td>
 &lt;td>風險高、要驗證&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>D + 寫測試固化&lt;/td>
 &lt;td>疊加&lt;/td>
 &lt;td>多寫 30 分鐘&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>我推薦 &lt;strong>D + 寫測試固化&lt;/strong>（疊加）— 解眼前痛 + 防回退。改 / 延後 / 跳過都行、跟我說。&lt;/p>&lt;/blockquote>
&lt;h3 id="對照">對照&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>維度&lt;/th>
 &lt;th>Before&lt;/th>
 &lt;th>After&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>呈現格式&lt;/td>
 &lt;td>yes/no 一句&lt;/td>
 &lt;td>結構表 + 推薦&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>策略數&lt;/td>
 &lt;td>單一 D&lt;/td>
 &lt;td>D + 測試疊加&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>批次邊界&lt;/td>
 &lt;td>一次&lt;/td>
 &lt;td>隱含分批（B/C 下輪）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>時間軸&lt;/td>
 &lt;td>立刻&lt;/td>
 &lt;td>提供延後條件&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>選項類型&lt;/td>
 &lt;td>binary&lt;/td>
 &lt;td>多選、可疊加&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>明顯多花字、明顯讓使用者更容易做正確決策。&lt;/p></description><content:encoded><![CDATA[<h2 id="結論">結論</h2>
<p>把決策呈現成 yes/no 問句（&ldquo;需要我繼續嗎？&quot;、&ldquo;要做嗎？&quot;、&ldquo;確認嗎？&quot;）= 把使用者的回應空間壓縮到 1 bit。<strong>1 bit 不夠表達決策</strong>：使用者實際上有「改方向 / 延後 / 疊加 / 分批 / 反問」等合法選項、被 yes/no 結構藏起來。</p>
<p>這是 <a href="../decision-dialogue-dimensions/">#79 五維度</a> collapse 的極致形態 — 預設窄格 + 預設立刻 + 預設單選 + 預設一次完成 + 預設單策略 + <strong>再加一層 reduce 到 binary</strong>。最容易寫、最隱形、最常見。</p>
<hr>
<h2 id="為什麼-yesno-看起來合理但其實不夠">為什麼 Yes/No 看起來合理但其實不夠</h2>
<p>Yes/No 問句的隱含預設：</p>
<ol>
<li><strong>使用者已經同意「我提出的方向」是對的</strong> — 只剩執行 / 不執行</li>
<li><strong>沒有「微調」的中間態</strong> — 想改一點點、要重起一輪 yes/no</li>
<li><strong>沒有「再多想想」</strong> — 沒有 yes 也沒有 no 的時候被迫硬選</li>
<li><strong>沒有「順便加 X」</strong> — 想疊加策略要破格自己提</li>
<li><strong>沒有「先做一半」</strong> — 全做 vs 不做、沒有分批選項</li>
</ol>
<p>實際上多數決策不是 1 bit 寬。當 agent 寫「要繼續嗎？」、使用者經常回答「等等、我想先 X」、「順便也做 Y」、「先做 A 不做 B」 — 這些回應全部都不是 yes 也不是 no、是<strong>對話越過 yes/no 結構自我修正</strong>。</p>
<p>修正成本由使用者承擔 = 反過來 #74 的問題（把整理成本丟回使用者）。</p>
<hr>
<h2 id="yesno-的常見變種">Yes/No 的常見變種</h2>
<p>agent 容易寫的 yes/no 變種：</p>
<table>
  <thead>
      <tr>
          <th>變種</th>
          <th>隱含預設</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>&ldquo;需要我 X 嗎？&rdquo;</td>
          <td>我已經想好要 X、你只要批准</td>
      </tr>
      <tr>
          <td>&ldquo;要繼續嗎？&rdquo;</td>
          <td>流程往下走、你只要不阻止</td>
      </tr>
      <tr>
          <td>&ldquo;OK 嗎？&rdquo;</td>
          <td>我做了 X、你只要不反對</td>
      </tr>
      <tr>
          <td>&ldquo;我先做 X、有問題嗎？&rdquo;</td>
          <td>我打算開幹、你只要沒異議</td>
      </tr>
      <tr>
          <td>&ldquo;確認一下、是這樣嗎？&rdquo;</td>
          <td>你之前說的我理解對嗎、回 yes/no</td>
      </tr>
      <tr>
          <td>&ldquo;如果 OK 我繼續做&rdquo;</td>
          <td>同意 → 繼續、不同意 → 卡住</td>
      </tr>
  </tbody>
</table>
<p>每個變種都把使用者推進「同意 / 不同意」的二元框、其他合法回應（改 / 延後 / 疊加 / 分批 / 反問澄清）需要使用者自己破格。</p>
<hr>
<h2 id="修法把-yesno-翻成多維展開">修法：把 Yes/No 翻成多維展開</h2>
<p>把 binary 拆回多維：</p>
<h3 id="beforeyesno">Before（yes/no）</h3>
<blockquote>
<p>&ldquo;我打算 ship D（UX hint）、需要我繼續嗎？&rdquo;</p></blockquote>
<h3 id="after五維展開">After（五維展開）</h3>
<blockquote>
<p>接下來的選項：</p>
<table>
  <thead>
      <tr>
          <th>選項</th>
          <th>適配性</th>
          <th>取捨</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>立刻 ship D</td>
          <td>解眼前痛</td>
          <td>不解根因</td>
      </tr>
      <tr>
          <td>延後 D、先補 telemetry</td>
          <td>等資料</td>
          <td>推遲 1 週</td>
      </tr>
      <tr>
          <td>跳過 D、做 B/C</td>
          <td>結構 fix</td>
          <td>風險高、要驗證</td>
      </tr>
      <tr>
          <td>D + 寫測試固化</td>
          <td>疊加</td>
          <td>多寫 30 分鐘</td>
      </tr>
  </tbody>
</table>
<p>我推薦 <strong>D + 寫測試固化</strong>（疊加）— 解眼前痛 + 防回退。改 / 延後 / 跳過都行、跟我說。</p></blockquote>
<h3 id="對照">對照</h3>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Before</th>
          <th>After</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>呈現格式</td>
          <td>yes/no 一句</td>
          <td>結構表 + 推薦</td>
      </tr>
      <tr>
          <td>策略數</td>
          <td>單一 D</td>
          <td>D + 測試疊加</td>
      </tr>
      <tr>
          <td>批次邊界</td>
          <td>一次</td>
          <td>隱含分批（B/C 下輪）</td>
      </tr>
      <tr>
          <td>時間軸</td>
          <td>立刻</td>
          <td>提供延後條件</td>
      </tr>
      <tr>
          <td>選項類型</td>
          <td>binary</td>
          <td>多選、可疊加</td>
      </tr>
  </tbody>
</table>
<p>明顯多花字、明顯讓使用者更容易做正確決策。</p>
<hr>
<h2 id="何時-yesno-真的合理">何時 Yes/No 真的合理</h2>
<p>不是所有 yes/no 都該展開。合理的少數情境：</p>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>為什麼 yes/no 夠用</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>已經三回合確認過、純最終 confirmation</td>
          <td>多選空間在前面回合已展開</td>
      </tr>
      <tr>
          <td>Atomic 動作、沒中間態（push 與否、刪除與否）</td>
          <td>真的是 binary、沒有 1.5</td>
      </tr>
      <tr>
          <td>使用者明確下了「OK 直接做」的指令</td>
          <td>多展開反而干擾節奏</td>
      </tr>
      <tr>
          <td>緊急情境、來不及展開</td>
          <td>時間壓力 &gt; 決策品質</td>
      </tr>
      <tr>
          <td>純 sanity check、執行已經決定（&ldquo;我跑 X、有沒有要先停？&quot;）</td>
          <td>預設執行、yes/no 是 last chance 攔截</td>
      </tr>
  </tbody>
</table>
<p>五類共通：<strong>多選空間已經消耗或不存在</strong>。其他情境都該展開。</p>
<hr>
<h2 id="跟其他卡的關係">跟其他卡的關係</h2>
<table>
  <thead>
      <tr>
          <th>卡</th>
          <th>關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../decision-dialogue-dimensions/">#79 決策對話的五維度</a></td>
          <td>本卡是 #79 在「最常見變種」的具體化 — yes/no 是五維全 collapse 再加 binary</td>
      </tr>
      <tr>
          <td><a href="../decision-presentation-options-recommendation/">#74 決策呈現格式</a></td>
          <td>yes/no 等同「給推薦但不給選項」、是 #74 反模式列的延伸</td>
      </tr>
      <tr>
          <td><a href="../ease-of-writing-vs-intent-alignment/">#67 寫作便利度</a></td>
          <td>yes/no 是「最容易寫」的格式（最少字）、跟使用者意圖對齊（多選空間）反相關</td>
      </tr>
      <tr>
          <td><a href="../external-trigger-for-high-roi-work/">#72 高 ROI 無觸發</a></td>
          <td>「展開 yes/no」是高 ROI 但無觸發的工作、需要 L3 結構性對策提示</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>寫到「需要 X 嗎？」</td>
          <td>翻成多選表</td>
      </tr>
      <tr>
          <td>寫到「要繼續嗎？」</td>
          <td>列出「改方向 / 延後 / 疊加」三選</td>
      </tr>
      <tr>
          <td>「OK 嗎？」結尾</td>
          <td>檢查是否真的是 last-mile confirm、否則展開</td>
      </tr>
      <tr>
          <td>使用者回 &ldquo;等等、先 X&rdquo;</td>
          <td>你寫了 yes/no、他在破格 — 下次該預先展開</td>
      </tr>
      <tr>
          <td>使用者回 &ldquo;順便也做 Y&rdquo;</td>
          <td>你漏了疊加維度</td>
      </tr>
      <tr>
          <td>「如果 OK 我繼續」隱含「不同意 = 卡住」</td>
          <td>使用者不同意有實際下一步嗎？沒有 = 變相強制同意</td>
      </tr>
      <tr>
          <td>一輪對話 yes/no 出現 ≥ 3 次</td>
          <td>你在用 binary 取代展開、退一步看是不是 #79 五維都該展開</td>
      </tr>
  </tbody>
</table>
<p><strong>核心</strong>：Yes/No 是 1 bit、決策空間是 N bit。<strong>用 yes/no 包裝不是「簡化」、是「藏掉維度」</strong>。簡化是少寫某個維度；藏掉維度是把多維壓成 1 bit、讓使用者破格才能表達。前者良性、後者不良。</p>
]]></content:encoded></item><item><title>Writing 的 multi-pass review：N 輪 review、每輪換 frame</title><link>https://tarrragon.github.io/blog/report/writing-multi-pass-review/</link><pubDate>Sun, 26 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/writing-multi-pass-review/</guid><description>&lt;h2 id="結論">結論&lt;/h2>
&lt;p>寫文字（文章 / 註解 / doc / prompt / commit message）的 ROI 來自 &lt;strong>N 輪不同 frame 的 re-read&lt;/strong>、不是單次「寫對」。每輪 catch 上一輪 frame miss 的東西：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>輪次&lt;/th>
 &lt;th>Frame&lt;/th>
 &lt;th>抓什麼&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>1&lt;/td>
 &lt;td>生成&lt;/td>
 &lt;td>把 idea 變字、預期會有錯&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>2&lt;/td>
 &lt;td>對意圖（&lt;a href="../ease-of-writing-vs-intent-alignment/">#67&lt;/a>）&lt;/td>
 &lt;td>寫出來跟原意對齊嗎、有沒有便利驅動的偏移&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>3&lt;/td>
 &lt;td>機會成本語氣&lt;/td>
 &lt;td>有沒有「應該」「不行」「正確」絕對主義？是不是該翻成「在 X 情境下 A 較好、Y 情境 B 較好」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>4&lt;/td>
 &lt;td>Grep-ability / 命名&lt;/td>
 &lt;td>關鍵字前置嗎？AI 能單次 grep 命中嗎？&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>5&lt;/td>
 &lt;td>反例 / 邊界&lt;/td>
 &lt;td>「何時不適用」段寫了嗎？反模式列了嗎？&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>每輪用「跟前一輪不同的眼睛」看同一份文字 — 才能 catch 不同層的問題。&lt;strong>第 1 輪的 frame 不可能同時 catch 所有層&lt;/strong>（&lt;a href="../literal-interception-vs-behavioral-refinement/">#82&lt;/a> 字面 vs 行為的 ceiling）。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼一輪寫不出全部維度">為什麼一輪寫不出全部維度&lt;/h2>
&lt;p>寫的時候 working memory 有限、必須 collapse 多個維度去專注其中之一：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>生成 frame&lt;/strong> 在意 idea 完整 → 顧不上語氣&lt;/li>
&lt;li>&lt;strong>語氣 frame&lt;/strong> 在意「機會成本 vs 絕對主義」→ 顧不上 grep-ability&lt;/li>
&lt;li>&lt;strong>grep frame&lt;/strong> 在意關鍵字前置 → 顧不上反例&lt;/li>
&lt;/ul>
&lt;p>要求一輪同時 catch 所有 = 認知超載。實際結果是「每個維度都做一半」。&lt;/p>
&lt;p>&lt;strong>多輪設計接受 working memory 限制、用 N 輪解 N 維&lt;/strong>。每輪只專注一個 frame、效率反而高。&lt;/p>
&lt;hr>
&lt;h2 id="五輪-review-的具體-checklist">五輪 review 的具體 checklist&lt;/h2>
&lt;h3 id="輪-1生成">輪 1：生成&lt;/h3>
&lt;ul>
&lt;li>&lt;input disabled="" type="checkbox"> idea 從頭寫到尾、不停下來改&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> 預期會有 typo、結構亂、語氣不對 — 不在這輪修&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> 跑得到結尾比寫得漂亮重要&lt;/li>
&lt;/ul>
&lt;h3 id="輪-2對意圖67">輪 2：對意圖（&lt;a href="../ease-of-writing-vs-intent-alignment/">#67&lt;/a>）&lt;/h3>
&lt;ul>
&lt;li>&lt;input disabled="" type="checkbox"> 開頭一句講清「這段在說什麼」嗎？&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> 有沒有 #67 的「便利驅動偏移」— 寫得順但其實偏題了？&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> 段落順序是不是「好寫」決定的、不是「易讀」決定的？&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> 有沒有「為了補滿格式」寫的廢段？&lt;/li>
&lt;/ul>
&lt;h3 id="輪-3機會成本語氣">輪 3：機會成本語氣&lt;/h3>
&lt;ul>
&lt;li>&lt;input disabled="" type="checkbox"> 跑 grep 抓「應該 / 必須 / 不行 / 不可以 / 正確的方式 / 唯一」這些絕對詞&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> 每個絕對詞檢查：是物理 / 法律 / 安全事實嗎？不是的話翻成「在 X 情境下 A 較好、Y 情境下 B 較好」&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> 反模式表的「為什麼不好」寫到「違反某個原則」、不寫「就是不對」&lt;/li>
&lt;/ul>
&lt;h3 id="輪-4grep-ability--命名">輪 4：Grep-ability / 命名&lt;/h3>
&lt;ul>
&lt;li>&lt;input disabled="" type="checkbox"> 關鍵字在段首、不在段中&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> 表格欄位用 grep 友善的分隔（&lt;code>:&lt;/code> &lt;code>|&lt;/code> &lt;code>→&lt;/code>）&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> 檔名 / slug 跟 title 對應、不要用流水號&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> 命名能用單次 grep 命中、不需要語意推理&lt;/li>
&lt;/ul>
&lt;h3 id="輪-5反例--邊界">輪 5：反例 / 邊界&lt;/h3>
&lt;ul>
&lt;li>&lt;input disabled="" type="checkbox"> 「何時不適用」段寫了嗎？&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> 反模式表給「為什麼不好 + 修法」嗎、還是只給警告？&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> 跨卡 cross-link 補了嗎？&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> 有沒有 over-claim、把「在多數情境下」說成「總是」？&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="套用到不同-output-類型">套用到不同 output 類型&lt;/h2>
&lt;p>每類有特化的輪次組合：&lt;/p></description><content:encoded><![CDATA[<h2 id="結論">結論</h2>
<p>寫文字（文章 / 註解 / doc / prompt / commit message）的 ROI 來自 <strong>N 輪不同 frame 的 re-read</strong>、不是單次「寫對」。每輪 catch 上一輪 frame miss 的東西：</p>
<table>
  <thead>
      <tr>
          <th>輪次</th>
          <th>Frame</th>
          <th>抓什麼</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>1</td>
          <td>生成</td>
          <td>把 idea 變字、預期會有錯</td>
      </tr>
      <tr>
          <td>2</td>
          <td>對意圖（<a href="../ease-of-writing-vs-intent-alignment/">#67</a>）</td>
          <td>寫出來跟原意對齊嗎、有沒有便利驅動的偏移</td>
      </tr>
      <tr>
          <td>3</td>
          <td>機會成本語氣</td>
          <td>有沒有「應該」「不行」「正確」絕對主義？是不是該翻成「在 X 情境下 A 較好、Y 情境 B 較好」</td>
      </tr>
      <tr>
          <td>4</td>
          <td>Grep-ability / 命名</td>
          <td>關鍵字前置嗎？AI 能單次 grep 命中嗎？</td>
      </tr>
      <tr>
          <td>5</td>
          <td>反例 / 邊界</td>
          <td>「何時不適用」段寫了嗎？反模式列了嗎？</td>
      </tr>
  </tbody>
</table>
<p>每輪用「跟前一輪不同的眼睛」看同一份文字 — 才能 catch 不同層的問題。<strong>第 1 輪的 frame 不可能同時 catch 所有層</strong>（<a href="../literal-interception-vs-behavioral-refinement/">#82</a> 字面 vs 行為的 ceiling）。</p>
<hr>
<h2 id="為什麼一輪寫不出全部維度">為什麼一輪寫不出全部維度</h2>
<p>寫的時候 working memory 有限、必須 collapse 多個維度去專注其中之一：</p>
<ul>
<li><strong>生成 frame</strong> 在意 idea 完整 → 顧不上語氣</li>
<li><strong>語氣 frame</strong> 在意「機會成本 vs 絕對主義」→ 顧不上 grep-ability</li>
<li><strong>grep frame</strong> 在意關鍵字前置 → 顧不上反例</li>
</ul>
<p>要求一輪同時 catch 所有 = 認知超載。實際結果是「每個維度都做一半」。</p>
<p><strong>多輪設計接受 working memory 限制、用 N 輪解 N 維</strong>。每輪只專注一個 frame、效率反而高。</p>
<hr>
<h2 id="五輪-review-的具體-checklist">五輪 review 的具體 checklist</h2>
<h3 id="輪-1生成">輪 1：生成</h3>
<ul>
<li><input disabled="" type="checkbox"> idea 從頭寫到尾、不停下來改</li>
<li><input disabled="" type="checkbox"> 預期會有 typo、結構亂、語氣不對 — 不在這輪修</li>
<li><input disabled="" type="checkbox"> 跑得到結尾比寫得漂亮重要</li>
</ul>
<h3 id="輪-2對意圖67">輪 2：對意圖（<a href="../ease-of-writing-vs-intent-alignment/">#67</a>）</h3>
<ul>
<li><input disabled="" type="checkbox"> 開頭一句講清「這段在說什麼」嗎？</li>
<li><input disabled="" type="checkbox"> 有沒有 #67 的「便利驅動偏移」— 寫得順但其實偏題了？</li>
<li><input disabled="" type="checkbox"> 段落順序是不是「好寫」決定的、不是「易讀」決定的？</li>
<li><input disabled="" type="checkbox"> 有沒有「為了補滿格式」寫的廢段？</li>
</ul>
<h3 id="輪-3機會成本語氣">輪 3：機會成本語氣</h3>
<ul>
<li><input disabled="" type="checkbox"> 跑 grep 抓「應該 / 必須 / 不行 / 不可以 / 正確的方式 / 唯一」這些絕對詞</li>
<li><input disabled="" type="checkbox"> 每個絕對詞檢查：是物理 / 法律 / 安全事實嗎？不是的話翻成「在 X 情境下 A 較好、Y 情境下 B 較好」</li>
<li><input disabled="" type="checkbox"> 反模式表的「為什麼不好」寫到「違反某個原則」、不寫「就是不對」</li>
</ul>
<h3 id="輪-4grep-ability--命名">輪 4：Grep-ability / 命名</h3>
<ul>
<li><input disabled="" type="checkbox"> 關鍵字在段首、不在段中</li>
<li><input disabled="" type="checkbox"> 表格欄位用 grep 友善的分隔（<code>:</code> <code>|</code> <code>→</code>）</li>
<li><input disabled="" type="checkbox"> 檔名 / slug 跟 title 對應、不要用流水號</li>
<li><input disabled="" type="checkbox"> 命名能用單次 grep 命中、不需要語意推理</li>
</ul>
<h3 id="輪-5反例--邊界">輪 5：反例 / 邊界</h3>
<ul>
<li><input disabled="" type="checkbox"> 「何時不適用」段寫了嗎？</li>
<li><input disabled="" type="checkbox"> 反模式表給「為什麼不好 + 修法」嗎、還是只給警告？</li>
<li><input disabled="" type="checkbox"> 跨卡 cross-link 補了嗎？</li>
<li><input disabled="" type="checkbox"> 有沒有 over-claim、把「在多數情境下」說成「總是」？</li>
</ul>
<hr>
<h2 id="套用到不同-output-類型">套用到不同 output 類型</h2>
<p>每類有特化的輪次組合：</p>
<h3 id="文章contentpostscontentreport">文章（content/posts、content/report）</h3>
<p>完整跑 1-5 輪。額外加：</p>
<ul>
<li><strong>輪 6</strong>：跨卡 cross-link 健康度（單向引用 vs 雙向）</li>
<li><strong>輪 7</strong>：放回 <code>_index.md</code> 的索引條描述對應到內容嗎</li>
</ul>
<h3 id="程式註解doc-comment--inline">程式註解（doc comment / inline）</h3>
<p>跑 1-3 輪 + 額外：</p>
<ul>
<li><strong>輪 4&rsquo;</strong>：grep-ability 改成「介面 vs 實作分層」— doc comment 不洩漏 impl、inline comment 講 why 不講 what</li>
<li><strong>輪 5&rsquo;</strong>：反例改成「這個註解 5 個月後讀還看得懂嗎」（時間軸 robust）</li>
</ul>
<h3 id="naming變數--函式--檔名--slug">Naming（變數 / 函式 / 檔名 / slug）</h3>
<p>→ 見 <a href="../naming-as-iterated-artifact/">#84 Naming 是 iterated artifact</a>、有特化的 4 輪設計。</p>
<h3 id="prompt給-llm-的指令">Prompt（給 LLM 的指令）</h3>
<p>跑 1-3 輪 + 額外：</p>
<ul>
<li><strong>輪 4&rsquo;&rsquo;</strong>：模糊指令（<a href="/blog/skills/requirement-protocol/clarifying-ambiguous-instructions/" data-link-title="Clarifying Ambiguous Instructions — 模糊指令澄清協議" data-link-desc="requirement-protocol reference：空間 / 相對位置 / 隔離 / 決定權 / 篩選五類模糊指令的澄清模板 &#43; visible 三問判準 &#43; 篩選三問。">clarifying-ambiguous-instructions</a>）— 「對齊」「靠近」這類詞翻成具體數字 / 條件</li>
<li><strong>輪 5&rsquo;&rsquo;</strong>：「邊界 case 預期行為」明示了嗎</li>
</ul>
<hr>
<h2 id="stakes-conditional-追加輪epistemic-rigor">Stakes-conditional 追加輪：Epistemic Rigor</h2>
<p>5 輪基本 frame 是 frame 軸（生成 / 意圖 / 語氣 / grep / 反例）；<strong>高 stakes 內容</strong>（reader 照做後錯誤不可逆 / 系統層 / 不可分批 ship 修正）追加 stakes 軸的 <strong>輪 E：epistemic rigor</strong>——比照學術 peer review 的 claim / evidence / method / threats / citation 五個 sub-check、確認論述強度足以承擔 reader 直接 implement 的下游風險。</p>
<h3 id="啟動條件opt-in不污染預設">啟動條件（opt-in、不污染預設）</h3>
<table>
  <thead>
      <tr>
          <th>內容類型</th>
          <th>是否跑輪 E</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>一般技術文章（layout / refactor / debug 教學）</td>
          <td>不跑（5 輪夠）</td>
      </tr>
      <tr>
          <td>資安 / cryptography / 防護 mitigation 教學</td>
          <td><strong>跑</strong></td>
      </tr>
      <tr>
          <td>Concurrency 正確性 / memory model claims</td>
          <td><strong>跑</strong></td>
      </tr>
      <tr>
          <td>Distributed consistency / consensus 演算法</td>
          <td><strong>跑</strong></td>
      </tr>
      <tr>
          <td>Financial 計算 / accounting / settlement</td>
          <td><strong>跑</strong></td>
      </tr>
      <tr>
          <td>Medical / safety-critical 計算</td>
          <td><strong>跑</strong></td>
      </tr>
      <tr>
          <td>任何「reader 照做後錯誤不可逆 / 系統層」的內容</td>
          <td><strong>跑</strong></td>
      </tr>
  </tbody>
</table>
<p>判別啟動的核心問題：「<strong>reader 照這段實作會不會在生產系統留 silent gap、且不能靠後續 ship 修補</strong>？」——會 → 跑輪 E、不會 → 5 輪即可。動機論證見 <a href="../security-teaching-rigor-asymmetry/">#99 資安教學審查標準對應風險不對稱</a>。</p>
<h3 id="輪-eepistemic-rigor-的-5-個-sub-check">輪 E：Epistemic Rigor 的 5 個 sub-check</h3>
<table>
  <thead>
      <tr>
          <th>Sub-check</th>
          <th>問題</th>
          <th>對應 audit 維度</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>E.1 Claim</td>
          <td>每個結論可拆 falsifiable 子句嗎？</td>
          <td><a href="../false-sense-of-security-as-primary-failure/">#100 false sense of security</a></td>
      </tr>
      <tr>
          <td>E.2 Evidence</td>
          <td>claim → evidence 推論鏈完整、有 mechanism 嗎？</td>
          <td><a href="../mitigation-threat-alignment/">#102 mitigation 對位</a></td>
      </tr>
      <tr>
          <td>E.3 Method</td>
          <td>reader 照 method 做能反向驗證嗎？</td>
          <td><a href="../threat-model-explicitness/">#101 threat model 明確性</a> + <a href="../mitigation-threat-alignment/">#102</a></td>
      </tr>
      <tr>
          <td>E.4 Threats</td>
          <td>什麼前提失效會 invalidate？</td>
          <td><a href="../mitigation-context-dependence/">#103 context-dependence</a></td>
      </tr>
      <tr>
          <td>E.5 Citation</td>
          <td>版本 / 句意 / current best practice 都對嗎？</td>
          <td><a href="../security-citation-currency-and-precision/">#104 citation 時效精確</a></td>
      </tr>
  </tbody>
</table>
<h3 id="輪-e-的產出格式">輪 E 的產出格式</h3>
<p>跑完輪 E、每個 weakness 對應到一個 dimension + tier、見 <a href="../security-audit-recommendation-tiers/">#105 audit recommendation 層級</a>：</p>
<ul>
<li><strong>Accept</strong>：無 weakness 或在容忍範圍</li>
<li><strong>Minor revise</strong>：補 boundary / contrast / 版本標記類小改、不阻擋 ship</li>
<li><strong>Major revise</strong>：結構性 false sense、需重寫、ship 前必須修</li>
<li><strong>Withdraw</strong>：教錯主動誤導 reader（過時 crypto 沒標 deprecated / 扭曲 citation 反向違反現行標準 / defense theater 當示範），保留 = 增加生產系統 risk、必須移除或全換</li>
</ul>
<p>withdraw tier 是高 stakes 內容跟一般內容的關鍵差異——一般內容 review 沒有「保留 = 增加 risk」的硬決策、高 stakes 必須有。</p>
<h3 id="跟-5-輪基本-frame-的分工">跟 5 輪基本 frame 的分工</h3>
<table>
  <thead>
      <tr>
          <th>軸</th>
          <th>5 輪基本 frame</th>
          <th>輪 E（高 stakes 追加）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>軸定位</td>
          <td>Frame 軸：每輪換一個寫作品質視角</td>
          <td>Stakes 軸：論述強度檢查</td>
      </tr>
      <tr>
          <td>觸發</td>
          <td>預設全跑（依 output 類型可跳少數輪）</td>
          <td>高 stakes 內容才 opt-in</td>
      </tr>
      <tr>
          <td>找的問題</td>
          <td>typo / 偏題 / 絕對主義 / grep / 邊界缺</td>
          <td>claim 空降 / 對位失效 / context 缺 / citation 過時 / withdraw-level 教錯</td>
      </tr>
      <tr>
          <td>失敗後果</td>
          <td>文字品質低、reader 用力讀</td>
          <td>reader 照做後實作出生產破口、silent failure</td>
      </tr>
  </tbody>
</table>
<p>兩軸正交、不取代——高 stakes 內容兩軸都跑（5 輪 frame + 輪 E stakes）、一般內容只跑 5 輪。輪 E 不在 5 輪裡是因為：把 epistemic rigor 設為預設會讓一般文章 over-audit、稀釋 review 紀律；設為 conditional opt-in 才能讓高 stakes 場景拉到學術級而不污染日常寫作。</p>
<p>→ 詳細維度展開（threat model 對稱 / mitigation 對位 mechanism / context-dependence / citation 時效）跟 audit recommendation tier 判準、見 <a href="../security-teaching-rigor-asymmetry/">#99</a> → <a href="../false-sense-of-security-as-primary-failure/">#100</a> → <a href="../threat-model-explicitness/">#101-104</a> → <a href="../security-audit-recommendation-tiers/">#105</a> 系列。</p>
<hr>
<h2 id="反模式跳輪的代價">反模式：跳輪的代價</h2>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>後果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>寫完直接 ship、跳過所有 review 輪</td>
          <td>第 1 輪生成 frame 沒抓到的全部漏</td>
      </tr>
      <tr>
          <td>每輪用同個 frame review</td>
          <td>角度沒換、重複 catch 同類錯、新類錯不會浮現</td>
      </tr>
      <tr>
          <td>「我邊寫邊改」融合多輪</td>
          <td>working memory 超載、每維都做一半</td>
      </tr>
      <tr>
          <td>跳過輪 3 機會成本語氣</td>
          <td>絕對主義教讀者規則、不教思考</td>
      </tr>
      <tr>
          <td>跳過輪 4 grep</td>
          <td>AI 找不到、文字變孤兒</td>
      </tr>
      <tr>
          <td>跳過輪 5 反例</td>
          <td>看起來是教條、不是 trade-off</td>
      </tr>
      <tr>
          <td>「下次寫的時候多注意」當 review 取代</td>
          <td><a href="../external-trigger-for-high-roi-work/">#72 高 ROI 無觸發</a> 紀律失效</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="何時可以跳某些輪">何時可以跳某些輪</h2>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>可跳的輪</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>內部 quick note、不會有人看</td>
          <td>跳 4 + 5（grep + 反例）</td>
      </tr>
      <tr>
          <td>Commit message</td>
          <td>跳 4 + 5、留 1-3</td>
      </tr>
      <tr>
          <td>Slack / chat 即時對話</td>
          <td>只跑輪 1</td>
      </tr>
      <tr>
          <td>引言 / 標題</td>
          <td>1-4 都跑、5 可省</td>
      </tr>
      <tr>
          <td>摘要 / TL;DR</td>
          <td>1-3 + 5（反例不適用、但語氣很重要）</td>
      </tr>
      <tr>
          <td>純資料填寫（schema / config）</td>
          <td>跳 3、其他都跑</td>
      </tr>
  </tbody>
</table>
<p>四類共通：<strong>ROI 不同、輪次組合不同</strong>。Production 文件 / 卡片 / 註解 全跑、即時通訊只跑核心。</p>
<hr>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<table>
  <thead>
      <tr>
          <th>原則</th>
          <th>關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../ease-of-writing-vs-intent-alignment/">#67 寫作便利度</a></td>
          <td>輪 2 的核心判準 — 為什麼便利寫法 ≠ 對齊意圖</td>
      </tr>
      <tr>
          <td><a href="../literal-interception-vs-behavioral-refinement/">#82 字面攔截 vs 行為精煉</a></td>
          <td>本卡是 #82 在「寫」這個動作的具體實例 — review 是 multi-pass、不是 hook</td>
      </tr>
      <tr>
          <td><a href="../cards-as-living-system-iteration/">#81 卡片系統的迭代浮現</a></td>
          <td>spiral 浮現本身就是寫作的 multi-pass 範例（卡片 → meta → reference）</td>
      </tr>
      <tr>
          <td><a href="../decision-dialogue-dimensions/">#79 決策對話的五維度</a></td>
          <td>寫決策呈現要用 #79 self-check + 本卡的輪 2 一起跑</td>
      </tr>
      <tr>
          <td><a href="../naming-as-iterated-artifact/">#84 Naming 是 iterated artifact</a></td>
          <td>本卡的輪 4 在 naming 場景的特化</td>
      </tr>
      <tr>
          <td><a href="../methodology-multi-pass-embedding/">#85 Methodology 的 multi-pass 該 embed 在 pillar</a></td>
          <td>本卡的 5 輪設計就是 compositional-writing 該 embed 為「第 6 原則」的內容</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>寫完直接 commit、覺得「OK 應該夠」</td>
          <td>跑五輪、每輪都會抓到東西</td>
      </tr>
      <tr>
          <td>每次 review 都「看不出哪裡可改」</td>
          <td>Frame 沒換、改用下一輪的 frame 看</td>
      </tr>
      <tr>
          <td>「這次先這樣、下次寫好一點」</td>
          <td>是 <a href="../external-trigger-for-high-roi-work/">#72 結構性跳過</a>、補 trigger（pre-commit / template / pair）</td>
      </tr>
      <tr>
          <td>反模式段空白</td>
          <td>跳了輪 5、補</td>
      </tr>
      <tr>
          <td>找不到自己寫過的卡</td>
          <td>輪 4 沒做、grep-ability 漏掉</td>
      </tr>
      <tr>
          <td>文字看起來像教條</td>
          <td>輪 3 的絕對主義詞沒翻、補</td>
      </tr>
      <tr>
          <td>段落開頭看不出在說什麼</td>
          <td>輪 2 的意圖顯性沒做</td>
      </tr>
  </tbody>
</table>
<p><strong>核心</strong>：Writing 的 ROI 來自<strong>多輪不同 frame</strong>、不是單次「寫得仔細」。要寫得仔細的部分太多、超過 working memory、必須分輪。<strong>跳輪的代價是某維度永遠做一半、累積成「看起來都對但其實有漏」的低品質文字</strong>。</p>
]]></content:encoded></item><item><title>Naming 是 iterated artifact：第一個名字幾乎不對、四輪 review 才收斂</title><link>https://tarrragon.github.io/blog/report/naming-as-iterated-artifact/</link><pubDate>Sun, 26 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/naming-as-iterated-artifact/</guid><description>&lt;h2 id="結論">結論&lt;/h2>
&lt;p>第一次寫的名字幾乎都不對 — 不是因為命名能力不夠、是因為&lt;strong>第一版命名只能基於「寫的當下看到的 context」&lt;/strong>、而正確的名字需要看到「未來所有 call-site / grep 結果 / 重構場景」。&lt;/p>
&lt;p>命名的正確設計是 &lt;strong>iterated artifact&lt;/strong>：寫 → re-read → 改 → 再 re-read → 收斂。每輪用不同 frame：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>輪&lt;/th>
 &lt;th>Frame&lt;/th>
 &lt;th>抓什麼&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>1&lt;/td>
 &lt;td>第一版&lt;/td>
 &lt;td>把概念變字串&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>2&lt;/td>
 &lt;td>Grep-ability&lt;/td>
 &lt;td>能單次 grep 命中嗎？跟其他 entity 名字不撞嗎？&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>3&lt;/td>
 &lt;td>Cross-call-site&lt;/td>
 &lt;td>從 caller 角度看、名字暗示的契約對嗎？&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>4&lt;/td>
 &lt;td>Impl 洩漏檢查&lt;/td>
 &lt;td>名字洩漏了 impl 細節嗎？換 impl 名字會錯嗎？&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>每輪可能 catch 到「上一輪沒看到」的問題、迫使重命名。&lt;strong>接受重命名是命名工作的常態、不是失敗&lt;/strong>。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼第一版幾乎不對">為什麼第一版幾乎不對&lt;/h2>
&lt;p>寫第一版時、認知資源都在「概念是什麼」、剩下的給命名只夠：&lt;/p>
&lt;ul>
&lt;li>看到當前 function 在做的事 → 命名只反映當前&lt;/li>
&lt;li>不知道未來會有 N 個 call-site → 沒考慮一致性&lt;/li>
&lt;li>不知道未來會有 grep / refactor → 沒考慮 unique-ness&lt;/li>
&lt;li>不知道未來會換 impl → 命名容易洩漏現在的 impl 細節&lt;/li>
&lt;/ul>
&lt;p>第一版命名是&lt;strong>對「現在的 context」過度擬合&lt;/strong>。下一輪 review 換 frame 才能看到擬合方向之外。&lt;/p>
&lt;hr>
&lt;h2 id="四輪-review-的具體-checklist">四輪 review 的具體 checklist&lt;/h2>
&lt;h3 id="輪-1第一版">輪 1：第一版&lt;/h3>
&lt;ul>
&lt;li>&lt;input disabled="" type="checkbox"> 名字反映「做什麼 / 是什麼」、不是「怎麼做」&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> 動詞 / 名詞符合語言慣例（function 動詞、value 名詞）&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> 不超過 4 個單字（長 ≠ 清楚）&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> 跑得到 next step、不在這輪糾結&lt;/li>
&lt;/ul>
&lt;h3 id="輪-2grep-ability">輪 2：Grep-ability&lt;/h3>
&lt;ul>
&lt;li>&lt;input disabled="" type="checkbox"> &lt;code>grep -r &amp;quot;&amp;lt;name&amp;gt;&amp;quot;&lt;/code> 能命中目標、不會被別的 entity 蓋過&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> 跟 framework / library reserved name 不撞（避免 &lt;code>data&lt;/code>、&lt;code>type&lt;/code>、&lt;code>value&lt;/code> 等過泛）&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> 名字不是其他名字的子字串（&lt;code>get&lt;/code> 會匹配 &lt;code>getName&lt;/code> &lt;code>getUser&lt;/code>&amp;hellip;）&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> 中英混合場景下、英文部分能 grep（不要用 &lt;code>處理器handler&lt;/code> 這種 mixed）&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> 縮寫慎用（&lt;code>usr&lt;/code> &lt;code>cfg&lt;/code> &lt;code>mgr&lt;/code> 增加 grep 失敗率）&lt;/li>
&lt;/ul>
&lt;h3 id="輪-3cross-call-site-一致性">輪 3：Cross-call-site 一致性&lt;/h3>
&lt;ul>
&lt;li>&lt;input disabled="" type="checkbox"> 從 caller 角度看、名字暗示的契約對嗎？&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> 跟同 module 其他類似 entity 命名格式一致嗎？（&lt;code>getUser&lt;/code> vs &lt;code>fetchUser&lt;/code> 不該混用）&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> 同一個概念在不同 file 用同名嗎？（不該 &lt;code>userId&lt;/code> / &lt;code>user_id&lt;/code> / &lt;code>uid&lt;/code> 三個並存）&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> 動詞時態一致嗎？（&lt;code>fetched&lt;/code> vs &lt;code>fetching&lt;/code> vs &lt;code>fetch&lt;/code> 對應狀態 / 動作 / 命令、不該混用）&lt;/li>
&lt;/ul>
&lt;h3 id="輪-4impl-洩漏檢查">輪 4：Impl 洩漏檢查&lt;/h3>
&lt;ul>
&lt;li>&lt;input disabled="" type="checkbox"> 名字含 impl 細節嗎？（&lt;code>fetchUserViaSql&lt;/code> ≠ &lt;code>fetchUser&lt;/code>、後者較好）&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> 換 impl 後名字還對嗎？（&lt;code>cacheGetUser&lt;/code> 改成走 DB 後名字錯了）&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> 名字洩漏 data structure 細節嗎？（&lt;code>userArray&lt;/code> ≠ &lt;code>users&lt;/code>、後者不綁 array）&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> 介面層名字 vs 實作層名字區分嗎？（介面用「做什麼」、實作用「怎麼做」可加細節）&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="套用到不同命名場景">套用到不同命名場景&lt;/h2>
&lt;h3 id="變數--函式">變數 / 函式&lt;/h3>
&lt;p>完整跑 1-4 輪。&lt;/p></description><content:encoded><![CDATA[<h2 id="結論">結論</h2>
<p>第一次寫的名字幾乎都不對 — 不是因為命名能力不夠、是因為<strong>第一版命名只能基於「寫的當下看到的 context」</strong>、而正確的名字需要看到「未來所有 call-site / grep 結果 / 重構場景」。</p>
<p>命名的正確設計是 <strong>iterated artifact</strong>：寫 → re-read → 改 → 再 re-read → 收斂。每輪用不同 frame：</p>
<table>
  <thead>
      <tr>
          <th>輪</th>
          <th>Frame</th>
          <th>抓什麼</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>1</td>
          <td>第一版</td>
          <td>把概念變字串</td>
      </tr>
      <tr>
          <td>2</td>
          <td>Grep-ability</td>
          <td>能單次 grep 命中嗎？跟其他 entity 名字不撞嗎？</td>
      </tr>
      <tr>
          <td>3</td>
          <td>Cross-call-site</td>
          <td>從 caller 角度看、名字暗示的契約對嗎？</td>
      </tr>
      <tr>
          <td>4</td>
          <td>Impl 洩漏檢查</td>
          <td>名字洩漏了 impl 細節嗎？換 impl 名字會錯嗎？</td>
      </tr>
  </tbody>
</table>
<p>每輪可能 catch 到「上一輪沒看到」的問題、迫使重命名。<strong>接受重命名是命名工作的常態、不是失敗</strong>。</p>
<hr>
<h2 id="為什麼第一版幾乎不對">為什麼第一版幾乎不對</h2>
<p>寫第一版時、認知資源都在「概念是什麼」、剩下的給命名只夠：</p>
<ul>
<li>看到當前 function 在做的事 → 命名只反映當前</li>
<li>不知道未來會有 N 個 call-site → 沒考慮一致性</li>
<li>不知道未來會有 grep / refactor → 沒考慮 unique-ness</li>
<li>不知道未來會換 impl → 命名容易洩漏現在的 impl 細節</li>
</ul>
<p>第一版命名是<strong>對「現在的 context」過度擬合</strong>。下一輪 review 換 frame 才能看到擬合方向之外。</p>
<hr>
<h2 id="四輪-review-的具體-checklist">四輪 review 的具體 checklist</h2>
<h3 id="輪-1第一版">輪 1：第一版</h3>
<ul>
<li><input disabled="" type="checkbox"> 名字反映「做什麼 / 是什麼」、不是「怎麼做」</li>
<li><input disabled="" type="checkbox"> 動詞 / 名詞符合語言慣例（function 動詞、value 名詞）</li>
<li><input disabled="" type="checkbox"> 不超過 4 個單字（長 ≠ 清楚）</li>
<li><input disabled="" type="checkbox"> 跑得到 next step、不在這輪糾結</li>
</ul>
<h3 id="輪-2grep-ability">輪 2：Grep-ability</h3>
<ul>
<li><input disabled="" type="checkbox"> <code>grep -r &quot;&lt;name&gt;&quot;</code> 能命中目標、不會被別的 entity 蓋過</li>
<li><input disabled="" type="checkbox"> 跟 framework / library reserved name 不撞（避免 <code>data</code>、<code>type</code>、<code>value</code> 等過泛）</li>
<li><input disabled="" type="checkbox"> 名字不是其他名字的子字串（<code>get</code> 會匹配 <code>getName</code> <code>getUser</code>&hellip;）</li>
<li><input disabled="" type="checkbox"> 中英混合場景下、英文部分能 grep（不要用 <code>處理器handler</code> 這種 mixed）</li>
<li><input disabled="" type="checkbox"> 縮寫慎用（<code>usr</code> <code>cfg</code> <code>mgr</code> 增加 grep 失敗率）</li>
</ul>
<h3 id="輪-3cross-call-site-一致性">輪 3：Cross-call-site 一致性</h3>
<ul>
<li><input disabled="" type="checkbox"> 從 caller 角度看、名字暗示的契約對嗎？</li>
<li><input disabled="" type="checkbox"> 跟同 module 其他類似 entity 命名格式一致嗎？（<code>getUser</code> vs <code>fetchUser</code> 不該混用）</li>
<li><input disabled="" type="checkbox"> 同一個概念在不同 file 用同名嗎？（不該 <code>userId</code> / <code>user_id</code> / <code>uid</code> 三個並存）</li>
<li><input disabled="" type="checkbox"> 動詞時態一致嗎？（<code>fetched</code> vs <code>fetching</code> vs <code>fetch</code> 對應狀態 / 動作 / 命令、不該混用）</li>
</ul>
<h3 id="輪-4impl-洩漏檢查">輪 4：Impl 洩漏檢查</h3>
<ul>
<li><input disabled="" type="checkbox"> 名字含 impl 細節嗎？（<code>fetchUserViaSql</code> ≠ <code>fetchUser</code>、後者較好）</li>
<li><input disabled="" type="checkbox"> 換 impl 後名字還對嗎？（<code>cacheGetUser</code> 改成走 DB 後名字錯了）</li>
<li><input disabled="" type="checkbox"> 名字洩漏 data structure 細節嗎？（<code>userArray</code> ≠ <code>users</code>、後者不綁 array）</li>
<li><input disabled="" type="checkbox"> 介面層名字 vs 實作層名字區分嗎？（介面用「做什麼」、實作用「怎麼做」可加細節）</li>
</ul>
<hr>
<h2 id="套用到不同命名場景">套用到不同命名場景</h2>
<h3 id="變數--函式">變數 / 函式</h3>
<p>完整跑 1-4 輪。</p>
<p>額外注意：</p>
<ul>
<li><strong>作用域</strong> — 越窄作用域越可短（loop counter <code>i</code>、close-up var <code>tmp</code>）；越寬作用域越要明確</li>
<li><strong>類型暗示</strong> — boolean 用 <code>is</code> / <code>has</code> / <code>should</code> 開頭</li>
</ul>
<h3 id="檔名--module">檔名 / module</h3>
<p>跑 1-4 + 加：</p>
<ul>
<li><strong>層級表達</strong> — 檔名能否反映在 directory 結構中的位置？</li>
<li><strong>避免 <code>utils</code> / <code>helpers</code> / <code>common</code></strong> — 這類是「不知該叫什麼」的訊號、強制再過一次輪 1-4</li>
</ul>
<h3 id="url-slug--route">URL slug / route</h3>
<p>跑 1-4 + 加：</p>
<ul>
<li><strong>SEO</strong> — 跟 search query 的 substring match 對齊（<a href="../search-engine-matching-mode-mismatch/">#73 search 匹配模式</a>）</li>
<li><strong>kebab-case 一致</strong></li>
<li><strong>不含 stop words</strong>（<code>the</code>、<code>a</code>、<code>is</code>、<code>of</code>、<code>with</code>、<code>and</code>）— 跟搜尋引擎 stemming 對齊</li>
</ul>
<h3 id="api-endpoint--db-column">API endpoint / DB column</h3>
<p>跑 1-4 + 加：</p>
<ul>
<li><strong>跨 service 一致性</strong> — 同一概念在 client / server / DB 用同名（避免 <code>user_id</code> / <code>userId</code> / <code>uid</code> 跨 layer 不一致）</li>
<li><strong>不可變更性</strong> — DB column / API endpoint 改名成本極高、輪 1-4 多跑幾次值得</li>
</ul>
<hr>
<h2 id="反模式放棄重命名">反模式：放棄重命名</h2>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>後果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>「先這樣、之後再改」</td>
          <td><a href="../external-trigger-for-high-roi-work/">#72 結構性跳過</a> — 永遠不改</td>
      </tr>
      <tr>
          <td>「重命名 PR 風險高、別做」</td>
          <td>累積成 cognitive debt、後續 onboarding / debug 成本爆炸</td>
      </tr>
      <tr>
          <td>「IDE 會自動重命名、不用想清楚」</td>
          <td>IDE 改不到 doc / commit / chat 引用</td>
      </tr>
      <tr>
          <td>用 <code>data</code> <code>value</code> <code>type</code> <code>info</code> <code>obj</code> 含糊命名</td>
          <td>grep 失敗率高、自帶 false-match</td>
      </tr>
      <tr>
          <td>用語言不一致的 <code>處理 handler</code></td>
          <td>中英混雜、grep 兩邊都失敗</td>
      </tr>
      <tr>
          <td><code>tempVar1</code> <code>tempVar2</code> 流水號</td>
          <td>看不出是什麼、純佔位</td>
      </tr>
      <tr>
          <td><code>getUserById</code> 名字洩漏 query strategy</td>
          <td>換成 cache hit 後名字錯了</td>
      </tr>
      <tr>
          <td>複數同義詞並存（<code>fetch</code> / <code>get</code> / <code>load</code> / <code>retrieve</code>）</td>
          <td>caller 不知選哪個</td>
      </tr>
      <tr>
          <td>介面命名洩漏 impl（<code>HashMapUserStore</code>）</td>
          <td>impl 換 RedisStore 後 caller 跟著改</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="何時可以跳輪">何時可以跳輪</h2>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>可跳輪</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Loop counter / 即時 close-up var</td>
          <td>只跑輪 1</td>
      </tr>
      <tr>
          <td>Test code 內部 helper</td>
          <td>跑輪 1 + 4</td>
      </tr>
      <tr>
          <td>Temporary script / one-off</td>
          <td>1 + 2</td>
      </tr>
      <tr>
          <td>跨 team API / DB schema</td>
          <td><strong>每輪都跑、跑兩遍</strong></td>
      </tr>
      <tr>
          <td>Public library / SDK</td>
          <td><strong>每輪都跑、跑兩遍</strong></td>
      </tr>
      <tr>
          <td>Production-facing URL / endpoint</td>
          <td><strong>不可跳、改名成本極高</strong></td>
      </tr>
  </tbody>
</table>
<p>兩極：作用域越窄越可省、跨邊界 / public 越要 multi-pass。</p>
<hr>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<table>
  <thead>
      <tr>
          <th>原則</th>
          <th>關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../writing-multi-pass-review/">#83 Writing 的 multi-pass review</a></td>
          <td>本卡是 #83 輪 4 在 naming 場景的特化</td>
      </tr>
      <tr>
          <td><a href="../literal-interception-vs-behavioral-refinement/">#82 字面攔截 vs 行為精煉</a></td>
          <td>命名 lint（max length、case style）只擋字面、grep-ability / 一致性 / impl 洩漏靠 multi-pass review</td>
      </tr>
      <tr>
          <td><a href="../ease-of-writing-vs-intent-alignment/">#67 寫作便利度</a></td>
          <td>第一版命名是「容易寫」、不是「對齊意圖」、需要重命名</td>
      </tr>
      <tr>
          <td><a href="../search-engine-matching-mode-mismatch/">#73 search 匹配模式</a></td>
          <td>URL slug 的命名要跟 search 預期匹配模式對齊</td>
      </tr>
      <tr>
          <td><a href="../single-source-of-truth/">#44 SSOT</a></td>
          <td>同概念跨 layer 用同名 = naming SSOT、不該允許多版本同義</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>第一次想到的名字直接用了</td>
          <td>跑輪 2-4、預期會改</td>
      </tr>
      <tr>
          <td><code>data</code> <code>type</code> <code>value</code> <code>info</code> <code>obj</code> 出現</td>
          <td>含糊命名、強制重新命</td>
      </tr>
      <tr>
          <td><code>utils</code> / <code>helpers</code> / <code>common</code> module</td>
          <td>「不知該叫什麼」訊號、重新分類</td>
      </tr>
      <tr>
          <td>Grep 命中太多無關結果</td>
          <td>名字太短 / 太泛、重命名加 prefix</td>
      </tr>
      <tr>
          <td>Caller code 看 callsite 不知契約</td>
          <td>介面名字洩漏不夠、補強或改名</td>
      </tr>
      <tr>
          <td>重構後類型 / impl 換了名字沒換</td>
          <td>命名洩漏 impl、重命名</td>
      </tr>
      <tr>
          <td>同概念出現 ≥ 2 個名字</td>
          <td>違反 SSOT、選一個改另一個</td>
      </tr>
      <tr>
          <td>重命名 PR 被 reject「沒必要」</td>
          <td>文化沒接受 naming 是 iterated、補 reviewer education</td>
      </tr>
  </tbody>
</table>
<p><strong>核心</strong>：命名是 <strong>iterated artifact</strong>、不是 single-shot 動作。第一版基於狹窄 context 幾乎必錯。<strong>接受 N 輪 review 跟 K 次重命名是常態</strong>、命名品質會提升一個量級。試圖一次寫對 = 第一版 ship 出去 = 後續長期付 cognitive 成本。</p>
]]></content:encoded></item><item><title>Engine 不可調時、把 transformation 移到外層</title><link>https://tarrragon.github.io/blog/report/transformation-at-outer-layer-when-engine-closed/</link><pubDate>Sun, 26 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/transformation-at-outer-layer-when-engine-closed/</guid><description>&lt;h2 id="結論">結論&lt;/h2>
&lt;p>當底層 engine（search engine、LLM、DB、compiler、framework）&lt;strong>沒開放某能力的客製 API&lt;/strong>、不該硬改 engine 內部、改在 engine 的&lt;strong>輸入層 / 外層做 transformation&lt;/strong>：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Engine 限制&lt;/th>
 &lt;th>外層 transformation&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Search engine 只 prefix match&lt;/td>
 &lt;td>Build-time emit suffix tokens（&amp;ldquo;backpressure&amp;rdquo; → 加 hidden &amp;ldquo;pressure&amp;rdquo;）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>LLM 不會 CoT reasoning&lt;/td>
 &lt;td>Prompt 層加「請逐步推理」instruction&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>DB 不能 query JSON 內欄位&lt;/td>
 &lt;td>預先 denormalize 成獨立 column&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Compiler 不可調 lowering&lt;/td>
 &lt;td>Source-level macro 展開&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Framework 沒 hook 點&lt;/td>
 &lt;td>Wrapper component / proxy&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>API rate limit 不能調&lt;/td>
 &lt;td>Client-side batching / queueing&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>核心原則&lt;/strong>：engine 不開放 = 不要硬改 engine、改改你給 engine 的輸入。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼移到外層是合理-escape">為什麼移到外層是合理 escape&lt;/h2>
&lt;p>直覺反應遇到 engine 限制是「fork engine」「升級到能調的 engine」「換 engine」 — 但這三條都成本爆炸：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Fork engine&lt;/strong>：維護 fork 成本（merge upstream changes、bug fix back-port）&lt;/li>
&lt;li>&lt;strong>升級&lt;/strong>：可能需要等好幾版、可能 breaking change、可能根本沒得升&lt;/li>
&lt;li>&lt;strong>換 engine&lt;/strong>：data migration、API 重寫、config 重學&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>外層 transformation&lt;/strong> 跳過這三條：&lt;/p>
&lt;ul>
&lt;li>不動 engine 內部&lt;/li>
&lt;li>用 engine 既有的合法 API（input、metadata、wrapper）&lt;/li>
&lt;li>升級 engine 時 transformation 通常仍兼容&lt;/li>
&lt;li>換 engine 時也常能直接搬&lt;/li>
&lt;/ul>
&lt;p>代價：&lt;/p>
&lt;ul>
&lt;li>多一層 indirection&lt;/li>
&lt;li>需要維護 transformation 邏輯&lt;/li>
&lt;li>可能有 leak（transformation 不完美、edge case 露出 engine 限制）&lt;/li>
&lt;/ul>
&lt;p>通常代價遠小於三條「動 engine」路線。&lt;/p>
&lt;hr>
&lt;h2 id="五個跨領域實例">五個跨領域實例&lt;/h2>
&lt;h3 id="1-searchsuffix-token-injection">1. Search：suffix token injection&lt;/h3>
&lt;p>Pagefind / 多數 build-time search engine 只 prefix match。要支援 substring，build-time emit 額外 hidden tokens：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c">&amp;lt;!-- 原文 --&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">p&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>backpressure handling&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">p&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="c">&amp;lt;!-- transformation 後 --&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">p&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>backpressure handling&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">p&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">span&lt;/span> &lt;span class="na">hidden&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>pressure handling&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">span&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>grep &amp;quot;pre&amp;quot;&lt;/code> → matches &lt;code>pressure&lt;/code> prefix → finds page。&lt;/p>
&lt;h3 id="2-llmprompt-level-chain-of-thought">2. LLM：prompt-level chain-of-thought&lt;/h3>
&lt;p>LLM 不會自動 CoT。在 prompt 層加：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">請先列出已知資訊、然後推理步驟、最後給出結論。&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Engine 沒變、輸入變了、行為變了。&lt;/p>
&lt;h3 id="3-dbdenormalized-columns">3. DB：denormalized columns&lt;/h3>
&lt;p>PostgreSQL JSON column query 慢、但:&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="k">ALTER&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">TABLE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">events&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ADD&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">COLUMN&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">user_id_extracted&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">UUID&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">GENERATED&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">ALWAYS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">AS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">data&lt;/span>&lt;span class="o">-&amp;gt;&amp;gt;&lt;/span>&lt;span class="s1">&amp;#39;user_id&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">STORED&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">CREATE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">INDEX&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ON&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">events&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">user_id_extracted&lt;/span>&lt;span class="p">);&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Engine 沒變、schema 加了 generated column、query 走 index、變快。&lt;/p></description><content:encoded><![CDATA[<h2 id="結論">結論</h2>
<p>當底層 engine（search engine、LLM、DB、compiler、framework）<strong>沒開放某能力的客製 API</strong>、不該硬改 engine 內部、改在 engine 的<strong>輸入層 / 外層做 transformation</strong>：</p>
<table>
  <thead>
      <tr>
          <th>Engine 限制</th>
          <th>外層 transformation</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Search engine 只 prefix match</td>
          <td>Build-time emit suffix tokens（&ldquo;backpressure&rdquo; → 加 hidden &ldquo;pressure&rdquo;）</td>
      </tr>
      <tr>
          <td>LLM 不會 CoT reasoning</td>
          <td>Prompt 層加「請逐步推理」instruction</td>
      </tr>
      <tr>
          <td>DB 不能 query JSON 內欄位</td>
          <td>預先 denormalize 成獨立 column</td>
      </tr>
      <tr>
          <td>Compiler 不可調 lowering</td>
          <td>Source-level macro 展開</td>
      </tr>
      <tr>
          <td>Framework 沒 hook 點</td>
          <td>Wrapper component / proxy</td>
      </tr>
      <tr>
          <td>API rate limit 不能調</td>
          <td>Client-side batching / queueing</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：engine 不開放 = 不要硬改 engine、改改你給 engine 的輸入。</p>
<hr>
<h2 id="為什麼移到外層是合理-escape">為什麼移到外層是合理 escape</h2>
<p>直覺反應遇到 engine 限制是「fork engine」「升級到能調的 engine」「換 engine」 — 但這三條都成本爆炸：</p>
<ul>
<li><strong>Fork engine</strong>：維護 fork 成本（merge upstream changes、bug fix back-port）</li>
<li><strong>升級</strong>：可能需要等好幾版、可能 breaking change、可能根本沒得升</li>
<li><strong>換 engine</strong>：data migration、API 重寫、config 重學</li>
</ul>
<p><strong>外層 transformation</strong> 跳過這三條：</p>
<ul>
<li>不動 engine 內部</li>
<li>用 engine 既有的合法 API（input、metadata、wrapper）</li>
<li>升級 engine 時 transformation 通常仍兼容</li>
<li>換 engine 時也常能直接搬</li>
</ul>
<p>代價：</p>
<ul>
<li>多一層 indirection</li>
<li>需要維護 transformation 邏輯</li>
<li>可能有 leak（transformation 不完美、edge case 露出 engine 限制）</li>
</ul>
<p>通常代價遠小於三條「動 engine」路線。</p>
<hr>
<h2 id="五個跨領域實例">五個跨領域實例</h2>
<h3 id="1-searchsuffix-token-injection">1. Search：suffix token injection</h3>
<p>Pagefind / 多數 build-time search engine 只 prefix match。要支援 substring，build-time emit 額外 hidden tokens：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="ln">1</span><span class="cl"><span class="c">&lt;!-- 原文 --&gt;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;</span>backpressure handling<span class="p">&lt;/</span><span class="nt">p</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c">&lt;!-- transformation 後 --&gt;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;</span>backpressure handling<span class="p">&lt;/</span><span class="nt">p</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="p">&lt;</span><span class="nt">span</span> <span class="na">hidden</span><span class="p">&gt;</span>pressure handling<span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;</span></span></span></code></pre></div><p><code>grep &quot;pre&quot;</code> → matches <code>pressure</code> prefix → finds page。</p>
<h3 id="2-llmprompt-level-chain-of-thought">2. LLM：prompt-level chain-of-thought</h3>
<p>LLM 不會自動 CoT。在 prompt 層加：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">請先列出已知資訊、然後推理步驟、最後給出結論。</span></span></code></pre></div><p>Engine 沒變、輸入變了、行為變了。</p>
<h3 id="3-dbdenormalized-columns">3. DB：denormalized columns</h3>
<p>PostgreSQL JSON column query 慢、但:</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">ALTER</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">events</span><span class="w"> </span><span class="k">ADD</span><span class="w"> </span><span class="k">COLUMN</span><span class="w"> </span><span class="n">user_id_extracted</span><span class="w"> </span><span class="n">UUID</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w">  </span><span class="k">GENERATED</span><span class="w"> </span><span class="n">ALWAYS</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="p">(</span><span class="k">data</span><span class="o">-&gt;&gt;</span><span class="s1">&#39;user_id&#39;</span><span class="p">)</span><span class="w"> </span><span class="n">STORED</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">CREATE</span><span class="w"> </span><span class="k">INDEX</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">events</span><span class="p">(</span><span class="n">user_id_extracted</span><span class="p">);</span></span></span></code></pre></div><p>Engine 沒變、schema 加了 generated column、query 走 index、變快。</p>
<h3 id="4-compilersource-level-macros">4. Compiler：source-level macros</h3>
<p>C 語言沒泛型、用 <code>#define</code> macro 模擬：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="ln">1</span><span class="cl"><span class="cp">#define SWAP(a, b, T) do { T tmp = a; a = b; b = tmp; } while(0)</span></span></span></code></pre></div><p>Compiler 沒變、source 經 preprocessor transformation、行為變了。</p>
<h3 id="5-frameworkwrapper-component">5. Framework：wrapper component</h3>
<p>React 沒 onAttach lifecycle、用 wrapper:</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-jsx" data-lang="jsx"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">function</span> <span class="nx">withMount</span><span class="p">(</span><span class="nx">Component</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="k">return</span> <span class="p">(</span><span class="nx">props</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="nx">useEffect</span><span class="p">(()</span> <span class="p">=&gt;</span> <span class="p">{</span> <span class="nx">props</span><span class="p">.</span><span class="nx">onMount</span><span class="o">?</span><span class="p">.();</span> <span class="p">},</span> <span class="p">[]);</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="k">return</span> <span class="p">&lt;</span><span class="nt">Component</span> <span class="p">{</span><span class="na">...props</span><span class="p">}</span> <span class="p">/&gt;;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">  <span class="p">};</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>React 沒變、加一層 wrapper、行為加上了。</p>
<hr>
<h2 id="input-跟-output-transformation-是不同-pattern">Input 跟 Output transformation 是不同 pattern</h2>
<p>外層 transformation 不是單一 pattern、要區分 <strong>input transformation</strong>（改 engine 看到的）跟 <strong>output transformation</strong>（改 engine 給出的）：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Input transformation</th>
          <th>Output transformation</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>動的位置</td>
          <td>Engine 之前（給 engine 的輸入）</td>
          <td>Engine 之後（engine 的輸出）</td>
      </tr>
      <tr>
          <td>Engine 行為</td>
          <td>改變（看到不同輸入、做出不同事）</td>
          <td>不變（後處理而已）</td>
      </tr>
      <tr>
          <td>副作用</td>
          <td>可能改變 ranking / 內部狀態</td>
          <td>純後加工、對 engine 透明</td>
      </tr>
      <tr>
          <td>失敗模式</td>
          <td>Input 錯誤 → engine 整個跑錯</td>
          <td>Output 處理錯 → 個別 case 不對</td>
      </tr>
      <tr>
          <td>適合</td>
          <td>改變 engine 「能看到什麼」</td>
          <td>補強 engine 「沒給的東西」</td>
      </tr>
  </tbody>
</table>
<h3 id="例對照">例對照</h3>
<p><strong>Search</strong>：</p>
<ul>
<li>Input：build-time 加 suffix tokens（engine 索引時看到 hidden &ldquo;pressure&rdquo;）</li>
<li>Output：result 出來後 client-side substring scan（engine 結果照常、額外加 fallback）</li>
</ul>
<p><strong>LLM</strong>：</p>
<ul>
<li>Input：prompt 加 &ldquo;請逐步推理&rdquo;（engine 看到不同 prompt）</li>
<li>Output：parse output 後 extract structured data（engine 輸出照常、後處理）</li>
</ul>
<p><strong>DB</strong>：</p>
<ul>
<li>Input：generated column（engine 索引時看到 denormalized column）</li>
<li>Output：query 結果後處理 / aggregation（engine 結果照常、應用層加工）</li>
</ul>
<h3 id="何時選哪個">何時選哪個</h3>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>選</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>想改變 engine 結果的「內容覆蓋」</td>
          <td>Input</td>
      </tr>
      <tr>
          <td>Engine 不該被改、純補強 output</td>
          <td>Output</td>
      </tr>
      <tr>
          <td>副作用衝突風險高（如 search ranking）</td>
          <td>Output（更安全）</td>
      </tr>
      <tr>
          <td>需要 engine 配合（index size、build cost）</td>
          <td>Input（更徹底）</td>
      </tr>
  </tbody>
</table>
<p><strong>多數實作 case 兩者疊加</strong>：input 解 80%（更徹底）、output 補剩下 20%（catch input 漏的）。</p>
<hr>
<h2 id="何時不該用外層-transformation">何時不該用外層 transformation</h2>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>為什麼</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Engine 開放了 API、有官方解</td>
          <td>用官方、別自己 transformation</td>
      </tr>
      <tr>
          <td>Transformation 跟 engine 行為衝突（例：injected tokens 影響 ranking）</td>
          <td>副作用大、考慮其他路</td>
      </tr>
      <tr>
          <td>Transformation 邏輯比 engine 還複雜</td>
          <td>可能該換 engine 了</td>
      </tr>
      <tr>
          <td>Transformation 永遠 catch 不全 edge case</td>
          <td>用了會誤導、不如顯式說「不支援」（<a href="../capability-gap-three-layer-escalation/">#86 L1</a>）</td>
      </tr>
      <tr>
          <td>Engine 升級會破壞 transformation</td>
          <td>維護成本長期高</td>
      </tr>
  </tbody>
</table>
<p>五類共通：<strong>transformation 的成本 / 風險 &gt; 動 engine 的成本</strong>。其他情境外層 transformation 是首選。</p>
<hr>
<h2 id="跟-45-外部組件合作四層的關係">跟 #45 外部組件合作四層的關係</h2>
<p><a href="../external-component-collaboration-layers/">#45</a> 講「離公共介面越近越穩」、本卡是這條原則的具體展開：</p>
<table>
  <thead>
      <tr>
          <th>#45 層次</th>
          <th>本卡對應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>公共介面層（最穩）</td>
          <td>Engine 開放的 API</td>
      </tr>
      <tr>
          <td>邊界層</td>
          <td><strong>外層 transformation</strong>（本卡焦點）</td>
      </tr>
      <tr>
          <td>內部結構層</td>
          <td>Engine 內部、不該動</td>
      </tr>
      <tr>
          <td>客戶端層</td>
          <td>Wrapper / proxy</td>
      </tr>
  </tbody>
</table>
<p><strong>外層 transformation 是邊界層的具體技法</strong> — 在 engine 公共介面外、做 input / output transformation。</p>
<hr>
<h2 id="反模式">反模式</h2>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>後果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>沒先試外層、直接 fork engine</td>
          <td>維護成本爆炸</td>
      </tr>
      <tr>
          <td>Transformation 寫得太聰明、catch 不全 case</td>
          <td>看似 work、暗藏 silent failure</td>
      </tr>
      <tr>
          <td>Transformation 跟 engine 預設行為衝突</td>
          <td>結果不可預期</td>
      </tr>
      <tr>
          <td>把 transformation 寫在 engine code 裡（混入）</td>
          <td>該升級 engine 時 transformation 跟著動、失去隔離價值</td>
      </tr>
      <tr>
          <td>Engine 升級後不重 review transformation</td>
          <td>可能新版已支援、舊 transformation 變累贅</td>
      </tr>
      <tr>
          <td>Transformation 沒文件、只有 implicit comment</td>
          <td>後人不懂為什麼 / 不敢碰</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<table>
  <thead>
      <tr>
          <th>原則</th>
          <th>關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../external-component-collaboration-layers/">#45 外部組件合作四層</a></td>
          <td>本卡是「邊界層」的具體技法</td>
      </tr>
      <tr>
          <td><a href="../capability-gap-three-layer-escalation/">#86 Capability gap 三層階梯</a></td>
          <td>外層 transformation 多是 L2 augmenting computation 的實作方式</td>
      </tr>
      <tr>
          <td><a href="../build-time-vs-runtime-computation-spectrum/">#87 Build-time vs Runtime</a></td>
          <td>Transformation 可放 build-time（suffix token）或 runtime（query rewrite）</td>
      </tr>
      <tr>
          <td><a href="../search-engine-matching-mode-mismatch/">#73 search 匹配模式</a></td>
          <td>search engine prefix-only 限制是本卡 case 1 的具體場景</td>
      </tr>
      <tr>
          <td><a href="../literal-interception-vs-behavioral-refinement/">#82 字面攔截 vs 行為精煉</a></td>
          <td>Transformation 是字面層 catch、適合 hook 自動化（build step）</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>「engine 不支援 X、所以 X 不能做」</td>
          <td>檢查能不能在外層做 transformation</td>
      </tr>
      <tr>
          <td>「我們需要 fork 這個 lib」</td>
          <td>先試外層、多數情況夠</td>
      </tr>
      <tr>
          <td>「等 upstream 加 feature」</td>
          <td>多半永遠等不到、外層先解</td>
      </tr>
      <tr>
          <td>「這個 hack 太醜、要改 engine」</td>
          <td>醜不是換工具的理由、看實際 ROI</td>
      </tr>
      <tr>
          <td>Transformation 寫了沒文件</td>
          <td>補 why、否則後人會誤拆</td>
      </tr>
      <tr>
          <td>同一 engine 累積 ≥ 3 種 transformation</td>
          <td>可能該換 engine 了</td>
      </tr>
      <tr>
          <td>升級 engine 後 transformation 沒測</td>
          <td>可能新版 native 支援、舊 transformation 多餘</td>
      </tr>
  </tbody>
</table>
<p><strong>核心</strong>：Engine 限制不等於 capability 限制 — engine 沒開放的能力、通常可在 engine 的輸入 / 輸出層做 transformation 補上。<strong>「engine 不支援」是表象、「我沒思考外層解」是根因</strong>。</p>
]]></content:encoded></item><item><title>L1 + L2 疊加時的訊號一致性：UX hint 跟自動 fallback 講的話要對齊</title><link>https://tarrragon.github.io/blog/report/layered-strategy-signal-consistency/</link><pubDate>Sun, 26 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/layered-strategy-signal-consistency/</guid><description>&lt;h2 id="結論">結論&lt;/h2>
&lt;p>把 &lt;a href="../capability-gap-three-layer-escalation/">L1 expectation alignment + L2 augmenting computation 疊加&lt;/a> 時、兩個 layer 給使用者的訊號要&lt;strong>對齊、不是 redundant 也不是 conflicting&lt;/strong>：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>兩 layer 的關係&lt;/th>
 &lt;th>使用者體驗&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;strong>Conflicting&lt;/strong>（L1 說一回事、L2 做相反事）&lt;/td>
 &lt;td>困惑、不信任系統&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>Redundant&lt;/strong>（L1 講 + L2 補的是同個東西）&lt;/td>
 &lt;td>噪音、L1 hint 失去意義&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>Layered consistent&lt;/strong>（L1 講 capability、L2 自動補 + 訊號明示「這是 fallback」）&lt;/td>
 &lt;td>清楚、信任&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>設計三條原則：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>L2 自動補時、訊號要明示「這是 fallback、不是 primary path」&lt;/strong>&lt;/li>
&lt;li>&lt;strong>L1 hint 要承認 L2 的存在&lt;/strong>（不要假裝 L2 不存在）&lt;/li>
&lt;li>&lt;strong>使用者一直能 trace「這個結果怎麼來的」&lt;/strong>&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="為什麼疊加會打架">為什麼疊加會打架&lt;/h2>
&lt;p>L1 跟 L2 各自設計、不協調時、訊號會相互削弱：&lt;/p>
&lt;h3 id="conflicting-例search">Conflicting 例：search&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Layer&lt;/th>
 &lt;th>訊號&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>L1 hint&lt;/td>
 &lt;td>&amp;ldquo;搜尋為前綴匹配、找 backpressure 請打 backpre&amp;rdquo;&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>L2 fallback&lt;/td>
 &lt;td>自動 substring 找到 backpressure、顯示為 normal result&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>User 打 &amp;ldquo;pre&amp;rdquo; → 看到 backpressure 結果 → 困惑：「不是說要打 backpre？」 → 不確定下次該怎麼搜。&lt;/p>
&lt;h3 id="redundant-例retry-with-hint">Redundant 例：retry with hint&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Layer&lt;/th>
 &lt;th>訊號&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>L1 hint&lt;/td>
 &lt;td>&amp;ldquo;網路不穩、稍後再試&amp;rdquo;&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>L2 retry&lt;/td>
 &lt;td>已經自動 retry 3 次&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>User 看到 hint → 自己 manual retry → 但 system 已經在 retry → 操作冗餘 → 不確定 retry 是 user 觸發還是 system。&lt;/p>
&lt;h3 id="conflicting-例editor-stale-data">Conflicting 例：editor stale data&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Layer&lt;/th>
 &lt;th>訊號&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>L1 banner&lt;/td>
 &lt;td>&amp;ldquo;資料同步可能延遲幾秒&amp;rdquo;&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>L2 fallback&lt;/td>
 &lt;td>Stale-while-revalidate 自動 refresh、user 沒感知&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>User 看到 banner、但每次資料其實都是 fresh（refresh 完成）→ banner 變 noise。Banner 撤掉後又會在某次 revalidation 失敗時 leak 出 stale data → 信任崩潰。&lt;/p>
&lt;hr>
&lt;h2 id="layered-consistency-的三設計原則">Layered Consistency 的三設計原則&lt;/h2>
&lt;h3 id="原則-1l2-自動補時訊號明示這是-fallback">原則 1：L2 自動補時、訊號明示「這是 fallback」&lt;/h3>
&lt;p>L2 不該無聲補強。當 L2 觸發、UI 應該標示：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>場景&lt;/th>
 &lt;th>Layered consistent 訊號&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Search prefix-only + substring fallback&lt;/td>
 &lt;td>Result 上方標 &amp;ldquo;找到 substring 匹配（非標準前綴）&amp;quot;、user 知道這是 fallback&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Retry on transient failure&lt;/td>
 &lt;td>Spinner + &amp;ldquo;重試中（第 N 次）&amp;quot;、user 不需自己 retry&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Stale-while-revalidate&lt;/td>
 &lt;td>&amp;ldquo;資料約 N 秒前&amp;rdquo;、user 知道是否需要 refresh&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>關鍵：&lt;strong>「自動補但隱形」是 silent UX&lt;/strong>、跟 &lt;a href="../literal-interception-vs-behavioral-refinement/">#82 字面攔截 vs 行為精煉&lt;/a> 的「false confidence」同骨。&lt;/p></description><content:encoded><![CDATA[<h2 id="結論">結論</h2>
<p>把 <a href="../capability-gap-three-layer-escalation/">L1 expectation alignment + L2 augmenting computation 疊加</a> 時、兩個 layer 給使用者的訊號要<strong>對齊、不是 redundant 也不是 conflicting</strong>：</p>
<table>
  <thead>
      <tr>
          <th>兩 layer 的關係</th>
          <th>使用者體驗</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Conflicting</strong>（L1 說一回事、L2 做相反事）</td>
          <td>困惑、不信任系統</td>
      </tr>
      <tr>
          <td><strong>Redundant</strong>（L1 講 + L2 補的是同個東西）</td>
          <td>噪音、L1 hint 失去意義</td>
      </tr>
      <tr>
          <td><strong>Layered consistent</strong>（L1 講 capability、L2 自動補 + 訊號明示「這是 fallback」）</td>
          <td>清楚、信任</td>
      </tr>
  </tbody>
</table>
<p>設計三條原則：</p>
<ol>
<li><strong>L2 自動補時、訊號要明示「這是 fallback、不是 primary path」</strong></li>
<li><strong>L1 hint 要承認 L2 的存在</strong>（不要假裝 L2 不存在）</li>
<li><strong>使用者一直能 trace「這個結果怎麼來的」</strong></li>
</ol>
<hr>
<h2 id="為什麼疊加會打架">為什麼疊加會打架</h2>
<p>L1 跟 L2 各自設計、不協調時、訊號會相互削弱：</p>
<h3 id="conflicting-例search">Conflicting 例：search</h3>
<table>
  <thead>
      <tr>
          <th>Layer</th>
          <th>訊號</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>L1 hint</td>
          <td>&ldquo;搜尋為前綴匹配、找 backpressure 請打 backpre&rdquo;</td>
      </tr>
      <tr>
          <td>L2 fallback</td>
          <td>自動 substring 找到 backpressure、顯示為 normal result</td>
      </tr>
  </tbody>
</table>
<p>User 打 &ldquo;pre&rdquo; → 看到 backpressure 結果 → 困惑：「不是說要打 backpre？」 → 不確定下次該怎麼搜。</p>
<h3 id="redundant-例retry-with-hint">Redundant 例：retry with hint</h3>
<table>
  <thead>
      <tr>
          <th>Layer</th>
          <th>訊號</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>L1 hint</td>
          <td>&ldquo;網路不穩、稍後再試&rdquo;</td>
      </tr>
      <tr>
          <td>L2 retry</td>
          <td>已經自動 retry 3 次</td>
      </tr>
  </tbody>
</table>
<p>User 看到 hint → 自己 manual retry → 但 system 已經在 retry → 操作冗餘 → 不確定 retry 是 user 觸發還是 system。</p>
<h3 id="conflicting-例editor-stale-data">Conflicting 例：editor stale data</h3>
<table>
  <thead>
      <tr>
          <th>Layer</th>
          <th>訊號</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>L1 banner</td>
          <td>&ldquo;資料同步可能延遲幾秒&rdquo;</td>
      </tr>
      <tr>
          <td>L2 fallback</td>
          <td>Stale-while-revalidate 自動 refresh、user 沒感知</td>
      </tr>
  </tbody>
</table>
<p>User 看到 banner、但每次資料其實都是 fresh（refresh 完成）→ banner 變 noise。Banner 撤掉後又會在某次 revalidation 失敗時 leak 出 stale data → 信任崩潰。</p>
<hr>
<h2 id="layered-consistency-的三設計原則">Layered Consistency 的三設計原則</h2>
<h3 id="原則-1l2-自動補時訊號明示這是-fallback">原則 1：L2 自動補時、訊號明示「這是 fallback」</h3>
<p>L2 不該無聲補強。當 L2 觸發、UI 應該標示：</p>
<table>
  <thead>
      <tr>
          <th>場景</th>
          <th>Layered consistent 訊號</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Search prefix-only + substring fallback</td>
          <td>Result 上方標 &ldquo;找到 substring 匹配（非標準前綴）&quot;、user 知道這是 fallback</td>
      </tr>
      <tr>
          <td>Retry on transient failure</td>
          <td>Spinner + &ldquo;重試中（第 N 次）&quot;、user 不需自己 retry</td>
      </tr>
      <tr>
          <td>Stale-while-revalidate</td>
          <td>&ldquo;資料約 N 秒前&rdquo;、user 知道是否需要 refresh</td>
      </tr>
  </tbody>
</table>
<p>關鍵：<strong>「自動補但隱形」是 silent UX</strong>、跟 <a href="../literal-interception-vs-behavioral-refinement/">#82 字面攔截 vs 行為精煉</a> 的「false confidence」同骨。</p>
<h3 id="原則-2l1-hint-要承認-l2-的存在">原則 2：L1 hint 要承認 L2 的存在</h3>
<p>L1 hint 不該假裝是「全部能做的事」：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">壞：搜尋為前綴匹配、找 backpressure 請打 backpre
</span></span><span class="line"><span class="ln">2</span><span class="cl">好：搜尋優先前綴匹配；找不到時會 fallback 到 substring（顯示時會標示）。
</span></span><span class="line"><span class="ln">3</span><span class="cl">   想精準找 backpressure 直接打完整詞、或打 backpre。</span></span></code></pre></div><p>L1 講 capability + L2 講 fallback、合在一起 = 完整的 mental model。</p>
<h3 id="原則-3可-trace-結果怎麼來的">原則 3：可 trace 「結果怎麼來的」</h3>
<p>User 能（不必、但能）看到結果的來源層：</p>
<ul>
<li>Search result 標 &ldquo;prefix match&rdquo; / &ldquo;substring fallback&rdquo;</li>
<li>API response 標 <code>from_cache: true</code> 或 <code>freshness_seconds: 30</code></li>
<li>LLM response 標「來自 RAG retrieval / 來自 base model knowledge」</li>
</ul>
<p>可 trace ≠ 強制顯示、是「想知道時可以知道」。預設可隱藏、debug / 進階 user 可展開。</p>
<hr>
<h2 id="反模式">反模式</h2>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>後果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>L2 隱形補強、L1 hint 沒提 L2</td>
          <td>使用者不知道有 fallback、抱怨 hint 不準</td>
      </tr>
      <tr>
          <td>L1 hint + L2 自動 retry 都顯示</td>
          <td>Redundant、user 重複動作</td>
      </tr>
      <tr>
          <td>L2 失敗時退回 L1 但訊號沒切換</td>
          <td>User 看到舊 hint、實際 system 在另一狀態</td>
      </tr>
      <tr>
          <td>「不要讓 user 看到 fallback」當原則</td>
          <td>Silent fallback 是 <a href="../visual-completion-vs-functional-completion/">#56 視覺完成 vs 功能完成</a> 的反例</td>
      </tr>
      <tr>
          <td>L1 / L2 是不同 team 設計、沒協調</td>
          <td>訊號自然衝突、需要 cross-team review</td>
      </tr>
      <tr>
          <td>Telemetry 沒分 L1 / L2 觸發比例</td>
          <td>不知道哪 layer 真的解 gap</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="何時-conflicting--redundant-是合理的">何時 conflicting / redundant 是合理的</h2>
<p>少數情境：</p>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>為什麼 conflicting / redundant 可接受</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>L1 是 legal disclaimer（必要法律文字）</td>
          <td>法律要求、不能因 L2 拿掉</td>
      </tr>
      <tr>
          <td>L2 是 emergency fallback、L1 是 primary</td>
          <td>各自負責不同 case、訊號可重疊</td>
      </tr>
      <tr>
          <td>安全 critical 多重提醒</td>
          <td>重要訊號值得 redundant</td>
      </tr>
  </tbody>
</table>
<p>三類共通：<strong>訊號重複的成本 &lt; 訊號漏掉的成本</strong>。其他情境追求 layered consistent。</p>
<hr>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<table>
  <thead>
      <tr>
          <th>原則</th>
          <th>關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../main-strategy-plus-supplementary/">#75 主策略 + 補強疊加</a></td>
          <td>#75 講疊加可行、本卡講疊加後 UX 訊號層怎麼設計</td>
      </tr>
      <tr>
          <td><a href="../capability-gap-three-layer-escalation/">#86 Capability gap 三層階梯</a></td>
          <td>#86 講選哪層、本卡講疊加多層時訊號</td>
      </tr>
      <tr>
          <td><a href="../decision-dialogue-dimensions/">#79 決策對話的五維度</a></td>
          <td>「使用者看到什麼」是 decision dialogue 的「呈現」維度、本卡是其特化</td>
      </tr>
      <tr>
          <td><a href="../visual-completion-vs-functional-completion/">#56 視覺完成 vs 功能完成</a></td>
          <td>Silent L2 fallback 是「視覺完成、功能不誠實」的變種</td>
      </tr>
      <tr>
          <td><a href="../pattern-honest-progress-ui/">#62 誠實進度 UI</a></td>
          <td>本卡的「fallback 訊號明示」原則跟誠實進度同骨</td>
      </tr>
      <tr>
          <td><a href="../literal-interception-vs-behavioral-refinement/">#82 字面攔截 vs 行為精煉</a></td>
          <td>「自動補但隱形」是 false confidence 的 UX 變種</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="套用到當前-search-planning-case">套用到當前 search planning case</h2>
<p>D + C1 疊加 case：</p>
<p><strong>Bad</strong>（conflicting）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">D hint: &#34;搜尋為前綴匹配、找 backpressure 請打 backpre&#34;
</span></span><span class="line"><span class="ln">2</span><span class="cl">C1 fallback: 打 &#34;pre&#34; 自動 substring 找到 backpressure、跟其他 prefix result 混排</span></span></code></pre></div><p><strong>Good</strong>（layered consistent）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">D hint: &#34;搜尋優先前綴匹配。找不到時自動 fallback 到 substring（會標示）。&#34;
</span></span><span class="line"><span class="ln">2</span><span class="cl">C1 fallback UI:
</span></span><span class="line"><span class="ln">3</span><span class="cl">  - Prefix matches（標準）：[後跟前綴匹配 results]
</span></span><span class="line"><span class="ln">4</span><span class="cl">  - Substring matches（fallback）：[標示後跟 fallback results]</span></span></code></pre></div><p>User 看到的：</p>
<ul>
<li>打 &ldquo;pre&rdquo; → 立刻看到 prefix matches（如「prefetch」）</li>
<li>同頁標 &ldquo;Substring fallback&rdquo; 段、列「backpressure」等 substring 命中</li>
<li>看 hint 也知道為什麼有兩段</li>
</ul>
<p>訊號對齊、user mental model 完整。</p>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>L1 hint 寫完才寫 L2、沒重 review L1</td>
          <td>退回重看 L1 是否承認 L2</td>
      </tr>
      <tr>
          <td>L2 自動補但 UI 看不出來</td>
          <td>加 fallback 訊號</td>
      </tr>
      <tr>
          <td>User 抱怨「hint 跟實際不一致」</td>
          <td>Layered consistency 沒做、補上</td>
      </tr>
      <tr>
          <td>L1 / L2 telemetry 沒分</td>
          <td>不知道誰實際 close gap、補</td>
      </tr>
      <tr>
          <td>Hint 越寫越長</td>
          <td>可能 L2 沒 surface、L1 在補 L2 該講的</td>
      </tr>
      <tr>
          <td>「user 看不到 fallback 比較單純」直覺</td>
          <td>Silent UX 反模式、 fallback 該明示</td>
      </tr>
  </tbody>
</table>
<p><strong>核心</strong>：L1 + L2 疊加不是「兩個獨立 layer 各自做事」、是<strong>一個 capability gap 上的兩個訊號</strong>。訊號要對齊、否則使用者收到的 mental model 是 broken。<strong>Silent fallback 看起來簡潔、實際是 false confidence</strong>。</p>
]]></content:encoded></item></channel></rss>