到昨天為止,教練其實看不到儀表板。
它一直跑在本地 localhost:5173,上課現場我拿給教練看的是手機裡的 HTML 檔,不是網址。
把 dist/ 丟上去這件事本身十分鐘就做完了,部落格就掛在 Cloudflare,加個子網域的事。
但 npm run build 按下去,第一件撞到的不是部署,是 build 工具自己跳出來的警告。
dist/index.html 0.49 kB │ gzip: 0.36 kB
dist/assets/index-BQh4EfYZ.css 22.19 kB │ gzip: 5.19 kB
dist/assets/index-CXWknSPw.js 707.00 kB │ gzip: 197.25 kB
(!) Some chunks are larger than 500 kB after minification.
整個儀表板打包成一個 JS 檔,707 KB。
裡面有五個分頁:總覽、六場全馬、配速計算、3D 路線、體重機。
而其中一個分頁跟其他四個完全不是同一個量級:3D 路線那頁要 Three.js。 node_modules/three 本體 25 MB,打包壓完之後也還有 580 KB,佔了整包的八成。
問題在於:打開這個儀表板的人,第一眼看到的是總覽頁。
但瀏覽器在畫出總覽頁之前,已經把整個 3D 引擎載下來了。 不管那個人會不會點進 3D 那頁。
以教練來說,他大概不會,他要看的是課表達成率和這週練了什麼,不必看東京馬的軌跡在空中轉。
Vue 有一個現成的做法:defineAsyncComponent 搭配動態 import()。
原本 App.vue 頂端是五行普通的 import,五個分頁的程式碼在 build 時被打成同一包。改成這樣:
import { ref, defineAsyncComponent } from 'vue';
import OverviewView from './views/OverviewView.vue';
// 總覽是進站第一眼,同步載入。其餘分頁動態載入 ——
// 3D 路線那頁光是 three.js 就佔掉整包的一半以上,而多數人不會點進去。
// 打開總覽卻先下載整個 3D 引擎,是沒有道理的。
const RacesView = defineAsyncComponent(() => import('./views/RacesView.vue'));
const Route3DView = defineAsyncComponent(() => import('./views/Route3DView.vue'));
const ScaleView = defineAsyncComponent(() => import('./views/ScaleView.vue'));
const PaceView = defineAsyncComponent(() => import('./views/PaceView.vue'));
只有總覽維持同步載入,因為那是每個人進站第一眼一定會看到的東西,把它也拆出去只會多一次來回。
其他四個分頁改成「點到才載」。import() 寫成函式呼叫,build 工具看到這個語法就知道:這個檔案要切成獨立的一塊,等真的被呼叫再抓。
<template> 那邊一個字都不用改,v-if 切分頁的寫法照舊。
dist/index.html 0.49 kB │ gzip: 0.36 kB
dist/assets/index-BQh4EfYZ.css 22.19 kB │ gzip: 5.19 kB
dist/assets/races-DQPEsZcJ.js 1.65 kB │ gzip: 1.08 kB
dist/assets/ScaleView-jpjt604J.js 4.90 kB │ gzip: 2.99 kB
dist/assets/PaceView-BoN0vEuf.js 7.06 kB │ gzip: 3.10 kB
dist/assets/RacesView-BalnynDg.js 19.33 kB │ gzip: 7.64 kB
dist/assets/index-DmpXxFDx.js 96.81 kB │ gzip: 38.66 kB
dist/assets/Route3DView-j789yytu.js 580.98 kB │ gzip: 148.05 kB
一包變八個檔。
| 拆分前 | 拆分後 | ||
|---|---|---|---|
| 首屏 JS | 707.00 kB | 96.81 kB | −86% |
| 首屏 JS(gzip) | 197.25 kB | 38.66 kB | −80% |
進站只載 97 KB:Vue 本體、Pinia、總覽頁,Three.js 那 580 KB 變成 Route3DView 自己的一塊,點進 3D 分頁的那一刻才開始下載。
其他三個分頁也各自成一塊,但它們都很小,拆不拆對首屏影響不到 30 KB。真正有差的只有 3D 那一塊。
再看一次拆分後的 build 輸出,最後一行:
(!) Some chunks are larger than 500 kB after minification.
還在
它抱怨的對象從 index 變成 Route3DView,580 KB 還是超過 500 KB 的門檻。警告沒有被解掉,只是從「每個人都要付」搬到「點進 3D 的人才要付」。
要不要繼續拆?
Three.js 本體就是那麼大,它是一個完整的 3D 引擎,可以再往下切,例如把 OrbitControls 之類的附加模組再分一塊,但那頁一進去就會用到那些東西,分開只是多幾次 request,總量沒有變少。
所以這裡的判斷是:這個警告是一個門檻,不是一個錯誤。 它提醒的是「有人會為這一塊付出下載成本」,而現在付這個成本的只剩真的要看 3D 的人,這個代價我覺得合理。
bundle 拆完,剩下的是資料。
這個 repo 是公開的,而 public/ 底下有三個檔案被 .gitignore 擋在外面:
public/sessions.json 逐日訓練紀錄
public/weights.json 體重與阻抗
public/load.json ATL / CTL / TSB
擋掉的理由 Day 18 提過:訓練紀錄會露出作息和常去的地點,體重是健康資料,程式碼公開沒問題,但資料不必跟著。
那公開版能放什麼?
六場全馬的路線本來就是公開的 ,東京馬、福岡馬、臺北馬的路線任何人都查得到,我的 GPS 軌跡跟官方路線圖差不了多少,3D 路線用的東京馬軌跡也是,這些可以上。
日常訓練的 GPS 不行 ,每一筆都會有訓練地點,容易透露習慣的訓練時間跟地點。
所以公開版和本機版跑的是同一套 code,差別只在 public/ 裡放了哪些檔案:
| 分頁 | 本機 | 公開版 |
|---|---|---|
| 總覽(達成率、體重、負荷) | ✓ | 沒有資料 |
| 六場全馬 | ✓ | ✓ |
| 配速計算 | ✓ | ✓ |
| 3D 路線 | ✓ | ✓(東京馬、8/13 間歇) |
| 體重機 | ✓ | 需要 Web Bluetooth,公開版無意義 |
這裡有一個容易漏掉的地方:.gitignore 擋的是 repo,不是 deploy。
那三個檔案在本機的 public/ 底下好好地躺著,npm run build 會原封不動把它們複製進 dist/。而 Cloudflare 的 wrangler deploy 上傳的就是整個 dist/。也就是說,什麼都不做直接部署,體重和逐日訓練紀錄會跟著上線,.gitignore 從頭到尾沒有機會攔。
所以多了一個 build:public,build 完先把不該上去的東西清掉再上傳:
"build:public": "npm run build && rm -f dist/sessions.json dist/weights.json dist/load.json && find dist/races -name '*.json' -not -name '*.min.json' -delete",
"deploy": "npm run build:public && wrangler deploy"
順手把那 3.5 MB 從沒被讀過的完整逐秒檔也清掉了,公開版的 dist/ 剩 2.5 MB。
那總覽頁在公開版長什麼樣?三個 JSON 都是 404,原本會在訓練紀錄卡片裡顯示一行「讀不到訓練紀錄:HTTP 404」,像壞掉。現在改成:三個檔都 404 就當作是公開版,頂端放一張說明卡,本週跑量和負荷曲線顯示「僅本機」,不放示意數字。

