<?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>外部組件 on Tarragon</title><link>https://tarrragon.github.io/blog/tags/%E5%A4%96%E9%83%A8%E7%B5%84%E4%BB%B6/</link><description>Recent content in 外部組件 on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Sun, 26 Apr 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/%E5%A4%96%E9%83%A8%E7%B5%84%E4%BB%B6/index.xml" rel="self" type="application/rss+xml"/><item><title>Framework Coexistence — 跟 framework-managed DOM 共處</title><link>https://tarrragon.github.io/blog/skills/frontend-with-playwright/framework-coexistence/</link><pubDate>Sun, 26 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/skills/frontend-with-playwright/framework-coexistence/</guid><description>&lt;p>跟 framework-managed DOM 共處：把 framework 子樹當禁區、客製 UI 注入到 boundary 外、JS 操作邊界由穩定性梯度決定、外部組件客製優先用公共介面。&lt;/p>
&lt;p>適用：跟 vendor library / framework component（pagefind、Vue widget、React component、jQuery plugin）共存的客製、注入客製 UI、覆寫 vendor 樣式。
不適用：完全自家寫的元件（沒有 framework 介入）。&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>自包含聲明&lt;/strong>：閱讀本文件不需要先讀其他 reference。本文件涵蓋 framework 邊界辨識、JS 操作的四級安全度、外部組件客製的四層合作。&lt;/p>&lt;/blockquote>
&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>客製 UI 注入到 framework 子樹後被還原&lt;/td>
 &lt;td>移到 framework 邊界外、用 CSS 控制視覺位置&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Vendor library 升級後客製樣式失效&lt;/td>
 &lt;td>改用公共介面（CSS var / API）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>不確定 reparent / 改 attribute / 改 textContent 哪個安全&lt;/td>
 &lt;td>看下方「JS 操作四級安全度」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>客製需求看似簡單但要動 framework 內部結構&lt;/td>
 &lt;td>評估「值不值得」、把成本攤開（見 cost report）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>寫了 MutationObserver 補 framework reconcile 後元素還原&lt;/td>
 &lt;td>換思路：注入到邊界外、不需要 observer&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="為什麼-framework-managed-dom-要當禁區">為什麼 framework-managed DOM 要當禁區&lt;/h2>
&lt;p>Framework（React / Vue / vendor JS widget）對它管的 DOM 子樹有&lt;strong>所有權&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>State 變動觸發 reconciliation、子樹重建&lt;/li>
&lt;li>我們改的 attribute / textContent / 子節點被還原&lt;/li>
&lt;li>innerHTML 改動可能觸發 Vue / React 的 dev mode 警告&lt;/li>
&lt;li>Event listener 失效（節點被替換）&lt;/li>
&lt;/ul>
&lt;p>把客製 UI &lt;strong>注入到 framework 邊界外&lt;/strong>（vendor root 的 sibling、或上一層 container 的另一個 child）→ framework 不管它 → 不會被還原。&lt;/p>
&lt;hr>
&lt;h2 id="framework-邊界的識別">Framework 邊界的識別&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>&amp;lt;div id=&amp;quot;app&amp;quot;&amp;gt;&lt;/code> / &lt;code>data-vue-component&lt;/code>&lt;/td>
 &lt;td>Vue / 自家 framework 的 root&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>&amp;lt;div data-reactroot&amp;gt;&lt;/code> / React Fiber 結構&lt;/td>
 &lt;td>React 的 root&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>.pagefind-ui&lt;/code> / &lt;code>.algolia-search&lt;/code> 等命名空間&lt;/td>
 &lt;td>Vendor library 的 root&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>子節點 attribute 含 &lt;code>__data&lt;/code> &lt;code>__key&lt;/code> 等內部標記&lt;/td>
 &lt;td>Framework 內部結構、子節點被管理&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>邊界外的 sibling / parent 通常是「自家 HTML」、安全。&lt;/p>
