这篇记录的是 Cloudflare Pages + Worker 的 service binding 该怎么配,以及为什么有些项目在本地能跑、上线后
context.env却是空的。
先说结论
如果你的架构是:
Pages 负责前端静态站点
Workers 负责 API 和
/auth/*Pages 通过 service binding 直接调用 Worker
那么要保证三件事:
Pages 项目里要有
[[services]]生产环境如果定义了
[env.production],那生产环境也要显式写[[env.production.services]]Worker 的
name必须和 Pages 里的service对上
这三项缺一项,Pages Function 里就拿不到绑定,表现为 context.env.WORKER 为空,或者你自己封装的 ctx.env.api 为空。
这类项目里各文件的职责
apps/web/wrangler.toml
Pages 项目的配置源。这里决定:
Pages 项目名
静态产物目录
service binding
各环境变量
apps/web/functions/[[path]].ts
Pages Functions 入口。它负责:
拦截
/api/*拦截
/auth/*通过 service binding 把请求转发到 Worker
apps/api/wrangler.toml
Worker 项目的配置源。这里决定:
Worker 名称
生产 / 开发环境变量
D1 / KV / R2 等绑定
是否保留
workers.dev默认地址
推荐目录结构
一个比较清晰的分工是:
apps/
web/
wrangler.toml
functions/[[path]].ts
src/
api/
wrangler.toml
src/其中:
web只管 Pages 和转发api只管业务 API 和认证
Pages 侧配置
1. 顶层配置
apps/web/wrangler.toml
name = "react-hono-starter"
pages_build_output_dir = "dist"
compatibility_date = "2024-09-25"
# Bind to the API worker - proxies /api/* and /auth/* without a network hop
[[services]]
binding = "WORKER"
service = "react-hono-starter-api"
[vars]
# Public vars accessible in the browser这部分的含义:
name是 Pages 项目名pages_build_output_dir是构建输出目录[[services]]是 Pages Functions 可直接使用的 service bindingbinding = "WORKER"就是context.env.WORKERservice = "react-hono-starter-api"必须对应 Worker 的实际脚本名
2. 生产环境配置
如果你写了 [env.production],就要注意:
某些 keys 是 inheritable
某些 keys 是 non-inheritable
services属于需要在环境里显式声明的那类配置
所以生产环境要再补一份:
[env.production]
vars = {}
[[env.production.services]]
binding = "WORKER"
service = "react-hono-starter-api"这一步很关键。
我这次遇到的问题就是:顶层有 [[services]],但上线后 Bindings 还是空,原因在于生产环境配置没有把 services 明确写出来。
Pages Functions 入口
apps/web/functions/[[path]].ts
interface Env {
WORKER: Fetcher;
}
export const onRequest: PagesFunction<Env> = (ctx) => {
const { pathname } = new URL(ctx.request.url);
if (pathname.startsWith('/api/') || pathname.startsWith('/auth/')) {
if (!ctx.env.WORKER) {
return new Response('Missing worker service binding', { status: 500 });
}
return ctx.env.WORKER.fetch(ctx.request);
}
return ctx.next();
};这里有两个点:
ctx.env.WORKER不是自动存在的,要靠wrangler.toml绑定/api/*和/auth/*都是直接转给 Worker
也就是说,Pages Functions 不是业务逻辑本身,它只是一个转发层。
Worker 侧配置
apps/api/wrangler.toml
name = "react-hono-starter-api"
main = "src/worker.ts"
compatibility_date = "2024-09-25"
compatibility_flags = ["nodejs_compat"]
workers_dev = false
[dev]
port = 8013这里最重要的是:
name = "react-hono-starter-api"这个名字要和 Pages 的
service对上workers_dev = false只影响默认的*.workers.dev地址
下面这些绑定是 Worker 自己用的:
D1
KV
R2
各类密钥和环境变量
它们不会直接影响 Pages 的 service binding,但会影响 Worker 能不能正常处理请求。
多环境配置怎么写
推荐思路
如果你有多个环境,最好把它们明确拆开:
productiondev或者像参考项目那样拆成
BACKEND_PROD、BACKEND_DEV
单 Worker,多环境变量
适合现在这个项目的写法:
[env.production]
vars = { ENVIRONMENT = "production", IS_DEBUG = "false" }
[[env.production.services]]
binding = "WORKER"
service = "react-hono-starter-api"开发环境也可以写:
[env.dev]
vars = { ENVIRONMENT = "development", IS_DEBUG = "true" }
[[env.dev.services]]
binding = "WORKER"
service = "react-hono-starter-api"双 Worker,更清晰的写法
如果你想像 sso-serverless 那样区分生产 / 预览,可以写成:
[[services]]
binding = "BACKEND_PROD"
service = "react-hono-starter-api"
[[services]]
binding = "BACKEND_DEV"
service = "react-hono-starter-api-dev"然后在 Pages Functions 里按 host 选择:
const backend = hostname.includes('localhost') ? env.BACKEND_DEV : env.BACKEND_PROD;
return backend.fetch(request);这种写法更适合:
生产和预览分开
多域名部署
需要明确区分环境的场景
workers_dev 有什么用
workers_dev 是 Worker 项目自己的开关。
开启时
Worker 会有一个默认的 *.workers.dev 访问地址。
关闭时
Worker 不再暴露这个默认地址。
它不影响什么
它不决定:
Pages 能不能看到 service binding
context.env.WORKER是否存在ctx.env.api是否为空
什么时候建议关掉
如果你只想让 Worker 作为 Pages 的内部后端使用,不想多出一个公网入口,就可以关掉:
workers_dev = false这能减少暴露面,也能让部署目标更明确。
为什么这次的问题会出现
这次真正的问题不是代码,而是 Pages 配置。
线上 react-hono-starter 的实际配置里,最开始没有把 service binding 带上,所以:
Pages Function 能运行
但
context.env里没有WORKER所以
/auth/me、/auth/login一进来就报缺绑定
修复方式是:
顶层声明
[[services]]生产环境声明
[[env.production.services]]Worker 的
name和 Pages 的service对齐重新部署 Pages
最后给一个最小可用模板
apps/web/wrangler.toml
name = "react-hono-starter"
pages_build_output_dir = "dist"
compatibility_date = "2024-09-25"
[[services]]
binding = "WORKER"
service = "react-hono-starter-api"
[env.production]
vars = {}
[[env.production.services]]
binding = "WORKER"
service = "react-hono-starter-api"apps/web/functions/[[path]].ts
interface Env {
WORKER: Fetcher;
}
export const onRequest: PagesFunction<Env> = (ctx) => {
const { pathname } = new URL(ctx.request.url);
if (pathname.startsWith('/api/') || pathname.startsWith('/auth/')) {
return ctx.env.WORKER.fetch(ctx.request);
}
return ctx.next();
};apps/api/wrangler.toml
name = "react-hono-starter-api"
main = "src/worker.ts"
compatibility_date = "2024-09-25"
compatibility_flags = ["nodejs_compat"]
workers_dev = false小结
这类 Pages + Workers 架构里,最容易踩坑的点不是代码,而是配置继承。
记住这三个关键词就够了:
service bindingenv.productionworkers_dev
前两个决定 Pages 能不能把请求转给 Worker,最后一个只决定 Worker 自己要不要暴露 workers.dev 默认域名。
本日志MD下载: https://blog-media.ropean.org/2026/06/efd544d8-ff24-4a6e-8ab4-1b22dec2df3d.md