一套 code、兩份資料。公開版掛在部落格的子網域底下,跟主站互不干擾:
https://training.superbeeee.com
上線後直接打三個個人資料的網址確認過,sessions.json、weights.json、load.json 都是 404,完整逐秒的賽事檔也是,不該上傳的東西都不在。
誠實地說:解了一半。
教練現在有一個網址可以開了:training.superbeeee.com。他點進去看得到六場全馬疊在一起、看得到東京馬的軌跡在空中轉、可以算配速。
但他真正要看的是總覽那頁:這週練了什麼、達成率多少、負荷曲線長什麼樣,那頁在公開版上是空的。 那些資料不能上網,所以教練能看到「我練得如何」的地方,仍是 Day 18 那份單檔 HTML 週報。
第一項的「教練看不到儀表板」,變成了「教練看得到儀表板,但看不到裡面最重要的那頁」。
不是沒有辦法,Cloudflare Access 設一條 policy、或自己寫十幾行 Worker 擋 Basic Auth,都能讓教練登入後看到總覽頁。真正讓我停手的是它改變了保證的性質——現在的保證是「那些檔案不在網路上」,加了鎖之後變成「那些檔案在網路上,但有一道鎖」。「不在」確認一次就結束了,它不會自己變回來;「在、但被擋住」得一直確認下去,而且我只驗得到我想得到的那幾個網址。
這是資料隱私和「給教練看」之間的取捨。三十天內我沒把握維護後者,所以留著週報那條路,待未來再改善。
系統的部分到今天為止告一個段落。
Day 1 的時候我寫過:最後一天,這套系統要能回答那個問題:我練得如何。
明天不寫新功能,明天打開它,問它這個問題。