<?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>Refactor on Tarragon</title><link>https://tarrragon.github.io/blog/tags/refactor/</link><description>Recent content in Refactor on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Sat, 25 Apr 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/refactor/index.xml" rel="self" type="application/rss+xml"/><item><title>CSS Layers 取代 specificity 戰</title><link>https://tarrragon.github.io/blog/report/css-layers-over-specificity/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/css-layers-over-specificity/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>CSS Layers 把樣式覆寫從「線性 specificity 數字戰」改成「分組權重順序」。&lt;/strong> 把外部組件 CSS &lt;code>@import&lt;/code> 進一個 layer、自家 CSS 留在 unlayered，自家規則自動贏 — 不論個別 selector specificity 數值。一次設定、所有 &lt;code>!important&lt;/code> 與 &lt;code>.x.x&lt;/code> 雙寫 hack 可以拿掉。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼-specificity-戰沒有贏家">為什麼 specificity 戰沒有贏家&lt;/h2>
&lt;h3 id="商業邏輯">商業邏輯&lt;/h3>
&lt;p>CSS specificity 是線性數字比較。組件作者用 &lt;code>.x.svelte-yyy.svelte-yyy&lt;/code> 雙寫 specificity 30 → 自家用 &lt;code>.search-shell .x&lt;/code> specificity 20 蓋不過 → 加 &lt;code>.x.x&lt;/code> 雙寫到 30 → 還是看 source order → 加 &lt;code>!important&lt;/code> → 跟其他 important 對撞 → 寫死多層 fallback。&lt;/p>
&lt;p>每加一層覆寫成本累積、未來 debug 越來越難。每個 &lt;code>!important&lt;/code> 都是一個 future debugging burden、&lt;code>!important&lt;/code> 之間沒有層級可言。&lt;/p>
&lt;h3 id="css-layers-的解法">CSS Layers 的解法&lt;/h3>
&lt;p>CSS &lt;code>@layer&lt;/code> 提供「分組權重」 — unlayered CSS &amp;gt; layered CSS（layer 越早宣告越低權）、跟 selector specificity 無關：&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">unlayered { ... } ← 最高權
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">@layer high { ... }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">@layer medium { ... }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">@layer low { ... } ← 最低權&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>把組件 CSS 整包丟進某個 layer、自家 CSS 留 unlayered、自家規則自動贏所有組件規則 — &lt;strong>不論 specificity&lt;/strong>。&lt;/p>
&lt;hr>
&lt;h2 id="這次任務的覆寫戰場">這次任務的覆寫戰場&lt;/h2>
&lt;h3 id="觀察">觀察&lt;/h3>
&lt;p>現在 &lt;code>search.html&lt;/code> 內為了蓋過 pagefind specificity 30 的寫法：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-css" data-lang="css">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="p">.&lt;/span>&lt;span class="nc">pagefind-ui__filter-block&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="k">border-bottom&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">0&lt;/span> &lt;span class="cp">!important&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="p">.&lt;/span>&lt;span class="nc">pagefind-ui__filter-panel&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="k">display&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">none&lt;/span> &lt;span class="cp">!important&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="p">.&lt;/span>&lt;span class="nc">search-filter-slot&lt;/span> &lt;span class="nt">fieldset&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="k">border&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">padding&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">margin&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>每條都靠 &lt;code>!important&lt;/code> 或 source order 取勝。可維護性低。&lt;/p>
&lt;h3 id="判讀">判讀&lt;/h3>
&lt;p>把 pagefind-ui.css 用 &lt;code>@import&lt;/code> 包進 layer：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-css" data-lang="css">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="p">@&lt;/span>&lt;span class="k">import&lt;/span> &lt;span class="nt">url&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="s2">&amp;#34;/blog/pagefind/pagefind-ui.css&amp;#34;&lt;/span>&lt;span class="o">)&lt;/span> &lt;span class="nt">layer&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="nt">pagefind&lt;/span>&lt;span class="o">)&lt;/span>&lt;span class="p">;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>自家 CSS 不加 layer 宣告、留 unlayered。自家規則優先級自動高於 layer(pagefind)。&lt;/p>
&lt;h3 id="執行refactor-步驟">執行：refactor 步驟&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-css" data-lang="css">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c">/* search.html / assets/search.css */&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="c">/* 把 pagefind 的整包 CSS 包進 layer */&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 class="k">import&lt;/span> &lt;span class="nt">url&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="s2">&amp;#34;/blog/pagefind/pagefind-ui.css&amp;#34;&lt;/span>&lt;span class="o">)&lt;/span> &lt;span class="nt">layer&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="nt">pagefind&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>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="c">/* 自家 CSS 留 unlayered、自動贏 */&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 class="nc">pagefind-ui__filter-block&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="k">border-bottom&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c">/* 不需要 !important */&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">}&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 class="nc">pagefind-ui__filter-panel&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">display&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">none&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c">/* 不需要 !important */&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;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="p">@&lt;/span>&lt;span class="k">media&lt;/span> &lt;span class="o">(&lt;/span>&lt;span class="nt">min-width&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nt">1400px&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">14&lt;/span>&lt;span class="cl"> &lt;span class="p">.&lt;/span>&lt;span class="nc">pagefind-ui__filter-panel&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="k">display&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">none&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">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>原本的 &lt;code>&amp;lt;link href=&amp;quot;...pagefind-ui.css&amp;quot; rel=&amp;quot;stylesheet&amp;quot;&amp;gt;&lt;/code> 改成上方 &lt;code>@import&lt;/code> 寫法、確保 import 在自家 CSS 之前發生（layered CSS 不會阻擋 unlayered CSS 的優先級）。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>CSS Layers 把樣式覆寫從「線性 specificity 數字戰」改成「分組權重順序」。</strong> 把外部組件 CSS <code>@import</code> 進一個 layer、自家 CSS 留在 unlayered，自家規則自動贏 — 不論個別 selector specificity 數值。一次設定、所有 <code>!important</code> 與 <code>.x.x</code> 雙寫 hack 可以拿掉。</p>
<hr>
<h2 id="為什麼-specificity-戰沒有贏家">為什麼 specificity 戰沒有贏家</h2>
<h3 id="商業邏輯">商業邏輯</h3>
<p>CSS specificity 是線性數字比較。組件作者用 <code>.x.svelte-yyy.svelte-yyy</code> 雙寫 specificity 30 → 自家用 <code>.search-shell .x</code> specificity 20 蓋不過 → 加 <code>.x.x</code> 雙寫到 30 → 還是看 source order → 加 <code>!important</code> → 跟其他 important 對撞 → 寫死多層 fallback。</p>
<p>每加一層覆寫成本累積、未來 debug 越來越難。每個 <code>!important</code> 都是一個 future debugging burden、<code>!important</code> 之間沒有層級可言。</p>
<h3 id="css-layers-的解法">CSS Layers 的解法</h3>
<p>CSS <code>@layer</code> 提供「分組權重」 — unlayered CSS &gt; layered CSS（layer 越早宣告越低權）、跟 selector specificity 無關：</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">unlayered { ... }              ← 最高權
</span></span><span class="line"><span class="ln">2</span><span class="cl">@layer high { ... }
</span></span><span class="line"><span class="ln">3</span><span class="cl">@layer medium { ... }
</span></span><span class="line"><span class="ln">4</span><span class="cl">@layer low { ... }             ← 最低權</span></span></code></pre></div><p>把組件 CSS 整包丟進某個 layer、自家 CSS 留 unlayered、自家規則自動贏所有組件規則 — <strong>不論 specificity</strong>。</p>
<hr>
<h2 id="這次任務的覆寫戰場">這次任務的覆寫戰場</h2>
<h3 id="觀察">觀察</h3>
<p>現在 <code>search.html</code> 內為了蓋過 pagefind specificity 30 的寫法：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-css" data-lang="css"><span class="line"><span class="ln">1</span><span class="cl"><span class="p">.</span><span class="nc">pagefind-ui__filter-block</span> <span class="p">{</span> <span class="k">border-bottom</span><span class="p">:</span> <span class="mi">0</span> <span class="cp">!important</span><span class="p">;</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="p">.</span><span class="nc">pagefind-ui__filter-panel</span> <span class="p">{</span> <span class="k">display</span><span class="p">:</span> <span class="kc">none</span> <span class="cp">!important</span><span class="p">;</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="p">.</span><span class="nc">search-filter-slot</span> <span class="nt">fieldset</span> <span class="p">{</span> <span class="k">border</span><span class="p">:</span> <span class="mi">0</span><span class="p">;</span> <span class="k">padding</span><span class="p">:</span> <span class="mi">0</span><span class="p">;</span> <span class="k">margin</span><span class="p">:</span> <span class="mi">0</span><span class="p">;</span> <span class="p">}</span></span></span></code></pre></div><p>每條都靠 <code>!important</code> 或 source order 取勝。可維護性低。</p>
<h3 id="判讀">判讀</h3>
<p>把 pagefind-ui.css 用 <code>@import</code> 包進 layer：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-css" data-lang="css"><span class="line"><span class="ln">1</span><span class="cl"><span class="p">@</span><span class="k">import</span> <span class="nt">url</span><span class="o">(</span><span class="s2">&#34;/blog/pagefind/pagefind-ui.css&#34;</span><span class="o">)</span> <span class="nt">layer</span><span class="o">(</span><span class="nt">pagefind</span><span class="o">)</span><span class="p">;</span></span></span></code></pre></div><p>自家 CSS 不加 layer 宣告、留 unlayered。自家規則優先級自動高於 layer(pagefind)。</p>
<h3 id="執行refactor-步驟">執行：refactor 步驟</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-css" data-lang="css"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c">/* search.html / assets/search.css */</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="c">/* 把 pagefind 的整包 CSS 包進 layer */</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="p">@</span><span class="k">import</span> <span class="nt">url</span><span class="o">(</span><span class="s2">&#34;/blog/pagefind/pagefind-ui.css&#34;</span><span class="o">)</span> <span class="nt">layer</span><span class="o">(</span><span class="nt">pagefind</span><span class="o">)</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="c">/* 自家 CSS 留 unlayered、自動贏 */</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="p">.</span><span class="nc">pagefind-ui__filter-block</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  <span class="k">border-bottom</span><span class="p">:</span> <span class="mi">0</span><span class="p">;</span>            <span class="c">/* 不需要 !important */</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="p">.</span><span class="nc">pagefind-ui__filter-panel</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">  <span class="k">display</span><span class="p">:</span> <span class="kc">none</span><span class="p">;</span>               <span class="c">/* 不需要 !important */</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="p">@</span><span class="k">media</span> <span class="o">(</span><span class="nt">min-width</span><span class="o">:</span> <span class="nt">1400px</span><span class="o">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">  <span class="p">.</span><span class="nc">pagefind-ui__filter-panel</span> <span class="p">{</span> <span class="k">display</span><span class="p">:</span> <span class="kc">none</span><span class="p">;</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>原本的 <code>&lt;link href=&quot;...pagefind-ui.css&quot; rel=&quot;stylesheet&quot;&gt;</code> 改成上方 <code>@import</code> 寫法、確保 import 在自家 CSS 之前發生（layered CSS 不會阻擋 unlayered CSS 的優先級）。</p>
<hr>
<h2 id="內在屬性比較四種-specificity-應對">內在屬性比較：四種 specificity 應對</h2>
<table>
  <thead>
      <tr>
          <th>方法</th>
          <th>維護成本</th>
          <th>可讀性</th>
          <th>升級兼容性</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>!important</code> 對抗</td>
          <td>高 — 每加一條未來 debug 成本上升</td>
          <td>低 — 不知為什麼要 important</td>
          <td>中 — 組件變更可能讓 important 用錯</td>
      </tr>
      <tr>
          <td>雙寫 class（<code>.x.x</code>）</td>
          <td>中 — selector 看起來奇怪</td>
          <td>低 — 維護者不知為什麼</td>
          <td>中 — 組件改 class 名就失效</td>
      </tr>
      <tr>
          <td>Inline style + setProperty important</td>
          <td>高 — 散落在 JS 各處</td>
          <td>最低 — 不在 CSS 找不到</td>
          <td>低 — JS 規則容易被 framework 重繪打破</td>
      </tr>
      <tr>
          <td>CSS Layers</td>
          <td>低 — 一次設定、規則簡單</td>
          <td>高 — 結構化分層</td>
          <td>高 — 跟組件升級無關</td>
      </tr>
  </tbody>
</table>
<p><strong>Layers 的所有指標都最佳</strong>。其他三種是 Layers 之前的 workaround、現在沒理由繼續用。</p>
<hr>
<h2 id="layers-的進階用法">Layers 的進階用法</h2>
<h3 id="多個外部組件分別-layer">多個外部組件分別 layer</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-css" data-lang="css"><span class="line"><span class="ln">1</span><span class="cl"><span class="p">@</span><span class="k">import</span> <span class="nt">url</span><span class="o">(</span><span class="s2">&#34;vendor-a.css&#34;</span><span class="o">)</span> <span class="nt">layer</span><span class="o">(</span><span class="nt">vendor-a</span><span class="o">)</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="p">@</span><span class="k">import</span> <span class="nt">url</span><span class="o">(</span><span class="s2">&#34;vendor-b.css&#34;</span><span class="o">)</span> <span class="nt">layer</span><span class="o">(</span><span class="nt">vendor-b</span><span class="o">)</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="p">@</span><span class="k">layer</span> <span class="nt">vendor-a</span><span class="o">,</span> <span class="nt">vendor-b</span><span class="p">;</span>   <span class="c">/* 後宣告的優先 */</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="c">/* 自家 unlayered */</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="p">.</span><span class="nc">my-overrides</span> <span class="p">{</span> <span class="err">...</span> <span class="p">}</span></span></span></code></pre></div><p><code>@layer name1, name2;</code> 顯式宣告 layer 順序、後宣告的權重高。</p>
<h3 id="自家-css-也分層">自家 CSS 也分層</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-css" data-lang="css"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="p">@</span><span class="k">layer</span> <span class="nt">base</span><span class="o">,</span> <span class="nt">components</span><span class="o">,</span> <span class="nt">utilities</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="p">@</span><span class="k">layer</span> <span class="nt">base</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="nt">body</span> <span class="p">{</span> <span class="k">font-family</span><span class="p">:</span> <span class="o">...</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 class="p">@</span><span class="k">layer</span> <span class="nt">components</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  <span class="p">.</span><span class="nc">button</span> <span class="p">{</span> <span class="k">padding</span><span class="p">:</span> <span class="o">...</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 class="p">@</span><span class="k">layer</span> <span class="nt">utilities</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">  <span class="p">.</span><span class="nc">text-center</span> <span class="p">{</span> <span class="k">text-align</span><span class="p">:</span> <span class="kc">center</span><span class="p">;</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>自家 CSS 內部也分層、避免 utilities 被 components 蓋過。</p>
<h3 id="跟-unlayered-並存">跟 unlayered 並存</h3>
<p>不是所有自家 CSS 都要分 layer。<strong>最高優先的自家規則留 unlayered、其他規則可以分層</strong>。</p>
<hr>
<h2 id="瀏覽器支援">瀏覽器支援</h2>
<p>CSS Layers 在所有主流瀏覽器（Chrome 99+、Firefox 97+、Safari 15.4+）支援、2022 年起。當前（2026）所有現代瀏覽器都支援。</p>
<p>對舊瀏覽器降級：不支援 <code>@layer</code> 的瀏覽器會把整個 <code>@layer { ... }</code> block 當作 invalid 跳過 — 自家 unlayered 規則仍然適用、效果一樣（但 vendor CSS 完全失效）。實務上不需要擔心。</p>
<hr>
<h2 id="設計取捨覆寫外部組件-css-的策略">設計取捨：覆寫外部組件 CSS 的策略</h2>
<p>四種做法、各自機會成本不同。這個專案選 A（CSS Layers）當預設、其他做法在特定情境合理。</p>
<h3 id="acss-layers這個專案的預設">A：CSS Layers（這個專案的預設）</h3>
<ul>
<li><strong>機制</strong>：<code>@import url(...) layer(vendor)</code> 把外部 CSS 包進低權層、自家 unlayered CSS 自動贏</li>
<li><strong>選 A 的理由</strong>：跨組件升級穩定、規則簡單、<code>!important</code> 完全不需要、跳出 specificity 線性比較戰場</li>
<li><strong>適合</strong>：所有現代瀏覽器（Chrome 99+ / Firefox 97+ / Safari 15.4+）的客製情境</li>
<li><strong>代價</strong>：需要重新引入 vendor CSS（從 <code>&lt;link&gt;</code> 改 <code>@import</code>）</li>
</ul>
<h3 id="b雙寫-class-提升-specificity">B：雙寫 class 提升 specificity</h3>
<ul>
<li><strong>機制</strong>：<code>.pagefind-ui__filter-block.pagefind-ui__filter-block</code> 寫兩次提升 specificity 從 10 到 20</li>
<li><strong>跟 A 的取捨</strong>：B 不需要改 vendor CSS 引入方式、A 需要；但 B 跟組件 specificity 競賽（組件作者改 hash 寫法就壞）、A 跳出競賽</li>
<li><strong>B 是反模式</strong>：跟組件 specificity 競賽（組件作者改 hash 寫法就壞） — 唯一例外是 vendor CSS 不能用 <code>@import</code> 引入（極罕見的 build pipeline 限制）</li>
</ul>
<h3 id="cimportant-對抗">C：<code>!important</code> 對抗</h3>
<ul>
<li><strong>機制</strong>：每條覆寫加 <code>!important</code>、用 importance 取勝</li>
<li><strong>跟 A 的取捨</strong>：C 短期有效、長期 important 之間沒層級可言；多個 important 對撞時 debug 困難</li>
<li><strong>C 才合理的情境</strong>：CSS Layers 不支援的舊瀏覽器（&lt; 2022 的版本）、且確認沒其他 important 對撞</li>
</ul>
<h3 id="dinline-style--setpropertyimportant">D：Inline style + <code>setProperty('important')</code></h3>
<ul>
<li><strong>機制</strong>：JS 用 <code>el.style.setProperty('display', 'none', 'important')</code></li>
<li><strong>成本特別高的原因</strong>：規則散落在 JS 各處、devtools 看不出意圖、跟 framework 重繪競爭</li>
<li><strong>D 才合理的情境</strong>：動態值（runtime 算的位置 / 尺寸）必須用 inline 表達 — 但即使這樣、也建議用 class toggle + CSS 變數（<a href="../class-toggle-over-important/">#28</a>）取代</li>
</ul>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>Refactor 動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>為了蓋過組件規則寫了 <code>!important</code></td>
          <td>評估改用 CSS Layers</td>
      </tr>
      <tr>
          <td>Selector 寫成 <code>.x.x</code> 雙寫只為了 specificity</td>
          <td>評估改用 CSS Layers</td>
      </tr>
      <tr>
          <td>覆寫邏輯散落在多個檔案 / inline style</td>
          <td>集中到一份 CSS、用 layers 分層</td>
      </tr>
      <tr>
          <td>組件升級後覆寫失效</td>
          <td>用 layers 隔離、跟組件 specificity 變動脫鉤</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：跟組件 CSS 競爭 specificity 是不必要的戰爭。Layers 提供更高層的權重機制、把覆寫簡化成「自家 vs 別人」的二元決定。</p>
]]></content:encoded></item><item><title>CSS / JS 拆出獨立檔案</title><link>https://tarrragon.github.io/blog/report/extract-css-js-files/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/extract-css-js-files/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>Inline CSS / JS 超過 ~30 行就值得拆出獨立檔案、走 Hugo &lt;code>resources.Get | minify | fingerprint&lt;/code> 引入。&lt;/strong> Template 變單純、editor 對 .css/.js 有 syntax highlight、minify 自動化、cache-busting fingerprint 自動處理。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼-inline-有上限">為什麼 inline 有上限&lt;/h2>
&lt;h3 id="商業邏輯">商業邏輯&lt;/h3>
&lt;p>Inline CSS / JS 在 Hugo template 內看似省事（一個檔案搞定），但隨著規模上升出現多個成本：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>規模&lt;/th>
 &lt;th>Inline 的代價&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&amp;lt; 10 行&lt;/td>
 &lt;td>幾乎無 — 一目了然&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>10-30 行&lt;/td>
 &lt;td>中 — Editor 不太能 highlight、template 開始混雜&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>30+ 行&lt;/td>
 &lt;td>高 — 找東西要在 template 模式間切換、minify 沒做、cache 控制困難&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>拆檔的成本是「多 1-2 個檔案」、收益是「multiple」 — 過了 30 行門檻、ROI 已正向。&lt;/p>
