ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Alchemy v2.0.0-beta.58 平台人体工学改造:把基础设施打包成可复用服务的 Layer 体系

Alchemy v2.0.0-beta.58 平台人体工学改造:把基础设施打包成可复用服务的 Layer 体系 Alchemy v2.0.0-beta.58 平台人体工学改造把基础设施打包成可复用服务的 Layer 体系【免费下载链接】t3code项目地址: https://gitcode.com/GitHub_Trending/t3/t3code这篇技术指南围绕 Alchemy 2.0.0-beta.58Platform Ergonomics 版本的完整更新内容展开。该版本重构了 Layer 与 Resource 的协作方式Platform tag 变成纯身份标识、props 迁移到.make()、DurableObjectState提升到初始化 Effect 顶层从而让你能把基础设施如 R2 存储、运行中的 Container打包进一个 Layer作为可复用服务提供给任意 Worker、Container 或 Durable Object。读完本文你将掌握 beta.58 的全部破坏性变更及其迁移路径并能基于新的layerContainer、命名空间化最小权限绑定和Command模块编写出真正的云无关服务层代码。本文所述内容以 docs/blog/2026-06-24-beta-58.md 为骨架并结合packages/alchemy源码与测试用例佐证对应的完整变更记录见仓库内 CHANGELOG.md。背景beta.58 在解决什么问题在 v2.0.0-beta.58 之前一个 Platform tag如Cloudflare.Worker同时承载了三件事身份、配置props、运行时实现。这导致两个实际痛点基础设施难以复用Worker / Container / Durable Object 的初始化逻辑与具体 tag 绑定无法把带存储的 Durable Object带容器的工具抽成独立服务再注入到别处初始化位置分散DurableObjectState只能在嵌套的内层Effect.gen里解析资源、绑定、依赖的声明被拆散。beta.58 的核心思路是把 props 和运行时实现从 tag 移到 Layer 上让 tag 成为纯身份pure identity而 Layer 成为基础设施的打包容器。这一设计在当前仓库中有完整的工程化支撑packages/alchemy/src/Cloudflare/Workers/Worker.ts、DurableObject.ts、R2/ReadWriteBucket.ts等模块共同实现了 tag 与 Layer 的分离packages/alchemy/test/Cloudflare/下的测试 fixture如R2/fixtures/readwrite-binding.ts、readwrite-http.ts、Container/fixtures/effectful/则直接验证了同一资源、两种实现 Layer的运行时行为。纯身份 tag一个 tag多种实现Props 从 tag 迁移到.make()新版中Cloudflare.WorkerWorkerB()的泛型参数只保留 tag 名称原来的 props如main: import.meta.filename移入默认导出的.make()调用- export class WorkerB extends Cloudflare.WorkerWorkerB()( - WorkerB, - { main: import.meta.filename }, // props on the tag - ) {} export class WorkerB extends Cloudflare.WorkerWorkerB()(WorkerB) {} export default WorkerB.make( { main: import.meta.filename }, // props on the Layer Effect.gen(function* () { /* ... */ }), );由于 tag 只是身份同一个 tag 可以拥有任意多个实现.make(propsA, implA)与.make(propsB, implB)为同一个 tag 产出两个不同的 Layer。这在测试与多环境部署中非常实用——测试时把真实实现换成 stub或者按 stage 调整实例规格export const SandboxProd Sandbox.make( { main: import.meta.filename, instanceType: standard-1 }, realImpl, ); export const SandboxTest Sandbox.make( { main: import.meta.filename, instanceType: dev }, fakeImpl, );内联形式保持不变如果你不想拆分内联写法依旧可用——实现作为第三个参数传入props 继续留在 tag 上export default Cloudflare.Worker( Worker, { main: import.meta.filename }, // still inline here Effect.gen(function* () { /* ... */ }), );DurableObjectState 提升到顶层DurableObjectState现在可以直接在外层初始化 Effect 中yield*不必再等进入嵌套的内层Effect.gen才解析Effect.gen(function* () { const state yield* Cloudflare.DurableObjectState; return Effect.gen(function* () { - const state yield* Cloudflare.DurableObjectState; let count (yield* state.storage.getnumber(count)) ?? 0; /* ... */ }); }),这一步是整个版本的关键初始化阶段正是资源、绑定、依赖的声明位置。状态、存储桶、队列等基础设施在这里被解析并捕获进 Layer之后这个 Layer 可以提供给任何 Worker / Durable Object / Container而无需改变其外部形态。实践一把 Durable Object 存储打包成 Sql 工具在 init 阶段解析DurableObjectState就能把 DO 的存储捕获进 Layer并将其暴露为服务——这正是Sql类工具的标准形态。做法是在 init 时解析state以及任何其他资源比如 R2 bucket然后返回运行时方法class Archive extends Context.ServiceArchive, { save: (key: string, body: string) Effect.Effectvoid, never, Alchemy.RuntimeContext; // - RuntimeContext-colored }()(Archive) {} const ArchiveLive Layer.effect( Archive, Effect.gen(function* () { // Resolve both references at init. const state yield* Cloudflare.DurableObjectState; const bucket yield* Cloudflare.R2.ReadWriteBucket(Bucket); return Archive.of({ // The body runs at runtime — both calls are RuntimeContext-colored. save: (key, body) Effect.gen(function* () { yield* bucket.put(key, body); yield* state.storage.put(saved:${key}, Date.now()); }), }); }), );注意这里的方法体返回类型带有Alchemy.RuntimeContext颜色colored effect——因为方法内部在运行时才会真正调用 R2 与 DO 存储。随后在 DO 的 init 上Effect.provide(ArchiveLive)内层 Effect 中直接yield* Archiveexport class Doc extends Cloudflare.DurableObjectNamespaceDoc()( Doc, Effect.gen(function* () { const archive yield* Archive; // resolved at init return Effect.gen(function* () { return { save: (key: string, body: string) archive.save(key, body), }; }); }).pipe(Effect.provide(ArchiveLive)), ) {}收益很明显DurableObjectState和 R2 都没有泄漏进Archive的公共类型——消费者只看到RuntimeContext服务保持云无关cloud-agnostic将来换存储后端也无需改动消费方。源码中Cloudflare/R2/ReadWriteBucket.ts定义了ReadWriteBucket服务与其客户端接口ReadWriteBucketClient extends ReadBucketClient, WriteBucketClientCloudflare/Workers/DurableObjectState.ts负责 state 的解析两个模块的边界正是文章示例中init 捕获、运行时调用分工的实现基础。实践二把运行中的 Container 打包成 Bash 工具旧版使用 Container 需要三步曲bind→start→ 使用。beta.58 将其压缩为直接yield*Container tag 得到运行中的容器实例不再是ContainerApplication资源并通过Cloudflare.layerContainer(Class, options)配置其运行方式Effect.gen(function* () { - const sandbox yield* Cloudflare.Container.bind(Sandbox); const sandbox yield* Sandbox; return Effect.gen(function* () { - const container yield* Cloudflare.start(sandbox, { - enableInternet: true, - }); return { - exec: (cmd: string) container.exec(cmd), exec: (cmd: string) sandbox.exec(cmd), }; }); - }), }).pipe( Effect.provide( Cloudflare.layerContainer(Sandbox, { enableInternet: true }), ), ),layerContainer内部完成绑定、启动、监控然后用运行中的实例满足Sandboxtag。因为它是普通 Layer任何 Effect 或 Layer 都可以yield* Sandbox拿到运行中的容器——所以Bash工具本质上就是一个捕获容器并对外执行命令的 Layerconst Bash Layer.effect( BashTool, Effect.gen(function* () { const sandbox yield* Sandbox; // running instance return BashTool.of({ run: (cmd: string) sandbox.exec(cmd), }); }), );BashLayer 只是要求requiresSandboxlayerContainer只在宿主最外层 init Effect 上 provide 一次同时满足Bash与所有需要容器的依赖.pipe( Effect.provide( Layer.provideMerge(Bash, Cloudflare.layerContainer(Sandbox)), ), )需要特别说明的是外部与远程 Container 镜像从context/dockerfile构建或从 registry 拉取没有运行时形态、也没有.make()——它们的 props 直接内联声明在 tag 上并由宿主的 Durable Object 通过Cloudflare.layerContainer注册。仓库测试目录packages/alchemy/test/Cloudflare/Container/fixtures/effectful/下的container.ts与object.ts正是这一用法的落地示例。RPC 在 Container 中真正可用Container RPC 一直纸面存在但从未被正确实现与测试。beta.58 起它真正可用了Container 与 Worker / Durable Object 一样声明类型化的运行时形态这些方法可以通过 RPC 调用——容器在fetchhandler 之外还暴露 Effect 返回型方法export class MyContainer extends Cloudflare.Container MyContainer, { ping: () Effect.Effectstring; readObject: (key: string) Effect.Effectstring | null, never, RuntimeContext; } ()(MyContainer) {} export default MyContainer.make( { main: import.meta.filename, dockerfile: FROM oven/bun:latest }, Effect.gen(function* () { const bucket yield* Cloudflare.R2.ReadWriteBucket(Storage); return { ping: () Effect.succeed(pong), readObject: (key) bucket.get(key).pipe(/* ... */), fetch: Effect.gen(function* () { /* ... */ }), }; }).pipe(Effect.provide(Cloudflare.R2.ReadWriteBucketHttp)), );从宿主侧看yield* MyContainer得到运行中的容器其方法经由 Cloudflare 的 RPC 机制以类型化 Effect 的形式往返——没有手写 fetch 管道没有手动序列化const container yield* MyContainer; const pong yield* container.ping(); // RPC const body yield* container.readObject(key); // RPC当你确实需要向容器进程内运行的 server 发起原始fetch时用container.getTcpPort(port)获取端口即可。WorkerLoader更名且 load 变为 EffectDynamicWorkerLoader更名为WorkerLoader且load(...)现在返回 Effect必须yield*对应 PR #653- const loader yield* Cloudflare.DynamicWorkerLoader(Loader); - const worker loader.load({ compatibilityDate, mainModule, modules }); const loader yield* Cloudflare.WorkerLoader(Loader); const worker yield* loader.load({ compatibilityDate, mainModule, modules });绑定命名空间化 最小权限原生或 HTTP 二选一绑定全面转向命名空间化约定资源上单一的.bind被移除改为按访问级别命名空间化的能力- const bucket yield* Cloudflare.R2Bucket.bind(Bucket); const bucket yield* Cloudflare.R2.ReadBucket(Bucket); // get / head / list const bucket yield* Cloudflare.R2.WriteBucket(Bucket); // put / delete / multipart const bucket yield* Cloudflare.R2.ReadWriteBucket(Bucket); // both按需选择最小权限然后 provide 一个实现 Layer原生 Worker 绑定或受限的 HTTP API token。// native Worker binding (env.BUCKET) .pipe(Effect.provide(Cloudflare.R2.ReadWriteBucketBinding)) // scoped HTTP API token — same client, runs anywhere (e.g. inside // a Container process that has no native binding) .pipe(Effect.provide(Cloudflare.R2.ReadWriteBucketHttp))关键设计无论 provide 哪个 Layer客户端接口完全一致。Worker 通过原生绑定读 bucket容器进程通过 token 读同一个 bucket——同一份代码只是换了 Layer。源码侧对应Cloudflare/R2/目录下的ReadBucket.ts、WriteBucket.ts、ReadWriteBucket.ts客户端接口与ReadWriteBucketBinding.ts、ReadWriteBucketHttp.ts、ReadWriteBucketLocal.ts三种实现 Layer测试 fixturepackages/alchemy/test/Cloudflare/R2/fixtures/readwrite-binding.ts与readwrite-http.ts分别验证了这两条路径。*HttpLayer 的实现原理*HttpLayer 的意义在于在没有原生绑定的环境典型如Container 进程内部复用同一套客户端。provide 它时会铸造一个受限的AccountApiToken并绑定一条只含该访问级别所需权限组的策略const token yield* AccountApiToken(${self.LogicalId}Token); yield* token.bind${bucket.LogicalId}({ policies: [ { effect: allow, permissionGroups: options.permissionGroups, // e.g. R2 read-only resources: { [com.cloudflare.api.account.${accountId}]: * }, }, ], });token 的value会被注入宿主机的secrets——在 Worker 上是secret_text在 Container 上是 secret。运行时客户端读取它并调用 R2 的 REST API。没有账户级宽泛 key也没有手动 token 搬运。Read / Write / ReadWrite 的拆分以及*Binding/*Http实现在 KV、Queues 及其他能力资源上遵循同样的约定。这套命名空间化约定是所有绑定的演进方向——AWS、Cloudflare 及其他 provider 的完整迁移将在 beta.59 落地。Command把 shell 命令变成一等资源Build模块被移除取而代之的是统一的Command模块——把 shell 命令建模为一等资源共享同一个执行器对应 PR #673。仓库中packages/alchemy/src/Command/下的Build.ts、Exec.ts、Dev.ts、Providers.ts就是该模块的实现。Command.Build原Build.Command运行一个产出构建产物的命令并在状态里跟踪outdir。输入输出会做内容哈希通过memo可选开启项目未变化时跳过重建const build yield* Command.Build(vite-build, { command: npm run build, cwd: ./frontend, outdir: dist, }); yield* Console.log(build.outdir); // path to dist, relative to process.cwd()源码层面Command/Build.ts对outdir与memo有更细的约定outdir是相对工作目录的输出路径memo默认哈希cwd下所有非 gitignore 文件外加最近的锁文件也支持传入 glob 缩小范围或显式设为false关闭记忆化、每次部署都重建。outdir在状态中以相对路径存储相对于进程启动时的initialCwd保证同一份状态在 CI runner 与本地笔记本之间可移植。Command.Exec新增纯粹为副作用而运行——迁移、代码生成、数据播种——没有输出契约只要命令以0退出就算成功。memo只在输入变化时重新运行yield* Command.Exec(migrate, { command: npm run db:migrate, cwd: ./packages/api, });Command.Dev原Build.DevServer长驻 dev server作用域限定在 stack 实例内。它运行在 dev sidecar 中因此能扛住用户代码的 HMR 重启并把打印出的第一个http(s)://URL 作为url输出暴露出来。在alchemy deploy时它是空操作const dev yield* Command.Dev(Frontend, { command: npm run dev, }); yield* Console.log(dev.url); // e.g. http://localhost:5173StaticSiteAWS Cloudflare与Cloudflare.Workers.Worker现在都通过Command完成构建与 dev 服务。由于资源类型 ID 被重命名引用旧Build.Command/Build.DevServer的既有状态不再匹配且Command.Build的输出哈希结构由{ outdir, hash }变为{ outdir, hash: { input, output } }——迁移时需留意状态兼容性。破坏性变更清单与迁移速查beta.58 包含以下破坏性变更官方文档逐条给出了迁移指引均见上文对应小节变更旧写法新写法Worker / Container 的 props写在 tag 泛型参数上移到 Layer 形式的.make()DurableObjectState在内层 init Effect 中解析在外层 init Effect 中解析Container 启动Cloudflare.Container.bindCloudflare.startyield*tag Cloudflare.layerContaineryield*tag 返回运行中的容器而非ContainerApplication资源资源绑定单个.bind命名空间化最小权限能力如Cloudflare.R2.ReadWriteBucket构建模块Build模块Build.Command/Build.DevServer新的Command模块Command.Build/Command.Dev动态加载器DynamicWorkerLoaderWorkerLoaderload返回 Effect下一步阅读运行 Container 的完整指南website/src/content/docs/cloudflare/compute 相关章节添加 Durable Object 与跨 Worker Durable Object同上目录Runtime as a colored function 的 Layer 语义website/src/content/docs/infrastructure-as-effects/完整变更记录CHANGELOG.md如果要从 beta.57 升级到 beta.58按上表逐项迁移即可此版本的layerContainer、命名空间化绑定与Command模块已为 beta.59 的跨 provider 统一绑定约定铺平了道路。【免费下载链接】t3code项目地址: https://gitcode.com/GitHub_Trending/t3/t3code创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表