简体   繁体   English

Apollo GraphQL react 客户端:订阅

[英]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,客户端有subscribeToMoreupdateQuery而不是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 起,以下设置也适用。

Subscription on the backend后台订阅

Install the following dependencies.安装以下依赖项。
$ npm i @apollo/server @graphql-tools/schema graphql-subscriptions graphql-ws ws cors body-parser mongoose graphql express

Set up the db models, I will refer to mongodb using mongoose and it might look like this one eg设置数据库模型,我将使用 mongoose 引用 mongodb,它可能看起来像这个,例如
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

Setup schema types & resolvers;设置模式类型和解析器; it might look like this one eg它可能看起来像这个,例如

// 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

At the main entry server file (eg index.js) the code might look like this one eg在主入口服务器文件(例如 index.js)中,代码可能如下所示
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()

Subscription on the CRA frontend在 CRA 前端订阅

Install the following dependencies.安装以下依赖项。
$ npm i @apollo/client graphql graphql-ws

General connection setup eg一般连接设置,例如

// 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>
)

Define the operations types for the client: queries, mutation & subscription eg为客户端定义操作类型:查询、变异和订阅,例如
// 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
    }
  }

Components, it might look like this one eg组件,它可能看起来像这个,例如

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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM