<?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>Chrome-Extension on Tarragon</title><link>https://tarrragon.github.io/blog/tags/chrome-extension/</link><description>Recent content in Chrome-Extension on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Fri, 17 Jul 2026 00:00:00 +0800</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/chrome-extension/index.xml" rel="self" type="application/rss+xml"/><item><title>U.C9 提取成功卻誤報失敗 — 結果通知鏈路被搶通道</title><link>https://tarrragon.github.io/blog/ux-design/cases/async-listener-false-failure/</link><pubDate>Fri, 17 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/ux-design/cases/async-listener-false-failure/</guid><description>&lt;p>這個案例的核心責任是說明「結果通知」不只是 UI 呈現問題 — 結果從執行端傳回 UI 的鏈路本身會斷、會錯，而鏈路故障的最壞形態是誠實度反轉：操作成功、UI 報失敗。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>電子書庫總覽 Chrome 擴充功能（book_overview_v1，Manifest V3 — Chrome 擴充平台的現行規格版本）的提取流程：popup 觸發 content script 擷取書目、寫入 storage、回報結果。實機測試中 content script 成功提取 96 本且 storage 已寫入，popup UI 卻顯示「提取失敗 / 未知錯誤」。&lt;/p>
&lt;p>根因在訊息通道語意（commit &lt;code>97c24b2b6&lt;/code>）：橋接模組的 message listener 宣告成 async。收到非它負責的訊息型別時函式體直接結束，async 函式回傳 &lt;code>Promise.resolve(undefined)&lt;/code> — Manifest V3 把「listener 回傳 Promise」解讀為「此 listener 負責回應」，把 undefined 搶先送回 popup，與真正處理該訊息的 listener 競爭。popup 拿到 undefined、走 else 分支拋「未知錯誤」。&lt;/p>
&lt;p>修復：listener 改為同步函式，非處理訊息回傳 undefined（不搶通道）、async 邏輯抽成 fire-and-forget handler。&lt;/p>
&lt;p>同一條鏈路的另一個斷點（commit &lt;code>a62ce00d8&lt;/code>）：訊息路由的 handler 沒被注入、事件協調器沒被啟動，&lt;code>EXTRACTION.COMPLETED&lt;/code> 事件沒有任何訂閱者 — 提取完成但資料未儲存、popup 連線失敗。鏈路斷的位置不同（組裝遺漏 vs 通道語意），使用者看到的都是「操作沒有結果」。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>結果通知有一個隱含前提：結果會正確到達 UI&lt;/strong>。回饋設計通常聚焦「到達之後怎麼呈現」（訊息形式、通知元件），但 popup / content script / service worker 是三個獨立 context，結果要跨兩次訊息通道才到 UI — 每一跳都是回饋鏈路的一部分，通道語意錯誤等於回饋設計全部白做。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>成功誤報失敗比沒有回饋更糟&lt;/strong>。零回饋讓使用者困惑；誠實度反轉讓使用者採取錯誤行動 — 重做一次提取（資料重複）、回報 bug、放棄使用。UI 的宣告與系統實際狀態相反時，使用者對介面的每一個訊息都會失去信任。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>通道語意是平台契約、不是實作細節&lt;/strong>。「listener 回傳 Promise = 認領回應權」是 Manifest V3 在較新版 Chrome 的行為（更早版本會忽略回傳的 Promise、通道靜默關閉 — 兩種行為下，共享通道上的 async listener 都是錯的），混用 async 語法糖與訊息協定就會觸發。這類契約在單元測試中不可見（mock 掉通道）、只有整合層才會現形。&lt;/p>
&lt;/li>
&lt;/ol>
&lt;h2 id="策略">策略&lt;/h2>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>結果通知鏈路要有端對端驗證&lt;/strong>：「操作成功 → UI 顯示成功」作為整合測試斷言，涵蓋跨 context 的完整鏈路，不只測 UI 元件收到資料後的呈現。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>通道上的每個 listener 明確宣告回應權&lt;/strong>：負責回應的同步回傳 true 保持通道、不負責的不回傳 — async listener 在共享通道上是候選錯誤。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>事件鏈路的組裝有清單&lt;/strong>：事件的發佈者與訂閱者在啟動時逐一核對（handler 已注入、coordinator 已啟動），組裝遺漏讓事件無聲消失。&lt;/p>
&lt;/li>
&lt;/ol>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>結果通知該呈現什麼 → &lt;a href="https://tarrragon.github.io/blog/ux-design/06-interaction-feedback/feedback-three-layers/" data-link-title="互動回饋三層模型：點擊確認、等待指示、結果通知" data-link-desc="使用者操作後的回饋依時間分層，缺層的症狀是重複提交與重複導航 — 診斷「按了沒反應」與多步驟流程卡狀態問題的檢查框架，涵蓋按鈕級與畫面級兩個尺度。">互動回饋三層模型&lt;/a>&lt;/li>
&lt;li>類似案例（UI 未接線、三層回饋全缺）→ &lt;a href="https://tarrragon.github.io/blog/ux-design/cases/export-button-zero-feedback/" data-link-title="U.C5 匯出按鈕按下零回饋 — 狀態機完備但 UI 沒接線" data-link-desc="Flutter app 匯出設定頁的確認匯出按鈕 onPressed 是空 callback，按下畫面毫無變化 — 使用者無法分辨匯出成功、進行中、還是功能根本沒做。ViewModel 的 idle/inProgress/completed/failed 狀態機早已完備，缺的只是頁面接線與三層回饋">U.C5 匯出按鈕零回饋&lt;/a>&lt;/li>
&lt;li>完成宣告的證據強度 → &lt;a href="https://tarrragon.github.io/blog/ux-design/cases/lazy-load-premature-completion/" data-link-title="U.C11 抓到 96/928 本就顯示完成 — 完成判定的證據強度不足" data-link-desc="批次 / 遍歷類操作宣告完成、實際只處理了一部分：「連續 N 輪沒有變化」是暫時停滯的訊號、不是窮盡的證據 — 完成判定需要獨立的窮盡證據（總數對照、終止標記）">U.C11 抓到 96/928 本就顯示完成&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明「結果通知」不只是 UI 呈現問題 — 結果從執行端傳回 UI 的鏈路本身會斷、會錯，而鏈路故障的最壞形態是誠實度反轉：操作成功、UI 報失敗。</p>
<h2 id="觀察">觀察</h2>
<p>電子書庫總覽 Chrome 擴充功能（book_overview_v1，Manifest V3 — Chrome 擴充平台的現行規格版本）的提取流程：popup 觸發 content script 擷取書目、寫入 storage、回報結果。實機測試中 content script 成功提取 96 本且 storage 已寫入，popup UI 卻顯示「提取失敗 / 未知錯誤」。</p>
<p>根因在訊息通道語意（commit <code>97c24b2b6</code>）：橋接模組的 message listener 宣告成 async。收到非它負責的訊息型別時函式體直接結束，async 函式回傳 <code>Promise.resolve(undefined)</code> — Manifest V3 把「listener 回傳 Promise」解讀為「此 listener 負責回應」，把 undefined 搶先送回 popup，與真正處理該訊息的 listener 競爭。popup 拿到 undefined、走 else 分支拋「未知錯誤」。</p>
<p>修復：listener 改為同步函式，非處理訊息回傳 undefined（不搶通道）、async 邏輯抽成 fire-and-forget handler。</p>
<p>同一條鏈路的另一個斷點（commit <code>a62ce00d8</code>）：訊息路由的 handler 沒被注入、事件協調器沒被啟動，<code>EXTRACTION.COMPLETED</code> 事件沒有任何訂閱者 — 提取完成但資料未儲存、popup 連線失敗。鏈路斷的位置不同（組裝遺漏 vs 通道語意），使用者看到的都是「操作沒有結果」。</p>
<h2 id="判讀">判讀</h2>
<ol>
<li>
<p><strong>結果通知有一個隱含前提：結果會正確到達 UI</strong>。回饋設計通常聚焦「到達之後怎麼呈現」（訊息形式、通知元件），但 popup / content script / service worker 是三個獨立 context，結果要跨兩次訊息通道才到 UI — 每一跳都是回饋鏈路的一部分，通道語意錯誤等於回饋設計全部白做。</p>
</li>
<li>
<p><strong>成功誤報失敗比沒有回饋更糟</strong>。零回饋讓使用者困惑；誠實度反轉讓使用者採取錯誤行動 — 重做一次提取（資料重複）、回報 bug、放棄使用。UI 的宣告與系統實際狀態相反時，使用者對介面的每一個訊息都會失去信任。</p>
</li>
<li>
<p><strong>通道語意是平台契約、不是實作細節</strong>。「listener 回傳 Promise = 認領回應權」是 Manifest V3 在較新版 Chrome 的行為（更早版本會忽略回傳的 Promise、通道靜默關閉 — 兩種行為下，共享通道上的 async listener 都是錯的），混用 async 語法糖與訊息協定就會觸發。這類契約在單元測試中不可見（mock 掉通道）、只有整合層才會現形。</p>
</li>
</ol>
<h2 id="策略">策略</h2>
<ol>
<li>
<p><strong>結果通知鏈路要有端對端驗證</strong>：「操作成功 → UI 顯示成功」作為整合測試斷言，涵蓋跨 context 的完整鏈路，不只測 UI 元件收到資料後的呈現。</p>
</li>
<li>
<p><strong>通道上的每個 listener 明確宣告回應權</strong>：負責回應的同步回傳 true 保持通道、不負責的不回傳 — async listener 在共享通道上是候選錯誤。</p>
</li>
<li>
<p><strong>事件鏈路的組裝有清單</strong>：事件的發佈者與訂閱者在啟動時逐一核對（handler 已注入、coordinator 已啟動），組裝遺漏讓事件無聲消失。</p>
</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>結果通知該呈現什麼 → <a href="/blog/ux-design/06-interaction-feedback/feedback-three-layers/" data-link-title="互動回饋三層模型：點擊確認、等待指示、結果通知" data-link-desc="使用者操作後的回饋依時間分層，缺層的症狀是重複提交與重複導航 — 診斷「按了沒反應」與多步驟流程卡狀態問題的檢查框架，涵蓋按鈕級與畫面級兩個尺度。">互動回饋三層模型</a></li>
<li>類似案例（UI 未接線、三層回饋全缺）→ <a href="/blog/ux-design/cases/export-button-zero-feedback/" data-link-title="U.C5 匯出按鈕按下零回饋 — 狀態機完備但 UI 沒接線" data-link-desc="Flutter app 匯出設定頁的確認匯出按鈕 onPressed 是空 callback，按下畫面毫無變化 — 使用者無法分辨匯出成功、進行中、還是功能根本沒做。ViewModel 的 idle/inProgress/completed/failed 狀態機早已完備，缺的只是頁面接線與三層回饋">U.C5 匯出按鈕零回饋</a></li>
<li>完成宣告的證據強度 → <a href="/blog/ux-design/cases/lazy-load-premature-completion/" data-link-title="U.C11 抓到 96/928 本就顯示完成 — 完成判定的證據強度不足" data-link-desc="批次 / 遍歷類操作宣告完成、實際只處理了一部分：「連續 N 輪沒有變化」是暫時停滯的訊號、不是窮盡的證據 — 完成判定需要獨立的窮盡證據（總數對照、終止標記）">U.C11 抓到 96/928 本就顯示完成</a></li>
</ul>
]]></content:encoded></item><item><title>U.C10 Service Worker 冷啟動期間的假離線 — initializing 狀態未建模</title><link>https://tarrragon.github.io/blog/ux-design/cases/service-worker-cold-start-false-offline/</link><pubDate>Fri, 17 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/ux-design/cases/service-worker-cold-start-false-offline/</guid><description>&lt;p>查詢對象有自己的生命週期時，「還不知道對方狀態」（initializing / unknown）是一個真實狀態 — 把它與「離線」「錯誤」合併，查詢對象醒得慢的那一次就會被呈現成假離線。這張卡記錄&lt;a href="https://tarrragon.github.io/blog/ux-design/knowledge-cards/screen-state-matrix/" data-link-title="Screen State Matrix（畫面狀態矩陣）" data-link-desc="說明用四欄表格（顯示/可用操作/進入條件/退出路徑）系統性地暴露畫面導航缺口的設計工具">畫面狀態矩陣&lt;/a>列狀態時的這個系統性遺漏。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>電子書庫總覽 Chrome 擴充功能（book_overview_v1）的 popup 開啟時向 background service worker 查詢狀態。Manifest V3 的 service worker 是事件驅動、閒置即卸載 — popup 開啟的瞬間 SW 可能正在冷啟動、初始化未完成、不回應 &lt;code>GET_STATUS&lt;/code>。popup 等待期間顯示「正在檢查狀態&amp;hellip;」，2 秒 timeout 後轉為「離線」且不再自動恢復 — 冷啟動偶爾超過 2 秒（極端 I/O、低階裝置，低頻但真實發生過）時，系統實際正常、使用者看到的卻是永久離線（&lt;code>src/background/background.js:272-284&lt;/code>，ticket 1.1.0-W1-019）。&lt;/p>
&lt;p>修復採雙管：查詢端加握手重試，加上被查詢端在初始化期間就回應 baseline 的 &lt;code>initializing&lt;/code> 狀態 — popup 據此顯示「初始化中」而非落入 timeout 判離線。&lt;/p>
&lt;p>同專案的另一起同型事故（commit &lt;code>86216c37f&lt;/code>）：popup 的書籍偵測數硬編「檢測中&amp;hellip;」、從未讀取健康查詢回應中的實際數字 — 過渡狀態的顯示寫死了、永遠停在過渡態。兩個事故一體兩面：一個把「還不知道」誤顯示成終態（離線）、一個把終態永遠顯示成「還不知道」。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>「還不知道」與「不可用」是不同狀態&lt;/strong>。離線 / 錯誤是查詢得到的答案，initializing 是還沒得到答案。合併兩者的畫面會把「這次醒得慢」定格成永久離線 — 頻率低不減輕代價，使用者據此做錯誤決策：放棄操作、重裝、回報故障。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>查詢對象的生命週期決定 initializing 是否必要&lt;/strong>。查詢對象與畫面同生命週期（同 process 的本地狀態）時不需要；查詢對象獨立生死（service worker、遠端服務、另一個 process、外部裝置）時，畫面開啟瞬間對方「還沒醒」是常態而非邊角 — initializing 必須是狀態矩陣裡的一行，有自己的顯示、操作與退出路徑。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>timeout 是 initializing 的退出路徑&lt;/strong>。「初始化中」不能無限停留 — 超過合理時間仍無回應才轉入離線 / 錯誤狀態。順序是 initializing → (回應) 正常態 / (timeout) 離線，而非直接顯示離線等回應來救。&lt;/p>
&lt;/li>
&lt;/ol>
&lt;h2 id="策略">策略&lt;/h2>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>列狀態時多問一句&lt;/strong>：這個畫面查詢的對象，跟畫面同生命週期嗎？不同 → 補 initializing 狀態進矩陣。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>被查詢方在初始化期間就能回應 baseline 狀態&lt;/strong> — 「我在、還沒準備好」與「沒有回應」對查詢方是完全不同的資訊。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>過渡狀態的顯示必須有資料來源與退出條件&lt;/strong> — 硬編的「檢測中&amp;hellip;」沒有讀任何回應、也永遠不會離開，等於把過渡態寫成死胡同。&lt;/p>
&lt;/li>
&lt;/ol>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>狀態矩陣的四欄與填寫步驟 → &lt;a href="https://tarrragon.github.io/blog/ux-design/01-screen-state-machine/state-matrix-definition/" data-link-title="畫面狀態矩陣的定義與填寫方法" data-link-desc="四欄矩陣（顯示 / 可用操作 / 進入條件 / 退出路徑）的定義、填寫步驟和檢查規則 — 退出路徑為空 = UX 死胡同">畫面狀態矩陣的定義與填寫方法&lt;/a>&lt;/li>
&lt;li>等待多久該顯示什麼指示 → &lt;a href="https://tarrragon.github.io/blog/ux-design/06-interaction-feedback/response-time-strategy/" data-link-title="時間感知與回應策略：Loading 的形式由等待時間門檻決定" data-link-desc="100ms / 400ms / 1s / 10s 時間門檻對應不同回饋策略 — 決定操作該不該顯示 loading、用 spinner、skeleton 還是進度條的判準。">時間感知與回應策略&lt;/a>&lt;/li>
&lt;li>類似案例（狀態設計遺漏）→ &lt;a href="https://tarrragon.github.io/blog/ux-design/cases/five-states-zero-exits/" data-link-title="U.C1 Terminal 畫面五個狀態零個退出路徑" data-link-desc="Flutter app 的 Terminal 畫面有 idle/connecting/connected/error/disconnected 五個 enum 狀態，每個狀態都沒有 back 或 disconnect 按鈕 — 使用者一旦進入就出不去">U.C1 五個狀態零個退出路徑&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>查詢對象有自己的生命週期時，「還不知道對方狀態」（initializing / unknown）是一個真實狀態 — 把它與「離線」「錯誤」合併，查詢對象醒得慢的那一次就會被呈現成假離線。這張卡記錄<a href="/blog/ux-design/knowledge-cards/screen-state-matrix/" data-link-title="Screen State Matrix（畫面狀態矩陣）" data-link-desc="說明用四欄表格（顯示/可用操作/進入條件/退出路徑）系統性地暴露畫面導航缺口的設計工具">畫面狀態矩陣</a>列狀態時的這個系統性遺漏。</p>
<h2 id="觀察">觀察</h2>
<p>電子書庫總覽 Chrome 擴充功能（book_overview_v1）的 popup 開啟時向 background service worker 查詢狀態。Manifest V3 的 service worker 是事件驅動、閒置即卸載 — popup 開啟的瞬間 SW 可能正在冷啟動、初始化未完成、不回應 <code>GET_STATUS</code>。popup 等待期間顯示「正在檢查狀態&hellip;」，2 秒 timeout 後轉為「離線」且不再自動恢復 — 冷啟動偶爾超過 2 秒（極端 I/O、低階裝置，低頻但真實發生過）時，系統實際正常、使用者看到的卻是永久離線（<code>src/background/background.js:272-284</code>，ticket 1.1.0-W1-019）。</p>
<p>修復採雙管：查詢端加握手重試，加上被查詢端在初始化期間就回應 baseline 的 <code>initializing</code> 狀態 — popup 據此顯示「初始化中」而非落入 timeout 判離線。</p>
<p>同專案的另一起同型事故（commit <code>86216c37f</code>）：popup 的書籍偵測數硬編「檢測中&hellip;」、從未讀取健康查詢回應中的實際數字 — 過渡狀態的顯示寫死了、永遠停在過渡態。兩個事故一體兩面：一個把「還不知道」誤顯示成終態（離線）、一個把終態永遠顯示成「還不知道」。</p>
<h2 id="判讀">判讀</h2>
<ol>
<li>
<p><strong>「還不知道」與「不可用」是不同狀態</strong>。離線 / 錯誤是查詢得到的答案，initializing 是還沒得到答案。合併兩者的畫面會把「這次醒得慢」定格成永久離線 — 頻率低不減輕代價，使用者據此做錯誤決策：放棄操作、重裝、回報故障。</p>
</li>
<li>
<p><strong>查詢對象的生命週期決定 initializing 是否必要</strong>。查詢對象與畫面同生命週期（同 process 的本地狀態）時不需要；查詢對象獨立生死（service worker、遠端服務、另一個 process、外部裝置）時，畫面開啟瞬間對方「還沒醒」是常態而非邊角 — initializing 必須是狀態矩陣裡的一行，有自己的顯示、操作與退出路徑。</p>
</li>
<li>
<p><strong>timeout 是 initializing 的退出路徑</strong>。「初始化中」不能無限停留 — 超過合理時間仍無回應才轉入離線 / 錯誤狀態。順序是 initializing → (回應) 正常態 / (timeout) 離線，而非直接顯示離線等回應來救。</p>
</li>
</ol>
<h2 id="策略">策略</h2>
<ol>
<li>
<p><strong>列狀態時多問一句</strong>：這個畫面查詢的對象，跟畫面同生命週期嗎？不同 → 補 initializing 狀態進矩陣。</p>
</li>
<li>
<p><strong>被查詢方在初始化期間就能回應 baseline 狀態</strong> — 「我在、還沒準備好」與「沒有回應」對查詢方是完全不同的資訊。</p>
</li>
<li>
<p><strong>過渡狀態的顯示必須有資料來源與退出條件</strong> — 硬編的「檢測中&hellip;」沒有讀任何回應、也永遠不會離開，等於把過渡態寫成死胡同。</p>
</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>狀態矩陣的四欄與填寫步驟 → <a href="/blog/ux-design/01-screen-state-machine/state-matrix-definition/" data-link-title="畫面狀態矩陣的定義與填寫方法" data-link-desc="四欄矩陣（顯示 / 可用操作 / 進入條件 / 退出路徑）的定義、填寫步驟和檢查規則 — 退出路徑為空 = UX 死胡同">畫面狀態矩陣的定義與填寫方法</a></li>
<li>等待多久該顯示什麼指示 → <a href="/blog/ux-design/06-interaction-feedback/response-time-strategy/" data-link-title="時間感知與回應策略：Loading 的形式由等待時間門檻決定" data-link-desc="100ms / 400ms / 1s / 10s 時間門檻對應不同回饋策略 — 決定操作該不該顯示 loading、用 spinner、skeleton 還是進度條的判準。">時間感知與回應策略</a></li>
<li>類似案例（狀態設計遺漏）→ <a href="/blog/ux-design/cases/five-states-zero-exits/" data-link-title="U.C1 Terminal 畫面五個狀態零個退出路徑" data-link-desc="Flutter app 的 Terminal 畫面有 idle/connecting/connected/error/disconnected 五個 enum 狀態，每個狀態都沒有 back 或 disconnect 按鈕 — 使用者一旦進入就出不去">U.C1 五個狀態零個退出路徑</a></li>
</ul>
]]></content:encoded></item><item><title>U.C11 抓到 96/928 本就顯示完成 — 完成判定的證據強度不足</title><link>https://tarrragon.github.io/blog/ux-design/cases/lazy-load-premature-completion/</link><pubDate>Fri, 17 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/ux-design/cases/lazy-load-premature-completion/</guid><description>&lt;p>「完成」是一個 UI 宣告，誠實度取決於判定條件的證據強度：「暫時沒有變化」與「確認沒有更多」是強度完全不同的兩種證據，混用會讓使用者拿到殘缺資料而不自知。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>電子書庫總覽 Chrome 擴充功能（book_overview_v1）從 lazy-load 書庫頁提取書目。提取器的兩種載入策略（捲動容器 / 點擊「更多&amp;hellip;」按鈕）設計成二擇一，實機上容器 selector 恆先命中，「更多&amp;hellip;」按鈕分支永不執行；首批 96 本載入後 count 連續 3 輪不變，觸發 &lt;code>count_stable&lt;/code> 條件判定完成 — 實際書庫有 928 本，提取器在 96 本就宣告成功結束（commit &lt;code>9d0556e1b&lt;/code>，W1-030 / W1-040）。&lt;/p>
&lt;p>修復：每輪同時捲動＋點擊按鈕（不再二擇一），涵蓋率從 96/928 提升到 928/928 — 可見書目全數取得（全書庫 944 本、其餘為封存借出、不在可見清單）。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>count 穩定是「沒有變化」、不是「沒有更多」&lt;/strong>。lazy-load 頁面的內容增長依賴特定觸發動作（捲動、點按鈕）— 觸發動作缺失時 count 永遠穩定，穩定訊號分不出「真的到底了」和「載入通道根本沒打開」。完成判定缺一個獨立的窮盡證據：頁面宣告的總數、終止標記（「沒有更多了」元素）、載入觸發器消失、或 API 側的窮盡宣告（分頁 cursor 耗盡、&lt;code>has_more=false&lt;/code>）。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>誤報完成比報錯更難察覺&lt;/strong>。提取失敗使用者會重試；「成功提取 96 本」看起來一切正常，使用者帶著殘缺資料做下游決策（統計、匯出、比對），錯誤在離開這個畫面很久之後才浮現、且很難回溯到提取階段。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>有總數可對照時，缺口是可偵測的&lt;/strong>。頁面顯示 928 本、提取到 96 本 — 10 倍差距在有對照數字時一眼可見。判定邏輯沒有利用這個訊號，是證據源的遺漏、不是證據不存在。&lt;/p>
&lt;/li>
&lt;/ol>
&lt;h2 id="策略">策略&lt;/h2>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>完成判定列出證據清單&lt;/strong>：宣告完成前問「終止條件是『確認沒有更多』還是『暫時沒變化』」。只有停滯訊號時，完成宣告降級為「已取得 N 筆、可能還有更多」的誠實表達。降級的適用邊界：下游需要全量的操作（備份、匯出、合規留存）不適用部分結果 — 這類場景把不完整按失敗處理、阻止下游動作。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>利用對照數字做完整性檢查&lt;/strong>：來源有總數（頁面計數、API total）時，提取數與總數的比對是最便宜的誤報偵測 — 差距大時不宣告完成、改報部分結果與原因。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>多策略遍歷用並行不用二擇一&lt;/strong>：載入觸發方式不確定時每輪全部嘗試，單一策略的 selector 誤命中不會關閉其他通道。&lt;/p>
&lt;/li>
&lt;/ol>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>進度指示與假進度反模式 → &lt;a href="https://tarrragon.github.io/blog/ux-design/06-interaction-feedback/response-time-strategy/" data-link-title="時間感知與回應策略：Loading 的形式由等待時間門檻決定" data-link-desc="100ms / 400ms / 1s / 10s 時間門檻對應不同回饋策略 — 決定操作該不該顯示 loading、用 spinner、skeleton 還是進度條的判準。">時間感知與回應策略&lt;/a>&lt;/li>
&lt;li>結果通知的呈現（部分成功的摘要 + 明細）→ &lt;a href="https://tarrragon.github.io/blog/ux-design/06-interaction-feedback/feedback-three-layers/" data-link-title="互動回饋三層模型：點擊確認、等待指示、結果通知" data-link-desc="使用者操作後的回饋依時間分層，缺層的症狀是重複提交與重複導航 — 診斷「按了沒反應」與多步驟流程卡狀態問題的檢查框架，涵蓋按鈕級與畫面級兩個尺度。">互動回饋三層模型&lt;/a>&lt;/li>
&lt;li>類似案例（結果通知鏈路錯誤）→ &lt;a href="https://tarrragon.github.io/blog/ux-design/cases/async-listener-false-failure/" data-link-title="U.C9 提取成功卻誤報失敗 — 結果通知鏈路被搶通道" data-link-desc="操作實際成功、資料已寫入，UI 卻顯示失敗時使用。多 context 的訊息通道語意（誰負責回應）是結果通知鏈路的一部分，async listener 搶通道會把 undefined 當成回應送回">U.C9 提取成功卻誤報失敗&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>「完成」是一個 UI 宣告，誠實度取決於判定條件的證據強度：「暫時沒有變化」與「確認沒有更多」是強度完全不同的兩種證據，混用會讓使用者拿到殘缺資料而不自知。</p>
<h2 id="觀察">觀察</h2>
<p>電子書庫總覽 Chrome 擴充功能（book_overview_v1）從 lazy-load 書庫頁提取書目。提取器的兩種載入策略（捲動容器 / 點擊「更多&hellip;」按鈕）設計成二擇一，實機上容器 selector 恆先命中，「更多&hellip;」按鈕分支永不執行；首批 96 本載入後 count 連續 3 輪不變，觸發 <code>count_stable</code> 條件判定完成 — 實際書庫有 928 本，提取器在 96 本就宣告成功結束（commit <code>9d0556e1b</code>，W1-030 / W1-040）。</p>
<p>修復：每輪同時捲動＋點擊按鈕（不再二擇一），涵蓋率從 96/928 提升到 928/928 — 可見書目全數取得（全書庫 944 本、其餘為封存借出、不在可見清單）。</p>
<h2 id="判讀">判讀</h2>
<ol>
<li>
<p><strong>count 穩定是「沒有變化」、不是「沒有更多」</strong>。lazy-load 頁面的內容增長依賴特定觸發動作（捲動、點按鈕）— 觸發動作缺失時 count 永遠穩定，穩定訊號分不出「真的到底了」和「載入通道根本沒打開」。完成判定缺一個獨立的窮盡證據：頁面宣告的總數、終止標記（「沒有更多了」元素）、載入觸發器消失、或 API 側的窮盡宣告（分頁 cursor 耗盡、<code>has_more=false</code>）。</p>
</li>
<li>
<p><strong>誤報完成比報錯更難察覺</strong>。提取失敗使用者會重試；「成功提取 96 本」看起來一切正常，使用者帶著殘缺資料做下游決策（統計、匯出、比對），錯誤在離開這個畫面很久之後才浮現、且很難回溯到提取階段。</p>
</li>
<li>
<p><strong>有總數可對照時，缺口是可偵測的</strong>。頁面顯示 928 本、提取到 96 本 — 10 倍差距在有對照數字時一眼可見。判定邏輯沒有利用這個訊號，是證據源的遺漏、不是證據不存在。</p>
</li>
</ol>
<h2 id="策略">策略</h2>
<ol>
<li>
<p><strong>完成判定列出證據清單</strong>：宣告完成前問「終止條件是『確認沒有更多』還是『暫時沒變化』」。只有停滯訊號時，完成宣告降級為「已取得 N 筆、可能還有更多」的誠實表達。降級的適用邊界：下游需要全量的操作（備份、匯出、合規留存）不適用部分結果 — 這類場景把不完整按失敗處理、阻止下游動作。</p>
</li>
<li>
<p><strong>利用對照數字做完整性檢查</strong>：來源有總數（頁面計數、API total）時，提取數與總數的比對是最便宜的誤報偵測 — 差距大時不宣告完成、改報部分結果與原因。</p>
</li>
<li>
<p><strong>多策略遍歷用並行不用二擇一</strong>：載入觸發方式不確定時每輪全部嘗試，單一策略的 selector 誤命中不會關閉其他通道。</p>
</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>進度指示與假進度反模式 → <a href="/blog/ux-design/06-interaction-feedback/response-time-strategy/" data-link-title="時間感知與回應策略：Loading 的形式由等待時間門檻決定" data-link-desc="100ms / 400ms / 1s / 10s 時間門檻對應不同回饋策略 — 決定操作該不該顯示 loading、用 spinner、skeleton 還是進度條的判準。">時間感知與回應策略</a></li>
<li>結果通知的呈現（部分成功的摘要 + 明細）→ <a href="/blog/ux-design/06-interaction-feedback/feedback-three-layers/" data-link-title="互動回饋三層模型：點擊確認、等待指示、結果通知" data-link-desc="使用者操作後的回饋依時間分層，缺層的症狀是重複提交與重複導航 — 診斷「按了沒反應」與多步驟流程卡狀態問題的檢查框架，涵蓋按鈕級與畫面級兩個尺度。">互動回饋三層模型</a></li>
<li>類似案例（結果通知鏈路錯誤）→ <a href="/blog/ux-design/cases/async-listener-false-failure/" data-link-title="U.C9 提取成功卻誤報失敗 — 結果通知鏈路被搶通道" data-link-desc="操作實際成功、資料已寫入，UI 卻顯示失敗時使用。多 context 的訊息通道語意（誰負責回應）是結果通知鏈路的一部分，async listener 搶通道會把 undefined 當成回應送回">U.C9 提取成功卻誤報失敗</a></li>
</ul>
]]></content:encoded></item><item><title>U.C12 匯入空檔會清空書庫 — 破壞性操作的確認與安全預設</title><link>https://tarrragon.github.io/blog/ux-design/cases/destructive-import-fail-safe-confirm/</link><pubDate>Fri, 17 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/ux-design/cases/destructive-import-fail-safe-confirm/</guid><description>&lt;p>這個案例的核心責任是展示破壞性操作 gate（使用者必須通過才能繼續的關卡）的完整設計（正面案例）：確認對話框攔截使用者意圖之外，確認機制本身的故障模式也被設計了 — UI 元件缺失時視為「未確認」，預設不執行破壞。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>電子書庫總覽 Chrome 擴充功能（book_overview_v1）的匯入功能有覆蓋 / 合併兩種模式；覆蓋模式的語意是「以檔案內容取代現有書庫」— 該模式下匯入空檔等於清空全部資料。設計（&lt;code>src/overview/import-flow-controller.js&lt;/code>，W1-049）：&lt;/p>
&lt;ol>
&lt;li>覆蓋模式下偵測到匯入內容為空時，彈出專屬確認 Modal 描述後果（現有書庫非空時文案帶具體數字「將清空現有 N 本書」）、使用者明確確認才執行；合併模式不觸發這道確認。&lt;/li>
&lt;li>Modal 帶 &lt;code>aria-labelledby&lt;/code> / &lt;code>aria-describedby&lt;/code>，螢幕閱讀器使用者能取得完整的後果描述。&lt;/li>
&lt;li>關鍵設計：&lt;strong>確認 Modal 的 DOM 元件缺失時，視為使用者未確認、預設不清空&lt;/strong>。確認機制故障不會讓破壞性操作靜默通過。&lt;/li>
&lt;/ol>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>破壞性操作 gate 是 gate 的一個獨立類型&lt;/strong>。認證 / 網路 / 權限 gate 攔「使用者能不能繼續」，破壞性操作 gate 攔「使用者是否理解後果」— 觸發條件不是身分或環境、是操作的破壞半徑（不可逆、影響既有資料、影響範圍大於使用者的直覺預期）。「匯入」聽起來是加法、覆蓋模式的實際語意是取代 — 語意與直覺預期的落差越大、越需要確認。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>確認機制本身有故障模式&lt;/strong>。元件沒渲染、事件沒綁上、動態載入失敗 — 確認 UI 故障時系統要選一個預設方向：執行（把故障當同意）或不執行（把故障當拒絕）。安全預設的原則是倒向不可逆性低的那邊：不清空可以重試匯入、清空無法還原。這與「fail-open vs fail-closed」的安全設計同構。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>確認對話框的可及性是 gate 有效性的一部分&lt;/strong>。看不到後果描述的確認（螢幕閱讀器讀不出 Modal 內容）等於沒有確認 — 使用者按了「確定」但不知道確定了什麼。&lt;/p>
&lt;/li>
&lt;/ol>
&lt;h2 id="策略">策略&lt;/h2>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>識別破壞性操作&lt;/strong>：列出所有會覆蓋 / 刪除 / 取代既有資料的操作，語意與直覺預期有落差的是高風險項（匯入的覆蓋模式 = 取代、同步 = 可能覆蓋、重設 = 清空）。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>確認 UI 描述後果、不只問「確定嗎」&lt;/strong>：帶上具體數字（「將清空現有 N 本書」）讓使用者對照自己的預期。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>為確認機制設計故障預設&lt;/strong>：確認元件不存在 / 事件未觸發 / 回應逾時，一律視為未確認。程式碼層的檢查訊號：確認邏輯的 else / null 分支走向哪邊。&lt;/p>
&lt;/li>
&lt;/ol>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>Gate 的必答問題與類型 → &lt;a href="https://tarrragon.github.io/blog/ux-design/02-gate-fallback/gate-three-questions/" data-link-title="Gate 分類與三問設計法" data-link-desc="每個 gate 設計時問三個問題：成功時做什麼、失敗時做什麼、使用者不知道發生什麼時做什麼">Gate 分類與三問設計法&lt;/a>&lt;/li>
&lt;li>確認機制故障的預設方向 → &lt;a href="https://tarrragon.github.io/blog/ux-design/knowledge-cards/fail-safe-default/" data-link-title="Fail-Safe Default（安全預設）" data-link-desc="說明保護機制本身故障時系統倒向哪個方向的設計決策 — 預設倒向不可逆性低的那邊，確認 UI 壞掉不該讓破壞性操作靜默通過">Fail-safe 預設&lt;/a>&lt;/li>
&lt;li>通知形式的干擾程度判準（Dialog 的阻斷性是設計需求）→ &lt;a href="https://tarrragon.github.io/blog/ux-design/06-interaction-feedback/notification-pattern-selection/" data-link-title="通知模式選擇：SnackBar、Dialog、Banner 與 Bottom Sheet" data-link-desc="操作結果該用 SnackBar 閃一下還是彈 Dialog 問使用者 — 干擾程度與是否需要使用者操作的二軸判準，選錯形式的症狀是通知被忽略或流程被打斷">通知模式選擇&lt;/a>&lt;/li>
&lt;li>對照案例（gate 缺 fallback）→ &lt;a href="https://tarrragon.github.io/blog/ux-design/cases/biometric-only-no-fallback/" data-link-title="U.C2 biometricOnly=true 無密碼 fallback" data-link-desc="Flutter app 的生物辨識設定 biometricOnly: true 阻擋所有非生物辨識認證方式 — Face ID 不可用時使用者直接被擋住，沒有替代路徑">U.C2 biometricOnly 無 fallback&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是展示破壞性操作 gate（使用者必須通過才能繼續的關卡）的完整設計（正面案例）：確認對話框攔截使用者意圖之外，確認機制本身的故障模式也被設計了 — UI 元件缺失時視為「未確認」，預設不執行破壞。</p>
<h2 id="觀察">觀察</h2>
<p>電子書庫總覽 Chrome 擴充功能（book_overview_v1）的匯入功能有覆蓋 / 合併兩種模式；覆蓋模式的語意是「以檔案內容取代現有書庫」— 該模式下匯入空檔等於清空全部資料。設計（<code>src/overview/import-flow-controller.js</code>，W1-049）：</p>
<ol>
<li>覆蓋模式下偵測到匯入內容為空時，彈出專屬確認 Modal 描述後果（現有書庫非空時文案帶具體數字「將清空現有 N 本書」）、使用者明確確認才執行；合併模式不觸發這道確認。</li>
<li>Modal 帶 <code>aria-labelledby</code> / <code>aria-describedby</code>，螢幕閱讀器使用者能取得完整的後果描述。</li>
<li>關鍵設計：<strong>確認 Modal 的 DOM 元件缺失時，視為使用者未確認、預設不清空</strong>。確認機制故障不會讓破壞性操作靜默通過。</li>
</ol>
<h2 id="判讀">判讀</h2>
<ol>
<li>
<p><strong>破壞性操作 gate 是 gate 的一個獨立類型</strong>。認證 / 網路 / 權限 gate 攔「使用者能不能繼續」，破壞性操作 gate 攔「使用者是否理解後果」— 觸發條件不是身分或環境、是操作的破壞半徑（不可逆、影響既有資料、影響範圍大於使用者的直覺預期）。「匯入」聽起來是加法、覆蓋模式的實際語意是取代 — 語意與直覺預期的落差越大、越需要確認。</p>
</li>
<li>
<p><strong>確認機制本身有故障模式</strong>。元件沒渲染、事件沒綁上、動態載入失敗 — 確認 UI 故障時系統要選一個預設方向：執行（把故障當同意）或不執行（把故障當拒絕）。安全預設的原則是倒向不可逆性低的那邊：不清空可以重試匯入、清空無法還原。這與「fail-open vs fail-closed」的安全設計同構。</p>
</li>
<li>
<p><strong>確認對話框的可及性是 gate 有效性的一部分</strong>。看不到後果描述的確認（螢幕閱讀器讀不出 Modal 內容）等於沒有確認 — 使用者按了「確定」但不知道確定了什麼。</p>
</li>
</ol>
<h2 id="策略">策略</h2>
<ol>
<li>
<p><strong>識別破壞性操作</strong>：列出所有會覆蓋 / 刪除 / 取代既有資料的操作，語意與直覺預期有落差的是高風險項（匯入的覆蓋模式 = 取代、同步 = 可能覆蓋、重設 = 清空）。</p>
</li>
<li>
<p><strong>確認 UI 描述後果、不只問「確定嗎」</strong>：帶上具體數字（「將清空現有 N 本書」）讓使用者對照自己的預期。</p>
</li>
<li>
<p><strong>為確認機制設計故障預設</strong>：確認元件不存在 / 事件未觸發 / 回應逾時，一律視為未確認。程式碼層的檢查訊號：確認邏輯的 else / null 分支走向哪邊。</p>
</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>Gate 的必答問題與類型 → <a href="/blog/ux-design/02-gate-fallback/gate-three-questions/" data-link-title="Gate 分類與三問設計法" data-link-desc="每個 gate 設計時問三個問題：成功時做什麼、失敗時做什麼、使用者不知道發生什麼時做什麼">Gate 分類與三問設計法</a></li>
<li>確認機制故障的預設方向 → <a href="/blog/ux-design/knowledge-cards/fail-safe-default/" data-link-title="Fail-Safe Default（安全預設）" data-link-desc="說明保護機制本身故障時系統倒向哪個方向的設計決策 — 預設倒向不可逆性低的那邊，確認 UI 壞掉不該讓破壞性操作靜默通過">Fail-safe 預設</a></li>
<li>通知形式的干擾程度判準（Dialog 的阻斷性是設計需求）→ <a href="/blog/ux-design/06-interaction-feedback/notification-pattern-selection/" data-link-title="通知模式選擇：SnackBar、Dialog、Banner 與 Bottom Sheet" data-link-desc="操作結果該用 SnackBar 閃一下還是彈 Dialog 問使用者 — 干擾程度與是否需要使用者操作的二軸判準，選錯形式的症狀是通知被忽略或流程被打斷">通知模式選擇</a></li>
<li>對照案例（gate 缺 fallback）→ <a href="/blog/ux-design/cases/biometric-only-no-fallback/" data-link-title="U.C2 biometricOnly=true 無密碼 fallback" data-link-desc="Flutter app 的生物辨識設定 biometricOnly: true 阻擋所有非生物辨識認證方式 — Face ID 不可用時使用者直接被擋住，沒有替代路徑">U.C2 biometricOnly 無 fallback</a></li>
</ul>
]]></content:encoded></item><item><title>U.C13 匯入錯誤卡片出現「重新載入擴充功能」— 錯誤行動與層級不對位</title><link>https://tarrragon.github.io/blog/ux-design/cases/import-error-reload-extension-mismatch/</link><pubDate>Fri, 17 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/ux-design/cases/import-error-reload-extension-mismatch/</guid><description>&lt;p>錯誤 UI 提供的行動，解決問題的層級要等於錯誤發生的層級 — 資料層的錯誤配上執行環境層的行動（重載擴充功能），既不解決問題、又放大破壞半徑。這張卡記錄這條對位原則被通用錯誤容器打破、由使用者回饋抓回來的過程。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>電子書庫總覽 Chrome 擴充功能（book_overview_v1）的匯入功能出錯時（檔案格式錯誤、內容不合法），錯誤顯示複用了 popup 的通用 &lt;code>errorContainer&lt;/code> — 這個容器是為擴充功能執行層錯誤設計的，帶「重新載入擴充功能」按鈕。使用者回饋指出：Chrome Web Store 上架版對「匯入錯誤」不該出現 reload extension 按鈕（commit &lt;code>72cd5f370&lt;/code>）。&lt;/p>
&lt;p>修復：建匯入專屬的 &lt;code>importErrorContainer&lt;/code>，只有「關閉」按鈕（沒有 retry / reload）— 匯入錯誤的正確下一步是換一個檔案再試，不是重載執行環境；同時把文案集中到 &lt;code>IMPORT_MESSAGES&lt;/code> 常數、移除對通用錯誤處理器的依賴。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>錯誤有層級、行動也有層級&lt;/strong>。檔案格式錯誤是資料層、訊息通道斷線是通訊層、service worker 崩潰是執行環境層。行動同樣分層：換檔案重試（資料層）、重新連線（通訊層）、重載擴充功能（執行環境層）。對位原則：行動解決的層級 = 錯誤發生的層級。層級過重的行動不解決問題（重載擴充功能不會讓壞檔案變好）、還附帶代價（狀態遺失、流程中斷）。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>通用錯誤容器是不對位的結構性來源&lt;/strong>。為最嚴重錯誤設計的容器（帶最重的行動）被所有錯誤複用時，輕錯誤自動繼承重行動。錯誤 UI 的複用要以「行動相容」為邊界、不是以「都是錯誤」為邊界。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>使用者會照著按鈕走&lt;/strong>。錯誤畫面上的按鈕是系統給的行動建議，使用者傾向直接採納 — 不對位的按鈕等於系統主動引導使用者做無效且有代價的操作。&lt;/p>
&lt;/li>
&lt;/ol>
&lt;h2 id="策略">策略&lt;/h2>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>對每個錯誤 UI 的行動清單問一句&lt;/strong>：這個行動解決的層級，等於這個錯誤發生的層級嗎？過重（重載 / 重啟 / 重裝出現在資料層錯誤）與過輕（執行環境崩潰只給「關閉」）都是缺口。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>錯誤容器按行動分組&lt;/strong>：行動集合不同的錯誤用不同容器 / 元件，避免複用時行動一起被繼承。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>文案與行動集中管理&lt;/strong>：錯誤訊息散在 HTML 各處時，行動不對位很難被掃描發現；集中成常數表後「哪類錯誤配哪些行動」一眼可查。&lt;/p>
&lt;/li>
&lt;/ol>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>錯誤訊息的診斷與行動職責 → &lt;a href="https://tarrragon.github.io/blog/ux-design/04-error-recovery/error-message-principles/" data-link-title="錯誤訊息撰寫原則" data-link-desc="錯誤訊息的兩個職責：使用者能讀懂發生什麼、使用者能決定下一步做什麼">錯誤訊息撰寫原則&lt;/a>&lt;/li>
&lt;li>重試行動的設計 → &lt;a href="https://tarrragon.github.io/blog/ux-design/04-error-recovery/retry-mechanism-ux/" data-link-title="Retry 機制 UX" data-link-desc="自動 vs 手動重試、指數退避 vs 立即重試 — 重試策略的選擇取決於失敗的可恢復性和使用者的等待意願">Retry 機制 UX&lt;/a>&lt;/li>
&lt;li>對照案例（行動缺失 — 只有重試沒有退路）→ &lt;a href="https://tarrragon.github.io/blog/ux-design/cases/five-states-zero-exits/" data-link-title="U.C1 Terminal 畫面五個狀態零個退出路徑" data-link-desc="Flutter app 的 Terminal 畫面有 idle/connecting/connected/error/disconnected 五個 enum 狀態，每個狀態都沒有 back 或 disconnect 按鈕 — 使用者一旦進入就出不去">U.C1 五個狀態零個退出路徑&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>錯誤 UI 提供的行動，解決問題的層級要等於錯誤發生的層級 — 資料層的錯誤配上執行環境層的行動（重載擴充功能），既不解決問題、又放大破壞半徑。這張卡記錄這條對位原則被通用錯誤容器打破、由使用者回饋抓回來的過程。</p>
<h2 id="觀察">觀察</h2>
<p>電子書庫總覽 Chrome 擴充功能（book_overview_v1）的匯入功能出錯時（檔案格式錯誤、內容不合法），錯誤顯示複用了 popup 的通用 <code>errorContainer</code> — 這個容器是為擴充功能執行層錯誤設計的，帶「重新載入擴充功能」按鈕。使用者回饋指出：Chrome Web Store 上架版對「匯入錯誤」不該出現 reload extension 按鈕（commit <code>72cd5f370</code>）。</p>
<p>修復：建匯入專屬的 <code>importErrorContainer</code>，只有「關閉」按鈕（沒有 retry / reload）— 匯入錯誤的正確下一步是換一個檔案再試，不是重載執行環境；同時把文案集中到 <code>IMPORT_MESSAGES</code> 常數、移除對通用錯誤處理器的依賴。</p>
<h2 id="判讀">判讀</h2>
<ol>
<li>
<p><strong>錯誤有層級、行動也有層級</strong>。檔案格式錯誤是資料層、訊息通道斷線是通訊層、service worker 崩潰是執行環境層。行動同樣分層：換檔案重試（資料層）、重新連線（通訊層）、重載擴充功能（執行環境層）。對位原則：行動解決的層級 = 錯誤發生的層級。層級過重的行動不解決問題（重載擴充功能不會讓壞檔案變好）、還附帶代價（狀態遺失、流程中斷）。</p>
</li>
<li>
<p><strong>通用錯誤容器是不對位的結構性來源</strong>。為最嚴重錯誤設計的容器（帶最重的行動）被所有錯誤複用時，輕錯誤自動繼承重行動。錯誤 UI 的複用要以「行動相容」為邊界、不是以「都是錯誤」為邊界。</p>
</li>
<li>
<p><strong>使用者會照著按鈕走</strong>。錯誤畫面上的按鈕是系統給的行動建議，使用者傾向直接採納 — 不對位的按鈕等於系統主動引導使用者做無效且有代價的操作。</p>
</li>
</ol>
<h2 id="策略">策略</h2>
<ol>
<li>
<p><strong>對每個錯誤 UI 的行動清單問一句</strong>：這個行動解決的層級，等於這個錯誤發生的層級嗎？過重（重載 / 重啟 / 重裝出現在資料層錯誤）與過輕（執行環境崩潰只給「關閉」）都是缺口。</p>
</li>
<li>
<p><strong>錯誤容器按行動分組</strong>：行動集合不同的錯誤用不同容器 / 元件，避免複用時行動一起被繼承。</p>
</li>
<li>
<p><strong>文案與行動集中管理</strong>：錯誤訊息散在 HTML 各處時，行動不對位很難被掃描發現；集中成常數表後「哪類錯誤配哪些行動」一眼可查。</p>
</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>錯誤訊息的診斷與行動職責 → <a href="/blog/ux-design/04-error-recovery/error-message-principles/" data-link-title="錯誤訊息撰寫原則" data-link-desc="錯誤訊息的兩個職責：使用者能讀懂發生什麼、使用者能決定下一步做什麼">錯誤訊息撰寫原則</a></li>
<li>重試行動的設計 → <a href="/blog/ux-design/04-error-recovery/retry-mechanism-ux/" data-link-title="Retry 機制 UX" data-link-desc="自動 vs 手動重試、指數退避 vs 立即重試 — 重試策略的選擇取決於失敗的可恢復性和使用者的等待意願">Retry 機制 UX</a></li>
<li>對照案例（行動缺失 — 只有重試沒有退路）→ <a href="/blog/ux-design/cases/five-states-zero-exits/" data-link-title="U.C1 Terminal 畫面五個狀態零個退出路徑" data-link-desc="Flutter app 的 Terminal 畫面有 idle/connecting/connected/error/disconnected 五個 enum 狀態，每個狀態都沒有 back 或 disconnect 按鈕 — 使用者一旦進入就出不去">U.C1 五個狀態零個退出路徑</a></li>
</ul>
]]></content:encoded></item><item><title>U.C14 hash SPA 的 pathname 永遠是根路徑 — 路由辨識遺漏 fragment</title><link>https://tarrragon.github.io/blog/ux-design/cases/hash-spa-route-label-loss/</link><pubDate>Fri, 17 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/ux-design/cases/hash-spa-route-label-loss/</guid><description>&lt;p>這個案例的核心責任是說明 web 路由辨識的一個系統性盲點：hash-based SPA（single-page application、單頁應用）的頁面資訊在 URL fragment 裡，只讀 pathname 的辨識邏輯會把所有頁面都判成根路徑 — 讀取的是別人的 app 時，對方用哪套路由慣例不由讀取方決定。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>電子書庫總覽 Chrome 擴充功能（book_overview_v1）的 popup 顯示「目前所在的目標網站頁面」標籤。目標平台 Readmoo 是 hash-based SPA — 書庫頁的 URL 是 &lt;code>read.readmoo.com/#/library&lt;/code>，路由資訊在 &lt;code>#&lt;/code> 之後。popup 用 &lt;code>URL.pathname&lt;/code> 取頁面路徑，pathname 永遠是 &lt;code>/&lt;/code>，所有頁面都被顯示成根路徑、無法辨識使用者目前在哪一頁（commit &lt;code>a27de860e&lt;/code>）。&lt;/p>
&lt;p>修復：改用 &lt;code>pathname + hash&lt;/code> 組合顯示；補三類 URL 的解析測試（hash SPA &lt;code>/#/library&lt;/code>、根路徑 &lt;code>/&lt;/code>、傳統 path &lt;code>/account&lt;/code>）；並掃描全 codebase 確認 pathname-only 邏輯只影響顯示標籤、無 functional 用途。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>web 路由有兩套並存的慣例&lt;/strong>。path-based（&lt;code>/library&lt;/code>、伺服器路由或 History API）與 hash-based（&lt;code>/#/library&lt;/code>、fragment 路由）。「目前在哪頁」的判斷邏輯只支援其中一套時，另一套的所有頁面都會塌縮成同一個值 — 塌縮是靜默的，pathname 讀 hash SPA 不會報錯、只會永遠回傳 &lt;code>/&lt;/code>。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>跨 app 邊界讀 URL 時，對方的路由慣例是輸入規格&lt;/strong>。擴充功能、爬蟲、分析工具讀取宿主頁面的 URL — 宿主用哪套路由不受讀取方控制、還會隨對方改版變動。辨識邏輯要把「對方是哪種路由形態」當成必須確認的規格項，不能假設「URL 的頁面資訊都在 path」。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>顯示層的錯位是低嚴重度、但同一邏輯的 functional 用途是高嚴重度&lt;/strong>。這個案例只影響標籤顯示；同樣的 pathname-only 邏輯若用於「判斷是否在可提取頁面」，會讓功能在所有頁面都誤判。修復時的全 codebase 掃描（確認無 functional 用途）就是在排除這個升級風險。&lt;/p>
&lt;/li>
&lt;/ol>
&lt;h2 id="策略">策略&lt;/h2>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>依 URL 辨識頁面的功能，列出目標的路由形態&lt;/strong>：path-based / hash-based / 混合，測試各涵蓋一組 URL。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>取「完整路由」用 pathname + hash 組合&lt;/strong>，只在確認目標是純 path-based 時才省略 fragment；組合前先確認 fragment 承載的是路由還是頁內錨點 — path-based 站的 &lt;code>#section&lt;/code> 是錨點、不是頁面。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>發現一處路由辨識錯誤時，掃描同 pattern 的所有用途&lt;/strong> — 顯示用途與 functional 用途的嚴重度不同，修復範圍要以掃描結果為準、不是以回報的症狀為準。&lt;/p>
&lt;/li>
&lt;/ol>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>Deep link 的 URL 結構設計 → &lt;a href="https://tarrragon.github.io/blog/ux-design/05-navigation-patterns/deep-link-design/" data-link-title="Deep link 設計" data-link-desc="URL scheme / Universal Link / App Link — deep link 讓外部來源直接導航到 app 的特定畫面">Deep link 設計&lt;/a>&lt;/li>
&lt;li>導航模式與宣告式路由 → &lt;a href="https://tarrragon.github.io/blog/ux-design/05-navigation-patterns/mobile-navigation-taxonomy/" data-link-title="Mobile 導航模式分類" data-link-desc="Push/pop stack / declarative router / tab bar / drawer — 四種 mobile 導航模式各自的適用場景和使用者心理模型">Mobile 導航模式分類&lt;/a>&lt;/li>
&lt;li>類似案例（外部頁面結構是輸入規格 — 提取器對 lazy-load 的假設）→ &lt;a href="https://tarrragon.github.io/blog/ux-design/cases/lazy-load-premature-completion/" data-link-title="U.C11 抓到 96/928 本就顯示完成 — 完成判定的證據強度不足" data-link-desc="批次 / 遍歷類操作宣告完成、實際只處理了一部分：「連續 N 輪沒有變化」是暫時停滯的訊號、不是窮盡的證據 — 完成判定需要獨立的窮盡證據（總數對照、終止標記）">U.C11 抓到 96/928 本就顯示完成&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明 web 路由辨識的一個系統性盲點：hash-based SPA（single-page application、單頁應用）的頁面資訊在 URL fragment 裡，只讀 pathname 的辨識邏輯會把所有頁面都判成根路徑 — 讀取的是別人的 app 時，對方用哪套路由慣例不由讀取方決定。</p>
<h2 id="觀察">觀察</h2>
<p>電子書庫總覽 Chrome 擴充功能（book_overview_v1）的 popup 顯示「目前所在的目標網站頁面」標籤。目標平台 Readmoo 是 hash-based SPA — 書庫頁的 URL 是 <code>read.readmoo.com/#/library</code>，路由資訊在 <code>#</code> 之後。popup 用 <code>URL.pathname</code> 取頁面路徑，pathname 永遠是 <code>/</code>，所有頁面都被顯示成根路徑、無法辨識使用者目前在哪一頁（commit <code>a27de860e</code>）。</p>
<p>修復：改用 <code>pathname + hash</code> 組合顯示；補三類 URL 的解析測試（hash SPA <code>/#/library</code>、根路徑 <code>/</code>、傳統 path <code>/account</code>）；並掃描全 codebase 確認 pathname-only 邏輯只影響顯示標籤、無 functional 用途。</p>
<h2 id="判讀">判讀</h2>
<ol>
<li>
<p><strong>web 路由有兩套並存的慣例</strong>。path-based（<code>/library</code>、伺服器路由或 History API）與 hash-based（<code>/#/library</code>、fragment 路由）。「目前在哪頁」的判斷邏輯只支援其中一套時，另一套的所有頁面都會塌縮成同一個值 — 塌縮是靜默的，pathname 讀 hash SPA 不會報錯、只會永遠回傳 <code>/</code>。</p>
</li>
<li>
<p><strong>跨 app 邊界讀 URL 時，對方的路由慣例是輸入規格</strong>。擴充功能、爬蟲、分析工具讀取宿主頁面的 URL — 宿主用哪套路由不受讀取方控制、還會隨對方改版變動。辨識邏輯要把「對方是哪種路由形態」當成必須確認的規格項，不能假設「URL 的頁面資訊都在 path」。</p>
</li>
<li>
<p><strong>顯示層的錯位是低嚴重度、但同一邏輯的 functional 用途是高嚴重度</strong>。這個案例只影響標籤顯示；同樣的 pathname-only 邏輯若用於「判斷是否在可提取頁面」，會讓功能在所有頁面都誤判。修復時的全 codebase 掃描（確認無 functional 用途）就是在排除這個升級風險。</p>
</li>
</ol>
<h2 id="策略">策略</h2>
<ol>
<li>
<p><strong>依 URL 辨識頁面的功能，列出目標的路由形態</strong>：path-based / hash-based / 混合，測試各涵蓋一組 URL。</p>
</li>
<li>
<p><strong>取「完整路由」用 pathname + hash 組合</strong>，只在確認目標是純 path-based 時才省略 fragment；組合前先確認 fragment 承載的是路由還是頁內錨點 — path-based 站的 <code>#section</code> 是錨點、不是頁面。</p>
</li>
<li>
<p><strong>發現一處路由辨識錯誤時，掃描同 pattern 的所有用途</strong> — 顯示用途與 functional 用途的嚴重度不同，修復範圍要以掃描結果為準、不是以回報的症狀為準。</p>
</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>Deep link 的 URL 結構設計 → <a href="/blog/ux-design/05-navigation-patterns/deep-link-design/" data-link-title="Deep link 設計" data-link-desc="URL scheme / Universal Link / App Link — deep link 讓外部來源直接導航到 app 的特定畫面">Deep link 設計</a></li>
<li>導航模式與宣告式路由 → <a href="/blog/ux-design/05-navigation-patterns/mobile-navigation-taxonomy/" data-link-title="Mobile 導航模式分類" data-link-desc="Push/pop stack / declarative router / tab bar / drawer — 四種 mobile 導航模式各自的適用場景和使用者心理模型">Mobile 導航模式分類</a></li>
<li>類似案例（外部頁面結構是輸入規格 — 提取器對 lazy-load 的假設）→ <a href="/blog/ux-design/cases/lazy-load-premature-completion/" data-link-title="U.C11 抓到 96/928 本就顯示完成 — 完成判定的證據強度不足" data-link-desc="批次 / 遍歷類操作宣告完成、實際只處理了一部分：「連續 N 輪沒有變化」是暫時停滯的訊號、不是窮盡的證據 — 完成判定需要獨立的窮盡證據（總數對照、終止標記）">U.C11 抓到 96/928 本就顯示完成</a></li>
</ul>
]]></content:encoded></item></channel></rss>