Effect 的 HTTP 路由怎么接到 Cloudflare Workers?
Workers 的入口只有一个 fetch(request, env) 函数,而 Effect 的路由是一棵由 Layer 组装的值。把后者塞进前者,看起来需要一大块胶水代码,实际上只需要几行。
用 HttpRouter.toWebHandler 把路由变成 Request -> Promise<Response> 的 handler,在模块顶层构建一次,fetch 里只做转发;env 里的 bindings 先包成一个 service,再作为请求级依赖传进去。
原因是 toWebHandler 恰好产出 Workers 要的形状,所以接合点非常薄。同一个路由在 Node 上用 HttpRouter.serve 加 NodeHttpServer.layer 启动,在 Workers 上用 toWebHandler,路由和 handler 一行不改,差异只在最外层的几行代码,这正是 Layer 作为运行时边界的收益。Workers 上跑 Effect 的整体思路见Effect 怎么接入 Cloudflare Workers?,更多坑见Effect 跑在 Cloudflare Workers 上:模块级 Runtime 与几个要小心的坑。
完整的入口长什么样?
// file: worker.ts
import { Context, Effect, Layer } from "effect"
import { HttpRouter, HttpServer, HttpServerResponse } from "effect/http"
export class Bindings extends Context.Service<Bindings, { readonly GREETING: string }>()("app/Bindings") {}
const Hello = HttpRouter.add(
"GET",
"/hello",
Effect.gen(function*() {
const { GREETING } = yield* Bindings
return HttpServerResponse.text(GREETING)
})
)
export const app = HttpRouter.toWebHandler(Hello.pipe(Layer.provide(HttpServer.layerServices)), {
disableLogger: true
})
export default {
fetch: (request: Request, env: { readonly GREETING: string }) =>
app.handler(request, Context.make(Bindings, env))
}为什么 env 要先包成 service?
运行时给你的 env(KV、D1、secret 之类的 bindings)不应该被业务代码直接读。包成 Bindings 这样的 service 之后,路由里写的是 yield* Bindings,依赖出现在类型里,测试时也可以换成别的实现。Context.Service 是 Effect 把依赖写进类型的方式,见Effect 里怎么用 Context.Service 把依赖写进类型?。
为什么 handler 要多传一个参数?
路由里依赖的 Bindings 没法通过 Layer.provide 消掉,因为它是请求级依赖,会变成 handler 的第二个参数。我最初只传了一个参数,编译器报「期望 2 个参数」。这其实是好事:如果你忘了把 env 传进去,是编译错误,不是线上某个请求才发现的 undefined。
toWebHandler 应该在哪里构建?
在模块作用域构建一次,不要放进 fetch 里。官方文档的提醒是:在 fetch 内启动的 fiber 会捕获那次请求的异步上下文;也不要依赖无限期的后台 fiber。上面的例子遵守了这一点:app 在模块顶层创建,fetch 里只做转发。运行器该放在哪里的一般原则,见Effect 程序应该在哪里 run?。
版本是 effect 4.0.5。我没有把这段代码部署到真实的 workerd,只做了按 Workers 形状的调用:用标准的Request调用worker.fetch,/hello得到 200 和配置的文本,未知路径得到 404。bindings 的真实形态、请求上下文的生命周期这些运行时特有的行为,我只能引用官方文档的说法,不能替你担保。这些模块的文档标注为 unstable,API 可能随小版本调整。