Cloudflare 收购 Deno 的消息刚在 Hacker News 上吵翻天,评论区一半人庆幸 Deno 有了靠山,另一半担心被厂商绑定。但真正值得开发者焦虑的不是谁收购谁,而是:如果明天你的 runtime 换东家、改价格或改变方向,你的代码要花多久才能迁移过去?很多团队的回答是“几周”,因为 process.env、fs、Buffer 和 Express 的 req.body 已经渗进代码库的每个角落。这篇文章给出一条务实的路:用 Web Standard APIs 写核心业务逻辑,再用一层薄薄的 adapter 对接不同 runtime,让同一套 TypeScript 几乎零修改跑在 Node 22+、Deno 2.x、Bun 1.2+ 和 Cloudflare Workers 上。\n\n先看底层基础。几年前每个 runtime 都有自己的专属 API,现在好了,WinterTC(原 WinterCG)推动下,Node、Deno、Bun、Workers 都实现了同一批标准接口:Request、Response、Headers、fetch,URL、URLSearchParams,ReadableStream、TextEncoder/TextDecoder,crypto.subtle、crypto.randomUUID,structuredClone、AbortController。也就是说,只要业务逻辑只碰这些 API,理论上就能在所有 runtime 上直接跑。真正需要留到 runtime 特定层的是读取环境变量、操作文件系统、访问 KV 存储这类底层能力。文章的核心原则很简单:核心逻辑只用 Web Standard APIs,凡是需要 node:fs、Deno.env 或 env.MY_KV 的地方,全部推到边缘,放进一个薄 adapter 层。\n\n具体做法是把应用写成一个 fetch handler——一个接收 Request 返回 Promise<Response> 的函数,这是所有 runtime 都懂的“共同契约”。文章用 URL 短链服务做示例,核心代码不依赖任何 runtime 特有的东西:用 crypto.subtle 生成 ID(比 node:crypto 更通用,连 Workers 都不用开 nodejs_compat 兼容层);config 通过依赖注入传进来,核心代码里绝不出现 process.env——这是从 Node 迁移到 Workers 时最容易踩的坑,因为 Workers 的 env 是 handler 的参数,不是全局变量;存储层也抽象成接口,Node 上可以用 Redis,Deno 用 Deno.KV,Workers 用 Workers KV,测试时直接用一个 Map 就够了。\n\n如果你不想自己写路由,Hono v4 就是按这个理念设计的,四个 runtime 通吃。但文章强调,用框架之前最好先弄懂底层机制。\n\n每个 runtime 的入口文件只有 5-15 行:读配置、建 store、把依赖传给 createApp,然后调用对应 runtime 的 serve 函数。比如 Node 用 @hono/node-server + ioredis,Deno 用 Deno.openKv + Deno.serve,Bun 直接 Bun.serve,Cloudflare Worker export default 一个 fetch 函数。核心代码完全不知道自己在哪个环境跑。\n\n光说能跑还不够,得用 CI 证明。文章展示了如何用同一个测试文件(直接用 Map 当 store,不监听端口)在不同 runtime 上跑:Node 22+ 自带 test runner 且支持 TS 剥离类型,Deno 2 能跑 node:test 兼容层,Bun 有 bun test,Workers 用 @cloudflare/vitest-pool-workers 在 workerd 里跑真实环境。GitHub Actions 里设一个 job matrix 四条命令一起跑。另外用 ESLint 限制核心目录的 import 和全局变量,禁止 node:*、fs、path、bun 等,也禁止 process、Buffer、Deno、Bun 这些全局。第一次开启这样的规则,你大概率看到几十个报错——那就是你积攒的“锁定债”。\n\n作者也分享了几次踩坑:Buffer 经常通过老库偷偷溜进来,改用 Uint8Array + TextEncoder;很多数据库驱动还走 node:net 或 node:tls,在 Workers 上跑不了,优先选基于 HTTP/fetch 的库;同样是 Request,不同 runtime 的 CPU 时间限制不同,Workers 对每个 request 有硬限制,Node 没有,所以 hash 重或 JSON 解析大的逻辑换到 edge 上要重新评估;top-level await 在 Deno/Node ESM 上没问题,但 Workers 建议在 handler 里 lazy-init。\n\n说到底,runtime 收购潮不会停,你无法控制谁买谁,但可以控制迁移成本。文章建议本周就做这些事:grep 代码库找出 process.env、Buffer、require("fs") 等业务逻辑里的 runtime 依赖;把 handler 改成 (Request) => Promise<Response> 签名,可以先从一个新 endpoint 开始;config 和 storage 改成通过接口注入;给核心目录开 ESLint 限制并在 CI 跑至少两个 runtime;优先使用 crypto.subtle、fetch、ReadableStream 而非 Node 专属 API。做到这些,选 Node 还是 Deno 就只是部署配置问题,不再是架构决策。下次看到“X 收购 Y”的新闻,你就可以安心喝咖啡了。

内容与图片版权归原作者所有 · 原文: https://dev.to/eme_gug_0821b41b948be6516/write-once-run-on-any-runtime-practical-runtime-agnostic-typescript-for-node-deno-bun-and-4g68