<?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>Messaging on Tarragon</title><link>https://tarrragon.github.io/blog/tags/messaging/</link><description>Recent content in Messaging 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/messaging/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></channel></rss>