接上大家的倉庫:從讀到寫
到今天為止,你頁面上的東西都是「你自己的」——你寫死的清單、你這台電腦記住的跟團紀錄。今天要把它接到一個全班共用的地方,先讀,再寫。寫進去的那一刻,同事的畫面上會出現你的名字。
開始之前 · 30 秒
- 今天改的還是你上一堂那個檔案——不用新開資料夾(第 3 堂起就是一頁持續演進)
- 忘記它在哪?直接問 Claude Code:「我的午餐揪團頁是哪一個檔案?」
- 今天全程看本機那個檔案就好,不用每次都 push 上線。最後交作業時再一起 push
本堂材料 · 下面的 Prompt 都已經幫你填好了,這裡是備查
| ① 資料網址 | https://vibe-course.mixxin.tw/api/lunch貼到瀏覽器就看得到——這是你的頁面等一下要抓的東西 |
| ② 投票網址 | https://vibe-course.mixxin.tw/api/join送出的內容: {"name": "店名", "who": "你的名字"} |
| ③ 店家表(Notion) | 點我開啟 · 餐廳列表 全班共用的餐廳清單,維護按鈕要連到這裡。只有講師改得動 |
| ④ 跟團紀錄表(Notion) | 點我開啟 · 跟團紀錄 你等一下投的每一票都會出現在這裡 |
| ⑤ 唯讀金鑰 | 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只能讀、不能寫,而且只開得了上面那兩張表 |
這跟等一下第 05 節要講的事情正好是一體兩面:金鑰的價值取決於它能做什麼。一把只能讀午餐清單的鑰匙可以印在講義上;一把能寫入的鑰匙連放進你的頁面都不行。
在動手之前先看清楚今天的地形。你的頁面要拿到全班的資料,中間會經過三格——而你今天只動最左邊那一格。
| ① 你的頁面 | 就是你一直在改的那個 index.html。今天要教它「去別的地方拿資料」 |
| ② 我們的伺服器 | 講師已經架好了。它手上有一把鑰匙,負責去 Notion 拿資料、整理乾淨再回給你 |
| ③ Notion 那張表 | 全班共用的資料就放在這。你等一下會親手打開它、也會親手寫一筆進去 |
一定會有人問:為什麼要中間那台?我的頁面不能直接去問 Notion 嗎?自己試一次比較快——
- 把這個網址貼到瀏覽器網址列按 Enter:
https://api.notion.com/v1/users - 你會看到一段錯誤訊息,大意是「你沒有帶通行證」——這就是「需要金鑰的資料」長的樣子
(另外我們也實測過:Notion 根本不接受瀏覽器直接打過去的請求,會被擋下來。)這條界線叫什麼、為什麼存在,本堂稍後第 06 節整段講。
現在就點開這張表:全班共用的餐廳列表 ↗
你會看到一張表。一列就是一筆紀錄,一欄就是一種資料(店名、跟團人數、發起人、日期)。就這樣,沒有更神祕的東西——你每天在 Notion 開的那些表,就是資料庫。
- 點開餐廳列表,確認你打得開
- 數一下現在總共有幾列——等一下你的頁面上要出現同一個數字
- 看一下欄位有哪些:店名、跟團人數、發起人……這些等一下會變成你頁面上的卡片
第 4 堂你抓過的那包 JSON,跟這張表其實是同一種東西的兩種樣子——表格是給人看的,JSON 是給程式看的。等一下你會看到同一筆資料的這兩個版本。
你要做的事:把頁面裡寫死的那份清單,換成「去問我們的伺服器」。
注意這一步只有讀。那張店家表是講師在 Notion 維運的——新增一家店、改地址、把收掉的店下架,都在 Notion 上做。你的頁面只負責把它顯示出來,動不了它。
REPLACE_ME——把它換成你的名字。起手式 · 用自己的話改
我的頁面目前的地點清單是寫死在程式碼裡的。 請改成從這個網址抓資料:https://vibe-course.mixxin.tw/api/lunch 抓到之後,顯示全班目前湊了幾家店,每一家做成一張卡片(店名、地址、評分)。 有些店沒有評分,那些不要顯示星等,也不要出現 null。 抓不到的時候要顯示「暫時讀不到,等一下再試」,不要整頁壞掉。 另外請在頁面上加一個「維護店家清單」的按鈕, 點了在新分頁打開這個網址:https://app.notion.com/p/3bb95ea1884180a8b215fcb748371e19 重要:請保留我原本的「我要跟」按鈕和它的 localStorage 邏輯, 不要把它們刪掉或重寫——等一下的步驟還要用到那顆按鈕。
- 貼上 Prompt A,讓 Claude Code 改你的頁面
- 重新整理頁面——清單變成從店家表來的了
- 按一下「我要跟」,確認數字還是會加(如果按鈕不見了或按了沒反應,跟 AI 說「我原本的我要跟按鈕不見了,請加回來」)
- 點那顆「維護店家清單」按鈕,確認它會開到 Notion 那張表
- 問旁邊的同事:你們畫面上的店家總數一樣嗎?
它把「畫面」跟「資料的家」連起來了。以前你要改店家資料得去改程式碼,現在任何人(包括不會寫程式的人)都能點那顆按鈕、直接在 Notion 上改一格,你的頁面下次重整就跟上。這就是「資料跟程式碼分開」的好處——維運的人不需要動到程式。
① 畫面沒變或變成空白 → 右鍵 → 檢查 → 上面那排找到 Console,紅色的字就是它在抱怨什麼。
② 把整段紅字複製起來,貼給 Claude Code,加一句:「我的頁面出現這個錯誤,請解釋原因並修好它。」
③ 想看資料到底有沒有回來 → 切到 Network 分頁,重整一次,找到那條請求點開看。
這兩個面板第 4 堂都用過,今天是它們真正派上用場的一天。
現在你的頁面讀得到全班的資料了。所以做個實驗:按下你頁面上的「我要跟」,然後請旁邊的同事重整他的頁面。
因為你按下去的那個動作,根本沒有送出去,它只是掉回你自己那台電腦的抽屜(localStorage)裡。讀可以隨便讀、寫卻不行——這個不對稱不是漏做,是接下來三節要解釋的東西。
要理解剛剛那面牆是什麼做的,先親手拿一把鑰匙。這把不是給頁面用的(頁面不能放鑰匙),是給你自己電腦上的 AI 用的,讓它可以直接去問 Notion。
密碼是「證明你是你」,金鑰比較像一把配好的鑰匙——可以只開特定幾扇門。今天給你的這把只能讀、不能寫,而且只開得了那一個資料庫。等一下你會親眼看到「不能寫」是真的。
先解決一個一定會卡住的地方
等一下建好之後,請按這個組合鍵確認它真的在:在 Finder 裡按 Command + Shift + 句點,隱藏檔案就會顯示出來,你應該看得到
.env.local 躺在那裡(再按一次收回去)。起手式 · 用自己的話改
請幫我在這個專案資料夾裡建一個 .env.local 檔案,內容是: NOTION_TOKEN=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 建好之後,請用那把 token 去查這個 Notion 資料庫, 告訴我現在總共有幾筆紀錄,並把前三筆的店名列出來。 資料庫網址:https://app.notion.com/p/3bb95ea1884180a8b215fcb748371e19 注意:Notion 在 2025-09-03 改過查詢方式,請用 data sources 的端點、 Notion-Version 帶 2025-09-03。如果你記得的是舊的 databases 端點,那是過時的寫法。
這就是這門課一直在練的:AI 很會寫,但它不一定知道「現在」。
- 貼上 Prompt B,讓 AI 幫你建檔並查資料
- AI 回你「現在有 N 筆」——對照一下,跟你剛剛在 Notion 上數的一樣嗎
- 在 Finder 按 Command + Shift + 句點,親眼看到 .env.local 在那裡
- 打開 GitHub Desktop,左邊那排「Changes」清單——.env.local 不在上面,因為它被列在一份「永遠不要送出去」的名單裡
現在測試這把鑰匙的極限
請用同一把 token,試著在那個 Notion 資料庫裡新增一筆紀錄, 店名寫「測試店」。如果失敗了,把伺服器回你的錯誤訊息原文貼給我, 並用白話解釋這個錯誤是什麼意思。
剛剛那個失敗不是 bug,是設計。這把鑰匙被設定成只能讀——就算你(或 AI)想寫,Notion 那端也會拒絕。權限不是靠大家講好不要亂來,是對方真的做得到擋你。
所以第 04 節那面牆,其實有兩層:你的按鈕根本沒把資料送出去(那是頁面還沒接上寫入通道),而就算送出去,你手上這把鑰匙也寫不進去(那是權限)。等一下要拆的是第一層——第二層永遠不會交到你手上。
前端跑在使用者的手上——他的瀏覽器、他的裝置。這句話有多實在,看一次就懂:
- 在你自己的頁面上右鍵 → 檢查,看那一整片程式碼——那是你寫的東西,攤在任何人面前
- 想一下:如果剛剛那把鑰匙寫在裡面,現在誰看得到它?
後端則跑在你鎖起來的房間裡,使用者看不到裡面的判斷,也改不了它。
任何「必須為真」的事情——價格對不對、這個人有沒有權限、密碼對不對——都要在後端判斷。因為前端的任何判斷,使用者自己就能繞過去。
你不需要會做後端,但你要有一個雷達,知道「這件事我不該碰,要交給工程師」。三個問題,中一個就要停手:
「活動報名頁顯示倒數天數」→ 三條都沒中,前端自己做沒問題。
「把折扣碼寫在報名頁裡,輸入對了就打折」→ 中第一條(祕密):任何人右鍵檢查就看得到那組折扣碼。
「用網址參數決定要不要給折扣」→ 中第二條(信任判斷):使用者自己改網址就能給自己打折。
「讓所有人看到同一份揪團名單」→ 中第三條(共用真相),這正是我們今天的處境。
前面那張店家表你只能讀。現在做第二件事:投票——這次是真的寫進去,而且寫完馬上讀回來變成畫面上的票數。
店家表是「有哪些店」——講師維運,你只讀。投票紀錄是「誰想吃哪一家」——每個人一直加,只增不改。
畫面上那個「3 人想吃」的數字,不是誰手動填的,是系統去數投票紀錄算出來的。
你的頁面依然拿不到可寫的鑰匙——它會去拜託那台伺服器代勞。伺服器手上有另一把可寫的鑰匙,藏在你看不到的房間裡。
起手式 · 用自己的話改
我有一個「我要跟」按鈕,目前點下去只有我自己的畫面會變。
請改成:點擊時,除了原本的動作之外,再把這一票送到這個網址:
https://vibe-course.mixxin.tw/api/join
用 POST,送出的內容長這樣:{"name": "店名", "who": "我的名字"}
(我的名字請填「REPLACE_ME」)
送出成功之後,在那張卡片上顯示「已投票 ✓」。
送出失敗的話,要在同一張卡片上顯示一段紅色的提示,內容是伺服器回給我的錯誤訊息
(回應的 JSON 裡有一個 error 欄位),前面加上「這一票沒送出去:」。
失敗時人數不要加,也不要讓整個頁面壞掉。
另外,請把每張卡片上的人數改成顯示「目前幾個人想吃」,
數字來自剛剛抓回來的資料,不要自己在前端算。
- 貼上 Prompt D,接上投票通道
- 按下「我要跟」——然後打開 跟團紀錄表:你剛剛那一票出現在最下面了
- 重整自己的頁面:卡片上的人數變成從投票紀錄算出來的
- 等 30 秒,再請旁邊的同事重整他的頁面(伺服器有做快取,資料不是每秒都重抓)——這次他看得到你那一票
- 用下面的 Prompt E 讓 AI 檢查一次,確認你的程式碼裡沒有金鑰
現在故意讓它失敗一次——這段很重要
你剛剛在 Claude Code 裡看過唯讀金鑰被擋(那個 403)。但那是你看到的。使用者看到的是什麼?現在把它演出來。
- 把你程式碼裡投票的那個網址,暫時改成
https://vibe-course.mixxin.tw/api/join-demo-fail(找不到的話跟 AI 說「把投票的網址改成這個」) - 重整頁面,按下「我要跟」
- 卡片上出現紅字:「這一票沒送出去:伺服器沒有可用的金鑰,這一票寫不進去」,而且人數沒有增加
- 打開 跟團紀錄表確認——真的沒有多出那一筆
- 把網址改回
https://vibe-course.mixxin.tw/api/join,再投一次,確認又正常了
剛剛那個端點是課程做的教具,它永遠會失敗,模擬的情況是:伺服器手上那把可寫的鑰匙過期了、被撤銷了、或根本沒設。
注意畫面上發生了什麼——你的程式碼一行都沒錯,按鈕、網址、送出的內容全都對。但鑰匙不能用,這一票就是進不去。整個功能的成敗,押在一個你看不到、也碰不到的東西上。
這也是為什麼那把鑰匙不能放在你的頁面裡:放在頁面裡,任何人打開原始碼就能拿走它,拿去亂寫;放在伺服器裡,它壞掉的時候只有管理的人能修——而使用者看到的,就是你剛剛做出來的那行紅字。
請檢查我目前的頁面程式碼, 確認裡面沒有任何金鑰、密碼、或看起來像祕密的字串。 如果有,告訴我在第幾行。
- 建立兩張 Notion 表:店家表(講師維運,只讀)+跟團紀錄表(學員寫入,只增不改)。詳見 repo 的 WORKER-SPEC.md
- 確認全班都打得開那兩張 Notion 表(連結已印在教材的「本堂材料」區塊)
- 建 integration A(只勾 Read content)=發給學員的唯讀金鑰;建 integration B(Read + Insert)=只放伺服器
- 把該 database 分享給這兩個 integration(未分享會回 404,不是 403)
- Worker:GET 端點(查 data source、整平成乾淨 JSON、加 30–60 秒快取)+ POST 端點(用 integration B 寫入)
- 查詢一律用 /v1/data_sources/{id}/query 且 Notion-Version 帶 2025-09-03(舊的 databases 端點官方已標 removed)
- CORS 必須開,而且要允許 null origin——學員多半是直接開本機檔案,瀏覽器送出的 Origin 是
null,只做來源白名單會讓全班同時失敗,而且錯誤只出現在 Console - POST 端點加速率限制,避免公開寫入被濫用(但不要用來源檢查當唯一防線,見上一條)
- 寫入成功後主動清掉 GET 的快取,或把快取縮到 30 秒內——否則學員寫進去卻看不到,會誤判成失敗
- 把兩個端點的完整網址與一個範例參數寫在投影片上(不要讓學員自己拼 API 文件)
- 實測一次:拿學員那把唯讀金鑰試寫入,確認真的被擋(403)
作業:一張 issue + 一個 PR——把你的頁面接上店家表(讀)並讓「我要跟」能投票(寫)。PR 描述回答這三題:
- 店家表跟投票紀錄是兩張不同的表,它們差在哪?為什麼一個只能讀、一個可以寫?
- 為什麼我的頁面不能直接去問 Notion?中間那台伺服器在幫我做什麼?
- 這個改動撞到危險雷達哪一條?為什麼一定要透過後端完成?
講師 / 助教檢核重點
- 頁面顯示的總數與同學一致(確認是讀同一份)
- 每人本機有 .env.local(顯示隱藏檔看得到),且 GitHub Desktop 的 Changes 清單上看不到它
- 每人親眼看過「唯讀金鑰試寫入被擋」的錯誤訊息
- 每人的「我要跟」成功寫進 Notion,且被同事的裝置看到
- 每人頁面程式碼中確認沒有金鑰字串
- 每人能覆誦危險雷達三檢查表