&lt;h3 id="拆檔的實際得益">拆檔的實際得益&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>維度&lt;/th>
 &lt;th>Inline&lt;/th>
 &lt;th>拆檔 + Resources Pipeline&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Editor syntax highlight&lt;/td>
 &lt;td>部分 — 看 editor 是否支援 mixed mode&lt;/td>
 &lt;td>完整 — 純 .css / .js 檔&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Minify&lt;/td>
 &lt;td>手動或 hugo template minify&lt;/td>
 &lt;td>Hugo &lt;code>minify&lt;/code> pipe 自動&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Cache-busting&lt;/td>
 &lt;td>手動加版本號&lt;/td>
 &lt;td>&lt;code>fingerprint&lt;/code> 自動&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>程式碼重用&lt;/td>
 &lt;td>難 — 跟 template 綁&lt;/td>
 &lt;td>容易 — 多 template 共用&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Version control diff&lt;/td>
 &lt;td>跟 template 改動混&lt;/td>
 &lt;td>純檔案改動、清楚&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;hr>
&lt;h2 id="這次任務的拆檔目標">這次任務的拆檔目標&lt;/h2>
&lt;h3 id="觀察">觀察&lt;/h3>
&lt;p>&lt;code>layouts/_default/search.html&lt;/code> 現況：&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>Hugo template 與 HTML&lt;/td>
 &lt;td>~30&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Inline &lt;code>&amp;lt;script&amp;gt;&lt;/code>&lt;/td>
 &lt;td>~110&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Inline &lt;code>&amp;lt;style&amp;gt;&lt;/code>&lt;/td>
 &lt;td>~80&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>總計&lt;/strong>&lt;/td>
 &lt;td>&lt;strong>~220&lt;/strong>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>220 行的 single-file template、CSS / JS 各超過拆檔門檻 3-4 倍。&lt;/p>
&lt;h3 id="判讀">判讀&lt;/h3>
&lt;p>把 CSS 拆到 &lt;code>assets/search.css&lt;/code>、JS 拆到 &lt;code>assets/search.js&lt;/code>、template 只剩 HTML 結構與 Hugo 引入。&lt;/p>
&lt;h3 id="執行拆檔步驟">執行：拆檔步驟&lt;/h3>
&lt;h4 id="step-1建立-assets-檔">Step 1：建立 assets 檔&lt;/h4>





&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">assets/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">├── search.css # 原本 inline &amp;lt;style&amp;gt; 內容
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">└── search.js # 原本 inline &amp;lt;script&amp;gt; 內容&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h4 id="step-2template-引入">Step 2：template 引入&lt;/h4>





