
文章同步發表在我的個人 Blog
前五天都在本機跑,今天要把投票頁放出去。
投票頁在這裡:https://room.gooliya.com/
走進小孩房,在白板上貼三個圓點就好,大概一分鐘。貼完才看得到大家怎麼選,如果你家裡也在帶小孩,歡迎進來貼一下,這份結果我後面幾天會拿來排功能的順序。
在開始之前想先聊聊。這次的問卷我想跳脫過去的做法,上線前有找 Jeff 聊過,他是資深研究員,平常也在教問卷設計。問卷要有趣味性,也要顧到填答效率,這兩件事得互相權衡。這一版設計的時間不夠,只能先做到這裡,未來有時間再回頭研究。
這篇寫的是從本機到正式環境的那一段:正式資料庫怎麼建、部署指令怎麼下、上線前我用什麼確認沒有出包。
你們家也有嗎?:https://room.gooliya.com/

走進一間小孩房,走到牆上的家庭白板前,用三個圓點貼出你們家比較常發生的情況。題目是「最近 1 個月,帶小孩的日常裡,哪些事最常讓你或另一半說出『你怎麼沒跟我說』?」,貼完才看得到大家怎麼選。

桌機用 WASD 走路,走到白板前按 E。手機請橫著拿,左下角有搖桿。

貼滿三票才看得到結果。百分比算的是「所有圓點中,各情境占多少」,不是「多少家庭選了這個」,因為一個人可以把三票押在同一張。

這個投票頁整套都放在 Cloudflare 上,用到四個服務,先簡單說明各自負責什麼。
| 服務 | 是什麼 | 這個專案用它做什麼 |
|---|---|---|
| Workers | Cloudflare 的程式執行環境,程式碼部署上去之後會散在它全球的節點,有人連進來就由比較近的節點執行 | 跑投票的 API |
| Static Assets | 掛在 Workers 上的靜態檔案服務 | 送前端 build 出來的 dist,不必另外開一個 Pages 專案,網頁和 API 就會在同一個網域底下 |
| D1 | Cloudflare 的 SQLite 資料庫,用一般的 SQL 操作 | 存選項、票數和 Email 訂閱,Worker 透過 binding 直接讀寫,程式裡不用寫連線字串,也不用管密碼 |
| Custom Domain | 自訂網域,綁定之後 DNS 記錄和憑證都由 Cloudflare 建好 | 綁上這個投票頁的 room.gooliya.com |
這些都寫在同一個設定檔 wrangler.jsonc 裡:
{
"name": "little-room-mvp",
"main": "worker/index.ts", // the API
"routes": [{ "pattern": "room.gooliya.com", "custom_domain": true }],
"assets": { // the web page
"directory": "./dist",
"not_found_handling": "single-page-application",
"run_worker_first": ["/api", "/api/*"] // API first, then static files
},
"d1_databases": [{ "binding": "DB", "database_name": "little-room-mvp", "database_id": "..." }]
}
run_worker_first 這一行是關鍵:/api 開頭的請求先交給 Worker,其他路徑才去找靜態檔案。少了它,API 會被當成找不到的頁面,直接回前端的 index.html。
操作這些服務的指令列工具叫 Wrangler,建資料庫、套 migration、部署都是用它。下面的步驟都會用到。
總共五個步驟。
建立正式資料庫
wrangler d1 create little-room-mvp # returns the production database id
把 ID 填進設定檔。wrangler.jsonc 換成剛拿到的 ID。本機開發仍然用同一個 binding 加 --local,兩邊不會互相影響。
這裡有個小問題:本機的 D1 檔案是按 database_id 存的,ID 一換就等於指向一個全新的空資料庫。換完之後本機的 API 全部回 500,重跑一次 npm run db:migrate:local 才恢復。
套用 migration
npm run db:migrate:remote # wrangler d1 migrations apply DB --remote
兩份 migration 根據順序執行:第一份建四張表並寫入五個正式選項,第二份是 Email 訂閱的表。
確認 seed。直接查正式資料庫,確認五個選項都在、排序正確、都是啟用狀態。
wrangler d1 execute DB --remote --command \
"SELECT sort_order, id, category FROM survey_options ORDER BY sort_order"
部署
npm run build && wrangler deploy
build 產生的 dist 和 Worker 會一起上去,部署完就拿到網址。
正式環境跟本機差比較多的是 cookie 和權限,所以我沒有只在畫面上點一點,而是直接對正式網址打 API。
| 檢查 | 結果 |
|---|---|
cookie 是否帶 Secure |
有,本機因為是 http 所以沒有,正式環境自動加上 |
| 三票上限 | 第 4 票回 409 |
| 取消別人的票 | 回 404,對方票數不變 |
| 未投滿三票就查結果 | 回 403 |
| 結果 API 的回應內容 | 只有選項 id、票數、百分比,沒有 session、沒有原始投票 |
| 補充情境的狀態 | 一律 pending,其他人看不到 |
| 無效 Email 直接打 API | 全部擋下來 |
| 公開 API 會不會回傳 Email | 不會 |
| 伺服器 log | 沒有 Email |
另外開一個無痕視窗走過一次,確認新訪客看到的是乾淨的狀態,不會看到別人的票或別人寫的便利貼。
pending,但還沒有介面可以審,所以目前不會公開顯示。另外:清掉 cookie 或換一個瀏覽器,就可以再投三票。 這是匿名投票的先天限制,要擋住得加真人驗證或 IP 限流,而這兩項我這次都沒做。
也因為這樣,畫面上不會寫「每個人只能投一次」。而這份結果我會當成互動式的意見收集,不是具代表性的統計研究。
正式資料庫建好了、migration 套用完成、五個選項確認無誤、Worker 部署上線,桌機和手機都完整走過一次流程。
還沒處理的是上線 smoke test 留下的測試資料,現在還在正式資料庫裡,開始收真實資料之前會先清掉。
Day 7 開始進 Swift。我想先找一些方法,讓自己可以比較快地探索和學習 Swift 的程式碼,