&lt;h3 id="範例pagefind-的邊界">範例：Pagefind 的邊界&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">div&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;search-page&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span> ← 自家 (邊界外、可控)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl"> &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">h1&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>Search&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">h1&lt;/span>&lt;span class="p">&amp;gt;&lt;/span> ← 自家
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">div&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;custom-filter&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span> ← 自家、客製 UI 放這裡
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> &lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">div&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">div&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;pagefind-ui&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span> ← Vendor root (邊界、入內就是禁區)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">form&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;pagefind-ui__form&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span> ← Pagefind 管
&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">input&lt;/span> &lt;span class="err">...&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 class="p">&amp;lt;&lt;/span>&lt;span class="nt">div&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;pagefind-ui__drawer&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span> ← Pagefind 管（重渲染時清空）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">div&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> &lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">form&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl"> &lt;span 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">12&lt;/span>&lt;span class="cl">&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">div&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>.custom-filter&lt;/code> 跟 &lt;code>.pagefind-ui&lt;/code> 是 sibling、不在 vendor 子樹內 → 用 CSS grid / absolute 定位讓它看起來在 search 流程內、但實際 framework 不管它。&lt;/p></description><content:encoded><![CDATA[<p>跟 framework-managed DOM 共處：把 framework 子樹當禁區、客製 UI 注入到 boundary 外、JS 操作邊界由穩定性梯度決定、外部組件客製優先用公共介面。</p>
<p>適用：跟 vendor library / framework component（pagefind、Vue widget、React component、jQuery plugin）共存的客製、注入客製 UI、覆寫 vendor 樣式。
不適用：完全自家寫的元件（沒有 framework 介入）。</p>
<blockquote>
<p><strong>自包含聲明</strong>：閱讀本文件不需要先讀其他 reference。本文件涵蓋 framework 邊界辨識、JS 操作的四級安全度、外部組件客製的四層合作。</p></blockquote>
<hr>
<h2 id="何時參閱本文件">何時參閱本文件</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的第一件事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>客製 UI 注入到 framework 子樹後被還原</td>
          <td>移到 framework 邊界外、用 CSS 控制視覺位置</td>
      </tr>
      <tr>
          <td>Vendor library 升級後客製樣式失效</td>
          <td>改用公共介面（CSS var / API）</td>
      </tr>
      <tr>
          <td>不確定 reparent / 改 attribute / 改 textContent 哪個安全</td>
          <td>看下方「JS 操作四級安全度」</td>
      </tr>
      <tr>
          <td>客製需求看似簡單但要動 framework 內部結構</td>
          <td>評估「值不值得」、把成本攤開（見 cost report）</td>
      </tr>
      <tr>
          <td>寫了 MutationObserver 補 framework reconcile 後元素還原</td>
          <td>換思路：注入到邊界外、不需要 observer</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="為什麼-framework-managed-dom-要當禁區">為什麼 framework-managed DOM 要當禁區</h2>
<p>Framework（React / Vue / vendor JS widget）對它管的 DOM 子樹有<strong>所有權</strong>：</p>
<ul>
<li>State 變動觸發 reconciliation、子樹重建</li>
<li>我們改的 attribute / textContent / 子節點被還原</li>
<li>innerHTML 改動可能觸發 Vue / React 的 dev mode 警告</li>
<li>Event listener 失效（節點被替換）</li>
</ul>
<p>把客製 UI <strong>注入到 framework 邊界外</strong>（vendor root 的 sibling、或上一層 container 的另一個 child）→ framework 不管它 → 不會被還原。</p>
<hr>
<h2 id="framework-邊界的識別">Framework 邊界的識別</h2>
<h3 id="邊界的可見訊號">邊界的可見訊號</h3>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>含義</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>&lt;div id=&quot;app&quot;&gt;</code> / <code>data-vue-component</code></td>
          <td>Vue / 自家 framework 的 root</td>
      </tr>
      <tr>
          <td><code>&lt;div data-reactroot&gt;</code> / React Fiber 結構</td>
          <td>React 的 root</td>
      </tr>
      <tr>
          <td><code>.pagefind-ui</code> / <code>.algolia-search</code> 等命名空間</td>
          <td>Vendor library 的 root</td>
      </tr>
      <tr>
          <td>子節點 attribute 含 <code>__data</code> <code>__key</code> 等內部標記</td>
          <td>Framework 內部結構、子節點被管理</td>
      </tr>
  </tbody>
</table>
<p>邊界外的 sibling / parent 通常是「自家 HTML」、安全。</p>
<h3 id="範例pagefind-的邊界">範例：Pagefind 的邊界</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;search-page&#34;</span><span class="p">&gt;</span>           ← 自家 (邊界外、可控)
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="p">&lt;</span><span class="nt">h1</span><span class="p">&gt;</span>Search<span class="p">&lt;/</span><span class="nt">h1</span><span class="p">&gt;</span>                   ← 自家
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="p">&lt;</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;custom-filter&#34;</span><span class="p">&gt;</span>       ← 自家、客製 UI 放這裡
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  <span class="p">&lt;</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;pagefind-ui&#34;</span><span class="p">&gt;</span>          ← Vendor root (邊界、入內就是禁區)
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="p">&lt;</span><span class="nt">form</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;pagefind-ui__form&#34;</span><span class="p">&gt;</span> ← Pagefind 管
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">      <span class="p">&lt;</span><span class="nt">input</span> <span class="err">...</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln"> 8</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__drawer&#34;</span><span class="p">&gt;</span>  ← Pagefind 管（重渲染時清空）
</span></span><span class="line"><span class="ln"> 9</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">10</span><span class="cl">    <span class="p">&lt;/</span><span class="nt">form</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">11</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">12</span><span class="cl"><span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span></span></span></code></pre></div><p><code>.custom-filter</code> 跟 <code>.pagefind-ui</code> 是 sibling、不在 vendor 子樹內 → 用 CSS grid / absolute 定位讓它看起來在 search 流程內、但實際 framework 不管它。</p>
<hr>
<h2 id="js-操作的四級安全度">JS 操作的四級安全度</h2>
<p>對 framework-managed 元素的操作、按穩定性排序：</p>
<table>
  <thead>
      <tr>
          <th>操作</th>
          <th>安全度</th>
          <th>為什麼</th>
          <th>補救</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Reparent 整節點</td>
          <td>高</td>
          <td>整節點搬遷、framework 通常不會還原</td>
          <td>-</td>
      </tr>
      <tr>
          <td>改 inline style</td>
          <td>中-高</td>
          <td>Style 通常不被 reconcile（除非 framework 重設）</td>
          <td>用 CSS class 取代</td>
      </tr>
      <tr>
          <td>改 attribute</td>
          <td>中</td>
          <td>部分 framework 會 reconcile attribute</td>
          <td>用 MutationObserver 補回</td>
      </tr>
      <tr>
          <td>改 textContent</td>
          <td>中-低</td>
          <td>多數 framework 會 reconcile text</td>
          <td>改注入新節點到邊界外</td>
      </tr>
      <tr>
          <td>改 innerHTML</td>
          <td>低</td>
          <td>子節點全重建、event listener 失效</td>
          <td>不要改、用其他方法</td>
      </tr>
      <tr>
          <td>改 framework 子節點</td>
          <td>極低</td>
          <td>reconcile 還原、可能 dev warning</td>
          <td>不要動</td>
      </tr>
  </tbody>
</table>
<p>選擇規則：<strong>從最高安全度起步、不行才升級</strong>。</p>
<hr>
<h2 id="客製-ui-注入的兩種模式">客製 UI 注入的兩種模式</h2>
<h3 id="模式-1注入到-framework-邊界外推薦">模式 1：注入到 framework 邊界外（推薦）</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="kr">const</span> <span class="nx">customEl</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="s1">&#39;div&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nx">customEl</span><span class="p">.</span><span class="nx">className</span> <span class="o">=</span> <span class="s1">&#39;custom-filter&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nx">customEl</span><span class="p">.</span><span class="nx">textContent</span> <span class="o">=</span> <span class="s1">&#39;Filter: All / Title / Content&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.search-page&#39;</span><span class="p">).</span><span class="nx">appendChild</span><span class="p">(</span><span class="nx">customEl</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1">// 注意：appendChild 到 .search-page、不是 .pagefind-ui
</span></span></span></code></pre></div>




<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-page</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="k">display</span><span class="p">:</span> <span class="k">grid</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="k">grid-template</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="s2">&#34;h1&#34;</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="s2">&#34;form&#34;</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="s2">&#34;custom-filter&#34;</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="s2">&#34;results&#34;</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="nc">pagefind-ui</span> <span class="p">{</span> <span class="k">grid-area</span><span class="p">:</span> <span class="n">form</span> <span class="o">/</span> <span class="n">form</span> <span class="o">/</span> <span class="n">results</span> <span class="o">/</span> <span class="n">results</span><span class="p">;</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">custom-filter</span> <span class="p">{</span> <span class="k">grid-area</span><span class="p">:</span> <span class="n">custom-filter</span><span class="p">;</span> <span class="p">}</span></span></span></code></pre></div><p>CSS grid 把客製 UI 排到 search 流程的某個位置、framework 不知情、不還原。</p>
<h3 id="模式-2reparent-framework-內節點次優">模式 2：reparent framework 內節點（次優）</h3>
<p>如果客製需要把 framework 內的某個元素移到別處 — reparent 整節點而不是改內部：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="kr">const</span> <span class="nx">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__filters&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="kr">const</span> <span class="nx">target</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;.sidebar&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nx">target</span><span class="p">.</span><span class="nx">appendChild</span><span class="p">(</span><span class="nx">filter</span><span class="p">);</span>  <span class="c1">// 整節點搬到 sidebar、不複製
</span></span></span></code></pre></div><p>整節點搬遷通常 framework 不會「還原」、因為 vDOM diff 看到 node 還在（只是 parent 變了）。</p>
<p>但有 case 例外（部分 framework 用 portal pattern、reparent 會被視為 unmount）→ 第 1 次嘗試後用 playwright 驗證行為、第 2 次失敗就停（見 requirement-protocol/failure-pivot-protocol）。</p>
<hr>
<h2 id="外部組件客製的四層合作穩定性梯度">外部組件客製的四層合作（穩定性梯度）</h2>
<p>跟外部組件合作時、選哪一層客製、決定升級時會不會壞。</p>
<table>
  <thead>
      <tr>
          <th>層</th>
          <th>範例</th>
          <th>升級穩定性</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>公共介面層</td>
          <td>CLI 參數、CSS variable、option 物件</td>
          <td>最高</td>
      </tr>
      <tr>
          <td>邊界層</td>
          <td>注入 root 的 sibling、用 CSS 包邊界外</td>
          <td>高</td>
      </tr>
      <tr>
          <td>邊界 DOM 層</td>
          <td>querySelector vendor 的 root 節點</td>
          <td>中</td>
      </tr>
      <tr>
          <td>內部結構層</td>
          <td>改 vendor 子節點 attribute / 樣式</td>
          <td>最低</td>
      </tr>
  </tbody>
</table>
<p><strong>選擇順序</strong>：先看公共介面有沒有提供（讀 docs）、沒有再用邊界層、再不行才碰邊界 DOM。內部結構層幾乎不要碰 — 升級時 minor version 都會壞。</p>
<h3 id="範例pagefind-的客製優先順序">範例：Pagefind 的客製優先順序</h3>
<table>
  <thead>
      <tr>
          <th>需求</th>
          <th>優先做法</th>
          <th>次選</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>改主題色</td>
          <td>公共：<code>--pagefind-ui-primary</code> CSS var</td>
          <td>邊界 DOM：覆寫 <code>.pagefind-ui__form</code></td>
      </tr>
      <tr>
          <td>加 filter UI</td>
          <td>邊界：在 <code>.pagefind-ui</code> sibling 注入</td>
          <td>內部：塞進 <code>.pagefind-ui__form</code> 內</td>
      </tr>
      <tr>
          <td>限定 search scope</td>
          <td>公共：<code>pagefindOptions.scope: 'main'</code></td>
          <td>內部：MutationObserver 過濾結果</td>
      </tr>
      <tr>
          <td>改 result 卡片排版</td>
          <td>邊界 DOM：覆寫 <code>.pagefind-ui__result</code> CSS（接受升級時可能要重檢）</td>
          <td>-</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="wrong-vs-right-對照">Wrong vs Right 對照</h2>
<h3 id="範例-1客製-filter-ui">範例 1：客製 filter UI</h3>
<p><strong>錯</strong>（注入到 vendor 子樹內）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="kr">const</span> <span class="nx">filter</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="s1">&#39;div&#39;</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="p">.</span><span class="nx">textContent</span> <span class="o">=</span> <span class="s1">&#39;Filter: ...&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui__form&#39;</span><span class="p">).</span><span class="nx">appendChild</span><span class="p">(</span><span class="nx">filter</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1">// → search 觸發 → form 重渲染 → filter 消失
</span></span></span></code></pre></div><p><strong>對</strong>（注入到邊界外）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="kr">const</span> <span class="nx">filter</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="s1">&#39;div&#39;</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="p">.</span><span class="nx">className</span> <span class="o">=</span> <span class="s1">&#39;custom-filter&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nx">filter</span><span class="p">.</span><span class="nx">textContent</span> <span class="o">=</span> <span class="s1">&#39;Filter: ...&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.search-page&#39;</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></code></pre></div>




<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-page</span> <span class="p">{</span> <span class="k">display</span><span class="p">:</span> <span class="k">grid</span><span class="p">;</span> <span class="k">grid-template-areas</span><span class="p">:</span> <span class="s2">&#34;h1&#34;</span> <span class="s2">&#34;form&#34;</span> <span class="s2">&#34;filter&#34;</span> <span class="s2">&#34;results&#34;</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</span> <span class="p">{</span> <span class="k">grid-area</span><span class="p">:</span> <span class="n">form</span> <span class="o">/</span> <span class="n">form</span> <span class="o">/</span> <span class="n">results</span> <span class="o">/</span> <span class="n">results</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">custom-filter</span> <span class="p">{</span> <span class="k">grid-area</span><span class="p">:</span> <span class="k">filter</span><span class="p">;</span> <span class="p">}</span></span></span></code></pre></div><h3 id="範例-2改-vendor-主題色">範例 2：改 vendor 主題色</h3>
<p><strong>錯</strong>：</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__form</span> <span class="p">{</span> <span class="k">background</span><span class="p">:</span> <span class="kc">blue</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__search-input</span> <span class="p">{</span> <span class="k">color</span><span class="p">:</span> <span class="kc">white</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">pagefind-ui__button</span> <span class="p">{</span> <span class="k">background</span><span class="p">:</span> <span class="kc">darkblue</span> <span class="cp">!important</span><span class="p">;</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c">/* ... 8 條 important */</span></span></span></code></pre></div><p>升級後 class 改名 → 全壞。</p>
<p><strong>對</strong>：</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></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="nv">--pagefind-ui-primary</span><span class="p">:</span> <span class="mh">#2c5282</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="nv">--pagefind-ui-text</span><span class="p">:</span> <span class="mh">#fff</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="nv">--pagefind-ui-background</span><span class="p">:</span> <span class="mh">#1a202c</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>讀 vendor docs、用提供的 CSS var。升級安全、5 行解決。</p>
<h3 id="範例-3把-vendor-filter-移到-sidebar">範例 3：把 vendor filter 移到 sidebar</h3>
<p><strong>錯</strong>（複製 + 同步）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="kr">const</span> <span class="nx">original</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__filters&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="kr">const</span> <span class="nx">clone</span> <span class="o">=</span> <span class="nx">original</span><span class="p">.</span><span class="nx">cloneNode</span><span class="p">(</span><span class="kc">true</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nx">sidebar</span><span class="p">.</span><span class="nx">appendChild</span><span class="p">(</span><span class="nx">clone</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1">// → 兩份、state 不同步、click 事件 listener 沒複製
</span></span></span></code></pre></div><p><strong>對</strong>（reparent 整節點）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="kr">const</span> <span class="nx">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__filters&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nx">sidebar</span><span class="p">.</span><span class="nx">appendChild</span><span class="p">(</span><span class="nx">filter</span><span class="p">);</span>  <span class="c1">// 整節點搬遷、event listener 跟著、state 唯一
</span></span></span></code></pre></div><p>整節點搬遷通常安全 — vDOM 看到 node 還在、不會 reconcile。寫完先用 playwright 驗證行為（dispatch input / click 看 filter 是否還工作）。</p>
<hr>
<h2 id="自檢清單dogfooding">自檢清單（dogfooding）</h2>
<p>跟 framework / vendor library 共處時：</p>
<ul>
<li><input disabled="" type="checkbox"> 我有沒有先看 vendor docs、確認有沒有公共介面（CSS var / API）？</li>
<li><input disabled="" type="checkbox"> 客製 UI 是注入到 framework 邊界外、還是內部？</li>
<li><input disabled="" type="checkbox"> JS 操作的元素是 framework 管的子節點嗎？如果是、有沒有用「四級安全度」最高的操作？</li>
<li><input disabled="" type="checkbox"> reparent / 改 attribute 後、有沒有用 playwright 驗證 framework 沒還原？</li>
<li><input disabled="" type="checkbox"> 升級風險有攤給使用者嗎？（見 requirement-protocol/cost-and-checkpoint）</li>
</ul>
<hr>
<h2 id="延伸閱讀">延伸閱讀</h2>
<p>對應的事後檢討（在 <code>content/report/</code>）：</p>
<ul>
<li><a href="/blog/report/coexisting-with-framework-managed-dom/" data-link-title="客製 UI 留 framework 邊界外、用 CSS 控制視覺位置" data-link-desc="Svelte / React 等框架對自己管轄的 DOM 子樹有完整渲染週期 — 客製 UI 注入這個子樹會被框架重繪清掉。本文展開「邊界外 &#43; CSS 視覺定位」這條策略：為什麼 framework 會清外來節點、CSS 怎麼達到注入想要的視覺效果、什麼時候這條策略不適用。">coexisting-with-framework-managed-dom</a> — 客製 UI 留 framework 邊界外、用 CSS 控制視覺位置</li>
<li><a href="/blog/report/component-boundary-and-js-impact/" data-link-title="JS 操作 framework 元件：邊界辨識與安全規則" data-link-desc="操作 framework 管的 DOM 前先界定『動什麼、邊界在哪、state 由誰維護』。本文展開邊界辨識的決策樹、整節點 reparent 的具體 do/don&#39;t 規則、灰區操作的 fail-safe 設計。">component-boundary-and-js-impact</a> — JS 操作 framework 元件的邊界辨識</li>
<li><a href="/blog/report/external-component-customization/" data-link-title="在外部組件上加客製功能：以邊界為中心的方法選擇" data-link-desc="客製外部組件穩定的程度取決於『離組件邊界多近』。本文用 Pagefind 整合到 Hugo theme 的三個情境（索引邊界、重置邊界、specificity 邊界）展開：在邊界上客製為什麼穩、各種替代方案的不足、以及下次提早辨識的訊號。">external-component-customization</a> — 在外部組件上加客製功能：以邊界為中心</li>
<li><a href="/blog/report/external-component-collaboration-layers/" data-link-title="跟外部組件合作的層次：離介面越近、合作越穩" data-link-desc="客製外部組件的穩定性與「離組件作者保證的對外介面多遠」成反比。每往內推一層、依賴前提增加、升級風險上升、可逆性下降。本文是 #1 / #5 / #19 / #24 四篇實作的共同抽象。">external-component-collaboration-layers</a> — 跟外部組件合作的四層次</li>
</ul>
<hr>
<p><strong>Last Updated</strong>: 2026-04-26
<strong>Version</strong>: 0.1.0</p>
]]></content:encoded></item></channel></rss>