[英]Apollo GraphQL react client: subscription
I was trying to make a little demo with GraphQL subscriptions and GraphQL Apollo client.我试图用 GraphQL 订阅和 GraphQL Apollo 客户端做一个小演示。 I already have my GraphQL API, but when I try to use Apollo client, it looks like it doesn't complete the websocket subscribe step:我已经有了我的 GraphQL API,但是当我尝试使用 Apollo 客户端时,它似乎没有完成 websocket 订阅步骤:
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import { ApolloClient, InMemoryCache, ApolloProvider, gql, useQuery } from '@apollo/client';
import { split, HttpLink } from '@apollo/client';
import { getMainDefinition } from '@apollo/client/utilities';
import { GraphQLWsLink } from '@apollo/client/link/subscriptions';
import { createClient } from 'graphql-ws';
import { useSubscription } from '@apollo/react-hooks'
import reportWebVitals from './reportWebVitals';
const httpLink = new HttpLink({
uri: 'https://mygraphql.api'
});
const wsLink = new GraphQLWsLink(createClient({
url: 'wss://mygraphql.api',
options: {
reconnect: true
}
}));
const splitLink = split(
({ query }) => {
const definition = getMainDefinition(query);
return (
definition.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
);
},
wsLink,
httpLink,
);
const client = new ApolloClient({
link: splitLink,
cache: new InMemoryCache(),
fetchOptions: {
mode: 'no-cors',
}
});
const FAMILIES_SUBSCRIPTION = gql`
subscription{
onFamilyCreated {
id
name
}
}
`;
function LastFamily() {
const { loading, error, data } = useSubscription(FAMILIES_SUBSCRIPTION, {
variables: { },
onData: data => console.log('new data', data)
});
if (loading) return <div>Loading...</div>;
if (error) return <div>Error!</div>;
console.log(data);
const family = data.onFamilyCreated[0];
return (
<div>
<h1>{family.name}</h1>
</div>
);
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
(<ApolloProvider client={client}>
<div>
<LastFamily />
</div>
</ApolloProvider>));
reportWebVitals();
According to graphql-transport-ws, to accomplish a success call, it should call connection_init and subscribe message.根据 graphql-transport-ws,要完成成功调用,它应该调用 connection_init 和订阅消息。 But when I open Dev Tools, it only sends "connection_init"但是当我打开 Dev Tools 时,它只发送“connection_init”
I'm expecting this output:我期待这个 output:
What step should I add to accomplish a successful call using graphql-transport-ws?我应该添加什么步骤来完成使用 graphql-transport-ws 的成功调用?
Ps I'm not a React Developer, just be kind. Ps 我不是 React 开发人员,请友善。
The solutions I'm putting up are based on @apollo/server v.4
, with expressMiddleware
and mongodb/mongoose on the backend and subscribeToMore
with updateQuery
on the client-side instead of useSubscription
hook.我提出的解决方案基于@apollo/server v.4
,后端有expressMiddleware
和 mongodb/mongoose,客户端有subscribeToMore
和updateQuery
而不是useSubscription
钩子。 In light of my observations, I believe there may be some issues with your backend code that require refactoring.根据我的观察,我认为您的后端代码可能存在一些需要重构的问题。 The transport library graphql-transport-ws has been deprecated
and advise to use graphql-ws
.传输库graphql-transport-ws has been deprecated
,建议使用graphql-ws
。 The following setup also applies as of 12.2022.自 12.2022 起,以下设置也适用。
$ npm i @apollo/server @graphql-tools/schema graphql-subscriptions graphql-ws ws cors body-parser mongoose graphql express
import mongoose from 'mongoose'
const Schema = mongoose.Schema
const model = mongoose.model
const FamilySchema = new Schema({
name: {
type: String,
unique: true, //optional
trim: true,
}
})
FamilySchema.virtual('id').get(function () {
return this._id.toHexString()
})
FamilySchema.set('toJSON', {
virtuals: true,
transform: (document, retObj) => {
delete retObj.__v
},
})
const FamilyModel = model('FamilyModel', FamilySchema)
export default FamilyModel
// typeDefs.js
const typeDefs = `#graphql
type Family {
id: ID!
name: String!
}
type Query {
families: [Family]!
family(familyId: ID!): Family!
}
type Mutation {
createFamily(name: String): Family
}
type Subscription {
familyCreated: Family
}
`
// resolvers.js
import { PubSub } from 'graphql-subscriptions'
import mongoose from 'mongoose'
import { GraphQLError } from 'graphql'
import FamilyModel from '../models/Family.js'
const pubsub = new PubSub()
const Family = FamilyModel
const resolvers = {
Query: {
families: async () => {
try {
const families = await Family.find({})
return families
} catch (error) {
console.error(error.message)
}
},
family: async (parent, args) => {
const family = await Family.findById(args.familyId)
return family
},
Mutation: {
createFamily: async (_, args) => {
const family = new Family({ ...args })
try {
const savedFamily = await family.save()
const createdFamily = {
id: savedFamily.id,
name: savedFamily.name
}
// resolvers for backend family subscription with object iterator FAMILY_ADDED
pubsub.publish('FAMILY_CREATED', { familyCreated: createdFamily })
return family
} catch (error) {
console.error(error.message)
}
}
},
Subscription: {
familyCreated: {
subscribe: () => pubsub.asyncIterator('FAMILY_CREATED'),
}
},
Family: {
id: async (parent, args, contextValue, info) => {
return parent.id
},
name: async (parent) => {
return parent.name
}
}
}
export default resolvers
import dotenv from 'dotenv'
import { ApolloServer } from '@apollo/server'
import { expressMiddleware } from '@apollo/server/express4'
import { ApolloServerPluginDrainHttpServer } from '@apollo/server/plugin/drainHttpServer'
import { makeExecutableSchema } from '@graphql-tools/schema'
import { WebSocketServer } from 'ws'
import { useServer } from 'graphql-ws/lib/use/ws'
import express from 'express'
import http from 'http'
import cors from 'cors'
import bodyParser from 'body-parser'
import typeDefs from './schema/tpeDefs.js'
import resolvers from './schema/resolvers.js'
import mongoose from 'mongoose'
dotenv.config()
...
mongoose.set('strictQuery', false)
let db_uri
if (process.env.NODE_ENV === 'development') {
db_uri = process.env.MONGO_DEV
}
mongoose.connect(db_uri).then(
() => {
console.log('Database connected')
},
(err) => {
console.log(err)
}
)
const startGraphQLServer = async () => {
const app = express()
const httpServer = http.createServer(app)
const schema = makeExecutableSchema({ typeDefs, resolvers })
const wsServer = new WebSocketServer({
server: httpServer,
path: '/',
})
const serverCleanup = useServer({ schema }, wsServer)
const server = new ApolloServer({
schema,
plugins: [
ApolloServerPluginDrainHttpServer({ httpServer }),
{
async serverWillStart() {
return {
async drainServer() {
await serverCleanup.dispose()
},
}
},
},
],
})
await server.start()
app.use(
'/',
cors(),
bodyParser.json(),
expressMiddleware(server)
)
const PORT = 4000
httpServer.listen(PORT, () =>
console.log(`Server is now running on http://localhost:${PORT}`)
)
}
startGraphQLServer()
$ npm i @apollo/client graphql graphql-ws
// src/client.js
import { ApolloClient, HttpLink, InMemoryCache, split } from '@apollo/client'
import { getMainDefinition } from '@apollo/client/utilities'
import { defaultOptions } from './graphql/defaultOptions'
import { GraphQLWsLink } from '@apollo/client/link/subscriptions'
import { createClient } from 'graphql-ws'
...
const baseUri = process.env.REACT_APP_BASE_URI // for the client
const wsBaseUri = process.env.REACT_APP_WS_BASE_URI // for the backend as websocket
const httpLink = new HttpLink({
uri: baseUri,
})
const wsLink = new GraphQLWsLink(
createClient({
url: wsBaseUri
})
)
const splitLink = split(
({ query }) => {
const definition = getMainDefinition(query)
return (
definition.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
)
},
wsLink,
httpLink
)
const client = new ApolloClient({
cache: new InMemoryCache(),
link: splitLink,
})
export default client
// src/index.js
import React from 'react'
import ReactDOM from 'react-dom/client'
import client from './client'
import { ApolloProvider } from '@apollo/client'
import App from './App'
const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(
<React.StrictMode>
<ApolloProvider client={client}>
<App />
</ApolloProvider>
</React.StrictMode>
)
// src/graphql.js
import { gql } from '@apollo/client'
// Queries
export const FAMILIES = gql`
query Families {
families {
id
name
}
}
`
export const FAMILY = gql`
query Family($familyId: ID) {
family {
id
name
}
}
`
// Mutation
export const CREATE_FAMILY = gql`
mutation createFamily($name: String!) {
createFamily(name: $name) {
id
name
}
}
`
// Subscription
export const FAMILY_SUBSCRIPTION = gql`
subscription {
familyCreated {
id
name
}
}
Apollo's useQuery
hook provides us with access to a function called subscribeToMore
. Apollo 的useQuery
钩子让我们可以访问一个名为subscribeToMore
的 function。 This function can be destructured and used to act on new data that comes in via subscription.这个 function 可以被解构并用于处理通过订阅传入的新数据。 This has the result of rendering our app real-time.这具有实时呈现我们的应用程序的结果。
The subscribeToMore
function utilizes a single object as an argument. subscribeToMore
function 使用单个 object 作为参数。 This object requires configuration to listen for and respond to subscriptions.这个 object 需要配置来监听和响应订阅。 At the very least, we must pass a subscription document to the document key in this object. This is a GraphQL document in which we define our subscription.至少,我们必须向这个 object 中的文档密钥传递一个订阅文档。这是一个 GraphQL 文档,我们在其中定义了我们的订阅。 We can a updateQuery
field that can be used to update the cache, similar to how we would do in a mutation.我们可以使用一个updateQuery
字段来更新缓存,类似于我们在突变中的做法。
// src/components/CreateFamilyForm.js
import { useMutation } from '@apollo/client'
import { CREATE_FAMILY, FAMILIES } from '../graphql'
...
const [createFamily, { error, loading, data }] = useMutation(CREATE_FAMILY, {
refetchQueries: [{ query: FAMILIES }], // be sure to refetchQueries after mutation
})
...
// src/components/FamilyList.js
import React, { useEffect, useState } from 'react'
import { useQuery } from '@apollo/client'
import { Families, FAMILY_SUBSCRIPTION } from '../graphql'
const { cloneDeep, orderBy } = pkg
...
export const FamilyList = () => {
const [families, setFamilies] = useState([])
const { loading, error, data, subscribeToMore } = useQuery(Families)
...
useEffect(() => {
if (data?.families) {
setFamilies(cloneDeep(data?.families)) // if you're using lodash but it can be also setFamilies(data?.families)
}
}, [data?.families])
useEffect(() => {
subscribeToMore({
document: FAMILY_SUBSCRIPTION,
updateQuery: (prev, { subscriptionData }) => {
if (!subscriptionData.data) return prev
const newFamily = subscriptionData.data.familyCreated
if (!prev.families.find((family) => family.id === newFamily.id)) {
return Object.assign({}, prev.families, {
families: [...prev.families, newFamily],
})
} else {
return prev
}
},
})
}, [subscribeToMore])
const sorted = orderBy(families, ['names'], ['desc']) // optional; order/sort the list
...
console.log(sorted)
// map the sorted on the return statement
return(...)
END.结尾。 Hard-coding some of the default resolvers are useful for ensuring that the value that you expect will returned while avoiding the return of null values.对一些默认解析器进行硬编码有助于确保返回预期的值,同时避免返回 null 值。 Perhaps not in every case, but for fields that refer to other models or schema.也许不是在所有情况下,但对于引用其他模型或模式的字段。
Happy coding!编码愉快!
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.