跳到內容

ch07-static-assets

取得並執行

這個範例可以獨立 clone 執行,不依賴其他章節。

npx degit mrsuner/cloudflare-serverless-platform-manuals/examples/ch07-static-assets
cd ch07-static-assets
npm install

可用指令

npm run dev	# wrangler dev
npm run deploy	# wrangler deploy
npm run typecheck	# tsc --noEmit
npm run cf-typegen	# wrangler types --env-interface CloudflareBindings

說明

Companion example for docs/07-static-assets.md.

Terminal window
npm install && npm run dev
Terminal window
B=localhost:8787
p() { printf "%-18s " "$1"; curl -s -o /tmp/b -w "%{http_code} %{redirect_url}" "$B$1"; printf " | "; head -c 55 /tmp/b; echo; }
p / # 200 index.html
p /about # 200 about.html
p /about.html # 307 -> /about
p /nope # 200 worker fallback
p /api/hello # 200 worker
p /api/docs # 200 worker <-- NOT excluded by !/api/docs/*
p /api/docs/ # 200 static file
p /app.js # 200 asset beats worker
p /old-about # 301 -> /about (_redirects)
p /go/index.html # 302 -> /app/index.html
p /asset-binding # 200 env.ASSETS.fetch()

Three extra configs demonstrate not_found_handling:

Terminal window
npx wrangler dev -c spa-a.jsonc # unset -> /deep/route 404, empty body
npx wrangler dev -c spa-b.jsonc # SPA -> /deep/route 200, index.html
npx wrangler dev -c spa-c.jsonc # 404-page

2026-07-28 · wrangler@4.114.0 · workerd@1.20260722.1

With no Worker and public/index.html present:

SettingGET /deep/route
unset ("none")404, empty body
"single-page-application"200, index.html
"404-page"404, 404.html (documented; not measured here)

Any client-side-routed app breaks on deep links and refresh without this set.

Negation patterns have a trailing-slash edge

Section titled “Negation patterns have a trailing-slash edge”

"run_worker_first": ["/api/*", "!/api/docs/*"]

/api/hello -> {"from":"worker",...} expected
/api/docs -> {"from":"worker",...} NOT excluded
/api/docs/ -> public/api/docs/index.html expected

The glob /api/docs/* requires the slash, so /api/docs only matches /api/*. Write both "!/api/docs" and "!/api/docs/*", and curl every rule.

public/app.js exists and the Worker handles every path:

GET /app.js -> console.log("asset js");

The Worker never ran, and was not billed.

✨ Parsed 1 valid header rule.
✨ Parsed 2 valid redirect rules.
GET /app/ -> Cache-Control: public, max-age=3600 / x-frame-options: DENY
GET /old-about -> 301 /about
GET /go/index.html -> 302 /app/index.html

Neither applies to responses generated by Worker code.

原始碼

wrangler.jsonc

{
  "$schema": "./node_modules/wrangler/config-schema.json",
  "name": "ch07-static-assets",
  "main": "src/index.ts",
  "compatibility_date": "2026-07-24",
  "assets": {
    "directory": "./public",
    "binding": "ASSETS",
    "run_worker_first": ["/api/*", "!/api/docs/*"]
  }
}

package.json

{ "name": "ch07-static-assets", "private": true, "type": "module",
  "scripts": { "dev": "wrangler dev", "deploy": "wrangler deploy", "typecheck": "tsc --noEmit",
    "cf-typegen": "wrangler types --env-interface CloudflareBindings" },
  "devDependencies": { "typescript": "^5.9.0", "wrangler": "^4.114.0" } }

src/index.ts

export default {
  async fetch(request: Request, env: CloudflareBindings): Promise<Response> {
    const url = new URL(request.url);

    // Only reached for paths where run_worker_first matched, or where no
    // asset matched and not_found_handling is "none".
    if (url.pathname.startsWith("/api/")) {
      return Response.json({ from: "worker", path: url.pathname });
    }

    if (url.pathname === "/asset-binding") {
      // The hostname is ignored; only the pathname is used to match assets.
      const res = await env.ASSETS.fetch(new URL("/about.html", url));
      return new Response(await res.text(), {
        headers: { "content-type": "text/plain; charset=utf-8" },
      });
    }

    return new Response(`worker fallback: ${url.pathname}\n`, { status: 200 });
  },
} satisfies ExportedHandler<CloudflareBindings>;

spa-a.jsonc

{ "name": "spa-a", "compatibility_date": "2026-07-24", "assets": { "directory": "./public" } }

spa-b.jsonc

{ "name": "spa-b", "compatibility_date": "2026-07-24", "assets": { "directory": "./public", "not_found_handling": "single-page-application" } }

spa-c.jsonc

{ "name": "spa-c", "compatibility_date": "2026-07-24", "assets": { "directory": "./public", "not_found_handling": "404-page" } }

public/_headers

/app/*
  X-Frame-Options: DENY
  Cache-Control: public, max-age=3600

public/_redirects

/old-about  /about  301
/go/*       /app/:splat  302

public/404.html

<!doctype html><title>404</title><h1>404.html</h1>

public/about.html

<!doctype html><title>about</title><h1>about.html</h1>

public/api/docs/index.html

<!doctype html><title>docs</title><h1>public/api/docs/index.html (static)</h1>

public/app.js

console.log("asset js");

public/app/index.html

<!doctype html><title>app</title><h1>public/app/index.html</h1>

public/index.html

<!doctype html><title>SPA shell</title><h1>index.html (SPA shell)</h1>

tsconfig.json

{ "compilerOptions": { "target": "esnext", "lib": ["esnext"], "module": "esnext",
  "moduleResolution": "bundler", "types": ["./worker-configuration.d.ts"],
  "strict": true, "skipLibCheck": true, "noEmit": true, "isolatedModules": true },
  "include": ["src/**/*.ts", "worker-configuration.d.ts"] }