事情起因很简单。
后台已经部署到 Cloudflare。浏览器打开预览地址,登录成功,跳到 /skills。标签页标题却是:
404 - 后台管理
页面不存在。
可本地同一条路由好好的。侧栏第一项就是技能。账号还是管理员。
于是第一个疑问自然冒出来:
路由明明在,为什么线上说页面不存在?
从一场 404 说起
这套后台拆成两个 Worker。
一个是后台 Web,负责页面和 Cookie 会话。
一个是后台 API,负责 REST。
Web 的 SSR loader 要去拉技能列表。当时的写法很直白:拿环境变量里的公网地址,fetch 过去。
预览环境里,这个地址长这样:
https://feat-admin-api.myaccount.workers.dev/api/admin/skills
我在笔记本上 curl 它,没登录会返回 401。路由在,API 也在。
可从 Web 这个 Worker 里面打过去,得到的是 404。
React Router 把这个 404 当成「当前页面不存在」,于是标签页变成了那行字。
真正的原因在 Fetch 文档里:同账号、同 zone 的 Worker 用全局 fetch 打对方的 workers.dev,请求不会进那个 Worker。它会落到空 origin 上。12
浏览器能打开那个 URL。
Worker 里不行。
所以第二个疑问来了:
两个 Worker 到底该怎么说话?
先换成内网
Cloudflare 给的答案不是再拼一个公网域名,而是 Service Binding。34
调用方 wrangler 里写:
{
"services": [
{
"binding": "ADMIN_API",
"service": "admin-api"
}
]
}
binding 是自己代码里的名字,env.ADMIN_API.fetch(...)。
service 是对方 wrangler 的 name,不是 URL,也不是自定义域名。
这就像 Compose 里的服务名、K8s 里的 ClusterIP:请求不出公网,也不吃同 zone 那条限制。
我改完之后,404 从「页面不存在」变成了另一件事——预览还在,绑定却指错了人。
两种预览
Builds 设置里,非生产分支的版本命令曾经是:
npx wrangler versions upload --env preview
地址栏里看到的却是:
https://feat-admin-web.myaccount.workers.dev
我下意识觉得:--env preview 就是为了生成这个 feat- 前缀。
错了。
这两个东西长得很像,完全不是一回事。
一个是外号。
一个是分身。
alias 是外号
Preview URL 的格式是:
<alias 或版本前缀>-<WORKER_NAME>.<子域>.workers.dev
所以:
feat-admin-web.myaccount.workers.dev
拆开就是:
- alias:
feat - Worker 名:
admin-web
脚本还是 admin-web。
生产的 Active Deployment 不会被换成这个版本。
自定义域名 admin.example.com 也不会指过来。
对应的命令是 Builds 默认的那条:7
npx wrangler versions upload
需要固定外号时,再加 --preview-alias feat。
它解决的是:我想看看这个 commit,但别动线上。
env 是分身
--env preview 要 wrangler 里真有一块 env.preview。6
没有的话,Wrangler 仍会按命名环境处理,默认把脚本名变成:
<顶层 name>-<环境名>
也就是 admin-web-preview。
Dashboard 里会多出一个 Worker。
Hyperdrive、R2、Service Binding、vars、secrets,都不会从顶层继承,得在 env.preview 里再写一遍。8
这是长期活着的 staging,不是 PR 预览。
你可以给它另绑一个域名,用另一套数据库,让后台 Web 的分身去调后台 API 的分身。
代价是账号里 Worker 数量翻倍,绑定要手搓配对。
Binding 跟谁
Service Binding 跟的是 Worker 名,不是 Git 分支,也不是 Preview URL。
走 alias 时:
- 预览版后台 Web 仍叫
admin-web service: "admin-api"仍然成立env.ADMIN_API.fetch打到的是admin-api当前正在接流量的部署9
同分支如果也改了 API,预览 UI 看不到那份 API。Binding 不会自动跟到「这个 PR 的 API preview」。
走 --env preview 时:
- 调用方变成
admin-web-preview - 若仍写
"service": "admin-api",分身会打进生产 API - 要隔离,必须显式写成
"service": "admin-api-preview" - 而且 API 那边也得是独立 deploy 出来的 Worker,不是
admin-api上的一个 version
漏写 service,比 404 更危险:页面能开,数据写进生产库。
域名跟谁
自定义域名跟的也是 Worker 名,不是分支名。
走 alias:
- 多出来的只有
*.workers.dev预览地址 admin.example.com仍绑在生产的admin-web上api.example.com仍绑在生产的admin-api上
走 --env:
- 分身是另一个脚本,可以另绑
staging-admin.example.com - 生产域名不会自动分给它
- 想让 staging Web 和 staging API 互相认识,还是得靠 Service Binding 里的
service字段,不是靠域名
之前那个同 zone 404,就是因为用公网 hostname 当「内网 DNS」。
Binding 不认 hostname。域名只负责人从浏览器走进来。自定义域名上的 Worker 互调,规则又不一样。10
这回怎么选
个人站点、PR 只想看页面:
- 版本命令用
npx wrangler versions upload - 不要加
--env preview - 需要的话再
--preview-alias feat
代价很明确:预览 UI 打的是生产 API。
只有这些情况才值得上 env:
- 预览不能碰生产库
- 要长期活着的 staging 域名
- 生产和预览要用不同的 R2、不同的 Supabase 项目
那是在复制一整套基础设施。
别用最重的模型,去解决最轻的问题。
收束
回到那次 404。
页面没有丢。
是两件独立的事叠在一起:
- 同 zone 的公网
fetch,进不了另一个 Worker - 把地址栏里的 alias,当成了
--env
修第一件,用 Service Binding。
修第二件,把版本命令里的 --env preview 拿掉。
以后再看见 feat-admin-web.….workers.dev,我会先拆一下:
前半段是外号,后半段才是它到底是谁。
参考资料
- Fetch · Worker to Worker
- Compatibility flag:
global_fetch_strictly_public - Limits · Worker-to-Worker subrequests
- Service bindings
- Wrangler
services配置 - Preview URLs
- Workers Builds 配置
- Wrangler environments
- Wrangler 配置 · Environments
- Versions and deployments
- Custom Domains · Worker to Worker
Footnotes
-
Fetch:同账号 Worker 互调,要么 Service Binding,要么
global_fetch_strictly_public。 ↩ -
Service bindings:不经过公网 URL,直接调另一个 Worker。 ↩
-
Wrangler
services:service填目标 Worker 名;带环境时为<name>-<env>。 ↩ -
Preview URLs:
<alias 或版本前缀>-<WORKER_NAME>.<subdomain>.workers.dev。 ↩ -
Wrangler environments:
--env部署的是另一套 Worker,不是 Preview alias。 ↩ ↩2 -
Workers Builds:非生产分支默认
wrangler versions upload;只有 wrangler 里真有 named environment 才加--env。 ↩ -
配置 · Environments:bindings 不继承,要在
env.<name>里重写。 ↩ -
Deployment management:改被调 Worker 的代码,不会给调用方自动出新 version;binding 打的是对方当前部署。 ↩
-
Custom Domains:同 zone 打 Route /
workers.dev会失败;目标挂在 Custom Domain 上则可以。 ↩