前三天,我們把展示櫃(viewer-core)蓋好了:能轉、能點、能帶你看重點。但它還住在「工廠」裡,得有人在旁邊開著開發伺服器才看得到。展場不是這樣:網路可能很爛、根本沒有,現場人員也不會下工程指令。
今天的任務:把整座展示櫃裝進一只行李箱,到現場打開就能演。這只行李箱在程式裡叫「離線播放包」(exhibit_pack/),做出它的工具叫 exhibit-kiosk(kiosk 就是展場那種放著給人戳的螢幕)。
| 行李箱裡的東西 | 實際是(demo 實測大小) | 內容 | 白話 |
|---|---|---|---|
| 入口 | index.html(1.3 KB) |
依序載入下面幾個 script | 唯一要雙擊的檔案 |
| 開場海報 | poster.png(124 KB) |
來自資產的等角截圖,沒有就自己畫佔位圖 | 櫃子還沒開燈時的招牌 |
| 說明書 | scene.json(5 KB) |
window.__EXHIBIT_SCENE__ = {...};(結構欄位:id/anchor/camera/order) |
鏡頭怎麼擺、哪些重點、怎麼巡場(Day 03、11 的說明書) |
| 解說文字 | hotspots.json(0.6 KB) |
window.__EXHIBIT_HOTSPOTS__ = {...};(title/body/media) |
每個熱點的標題與文案,單獨抽出來給現場人員改 |
| 展品(備份) | exhibit_kiosk.glb(61 KB) |
模型原檔,執行時不讀,只給安檢看 | 那顆齒輪減速機,原樣一份 |
| 展品(實際用的) | assets/model-data.js(81 KB) |
同一顆模型轉成 base64 文字:window.__EXHIBIT_ASSETS_B64__ = {...}; |
那顆齒輪減速機,包成文字塞進包裹的另一份 |
| 展示櫃的機關 | assets/kiosk.bundle.js(736 KB) |
esbuild 打包的 viewer-core +展場行為(iife、壓縮) |
打包好的檢視器加上展場專用的行為 |
| 備用電 | assets/poster.js(1 KB) |
保底狀態機,最先載入 | 一個很小、最先載入的保底程式 |
| 樣式 | assets/kiosk.css(3 KB) |
放大字體、加粗按鈕、海報樣式 | 裝飾與尺寸 |
| 解碼器(備而不用) | assets/decoders/(約 2.4 MB) |
three.js 的 Draco/KTX2 解碼器本機副本 | 目前的模型沒壓縮,用不上 |
| 給現場的說明 | README.txt(3 KB) |
白話的使用與改字教學 | 現場人員的說明書 |
我用真實資產(ast_gearbox_demo)重新打包一次,整個資料夾約 3.4 MB,其中主程式(機關)約 736 KB、模型本身只有約 61 KB;剩下大部分是預先放進去的壓縮解碼器(約 2.4 MB,下面「還沒做」會說明為什麼現在用不上)。
在自己電腦上開發時,我們開一個小伺服器,用網址看網頁,瀏覽器覺得「這是正常網站」。但展場是直接打開檔案(網址以 file:// 開頭),瀏覽器覺得「這是來歷不明的檔案,憑什麼去翻別的檔案?」於是有一條規矩:
這個檔案不准打電話給別人要資料(不能用
fetch、XMLHttpRequest這類「請求」,也不准用type="module"的載入方式)。
但直接裝在包裹裡送到手上的(一般的<script src>標籤)可以。
為了確認這件事是規矩、不是某個瀏覽器的毛病,我寫了一個十行的小測試頁,請人在 Safari、Chrome、Edge、Arc 各雙擊一次。fetch 和 XMLHttpRequest 兩種「打電話」方式,四個瀏覽器全部失敗。
所以打包方式整個順著規矩改:
| 原本的做法(打電話) | 現在的做法(拆包裹) |
|---|---|
| 說明書是純 JSON 檔,執行時去讀 | 包成一小段程式 window.__EXHIBIT_SCENE__ = {...};,用 <script src> 載入 |
| 解說文字同上 | 同上(window.__EXHIBIT_HOTSPOTS__) |
模型(GLB)由 viewer-core 內部呼叫 fetch() 讀取 |
模型轉成 base64 文字,同樣用 <script src> 載入;再對全域 fetch 攔截:請求的檔名對得上手上有的,就直接組一個回應遞過去 |
打包成 ES module(type="module") |
改成一般 script(format: "iife") |
最後那個攔截很好玩:viewer-core 完全不知道自己沒有真的打電話,它以為模型是從網路載來的,所以檢視器一行都不用改。
教訓:「本機伺服器上看起來正常」跟「真的雙擊」是兩種不同的安全規則。這個專案的驗收寫成「拔網路線、雙擊」,就是因為這件事。
上表 scene.json 那欄寫的 window.__EXHIBIT_SCENE__ = {...}; 就是「把資料改寫成一行 JavaScript 的變數賦值」:window 是瀏覽器頁面的全域物件,__EXHIBIT_SCENE__ 是我們取的變數名,等號右邊的 {...} 跟原本 JSON 的內容一模一樣。瀏覽器用 <script src="./scene.json"> 載入它(就是 HTML 引用外部腳本檔,不是把程式碼塞進別處),執行這一行後,資料就已經放在 window 上,展示櫃的機關直接讀就好,完全不必「打電話」去要。
這算後門嗎?不是。它沒有繞過任何限制,只是走了瀏覽器本來就允許的那條路(file:// 下 <script src> 可以載入同資料夾的腳本);很多離線文件(例如程式庫的離線搜尋索引)也是這樣做的。代價是檔名雖叫 .json,內容不是純 JSON,所以拿去做 JSON 格式檢查(linter:專門檢查 JSON 括號、逗號、引號有沒有寫對的工具)會報錯。
check行李箱打包完,要過一道安檢(exhibit-kiosk check),回傳一行結果,欄位有:
external_requests:有沒有任何東西偷偷想連外面的網路(必須是空的)missing_files:index.html 提到的檔案,行李箱裡都在嗎invalid_glb:模型有沒有壞verdict:三項都乾淨才是 pass
安檢本身修過兩次:
src/href、fetch() 呼叫、CSS 的 url())。模型載入需要一點時間,這段空檔不能是白畫面:
reports/views/iso.png),沒有就由程式自己畫一張中性的佔位圖。保底的狀態機刻意放在一個約 1 KB、最先載入的獨立檔案(poster.js)。理由是:如果它跟大檔案綁在一起,大檔案壞了,保底也跟著壞,等於停電時備用電也一起斷。
驗證方式是「一個一個把東西弄壞」:
| 弄壞什麼 | 結果 |
|---|---|
| 內嵌模型截掉三分之二 | 停在海報並顯示提示 |
| 主程式被截斷 | 約 20 秒後出現逾時提示 |
| 主程式整個不見 | 約 1 秒內出現提示 |
| 解說文字檔少一個逗號 | 3D 照常,但熱點標題退成 hs_01、解說消失 |
| 閒置多久 | 發生什麼 | 由誰決定 |
|---|---|---|
| 20 秒 | 展品自己慢慢轉(每秒 12 度) | 說明書的 auto_rotate |
| 60 秒 | 鏡頭回到初始位置 | 說明書的 idle_reset_s |
| 120 秒 | 自動巡場:依序飛到 8 個重點,每站停 8 秒,繞完再繞 | 說明書的 tour+展場程式的預設值 |
60 秒跟 120 秒是兩個互相不認識的計時器:前者只看「有沒有人在畫布上拖曳」,後者看整個頁面有沒有人按下去。這樣不必去改檢視器。
實機才抓得到的 bug:我原本把「滑鼠動了」也算成「有人來了」。在真的電腦上,滑鼠放著不動也會有微小飄移,所以「閒置 120 秒」幾乎永遠等不到,但 60 秒那個因為只認拖曳,一直正常。兩者行為對不起來,才被抓到。現在只認真的按下去、滑了一下、按了鍵。
解說文字單獨在 hotspots.json,用記事本打開、改標題或內文、存檔、重新整理就生效,不必重新打包。說明檔還特別寫了:開頭那行不要刪;改壞了不會當機,只是標題變編號,改回去就好。
下一篇:檢查規則與場景包。