【问题标题】:Relay Modern: Connecting websocket to network layerRelay Modern:将 websocket 连接到网络层
【发布时间】:2017-11-01 06:31:33
【问题描述】:

我在弄清楚如何将 Relay Modern 网络层与我的 websocket 实例连接时遇到问题。

我目前正在将一个 websocket 实例实例化为:

const subscriptionWebSocket = new ReconnectingWebSocket('ws://url.url/ws/subscriptions/', null, options);

我正在指定订阅并创建requestSubscription 的新实例:

const subscription = graphql`
  subscription mainSubscription {
    testData {
      anotherNode {
        data
      }
    }
  }
`;

requestSubscription(
  environment,
  {
    subscription,
    variables: {},
    onComplete: () => {...},
    onError: (error) => {...},
    onNext: (response) => {...},
    updater: (updaterStoreConfig) => {...},
  },
);

然后允许我发送任何订阅请求:

function subscriptionHandler(subscriptionConfig, variables, cacheConfig, observer) {
  subscriptionWebSocket.send(JSON.stringify(subscriptionConfig.text));

  return {
    dispose: () => {
      console.log('subscriptionHandler: Disposing subscription');
    },
  };
}

const network = Network.create(fetchQuery, subscriptionHandler);

通过我的服务器(目前使用 Graphene-python),我能够解释服务器上收到的消息。

但是,我遇到的问题是如何响应订阅;例如,当我的数据库发生变化时,我想生成响应并返回给任何潜在的订阅者。

问题是,如何将 onMessage 事件从我的 websocket 实例连接到我的 Relay Modern Network Layer?我浏览了relay的源代码,但似乎无法弄清楚什么回调,或者什么方法应该实现onreceive。

感谢任何提示。

【问题讨论】:

标签: reactjs websocket relayjs


【解决方案1】:

对于最近遇到此问题的任何人,由于所涉及的库的最新更新,我对上述任一解决方案都没有成功。然而,它们是一个很好的起点,我根据官方的中继现代待办事项示例整理了一个小例子,它非常简约,使用了来自 Apollo 的帮助程序库,但与现代中继很相配:

https://github.com/jeremy-colin/relay-examples-subscription

它包括服务器和客户端

希望对你有帮助

