架構藍圖:短網址服務(你的第一個全端應用)
貼上一條長網址,換回一條短的,點下去就跳轉——最適合的第一個專案。
我們要做什麼?
短網址服務是全端應用的經典「Hello World」。你貼上一條又長又醜的連結,應用程式把它存在一個隨機短代碼底下,之後拜訪那個代碼就會把你轉址回原本的連結。用 Cloudflare,你只要一個 HTML 頁面、一個 Worker、一個 KV 命名空間就能做出整套——完全不用管伺服器。
這個應用程式就只做兩件事:(1) 建立——把長網址變成短代碼並存起來;(2) 還原——拿短代碼把瀏覽器送去長網址。我們先用一張圖看懂這兩件事。
把它想成…
一張寄物號碼牌。你交出一件笨重的大衣(長網址),換得一張小小的號碼牌(短代碼)。之後你出示號碼牌,服務員就把你那件大衣拿回來。KV 就是那排衣架;代碼就是你的號碼牌號碼。
各個元件怎麼搭
三個小小的層,各司其職。請求的流向是 前端 → Worker → KV,再原路回來。因為 Worker 同時也提供那個靜態表單,你的整個應用就活在同一個網址底下。
1. 前端表單
一個單純的 HTML 頁面,一個輸入框加一個 fetch() 呼叫。不需要任何框架。
2. Worker(大腦)
一個函式,讀取請求、判斷是要建立還是要查詢,然後跟 KV 溝通。
3. KV(記憶體)
一個全球鍵值資料庫,保存每一組 代碼 → 網址 的配對,毫秒內就能讀回。
短代碼
一段像 abc123 的迷你隨機字串,它同時是 key,也是短網址的結尾。
每次點擊背後發生什麼
兩條流程都跟同樣三位角色互動:你的瀏覽器、Worker、以及 KV。以下是「建立連結」再「開啟連結」時,一來一回的確切順序。
一步步動手做
五個步驟,從空資料夾到上線的短網址服務。先用 npm create cloudflare@latest 建立專案,然後跟著做——你需要的每個檔案都在下面。
建立 KV 命名空間
命名空間就是一桶獨立的鍵值配對。Wrangler 會印出一組 id——複製起來給下一步用。
npx wrangler kv namespace create LINKS在 wrangler.jsonc 綁定 KV
這個設定檔指定你的進入點 Worker、指向靜態檔,並讓那個命名空間能在程式碼中用 env.LINKS 取用。
{ "name": "url-shortener", "main": "src/index.js", "compatibility_date": "2025-06-01", // Serve everything in ./public as static files (the HTML form lives here) "assets": { "directory": "./public" }, // Make the KV namespace available in code as env.LINKS "kv_namespaces": [ { "binding": "LINKS", "id": "<paste-your-id-here>" } ] }做出前端表單
把這個存成 public/index.html。它會自動在 / 被提供,並用 fetch() 把網址 POST 給你的 Worker。
<!DOCTYPE html> <html lang="zh-Hant"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>URL Shortener 短網址</title> </head> <body> <h1>Shorten a URL 縮短網址</h1> <form id="shorten-form"> <input id="long-url" type="url" placeholder="https://example.com/a/very/long/path" required /> <button type="submit">Shorten</button> </form> <p id="result"></p> <script> const form = document.getElementById("shorten-form"); form.addEventListener("submit", async (event) => { event.preventDefault(); const longUrl = document.getElementById("long-url").value; // POST the long URL to our Worker as JSON const response = await fetch("/api/shorten", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ url: longUrl }), }); const data = await response.json(); const result = document.getElementById("result"); if (data.short) { const shortUrl = location.origin + data.short; result.innerHTML = 'Short link: <a href="' + shortUrl + '">' + shortUrl + "</a>"; } else { result.textContent = data.error || "Something went wrong"; } }); </script> </body> </html>寫 Worker
把這個存成 src/index.js。在 POST 時它產生代碼並儲存;在 GET 時查出代碼並轉址。這就是整個後端。
// src/index.js — the entire back-end of the URL shortener // Build a random short code, e.g. "a8Kp2Z", from safe characters. function makeCode(length = 6) { const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; const bytes = crypto.getRandomValues(new Uint8Array(length)); let code = ""; for (const byte of bytes) { code += chars[byte % chars.length]; } return code; } export default { async fetch(request, env) { const url = new URL(request.url); // 1) CREATE a short link: POST /api/shorten with { "url": "https://..." } if (request.method === "POST" && url.pathname === "/api/shorten") { const body = await request.json(); const longUrl = body.url; // Validate the input before trusting it. const ok = longUrl && (longUrl.startsWith("http://") || longUrl.startsWith("https://")); if (!ok) { return Response.json( { error: "Please send a valid http(s) URL" }, { status: 400 } ); } const code = makeCode(6); await env.LINKS.put(code, longUrl); // save the pair: code -> long URL return Response.json({ short: "/" + code }); } // 2) RESOLVE a short link: GET /<code> -> 302 redirect to the long URL if (request.method === "GET") { const code = url.pathname.slice(1); // drop the leading "/" const longUrl = await env.LINKS.get(code); // look the code up in KV if (longUrl) { return Response.redirect(longUrl, 302); } return new Response("Short link not found", { status: 404 }); } return new Response("Method not allowed", { status: 405 }); }, };先在本機跑,再部署
wrangler dev 在你電腦上跑;wrangler deploy 把它放上 Cloudflare 全球網路,給你一個公開網址。
# Run it on your own machine first npx wrangler dev # open http://localhost:8787 , paste a URL, then click the short link # Happy with it? Ship it worldwide: npx wrangler deploy
重點概念
KV 存的是 代碼 → 網址
每個短代碼就是一個 key,長網址是它的 value。put(code, url) 存下這組配對,get(code) 再把它讀回來。沒有資料表,也沒有 SQL。
302 轉址
Response.redirect(url, 302) 等於告訴瀏覽器「這個連結其實在別處,現在去那裡吧」。302 代表「暫時轉址」,所以你之後還能改目標網址。
隨機短代碼
我們從 a-z、A-Z、0-9 裡隨機挑 6 個字元——超過 560 億種組合,所以小型應用幾乎不可能撞碼。
最終一致性在這裡剛剛好
剛建立的代碼可能要約 60 秒才會在每個地區都生效。對「分享連結」這種需求完全沒問題。
靜態資源 + Worker
HTML 表單是一個會被自動提供的靜態檔;任何不是檔案的路徑(像 /abc123)就會落到你的 Worker 處理。
驗證輸入
存檔前一定要檢查送來的網址真的以 http:// 或 https:// 開頭——別輕信原始表單資料。
301 與 302 的差別
301 = 永久轉址(瀏覽器和搜尋引擎會用力快取);302 = 暫時轉址。學習階段先用 302,這樣你隨時能改某個代碼指向哪裡。
再升級:統計點擊次數
基本功能跑起來後,一個好玩的下一步是統計每條短連結被開過幾次。快速做法:在 KV 裡放一個計數器。穩健做法:把每次點擊記進 D1(SQL)資料表,這樣就能做分析。
快速做法——KV 計數器
// Inside the GET branch, right after you read longUrl:
const key = "clicks:" + code;
const hits = parseInt((await env.LINKS.get(key)) || "0", 10);
await env.LINKS.put(key, String(hits + 1)); // best-effort click count穩健做法——D1 資料表
用 D1,你為每條連結留一列、每次點擊留一列。資料長相如下:
-- schema.sql (run with: npx wrangler d1 execute mydb --file schema.sql)
CREATE TABLE links (
code TEXT PRIMARY KEY,
long_url TEXT NOT NULL,
created_at INTEGER NOT NULL
);
CREATE TABLE clicks (
id INTEGER PRIMARY KEY,
code TEXT NOT NULL,
clicked_at INTEGER NOT NULL
);KV 還是 D1?
KV 很適合 代碼 → 網址 的查找(超大量的快速讀取)。D1 比較適合你想查詢、彙總的點擊紀錄。很多真實應用會兩個一起用。
小提示、陷阱與限制
開放轉址可能被濫用
因為任何人都能縮短任何網址,壞人可能把釣魚連結藏在你的網域後面。學習專案沒關係;但要上正式環境,請考慮加黑名單,或只允許縮短你自己的網址。
- KV 免費額度:每天 10 萬次讀取、1,000 次寫入——對個人短網址服務綽綽有餘。
- 讀取又快又便宜;同一個 key 寫入限約每秒 1 次,但每個代碼只寫一次,所以沒問題。
- 想對極小的撞碼機率再保險一點,可以在儲存前先檢查代碼是否已存在。
- 新代碼可能要約 60 秒才會在每個地區都讀得到(最終一致性)。