到目前為止,這條產線已經可以產出 Word、PDF、HTML,還有教學影片。今天來做最後一種:操作導覽。
操作導覽是一種直接內嵌在產品中的教學,本質上與前面幾種產出不太一樣,並不是獨立於產品的文件。它通常會放在畫面中顯眼的位置 (e.g. App 右上角的「?」按鈕)。開啟操作導覽後,它會一步一步指著畫面上的元件,告訴使用者現在該點哪裡、該填什麼。
為了方便大家理解,可以先看一下今天的成果:使用者按下「?」、選「新增攝影機」,接著照著導覽一步一步把攝影機建立起來。

要做導覽,最直覺的做法是找一個導覽套件,然後在程式碼裡一步一步寫「第一步指向這顆按鈕、標題是什麼、說明是什麼」。但這樣就又多了一份要手動維護的東西,UI 一改,導覽就跟著過期,這正是這個系列從 Day 01 開始就想解決的問題。
仔細想想,導覽需要的資訊,產線裡其實都已經有了:
| 導覽需要的 | 產線裡已經有的 |
|---|---|
| 要指向哪個元件 | manifest 每個 step 的 testid |
| 這個元件叫什麼 | annotate 的 legend (Day 12) |
| 這一步要怎麼做 | 正文的編號步驟 (Day 18) |
| 做完會看到什麼 | 正文的「完成後」 |
差別只在於「誰來操作」:截圖是 runner 替使用者點按鈕、填欄位,然後按快門;導覽則是 App 指著按鈕,請使用者自己點。所以導覽跟 Day 24 的影片一樣,只是同一份 manifest 的另一種產出。
整體的流程是這樣:
npm run tour」得到「tours.json」tours.json」進行 App 裡的導覽導覽套件有很多選擇,範例專案用的是 driver.js (MIT 授權),裝在 App 這一側:
npm install driver.js -w demo-stream-app
或許各位讀者會想問:先前 Day 11 不是已經自己做了一套疊層,可以畫框、畫標號了嗎?為什麼不直接拿來用?
這是因為,當初設計那套流程,是為了修飾文件中的截圖用的,它的功能某種程度上與導覽套件的功能重複,既然別人都做好了,直接拿來用不香嗎?

App 端的程式碼只有一小段:讀 tours.json,把每一步轉成 driver.js 的步驟,再在右上角放一個「?」選單。這段只要寫一次,交給 AI Agent 就能完成,這裡就不貼了。
範例專案 auto-manual-gen 的 chore/day26 分支新增了一個 tour 指令。要做成導覽的章節,在 manifest 明確標上 tour: true:
id: camera-add
title: { zh-Hant: 新增攝影機, en: Adding a Camera }
video: true
tour: true
執行 npm run tour,就會產出 App 讀的 tours.json。以「新增攝影機」為例 (中文版,省略部分欄位):
{
"id": "camera-add",
"steps": [
{ "kind": "click", "testid": "camera-add", "text": "點擊清單右上角的「新增攝影機」。" },
{ "kind": "fill", "testid": "camera-dialog-name", "title": "顯示名稱", "example": "大門西側" },
{ "kind": "info", "testid": "camera-dialog-zone", "title": "安裝位置" },
...
{ "kind": "done", "title": "新增攝影機", "text": "畫面出現「已新增攝影機『…』」的通知,表示攝影機已經建立。" }
]
}
轉換的規則很單純,manifest 的每一種 step 對應到導覽的一種 kind:
| manifest | 導覽 | App 怎麼往下走 |
|---|---|---|
click / dblclick |
click / dblclick |
等使用者自己點了這個元件 |
fill |
fill |
使用者輸入完,按「下一步」 |
截圖的 annotate |
info |
介紹這個元件,按「下一步」 |
| 正文的「完成後」 | done |
最後一步,不指向任何元件 |
waitFor 等 |
(略過) | runner 自己要等的,使用者看不到 |
每一步的說明文字,則跟昨天的字幕一樣:用截圖當錨點,把正文的編號步驟配對到 manifest 的 step。字幕跟導覽用的是同一套配對,不用再寫一份。
「介面總覽」這種沒有操作的章節,就只剩 info 跟 done:逐一介紹三個區域,最後加上「完成後」的說明,一共四步。

