
(端起手中的茶杯,看著裊裊升起的熱氣,輕輕地吹了吹,隨後慢條斯理地抿了一口。那口琥珀熱茶 (Amber Tea) 的溫潤在喉間散開,帶著午後院子裡特有的慢步調與生活感。)
這時候,院子裡的風漸漸涼了,天邊不知不覺已經染上了一層溫柔的晚霞暮紫 (Twilight Mauve)。我摸了摸自己像白髮銀霜 (Silver Hair) 一樣花白的頭髮,看著兩位寶貝孫女。懂事的大孫女正捧著筆電,小孫女則穿著那件代表她們活潑朝氣的稚嫩粉櫻 (Soft Blossom) 色毛衣,一邊嚼著糖果,一邊好奇地探頭看著姐姐的螢幕。
大孫女一臉苦惱地說:「阿公,我們學校接了一個展覽推廣的實作專案。在自動化大展的攤位現場,想要讓來參觀的人掃描 Qr Code 進入一個漂亮的 Landing Page,填寫姓名、公司、Email,就可以當場下載一份我們精心準備的『AI 導入應用落地報告與完整資料包』。可是,如果要在現場架設後端伺服器、連資料庫,還要處理資安防護跟維護,時間根本不夠;要是直接放一個陽春的 Google 表單,又顯得很不專業,而且大家不填表單也能輕鬆猜到下載網址,這該怎麼辦呢?」
阿公聽了,哈哈大笑,摸摸她們的頭說:
「傻孩子,這就是實體展覽現場最常遇到的『流量收割』考驗了。展覽現場人山人海,網路通常很慢,如果還要經過複雜的後端伺服器轉手,網頁載入太慢,人家扭頭就走了。
來,阿公今天不講古,阿公教你一招最乾淨、最省事,而且在業界非常實用的 「免後端」安全名單收集架構。我們用最現代的 React + Vite,搭配大家都有的 Google Apps Script (GAS) 與 Google 雲端試算表,不用自己維運任何一台伺服器,就能建構出一個安全又極速的 Landing Page。坐過來,聽阿公慢條斯理地跟你道來……」
在大型展覽現場,想要成功收割流量,痛點主要有三個:
阿公常說,下田耕作要懂得藉力使力。既然 Google 已經幫我們做好了最穩固的雲端試算表,我們何不把它當成我們的資料庫,用「無伺服器 (Serverless)」的智慧來解決這一切?
這套架構的核心原則就是:前端極速呈現、後端完全託管、資料安全解鎖。
我們使用現代化的 Vite 作為建構工具,結合 React 與 TypeScript。這樣建立出來的網頁,在打包後只是一堆極為輕量的靜態檔案,可以直接託管在完全免費、速度極快的靜態平台上(例如 GitHub Pages、Vercel 或 Cloudflare Pages)。
我們不需要租用資料庫。我們直接建立一個 Google 雲端試算表,並在裡面綁定一段 Google Apps Script (Code.gs) 程式碼。
「阿公,」大孫女眨著眼睛問,「如果我們直接用 JavaScript 的 fetch 傳送表單資料給 Google Web App,不是會被瀏覽器的 CORS 安全機制阻擋嗎?」
「問得好!」阿公笑著喝了口茶,指著螢幕說:
「這就是最巧妙的地方了。我們在 React 頁面中,放一個寬度與高度都設為 0、完全隱形的 <iframe>:
<iframe id="hidden_iframe" name="hidden_iframe" style="display: none;"></iframe>
然後,我們把表單 <form> 的 target 屬性直接指向這個隱藏的 iframe,而 action 則設定為我們 Google Apps Script 的 Web App URL:
<form
target="hidden_iframe"
action="https://script.google.com/macros/s/YOUR_GAS_ID/exec"
method="POST"
>
當使用者按下提交時,瀏覽器會透過這個隱藏的 iframe 將資料 POST 出去。因為這是標準的 HTML 表單提交行為,瀏覽器絕對不會觸發 CORS 阻擋!網頁不僅不會跳轉,還能順順利利地把資料送到 Google 雲端去。」
postMessage 雙向通訊當 Google Apps Script 成功將資料寫入試算表後,它會回傳一小段 HTML 網頁給那個隱藏的 iframe,這段網頁中包含了一句關鍵的 JavaScript:
// 告訴父視窗:寫入成功囉!
window.parent.postMessage({ status: 'success', token: 'SECURE_REPRESENTATIVE_TOKEN' }, '*');
window.top.postMessage({ status: 'success', token: 'SECURE_REPRESENTATIVE_TOKEN' }, '*');
在我們的 React 前端,只要在 App.tsx 啟動時註冊一個事件監聽器(Event Listener):
useEffect(() => {
const handleMessage = (event: MessageEvent) => {
// 驗證訊息來源與狀態
if (event.data && event.data.status === 'success') {
// 後端寫入成功,更新前端狀態,解鎖下載連結!
setIsSubmitted(true);
}
};
window.addEventListener('message', handleMessage);
return () => window.removeEventListener('message', handleMessage);
}, []);
當使用者提交表單時,前端畫面會先顯示「記錄中...」的載入狀態。只有當隱藏的 iframe 接收到 Apps Script 傳回的「寫入成功」信號時,前端的狀態才會被改變,進而解鎖下載按鈕。
大孫女點點頭,但眉頭還是鎖著:「阿公,那如果有人很聰明,他不用填表單,直接打開瀏覽器的 F12 原始碼,不就能看到下載按鈕的 <a href="downloads/package.zip">,直接繞過我們去下載嗎?」
老爺爺笑瞇瞇地摸了摸大孫女的頭說:
「這就是阿公要教你的『江湖一點訣』了。很多年輕工程師圖省事,會把下載按鈕先藏在 CSS 裡(設定 display: none),填完表單後再用 JS 把它改成 display: block。這在懂一點程式的人眼裡,就像是用一張薄紙遮住寶箱一樣,滑鼠點兩下就破解了。」
為了防止資源被白嫖,我們在設計 React 程式碼時,必須遵守以下安全原則:
isSubmitted 狀態為 false 之前,整個下載按鈕的 HTML 標籤在網頁上是完全不存在的,瀏覽器渲染樹(Render Tree)裡連半個字都沒有。message 事件,且攜帶了由後端 GAS 回傳的正確、隨機生成的 Response Token 時,我們才透過 React 的狀態改變(State Change) 動態渲染(Dynamic Rendering) 出下載按鈕。// 只有在 isSubmitted 變為 true 時,下載按鈕才會出現在 DOM 中
return (
<div className="download-section">
{!isSubmitted ? (
<FormSection onSubmitStart={() => setIsLoading(true)} />
) : (
<div className="success-unlock-card">
<h3>🎉 驗證成功!感謝您的參與</h3>
<p>您的名單已成功記錄。請點選下方按鈕下載獨家 AI 資源包:</p>
{/* 指向我們 public/downloads 下的完整 zip 資料包 */}
<a
href="/downloads/2026-automation-taipei-bank-ai-package.zip"
className="download-btn-neon"
download
>
立即下載完整資料包 (ZIP)
</a>
</div>
)}
</div>
);
配合這個邏輯,在使用者還沒成功提交名單前,瀏覽器的 HTML 裡面根本找不到 /downloads/2026-automation-taipei-bank-ai-package.zip 這個檔案路徑,自然也就無法「白嫖」下載了!
這時候,旁邊的小孫女靠在阿公的膝蓋旁,打了個秀氣的哈欠,揉著眼睛嘟囔著:「阿公,聽起來好像在玩密室逃脫喔,要拿到鑰匙(Token)門才會打開。」
阿公笑著拍拍她的肩膀說:「對啊,寫程式就是這樣,要有邏輯、有規矩,還要懂得保護自己的心血。大妞,你看,這樣一來,我們既不需要花錢架設繁重的後端伺服器,又能確保名單一個都不漏地存進 Google 試算表,還防範了那些想不填表單就拿走資料的人。」
大孫女聽得眼睛直發亮,迫不及待地在筆電上開始建置她的 GAS 專案與 React 元件。
天色漸漸暗了,杯裡的琥珀熱茶也差不多喝完了。看著她們兩姊妹湊在一起討論程式碼的背影,阿公心裡真是暖洋洋的。
Hidden iframe 解決了跨網域表單提交,但「未渲染進 DOM」似乎不等於真正授權:ZIP 若放在公開靜態路徑,網址仍可能從前端 bundle 找到。若要真的防止未填表下載,你會考慮由後端核發短效 signed URL 嗎?
感謝建議,會列入後續優化考量,謝謝。