sell整合實戰
link

架構藍圖:短網址服務(你的第一個全端應用)

貼上一條長網址,換回一條短的,點下去就跳轉——最適合的第一個專案。

3層串接
6短碼長度
302轉址狀態碼
1KV 命名空間
insights

我們要做什麼?

短網址服務是全端應用的經典「Hello World」。你貼上一條又長又醜的連結,應用程式把它存在一個隨機短代碼底下,之後拜訪那個代碼就會把你轉址回原本的連結。用 Cloudflare,你只要一個 HTML 頁面、一個 Worker、一個 KV 命名空間就能做出整套——完全不用管伺服器。

這個應用程式就只做兩件事:(1) 建立——把長網址變成短代碼並存起來;(2) 還原——拿短代碼把瀏覽器送去長網址。我們先用一張圖看懂這兩件事。

sticky_note_2

把它想成…

一張寄物號碼牌。你交出一件笨重的大衣(長網址),換得一張小小的號碼牌(短代碼)。之後你出示號碼牌,服務員就把你那件大衣拿回來。KV 就是那排衣架;代碼就是你的號碼牌號碼。

schema兩條流程一眼看懂

開啟短網址

建立短網址

POST 長網址

回傳短網址

302 轉址

put(code, url)

get(code)

KV 儲存

瀏覽器表單

Worker 建立

瀏覽器 /code

Worker 查詢

account_tree

各個元件怎麼搭

三個小小的層,各司其職。請求的流向是 前端 → Worker → KV,再原路回來。因為 Worker 同時也提供那個靜態表單,你的整個應用就活在同一個網址底下。

web

1. 前端表單

一個單純的 HTML 頁面,一個輸入框加一個 fetch() 呼叫。不需要任何框架。

bolt

2. Worker(大腦)

一個函式,讀取請求、判斷是要建立還是要查詢,然後跟 KV 溝通。

key

3. KV(記憶體)

一個全球鍵值資料庫,保存每一組 代碼 → 網址 的配對,毫秒內就能讀回。

tag

短代碼

一段像 abc123 的迷你隨機字串,它同時是 key,也是短網址的結尾。

schemaWorker 內部:一個路由、兩件工作

POST /api/shorten

GET /code

put(code, url)

get(code)

進來的請求

比對路由

建立處理器

查詢處理器

KV:LINKS

回傳 JSON 短網址

回傳 302 轉址

swap_vert

每次點擊背後發生什麼

兩條流程都跟同樣三位角色互動:你的瀏覽器、Worker、以及 KV。以下是「建立連結」再「開啟連結」時,一來一回的確切順序。

schema先建立、再還原
KVWorker瀏覽器KVWorker瀏覽器建立短網址開啟短網址POST /api/shorten 帶長網址產生隨機代碼put(code, longUrl)已儲存短網址 /abc123GET /abc123get(abc123)長網址302 轉址到長網址
construction

一步步動手做

五個步驟,從空資料夾到上線的短網址服務。先用 npm create cloudflare@latest 建立專案,然後跟著做——你需要的每個檔案都在下面。

  1. 建立 KV 命名空間

    命名空間就是一桶獨立的鍵值配對。Wrangler 會印出一組 id——複製起來給下一步用。

    bash
    npx wrangler kv namespace create LINKS
  2. 在 wrangler.jsonc 綁定 KV

    這個設定檔指定你的進入點 Worker、指向靜態檔,並讓那個命名空間能在程式碼中用 env.LINKS 取用。

    jsonc
    {
      "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>" }
      ]
    }
  3. 做出前端表單

    把這個存成 public/index.html。它會自動在 / 被提供,並用 fetch() 把網址 POST 給你的 Worker。

    html
    <!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>
  4. 寫 Worker

    把這個存成 src/index.js。在 POST 時它產生代碼並儲存;在 GET 時查出代碼並轉址。這就是整個後端。

    js
    // 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 });
      },
    };
  5. 先在本機跑,再部署

    wrangler dev 在你電腦上跑;wrangler deploy 把它放上 Cloudflare 全球網路,給你一個公開網址。

    bash
    # 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
school

重點概念

key

KV 存的是 代碼 → 網址

每個短代碼就是一個 key,長網址是它的 value。put(code, url) 存下這組配對,get(code) 再把它讀回來。沒有資料表,也沒有 SQL。

subdirectory_arrow_right

302 轉址

Response.redirect(url, 302) 等於告訴瀏覽器「這個連結其實在別處,現在去那裡吧」。302 代表「暫時轉址」,所以你之後還能改目標網址。

casino

隨機短代碼

我們從 a-z、A-Z、0-9 裡隨機挑 6 個字元——超過 560 億種組合,所以小型應用幾乎不可能撞碼。

schedule

最終一致性在這裡剛剛好

剛建立的代碼可能要約 60 秒才會在每個地區都生效。對「分享連結」這種需求完全沒問題。

web

靜態資源 + Worker

HTML 表單是一個會被自動提供的靜態檔;任何不是檔案的路徑(像 /abc123)就會落到你的 Worker 處理。

shield

驗證輸入

存檔前一定要檢查送來的網址真的以 http:// 或 https:// 開頭——別輕信原始表單資料。

info

301 與 302 的差別

301 = 永久轉址(瀏覽器和搜尋引擎會用力快取);302 = 暫時轉址。學習階段先用 302,這樣你隨時能改某個代碼指向哪裡。

trending_up

再升級:統計點擊次數

基本功能跑起來後,一個好玩的下一步是統計每條短連結被開過幾次。快速做法:在 KV 裡放一個計數器。穩健做法:把每次點擊記進 D1(SQL)資料表,這樣就能做分析。

快速做法——KV 計數器

js加在 GET 分支裡
// 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點擊統計資料結構(D1 升級版)

記錄

LINKS

text

code

PK

text

long_url

int

created_at

CLICKS

int

id

PK

text

code

FK

int

clicked_at

sqlschema.sql
-- 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
);
lightbulb

KV 還是 D1?

KV 很適合 代碼 → 網址 的查找(超大量的快速讀取)。D1 比較適合你想查詢、彙總的點擊紀錄。很多真實應用會兩個一起用。

tips_and_updates

小提示、陷阱與限制

warning

開放轉址可能被濫用

因為任何人都能縮短任何網址,壞人可能把釣魚連結藏在你的網域後面。學習專案沒關係;但要上正式環境,請考慮加黑名單,或只允許縮短你自己的網址。

  • KV 免費額度:每天 10 萬次讀取、1,000 次寫入——對個人短網址服務綽綽有餘。
  • 讀取又快又便宜;同一個 key 寫入限約每秒 1 次,但每個代碼只寫一次,所以沒問題。
  • 想對極小的撞碼機率再保險一點,可以在儲存前先檢查代碼是否已存在。
  • 新代碼可能要約 60 秒才會在每個地區都讀得到(最終一致性)。