這份 JSON 刻意跟導覽套件無關,裡面只有 testid、文字與 kind。之後如果想換一套套件,只要改 App 端那一小段轉換,產線這邊不用動。
有一點值得提一下:轉換規則雖然單純,但有時候照 manifest 的順序直接轉並不適合。
「新增攝影機」的第一張截圖標了四個欄位,接著又要填其中兩個。照順序轉的話,導覽會先把四個欄位介紹一遍,再回頭請使用者填「顯示名稱」,同一個欄位被指了兩次。
轉換時,標註的欄位如果接下來要輸入,就在介紹到它的時候直接請使用者輸入;標註的元件如果就是下一個要點的 (例如「建立」),就跟點擊併成一步。
這邊提幾點我覺得該說明的東西。
在這個範例中,導覽只能往前,不能回上一步。
一般的導覽只是介紹畫面,前後切換都沒關係;但這裡的步驟是有副作用的,使用者點了「新增攝影機」,對話框就開了,這時候回到「請點擊新增攝影機」,按鈕已經被對話框蓋住了。截圖的 runner 每一章都會重新開機,導覽沒辦法,想重來就只能關掉再開一次。
五章裡面,我只挑了兩章做成導覽。原因是手冊是在固定的示範資料上拍攝的 (Day 09),導覽卻是在使用者真實的畫面上進行,所以依賴示範資料或目前狀態的章節就不適合:
| 章節 | 適合嗎 | 原因 |
|---|---|---|
| 介面總覽 | ✅ | 只介紹區域,沒有操作 |
| 即時監控畫面 | ❌ | 要雙擊「Lobby-01」,但使用者的清單裡不一定有這台 |
| 儲存版面設定 | ❌ | 要雙擊「Gate-A」,同上 |
| 影像分析設定 | ❌ | 要「開啟」人臉辨識,但如果使用者已經開了,照著點反而會關掉 |
| 新增攝影機 | ✅ | 從按鈕開始,不依賴任何既有資料 |
這也是 tour: true 要明確標出來、而不是預設全部都做的原因。
就算章節本身適合,正文裡還是可能藏著示範資料。「新增攝影機」的「完成後」寫的是:
畫面出現「已新增攝影機『大門西側』」的通知
放在手冊裡沒問題,截圖上確實是「大門西側」。但放進導覽,使用者輸入的是自己的名稱,說明卻寫著「大門西側」,看起來就像導覽壞掉了。
所以轉換時,會把 manifest 裡所有 fill 的示範值在說明裡換成「…」,只在 fill 那一步當作範例出現:


範例只是參考,這裡輸入的是「測試門口」,最後一步的說明不會跟右下角的通知打架。
導覽的內容只有一個來源:manifest 跟正文。只要它們是對的,導覽就是對的。
前面幾天的產物 (截圖、Word、影片) 都不進版控,但 tours.json 是例外。截圖是給手冊用的,每次 build 都會重新產生;tours.json 則是 App 的一部分,要跟著 App 一起打包、跟著 App 的版本發布。它的角色比較像 App 的 i18n 檔,所以直接放在 App 的原始碼裡,一起 commit。
既然是產生出來的檔案,就要防止它過期。npm run tour -- --check 會重新產生一次並跟現有的檔案比對,對不上就以非 0 結束。這跟 Day 23 的 sync 一樣,可以直接放進 CI 或 pre-commit,不用靠人記得。
| 想做的事 | 要做什麼 |
|---|---|
| UI 或正文改了 | 照平常一樣更新 manifest 與正文,再跑一次 npm run tour |
| 多做一章導覽 | 在 manifest 標上 tour: true,跑 npm run tour |
| 拿掉一章導覽 | 拿掉 tour: true,跑 npm run tour |
「?」選單裡的章節也是直接從 tours.json 列出來的,App 的程式碼完全不用動。真的要改程式碼的,只有這幾種情況:
hover,要在轉換規則裡多加一種 kind。tours.json 會照 manifest 的語言自動產生,但「下一步」、「完成」這幾個按鈕文字要補進 App 的語系檔。今天為產線加上了最後一種產出:操作導覽。
回頭看,這件事能這麼快做完,靠的還是 Day 08 的 data-testid。截圖、多語言、影片,再到今天的導覽,找元件的方式從頭到尾都沒變過。如果當初是用畫面上的文字或 CSS class 找元件,導覽一上線就會跟著 UI 一起壞掉。
接下來,就可以來收尾了。把這整個產線整理一下,讓同事也可以使用XD