サブスクリプションは強力なGraphQL機能であり、フロントエンドのWebSocketなどのテクノロジーを使用して、バックエンドサーバーからリアルタイムで更新を簡単に受信できます。 このクイック投稿では、ApolloClient2.0を使用してAngularでサブスクリプションのフロントエンドをセットアップする方法について説明します。

この投稿の例では、サーバー側でサブスクリプションが適切にセットアップされた状態でGraphQLサーバーが既に稼働していることを前提としています。 graphql-yoga などのツールを使用して独自のGraphQLサーバーを簡単にセットアップするか、Graphcoolフレームワークを使用してGraphcoolにGraphQLサービスをホストさせることができます。

必要なパッケージ

GraphQLサブスクリプションと、通常のクエリとミューテーションの両方を備えたアプリを作成することを想定しているため、通常のHTTPリンクとWebSocketリンクの両方を使用してセットアップします。 split は、 apollo-link パッケージのユーティリティであり、リクエストを正しいリンクに簡単に転送できます。

まず、すべてを機能させるために、たくさんのパッケージが必要になります: apollo-angular apollo-angular-link-http apollo-cache-inmemory [X154X ]、 apollo-link apollo-link-ws apollo-utilities graphql graphql-tag [X268X ]、apollo-clientおよびsubscriptions-transport-ws

npmまたはYarnを使用して、それらすべてを一度にインストールしましょう。

$ npm i apollo-angular apollo-angular-link-http apollo-cache-inmemory apollo-link apollo-link-ws apollo-utilities graphql graphql-tag apollo-client subscriptions-transport-ws

# or, using Yarn:
$ yarn add apollo-angular apollo-angular-link-http apollo-cache-inmemory apollo-link apollo-link-ws apollo-utilities graphql graphql-tag apollo-client subscriptions-transport-ws

設定

Apollo構成とリンクを使用してモジュールを作成します。 それをGraphQLConfigModuleと呼びましょう。

apollo.config.ts
import { NgModule } from '@angular/core';
import { HttpClientModule, HttpClient } from '@angular/common/http';

