ARTICLE DETAIL

资讯详情

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

用 tRPC standalone-server 示例从零搭建端到端类型安全的 HTTP + WebSocket 服务

用 tRPC standalone-server 示例从零搭建端到端类型安全的 HTTP + WebSocket 服务 用 tRPC standalone-server 示例从零搭建端到端类型安全的 HTTP WebSocket 服务【免费下载链接】trpc‍♀️ Move Fast and Break Nothing. End-to-end typesafe APIs made easy.项目地址: https://gitcode.com/GitHub_Trending/tr/trpc导读examples/standalone-server是 tRPC 官方维护的极简bare-minimum参考实现仅用server.ts与client.ts两个文件就跑通了Node.js 原生 HTTP 服务器 WebSocket 订阅双通道以及基于vanillaTRPCClient无任何框架依赖的端到端类型安全调用。通过本文你将掌握createHTTPServer/applyWSSHandler的最小服务端骨架、客户端splitLink按请求类型分流 HTTP 与 WS 的核心模式以及 query、mutation、subscription 三种 procedure 的完整调用链与运行/测试方式。一、示例定位为什么需要一个零依赖框架的独立服务器tRPC 通常与 Next.js、Express、Fastify 等框架或 Serverless 环境组合使用但官方文档同时强调当你想要一个能跑在任何 Node.js 环境、不引入任何 Web 框架的新项目时Standalone Adapter 是最简单直接的入口——它本质上是包裹在 Node.js 原生 HTTP Server 之上的一个薄层。官方还指出很多生产项目在本地开发时难以直接运行部署形态的适配器如 Lambda因此会保留两个入口本地用 Standalone部署用其他适配器。本示例正是这一理念的最小落地仓库根目录内可直接对照阅读其入口文件 src/server.ts 与 src/client.ts。SKILL 目录中的官方技能文档 adapter-standalone/SKILL.md 也直接将该示例列为sources进一步印证它承载了 Standalone Adapter 的标准用法。示例的三个核心特性源自 README.mdStandalone HTTP 服务器 WebSocket同一进程、同一端口同时对外提供 HTTP 请求与 WebSocket 订阅VanillaTRPCClient客户端不依赖 React、Next.js 等上层封装直接用核心包trpc/client发起调用适用于纯 Node 脚本、CLI 工具等场景Bare-minimum全部代码集中在两个源文件内无框架、无数据库、无多余目录。二、运行环境与启动脚本先看示例的 package.json了解它的运行时依赖与脚本设计。关键依赖依赖说明trpc/server服务端核心包提供initTRPC与 adapterstrpc/client客户端核心包提供createTRPCClient与各 linkwsWebSocket 服务端库Node 没有内置 WS server需要单独安装zod输入校验schema 声明trpc/react-query仓库 workspace 统一引入本示例并未使用开发依赖方面示例使用tsx直接运行 TypeScript免编译、npm-run-allrun-p并行启动、wait-port等待 2022 端口就绪后再启动客户端、start-server-and-test测试编排、esbuild打包构建。npm scripts 一览均在examples/standalone-server目录内执行脚本作用dev:servertsx watch src/server热重载启动服务端dev:client先wait-port 2022等待端口再tsx watch src/clientdev用run-p dev:*并行启动 server 与 clientbuild用 esbuild 将server.ts/client.ts打包为 Node ESM 产物到dist/typechecktsc做全量类型检查test-devstart-server-and-test起服务→探测 2022 端口→跑客户端test-start等价流程但运行的是dist/中 esbuild 产物由于仓库使用 pnpm workspace从仓库根目录可以直接执行pnpm --filter examples-standalone-server dev或先安装依赖后在示例目录中运行pnpm dev。沙箱配置 sandbox.config.json 声明了 Node 20 容器说明该示例面向 Node 20 环境且tsconfig.json使用type: module ESM 模块体系运行。启动顺序上的细节dev:client依赖wait-port 2022是因为客户端一旦启动就会立刻发起 WebSocket 连接与 query 调用若服务端未就绪会直接失败这从侧面说明本示例是先有服务、后有调用的同步演示模型。三、服务端解剖一个 Router 同时服务 HTTP 与 WS服务端全部逻辑在 src/server.ts。它展示了 tRPC 服务端的标准三段式初始化initTRPC→ 声明 Router → 挂载到 Adapter。3.1 ContextHTTP 与 WS 共用的创建函数// This is how you initialize a context for the server function createContext( opts: CreateHTTPContextOptions | CreateWSSContextFnOptions, ) { return {}; } type Context AwaitedReturnTypetypeof createContext;这里有个值得学习的细节HTTP 请求与 WebSocket 连接都会执行 context 创建两者类型分别是CreateHTTPContextOptions来自trpc/server/adapters/standalone与CreateWSSContextFnOptions来自trpc/server/adapters/ws。为了让一个函数同时兼容两条通道示例将其参数类型声明为两者的联合类型再通过AwaitedReturnType...提取返回类型。实际项目中你可以在这个函数里读取opts.req做鉴权、解析opts.info.connectionParams等将用户态注入 Context 供各 procedure 使用。从源码看standalone.ts 中CreateHTTPContextOptions是NodeHTTPCreateContextFnOptionshttp.IncomingMessage, http.ServerResponse即提供req/res而 ws.ts 中CreateWSSContextFnOptions为NodeHTTPCreateContextFnOptionsIncomingMessage, ws.WebSocket除req外还透传res即 WebSocket 实例与info含connectionParams与中止信号signal。3.2 三种 procedurequery / mutation / subscriptionconst t initTRPC.contextContext().create(); const publicProcedure t.procedure; const router t.router; const greetingRouter router({ hello: publicProcedure .input( z.object({ name: z.string(), }), ) .query(({ input }) Hello, ${input.name}!), }); const postRouter router({ createPost: publicProcedure .input( z.object({ title: z.string(), text: z.string(), }), ) .mutation(({ input }) { // imagine db call here return { id: ${Math.random()}, ...input, }; }), randomNumber: publicProcedure.subscription(() { return observable{ randomNumber: number }((emit) { const timer setInterval(() { // emits a number every second emit.next({ randomNumber: Math.random() }); }, 200); return () { clearInterval(timer); }; }); }), });要点归纳queryhello入参经 zod 校验为{ name: string }同步返回字符串走 HTTP GET/POST 语义mutationcreatePost接收{ title, text }模拟数据库写入后返回带id的对象——代码注释// imagine db call here明确指出该位置应替换为真实的 DB/ORM 调用subscriptionrandomNumber必须返回trpc/server/observable提供的observable或异步生成器。示例每 200msemit.next(...)一个随机数并在清理函数中clearInterval这是释放定时器等资源的规范姿势当客户端取消订阅或断开连接时该清理函数会被执行。subscription背后的运行时行为可在 ws.ts 中看到服务端把 observable 转成异步迭代器后逐条转发客户端通过subscription.stop消息触发服务端abort见 ws.ts 中对subscription.stop的处理分支。这正是订阅取消能及时停止后端推送的底层保障。3.3 Router 合并与类型导出// Merge routers together const appRouter router({ greeting: greetingRouter, post: postRouter, }); export type AppRouter typeof appRouter;用嵌套 Router组织命名空间客户端将以trpc.greeting.hello、trpc.post.randomNumber的路径访问导出类型而非实例export type AppRouter typeof appRouter;是 tRPC 端到端类型安全的枢纽客户端侧通过import type { AppRouter } from ./server拿到整棵 API 的类型签名。关于 Router 合并与类型导出的更多细节可参考 merging-routers.md 与 procedures.md。3.4 同一端口上的 HTTP 服务器 WebSocket 服务器// http server const server createHTTPServer({ router: appRouter, createContext, }); // ws server const wss new WebSocketServer({ server }); applyWSSHandlerAppRouter({ wss, router: appRouter, createContext, }); server.listen(2022);这是整个示例最核心的架构技巧createHTTPServer({ router, createContext })创建原生 HTTP 服务器并挂载 tRPC 请求处理。从源码看standalone.ts 的实现极其直白——createHTTPServer就是http.createServer(createHTTPHandler(opts))即它返回的是一个标准http.Server实例因此可以调用 Node 原生.listen()new WebSocketServer({ server })把ws的 WebSocketServer 附着在同一个 HTTP server上——HTTP 升级握手与普通请求共享 2022 端口无需单独开 WS 端口applyWSSHandler将 tRPC 的 WS 协议处理绑定到wss的connection事件上。源码中 applyWSSHandler 支持prefix按路径前缀过滤连接、keepAlive心跳保活、experimental_encoder自定义线协议编码等选项并返回一个带broadcastReconnectNotification()的对象可用于服务端向所有客户端广播请重连的消息。server.ts 末尾还保留了注释掉的调试片段wss.clients.size可以实时观察在线客户端数量实战排查连接问题时非常有用。四、客户端解剖splitLink 把订阅走 WS、其余走 HTTP自动分流客户端 src/client.ts 演示的是纯 Node 环境的 vanilla 调用方式。由于 Node 默认不带WebSocket全局对象第一步要先做 shimimport { WebSocket } from ws; globalThis.WebSocket WebSocket as any;随后创建 WS 客户端并组装 linksconst wsClient createWSClient({ url: ws://localhost:2022, }); const trpc createTRPCClientAppRouter({ links: [ // call subscriptions through websockets and the rest over http splitLink({ condition(op) { return op.type subscription; }, true: wsLink({ client: wsClient, }), false: httpLink({ url: http://localhost:2022, }), }), ], });这段代码是HTTP WS 双通道在客户端侧的对应物值得逐行理解splitLink是一个路由器性质的 link对每个操作执行condition(op)判断返回true走wsLinkfalse走httpLink分流规则op.type subscription表示只有订阅操作走 WebSocketquery 与 mutation 全部走 HTTP。这与大多数业务场景订阅是长连接、即时查询是一次性请求匹配避免为普通请求长期占用 WS 连接wsLink/httpLink分别实现 WebSocket 与 HTTP 传输层createWSClient({ url })负责 WS 连接生命周期管理断线重连、关闭。关于 link 体系的更多内容可参考 www/docs/client/links/overview.md。从 react-query 侧文档 等同仓库资料可以得知splitLink的这一写法是 tRPC 社区处理混合传输的标准模板也是官网 quickstart 中推荐的模式之一。五、端到端演示query、mutation、subscription 依次跑通main()函数顺序演示了三种 procedure 的完整调用方式async function main() { const helloResponse await trpc.greeting.hello.query({ name: world, }); console.log(helloResponse, helloResponse); const createPostRes await trpc.post.createPost.mutate({ title: hello world, text: check out https://tRPC.io, }); console.log(createPostResponse, createPostRes); let count 0; await new Promisevoid((resolve) { const subscription trpc.post.randomNumber.subscribe(undefined, { onData(data) { // ^ note that data here is inferred console.log(received, data); count; if (count 3) { // stop after 3 pulls subscription.unsubscribe(); resolve(); } }, onError(err) { console.error(error, err); }, }); }); await wsClient.close(); } void main();三个关键观测点类型推断贯穿全链路helloResponse自动推断为stringdata被推断为{ randomNumber: number }——这正是注释datahere is inferred 想强调的效果也是AppRouter类型从服务端流向客户端的结果。入参的类型约束同样生效例如给createPost少传字段或给hello传非string的name都会在编译期直接报错。订阅的生命周期管理subscribe(undefined, {...})的第一个参数是输入本例无输入故传undefined回调中维护计数器收到 3 条数据后调用subscription.unsubscribe()主动停止并resolve()退出 Promise随后wsClient.close()关闭 WS 连接保证 Node 进程能干净退出——如果忘记关闭连接/取消订阅事件循环会被计时器或连接句柄拖住。错误处理路径onError回调负责捕获订阅期间的传输或过程错误而 query/mutation 的失败则通过await抛出的异常捕获。执行后预期日志大致为helloResponse Hello, world! createPostResponse { id: 0.123456789, title: hello world, text: ... } received { randomNumber: 0.53 } received { randomNumber: 0.87 } ...六、从最小示例到实战createHTTPHandler、basePath、CORS 与 HTTP/2最小示例刻意省略了生产环境常见需求官方文档与技能文档则给出了补齐这些能力的标准配方见 www/docs/server/adapters/standalone.md 与 packages/server/skills/adapter-standalone/SKILL.md。以下是可直接迁移的关键模式。6.1 自定义 HTTP servercreateHTTPServer 之外的自由度createHTTPServer不适合需要在同一个 HTTP 服务里塞入健康检查、静态资源等自定义逻辑的场景。此时改用createHTTPHandler返回一个裸的RequestListener由你自己掌控http.createServerimport { createServer } from http; import { createHTTPHandler } from trpc/server/adapters/standalone; const handler createHTTPHandler({ router: appRouter, createContext() { return {}; }, }); createServer((req, res) { if (req.url?.startsWith(/health)) { res.writeHead(200); res.end(OK); return; } handler(req, res); }).listen(3000);从源码角度看createHTTPServer本身就是http.createServer(createHTTPHandler(opts))standalone.ts所以这种写法与最小示例在 tRPC 处理链路上完全等价只是把主动权交还给你。这也意味着 src/server.ts 中server.listen(2022)返回的就是标准http.Server.clients、.close()等 Node API 均可直接使用。6.2 basePath剥离 URL 前缀再路由当服务需要以/trpc/作为统一前缀对外暴露例如前置网关按路径分流时使用basePathconst handler createHTTPHandler({ router: appRouter, basePath: /trpc/, });basePath会在路由前从请求路径中剥离前缀使/trpc/greeting.hello仍解析到helloprocedure。注意源码注释强调务必包含结尾斜杠example /trpc/且默认值为/standalone.ts。底层实现中 handler 用url.pathname.slice(basePath.length)完成裁剪见 standalone.ts。6.3 CORSStandalone 默认不做跨域处理官方文档明确说明Standalone 服务器默认不响应 HTTP OPTIONS 预检、不设置任何 CORS 头。如果你的客户端跑在浏览器且与 API 不同源例如本地开发时 Vite dev server 在 5173、API 在 2022就必须显式处理。最简做法是借助cors包并以middleware选项注入npm install cors types/corsimport cors from cors; createHTTPServer({ middleware: cors({ origin: http://localhost:5173 }), router: appRouter, createContext, }).listen(3000);middleware接受任何 connect/Node 风格中间件函数但官方提醒它只是一个简单的逃生舱它不会替你组合多个中间件若需要多中间件组合应改用 Express 适配器或用connect做组合或直接回到 6.1 的自定义createHTTPHandler方案。关于 Express 对照可参见 www/docs/server/adapters/express.md。6.4 HTTP/2 与 TLS需要 HTTP/2 TLS 时Standalone 提供createHTTP2Handler与配套的CreateHTTP2ContextOptionsstandalone.tsimport http2 from http2; import { readFileSync } from node:fs; import { createHTTP2Handler } from trpc/server/adapters/standalone; import type { CreateHTTP2ContextOptions } from trpc/server/adapters/standalone; async function createContext(opts: CreateHTTP2ContextOptions) { return {}; } const handler createHTTP2Handler({ router: appRouter, createContext }); const server http2.createSecureServer( { key: readFileSync(./certs/server.key), cert: readFileSync(./certs/server.crt) }, (req, res) handler(req, res), ); server.listen(3001);6.5 maxBatchSize限制单次批量请求条数tRPC 支持把多个请求打包进一次 HTTP 请求request batching。Standalone 提供maxBatchSize上限保护超过上限的批量请求会收到400 Bad RequestcreateHTTPServer({ router: appRouter, maxBatchSize: 10, }).listen(3000);同时应把客户端httpBatchLink的maxItems设为相同值避免客户端发出的批次数超过服务端限额。6.6 WS 增强项在 ws.ts 中可以看到applyWSSHandler还支持keepAlive心跳保活默认关闭启用后pingMs默认 30s、pongWaitMs默认 5s 未收到 pong 即terminate()断连、prefix按 URL 前缀决定是否接受该 WS 连接、experimental_encoder自定义线协议编码等选项。最小示例虽未使用但它们是真实生产订阅服务如示例同源的 www/docs/server/subscriptions.md最常见的调优项。七、为什么说这是从零起步的最佳学习样板综合来看examples/standalone-server以两个源文件覆盖了 tRPC 应用的最小闭环层次本示例中的落点对应能力Router 定义greeting/post嵌套路由组织 API 命名空间Procedure 类型query / mutation / subscriptiontRPC 三类过程全覆盖服务端传输createHTTPServerapplyWSSHandler共用 2022 端口一次启动双协议客户端传输splitLink分流wsLink/httpLink按需选择传输层类型安全闭环export type AppRouter↔import type入参/出参全程编译期校验运行与测试pnpm dev/pnpm test-dev热重载与编排验证官方文档将 Standalone Adapter 定位为本地开发与基于服务器的生产环境的理想起点同时坦诚其 CORS 缺失、中间件组合能力有限等边界——这恰恰说明它适合学习 tRPC 核心概念与快速原型而当需要框架级中间件生态时应转向 adapter-express、adapter-fastify 或面向 Serverless 的 adapter-fetch、adapter-aws-lambda 等适配器。想要亲手验证端到端流程可以在示例目录执行pnpm dev自动先后启动 server 与 client观察控制台输出或运行pnpm build pnpm test-start验证 esbuild 产物同样可用。如果你打算把它作为新项目起点只需保留server.ts中的 Router 骨架与client.ts中的 link 配置将hello替换为你的真实业务 procedure 即可。参考文件索引示例入口文档examples/standalone-server/README.md服务端源码examples/standalone-server/src/server.ts客户端源码examples/standalone-server/src/client.ts工程配置examples/standalone-server/package.jsonAdapter 实现packages/server/src/adapters/standalone.tsWebSocket handler 实现packages/server/src/adapters/ws.ts官方适配器文档www/docs/server/adapters/standalone.md技能文档packages/server/skills/adapter-standalone/SKILL.md【免费下载链接】trpc‍♀️ Move Fast and Break Nothing. End-to-end typesafe APIs made easy.项目地址: https://gitcode.com/GitHub_Trending/tr/trpc创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表