- 后端
- API设计
【免费下载链接】graphql-yoga
🧘 Rewrite of a fully-featured GraphQL Server with focus on easy setup, performance & great developer experience. The core of Yoga implements WHATWG Fetch API and can run/deploy on any JS environment.
本文以 graphql-yoga 仓库中examples/envelop目录下的 graphql-ws 示例为核心,讲解如何用 Envelop(Yoga 生态的请求编排层)配合graphql-ws库搭建一个支持 Subscription 的 WebSocket GraphQL 服务。读完本文,你可以完整复现该示例的运行流程,理解envelop()返回的每次请求级 API(parse/validate/contextFactory/execute/subscribe)如何注入到graphql-ws的onSubscribe钩子中,并掌握端口、路径等关键配置。
示例定位:Envelop 与 WebSocket 协议层的关系
在 graphql-yoga 仓库中,packages/envelop是独立维护的核心包集合(core、types、plugins 等),examples/envelop目录则提供了一批“Envelop 与不同 GraphQL 协议/宿主组合”的可运行示例,graphql-ws 示例 就是其中之一。它的定位很明确:
- 不经过 HTTP:
graphql-ws(官方 WebSocket 订阅协议库)通过useServer直接挂载到 Node.js 的wsWebSocket 服务上; - Envelop 负责请求编排:schema、解析器、校验、上下文构造等逻辑全部由
envelop()返回的getEnveloped按请求(这里按 WebSocket 连接)组装; - 示例最小化:整个服务端只有一个入口文件 index.ts,便于理解协议层与 Envelop 的接线方式。
示例自带的 README 说明了运行前提与验证方式,下面的内容将 README 中的三个步骤展开,并逐段解析入口源码。
运行示例:依赖安装与启动步骤
README 给出的运行流程共三步:
在仓库根目录用
pnpm安装全部依赖。本仓库是 pnpm workspace 工程(根目录有 pnpm-workspace.yaml),示例包@envelop-examples/graphql-ws通过通配依赖引用本仓库内的@envelop/core:{ "dependencies": { "@envelop/core": "*", "@graphql-tools/schema": "10.0.31", "graphql": "17.0.2", "graphql-ws": "^6.0.0", "ws": "8.20.1" }, "scripts": { "start": "ts-node index.ts" } }以上片段来自 package.json。注意几个关键点:
"@envelop/core": "*"表示由 workspace 解析到本仓库packages/envelop/core的本地版本,因此必须在仓库根目录执行安装,单独在该目录pnpm install无法解析这个依赖;- 运行时脚本是
ts-node index.ts,即直接用 ts-node 编译执行 TypeScript 入口,无需预先构建; graphql-ws@^6.0.0是 WebSocket 协议实现,ws@8.20.1提供 Node.js 侧的 WebSocket 服务器。
进入示例目录并启动服务:
cd examples/envelop/graphql-ws pnpm run start启动后 WebSocket 服务监听在本机3415 端口、
/graphql路径(这两个值定义在入口文件的ws.Server配置中,见下文)。用 GraphiQL 验证连接:README 指引使用
graphql-ws项目官方维护的 “GraphiQL + graphql-ws” 客户端示例页(该示例是一个可直接在浏览器打开的 HTML 文件,在graphql-ws项目的仓库资料中提供),将其中的 WebSocket URL 改为ws://localhost:3415/graphql,在浏览器中打开即可。在该页面执行订阅操作后,你会每秒收到一条来自服务端的问候消息,与下文Subscription解析器的行为一一对应。
源码解析:从 Schema 到 WebSocket 服务器的完整接线
整个服务端逻辑集中在 index.ts,可以按“Schema 定义 → Envelop 装配 → graphql-ws 服务器挂载”三段来读。
1. 定义一个带 Subscription 的可执行 Schema
示例用@graphql-tools/schema的makeExecutableSchema构造 schema(index.ts 第 7-31 行):
const schema = makeExecutableSchema({ typeDefs: /* GraphQL */ ` type Query { hello: String! } type Subscription { greetings: String! } `, resolvers: { Query: { hello: () => 'Hello World!', }, Subscription: { greetings: { subscribe: async function* sayHiIn5Languages() { for (const hi of ['Hi', 'Bonjour', 'Hola', 'Ciao', 'Zdravo']) { yield { greetings: hi }; await new Promise(resolve => setTimeout(resolve, 1000)); // wait 1 second } }, }, }, }, });Subscription.greetings的解析器是一个异步生成器:依次产出 5 条不同语言的问候(Hi / Bonjour / Hola / Ciao / Zdravo),每 1 秒推送一条,共 5 秒后结束。这正是 GraphQL Subscription 的最小实现形态——解析器返回 AsyncIterable,服务端把每次yield的值作为一条next消息推给客户端。
2. 装配 Envelop:envelop()与getEnveloped
const getEnveloped = envelop({ parse, validate, execute, subscribe, plugins: [useSchema(schema), useLogger()], });(index.ts 第 33-39 行)
这里直接传入graphql包的四个引擎函数parse / validate / execute / subscribe作为默认实现,并用两个插件完成装配:
useSchema(schema):把上面定义的 schema 注入 Envelop。从 use-schema.ts 的实现看,该插件只是通过onPluginInit钩子调用setSchema(schema),把 schema 写入编排器状态,之后每次请求拿到的schema都来自这里;useLogger():打印每次解析后的操作,便于在终端观察订阅请求。
envelop()的返回值并不是“已经执行好的结果”,而是一个getEnveloped函数。从 create.ts 的源码看,envelop()先根据插件列表创建 Envelop 编排器(orchestrator)与 instrumentation,然后返回一个getEnveloped(initialContext);调用getEnveloped时才会运行各插件的初始化逻辑,并返回一组绑定到当前请求上下文的 API:
return { parse: ..., validate: ..., contextFactory: ..., execute: ..., subscribe: ..., schema: ..., };也就是说:每调用一次getEnveloped(ctx),就得到一套针对该连接/请求定制的 parse、validate、contextFactory、execute、subscribe 和 schema,插件可以在每个钩子(init、parse、context、execute、validate 等)上改写这组函数的行为。这个“一次连接、一次装配”的模型正是 Envelop 适配 WebSocket 这类长连接协议的关键——HTTP 场景中按请求装配,WebSocket 场景则按连接装配。
3. 挂载useServer:把 Envelop 接入 graphql-ws
useServer( { execute: (args: any) => args.rootValue.execute(args), subscribe: (args: any) => args.rootValue.subscribe(args), onSubscribe: async (ctx, msg) => { const { schema, execute, subscribe, contextFactory, parse, validate } = getEnveloped({ connectionParams: ctx.connectionParams, socket: ctx.extra.socket, request: ctx.extra.request, }); const args = { schema, operationName: msg.payload.operationName, document: parse(msg.payload.query), variableValues: msg.payload.variables, contextValue: await contextFactory(), rootValue: { execute, subscribe, }, }; const errors = validate(args.schema, args.document); if (errors.length) return errors; return args; }, }, new ws.Server({ port: 3415, path: '/graphql', }), );(index.ts 第 41-74 行)
这段接线可以分为四层理解:
(1)WebSocket 服务器配置。useServer的第二个参数是ws库创建的服务器实例,port: 3415、path: '/graphql'决定了客户端必须连接ws://localhost:3415/graphql。README 中要求的 GraphiQL URL 正是由此而来。
(2)onSubscribe:每次订阅建立时的入口钩子。graphql-ws在客户端发送subscribe消息后调用onSubscribe(ctx, msg),其中ctx.connectionParams是客户端连接参数,ctx.extra.socket/ctx.extra.request分别携带底层 WebSocket 连接与 HTTP 升级请求(这两个字段是useServer对ws的ws/req对象做的透传)。钩子有两种返回值:
- 返回错误数组(或 falsy 值)→ 拒绝该订阅,错误会作为
error消息发回客户端; - 返回参数对象 →
graphql-ws用其中的schema、document、variableValues、operationName、contextValue、rootValue去执行订阅(Query 操作同理走execute)。
(3)getEnveloped的注入点。示例把connectionParams、socket、request放进初始上下文交给getEnveloped,之后插件在onEnveloped/onContext等钩子里就能按连接定制行为(例如基于connectionParams做鉴权、基于socket记录日志)。返回的contextFactory被await后作为contextValue使用——Envelop 的上下文钩子链(onContext)就发生在这一次调用内部。
(4)rootValue 的技巧。execute/subscribe被放进rootValue,而顶层配置又写为execute: (args) => args.rootValue.execute(args)、subscribe: (args) => args.rootValue.subscribe(args)。graphql-ws的execute/subscribe回调在每次消息执行时才被调用,此时args.rootValue正是onSubscribe返回的对象——这样每次 Query/Subscription 执行拿到的都是 Envelop 编排过、绑定到当前连接的execute/subscribe,而不是模块加载时的原始graphql引擎函数。
(5)校验时机。validate(args.schema, args.document)在onSubscribe内同步执行;若有错误直接返回错误数组,拒绝订阅。这里用的是 Envelop 返回的validate,因此插件追加的自定义校验规则同样会在订阅建立前生效。
4. 客户端视角:订阅会收到什么
在 GraphiQL(graphql-ws 客户端模式)中执行:
subscription { greetings }服务端异步生成器每秒yield一次,客户端会依次收到 5 条消息:Hi、Bonjour、Hola、Ciao、Zdravo,5 秒后订阅自然结束。若执行query { hello },则得到"Hello World!"——该 Query 同样经过 Envelop 编排后的execute执行。
关键参数与复用要点汇总
| 配置/参数 | 取值(本示例) | 出处 | 说明 |
|---|---|---|---|
| WebSocket 端口 | 3415 | index.ts | ws.Server({ port }),客户端 URL 需一致 |
| 路径 | /graphql | 同上 | 完整端点为ws://localhost:3415/graphql |
| 依赖版本 | graphql-ws@^6.0.0、ws@8.20.1、graphql@17.0.2 | package.json | 升级协议库时注意useServer选项兼容性 |
| 运行方式 | ts-node index.ts | 同上 | 需先在仓库根目录用 pnpm 安装 workspace 依赖 |
| Envelop 插件 | useSchema(schema)、useLogger() | index.ts | 可扩展为鉴权、限流等插件(见packages/envelop/plugins) |
复用这个骨架时的几个要点:
- 保持“每次连接调用一次
getEnveloped,这样插件上下文(connectionParams、socket 等)天然按连接隔离; - 鉴权放在
onSubscribe:graphql-ws的onSubscribe返回错误即拒绝订阅,这是该协议推荐的鉴权位置,也可以借助 Envelop 插件(如packages/envelop/plugins下的generic-auth、disable-introspection)以插件形式实现; - 生产环境注意
ws的路径约束:ws.Server要求显式path,不要省略,否则非 WebSocket 的 HTTP 请求处理行为受限; - 该示例刻意不含 HTTP 端点。若同一服务还要提供 GraphQL over HTTP,可参考仓库
examples/envelop/graphql-http等其他示例的组合方式,或直接在 Yoga 中集成graphql-ws协议。
小结
这个示例用不到 75 行代码展示了 Envelop 在长连接协议下的标准接线方式:envelop()负责装配出按连接定制的 parse/validate/execute/subscribe 管线,graphql-ws的useServer在onSubscribe钩子中消费这条管线,ws.Server决定监听地址。运行入口与验证方式见 examples/envelop/graphql-ws/README.md,完整源码见 examples/envelop/graphql-ws/index.ts,Envelop 核心装配逻辑可继续深入 packages/envelop/core/src/create.ts 与 packages/envelop/core/src/plugins/use-schema.ts 查阅。
- 后端
- API设计
【免费下载链接】graphql-yoga
🧘 Rewrite of a fully-featured GraphQL Server with focus on easy setup, performance & great developer experience. The core of Yoga implements WHATWG Fetch API and can run/deploy on any JS environment.
相关推荐
GraphQL Yoga 生态实战:基于 Envelop 与 graphql-sse 实现 Server-Sent Events 订阅服务
GraphQL Yoga 生态实战:基于 Envelop 与 graphql sse 实现 Server Sent Events 订阅服务 本文以仓库中 exa
后端API设计从零开始使用graphql-ws:构建高性能WebSocket GraphQL服务的完整指南
从零开始使用graphql ws:构建高性能WebSocket GraphQL服务的完整指南 graphql ws是一个零依赖、轻量级且符合GraphQL ov
基于 Prisma 服务构建 GraphQL 服务器:使用 graphql-yoga 与 prisma-binding 的完整实战
基于 Prisma 服务构建 GraphQL 服务器:使用 graphql yoga 与 prisma binding 的完整实战 本文是一篇完整的实战指南:以
后端数据库GraphQL
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考