# Pages + Workers Bindings 配置说明

> 这篇记录的是 Cloudflare Pages + Worker 的 service binding 该怎么配，以及为什么有些项目在本地能跑、上线后 `context.env` 却是空的。

## 先说结论

如果你的架构是：

- Pages 负责前端静态站点
- Workers 负责 API 和 `/auth/*`
- Pages 通过 service binding 直接调用 Worker

那么要保证三件事：

1. Pages 项目里要有 `[[services]]`
2. 生产环境如果定义了 `[env.production]`，那生产环境也要显式写 `[[env.production.services]]`
3. 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` 默认地址

---

## 推荐目录结构

一个比较清晰的分工是：

```text
apps/
  web/
    wrangler.toml
    functions/[[path]].ts
    src/
  api/
    wrangler.toml
    src/
```

其中：

- `web` 只管 Pages 和转发
- `api` 只管业务 API 和认证

---

## Pages 侧配置

### 1. 顶层配置

`apps/web/wrangler.toml`

```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 binding
- `binding = "WORKER"` 就是 `context.env.WORKER`
- `service = "react-hono-starter-api"` 必须对应 Worker 的实际脚本名

### 2. 生产环境配置

如果你写了 `[env.production]`，就要注意：

- 某些 keys 是 inheritable
- 某些 keys 是 non-inheritable
- `services` 属于需要在环境里显式声明的那类配置

所以生产环境要再补一份：

```toml
[env.production]
vars = {}

[[env.production.services]]
binding = "WORKER"
service = "react-hono-starter-api"
```

这一步很关键。  
我这次遇到的问题就是：顶层有 `[[services]]`，但上线后 `Bindings` 还是空，原因在于生产环境配置没有把 `services` 明确写出来。

---

## Pages Functions 入口

`apps/web/functions/[[path]].ts`

```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`

```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 能不能正常处理请求。

---

## 多环境配置怎么写

### 推荐思路

如果你有多个环境，最好把它们明确拆开：

- `production`
- `dev`
- 或者像参考项目那样拆成 `BACKEND_PROD`、`BACKEND_DEV`

### 单 Worker，多环境变量

适合现在这个项目的写法：

```toml
[env.production]
vars = { ENVIRONMENT = "production", IS_DEBUG = "false" }

[[env.production.services]]
binding = "WORKER"
service = "react-hono-starter-api"
```

开发环境也可以写：

```toml
[env.dev]
vars = { ENVIRONMENT = "development", IS_DEBUG = "true" }

[[env.dev.services]]
binding = "WORKER"
service = "react-hono-starter-api"
```

### 双 Worker，更清晰的写法

如果你想像 `sso-serverless` 那样区分生产 / 预览，可以写成：

```toml
[[services]]
binding = "BACKEND_PROD"
service = "react-hono-starter-api"

[[services]]
binding = "BACKEND_DEV"
service = "react-hono-starter-api-dev"
```

然后在 Pages Functions 里按 host 选择：

```ts
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 的内部后端使用，不想多出一个公网入口，就可以关掉：

```toml
workers_dev = false
```

这能减少暴露面，也能让部署目标更明确。

---

## 为什么这次的问题会出现

这次真正的问题不是代码，而是 Pages 配置。

线上 `react-hono-starter` 的实际配置里，最开始没有把 service binding 带上，所以：

- Pages Function 能运行
- 但 `context.env` 里没有 `WORKER`
- 所以 `/auth/me`、`/auth/login` 一进来就报缺绑定

修复方式是：

1. 顶层声明 `[[services]]`
2. 生产环境声明 `[[env.production.services]]`
3. Worker 的 `name` 和 Pages 的 `service` 对齐
4. 重新部署 Pages

---

## 最后给一个最小可用模板

### `apps/web/wrangler.toml`

```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`

```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`

```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 binding`
- `env.production`
- `workers_dev`

前两个决定 Pages 能不能把请求转给 Worker，最后一个只决定 Worker 自己要不要暴露 `workers.dev` 默认域名。