&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">{{ define &amp;#34;main&amp;#34; }}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">{{- $css := resources.Get &amp;#34;search.css&amp;#34; | minify | fingerprint -}}
&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">link&lt;/span> &lt;span class="na">href&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;{{ $css.RelPermalink }}&amp;#34;&lt;/span> &lt;span class="na">rel&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;stylesheet&amp;#34;&lt;/span> &lt;span class="na">integrity&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;{{ $css.Data.Integrity }}&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>&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">data-pagefind-ignore&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;search-shell&amp;#34;&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>&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">{{- $js := resources.Get &amp;#34;search.js&amp;#34; | minify | fingerprint -}}
&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">script&lt;/span> &lt;span class="na">src&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;{{ $js.RelPermalink }}&amp;#34;&lt;/span> &lt;span class="na">integrity&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;{{ $js.Data.Integrity }}&amp;#34;&lt;/span> &lt;span class="na">defer&lt;/span>&lt;span class="p">&amp;gt;&amp;lt;/&lt;/span>&lt;span class="nt">script&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">{{ end }}&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h4 id="step-3js-從全域-windowpagefindui-改為-module-模式">Step 3：JS 從全域 &lt;code>window.PagefindUI&lt;/code> 改為 module 模式&lt;/h4>
&lt;p>如果原本 inline JS 用 &lt;code>new PagefindUI(...)&lt;/code> 直接執行、拆檔後仍然可以這樣寫。但若想進一步，把 init 包成 function：&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>Inline CSS / JS 超過 ~30 行就值得拆出獨立檔案、走 Hugo <code>resources.Get | minify | fingerprint</code> 引入。</strong> Template 變單純、editor 對 .css/.js 有 syntax highlight、minify 自動化、cache-busting fingerprint 自動處理。</p>
<hr>
<h2 id="為什麼-inline-有上限">為什麼 inline 有上限</h2>
<h3 id="商業邏輯">商業邏輯</h3>
<p>Inline CSS / JS 在 Hugo template 內看似省事（一個檔案搞定），但隨著規模上升出現多個成本：</p>
<table>
  <thead>
      <tr>
          <th>規模</th>
          <th>Inline 的代價</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>&lt; 10 行</td>
          <td>幾乎無 — 一目了然</td>
      </tr>
      <tr>
          <td>10-30 行</td>
          <td>中 — Editor 不太能 highlight、template 開始混雜</td>
      </tr>
      <tr>
          <td>30+ 行</td>
          <td>高 — 找東西要在 template 模式間切換、minify 沒做、cache 控制困難</td>
      </tr>
  </tbody>
</table>
<p>拆檔的成本是「多 1-2 個檔案」、收益是「multiple」 — 過了 30 行門檻、ROI 已正向。</p>
<h3 id="拆檔的實際得益">拆檔的實際得益</h3>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Inline</th>
          <th>拆檔 + Resources Pipeline</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Editor syntax highlight</td>
          <td>部分 — 看 editor 是否支援 mixed mode</td>
          <td>完整 — 純 .css / .js 檔</td>
      </tr>
      <tr>
          <td>Minify</td>
          <td>手動或 hugo template minify</td>
          <td>Hugo <code>minify</code> pipe 自動</td>
      </tr>
      <tr>
          <td>Cache-busting</td>
          <td>手動加版本號</td>
          <td><code>fingerprint</code> 自動</td>
      </tr>
      <tr>
          <td>程式碼重用</td>
          <td>難 — 跟 template 綁</td>
          <td>容易 — 多 template 共用</td>
      </tr>
      <tr>
          <td>Version control diff</td>
          <td>跟 template 改動混</td>
          <td>純檔案改動、清楚</td>
      </tr>
      <tr>
          <td>測試</td>
          <td>難</td>
          <td>可單獨測</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="這次任務的拆檔目標">這次任務的拆檔目標</h2>
<h3 id="觀察">觀察</h3>
<p><code>layouts/_default/search.html</code> 現況：</p>
<table>
  <thead>
      <tr>
          <th>段落</th>
          <th>行數</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Hugo template 與 HTML</td>
          <td>~30</td>
      </tr>
      <tr>
          <td>Inline <code>&lt;script&gt;</code></td>
          <td>~110</td>
      </tr>
      <tr>
          <td>Inline <code>&lt;style&gt;</code></td>
          <td>~80</td>
      </tr>
      <tr>
          <td><strong>總計</strong></td>
          <td><strong>~220</strong></td>
      </tr>
  </tbody>
</table>
<p>220 行的 single-file template、CSS / JS 各超過拆檔門檻 3-4 倍。</p>
<h3 id="判讀">判讀</h3>
<p>把 CSS 拆到 <code>assets/search.css</code>、JS 拆到 <code>assets/search.js</code>、template 只剩 HTML 結構與 Hugo 引入。</p>
<h3 id="執行拆檔步驟">執行：拆檔步驟</h3>
<h4 id="step-1建立-assets-檔">Step 1：建立 assets 檔</h4>





<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">assets/
</span></span><span class="line"><span class="ln">2</span><span class="cl">├── search.css      # 原本 inline &lt;style&gt; 內容
</span></span><span class="line"><span class="ln">3</span><span class="cl">└── search.js       # 原本 inline &lt;script&gt; 內容</span></span></code></pre></div><h4 id="step-2template-引入">Step 2：template 引入</h4>





<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">{{ define &#34;main&#34; }}
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">{{- $css := resources.Get &#34;search.css&#34; | minify | fingerprint -}}
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="p">&lt;</span><span class="nt">link</span> <span class="na">href</span><span class="o">=</span><span class="s">&#34;{{ $css.RelPermalink }}&#34;</span> <span class="na">rel</span><span class="o">=</span><span class="s">&#34;stylesheet&#34;</span> <span class="na">integrity</span><span class="o">=</span><span class="s">&#34;{{ $css.Data.Integrity }}&#34;</span><span class="p">&gt;</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="p">&lt;</span><span class="nt">div</span> <span class="na">data-pagefind-ignore</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;search-shell&#34;</span><span class="p">&gt;</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="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">{{- $js := resources.Get &#34;search.js&#34; | minify | fingerprint -}}
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="p">&lt;</span><span class="nt">script</span> <span class="na">src</span><span class="o">=</span><span class="s">&#34;{{ $js.RelPermalink }}&#34;</span> <span class="na">integrity</span><span class="o">=</span><span class="s">&#34;{{ $js.Data.Integrity }}&#34;</span> <span class="na">defer</span><span class="p">&gt;&lt;/</span><span class="nt">script</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">{{ end }}</span></span></code></pre></div><h4 id="step-3js-從全域-windowpagefindui-改為-module-模式">Step 3：JS 從全域 <code>window.PagefindUI</code> 改為 module 模式</h4>
<p>如果原本 inline JS 用 <code>new PagefindUI(...)</code> 直接執行、拆檔後仍然可以這樣寫。但若想進一步，把 init 包成 function：</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">// assets/search.js
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></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">function</span> <span class="nx">init</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">new</span> <span class="nx">PagefindUI</span><span class="p">({</span> <span class="p">...</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="c1">// ... rest of setup
</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="k">if</span> <span class="p">(</span><span class="nb">document</span><span class="p">.</span><span class="nx">readyState</span> <span class="o">===</span> <span class="s1">&#39;loading&#39;</span><span class="p">)</span> <span class="p">{</span>
</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">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><span class="line"><span class="ln"> 9</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">10</span><span class="cl">    <span class="nx">init</span><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="p">})();</span></span></span></code></pre></div><h4 id="step-4清理-template">Step 4：清理 template</h4>
<p>Template 從 220 行降到 ~30 行 — 只剩 HTML 結構。</p>
<hr>
<h2 id="內在屬性比較四種引入方式">內在屬性比較：四種引入方式</h2>
<table>
  <thead>
      <tr>
          <th>方式</th>
          <th>維護成本</th>
          <th>Cache 控制</th>
          <th>可重用性</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Inline <code>&lt;style&gt;</code> / <code>&lt;script&gt;</code></td>
          <td>中 — template 混雜</td>
          <td>自動跟著 template</td>
          <td>低 — 跟特定 template 綁</td>
      </tr>
      <tr>
          <td>拆 .css / .js + 直接 link / script tag</td>
          <td>低 — 純檔案</td>
          <td>手動加版本號</td>
          <td>高</td>
      </tr>
      <tr>
          <td>Hugo resources.Get + minify</td>
          <td>低</td>
          <td>內容變動觸發新 path</td>
          <td>高</td>
      </tr>
      <tr>
          <td>Hugo resources.Get + minify + fingerprint</td>
          <td>低</td>
          <td>內容 hash 自動 cache-bust</td>
          <td>高 + 安全</td>
      </tr>
  </tbody>
</table>
<p>優先選 fingerprint — Hugo 自動處理快取、瀏覽器看到內容變動的 fingerprint 一定 reload。</p>
<hr>
<h2 id="hugo-resources-pipeline-的細節">Hugo Resources Pipeline 的細節</h2>
<h3 id="resourcesget"><code>resources.Get</code></h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="p">{{</span> <span class="err">$</span><span class="nx">css</span> <span class="o">:=</span> <span class="nx">resources</span><span class="p">.</span><span class="nx">Get</span> <span class="s">&#34;search.css&#34;</span> <span class="p">}}</span></span></span></code></pre></div><p>讀 <code>assets/search.css</code>。如果路徑下沒有、回傳 nil（要做 nil 檢查）。</p>
<h3 id="-minify"><code>| minify</code></h3>
<p>去除空白、註解、合併 selector — 減少傳輸大小。</p>
<h3 id="-fingerprint"><code>| fingerprint</code></h3>
<p>對檔案內容做 hash、加到 URL（<code>search.abc123.css</code>）。內容變動時 fingerprint 變、瀏覽器把它當新檔案。</p>
<h3 id="relpermalink--permalink"><code>.RelPermalink</code> / <code>.Permalink</code></h3>
<p><code>RelPermalink</code> — site root 相對路徑（<code>/search.abc123.css</code>）<br>
<code>Permalink</code> — 完整 URL（<code>https://site.com/search.abc123.css</code>）</p>
<p>通常用 <code>RelPermalink</code> 即可。</p>
<h3 id="dataintegrity"><code>.Data.Integrity</code></h3>
<p>Subresource Integrity hash — 給 <code>integrity</code> attribute 用、瀏覽器驗證下載內容沒被篡改。</p>
<hr>
<h2 id="拆檔的判斷門檻">拆檔的判斷門檻</h2>
<table>
  <thead>
      <tr>
          <th>Template 內含</th>
          <th>建議</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>0-10 行 inline CSS / JS</td>
          <td>不拆 — 維護成本最低</td>
      </tr>
      <tr>
          <td>10-30 行</td>
          <td>視情況 — 有重用性需求就拆</td>
      </tr>
      <tr>
          <td>30+ 行</td>
          <td>拆 — 各方面收益都正向</td>
      </tr>
      <tr>
          <td>50+ 行</td>
          <td>強烈建議拆</td>
      </tr>
      <tr>
          <td>多個 template 共用同一段</td>
          <td>立刻拆 — 重用性主導</td>
      </tr>
  </tbody>
</table>
<p>當前 search.html 的 ~190 行 inline 程式碼遠超門檻、屬於「強烈建議拆」。</p>
<hr>
<h2 id="設計取捨css--js-引入策略">設計取捨：CSS / JS 引入策略</h2>
<p>四種做法、各自機會成本不同。這個專案在 inline &gt; 30 行時選 A（拆檔 + Hugo pipeline）當預設、其他做法在特定情境合理。</p>
<h3 id="a拆檔--hugo-resourcesget--minify--fingerprint這個專案的預設">A：拆檔 + Hugo <code>resources.Get | minify | fingerprint</code>（這個專案的預設）</h3>
<ul>
<li><strong>機制</strong>：CSS / JS 拆到 <code>assets/</code>、template 用 <code>resources.Get | minify | fingerprint</code> 引入</li>
<li><strong>選 A 的理由</strong>：minify 自動、cache-bust 自動、editor syntax highlight、跨 template 重用</li>
<li><strong>適合</strong>：規模超過 30 行、預期長期維護的客製</li>
<li><strong>代價</strong>：多 1-2 個檔案、template 跟 assets 分屬兩處（grep 多一步）</li>
</ul>
<h3 id="b拆檔--直接-link--script-tag">B：拆檔 + 直接 <code>&lt;link&gt;</code> / <code>&lt;script&gt;</code> tag</h3>
<ul>
<li><strong>機制</strong>：拆檔到 <code>static/</code> 或 <code>assets/</code>、template 直接 link</li>
<li><strong>跟 A 的取捨</strong>：B 簡單、A 自動處理 minify / fingerprint；B 改檔案後 cache 可能用舊版（要手動加版本號）</li>
<li><strong>B 比 A 好的情境</strong>：簡單 prototype、確定不需要 cache-bust（純內部工具）</li>
</ul>
<h3 id="c保持-inline">C：保持 inline</h3>
<ul>
<li><strong>機制</strong>：CSS / JS 寫在 template 的 <code>&lt;style&gt;</code> / <code>&lt;script&gt;</code> 內</li>
<li><strong>跟 A 的取捨</strong>：C 一個檔案搞定、A 拆兩個；但 C 在 30+ 行時 syntax highlight 失效、難維護</li>
<li><strong>C 比 A 好的情境</strong>：&lt; 10 行的小段、跟 template 邏輯緊密相關</li>
</ul>
<h3 id="dcdn-引入第三方資源">D：CDN 引入第三方資源</h3>
<ul>
<li><strong>機制</strong>：<code>&lt;script src=&quot;https://cdn.../lib.js&quot;&gt;</code></li>
<li><strong>成本特別高的原因</strong>：依賴第三方可用性、跨域 CORS / SRI 處理、隱私問題（追蹤）</li>
<li><strong>D 才合理的情境</strong>：第三方明確支援 SRI 且 CDN 是官方建議方式（少數 vendor library）</li>
</ul>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>拆檔動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Template 內 <code>&lt;style&gt;</code> / <code>&lt;script&gt;</code> 超過 30 行</td>
          <td>拆到 <code>assets/</code> 下對應 .css / .js</td>
      </tr>
      <tr>
          <td>Editor 對 inline CSS / JS 沒 highlight</td>
          <td>拆檔讓 editor 套對應 mode</td>
      </tr>
      <tr>
          <td>改 inline JS 後 cache 沒更新</td>
          <td>拆檔 + fingerprint 自動 cache-bust</td>
      </tr>
      <tr>
          <td>同樣的 CSS / JS 在多個 template 重複</td>
          <td>拆出共用檔案</td>
      </tr>
      <tr>
          <td>Inline 程式碼跟 Hugo template 邏輯混在一起難 grep</td>
          <td>拆檔讓 grep 範圍清楚</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：Template 是 markup 的家、CSS / JS 是各自獨立檔案的家。三者混在一個檔案是過渡狀態、不是長期方案。</p>
]]></content:encoded></item><item><title>CSS 變數定義位置統一</title><link>https://tarrragon.github.io/blog/report/css-variable-single-location/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/css-variable-single-location/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>CSS 變數的定義位置只能有一處。&lt;/strong> 一次定義在離 root 最近的合適 selector（&lt;code>:root&lt;/code> 或頁面層級的 body class），其他地方只用 &lt;code>var()&lt;/code> 引用、不重複宣告。改 token 只動一處、所有引用點自動跟上。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼定義位置要單一">為什麼定義位置要單一&lt;/h2>
&lt;h3 id="商業邏輯">商業邏輯&lt;/h3>
&lt;p>CSS 變數的價值是「單一來源、多處引用」。把定義散在多個 selector：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-css" data-lang="css">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="p">:&lt;/span>&lt;span class="nd">root&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nv">--search-title-h&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">64&lt;/span>&lt;span class="kt">px&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="p">.&lt;/span>&lt;span class="nc">search-shell&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nv">--pagefind-ui-scale&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">1.0&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="nt">body&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nc">page-search&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nv">--search-form-h&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">68&lt;/span>&lt;span class="kt">px&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>每個變數的「真相」分散在不同位置 — 改一個 token 要先 grep 找到定義位置、可能漏改。&lt;/p>
&lt;p>更嚴重：同名變數在不同 selector 重複定義時、值依 cascade 順序決定 — 維護者不易看出哪個值生效。&lt;/p>
&lt;h3 id="統一定義的位置選擇">統一定義的位置選擇&lt;/h3>
&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;code>:root&lt;/code>&lt;/td>
 &lt;td>全站適用的 design token&lt;/td>
 &lt;td>全站&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>body.page-X&lt;/code>&lt;/td>
 &lt;td>特定頁面類型適用&lt;/td>
 &lt;td>該類型頁面&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>.component-name&lt;/code>&lt;/td>
 &lt;td>特定 component 內適用&lt;/td>
 &lt;td>該 component 子樹&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>選擇原則：&lt;strong>定義在「跟使用範圍最匹配的最高層級」selector&lt;/strong>。全站用 &lt;code>:root&lt;/code>、頁面類型用 body class、組件內用組件 class。&lt;/p>
&lt;hr>
&lt;h2 id="這次任務的散落問題">這次任務的散落問題&lt;/h2>
&lt;h3 id="觀察">觀察&lt;/h3>
&lt;p>&lt;code>search.html&lt;/code> 內 CSS 變數定義散在三處：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-css" data-lang="css">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="nt">body&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nc">page-search&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="nv">--search-title-h&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">64&lt;/span>&lt;span class="kt">px&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="nv">--search-form-h&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">68&lt;/span>&lt;span class="kt">px&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="nv">--search-gap&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">20&lt;/span>&lt;span class="kt">px&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="p">:&lt;/span>&lt;span class="nd">root&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="nv">--search-scope-h&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">60&lt;/span>&lt;span class="kt">px&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c">/* JS 量測會覆寫 */&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">}&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="p">.&lt;/span>&lt;span class="nc">search-shell&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="nv">--pagefind-ui-scale&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">1.0&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="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>三處定義 — 雖然各有理由（body 範圍、JS 寫入點、cascade 給 pagefind），但維護者要知道「改 search-form-h 在哪改」需要全文 grep。&lt;/p>
&lt;h3 id="判讀">判讀&lt;/h3>
&lt;p>整理後集中在 &lt;code>body.page-search&lt;/code>（搜尋頁的 root selector）：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-css" data-lang="css">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="nt">body&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nc">page-search&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="c">/* 設計 token：寫死值 */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &lt;span class="nv">--search-title-h&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">64&lt;/span>&lt;span class="kt">px&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="nv">--search-form-h&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">68&lt;/span>&lt;span class="kt">px&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="nv">--search-gap&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">20&lt;/span>&lt;span class="kt">px&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>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="c">/* JS 量測寫入 fallback：JS 會用 setProperty 覆寫到 :root */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="nv">--search-scope-h&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">60&lt;/span>&lt;span class="kt">px&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="c">/* 給 pagefind cascade 的 scale */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl"> &lt;span class="nv">--pagefind-ui-scale&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">1.0&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>一個 selector 看到所有 search 相關 token、cascade 到子樹生效。&lt;/p>
&lt;h3 id="執行">執行&lt;/h3>
&lt;p>JS 量測寫入 scope-h 時、寫到 &lt;code>body.page-search&lt;/code> 而非 &lt;code>:root&lt;/code>：&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">function&lt;/span> &lt;span class="nx">syncScopeHeight&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">h&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">scopeEl&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">offsetHeight&lt;/span> &lt;span class="o">||&lt;/span> &lt;span class="mi">56&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">body&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">style&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">setProperty&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;--search-scope-h&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">h&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="s1">&amp;#39;px&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>寫到 body.style 直接覆蓋 body.page-search 的 fallback 值。Cascade 到所有後代生效。&lt;/p>
&lt;hr>
&lt;h2 id="變數命名與分類">變數命名與分類&lt;/h2>
&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>--token-*&lt;/code> 或無前綴&lt;/td>
 &lt;td>全站設計 token（顏色、字型）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>--page-search-*&lt;/code>&lt;/td>
 &lt;td>搜尋頁專用&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>--pagefind-ui-*&lt;/code>&lt;/td>
 &lt;td>Pagefind 提供的 hook（不是我們命名、是組件預期）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>前綴讓維護者一眼看出變數的「歸屬」、不會誤改別處變數。&lt;/p>
&lt;h3 id="分類定義">分類定義&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-css" data-lang="css">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="nt">body&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nc">page-search&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="c">/* === 對齊 token === */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &lt;span class="nv">--search-title-h&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">64&lt;/span>&lt;span class="kt">px&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="nv">--search-form-h&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">68&lt;/span>&lt;span class="kt">px&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="nv">--search-gap&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">20&lt;/span>&lt;span class="kt">px&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="nv">--search-scope-h&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">60&lt;/span>&lt;span class="kt">px&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c">/* JS 寫入 */&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">/* === 響應式 breakpoint === */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="c">/* (CSS 變數無法用在 @media query、breakpoint 寫死在 query 內) */&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="c">/* === 對組件的 hook === */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl"> &lt;span class="nv">--pagefind-ui-scale&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">1.0&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="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>分類註解讓維護者知道「我要改哪類 token」、找對位置。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>CSS 變數的定義位置只能有一處。</strong> 一次定義在離 root 最近的合適 selector（<code>:root</code> 或頁面層級的 body class），其他地方只用 <code>var()</code> 引用、不重複宣告。改 token 只動一處、所有引用點自動跟上。</p>
<hr>
<h2 id="為什麼定義位置要單一">為什麼定義位置要單一</h2>
<h3 id="商業邏輯">商業邏輯</h3>
<p>CSS 變數的價值是「單一來源、多處引用」。把定義散在多個 selector：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-css" data-lang="css"><span class="line"><span class="ln">1</span><span class="cl"><span class="p">:</span><span class="nd">root</span>           <span class="p">{</span> <span class="nv">--search-title-h</span><span class="p">:</span> <span class="mi">64</span><span class="kt">px</span><span class="p">;</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="p">.</span><span class="nc">search-shell</span>   <span class="p">{</span> <span class="nv">--pagefind-ui-scale</span><span class="p">:</span> <span class="mf">1.0</span><span class="p">;</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nt">body</span><span class="p">.</span><span class="nc">page-search</span> <span class="p">{</span> <span class="nv">--search-form-h</span><span class="p">:</span> <span class="mi">68</span><span class="kt">px</span><span class="p">;</span> <span class="p">}</span></span></span></code></pre></div><p>每個變數的「真相」分散在不同位置 — 改一個 token 要先 grep 找到定義位置、可能漏改。</p>
<p>更嚴重：同名變數在不同 selector 重複定義時、值依 cascade 順序決定 — 維護者不易看出哪個值生效。</p>
<h3 id="統一定義的位置選擇">統一定義的位置選擇</h3>
<table>
  <thead>
      <tr>
          <th>位置</th>
          <th>適用情境</th>
          <th>影響範圍</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>:root</code></td>
          <td>全站適用的 design token</td>
          <td>全站</td>
      </tr>
      <tr>
          <td><code>body.page-X</code></td>
          <td>特定頁面類型適用</td>
          <td>該類型頁面</td>
      </tr>
      <tr>
          <td><code>.component-name</code></td>
          <td>特定 component 內適用</td>
          <td>該 component 子樹</td>
      </tr>
  </tbody>
</table>
<p>選擇原則：<strong>定義在「跟使用範圍最匹配的最高層級」selector</strong>。全站用 <code>:root</code>、頁面類型用 body class、組件內用組件 class。</p>
<hr>
<h2 id="這次任務的散落問題">這次任務的散落問題</h2>
<h3 id="觀察">觀察</h3>
<p><code>search.html</code> 內 CSS 變數定義散在三處：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-css" data-lang="css"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nt">body</span><span class="p">.</span><span class="nc">page-search</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="nv">--search-title-h</span><span class="p">:</span> <span class="mi">64</span><span class="kt">px</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="nv">--search-form-h</span><span class="p">:</span> <span class="mi">68</span><span class="kt">px</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="nv">--search-gap</span><span class="p">:</span> <span class="mi">20</span><span class="kt">px</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="p">:</span><span class="nd">root</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  <span class="nv">--search-scope-h</span><span class="p">:</span> <span class="mi">60</span><span class="kt">px</span><span class="p">;</span>   <span class="c">/* JS 量測會覆寫 */</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></span><span class="line"><span class="ln">11</span><span class="cl"><span class="p">.</span><span class="nc">search-shell</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">  <span class="nv">--pagefind-ui-scale</span><span class="p">:</span> <span class="mf">1.0</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>三處定義 — 雖然各有理由（body 範圍、JS 寫入點、cascade 給 pagefind），但維護者要知道「改 search-form-h 在哪改」需要全文 grep。</p>
<h3 id="判讀">判讀</h3>
<p>整理後集中在 <code>body.page-search</code>（搜尋頁的 root selector）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-css" data-lang="css"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nt">body</span><span class="p">.</span><span class="nc">page-search</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="c">/* 設計 token：寫死值 */</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="nv">--search-title-h</span><span class="p">:</span> <span class="mi">64</span><span class="kt">px</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="nv">--search-form-h</span><span class="p">:</span> <span class="mi">68</span><span class="kt">px</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  <span class="nv">--search-gap</span><span class="p">:</span> <span class="mi">20</span><span class="kt">px</span><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="c">/* JS 量測寫入 fallback：JS 會用 setProperty 覆寫到 :root */</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  <span class="nv">--search-scope-h</span><span class="p">:</span> <span class="mi">60</span><span class="kt">px</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="c">/* 給 pagefind cascade 的 scale */</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">  <span class="nv">--pagefind-ui-scale</span><span class="p">:</span> <span class="mf">1.0</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>一個 selector 看到所有 search 相關 token、cascade 到子樹生效。</p>
<h3 id="執行">執行</h3>
<p>JS 量測寫入 scope-h 時、寫到 <code>body.page-search</code> 而非 <code>:root</code>：</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">syncScopeHeight</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">h</span> <span class="o">=</span> <span class="nx">scopeEl</span><span class="p">.</span><span class="nx">offsetHeight</span> <span class="o">||</span> <span class="mi">56</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">body</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">setProperty</span><span class="p">(</span><span class="s1">&#39;--search-scope-h&#39;</span><span class="p">,</span> <span class="nx">h</span> <span class="o">+</span> <span class="s1">&#39;px&#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>寫到 body.style 直接覆蓋 body.page-search 的 fallback 值。Cascade 到所有後代生效。</p>
<hr>
<h2 id="變數命名與分類">變數命名與分類</h2>
<h3 id="命名前綴標明範圍">命名前綴標明範圍</h3>
<table>
  <thead>
      <tr>
          <th>前綴</th>
          <th>範圍</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>--token-*</code> 或無前綴</td>
          <td>全站設計 token（顏色、字型）</td>
      </tr>
      <tr>
          <td><code>--page-search-*</code></td>
          <td>搜尋頁專用</td>
      </tr>
      <tr>
          <td><code>--pagefind-ui-*</code></td>
          <td>Pagefind 提供的 hook（不是我們命名、是組件預期）</td>
      </tr>
  </tbody>
</table>
<p>前綴讓維護者一眼看出變數的「歸屬」、不會誤改別處變數。</p>
<h3 id="分類定義">分類定義</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-css" data-lang="css"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nt">body</span><span class="p">.</span><span class="nc">page-search</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="c">/* === 對齊 token === */</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="nv">--search-title-h</span><span class="p">:</span> <span class="mi">64</span><span class="kt">px</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="nv">--search-form-h</span><span class="p">:</span> <span class="mi">68</span><span class="kt">px</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  <span class="nv">--search-gap</span><span class="p">:</span> <span class="mi">20</span><span class="kt">px</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">  <span class="nv">--search-scope-h</span><span class="p">:</span> <span class="mi">60</span><span class="kt">px</span><span class="p">;</span>     <span class="c">/* JS 寫入 */</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">/* === 響應式 breakpoint === */</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  <span class="c">/* (CSS 變數無法用在 @media query、breakpoint 寫死在 query 內) */</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="c">/* === 對組件的 hook === */</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">  <span class="nv">--pagefind-ui-scale</span><span class="p">:</span> <span class="mf">1.0</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>分類註解讓維護者知道「我要改哪類 token」、找對位置。</p>
<hr>
<h2 id="內在屬性比較四種變數定義方式">內在屬性比較：四種變數定義方式</h2>
<table>
  <thead>
      <tr>
          <th>方式</th>
          <th>維護成本</th>
          <th>可見性</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>散在多個 selector 定義</td>
          <td>高 — grep 找定義</td>
          <td>低 — 不知哪個生效</td>
      </tr>
      <tr>
          <td>集中在一個 selector</td>
          <td>低 — 改一處</td>
          <td>高 — 全部變數一覽</td>
      </tr>
      <tr>
          <td>集中 + 分類註解</td>
          <td>低</td>
          <td>最高 — 結構化</td>
      </tr>
      <tr>
          <td>集中 + JS 寫入用同一 selector</td>
          <td>低</td>
          <td>最高 + JS 動態同步</td>
      </tr>
  </tbody>
</table>
<p>優先選「集中 + 分類 + JS 寫入同 selector」。</p>
<hr>
<h2 id="變數的-fallback-策略">變數的 fallback 策略</h2>
<blockquote>
<p><strong>責任邊界</strong>：本節只談「fallback 值寫在哪個 selector」、屬於定義位置議題。「該不該用 runtime 量測」這個更上層的策略選擇由 <a href="../runtime-measurement-unification/">#27 runtime 量測模式統一</a> 處理 — 那邊主張「全寫死 vs 全量測、不要混搭」。</p></blockquote>
<p>JS 量測寫入的變數、CSS 應該有 fallback 值供 JS 還沒跑完時用：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-css" data-lang="css"><span class="line"><span class="ln">1</span><span class="cl"><span class="nt">body</span><span class="p">.</span><span class="nc">page-search</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="nv">--search-scope-h</span><span class="p">:</span> <span class="mi">60</span><span class="kt">px</span><span class="p">;</span>   <span class="c">/* fallback、JS 會覆寫 */</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></span><span class="line"><span class="ln">5</span><span class="cl"><span class="p">.</span><span class="nc">search-shell</span> <span class="p">.</span><span class="nc">pagefind-ui__drawer</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">  <span class="k">margin-top</span><span class="p">:</span> <span class="nb">calc</span><span class="p">(</span><span class="nf">var</span><span class="p">(</span><span class="o">--</span><span class="n">search</span><span class="o">-</span><span class="n">scope</span><span class="o">-</span><span class="n">h</span><span class="p">)</span> <span class="o">+</span> <span class="mi">8</span><span class="kt">px</span><span class="p">);</span>  <span class="c">/* JS 跑完前用 60px */</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>或用 <code>var()</code> 第二參數：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-css" data-lang="css"><span class="line"><span class="ln">1</span><span class="cl"><span class="nt">margin-top</span><span class="o">:</span> <span class="nt">calc</span><span class="o">(</span><span class="nt">var</span><span class="o">(</span><span class="nt">--search-scope-h</span><span class="o">,</span> <span class="nt">60px</span><span class="o">)</span> <span class="o">+</span> <span class="nt">8px</span><span class="o">);</span></span></span></code></pre></div><p>兩種寫法效果相近 — 第一種讓 token 集中在 body.page-search 內、推薦使用。</p>
<hr>
<h2 id="設計取捨css-變數定義位置策略">設計取捨：CSS 變數定義位置策略</h2>
<p>四種做法、各自機會成本不同。這個專案選 A（集中在使用範圍的最高層）當預設、其他做法在特定情境合理。</p>
<blockquote>
<p>本篇是 <a href="../single-source-of-truth/">#44 SSoT</a> 抽象原則在「CSS 變數定義位置」這個面向的應用。</p></blockquote>
<h3 id="a集中在跟使用範圍最匹配的最高層selector這個專案的預設">A：集中在「跟使用範圍最匹配的最高層」selector（這個專案的預設）</h3>
<ul>
<li><strong>機制</strong>：全站 token 在 <code>:root</code>、頁面 token 在 <code>body.page-X</code>、組件 token 在 <code>.component</code>、JS 寫入也用同 selector</li>
<li><strong>選 A 的理由</strong>：定義住址唯一、改 token 自動跟上、cascade 範圍跟使用範圍一致</li>
<li><strong>適合</strong>：絕大多數 design token 系統</li>
<li><strong>代價</strong>：要先想清楚每個變數的「使用範圍」、不能無腦丟一處</li>
</ul>
<h3 id="b所有變數都丟-root">B：所有變數都丟 <code>:root</code></h3>
<ul>
<li><strong>機制</strong>：不分使用範圍、全部 <code>:root</code></li>
<li><strong>跟 A 的取捨</strong>：B 簡單一致、A 按範圍分；但 B 不在乎 scope、可能跟其他組件變數命名衝突、且 cascade 範圍過大</li>
<li><strong>B 比 A 好的情境</strong>：純 design system token（顏色 / 字型）、確實全站適用</li>
</ul>
<h3 id="c散在多個-selector-各自定義">C：散在多個 selector 各自定義</h3>
<ul>
<li><strong>機制</strong>：每個 component 各自定義需要的變數</li>
<li><strong>跟 A 的取捨</strong>：C 元件自包含、A 集中管理；但 C 同名 token 散落多處、cascade 順序決定值、改一處可能漏其他</li>
<li><strong>C 才合理的情境</strong>：完全獨立的元件、不共用任何 token（罕見）</li>
</ul>
<h3 id="d每處引用點都重複定義">D：每處引用點都重複定義</h3>
<ul>
<li><strong>機制</strong>：用 var 引用前都重新宣告一次</li>
<li><strong>D 是反模式</strong>：徹底違反 SSoT、改 token 要 grep 找全、必漏改 — 重複定義是 magic number 散落的另一種形式</li>
<li><strong>看起來吸引人的原因</strong>：每處就地寫值最快、不用想 token 該定義在哪</li>
<li><strong>實際發生的代價</strong>：未來改值時掃不到全部、UI 出現「有的地方變、有的沒變」的怪 bug</li>
</ul>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>Refactor 動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>同名變數在多個 selector 定義</td>
          <td>集中到一個 selector、移除其他</td>
      </tr>
      <tr>
          <td>改一個 token 要 grep 找定義位置</td>
          <td>集中 + 分類註解</td>
      </tr>
      <tr>
          <td>Token 命名沒前綴、跟其他組件變數混</td>
          <td>加範圍前綴（<code>--page-X-*</code>）</td>
      </tr>
      <tr>
          <td>JS 寫入變數的位置跟 CSS 定義不同</td>
          <td>對齊到同一 selector</td>
      </tr>
      <tr>
          <td>變數值在 cascade 中被另一處覆蓋</td>
          <td>找出兩處、決定哪一處保留</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：CSS 變數是設計 token 系統的基礎、定義位置就是 token 的「住址」。住址一個就好、不要一物多址。</p>
<p>「就地寫個值」是便利（不用找 token 位置）、「集中定義 + 引用」是對齊 — 同 <a href="../single-source-of-truth/">#44 SSOT</a> 跟 <a href="../ease-of-writing-vs-intent-alignment/">#67 便利 vs 對齊反相關</a>。</p>
]]></content:encoded></item><item><title>runtime 量測模式統一</title><link>https://tarrragon.github.io/blog/report/runtime-measurement-unification/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/runtime-measurement-unification/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>對齊基準上的尺寸值、要嘛統一寫死、要嘛統一 runtime 量測 — 不要混搭。&lt;/strong> 混搭時某些變化（字型替換、scale 改變、theme 切換）會打破對齊、且問題只在特定情境出現、難以重現。選一邊走到底。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼混搭會不穩">為什麼混搭會不穩&lt;/h2>
&lt;h3 id="商業邏輯">商業邏輯&lt;/h3>
&lt;p>對齊問題本質是「方程組」 — 每個變數的值都要正確、結果才對。&lt;/p>
&lt;p>寫死值的特徵：&lt;/p>
&lt;ul>
&lt;li>來源是 build time 設計決定&lt;/li>
&lt;li>變動需要手動改 CSS&lt;/li>
&lt;li>假設某個渲染條件成立（特定字型、特定 scale）&lt;/li>
&lt;/ul>
&lt;p>量測值的特徵：&lt;/p>
&lt;ul>
&lt;li>來源是 runtime DOM 量測&lt;/li>
&lt;li>自動跟著實際渲染走&lt;/li>
&lt;li>不依賴特定渲染條件&lt;/li>
&lt;/ul>
&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;/td>
 &lt;td>設計 token 穩定、組件提供 scale hook 可鎖定&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>全部量測（runtime 同步）&lt;/td>
 &lt;td>內容動態、字型 / 排版可能變動&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>選擇看「願意接受多少不確定性」 — 全寫死要鎖很多條件、全量測要寫多個 ResizeObserver。&lt;/p>
&lt;hr>
&lt;h2 id="這次任務的混搭問題">這次任務的混搭問題&lt;/h2>
&lt;h3 id="觀察">觀察&lt;/h3>
&lt;p>對齊基準上四個值的處理：&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;code>--search-title-h&lt;/code> (H1)&lt;/td>
 &lt;td>寫死 64px&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>--search-form-h&lt;/code> (input)&lt;/td>
 &lt;td>寫死 68px、靠 &lt;code>--pagefind-ui-scale: 1.0&lt;/code> 鎖定&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>--search-gap&lt;/code> (drawer 上方)&lt;/td>
 &lt;td>寫死 20px&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>--search-scope-h&lt;/code>&lt;/td>
 &lt;td>ResizeObserver 量測寫回&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>混搭：前三個寫死、第四個量測。&lt;/p>
&lt;h3 id="判讀">判讀&lt;/h3>
&lt;p>當前情境穩定 — pagefind scale 鎖在 1.0、theme h1 高度可預測。但若：&lt;/p>
&lt;ul>
&lt;li>Theme 升級改 h1 line-height → 寫死 64px 不準&lt;/li>
&lt;li>使用者裝置字型不同 → form 內容寬度變動可能間接影響高度&lt;/li>
&lt;li>pagefind 升級 input 高度算法 → 寫死 68px 不準&lt;/li>
&lt;/ul>
&lt;p>寫死值「假設某些條件成立」、條件變了寫死值就錯。&lt;/p>
&lt;h3 id="執行兩種統一方向">執行：兩種統一方向&lt;/h3>
&lt;h4 id="方向-1全部寫死鎖更多渲染條件">方向 1：全部寫死、鎖更多渲染條件&lt;/h4>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-css" data-lang="css">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="nt">body&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nc">page-search&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="nv">--search-title-h&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">64&lt;/span>&lt;span class="kt">px&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="nv">--search-form-h&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">68&lt;/span>&lt;span class="kt">px&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="nv">--search-gap&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">20&lt;/span>&lt;span class="kt">px&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="nv">--search-scope-h&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">56&lt;/span>&lt;span class="kt">px&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c">/* 不再 JS 量測 */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> &lt;span class="nv">--pagefind-ui-scale&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">1.0&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>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="p">.&lt;/span>&lt;span class="nc">search-title&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="k">height&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nf">var&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">--&lt;/span>&lt;span class="n">search&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">title&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">h&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">line-height&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nf">var&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">--&lt;/span>&lt;span class="n">search&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">title&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">h&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="k">margin&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c">/* 鎖 H1 margin */&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">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">
&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 class="nc">search-scope&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="k">height&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nf">var&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">--&lt;/span>&lt;span class="n">search&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">scope&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">h&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="c">/* 鎖 scope 高度、超過裁掉 */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl"> &lt;span class="k">overflow&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">hidden&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="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>代價：scope 內容超過時被裁、UI 可能不適合動態內容。&lt;/p>
&lt;h4 id="方向-2全部量測resizeobserver-同步所有">方向 2：全部量測、ResizeObserver 同步所有&lt;/h4>





&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">measureAll&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">setVar&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;--search-title-h&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">titleEl&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">offsetHeight&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">setVar&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;--search-form-h&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">formEl&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">offsetHeight&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">setVar&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;--search-scope-h&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">scopeEl&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">offsetHeight&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">// gap 是 pagefind drawer 內建、無法從外部量測
&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="kd">function&lt;/span> &lt;span class="nx">setVar&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">name&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">val&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="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">style&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">setProperty&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">name&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">val&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="s1">&amp;#39;px&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="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="p">[&lt;/span>&lt;span class="nx">titleEl&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">formEl&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">scopeEl&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">el&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">12&lt;/span>&lt;span class="cl"> &lt;span class="k">new&lt;/span> &lt;span class="nx">ResizeObserver&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">measureAll&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">observe&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">13&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 多了一層、初始載入時 fallback 值不對齊（直到 JS 跑完）。&lt;/p>
&lt;h3 id="推薦">推薦&lt;/h3>
&lt;p>&lt;strong>這個專案選方向 1（全寫死）&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>Pagefind scale 已能鎖定&lt;/li>
&lt;li>Theme 由本人控制、h1 變動可預期&lt;/li>
&lt;li>Scope UI 設計成單行、不需要動態高度&lt;/li>
&lt;/ul>
&lt;p>把當前 scope-h 從量測改寫死、移除 ResizeObserver。混搭問題消失。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>對齊基準上的尺寸值、要嘛統一寫死、要嘛統一 runtime 量測 — 不要混搭。</strong> 混搭時某些變化（字型替換、scale 改變、theme 切換）會打破對齊、且問題只在特定情境出現、難以重現。選一邊走到底。</p>
<hr>
<h2 id="為什麼混搭會不穩">為什麼混搭會不穩</h2>
<h3 id="商業邏輯">商業邏輯</h3>
<p>對齊問題本質是「方程組」 — 每個變數的值都要正確、結果才對。</p>
<p>寫死值的特徵：</p>
<ul>
<li>來源是 build time 設計決定</li>
<li>變動需要手動改 CSS</li>
<li>假設某個渲染條件成立（特定字型、特定 scale）</li>
</ul>
<p>量測值的特徵：</p>
<ul>
<li>來源是 runtime DOM 量測</li>
<li>自動跟著實際渲染走</li>
<li>不依賴特定渲染條件</li>
</ul>
<p>混搭時的失敗模式：寫死值依賴的渲染條件變了、但量測值跟著變、寫死值沒跟 — 兩者錯位、對齊壞掉。</p>
<h3 id="統一往一邊靠的選擇">統一往一邊靠的選擇</h3>
<table>
  <thead>
      <tr>
          <th>統一策略</th>
          <th>適合</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>全部寫死（鎖渲染條件）</td>
          <td>設計 token 穩定、組件提供 scale hook 可鎖定</td>
      </tr>
      <tr>
          <td>全部量測（runtime 同步）</td>
          <td>內容動態、字型 / 排版可能變動</td>
      </tr>
  </tbody>
</table>
<p>選擇看「願意接受多少不確定性」 — 全寫死要鎖很多條件、全量測要寫多個 ResizeObserver。</p>
<hr>
<h2 id="這次任務的混搭問題">這次任務的混搭問題</h2>
<h3 id="觀察">觀察</h3>
<p>對齊基準上四個值的處理：</p>
<table>
  <thead>
      <tr>
          <th>值</th>
          <th>來源</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>--search-title-h</code> (H1)</td>
          <td>寫死 64px</td>
      </tr>
      <tr>
          <td><code>--search-form-h</code> (input)</td>
          <td>寫死 68px、靠 <code>--pagefind-ui-scale: 1.0</code> 鎖定</td>
      </tr>
      <tr>
          <td><code>--search-gap</code> (drawer 上方)</td>
          <td>寫死 20px</td>
      </tr>
      <tr>
          <td><code>--search-scope-h</code></td>
          <td>ResizeObserver 量測寫回</td>
      </tr>
  </tbody>
</table>
<p>混搭：前三個寫死、第四個量測。</p>
<h3 id="判讀">判讀</h3>
<p>當前情境穩定 — pagefind scale 鎖在 1.0、theme h1 高度可預測。但若：</p>
<ul>
<li>Theme 升級改 h1 line-height → 寫死 64px 不準</li>
<li>使用者裝置字型不同 → form 內容寬度變動可能間接影響高度</li>
<li>pagefind 升級 input 高度算法 → 寫死 68px 不準</li>
</ul>
<p>寫死值「假設某些條件成立」、條件變了寫死值就錯。</p>
<h3 id="執行兩種統一方向">執行：兩種統一方向</h3>
<h4 id="方向-1全部寫死鎖更多渲染條件">方向 1：全部寫死、鎖更多渲染條件</h4>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-css" data-lang="css"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nt">body</span><span class="p">.</span><span class="nc">page-search</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="nv">--search-title-h</span><span class="p">:</span> <span class="mi">64</span><span class="kt">px</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="nv">--search-form-h</span><span class="p">:</span> <span class="mi">68</span><span class="kt">px</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="nv">--search-gap</span><span class="p">:</span> <span class="mi">20</span><span class="kt">px</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  <span class="nv">--search-scope-h</span><span class="p">:</span> <span class="mi">56</span><span class="kt">px</span><span class="p">;</span>            <span class="c">/* 不再 JS 量測 */</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">  <span class="nv">--pagefind-ui-scale</span><span class="p">:</span> <span class="mf">1.0</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></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="p">.</span><span class="nc">search-title</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">  <span class="k">height</span><span class="p">:</span> <span class="nf">var</span><span class="p">(</span><span class="o">--</span><span class="n">search</span><span class="o">-</span><span class="n">title</span><span class="o">-</span><span class="n">h</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">  <span class="k">line-height</span><span class="p">:</span> <span class="nf">var</span><span class="p">(</span><span class="o">--</span><span class="n">search</span><span class="o">-</span><span class="n">title</span><span class="o">-</span><span class="n">h</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">  <span class="k">margin</span><span class="p">:</span> <span class="mi">0</span><span class="p">;</span>                         <span class="c">/* 鎖 H1 margin */</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></span><span class="line"><span class="ln">15</span><span class="cl"><span class="p">.</span><span class="nc">search-scope</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">  <span class="k">height</span><span class="p">:</span> <span class="nf">var</span><span class="p">(</span><span class="o">--</span><span class="n">search</span><span class="o">-</span><span class="n">scope</span><span class="o">-</span><span class="n">h</span><span class="p">);</span>     <span class="c">/* 鎖 scope 高度、超過裁掉 */</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">  <span class="k">overflow</span><span class="p">:</span> <span class="kc">hidden</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>代價：scope 內容超過時被裁、UI 可能不適合動態內容。</p>
<h4 id="方向-2全部量測resizeobserver-同步所有">方向 2：全部量測、ResizeObserver 同步所有</h4>





<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">measureAll</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">setVar</span><span class="p">(</span><span class="s1">&#39;--search-title-h&#39;</span><span class="p">,</span> <span class="nx">titleEl</span><span class="p">.</span><span class="nx">offsetHeight</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="nx">setVar</span><span class="p">(</span><span class="s1">&#39;--search-form-h&#39;</span><span class="p">,</span> <span class="nx">formEl</span><span class="p">.</span><span class="nx">offsetHeight</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="nx">setVar</span><span class="p">(</span><span class="s1">&#39;--search-scope-h&#39;</span><span class="p">,</span> <span class="nx">scopeEl</span><span class="p">.</span><span class="nx">offsetHeight</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  <span class="c1">// gap 是 pagefind drawer 內建、無法從外部量測
</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="kd">function</span> <span class="nx">setVar</span><span class="p">(</span><span class="nx">name</span><span class="p">,</span> <span class="nx">val</span><span class="p">)</span> <span class="p">{</span>
</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">body</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">setProperty</span><span class="p">(</span><span class="nx">name</span><span class="p">,</span> <span class="nx">val</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 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="p">[</span><span class="nx">titleEl</span><span class="p">,</span> <span class="nx">formEl</span><span class="p">,</span> <span class="nx">scopeEl</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">12</span><span class="cl">  <span class="k">new</span> <span class="nx">ResizeObserver</span><span class="p">(</span><span class="nx">measureAll</span><span class="p">).</span><span class="nx">observe</span><span class="p">(</span><span class="nx">el</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="p">});</span></span></span></code></pre></div><p>代價：JS 多了一層、初始載入時 fallback 值不對齊（直到 JS 跑完）。</p>
<h3 id="推薦">推薦</h3>
<p><strong>這個專案選方向 1（全寫死）</strong>：</p>
<ul>
<li>Pagefind scale 已能鎖定</li>
<li>Theme 由本人控制、h1 變動可預期</li>
<li>Scope UI 設計成單行、不需要動態高度</li>
</ul>
<p>把當前 scope-h 從量測改寫死、移除 ResizeObserver。混搭問題消失。</p>
<hr>
<h2 id="內在屬性比較四種對齊值來源策略">內在屬性比較：四種對齊值來源策略</h2>
<table>
  <thead>
      <tr>
          <th>策略</th>
          <th>穩定性</th>
          <th>維護成本</th>
          <th>對動態內容適應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>全寫死 + 鎖渲染條件</td>
          <td>高 — 條件鎖死後值穩定</td>
          <td>低 — 純 CSS</td>
          <td>低 — 動態內容超過值會裁</td>
      </tr>
      <tr>
          <td>全量測 ResizeObserver</td>
          <td>高 — 值跟著實際走</td>
          <td>中 — JS 多一層</td>
          <td>高</td>
      </tr>
      <tr>
          <td>混搭（部分寫死、部分量測）</td>
          <td>中 — 邊界 case 壞</td>
          <td>中</td>
          <td>中</td>
      </tr>
      <tr>
          <td>Magic number 估算</td>
          <td>低</td>
          <td>不適用</td>
          <td>低</td>
      </tr>
  </tbody>
</table>
<p>選擇順序：<strong>內容靜態 → 全寫死；內容動態 → 全量測；不要混搭</strong>。</p>
<hr>
<h2 id="鎖定渲染條件的具體技巧">鎖定渲染條件的具體技巧</h2>
<h3 id="1-使用組件提供的-scale-hook">1. 使用組件提供的 scale hook</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-css" data-lang="css"><span class="line"><span class="ln">1</span><span class="cl"><span class="p">.</span><span class="nc">search-shell</span> <span class="p">{</span> <span class="nv">--pagefind-ui-scale</span><span class="p">:</span> <span class="mf">1.0</span><span class="p">;</span> <span class="p">}</span></span></span></code></pre></div><p>讓組件按我們指定的 scale 渲染、寫死值才有意義。</p>
<h3 id="2-寫死-h1-height--line-height--margin">2. 寫死 H1 height + line-height + margin</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-css" data-lang="css"><span class="line"><span class="ln">1</span><span class="cl"><span class="p">.</span><span class="nc">search-title</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="k">height</span><span class="p">:</span> <span class="mi">64</span><span class="kt">px</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="k">line-height</span><span class="p">:</span> <span class="mi">64</span><span class="kt">px</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="k">margin</span><span class="p">:</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">  <span class="c">/* 確保 box height 永遠是 64、不受 font / padding 影響 */</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>不留任何「看 box-sizing 與 inheritance 決定」的空間。</p>
<h3 id="3-用-box-sizing-border-box-確保-padding-不影響-box-height">3. 用 box-sizing: border-box 確保 padding 不影響 box height</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-css" data-lang="css"><span class="line"><span class="ln">1</span><span class="cl"><span class="p">.</span><span class="nc">search-scope</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="k">box-sizing</span><span class="p">:</span> <span class="kc">border-box</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="k">height</span><span class="p">:</span> <span class="nf">var</span><span class="p">(</span><span class="o">--</span><span class="n">search</span><span class="o">-</span><span class="n">scope</span><span class="o">-</span><span class="n">h</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="k">padding</span><span class="p">:</span> <span class="mi">8</span><span class="kt">px</span> <span class="mi">16</span><span class="kt">px</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">  <span class="c">/* total height 還是 var(--search-scope-h)、padding 算在內 */</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><hr>
<h2 id="設計取捨對齊基準上來源機制的選擇">設計取捨：對齊基準上來源機制的選擇</h2>
<p>四種做法、各自機會成本不同。預設依內容性質選 — 內容靜態 → A、內容動態 → B、混搭 / 估算永遠不是答案。</p>
<blockquote>
<p>本篇是 <a href="../single-source-of-truth/">#44 SSoT</a> 抽象原則在「來源機制統一」這個面向的應用。</p></blockquote>
<h3 id="a全寫死--鎖渲染條件內容靜態的預設">A：全寫死 + 鎖渲染條件（內容靜態的預設）</h3>
<ul>
<li><strong>機制</strong>：所有對齊基準值用 CSS 變數寫死、同時鎖定相關渲染條件（pagefind scale、H1 line-height、box-sizing）</li>
<li><strong>選 A 的理由</strong>：純 CSS 不依 JS、值 build time 確定、改 token 自動跟上</li>
<li><strong>適合</strong>：對齊內容靜態、可預測（設計穩定的搜尋頁、文章頁）</li>
<li><strong>代價</strong>：需要鎖很多渲染條件（scale / line-height / box-sizing 等）、scope 內容超過寫死值會被裁</li>
</ul>
<h3 id="b全量測-resizeobserver-寫回變數內容動態的預設">B：全量測 ResizeObserver 寫回變數（內容動態的預設）</h3>
<ul>
<li><strong>機制</strong>：所有對齊基準值用 ResizeObserver 量、寫回 CSS 變數、其他元素引用</li>
<li><strong>跟 A 的取捨</strong>：B 自動跟著實際渲染、A 假設條件穩定；B 多 JS 一層、初始 fallback 值不對齊（直到 JS 跑完）</li>
<li><strong>B 比 A 好的情境</strong>：內容動態（字型可能變、theme 切換、跨環境部署）</li>
</ul>
<h3 id="c混搭部分寫死部分量測">C：混搭（部分寫死、部分量測）</h3>
<ul>
<li><strong>機制</strong>：「主要值寫死、邊界值量測」混合策略</li>
<li><strong>C 是反模式</strong>：邊界情境（字型變、scale 變、theme 切換）下兩者錯位、對齊在某些 case 壞、難以重現</li>
<li><strong>看起來吸引人的原因</strong>：「主要情境寫死、邊界情境量測」聽起來合理、實際「主要 vs 邊界」判斷不可靠</li>
<li><strong>實際發生的代價</strong>：邊界常常變主要、混搭策略下 debug 範圍從「某情境」擴張到「整套是不是錯」</li>
</ul>
<h3 id="dmagic-number-估算">D：Magic number 估算</h3>
<ul>
<li><strong>機制</strong>：執行者依感覺給數字、不寫變數、不量測</li>
<li><strong>D 是反模式</strong>：任何「沒來源」的值都會在邊界情境爆掉 — 跨情境（字型 / scale / theme）必壞</li>
<li><strong>看起來吸引人的原因</strong>：執行者依感覺給數字最快、不用 query 也不用 query DevTools</li>
<li><strong>實際發生的代價</strong>：估錯時錯誤被視覺接受、ship 後在邊界情境暴露、信任損失</li>
</ul>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>Refactor 動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>對齊在某些字型 / 主題 / 縮放下壞掉</td>
          <td>找出依賴的渲染條件、鎖定或改量測</td>
      </tr>
      <tr>
          <td>改了某個 token 要去多處驗證對齊</td>
          <td>統一來源（全寫死 or 全量測）</td>
      </tr>
      <tr>
          <td>ResizeObserver 量了 A、B 卻寫死</td>
          <td>評估 B 是否也需要量、避免混搭</td>
      </tr>
      <tr>
          <td>寫死值跟實際渲染差距 &gt; 2px</td>
          <td>該值依賴的條件沒鎖、改量測或鎖條件</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：對齊問題的根因常常是「混搭」 — 用統一策略消除這個根因、debug 範圍從「某個情境」縮到「整套策略對嗎」。</p>
<p>混搭通常是便利驅動的結果（每處用最快的方式）、統一策略需要先對齊原則 — 同 <a href="../single-source-of-truth/">#44 SSOT</a> 跟 <a href="../ease-of-writing-vs-intent-alignment/">#67 便利 vs 對齊反相關</a>。</p>
]]></content:encoded></item><item><title>以 class toggle 取代 inline `display: none !important`</title><link>https://tarrragon.github.io/blog/report/class-toggle-over-important/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/class-toggle-over-important/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>JS 改 DOM 元素的視覺狀態、用 class toggle、不用 inline style.setProperty important hack。&lt;/strong> Class toggle 的好處：CSS 規則集中可讀、devtools 看到語意化的 class 名而非神秘 inline style、未來改視覺只動 CSS 不動 JS。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼-class-toggle-比-inline-style-好">為什麼 class toggle 比 inline style 好&lt;/h2>
&lt;h3 id="商業邏輯">商業邏輯&lt;/h3>
&lt;p>兩種方式都能達成「JS 控制視覺」、差別在「視覺規則的家在哪」：&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>&lt;code>el.style.display = 'none'&lt;/code>&lt;/td>
 &lt;td>散在 JS 各處&lt;/td>
 &lt;td>高 — 改視覺要找 JS、不在 CSS&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>el.classList.toggle('is-hidden')&lt;/code> + &lt;code>.is-hidden { display: none }&lt;/code>&lt;/td>
 &lt;td>集中在 CSS&lt;/td>
 &lt;td>低 — 改視覺改 CSS&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>集中在 CSS = 設計系統的單一來源、devtools Element 面板看 class 知道狀態、code review 容易理解。&lt;/p>
&lt;p>&lt;code>!important&lt;/code> 的需求消失：只要 CSS Layers 把 vendor CSS 包進低權層、自家 unlayered CSS 自然贏、不需要 important。&lt;/p>
&lt;h3 id="何時-inline-style-是必要的">何時 inline style 是必要的&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>情境&lt;/th>
 &lt;th>inline style 必要&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>動態值（隨 runtime 計算）&lt;/td>
 &lt;td>是 — 如 &lt;code>el.style.top = scrollY + 'px'&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>動畫起點 / 終點&lt;/td>
 &lt;td>是 — &lt;code>el.style.transform = ...&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>切換 boolean 狀態（顯示/隱藏）&lt;/td>
 &lt;td>否 — 用 class toggle&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>套用設計系統一致樣式&lt;/td>
 &lt;td>否 — 用 class toggle&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>「狀態切換」是 class toggle 的場景、不是 inline style 的場景。&lt;/p>
&lt;hr>
&lt;h2 id="這次任務的重構機會">這次任務的重構機會&lt;/h2>
&lt;h3 id="觀察">觀察&lt;/h3>
&lt;p>Scope filter 用：&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="nx">items&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="kd">var&lt;/span> &lt;span class="nx">show&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">scope&lt;/span> &lt;span class="o">===&lt;/span> &lt;span class="s1">&amp;#39;title&amp;#39;&lt;/span> &lt;span class="o">?&lt;/span> &lt;span class="nx">re&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">test&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">title&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">:&lt;/span> &lt;span class="nx">re&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">test&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">excerpt&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">show&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">el&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">style&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">removeProperty&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;display&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="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">el&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">style&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">setProperty&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;display&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;none&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;important&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="p">});&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>setProperty important&lt;/code> 是為了壓過 Svelte 重繪可能 reset 的 inline style。CSS Layers 之後、important 不再必要。&lt;/p>
&lt;h3 id="判讀">判讀&lt;/h3>
&lt;p>改用 class toggle + layered CSS：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-css" data-lang="css">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c">/* assets/search.css，unlayered（pagefind 在 layer(pagefind) 內） */&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">.&lt;/span>&lt;span class="nc">pagefind-ui__result&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nc">is-scope-filtered&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">display&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">none&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;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">items&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="kd">var&lt;/span> &lt;span class="nx">show&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">scope&lt;/span> &lt;span class="o">===&lt;/span> &lt;span class="s1">&amp;#39;title&amp;#39;&lt;/span> &lt;span class="o">?&lt;/span> &lt;span class="nx">re&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">test&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">title&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">:&lt;/span> &lt;span class="nx">re&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">test&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">excerpt&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">classList&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">toggle&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;is-scope-filtered&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="o">!&lt;/span>&lt;span class="nx">show&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>更乾淨：&lt;/p>
&lt;ul>
&lt;li>CSS 規則集中&lt;/li>
&lt;li>DevTools Element 面板看到 &lt;code>.is-scope-filtered&lt;/code> 就知道為什麼隱藏&lt;/li>
&lt;li>JS 邏輯簡化（&lt;code>classList.toggle&lt;/code> 一行解兩種狀態）&lt;/li>
&lt;li>不需要 &lt;code>!important&lt;/code>&lt;/li>
&lt;/ul>
&lt;h3 id="執行-prerequisite">執行 prerequisite&lt;/h3>
&lt;p>要這個 refactor 生效、先做 #24（CSS Layers）：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-css" data-lang="css">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="p">@&lt;/span>&lt;span class="k">import&lt;/span> &lt;span class="nt">url&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="s2">&amp;#34;/blog/pagefind/pagefind-ui.css&amp;#34;&lt;/span>&lt;span class="o">)&lt;/span> &lt;span class="nt">layer&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="nt">pagefind&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>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="c">/* unlayered，自動勝過 pagefind 的所有 specificity */&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 class="nc">pagefind-ui__result&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nc">is-scope-filtered&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="k">display&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">none&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>否則 layered 的 pagefind CSS 可能用 specificity 30 蓋過 &lt;code>.is-scope-filtered&lt;/code>（specificity 20）。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>JS 改 DOM 元素的視覺狀態、用 class toggle、不用 inline style.setProperty important hack。</strong> Class toggle 的好處：CSS 規則集中可讀、devtools 看到語意化的 class 名而非神秘 inline style、未來改視覺只動 CSS 不動 JS。</p>
<hr>
<h2 id="為什麼-class-toggle-比-inline-style-好">為什麼 class toggle 比 inline style 好</h2>
<h3 id="商業邏輯">商業邏輯</h3>
<p>兩種方式都能達成「JS 控制視覺」、差別在「視覺規則的家在哪」：</p>
<table>
  <thead>
      <tr>
          <th>方式</th>
          <th>視覺規則住址</th>
          <th>維護成本</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>el.style.display = 'none'</code></td>
          <td>散在 JS 各處</td>
          <td>高 — 改視覺要找 JS、不在 CSS</td>
      </tr>
      <tr>
          <td><code>el.classList.toggle('is-hidden')</code> + <code>.is-hidden { display: none }</code></td>
          <td>集中在 CSS</td>
          <td>低 — 改視覺改 CSS</td>
      </tr>
  </tbody>
</table>
<p>集中在 CSS = 設計系統的單一來源、devtools Element 面板看 class 知道狀態、code review 容易理解。</p>
<p><code>!important</code> 的需求消失：只要 CSS Layers 把 vendor CSS 包進低權層、自家 unlayered CSS 自然贏、不需要 important。</p>
<h3 id="何時-inline-style-是必要的">何時 inline style 是必要的</h3>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>inline style 必要</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>動態值（隨 runtime 計算）</td>
          <td>是 — 如 <code>el.style.top = scrollY + 'px'</code></td>
      </tr>
      <tr>
          <td>動畫起點 / 終點</td>
          <td>是 — <code>el.style.transform = ...</code></td>
      </tr>
      <tr>
          <td>切換 boolean 狀態（顯示/隱藏）</td>
          <td>否 — 用 class toggle</td>
      </tr>
      <tr>
          <td>套用設計系統一致樣式</td>
          <td>否 — 用 class toggle</td>
      </tr>
  </tbody>
</table>
<p>「狀態切換」是 class toggle 的場景、不是 inline style 的場景。</p>
<hr>
<h2 id="這次任務的重構機會">這次任務的重構機會</h2>
<h3 id="觀察">觀察</h3>
<p>Scope filter 用：</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">items</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="kd">var</span> <span class="nx">show</span> <span class="o">=</span> <span class="nx">scope</span> <span class="o">===</span> <span class="s1">&#39;title&#39;</span> <span class="o">?</span> <span class="nx">re</span><span class="p">.</span><span class="nx">test</span><span class="p">(</span><span class="nx">title</span><span class="p">)</span> <span class="o">:</span> <span class="nx">re</span><span class="p">.</span><span class="nx">test</span><span class="p">(</span><span class="nx">excerpt</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">show</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">el</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">removeProperty</span><span class="p">(</span><span class="s1">&#39;display&#39;</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">el</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">setProperty</span><span class="p">(</span><span class="s1">&#39;display&#39;</span><span class="p">,</span> <span class="s1">&#39;none&#39;</span><span class="p">,</span> <span class="s1">&#39;important&#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="p">});</span></span></span></code></pre></div><p><code>setProperty important</code> 是為了壓過 Svelte 重繪可能 reset 的 inline style。CSS Layers 之後、important 不再必要。</p>
<h3 id="判讀">判讀</h3>
<p>改用 class toggle + layered CSS：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-css" data-lang="css"><span class="line"><span class="ln">1</span><span class="cl"><span class="c">/* assets/search.css，unlayered（pagefind 在 layer(pagefind) 內） */</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="p">.</span><span class="nc">pagefind-ui__result</span><span class="p">.</span><span class="nc">is-scope-filtered</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="k">display</span><span class="p">:</span> <span class="kc">none</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>




<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">items</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="kd">var</span> <span class="nx">show</span> <span class="o">=</span> <span class="nx">scope</span> <span class="o">===</span> <span class="s1">&#39;title&#39;</span> <span class="o">?</span> <span class="nx">re</span><span class="p">.</span><span class="nx">test</span><span class="p">(</span><span class="nx">title</span><span class="p">)</span> <span class="o">:</span> <span class="nx">re</span><span class="p">.</span><span class="nx">test</span><span class="p">(</span><span class="nx">excerpt</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">classList</span><span class="p">.</span><span class="nx">toggle</span><span class="p">(</span><span class="s1">&#39;is-scope-filtered&#39;</span><span class="p">,</span> <span class="o">!</span><span class="nx">show</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>更乾淨：</p>
<ul>
<li>CSS 規則集中</li>
<li>DevTools Element 面板看到 <code>.is-scope-filtered</code> 就知道為什麼隱藏</li>
<li>JS 邏輯簡化（<code>classList.toggle</code> 一行解兩種狀態）</li>
<li>不需要 <code>!important</code></li>
</ul>
<h3 id="執行-prerequisite">執行 prerequisite</h3>
<p>要這個 refactor 生效、先做 #24（CSS Layers）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-css" data-lang="css"><span class="line"><span class="ln">1</span><span class="cl"><span class="p">@</span><span class="k">import</span> <span class="nt">url</span><span class="o">(</span><span class="s2">&#34;/blog/pagefind/pagefind-ui.css&#34;</span><span class="o">)</span> <span class="nt">layer</span><span class="o">(</span><span class="nt">pagefind</span><span class="o">)</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="c">/* unlayered，自動勝過 pagefind 的所有 specificity */</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="p">.</span><span class="nc">pagefind-ui__result</span><span class="p">.</span><span class="nc">is-scope-filtered</span> <span class="p">{</span> <span class="k">display</span><span class="p">:</span> <span class="kc">none</span><span class="p">;</span> <span class="p">}</span></span></span></code></pre></div><p>否則 layered 的 pagefind CSS 可能用 specificity 30 蓋過 <code>.is-scope-filtered</code>（specificity 20）。</p>
<hr>
<h2 id="內在屬性比較四種-js-控制視覺方式">內在屬性比較：四種 JS 控制視覺方式</h2>
<table>
  <thead>
      <tr>
          <th>方式</th>
          <th>維護成本</th>
          <th>DevTools 可讀性</th>
          <th>Important 需求</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>el.style.display = 'none'</code></td>
          <td>中 — 規則散在 JS</td>
          <td>中 — 看到 inline style</td>
          <td>否</td>
      </tr>
      <tr>
          <td><code>el.style.setProperty('display','none','important')</code></td>
          <td>高 — important 散在 JS</td>
          <td>中</td>
          <td>是 — 跟 framework 競爭</td>
      </tr>
      <tr>
          <td><code>el.classList.toggle('is-hidden')</code> + CSS</td>
          <td>低 — 規則在 CSS</td>
          <td>高 — 看 class 知狀態</td>
          <td>否（CSS Layers 環境下）</td>
      </tr>
      <tr>
          <td><code>el.dataset.state = 'hidden'</code> + <code>[data-state=hidden]</code> CSS</td>
          <td>低 — 規則在 CSS</td>
          <td>高 — attribute 也表達狀態</td>
          <td>否</td>
      </tr>
  </tbody>
</table>
<p>優先選 class toggle（或 dataset） — CSS-first、可讀、可維護。</p>
<hr>
<h2 id="class-toggle-的命名慣例">Class toggle 的命名慣例</h2>
<h3 id="用-is-x--has-x-表狀態">用 <code>is-X</code> / <code>has-X</code> 表狀態</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-css" data-lang="css"><span class="line"><span class="ln">1</span><span class="cl"><span class="p">.</span><span class="nc">is-scope-filtered</span> <span class="p">{</span> <span class="k">display</span><span class="p">:</span> <span class="kc">none</span><span class="p">;</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="p">.</span><span class="nc">is-loading</span> <span class="p">{</span> <span class="k">opacity</span><span class="p">:</span> <span class="mf">0.5</span><span class="p">;</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="p">.</span><span class="nc">has-error</span> <span class="p">{</span> <span class="k">border-color</span><span class="p">:</span> <span class="kc">red</span><span class="p">;</span> <span class="p">}</span></span></span></code></pre></div><p><code>is-</code> / <code>has-</code> 前綴讓「狀態 class」跟「結構 class」（如 <code>.search-shell</code>）視覺區分、code review 一眼看出哪些是動態狀態。</p>
<h3 id="用-bem-modifier">用 BEM modifier</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-css" data-lang="css"><span class="line"><span class="ln">1</span><span class="cl"><span class="p">.</span><span class="nc">search-result--filtered</span> <span class="p">{</span> <span class="k">display</span><span class="p">:</span> <span class="kc">none</span><span class="p">;</span> <span class="p">}</span></span></span></code></pre></div><p>BEM 風格也可以、看專案 convention。重點是有規律、不要混雜。</p>
<hr>
<h2 id="devtools-可讀性的具體差異">DevTools 可讀性的具體差異</h2>
<h3 id="inline-style-視角">Inline style 視角</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;pagefind-ui__result svelte-j9e30&#34;</span> <span class="na">style</span><span class="o">=</span><span class="s">&#34;display: none !important;&#34;</span><span class="p">&gt;</span></span></span></code></pre></div><p>DevTools 顯示「inline style 設了 important」 — 但不知道為什麼。要 grep JS 找出哪段邏輯設的。</p>
<h3 id="class-toggle-視角">Class toggle 視角</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;pagefind-ui__result svelte-j9e30 is-scope-filtered&#34;</span><span class="p">&gt;</span></span></span></code></pre></div><p>DevTools 顯示「有 <code>.is-scope-filtered</code> class」 — class 名本身解釋為什麼隱藏。CSS 面板也直接顯示對應規則。</p>
<hr>
<h2 id="設計取捨js-控制視覺狀態的策略">設計取捨：JS 控制視覺狀態的策略</h2>
<p>四種做法、各自機會成本不同。這個專案選 A（class toggle）當預設、其他做法在特定情境合理。</p>
<h3 id="aclass-toggle--css-規則這個專案的預設">A：Class toggle + CSS 規則（這個專案的預設）</h3>
<ul>
<li><strong>機制</strong>：<code>el.classList.toggle('is-scope-filtered')</code>、CSS 內定義 <code>.is-scope-filtered { display: none }</code></li>
<li><strong>選 A 的理由</strong>：CSS 規則集中、devtools 看 class 知狀態、改視覺只動 CSS、配 CSS Layers 不需 <code>!important</code></li>
<li><strong>適合</strong>：布林狀態切換（顯示 / 隱藏 / 啟用 / 停用）</li>
<li><strong>代價</strong>：需要在 CSS 預先定義 class 規則（多一份 CSS）</li>
</ul>
<h3 id="binline-stylex--">B：Inline <code>style.X = ...</code></h3>
<ul>
<li><strong>機制</strong>：<code>el.style.display = 'none'</code></li>
<li><strong>跟 A 的取捨</strong>：B 一行 JS、A 需要 CSS 規則；但 B 規則散在 JS 各處、devtools 看到 <code>display: none</code> inline 不知道為什麼</li>
<li><strong>B 比 A 好的情境</strong>：runtime 計算的動態值（<code>el.style.top = scrollY + 'px'</code>）— 這類值無法預先寫進 CSS</li>
</ul>
<h3 id="cinline--setpropertyimportant">C：Inline + <code>setProperty('important')</code></h3>
<ul>
<li><strong>機制</strong>：<code>el.style.setProperty('display', 'none', 'important')</code></li>
<li><strong>跟 A/B 的取捨</strong>：C 比 B 多 important、為了壓過 framework 重繪 reset 的 inline；但 C 進入 <code>!important</code> 戰、未來新 important 對撞 debug 困難</li>
<li><strong>C 才合理的情境</strong>：framework 強制 reset 自家 inline style、且不能用 layered CSS（極罕見）</li>
<li><strong>更好的解</strong>：用 <a href="../css-layers-over-specificity/">#24 CSS Layers</a> 解 specificity 戰、本卡片 A 即可</li>
</ul>
<h3 id="ddataset-attribute--css-attribute-selector">D：Dataset attribute + CSS attribute selector</h3>
<ul>
<li><strong>機制</strong>：<code>el.dataset.state = 'hidden'</code>、CSS <code>[data-state=&quot;hidden&quot;] { display: none }</code></li>
<li><strong>跟 A 的取捨</strong>：D 用 attribute 表狀態、A 用 class；D 在「狀態值多種」時更合適（例如 <code>data-state=&quot;loading|ready|error&quot;</code>）</li>
<li><strong>D 比 A 好的情境</strong>：狀態有多個值（不只 boolean）、需要 CSS attribute selector 表達多分支</li>
</ul>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>Refactor 動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>JS 用 <code>style.setProperty(..., 'important')</code></td>
          <td>改用 class toggle、用 CSS Layers 解決 specificity</td>
      </tr>
      <tr>
          <td><code>el.style.display = 'none'</code> 散落多處</td>
          <td>集中為 <code>.is-X</code> class、JS 只 toggle</td>
      </tr>
      <tr>
          <td>DevTools 看到 inline style 不知為什麼</td>
          <td>改用語意化 class、devtools 看 class 自帶解釋</td>
      </tr>
      <tr>
          <td>視覺改動要改 JS（不是 CSS）</td>
          <td>Refactor 為 class toggle、視覺改動只動 CSS</td>
      </tr>
      <tr>
          <td>改視覺需要對抗 framework reset</td>
          <td>用 CSS Layers 把 framework 規則降層、自家規則不需 important</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：CSS 是視覺規則的家、JS 控制狀態 - 兩者透過 class toggle 介面共處、不互相侵入。</p>
<p>Inline style + !important 是「立刻生效」的便利、class toggle 是「樣式留 CSS」的對齊 — 這個反相關的更高層原則見 <a href="../ease-of-writing-vs-intent-alignment/">#67 寫作便利度跟意圖對齊反相關</a>。</p>
]]></content:encoded></item><item><title>setTimeout 輪詢換 MutationObserver</title><link>https://tarrragon.github.io/blog/report/mutationobserver-over-polling/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/mutationobserver-over-polling/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>等待元素出現用 MutationObserver、不用 setTimeout 輪詢。&lt;/strong> Observer 是 event-driven、元素出現的瞬間觸發、無延遲；輪詢是 time-based、最快回應時間 = 輪詢間隔、且 CPU 一直跑。&lt;/p>
&lt;p>輪詢只在「沒有事件可監聽」時才用。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼-observer-比輪詢好">為什麼 observer 比輪詢好&lt;/h2>
&lt;h3 id="商業邏輯">商業邏輯&lt;/h3>
&lt;p>「等待某個 DOM 元素出現」這件事的本質是「監聽 DOM 變化、出現時觸發」 — 完全是 event-driven 場景。&lt;/p>
&lt;p>&lt;code>setTimeout&lt;/code> 輪詢的特徵：&lt;/p>
&lt;ul>
&lt;li>每隔 N ms 檢查一次、最快 N ms 才能回應&lt;/li>
&lt;li>即使元素已經出現、要等到下次檢查才知道&lt;/li>
&lt;li>CPU 持續被佔用（即使元素永遠不出現）&lt;/li>
&lt;/ul>
&lt;p>&lt;code>MutationObserver&lt;/code> 的特徵：&lt;/p>
&lt;ul>
&lt;li>元素出現的瞬間觸發 callback&lt;/li>
&lt;li>0 延遲&lt;/li>
&lt;li>DOM 沒變動時 observer 不耗 CPU&lt;/li>
&lt;/ul>
&lt;p>兩者效能差異在現代設備上不明顯、但設計上 observer 才是「適合這個場景」的工具。&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>等待 DOM 元素出現&lt;/td>
 &lt;td>否 — 用 MutationObserver&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>等待元素尺寸變化&lt;/td>
 &lt;td>否 — 用 ResizeObserver&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>等待元素進入 viewport&lt;/td>
 &lt;td>否 — 用 IntersectionObserver&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>等待外部 API 結果&lt;/td>
 &lt;td>否 — 用 promise / async&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>等待全局變數出現（無事件）&lt;/td>
 &lt;td>是 — 必要時輪詢&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>「無事件可監聽」時才輪詢 — 這類場景在現代 Web 開發少見。&lt;/p>
&lt;hr>
&lt;h2 id="這次任務的輪詢">這次任務的輪詢&lt;/h2>
&lt;h3 id="觀察">觀察&lt;/h3>
&lt;p>&lt;code>search.html&lt;/code> 用 setTimeout 等 pagefind UI mount：&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">function&lt;/span> &lt;span class="nx">waitAndInit&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">filter&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;.pagefind-ui__filter-panel&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">drawer&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;.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"> 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">filter&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="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">setTimeout&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">waitAndInit&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">100&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">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 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">// 找到了、開始 setup
&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">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">reorderFilters&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">setupScopeFilter&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">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">13&lt;/span>&lt;span class="cl">&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">waitAndInit&lt;/span>&lt;span class="p">();&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>每 100ms 檢查一次、有延遲、CPU 一直跑（雖然輕）。&lt;/p>
&lt;h3 id="判讀">判讀&lt;/h3>
&lt;p>改用 MutationObserver 監聽 &lt;code>#search&lt;/code>（pagefind mount target）的子節點變化：&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">function&lt;/span> &lt;span class="nx">waitForPagefind&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">searchRoot&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">onReady&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">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">searchRoot&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 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">onReady&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="k">return&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="c1">// 否則 observe DOM 變動
&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="kd">var&lt;/span> &lt;span class="nx">observer&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"> 9&lt;/span>&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">searchRoot&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 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">observer&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">11&lt;/span>&lt;span class="cl"> &lt;span class="nx">onReady&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;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl"> &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">observer&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">observe&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">searchRoot&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;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="nx">waitForPagefind&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">getElementById&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;search&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">18&lt;/span>&lt;span class="cl"> &lt;span class="nx">filter&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;.pagefind-ui__filter-panel&amp;#39;&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">drawer&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;.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">20&lt;/span>&lt;span class="cl"> &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">21&lt;/span>&lt;span class="cl"> &lt;span class="nx">reorderFilters&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="nx">setupScopeFilter&lt;/span>&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="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">24&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>
&lt;ul>
&lt;li>pagefind 渲染完瞬間觸發、無延遲&lt;/li>
&lt;li>&lt;code>disconnect()&lt;/code> 後 observer 不再耗資源&lt;/li>
&lt;li>已存在時 fast path 直接觸發&lt;/li>
&lt;/ul>
&lt;h3 id="執行通用-helper">執行：通用 helper&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="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"> * 等待 selector 在 root 內出現、觸發 callback。
&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"> * 已存在則 sync 觸發；不存在則用 MutationObserver 等待。
&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"> */&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">function&lt;/span> &lt;span class="nx">waitForElement&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">root&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">selector&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">callback&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="kd">var&lt;/span> &lt;span class="nx">existing&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">root&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">selector&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="nx">existing&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="nx">callback&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">existing&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="k">return&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="kd">var&lt;/span> &lt;span class="nx">observer&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">12&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">el&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">root&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">selector&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="k">if&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">14&lt;/span>&lt;span class="cl"> &lt;span class="nx">observer&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">15&lt;/span>&lt;span class="cl"> &lt;span class="nx">callback&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">16&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl"> &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">observer&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">observe&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">root&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;span class="line">&lt;span class="ln">19&lt;/span>&lt;span class="cl">&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>&lt;/span>&lt;span class="line">&lt;span class="ln">21&lt;/span>&lt;span class="cl">&lt;span class="c1">// 用法
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">22&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="nx">waitForElement&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">searchRoot&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 class="kd">function&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">drawer&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">23&lt;/span>&lt;span class="cl"> &lt;span class="c1">// 開始 setup
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">24&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>把 wait 抽成 helper、setup code 變得更簡潔。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>等待元素出現用 MutationObserver、不用 setTimeout 輪詢。</strong> Observer 是 event-driven、元素出現的瞬間觸發、無延遲；輪詢是 time-based、最快回應時間 = 輪詢間隔、且 CPU 一直跑。</p>
<p>輪詢只在「沒有事件可監聽」時才用。</p>
<hr>
<h2 id="為什麼-observer-比輪詢好">為什麼 observer 比輪詢好</h2>
<h3 id="商業邏輯">商業邏輯</h3>
<p>「等待某個 DOM 元素出現」這件事的本質是「監聽 DOM 變化、出現時觸發」 — 完全是 event-driven 場景。</p>
<p><code>setTimeout</code> 輪詢的特徵：</p>
<ul>
<li>每隔 N ms 檢查一次、最快 N ms 才能回應</li>
<li>即使元素已經出現、要等到下次檢查才知道</li>
<li>CPU 持續被佔用（即使元素永遠不出現）</li>
</ul>
<p><code>MutationObserver</code> 的特徵：</p>
<ul>
<li>元素出現的瞬間觸發 callback</li>
<li>0 延遲</li>
<li>DOM 沒變動時 observer 不耗 CPU</li>
</ul>
<p>兩者效能差異在現代設備上不明顯、但設計上 observer 才是「適合這個場景」的工具。</p>
<h3 id="何時輪詢是必要的">何時輪詢是必要的</h3>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>輪詢必要</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>等待 DOM 元素出現</td>
          <td>否 — 用 MutationObserver</td>
      </tr>
      <tr>
          <td>等待元素尺寸變化</td>
          <td>否 — 用 ResizeObserver</td>
      </tr>
      <tr>
          <td>等待元素進入 viewport</td>
          <td>否 — 用 IntersectionObserver</td>
      </tr>
      <tr>
          <td>等待外部 API 結果</td>
          <td>否 — 用 promise / async</td>
      </tr>
      <tr>
          <td>等待全局變數出現（無事件）</td>
          <td>是 — 必要時輪詢</td>
      </tr>
  </tbody>
</table>
<p>「無事件可監聽」時才輪詢 — 這類場景在現代 Web 開發少見。</p>
<hr>
<h2 id="這次任務的輪詢">這次任務的輪詢</h2>
<h3 id="觀察">觀察</h3>
<p><code>search.html</code> 用 setTimeout 等 pagefind UI mount：</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">waitAndInit</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">filter</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;.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="nx">drawer</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;.pagefind-ui__drawer&#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">filter</span> <span class="o">||</span> <span class="o">!</span><span class="nx">drawer</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">setTimeout</span><span class="p">(</span><span class="nx">waitAndInit</span><span class="p">,</span> <span class="mi">100</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="k">return</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">// 找到了、開始 setup
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"></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">reorderFilters</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">  <span class="nx">setupScopeFilter</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">12</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">13</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="nx">waitAndInit</span><span class="p">();</span></span></span></code></pre></div><p>每 100ms 檢查一次、有延遲、CPU 一直跑（雖然輕）。</p>
<h3 id="判讀">判讀</h3>
<p>改用 MutationObserver 監聽 <code>#search</code>（pagefind mount target）的子節點變化：</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">waitForPagefind</span><span class="p">(</span><span class="nx">searchRoot</span><span class="p">,</span> <span class="nx">onReady</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">if</span> <span class="p">(</span><span class="nx">searchRoot</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 class="p">{</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="nx">onReady</span><span class="p">();</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="k">return</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="c1">// 否則 observe DOM 變動
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"></span>  <span class="kd">var</span> <span class="nx">observer</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"> 9</span><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="nx">searchRoot</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 class="p">{</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">      <span class="nx">observer</span><span class="p">.</span><span class="nx">disconnect</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">      <span class="nx">onReady</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <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="nx">observer</span><span class="p">.</span><span class="nx">observe</span><span class="p">(</span><span class="nx">searchRoot</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="nx">waitForPagefind</span><span class="p">(</span><span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="s1">&#39;search&#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">18</span><span class="cl">  <span class="nx">filter</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;.pagefind-ui__filter-panel&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">  <span class="nx">drawer</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;.pagefind-ui__drawer&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">  <span class="nx">place</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">  <span class="nx">reorderFilters</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">  <span class="nx">setupScopeFilter</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">23</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">24</span><span class="cl"><span class="p">});</span></span></span></code></pre></div><p>特性：</p>
<ul>
<li>pagefind 渲染完瞬間觸發、無延遲</li>
<li><code>disconnect()</code> 後 observer 不再耗資源</li>
<li>已存在時 fast path 直接觸發</li>
</ul>
<h3 id="執行通用-helper">執行：通用 helper</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="cm">/**
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="cm"> * 等待 selector 在 root 內出現、觸發 callback。
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="cm"> * 已存在則 sync 觸發；不存在則用 MutationObserver 等待。
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="cm"> */</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="kd">function</span> <span class="nx">waitForElement</span><span class="p">(</span><span class="nx">root</span><span class="p">,</span> <span class="nx">selector</span><span class="p">,</span> <span class="nx">callback</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">existing</span> <span class="o">=</span> <span class="nx">root</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="nx">selector</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">existing</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">callback</span><span class="p">(</span><span class="nx">existing</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="k">return</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="kd">var</span> <span class="nx">observer</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">12</span><span class="cl">    <span class="kd">var</span> <span class="nx">el</span> <span class="o">=</span> <span class="nx">root</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="nx">selector</span><span class="p">);</span>
</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">el</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">observer</span><span class="p">.</span><span class="nx">disconnect</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">      <span class="nx">callback</span><span class="p">(</span><span class="nx">el</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <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="nx">observer</span><span class="p">.</span><span class="nx">observe</span><span class="p">(</span><span class="nx">root</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">19</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">
</span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="c1">// 用法
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="c1"></span><span class="nx">waitForElement</span><span class="p">(</span><span class="nx">searchRoot</span><span class="p">,</span> <span class="s1">&#39;.pagefind-ui__drawer&#39;</span><span class="p">,</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">drawer</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">  <span class="c1">// 開始 setup
</span></span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="c1"></span><span class="p">});</span></span></span></code></pre></div><p>把 wait 抽成 helper、setup code 變得更簡潔。</p>
<hr>
<h2 id="內在屬性比較四種等待機制">內在屬性比較：四種等待機制</h2>
<table>
  <thead>
      <tr>
          <th>機制</th>
          <th>延遲</th>
          <th>CPU 使用</th>
          <th>適用情境</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>setTimeout</code> 單次</td>
          <td>固定延遲</td>
          <td>0</td>
          <td>等已知時間</td>
      </tr>
      <tr>
          <td><code>setTimeout</code> 輪詢</td>
          <td>平均 = 間隔 / 2</td>
          <td>持續低使用</td>
          <td>沒事件可監聽</td>
      </tr>
      <tr>
          <td><code>MutationObserver</code></td>
          <td>0 — 變動瞬間</td>
          <td>DOM 變動時短暫</td>
          <td>等待 DOM 元素</td>
      </tr>
      <tr>
          <td>Promise / async</td>
          <td>0 — resolve 瞬間</td>
          <td>0</td>
          <td>等待 async 操作</td>
      </tr>
  </tbody>
</table>
<p>優先順序：<strong>event-driven &gt; async &gt; polling &gt; timeout</strong>。輪詢是最後選擇。</p>
<hr>
<h2 id="mutationobserver-的細節">MutationObserver 的細節</h2>
<h3 id="observe-option-選對">Observe option 選對</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">observer</span><span class="p">.</span><span class="nx">observe</span><span class="p">(</span><span class="nx">root</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">childList</span><span class="o">:</span> <span class="kc">true</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="nx">subtree</span><span class="o">:</span> <span class="kc">true</span><span class="p">,</span>      <span class="c1">// 包含深層子節點
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"></span>  <span class="nx">attributes</span><span class="o">:</span> <span class="kc">false</span><span class="p">,</span>  <span class="c1">// 不看 attribute 變動
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span>  <span class="nx">characterData</span><span class="o">:</span> <span class="kc">false</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>只勾必要的、不要全部勾 — 觸發頻率影響效能。</p>
<h3 id="找到目標後-disconnect">找到目標後 disconnect</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">observer</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">2</span><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="nx">found</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">observer</span><span class="p">.</span><span class="nx">disconnect</span><span class="p">();</span>   <span class="c1">// 立刻停、不要繼續監聽
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"></span>    <span class="nx">callback</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 class="p">});</span></span></span></code></pre></div><p>不 disconnect 的話、observer 一直 active、未來任何 DOM 變動都觸發 callback。</p>
<h3 id="已存在的-fast-path">已存在的 fast path</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">if</span> <span class="p">(</span><span class="nx">root</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="nx">selector</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">callback</span><span class="p">();</span>   <span class="c1">// 已存在則直接觸發、不需 observer
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></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="p">}</span></span></span></code></pre></div><p>避免「元素已經存在但還是要等下次變動才觸發」的延遲。</p>
<hr>
<h2 id="設計取捨等待-dom-元素出現的策略">設計取捨：等待 DOM 元素出現的策略</h2>
<p>四種做法、各自機會成本不同。這個專案選 A（MutationObserver + fast path）當預設、其他做法在特定情境合理。</p>
<h3 id="amutationobserver--already-exists-fast-path這個專案的預設">A：MutationObserver + already-exists fast path（這個專案的預設）</h3>
<ul>
<li><strong>機制</strong>：先檢查目標是否已存在（直接觸發）、否則 observe DOM 變動、找到後 disconnect</li>
<li><strong>選 A 的理由</strong>：0 延遲、CPU 不被輪詢吃、找到後立即停</li>
<li><strong>適合</strong>：等待 framework / 第三方 library 動態 mount 的元素</li>
<li><strong>代價</strong>：需要寫 fast path + observer + disconnect 三段邏輯（用 helper 包裝即可一行調用）</li>
</ul>
<h3 id="bsettimeout-輪詢">B：<code>setTimeout</code> 輪詢</h3>
<ul>
<li><strong>機制</strong>：每隔 N ms 檢查、找到就停</li>
<li><strong>跟 A 的取捨</strong>：B 寫法簡單、A 設計嚴謹；但 B 有最快回應 = N ms 的延遲、CPU 一直跑</li>
<li><strong>B 比 A 好的情境</strong>：等待對象是無事件可監聽的狀態（全局變數出現、外部 API 結果且無 promise 介面），MutationObserver 無處掛載</li>
</ul>
<h3 id="cpromise--async如果-api-提供">C：Promise / async（如果 API 提供）</h3>
<ul>
<li><strong>機制</strong>：<code>await framework.ready()</code> 等 framework 提供的 promise</li>
<li><strong>跟 A 的取捨</strong>：C 是最乾淨的解、但需要 framework / library 提供 promise API</li>
<li><strong>C 比 A 好的情境</strong>：等的目標有官方 promise 介面（避免自行 observe 內部 DOM）</li>
</ul>
<h3 id="drequestanimationframe-迴圈">D：<code>requestAnimationFrame</code> 迴圈</h3>
<ul>
<li><strong>機制</strong>：每個 frame 檢查一次</li>
<li><strong>跟 B 的取捨</strong>：D 跟著 frame、不會在 idle 時跑；但仍是輪詢、延遲 16ms</li>
<li><strong>D 才合理的情境</strong>：等待動畫 frame 相關狀態（罕見）— 純等 DOM 元素仍應用 A</li>
</ul>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>Refactor 動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>setTimeout</code> 用來等 DOM 元素</td>
          <td>改 <code>MutationObserver</code> + disconnect</td>
      </tr>
      <tr>
          <td><code>setInterval</code> 不停跑檢查元素狀態</td>
          <td>改 <code>MutationObserver</code> 或 <code>ResizeObserver</code></td>
      </tr>
      <tr>
          <td>等待邏輯有「最快 X ms 才回應」的延遲</td>
          <td>改 event-driven 機制、消除延遲</td>
      </tr>
      <tr>
          <td>Observer 找到目標後沒 disconnect</td>
          <td>加 disconnect、避免繼續觸發</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：DOM 變動有對應的 event 機制可監聽 — 用對機制就有 0 延遲、無 CPU 浪費。輪詢是「沒辦法的辦法」、不是 default。</p>
]]></content:encoded></item><item><title>Init function 是 orchestrator、職責拆出獨立 function</title><link>https://tarrragon.github.io/blog/report/split-setup-by-responsibility/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/split-setup-by-responsibility/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>單一函式做 ≥ 3 件無關的事就拆。&lt;/strong> 每個函式只負責一個職責、有明確的 input / output、可以獨立 debug 與測試。Init function 變成「組合各職責 function 的 orchestrator」。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼拆函式">為什麼拆函式&lt;/h2>
&lt;h3 id="商業邏輯">商業邏輯&lt;/h3>
&lt;p>單一函式做多件事的成本：&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>一函式 50 行做 1 件事&lt;/td>
 &lt;td>低 — 容易讀、職責清楚&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>一函式 100 行做 3 件事&lt;/td>
 &lt;td>中 — 邏輯交織、debug 要分辨哪段&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>一函式 200 行做 5+ 件事&lt;/td>
 &lt;td>高 — 沒人想動、改一處可能影響別處&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>拆函式的成本是「多寫幾個函式名與簽名」、收益是「每個函式範圍小、debug 容易、可單獨重用」。&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;/td>
 &lt;td>切出沒邏輯意義的片段、更亂&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>按職責拆&lt;/td>
 &lt;td>每個函式名能描述「做什麼」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>按 input / output 拆&lt;/td>
 &lt;td>函式變得 testable、可組合&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>按職責拆的判斷：能不能用一個動詞片語描述函式做什麼？做不到 → 多個職責、該拆。&lt;/p>
&lt;hr>
&lt;h2 id="這次任務的拆分機會">這次任務的拆分機會&lt;/h2>
&lt;h3 id="觀察">觀察&lt;/h3>
&lt;p>&lt;code>setupScopeFilter()&lt;/code> 現況做 5 件事：&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">function&lt;/span> &lt;span class="nx">setupScopeFilter&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">// 1. 找元素
&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="kd">var&lt;/span> &lt;span class="nx">scopeEl&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-scope&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">input&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;.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="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>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="c1">// 2. 量測 scope 高度寫回 CSS 變數
&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="kd">function&lt;/span> &lt;span class="nx">syncScopeHeight&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"> 9&lt;/span>&lt;span class="cl"> &lt;span class="nx">syncScopeHeight&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="k">new&lt;/span> &lt;span class="nx">ResizeObserver&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">syncScopeHeight&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">observe&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">scopeEl&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>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl"> &lt;span class="c1">// 3. 把 filter-panel 搬到 sidebar (position function)
&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="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 class="p">...&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>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl"> &lt;span class="c1">// 4. 註冊 scope filter listener + apply
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="kd">function&lt;/span> &lt;span class="nx">apply&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">17&lt;/span>&lt;span class="cl"> &lt;span class="nx">scopeEl&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">apply&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="c1">// ...
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">20&lt;/span>&lt;span class="cl"> &lt;span class="c1">// 5. Reorder filter blocks
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">21&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="kd">function&lt;/span> &lt;span class="nx">reorderFilters&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">22&lt;/span>&lt;span class="cl"> &lt;span class="nx">reorderFilters&lt;/span>&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>5 個職責塞在一個函式：找元素、量高度、搬 slot、scope filter、reorder filter。&lt;/p>
&lt;h3 id="判讀">判讀&lt;/h3>
&lt;p>按職責拆成獨立函式：&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">function&lt;/span> &lt;span class="nx">findSearchElements&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="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 class="nx">shell&lt;/span>&lt;span class="o">:&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">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="nx">filter&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__filter-panel&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 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="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>&lt;/span>&lt;span class="line">&lt;span class="ln">12&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">scopeEl&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="kd">function&lt;/span> &lt;span class="nx">update&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">14&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">h&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">scopeEl&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">offsetHeight&lt;/span> &lt;span class="o">||&lt;/span> &lt;span class="mi">56&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="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">style&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">setProperty&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;--search-scope-h&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">h&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="s1">&amp;#39;px&amp;#39;&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;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl"> &lt;span class="nx">update&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="k">new&lt;/span> &lt;span class="nx">ResizeObserver&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">update&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">observe&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">scopeEl&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="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">20&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">21&lt;/span>&lt;span class="cl">&lt;span class="kd">function&lt;/span> &lt;span class="nx">setupFilterSlotSwap&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">slot&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">breakpoint&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">22&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: &amp;#39;&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="nx">breakpoint&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="s1">&amp;#39;px)&amp;#39;&lt;/span>&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="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">24&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="nx">slot&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">25&lt;/span>&lt;span class="cl"> &lt;span class="k">else&lt;/span> &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">26&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">27&lt;/span>&lt;span class="cl"> &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">28&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">29&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">30&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">31&lt;/span>&lt;span class="cl">&lt;span class="kd">function&lt;/span> &lt;span class="nx">reorderFilters&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">filterPanel&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">desiredOrder&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">32&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">blocks&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">filterPanel&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__filter-block&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">33&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">byKey&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">34&lt;/span>&lt;span class="cl"> &lt;span class="nx">blocks&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">b&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">35&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">key&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">b&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__filter-name&amp;#39;&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">textContent&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">trim&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="nx">toLowerCase&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">36&lt;/span>&lt;span class="cl"> &lt;span class="nx">byKey&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="nx">key&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">b&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">37&lt;/span>&lt;span class="cl"> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">38&lt;/span>&lt;span class="cl"> &lt;span class="nx">desiredOrder&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">k&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">39&lt;/span>&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">byKey&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="nx">k&lt;/span>&lt;span class="p">])&lt;/span> &lt;span class="nx">filterPanel&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">byKey&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="nx">k&lt;/span>&lt;span class="p">]);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">40&lt;/span>&lt;span class="cl"> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">41&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">42&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">43&lt;/span>&lt;span class="cl">&lt;span class="kd">function&lt;/span> &lt;span class="nx">setupScopeFilter&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">scopeEl&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">input&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">ui&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">44&lt;/span>&lt;span class="cl"> &lt;span class="kd">function&lt;/span> &lt;span class="nx">getScope&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">45&lt;/span>&lt;span class="cl"> &lt;span class="kd">function&lt;/span> &lt;span class="nx">apply&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">46&lt;/span>&lt;span class="cl"> &lt;span class="kd">function&lt;/span> &lt;span class="nx">schedule&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">47&lt;/span>&lt;span class="cl"> &lt;span class="nx">scopeEl&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">schedule&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">48&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="nx">schedule&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">49&lt;/span>&lt;span class="cl"> &lt;span class="k">new&lt;/span> &lt;span class="nx">MutationObserver&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">schedule&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">observe&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">ui&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;span class="line">&lt;span class="ln">50&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;code>init()&lt;/code> 變成 orchestrator：&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">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="nx">waitForElement&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">shell&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 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"> 6&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="nx">findSearchElements&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"> 7&lt;/span>&lt;span class="cl"> &lt;span class="nx">syncScopeHeight&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">els&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"> 8&lt;/span>&lt;span class="cl"> &lt;span class="nx">setupFilterSlotSwap&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">els&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">els&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">drawer&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">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 class="mi">1400&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="nx">reorderFilters&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">els&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">filter&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;type&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;tag&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="nx">setupScopeFilter&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">els&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">scope&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">els&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">input&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">els&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">ui&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="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;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&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>每個拆出的函式：&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>單一函式做 ≥ 3 件無關的事就拆。</strong> 每個函式只負責一個職責、有明確的 input / output、可以獨立 debug 與測試。Init function 變成「組合各職責 function 的 orchestrator」。</p>
<hr>
<h2 id="為什麼拆函式">為什麼拆函式</h2>
<h3 id="商業邏輯">商業邏輯</h3>
<p>單一函式做多件事的成本：</p>
<table>
  <thead>
      <tr>
          <th>規模</th>
          <th>維護痛點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>一函式 50 行做 1 件事</td>
          <td>低 — 容易讀、職責清楚</td>
      </tr>
      <tr>
          <td>一函式 100 行做 3 件事</td>
          <td>中 — 邏輯交織、debug 要分辨哪段</td>
      </tr>
      <tr>
          <td>一函式 200 行做 5+ 件事</td>
          <td>高 — 沒人想動、改一處可能影響別處</td>
      </tr>
  </tbody>
</table>
<p>拆函式的成本是「多寫幾個函式名與簽名」、收益是「每個函式範圍小、debug 容易、可單獨重用」。</p>
<h3 id="拆的依據是職責不是行數">拆的依據是「職責」、不是行數</h3>
<table>
  <thead>
      <tr>
          <th>拆法</th>
          <th>結果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>按行數機械拆</td>
          <td>切出沒邏輯意義的片段、更亂</td>
      </tr>
      <tr>
          <td>按職責拆</td>
          <td>每個函式名能描述「做什麼」</td>
      </tr>
      <tr>
          <td>按 input / output 拆</td>
          <td>函式變得 testable、可組合</td>
      </tr>
  </tbody>
</table>
<p>按職責拆的判斷：能不能用一個動詞片語描述函式做什麼？做不到 → 多個職責、該拆。</p>
<hr>
<h2 id="這次任務的拆分機會">這次任務的拆分機會</h2>
<h3 id="觀察">觀察</h3>
<p><code>setupScopeFilter()</code> 現況做 5 件事：</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">setupScopeFilter</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">// 1. 找元素
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1"></span>  <span class="kd">var</span> <span class="nx">scopeEl</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-scope&#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">input</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;.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="c1">// ...
</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">// 2. 量測 scope 高度寫回 CSS 變數
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"></span>  <span class="kd">function</span> <span class="nx">syncScopeHeight</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"> 9</span><span class="cl">  <span class="nx">syncScopeHeight</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">  <span class="k">new</span> <span class="nx">ResizeObserver</span><span class="p">(</span><span class="nx">syncScopeHeight</span><span class="p">).</span><span class="nx">observe</span><span class="p">(</span><span class="nx">scopeEl</span><span class="p">);</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="c1">// 3. 把 filter-panel 搬到 sidebar (position function)
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1"></span>  <span class="kd">function</span> <span class="nx">place</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">14</span><span class="cl">
</span></span><span class="line"><span class="ln">15</span><span class="cl">  <span class="c1">// 4. 註冊 scope filter listener + apply
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="c1"></span>  <span class="kd">function</span> <span class="nx">apply</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">17</span><span class="cl">  <span class="nx">scopeEl</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">apply</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">  <span class="c1">// ...
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="c1"></span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">  <span class="c1">// 5. Reorder filter blocks
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="c1"></span>  <span class="kd">function</span> <span class="nx">reorderFilters</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">22</span><span class="cl">  <span class="nx">reorderFilters</span><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>5 個職責塞在一個函式：找元素、量高度、搬 slot、scope filter、reorder filter。</p>
<h3 id="判讀">判讀</h3>
<p>按職責拆成獨立函式：</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">findSearchElements</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="k">return</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="nx">shell</span><span class="o">:</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">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="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"> 8</span><span class="cl">    <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="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></span><span class="line"><span class="ln">12</span><span class="cl"><span class="kd">function</span> <span class="nx">syncScopeHeight</span><span class="p">(</span><span class="nx">scopeEl</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">  <span class="kd">function</span> <span class="nx">update</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="kd">var</span> <span class="nx">h</span> <span class="o">=</span> <span class="nx">scopeEl</span><span class="p">.</span><span class="nx">offsetHeight</span> <span class="o">||</span> <span class="mi">56</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">    <span class="nb">document</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">setProperty</span><span class="p">(</span><span class="s1">&#39;--search-scope-h&#39;</span><span class="p">,</span> <span class="nx">h</span> <span class="o">+</span> <span class="s1">&#39;px&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">  <span class="nx">update</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">  <span class="k">new</span> <span class="nx">ResizeObserver</span><span class="p">(</span><span class="nx">update</span><span class="p">).</span><span class="nx">observe</span><span class="p">(</span><span class="nx">scopeEl</span><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></span><span class="line"><span class="ln">21</span><span class="cl"><span class="kd">function</span> <span class="nx">setupFilterSlotSwap</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">slot</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">22</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">23</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">24</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">slot</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">25</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">26</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">27</span><span class="cl">  <span class="nx">place</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">28</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">29</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">30</span><span class="cl">
</span></span><span class="line"><span class="ln">31</span><span class="cl"><span class="kd">function</span> <span class="nx">reorderFilters</span><span class="p">(</span><span class="nx">filterPanel</span><span class="p">,</span> <span class="nx">desiredOrder</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">32</span><span class="cl">  <span class="kd">var</span> <span class="nx">blocks</span> <span class="o">=</span> <span class="nx">filterPanel</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui__filter-block&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">33</span><span class="cl">  <span class="kd">var</span> <span class="nx">byKey</span> <span class="o">=</span> <span class="p">{};</span>
</span></span><span class="line"><span class="ln">34</span><span class="cl">  <span class="nx">blocks</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">b</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">35</span><span class="cl">    <span class="kd">var</span> <span class="nx">key</span> <span class="o">=</span> <span class="nx">b</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui__filter-name&#39;</span><span class="p">).</span><span class="nx">textContent</span><span class="p">.</span><span class="nx">trim</span><span class="p">().</span><span class="nx">toLowerCase</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">36</span><span class="cl">    <span class="nx">byKey</span><span class="p">[</span><span class="nx">key</span><span class="p">]</span> <span class="o">=</span> <span class="nx">b</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">37</span><span class="cl">  <span class="p">});</span>
</span></span><span class="line"><span class="ln">38</span><span class="cl">  <span class="nx">desiredOrder</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">k</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">39</span><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="nx">byKey</span><span class="p">[</span><span class="nx">k</span><span class="p">])</span> <span class="nx">filterPanel</span><span class="p">.</span><span class="nx">appendChild</span><span class="p">(</span><span class="nx">byKey</span><span class="p">[</span><span class="nx">k</span><span class="p">]);</span>
</span></span><span class="line"><span class="ln">40</span><span class="cl">  <span class="p">});</span>
</span></span><span class="line"><span class="ln">41</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">42</span><span class="cl">
</span></span><span class="line"><span class="ln">43</span><span class="cl"><span class="kd">function</span> <span class="nx">setupScopeFilter</span><span class="p">(</span><span class="nx">scopeEl</span><span class="p">,</span> <span class="nx">input</span><span class="p">,</span> <span class="nx">ui</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">44</span><span class="cl">  <span class="kd">function</span> <span class="nx">getScope</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">45</span><span class="cl">  <span class="kd">function</span> <span class="nx">apply</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">46</span><span class="cl">  <span class="kd">function</span> <span class="nx">schedule</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">47</span><span class="cl">  <span class="nx">scopeEl</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">schedule</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">48</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="nx">schedule</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">49</span><span class="cl">  <span class="k">new</span> <span class="nx">MutationObserver</span><span class="p">(</span><span class="nx">schedule</span><span class="p">).</span><span class="nx">observe</span><span class="p">(</span><span class="nx">ui</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">50</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p><code>init()</code> 變成 orchestrator：</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">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="nx">waitForElement</span><span class="p">(</span><span class="nx">shell</span><span class="p">,</span> <span class="s1">&#39;.pagefind-ui__drawer&#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"> 6</span><span class="cl">    <span class="kd">var</span> <span class="nx">els</span> <span class="o">=</span> <span class="nx">findSearchElements</span><span class="p">(</span><span class="nx">shell</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="nx">syncScopeHeight</span><span class="p">(</span><span class="nx">els</span><span class="p">.</span><span class="nx">scope</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="nx">setupFilterSlotSwap</span><span class="p">(</span><span class="nx">els</span><span class="p">.</span><span class="nx">filter</span><span class="p">,</span> <span class="nx">els</span><span class="p">.</span><span class="nx">drawer</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;.search-filter-slot&#39;</span><span class="p">),</span> <span class="mi">1400</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="nx">reorderFilters</span><span class="p">(</span><span class="nx">els</span><span class="p">.</span><span class="nx">filter</span><span class="p">,</span> <span class="p">[</span><span class="s1">&#39;type&#39;</span><span class="p">,</span> <span class="s1">&#39;tag&#39;</span><span class="p">]);</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="nx">setupScopeFilter</span><span class="p">(</span><span class="nx">els</span><span class="p">.</span><span class="nx">scope</span><span class="p">,</span> <span class="nx">els</span><span class="p">.</span><span class="nx">input</span><span class="p">,</span> <span class="nx">els</span><span class="p">.</span><span class="nx">ui</span><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="p">}</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="nx">init</span><span class="p">();</span></span></span></code></pre></div><p>每個拆出的函式：</p>
<ul>
<li>名字描述做什麼（動詞 + 名詞）</li>
<li>接受需要的元素當參數（不依賴全局）</li>
<li>不知道其他函式的存在（解耦）</li>
</ul>
<hr>
<h2 id="內在屬性比較四種函式拆分粒度">內在屬性比較：四種函式拆分粒度</h2>
<table>
  <thead>
      <tr>
          <th>粒度</th>
          <th>維護成本</th>
          <th>Debug 範圍</th>
          <th>可重用性</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>一個 mega init function</td>
          <td>高 — 200+ 行交織</td>
          <td>整個函式都要看</td>
          <td>低 — 跟特定 setup 綁</td>
      </tr>
      <tr>
          <td>按行數機械拆（每 30 行一份）</td>
          <td>中 — 切出無意義片段</td>
          <td>中</td>
          <td>低</td>
      </tr>
      <tr>
          <td>按職責拆</td>
          <td>低 — 每函式單一職責</td>
          <td>函式內部、範圍小</td>
          <td>高</td>
      </tr>
      <tr>
          <td>按職責拆 + class 包裝</td>
          <td>低</td>
          <td>範圍小</td>
          <td>最高 — 多實例</td>
      </tr>
  </tbody>
</table>
<p>優先按職責拆 — 函式名表達 intent、debug 範圍小、單獨可測。</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="nx">syncScopeHeight</span><span class="p">()</span>           <span class="c1">// 動詞 + 對象
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="nx">setupFilterSlotSwap</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="nx">reorderFilters</span><span class="p">()</span>            <span class="c1">// 動詞 + 對象
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"></span><span class="nx">findSearchElements</span><span class="p">()</span>        <span class="c1">// 動詞 + 對象
</span></span></span></code></pre></div><p>不要：</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">filter</span><span class="p">()</span>        <span class="c1">// 動詞模糊（filter 是動詞還是名詞？）
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="nx">handle</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="nx">init</span><span class="p">()</span>          <span class="c1">// 只有 orchestrator 用、不要散在各處
</span></span></span></code></pre></div><h3 id="2-參數是該函式需要的不傳一個-mega-object">2. 參數是該函式需要的、不傳一個 mega object</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">// 好 — 函式知道它需要什麼
</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">syncScopeHeight</span><span class="p">(</span><span class="nx">scopeEl</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">function</span> <span class="nx">syncScopeHeight</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">6</span><span class="cl">  <span class="kd">var</span> <span class="nx">scope</span> <span class="o">=</span> <span class="nx">allElements</span><span class="p">.</span><span class="nx">scope</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>明確參數 = 明確依賴 = 容易測試。</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="kd">function</span> <span class="nx">syncScopeHeight</span><span class="p">(</span><span class="nx">scopeEl</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">function</span> <span class="nx">update</span><span class="p">()</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">body</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">setProperty</span><span class="p">(</span><span class="s1">&#39;--search-scope-h&#39;</span><span class="p">,</span> <span class="nx">scopeEl</span><span class="p">.</span><span class="nx">offsetHeight</span> <span class="o">+</span> <span class="s1">&#39;px&#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 class="nx">update</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">  <span class="k">new</span> <span class="nx">ResizeObserver</span><span class="p">(</span><span class="nx">update</span><span class="p">).</span><span class="nx">observe</span><span class="p">(</span><span class="nx">scopeEl</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>副作用（DOM 變動、event listener、observer）都在這個函式內。沒散到別處。</p>
<h3 id="4-不依賴外部變數">4. 不依賴外部變數</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">// 好 — 純函式、依賴只在參數
</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">reorderFilters</span><span class="p">(</span><span class="nx">filterPanel</span><span class="p">,</span> <span class="nx">desiredOrder</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">desiredOrder</span> <span class="o">=</span> <span class="p">[</span><span class="s1">&#39;type&#39;</span><span class="p">,</span> <span class="s1">&#39;tag&#39;</span><span class="p">];</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="kd">function</span> <span class="nx">reorderFilters</span><span class="p">(</span><span class="nx">filterPanel</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">// 用了 desiredOrder
</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>純函式 = 無隱式依賴 = 重用方便、測試方便。</p>
<hr>
<h2 id="設計取捨大型-init-function-的拆分策略">設計取捨：大型 init function 的拆分策略</h2>
<p>四種做法、各自機會成本不同。這個專案選 A（按職責拆 + 純函式）當預設、其他做法在特定情境合理。</p>
<h3 id="a按職責拆--純函式--init-當-orchestrator這個專案的預設">A：按職責拆 + 純函式 + init 當 orchestrator（這個專案的預設）</h3>
<ul>
<li><strong>機制</strong>：每職責一個函式（動詞 + 對象命名）、依賴透過參數傳入、init 組合各函式</li>
<li><strong>選 A 的理由</strong>：debug 範圍小（職責 = 函式 = grep 範圍）、單獨可測、可重用</li>
<li><strong>適合</strong>：&gt; 50 行的 init function、預期長期維護</li>
<li><strong>代價</strong>：多寫幾個函式名與簽名、檔案 LOC 略增</li>
</ul>
<h3 id="b按行數機械拆每-30-行一份">B：按行數機械拆（每 30 行一份）</h3>
<ul>
<li><strong>機制</strong>：固定 LOC 拆檔、不考慮職責邊界</li>
<li><strong>跟 A 的取捨</strong>：B 拆完後切片無邏輯意義、A 切片各自完整；B 更亂、debug 反而更難</li>
<li><strong>B 是反模式</strong>：「行數」不是有意義的拆分判準 — 拆完後切片無邏輯意義、debug 反而更難</li>
</ul>
<h3 id="c保持-mega-init-function">C：保持 mega init function</h3>
<ul>
<li><strong>機制</strong>：所有 setup 邏輯塞在一個 init 內</li>
<li><strong>跟 A 的取捨</strong>：C 一個函式看完所有 setup、A 散在多函式；但 C 在 200+ 行時改一處要小心整體</li>
<li><strong>C 才合理的情境</strong>：&lt; 50 行的 init、職責本來就單一</li>
</ul>
<h3 id="d按職責拆--class-包裝多實例">D：按職責拆 + class 包裝多實例</h3>
<ul>
<li><strong>機制</strong>：把 setup 包成 class、<code>new SearchShell(rootEl)</code> 建立實例</li>
<li><strong>跟 A 的取捨</strong>：D 多實例支援更乾淨（每實例自己的 state）、A 用純函式 + 起點當參數也能達成</li>
<li><strong>D 比 A 好的情境</strong>：元件有複雜的內部 state、預期會被多次實例化（library 設計）</li>
</ul>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>Refactor 動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>一個函式 100+ 行</td>
          <td>列出做的事、按職責拆</td>
      </tr>
      <tr>
          <td>函式名抽象（<code>init</code> / <code>handle</code> / <code>process</code>）</td>
          <td>改名動詞 + 對象、表達 intent</td>
      </tr>
      <tr>
          <td>函式內讀外部全局變數</td>
          <td>把依賴改為參數、純函式化</td>
      </tr>
      <tr>
          <td>Debug 時要 grep 整個函式找哪段邏輯</td>
          <td>拆完後職責 = 函式 = grep 範圍縮小</td>
      </tr>
      <tr>
          <td>同一段邏輯複製到別處</td>
          <td>拆成獨立函式、兩處引用</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：函式是「做一件事」的單位。一個函式越多職責、debug 與重用越難。拆 = 投資、回報是未來的維護成本下降。</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></channel></rss>