import { Apollo, ApolloModule } from 'apollo-angular';
import { HttpLinkModule, HttpLink } from 'apollo-angular-link-http';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { split } from 'apollo-link';
import { WebSocketLink } from 'apollo-link-ws';
import { getMainDefinition } from 'apollo-utilities';
@NgModule({
  exports: [HttpClientModule, ApolloModule, HttpLinkModule]
})
export class GraphQLConfigModule {
  constructor(apollo: Apollo, private httpClient: HttpClient) {
    const httpLink = new HttpLink(httpClient).create({
      uri: 'REGULAR_ENDPOINT'
    });
const subscriptionLink = new WebSocketLink({
  uri:
    '___SUBSCRIPTION_ENDPOINT___',
  options: {
    reconnect: true,
    connectionParams: {
      authToken: localStorage.getItem('token') || null
    }
  }
});

const link = split(
  ({ query }) => {
    const { kind, operation } = getMainDefinition(query);
    return kind === 'OperationDefinition' && operation === 'subscription';
  },
  subscriptionLink,
  httpLink
);

apollo.create({
  link,
  cache: new InMemoryCache()
});

構成について注意すべき点がいくつかあります。

  • モジュールのコンストラクターでは、最初にapollo-angular-link-httpパッケージを使用してHTTPリンクを定義します。 内部的には、リンクはAngularのHttpClientを使用しています。
  • 次に、GraphQLサーバーのサブスクリプションエンドポイントとのWebSocketリンクも定義します。 ここでは、WebSocket接続を認証するためにlocalStorageから認証トークンも取得していることがわかります。 サーバーが認証されたサブスクリプションリクエストのみを受け入れる場合は、これが必要になります。
  • 次に、 split ユーティリティを使用します。このユーティリティは、クエリを受け取り、ブール値を返します。 ここでは、 getMainDefinition という別のユーティリティを使用してクエリをチェックし、クエリの種類と操作名を抽出します。 そこから、クエリがサブスクリプションであるかどうかを確認でき、サブスクリプションである場合は、サブスクリプションリンクを使用します。 それ以外の場合、リクエストはHTTPリンクを使用します。
  • 最後に、リンクを作成し、InMemoryCacheをキャッシュに使用します。

この時点で、TypeScriptコンパイラがCannot find name 'AsyncIterator'のようなもので文句を言う場合は、tsconfig.jsonファイルのライブラリのリストにesnextを追加できます。

tsconfig.json
{
  "compileOnSave": false,
  "compilerOptions": {
    "outDir": "./dist/out-tsc",
    ...,
    "lib": [
      "es2017",
      "dom",
      "esnext"
    ]
  }
}

この構成モジュールを配置したら、セットアップに必要なのは、メインのアプリモジュールにモジュールをインポートすることだけです。

app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { GraphQLConfigModule } from './apollo.config';
import { AppComponent } from './app.component';

シンプルなサブスクリプション

これで、フロントエンドでさまざまなイベントをサブスクライブする準備が整いました。 サーバーで作成された新しいToDoを自動的に受信するために実行する簡単なサブスクリプションクエリは次のとおりです。

subscription newTodos {
  Todo(filter: { mutation_in: [CREATED] }) {
    node {
      title
      description
      completed
    }
  }
}

実際のアプリの場合、サービスでサブスクリプションロジックを分離することをお勧めしますが、ここでは、簡単にするために、アプリコンポーネントのクラスをすべて実行します。

app.component.ts
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs/Subscription';

import { Apollo } from 'apollo-angular';
import gql from 'graphql-tag';
const subscription = gqlsubscription newTodos {
    Todo(filter: { mutation_in: [CREATED] }) {
      node {
        title
        description
        completed
      }
    }
  };
interface TodoItem {
  title: string;
  name: string;
  completed: boolean;
}
@Component({ ... })
export class AppComponent implements OnInit, OnDestroy {
  todoSubscription: Subscription;
  todoItems: TodoItem[] = [];
  constructor(private apollo: Apollo) {}
  ngOnInit() {
    this.todoSubscription = this.apollo
      .subscribe({
        query: subscription
      })
      .subscribe(({ data }) => {
        this.todoItems = [...this.todoItems, data.Todo.node];
      });
  }

通常のGraphQLクエリの実行と非常によく似ていることに注意してください。 これが適切に行われると、フロントエンドアプリは自動的に新しいtodoアイテムを受け取ります。

コンポーネントのテンプレートに、次のようなToDoアイテムを表示できます。

app.component.html
<ul>
  <li *ngFor="let item of todoItems">{{ item.title }} - {{ item.description }}</li>
</ul>

watchQuery+サブスクリプション

これまでの例はうまく機能していますが、アプリは新しいToDoしか取得できません。 更新するとすぐに、ToDoアイテムの空のリストが再び表示されます。

簡単な解決策は、最初に通常のクエリを実行してから、サブスクリプションを使用して追加のToDoを自動的に受け取ることです。 これは、最初のwatchQuerysubscribeToMoreメソッドを使用して簡単に実行できます。

app.component.ts
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs/Subscription';

import { Apollo, QueryRef } from 'apollo-angular';
import gql from 'graphql-tag';
const subscription = gqlsubscription newTodos {
    Todo(filter: { mutation_in: [CREATED] }) {
      node {
        title
        description
        completed
      }
    }
  };
const allTodosQuery = gqlquery getTodos {
    allTodos {
      title
      description
      completed
    }
  };
interface TodoItem {
  title: string;
  description: string;
  completed: boolean;
}
@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit, OnDestroy {
  todoSubscription: Subscription;
  todoItems: TodoItem[] = [];
  todoQuery: QueryRef<any>;
  constructor(private apollo: Apollo) {}
  ngOnInit() {
    this.todoQuery = this.apollo.watchQuery({
      query: allTodosQuery
    });
this.todoSubscription = this.todoQuery.valueChanges.subscribe(
  ({ data }) => {
    this.todoItems = [...data.allTodos];
  }
);

this.setupSubscription();  }
  setupSubscription() {
    this.todoQuery.subscribeToMore({
      document: subscription,
      updateQuery: (prev, { subscriptionData }) => {
        if (!subscriptionData.data) {
          return prev;
        }
    const newTodo = subscriptionData.data.Todo.node;

    return Object.assign({}, prev, {
      allTodos: [...prev['allTodos'], newTodo]
    });
  }
});  }

  • 最初にwatchQueryをセットアップし、その valueChanges をサブスクライブして、コンポーネントの初期化時にすべてのToDoアイテムを取得します。
  • 次に、watchQueryでsubscribeToMoreを使用してサブスクリプションを設定します。
  • subscribeToMore は、クエリドキュメント(サブスクリプションクエリ)、必要に応じて変数、前のクエリからデータを取得するクエリ更新関数、およびサブスクリプションデータを含むオブジェクト( subsetData )を受け取ります。 。
  • サブスクリプションデータが空の場合は、以前のデータを返すだけですが、そうでない場合は、以前のデータと新しいデータを含む新しいオブジェクトを作成して返します。