除了用查询获取数据和用突变修改数据以外,GraphQL还特别支持第三种操作类型,叫subscription。GraphQL订阅是一种将数据从服务器推送到客户端的方法,前提是这些客户端选择监听来自服务器的即时消息。订阅类似于查询操作,因为它们都指定了一组要传递给客户端的字段,但订阅不是立即返回单个应答,而是打开一个通道,每当在服务器上发生特定事件时,才将结果发送给客户端。
订阅的一个常见用例是通知客户端特定事件,例如创建新对象、更新字段等(在此阅读更多)。
使用Apollo驱动程序启用订阅
要启用订阅,设置installSubscriptionHandlers属性为true即可。
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
installSubscriptionHandlers: true,
}),
警告⚠️
installSubscriptionHandlers配置项在最新版本的Apollo服务器中已经被移除了,并将很快在此包中弃用。默认情况下,installSubscriptionHandlers将回退使用subscriptions-transport-ws(阅读更多),但我们强烈建议你使用graphql-ws(阅读更多)库来替代。
要改用graphql-ws包,可以使用以下配置:
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
subscriptions: {
'graphql-ws': true
},
}),
提示
你可以同时使用这两个包(subscriptions-transport-ws和graphql-ws),以便向后兼容。
代码优先
使用代码优先方式创建订阅,我们可以使用@Subscription()装饰器(从@nestjs/graphql包导出)和从graphql-subscriptions包导出的PubSub类,这个类可以提供一个简单的发布/订阅 API。
以下订阅处理器通过调用 PubSub#asyncIterator 负责订阅事件。此方法接收一个参数,即triggerName,它对应一个事件主题名称。
const pubSub = new PubSub();
@Resolver((of) => Author)
export class AuthorResolver {
// ...
@Subscription((returns) => Comment)
commentAdded() {
return pubSub.asyncIterator('commentAdded');
}
}
提示
所有装饰器都是从@nestjs/graphql包中导出的,但PubSub类是从graphql-subscriptions包里导出的。
注意
PubSub是一个类,暴露了一个简单的publish和subscribeAPI。在此了解更多关于它的信息。注意Apollo 在文档中警告了这种默认实现不适合用于生产环境(在此阅读更多)。生产应用应该使用由外部存储(译者注:例如Redis)支持的PubSub实现(在此了解更多)。
最终在SDL中生成GraphQL schema 的以下部分:
type Subscription {
commentAdded(): Comment!
}
请注意,根据定义,订阅会返回一个具有单个顶级属性的对象,该属性的键是订阅的名称。这个名字要么是从订阅处理器方法(即:上面的commentAdded)的名字继承的,要么是通过将带有键名的选项作为第二个参数,传递给@Subscription() 装饰器来显式提供,如下所示。
@Subscription(returns => Comment, {
name: 'commentAdded',
})
subscribeToCommentAdded() {
return pubSub.asyncIterator('commentAdded');
}
此构造生成与前面的代码示例相同的SDL,但允许我们将方法名和订阅分离。
发布
现在可以发布事件了,我们使用PubSub#publish方法。我们通常在突变中使用发布,当对象图的一部分发生更改时触发客户端更新。例如:
//posts/posts.resolver.ts
@Mutation(returns => Post)
async addComment(
@Args('postId', { type: () => Int }) postId: number,
@Args('comment', { type: () => Comment }) comment: CommentInput,
) {
const newComment = this.commentsService.addComment({ id: postId, comment });
pubSub.publish('commentAdded', { commentAdded: newComment });
return newComment;
}
PubSub#publish方法接收一个triggerName(已经提到过,你可以把这个认为是一个事件主题的名称)作为第一个参数,以及事件负载作为第二个参数。如前所述,根据定义,订阅返回一个值并且该值具有特定数据结构。再次查看为我们的commentAdded订阅生成的SDL:
type Subscription {
commentAdded(): Comment!
}
这告诉我们,订阅必须返回一个名为commentAdded且带有顶级属性的对象,它的值是一个Comment对象。需要重点注意的是,这个通过PubSub#publish方法监听的事件的负载的数据结构,必须与我们预期从订阅返回的值的数据结构相对应。所以,在上面的示例中,pubSub.publish('commentAdded', { commentAdded: newComment }) 这段代码表明发布了一个带有适当数据结构负载的commentAdded事件。如果这些数据结构不匹配,你的订阅将会在GraphQL验证阶段失败。
过滤订阅
要过滤特定的事件,需要将filter属性设置为一个过滤函数。此函数的作用类似于传递给数组filter的函数。它需要两个参数:payload包含事件负载(由事件发布者发送),variables接受在订阅请求期间传入的任何参数。它返回一个布尔值来决定是否该事件应该被发布到客户端监听者。
@Subscription(returns => Comment, {
filter: (payload, variables) =>
payload.commentAdded.title === variables.title,
})
commentAdded(@Args('title') title: string) {
return pubSub.asyncIterator('commentAdded');
}
修改订阅负载
要修改已发布的事件负载,需要将resolve属性设置为函数。此函数接收事件负载(由事件发布者发送)并返回适当的值。
@Subscription(returns => Comment, {
resolve: value => value,
})
commentAdded() {
return pubSub.asyncIterator('commentAdded');
}
注意
如果你使用resolve选项,你应该返回一个展开的负载(例如,在我们的例子中,直接返回一个newComment对象,而不是一个{ commentAdded: newComment }对象)
如果你需要访问注入的提供者(例如:使用一个外部服务来验证数据),可以用以下构造。
@Subscription(returns => Comment, {
resolve(this: AuthorResolver, value) {
// "this" refers to an instance of "AuthorResolver"
return value;
}
})
commentAdded() {
return pubSub.asyncIterator('commentAdded');
}
同样的构造也适用于过滤:
@Subscription(returns => Comment, {
filter(this: AuthorResolver, payload, variables) {
// "this" refers to an instance of "AuthorResolver"
return payload.commentAdded.title === variables.title;
}
})
commentAdded() {
return pubSub.asyncIterator('commentAdded');
}
模式优先
在Nest中创建等效订阅,我们将使用@Subscription() 装饰器。
const pubSub = new PubSub();
@Resolver('Author')
export class AuthorResolver {
// ...
@Subscription()
commentAdded() {
return pubSub.asyncIterator('commentAdded');
}
}
基于上下文和参数来过滤指定事件,设置filter属性。
@Subscription('commentAdded', {
filter: (payload, variables) =>
payload.commentAdded.title === variables.title,
})
commentAdded() {
return pubSub.asyncIterator('commentAdded');
}
修改已发布的负载,我们可以使用一个resolve函数。
@Subscription('commentAdded', {
resolve: value => value,
})
commentAdded() {
return pubSub.asyncIterator('commentAdded');
}
如果你要访问注入的提供者(例如:使用一个外部服务来验证数据),使用以下构造:
@Subscription('commentAdded', {
resolve(this: AuthorResolver, value) {
// "this" refers to an instance of "AuthorResolver"
return value;
}
})
commentAdded() {
return pubSub.asyncIterator('commentAdded');
}
同样的构造也适用于过滤:
@Subscription('commentAdded', {
filter(this: AuthorResolver, payload, variables) {
// "this" refers to an instance of "AuthorResolver"
return payload.commentAdded.title === variables.title;
}
})
commentAdded() {
return pubSub.asyncIterator('commentAdded');
}
最后一步是更新类型定义文件。
type Author {
id: Int!
firstName: String
lastName: String
posts: [Post]
}
type Post {
id: Int!
title: String
votes: Int
}
type Query {
author(id: Int!): Author
}
type Comment {
id: String
content: String
}
type Subscription {
commentAdded(title: String!): Comment
}
有了这些,我们就已经创建了一个commentAdded(title: String!): Comment 订阅了。你可以在此处找到示例代码的全部实现。
发布订阅
在上面我们已经实例化了一个本地的PubSub实例。首选的方式是将PubSub定义为提供者并通过构造器(使用@Inject() 装饰器)注入它。这允许我们在整个应用中重用该实例。例如,像下面这样定义一个提供者,然后在需要的地方注入'PUB_SUB'。
{
provide: 'PUB_SUB',
useValue: new PubSub(),
}
自定义订阅服务器
要自定义订阅服务器(例如,更改路径),使用subscriptions 选项属性。
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
subscriptions: {
'subscriptions-transport-ws': {
path: '/graphql'
},
}
}),
如果你使用graphql-ws包进行订阅,将subscriptions-transport-ws这键替换为 graphql-ws即可,如下所示:
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
subscriptions: {
'graphql-ws': {
path: '/graphql'
},
}
}),
WebSockets身份验证
检查用户是否已经验证,可以在onConnect回调函数中完成,你可以在subscriptions配置项中指定这个函数。
onConnect将接收从SubscriptionClient传入的connectionParams作为第一个参数(阅读更多)。
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
subscriptions: {
'subscriptions-transport-ws': {
onConnect: (connectionParams) => {
const authToken = connectionParams.authToken;
if (!isValid(authToken)) {
throw new Error('Token is not valid');
}
// extract user information from token
const user = parseToken(authToken);
// return user info to add them to the context later
return { user };
},
}
},
context: ({ connection }) => {
// connection.context will be equal to what was returned by the "onConnect" callback
},
}),
在本例中,authToken仅在首次建立连接时,由客户端发送一次。使用此连接进行的所有订阅都将具有相同的authToken,因此具有相同的用户信息。
注意
subscriptions-transport-ws中存在一个允许连接跳过onConnect阶段的错误(阅读更多)。你不能假定onConnect在用户开始订阅时总是被调用,并且总是检查上下文是否被填充。
如果你正在使用graphql-ws包,onConnect回调的签名会略有不同:
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
subscriptions: {
'graphql-ws': {
onConnect: (context: Context<any>) => {
const { connectionParams, extra } = context;
// user validation will remain the same as in the example above
// when using with graphql-ws, additional context value should be stored in the extra field
extra.user = { user: {} };
},
},
},
context: ({ extra }) => {
// you can now access your additional context value through the extra field
},
});
使用Mercurius驱动程序启用订阅
要启用订阅,设置subscription属性为true即可。
GraphQLModule.forRoot<MercuriusDriverConfig>({
driver: MercuriusDriver,
subscription: true,
}),
提示
你还可以传递选项对象来设置自定义发射器,验证传入连接等。在此阅读更多(查看subscription)。
代码优先
用代码优先的方式创建订阅,我们需使用@Subscription() 装饰器(从@nestjs/graphql 包导出)以及mercurius包中的PubSub类,它提供了一个简单的发布/订阅 API。
以下订阅处理器通过调用 PubSub#asyncIterator 负责订阅事件。此方法接收一个参数,即triggerName,它对应一个事件主题名称。
@Resolver((of) => Author)
export class AuthorResolver {
// ...
@Subscription((returns) => Comment)
commentAdded(@Context('pubsub') pubSub: PubSub) {
return pubSub.subscribe('commentAdded');
}
}
提示
所有装饰器都是从@nestjs/graphql包中导出的,但PubSub类是从mercurius包里导出的。
注意
PubSub是一个类,暴露了一个简单的publish和subscribeAPI。在此节中查看如何注册一个自定义PubSub类。
最终在SDL中生成GraphQL schema 的以下部分:
type Subscription {
commentAdded(): Comment!
}
请注意,根据定义,订阅会返回一个具有单个顶级属性的对象,该属性的键是订阅的名称。这个名字要么是从订阅处理器方法(即:上面的commentAdded)的名字继承的,要么是通过将带有键名的选项作为第二个参数,传递给@Subscription() 装饰器来显式提供,如下所示。
@Subscription(returns => Comment, {
name: 'commentAdded',
})
subscribeToCommentAdded(@Context('pubsub') pubSub: PubSub) {
return pubSub.subscribe('commentAdded');
}
此构造生成与前面的代码示例相同的SDL,但允许我们将方法名和订阅分离。
发布
现在可以发布事件了,我们使用PubSub#publish方法。我们通常在突变中使用发布,当对象图的一部分发生更改时触发客户端更新。例如:
//posts/posts.resolver.ts
@Mutation(returns => Post)
async addComment(
@Args('postId', { type: () => Int }) postId: number,
@Args('comment', { type: () => Comment }) comment: CommentInput,
@Context('pubsub') pubSub: PubSub,
) {
const newComment = this.commentsService.addComment({ id: postId, comment });
await pubSub.publish({
topic: 'commentAdded',
payload: {
commentAdded: newComment
}
});
return newComment;
}
如前所述,根据定义,订阅返回一个值并且该值具有特定数据结构。再次查看为我们的commentAdded订阅生成的SDL:
type Subscription {
commentAdded(): Comment!
}
这告诉我们,订阅必须返回一个名为commentAdded且带有顶级属性的对象,它的值是一个Comment对象。需要重点注意的是,这个通过PubSub#publish方法监听的事件的负载的数据结构,必须与我们预期从订阅返回的值的数据结构相对应。所以,在上面的示例中,pubSub.publish(topic: 'commentAdded', { commentAdded: newComment }) 这段代码表明发布了一个带有适当数据结构负载的commentAdded事件。如果这些数据结构不匹配,你的订阅将会在GraphQL验证阶段失败。
过滤订阅
要过滤特定的事件,需要将filter属性设置为一个过滤函数。此函数的作用类似于传递给数组filter的函数。它需要两个参数:payload包含事件负载(由事件发布者发送),variables接受在订阅请求期间传入的任何参数。它返回一个布尔值来决定是否该事件应该被发布到客户端监听者。
@Subscription(returns => Comment, {
filter: (payload, variables) =>
payload.commentAdded.title === variables.title,
})
commentAdded(@Args('title') title: string, @Context('pubsub') pubSub: PubSub) {
return pubSub.subscribe('commentAdded');
}
如果你需要访问注入的提供者(例如:使用一个外部服务来验证数据),可以用以下构造。
@Subscription(returns => Comment, {
filter(this: AuthorResolver, payload, variables) {
// "this" refers to an instance of "AuthorResolver"
return payload.commentAdded.title === variables.title;
}
})
commentAdded(@Args('title') title: string, @Context('pubsub') pubSub: PubSub) {
return pubSub.subscribe('commentAdded');
}
模式优先
在Nest中创建等效订阅,我们将使用@Subscription() 装饰器。
const pubSub = new PubSub();
@Resolver('Author')
export class AuthorResolver {
// ...
@Subscription()
commentAdded(@Context('pubsub') pubSub: PubSub) {
return pubSub.subscribe('commentAdded');
}
}
基于上下文和参数来过滤指定事件,设置filter属性。
@Subscription('commentAdded', {
filter: (payload, variables) =>
payload.commentAdded.title === variables.title,
})
commentAdded(@Context('pubsub') pubSub: PubSub) {
return pubSub.subscribe('commentAdded');
}
如果你要访问注入的提供者(例如:使用一个外部服务来验证数据),使用以下构造:
@Subscription('commentAdded', {
filter(this: AuthorResolver, payload, variables) {
// "this" refers to an instance of "AuthorResolver"
return payload.commentAdded.title === variables.title;
}
})
commentAdded(@Context('pubsub') pubSub: PubSub) {
return pubSub.subscribe('commentAdded');
}
最后一步是更新类型定义文件。
type Author {
id: Int!
firstName: String
lastName: String
posts: [Post]
}
type Post {
id: Int!
title: String
votes: Int
}
type Query {
author(id: Int!): Author
}
type Comment {
id: String
content: String
}
type Subscription {
commentAdded(title: String!): Comment
}
发布订阅
在上面的示例中,我们使用的是默认的PubSub发射器(mqemitter),首选的方式(生产)是使用mqemmiter-redis。另外,也可以提供一个自定义的PubSub实现(在此了解更多)。
GraphQLModule.forRoot<MercuriusDriverConfig>({
driver: MercuriusDriver,
subscription: {
emitter: require('mqemitter-redis')({
port: 6579,
host: '127.0.0.1',
}),
},
});
WebSockets身份验证
检查用户是否已经验证,可以在verifyClient回调函数中完成,你可以在subscriptions配置项中指定这个函数。
verifyClient将接收info对象作为第一个参数,你可以使用它来检索请求的头信息。
GraphQLModule.forRoot<MercuriusDriverConfig>({
driver: MercuriusDriver,
subscription: {
verifyClient: (info, next) => {
const authorization = info.req.headers?.authorization as string;
if (!authorization?.startsWith('Bearer ')) {
return next(false);
}
next(true);
},
}
}),