背景任務與多工介面紀律(Async Task UI)
什麼時候用
任何時候介面上出現「開始」按鈕、而工作要跑超過幾秒:報表/文件產出、批次匯入、長時間查詢、
AI 生成、檔案轉換。這類介面的物理是固定的:HTTP 是無狀態的、前端是會死的(重整/關頁/斷線)、
使用者是會亂點的——而任務必須活得比這三者都久。業界對此有一套高度一致的標準骨架
(送出→立即回任務 ID→背景執行→狀態端點輪詢→終態→領取結果);這份技能是那套骨架
加上真實踩雷換來的鐵則。失敗模式極其固定,標準解也是——不需要發明,需要的是不偏離。
核心法則
法則 1:狀態機先於介面——先定生死,再畫畫面
動手寫任何任務 UI 元件之前,先把任務狀態機定下來:最小集
queued(排隊)→ running(執行中)→ succeeded / failed / cancelled(終態),
外加正交的 delivered(已領取)旗標。
- 狀態轉移寫成一張表入碼(哪個狀態能到哪個狀態),禁止散落在各處的 if;
轉移表配測試——非法轉移會吠。
- 進度百分比、預估時間都是裝飾,狀態才是骨架;先有誠實的狀態,才配有進度條
(給不出誠實的百分比就別給,狀態文字比假進度可信)。
- 沒先定狀態機就寫介面=每個元件各自發明任務的生死,之後每顆 bug 都是「兩處對任務
狀態的理解不一致」。
法則 2:終態不可逆——完成的任務進收件匣,絕不還魂
終態(succeeded/failed/cancelled)是單行道:任何自動機制(重連、輪詢、恢復)
永不把終態任務重新開成工作視圖。
- 完成而未領取(succeeded 且未 delivered)的任務 → 收件匣/任務中心:被動列出,
使用者點了才開;絕不自動佔用分頁/版面。
- 歷史殘留任務(幾天前的、測試留下的)一律歸收件匣;「載入時把所有找得到的任務
都開成分頁」是本技能最經典的案發現場(見下)。
- 收件匣同時解掉「任務完成時使用者不在場」的交付問題——結果有家可歸,不必賭
使用者的視窗還開著。
法則 3:視圖與任務分離——伺服器是真相源,前端只是望遠鏡
任務的生死存亡絕不繫於任何視窗/分頁/前端物件的存活:
- 送出後任務屬於伺服器;前端拿 task_id 輪詢狀態端點——關頁、重整、斷線,任務照跑。
- 重整=望遠鏡重新對焦:載入時查詢「進行中/排隊中」任務、把視圖掛回去;
不是任務重生、更不是任務死亡。
- 重連機制只認非終態(法則 2);且「掛回」=更新既有視圖或掛回既有容器,
絕不因輪詢撈到東西就開新版面——視圖的誕生只有兩個合法來源:使用者動作、
載入時掛回真正在跑的任務。
- 多視圖(多分頁)時,每個視圖各持自己的狀態容器(per-tab context),
禁全域單例——全域狀態跨 await 交換必有 re-entrancy 撞況。
法則 4:冪等是送出的義務——連點、重試、重載都不得生出第二份工作
背景工作天生會被觸發多次(連點、網路重試、頁面重載後再按)——同一份邏輯工作,
跑幾次都只能算一次:
- 前端:送出即禁用按鈕/去抖(第一道,擋手速)。
- 服務端:冪等鍵(內容鍵或請求鍵)——同鍵重複送達=回同一個 task_id,不開新任務
(第二道,擋一切前端擋不住的)。兩道都要,只有前端的=沒有。
- 取消也要冪等:連按取消無害;跑中取消走
cancel_requested 中間態(UI 顯「取消中…」),
由工作端在檢查點收斂到 cancelled——不假裝立即死,也不讓使用者狂點。
法則 5:並行語意顯性化——不讓使用者猜,空白本身就是缺陷
能不能再開一個?排在第幾?上限幾個?——這些問題的答案必須寫在介面上:
- 有佇列就顯示順位(「排隊中,前面 N 筆」),不顯示假的「執行中 0%」。
- 有上限就在達上限時擋下並講明(「已達上限 N,需先關閉一個」),不是默默失敗。
- 使用者對背景任務的核心焦慮是掌控感:重整安不安全、能不能平行、系統到底更新了沒
——UI 不明講,使用者就開始亂試,亂試就撞出法則 4 要擋的事。
- 一個新開的工作區必須立即可用或明講為何不可用;渲染成空白讓人猜=缺陷,
不是「還沒做完的功能」。
法則 6:髒環境是預設——乾淨環境全綠≠真環境全綠
任務型 UI 的測試 fixture 必含兩軌:乾淨環境 + 髒環境(預置歷史終態任務
N 筆+進行中 1 筆再載入)。
- 真實使用者的環境永遠帶著歷史:昨天的任務、上週的殘留、測到一半的孤兒。
只在乾淨環境起跑的測試,對「載入」這個最日常的動作是零覆蓋。
- 髒環境斷言最小集:載入後恰好掛回「真正在跑的」、終態 0 自動復活、
收件匣列出全部未領取。
- 交付前的自走驗收,環境=真實使用者環境的等價複本(含全部殘留),
乾淨環境自走=沒自走。
本專案案發現場(佐證,非通用必需)
- 歷史任務還魂(法則 2/3/6 同場):某報表系統為「重整後恢復」加了重連機制,
但未過濾任務狀態、且輪詢每撈到一筆就開一個新分頁——使用者僅上傳一份檔案,
數天前的已完成批次逐一自己「開門走進來」佔滿分頁列。三重違規:終態被復活(法則 2)、
輪詢開新版面(法則 3)、旅程測試全部乾淨環境起跑所以全綠(法則 6)。
修法=狀態機補終態+完成未領取進收件匣+重連只認非終態+髒環境測試雙軌。
- 完成了卻找不到結果(法則 1/3):多份批次寫結果到 多份結果容器,交付視窗
卻讀單份的 state['result'] → 進度打了 ✓、開窗 404。根因=沒有統一狀態機,
單份/多份各自發明了「完成」的形狀。5 份成品其實好端端躺在磁碟——壞的是門牌,
不是房子;但對使用者而言「拿不到=沒做出來」,嚴重度按後者計。
- 全域狀態互踩(法則 3):多分頁共用模組全域變數+單一 DOM 容器,
切分頁互相 clobber、跑中視圖消失;計時器跨 await 交換全域=re-entrancy。
修法=per-tab context 穿透,狀態綁分頁不綁模組。
- 連點防護(法則 4):「開始」連按三次=三份任務。修法=前端去抖+服務端冪等,
連送 3 次只 1 次執行,測試鎖住。
- 姊妹技能:狀態機/終態的驗證思路同 verification-discipline(獨立期望值、
破壞測試會吠);「先查外部再開工」的時點紀律見 engineering-economy 法則 1;
交付站/旅程級驗收見 ui-station-delivery;髒環境雙軌與快取還魂的資料層對應
見 cache-discipline(壞 reader 種的快取結構性作廢)。
1---2name: async-task-ui-23description: 背景任務與多工介面紀律——任何「使用者按下開始、系統在背景長時間工作」的介面(報表產出、批次處理、匯入匯出、長查詢、AI 生成)怎麼做到不騙人、不失控、不還魂。用於設計或修改任務型 UI:狀態機先於介面、終態不可逆、完成未領取進收件匣不自動佔版面、視圖與任務分離(伺服器為真相源)、送出必冪等、並行語意顯性化、測試必含髒環境。背景任務 UI 的失敗模式極其固定:重複送出、進度假死、重整丟任務、歷史任務還魂、完成了卻找不到結果——全部有標準解。4---56# 背景任務與多工介面紀律(Async Task UI)78## 什麼時候用910任何時候介面上出現「開始」按鈕、而工作要跑超過幾秒:報表/文件產出、批次匯入、長時間查詢、11AI 生成、檔案轉換。這類介面的物理是固定的:**HTTP 是無狀態的、前端是會死的(重整/關頁/斷線)、12使用者是會亂點的**——而任務必須活得比這三者都久。業界對此有一套高度一致的標準骨架13(送出→立即回任務 ID→背景執行→狀態端點輪詢→終態→領取結果);這份技能是那套骨架14加上真實踩雷換來的鐵則。失敗模式極其固定,標準解也是——不需要發明,需要的是不偏離。1516---1718## 核心法則1920### 法則 1:狀態機先於介面——先定生死,再畫畫面2122動手寫任何任務 UI 元件之前,先把任務狀態機定下來:最小集23`queued(排隊)→ running(執行中)→ succeeded / failed / cancelled(終態)`,24外加正交的 `delivered(已領取)`旗標。25- **狀態轉移寫成一張表入碼**(哪個狀態能到哪個狀態),禁止散落在各處的 if;26 轉移表配測試——非法轉移會吠。27- 進度百分比、預估時間都是裝飾,**狀態才是骨架**;先有誠實的狀態,才配有進度條28 (給不出誠實的百分比就別給,狀態文字比假進度可信)。29- 沒先定狀態機就寫介面=每個元件各自發明任務的生死,之後每顆 bug 都是「兩處對任務30 狀態的理解不一致」。3132### 法則 2:終態不可逆——完成的任務進收件匣,絕不還魂3334終態(succeeded/failed/cancelled)是單行道:**任何自動機制(重連、輪詢、恢復)35永不把終態任務重新開成工作視圖**。36- 完成而未領取(succeeded 且未 delivered)的任務 → **收件匣/任務中心**:被動列出,37 使用者點了才開;絕不自動佔用分頁/版面。38- 歷史殘留任務(幾天前的、測試留下的)一律歸收件匣;「載入時把所有找得到的任務39 都開成分頁」是本技能最經典的案發現場(見下)。40- 收件匣同時解掉「任務完成時使用者不在場」的交付問題——結果有家可歸,不必賭41 使用者的視窗還開著。4243### 法則 3:視圖與任務分離——伺服器是真相源,前端只是望遠鏡4445任務的生死存亡**絕不繫於任何視窗/分頁/前端物件的存活**:46- 送出後任務屬於伺服器;前端拿 task_id 輪詢狀態端點——關頁、重整、斷線,任務照跑。47- **重整=望遠鏡重新對焦**:載入時查詢「進行中/排隊中」任務、把視圖掛回去;48 不是任務重生、更不是任務死亡。49- 重連機制只認**非終態**(法則 2);且「掛回」=更新既有視圖或掛回既有容器,50 **絕不因輪詢撈到東西就開新版面**——視圖的誕生只有兩個合法來源:使用者動作、51 載入時掛回真正在跑的任務。52- 多視圖(多分頁)時,每個視圖各持自己的狀態容器(per-tab context),53 禁全域單例——全域狀態跨 await 交換必有 re-entrancy 撞況。5455### 法則 4:冪等是送出的義務——連點、重試、重載都不得生出第二份工作5657背景工作天生會被觸發多次(連點、網路重試、頁面重載後再按)——**同一份邏輯工作,58跑幾次都只能算一次**:59- 前端:送出即禁用按鈕/去抖(第一道,擋手速)。60- 服務端:冪等鍵(內容鍵或請求鍵)——同鍵重複送達=回同一個 task_id,不開新任務61 (第二道,擋一切前端擋不住的)。兩道都要,只有前端的=沒有。62- 取消也要冪等:連按取消無害;跑中取消走 `cancel_requested` 中間態(UI 顯「取消中…」),63 由工作端在檢查點收斂到 cancelled——不假裝立即死,也不讓使用者狂點。6465### 法則 5:並行語意顯性化——不讓使用者猜,空白本身就是缺陷6667能不能再開一個?排在第幾?上限幾個?——這些問題的答案必須**寫在介面上**:68- 有佇列就顯示順位(「排隊中,前面 N 筆」),不顯示假的「執行中 0%」。69- 有上限就在達上限時**擋下並講明**(「已達上限 N,需先關閉一個」),不是默默失敗。70- 使用者對背景任務的核心焦慮是掌控感:重整安不安全、能不能平行、系統到底更新了沒71 ——UI 不明講,使用者就開始亂試,亂試就撞出法則 4 要擋的事。72- 一個新開的工作區必須立即可用或明講為何不可用;**渲染成空白讓人猜=缺陷**,73 不是「還沒做完的功能」。7475### 法則 6:髒環境是預設——乾淨環境全綠≠真環境全綠7677任務型 UI 的測試 fixture 必含**兩軌**:乾淨環境 + 髒環境(預置歷史終態任務78N 筆+進行中 1 筆再載入)。79- 真實使用者的環境永遠帶著歷史:昨天的任務、上週的殘留、測到一半的孤兒。80 只在乾淨環境起跑的測試,對「載入」這個最日常的動作是零覆蓋。81- 髒環境斷言最小集:載入後恰好掛回「真正在跑的」、終態 0 自動復活、82 收件匣列出全部未領取。83- 交付前的自走驗收,環境=**真實使用者環境的等價複本**(含全部殘留),84 乾淨環境自走=沒自走。8586---8788## 本專案案發現場(佐證,非通用必需)8990- **歷史任務還魂(法則 2/3/6 同場)**:某報表系統為「重整後恢復」加了重連機制,91 但未過濾任務狀態、且輪詢每撈到一筆就開一個新分頁——使用者僅上傳一份檔案,92 數天前的已完成批次逐一自己「開門走進來」佔滿分頁列。三重違規:終態被復活(法則 2)、93 輪詢開新版面(法則 3)、旅程測試全部乾淨環境起跑所以全綠(法則 6)。94 修法=狀態機補終態+完成未領取進收件匣+重連只認非終態+髒環境測試雙軌。95- **完成了卻找不到結果(法則 1/3)**:多份批次寫結果到 多份結果容器,交付視窗96 卻讀單份的 state['result'] → 進度打了 ✓、開窗 404。根因=沒有統一狀態機,97 單份/多份各自發明了「完成」的形狀。5 份成品其實好端端躺在磁碟——壞的是門牌,98 不是房子;但對使用者而言「拿不到=沒做出來」,嚴重度按後者計。99- **全域狀態互踩(法則 3)**:多分頁共用模組全域變數+單一 DOM 容器,100 切分頁互相 clobber、跑中視圖消失;計時器跨 await 交換全域=re-entrancy。101 修法=per-tab context 穿透,狀態綁分頁不綁模組。102- **連點防護(法則 4)**:「開始」連按三次=三份任務。修法=前端去抖+服務端冪等,103 連送 3 次只 1 次執行,測試鎖住。104- **姊妹技能**:狀態機/終態的驗證思路同 verification-discipline(獨立期望值、105 破壞測試會吠);「先查外部再開工」的時點紀律見 engineering-economy 法則 1;106 交付站/旅程級驗收見 ui-station-delivery;髒環境雙軌與快取還魂的資料層對應107 見 cache-discipline(壞 reader 種的快取結構性作廢)。