【讨论】:

    【解决方案2】:

    我也设法使用 Relay Modern 进行订阅,并想分享我的最小设置,也许它对某人有帮助!

    请注意,我使用的不是WebSocket,而是subscriptions-transport-ws 中的SubscriptionClient 来管理与服务器的连接。

    这是我的最小设置代码:

    Environment.js

    import { SubscriptionClient } from 'subscriptions-transport-ws'
    const {
      Environment,
      Network,
      RecordSource,
      Store,
    } = require('relay-runtime')
    const store = new Store(new RecordSource())
    
    
    const fetchQuery = (operation, variables) => {
      return fetch('https://api.graph.cool/relay/v1/__GRAPHCOOL_PROJECT_ID__', {
        method: 'POST',
        headers: {
          'Accept': 'application/json',
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          query: operation.text,
          variables,
        }),
      }).then(response => {
        return response.json()
      })
    }
    
    const websocketURL = 'wss://subscriptions.graph.cool/v1/__GRAPHCOOL_PROJECT_ID__'
    
    function setupSubscription(
      config,
      variables,
      cacheConfig,
      observer,
    ) {
      const query = config.text
    
      const subscriptionClient = new SubscriptionClient(websocketURL, {reconnect: true})
      const id = subscriptionClient.subscribe({query, variables}, (error, result) => {
        observer.onNext({data: result})
      })
    }
    
    const network = Network.create(fetchQuery, setupSubscription)
    const environment = new Environment({
      network,
      store,
    })
    
    export default environment
    

    NewLinkSubscription.js

    import {
      graphql,
      requestSubscription
    } from 'react-relay'
    import environment from '../Environment'
    
    const newLinkSubscription = graphql`
      subscription NewLinkSubscription {
        Link {
          mutation
          node {
            id
            description
            url
            createdAt
            postedBy {
              id
              name
            }
          }
        }
      }
    `
    
    export default (onNext, onError, onCompleted, updater) => {
    
      const subscriptionConfig = {
        subscription: newLinkSubscription,
        variables: {},
        onError,
        onNext,
        onCompleted,
        updater
      }
    
      requestSubscription(
        environment,
        subscriptionConfig
      )
    
    }
    

    现在您可以简单地使用导出的函数进行订阅。例如,在componentDidMount 中的一个 React 组件中,我现在可以执行以下操作:

    componentDidMount() {
      NewLinkSubscription(
        response => console.log(`Received data: `, response),
        error => console.log(`An error occurred:`, error),
        () => console.log(`Completed`)
      )
    }
    

    注意SubscriptionClient只有在你的服务器实现this协议时才能使用!

    如果您想了解更多信息,请查看完整堆栈 How to GraphQL tutorial,其中详细描述了如何使订阅与 Relay Modern 一起使用。

    【讨论】:

    【解决方案3】:

    在此线程中找到帮助后,我将写下我是如何处理此问题的。它可能对其他人有用。这在很大程度上取决于您选择的服务器端解决方案。

    我的做法:

    首先,我构建了一个 SubscriptionHandler,它将通过 SubscriptionHandler#setupSubscription 处理 requestStream#subscribeFunction。

    SubscriptionHandler 实例化一个 WebSocket(使用 ReconnectingWebSockets 的自定义版本)并将 onmessage 事件附加到一个内部方法(SubscriptionHandler#receiveSubscriptionPayload),该方法会将有效负载添加到相应的请求中。

    我们通过 SubscriptionHandler#newSubscription 创建新订阅,它将使用内部属性 SubscriptionHandler.subscriptions 添加此订阅的键控条目(我们在查询和变量上使用 MD5 哈希实用程序);意味着该对象将出现:

    SubscriptionHandler.subscriptions = {
      [md5hash]: {
        query: QueryObject,
        variables: SubscriptionVariables,
        observer: Observer (contains OnNext method)
    }
    

    每当服务器发送订阅响应时,都会调用 SubscriptionHandler#receiveSubscriptionPayload 方法,它会使用 query/variables md5 哈希来识别有效负载属于哪个订阅,然后使用 SubscriptionHandler.subscriptions 观察者 onNext 方法。

    这种方式需要服务器返回如下消息:

    export type ServerResponseMessageParsed = {
      payload: QueryPayload,
      request: {
        query: string,
        variables: Object,
      }
    }
    

    我不知道这是否是处理订阅的好方法,但它现在适用于我当前的设置。

    SubscriptionHandler.js

    class SubscriptionHandler {
      subscriptions: Object;
      subscriptionEnvironment: RelayModernEnvironment;
      websocket: Object;
    
      /**
       * The SubscriptionHandler constructor. Will setup a new websocket and bind
       * it to internal method to handle receving messages from the ws server.
       *
       * @param  {string} websocketUrl      - The WebSocket URL to listen to.
       * @param  {Object} webSocketSettings - The options object.
       *                                      See ReconnectingWebSocket.
       */
      constructor(websocketUrl: string, webSocketSettings: WebSocketSettings) {
        // All subscription hashes and objects will be stored in the
        // this.subscriptions attribute on the subscription handler.
        this.subscriptions = {};
    
        // Store the given environment internally to be reused when registering new
        // subscriptions. This is required as per the requestRelaySubscription spec
        // (method requestSubscription).
        this.subscriptionEnvironment = null;
    
        // Create a new WebSocket instance to be able to receive messages on the
        // given URL. Always opt for default protocol for the RWS, second arg.
        this.websocket = new ReconnectingWebSocket(
          websocketUrl,
          null,  // Protocol.
          webSocketSettings,
        );
    
        // Bind an internal method to handle incoming messages from the websocket.
        this.websocket.onmessage = this.receiveSubscriptionPayload;
      }
    
      /**
       * Method to attach the Relay Environment to the subscription handler.
       * This is required as the Network needs to be instantiated with the
       * SubscriptionHandler's methods, and the Environment needs the Network Layer.
       *
       * @param  {Object} environment - The apps environment.
       */
      attachEnvironment = (environment: RelayModernEnvironment) => {
        this.subscriptionEnvironment = environment;
      }
    
      /**
       * Generates a hash from a given query and variable pair. The method
       * used is a recreatable MD5 hash, which is used as a 'key' for the given
       * subscription. Using the MD5 hash we can identify what subscription is valid
       * based on the query/variable given from the server.
       *
       * @param  {string} query     - A string representation of the subscription.
       * @param  {Object} variables - An object containing all variables used
       *                              in the query.
       * @return {string}             The MD5 hash of the query and variables.
       */
      getHash = (query: string, variables: HashVariables) => {
        const queryString = query.replace(/\s+/gm, '');
        const variablesString = JSON.stringify(variables);
        const hash = md5(queryString + variablesString).toString();
        return hash;
      }
    
      /**
       * Method to be bound to the class websocket instance. The method will be
       * called each time the WebSocket receives a message on the subscribed URL
       * (see this.websocket options).
       *
       * @param  {string} message - The message received from the websocket.
       */
      receiveSubscriptionPayload = (message: ServerResponseMessage) => {
        const response: ServerResponseMessageParsed = JSON.parse(message.data);
        const { query, variables } = response.request;
        const hash = this.getHash(query, variables);
    
        // Fetch the subscription instance from the subscription handlers stored
        // subscriptions.
        const subscription = this.subscriptions[hash];
    
        if (subscription) {
          // Execute the onNext method with the received payload after validating
          // that the received hash is currently stored. If a diff occurs, meaning
          // no hash is stored for the received response, ignore the execution.
          subscription.observer.onNext(response.payload);
        } else {
          console.warn(Received payload for unregistered hash: ${hash});
        }
      }
    
      /**
       * Method to generate new subscriptions that will be bound to the
       * SubscriptionHandler's environment and will be stored internally in the
       * instantiated handler object.
       *
       * @param {string} subscriptionQuery - The query to subscribe to. Needs to
       *                                     be a validated subscription type.
       * @param {Object} variables         - The variables for the passed query.
       * @param {Object} configs           - A subscription configuration. If
       *                                     override is required.
       */
      newSubscription = (
          subscriptionQuery: GraphQLTaggedNode,
          variables: Variables,
          configs: GraphQLSubscriptionConfig,
      ) => {
        const config = configs || DEFAULT_CONFIG;
    
        requestSubscription(
          this.subscriptionEnvironment,
          {
            subscription: subscriptionQuery,
            variables: {},
            ...config,
          },
        );
      }
    
      setupSubscription = (
        config: ConcreteBatch,
        variables: Variables,
        cacheConfig: ?CacheConfig,
        observer: Observer,
      ) => {
        const query = config.text;
    
        // Get the hash from the given subscriptionQuery and variables. Used to
        // identify this specific subscription.
        const hash = this.getHash(query, variables);
    
        // Store the newly created subscription request internally to be re-used
        // upon message receival or local data updates.
        this.subscriptions[hash] = { query, variables };
    
        const subscription = this.subscriptions[hash];
        subscription.observer = observer;
    
        // Temp fix to avoid WS Connection state.
        setTimeout(() => {
          this.websocket.send(JSON.stringify({ query, variables }));
        }, 100);
      }
    }
    
    const subscriptionHandler = new SubscriptionHandler(WS_URL, WS_OPTIONS);
    
    export default subscriptionHandler;
    

    【讨论】:

      【解决方案4】:

      我认为this repo 会满足您的需求。 帮助您在服务器端创建订阅

      【讨论】:

      • 我正在寻找 RelayJS 中的实现,因为我正在将旧系统升级到 Relay Modern。我之前使用github.com/edvinerikson/relay-subscriptions 有一段时间了,有点成功。
      • 为 RelayJS 实现订阅服务器端只是对 GraphQL 的订阅。没有额外的协议。
      • 你已经有了 Relay Modern 的订阅客户端,现在你需要实现服务器端,我认为我链接的 repo 可以解决这个问题
      • “你已经有了 Relay Modern 的客户端订阅”部分正确,因为我可以将订阅发送到服务器;但问题是如何设置网络层以接收服务器端响应?我应该用相同的方法(上面的subscriptionHandler)设置响应逻辑吗?
      • 补充一下,我有一个支持使用 Graphene-python 构建的订阅的服务器。因此可以生成响应负载(架构支持它)。
      猜你喜欢
      • 2018-08-30
      • 2017-12-12
      • 2018-05-17
      • 2016-10-14
      • 2018-04-23
      • 2017-10-15
      • 2018-05-15
      • 2017-07-27
      • 1970-01-01
      相关资源
      最近更新 更多