回部落格
iThome 鐵人賽Vibe CodingCloudflare WorkersD1Wrangler部署

iThome 鐵人賽 Day 6:投票頁上線,建立正式 D1、部署 Worker,上線前我檢查了什麼

把投票頁從本機搬到 Cloudflare:Workers、D1 和自訂網域各自負責什麼,以及換了 database_id 之後踩到的坑。

Steven Chang
Steven Chang
2026年9月20日 01 Min read
訂閱
iThome 鐵人賽 Day 6:投票頁上線,建立正式 D1、部署 Worker,上線前我檢查了什麼

本文同步發表於 iThome 鐵人賽,系列:神隊友.swift:30 天 Vibe Coding 打造育兒 iOS App

前五天都在本機跑,今天要把投票頁放出去。

在開始之前想先聊聊。這次的問卷我想跳脫過去的做法,上線前有找 Jeff 聊過,他是資深研究員,平常也在教問卷設計。問卷要有趣味性,也要顧到填答效率,這兩件事得互相權衡。這一版設計的時間不夠,只能先做到這裡,未來有時間再回頭研究。

這篇寫的是從本機到正式環境的那一段:正式資料庫怎麼建、部署指令怎麼下、上線前我用什麼確認沒有出包。

1. 先看成品

你們家也有嗎?:https://room.gooliya.com/

你們家也有嗎?

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

家庭白板

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

投票結果

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

收尾畫面

2. 用到 Cloudflare 的哪些東西

這個投票頁整套都放在 Cloudflare 上,用到四個服務,先簡單說明各自負責什麼。

服務是什麼這個專案用它做什麼
WorkersCloudflare 的程式執行環境,程式碼部署上去之後會散在它全球的節點,有人連進來就由比較近的節點執行跑投票的 API
Static Assets掛在 Workers 上的靜態檔案服務送前端 build 出來的 dist,不必另外開一個 Pages 專案,網頁和 API 就會在同一個網域底下
D1Cloudflare 的 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、部署都是用它。下面的步驟都會用到。

3. 部署做了什麼

總共五個步驟。

建立正式資料庫

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 會一起上去,部署完就拿到網址。

4. 上線前檢查了什麼

正式環境跟本機差比較多的是 cookie 和權限,所以我沒有只在畫面上點一點,而是直接對正式網址打 API。

檢查結果
cookie 是否帶 Secure有,本機因為是 http 所以沒有,正式環境自動加上
三票上限第 4 票回 409
取消別人的票回 404,對方票數不變
未投滿三票就查結果回 403
結果 API 的回應內容只有選項 id、票數、百分比,沒有 session、沒有原始投票
補充情境的狀態一律 pending,其他人看不到
無效 Email 直接打 API全部擋下來
公開 API 會不會回傳 Email不會
伺服器 log沒有 Email

另外開一個無痕視窗走過一次,確認新訪客看到的是乾淨的狀態,不會看到別人的票或別人寫的便利貼。

5. 沒有先做的部分

  • Turnstile 真人驗證:現在完全沒有。
  • IP 限流:沒有。
  • 審核後台:補充情境寫進資料庫是 pending,但還沒有介面可以審,所以目前不會公開顯示。
  • 結果分析:資料都在 D1,搜集完後再決定如何分析。

另外:清掉 cookie 或換一個瀏覽器,就可以再投三票。 這是匿名投票的先天限制,要擋住得加真人驗證或 IP 限流,而這兩項我這次都沒做。

也因為這樣,畫面上不會寫「每個人只能投一次」。而這份結果我會當成互動式的意見收集,不是具代表性的統計研究。

6. 目前的狀態

正式資料庫建好了、migration 套用完成、五個選項確認無誤、Worker 部署上線,桌機和手機都完整走過一次流程。

還沒處理的是上線 smoke test 留下的測試資料,現在還在正式資料庫裡,開始收真實資料之前會先清掉。

明天

Day 7 開始進 Swift。我想先找一些方法,讓自己可以比較快地探索和學習 Swift 的程式碼。

分享

iThome 鐵人賽 Day 6:投票頁上線,建立正式 D1、部署 Worker,上線前我檢查了什麼
iThome 鐵人賽 Day 6:投票頁上線,建立正式 D1、部署 Worker,上線前我檢查了什麼

把投票頁從本機搬到 Cloudflare:Workers、D1 和自訂網域各自負責什麼,以及換了 database_id 之後踩到的坑。

Gooliya 鼓櫟數位
Steven Chang

Steven Chang

軟體工程師,平常主要有個人接案、DevOps、系統開發、維運。研究 AI 是為了省下時間陪小朋友。

專案卡住了,或是想把重複的工作交出去?

我做 AI 專案救援、系統架構開發、AI Agent 與流程自動化。先聊聊你的情況,不用先想清楚要做什麼。

免費訂閱 Gooliya 鼓櫟數位電子報

雜談 AI、開發與自動化,新文章上線時寄給你。

訂閱服務由 Kit 提供,Email 只用來寄送新文章