2019.02.12
AppSync Chat Starterをクローンして起動するところまで前回取り組んだ。今回はメッセージ送信部分のコードを読んでいく。
Google Chromeのデベロッパーツールで送信ボタンを選択してクラス名等を見てみる。
「Type a Message」という文字列はアプリケーションの他の部分では出てきそうになかったので、プロジェクト全体から「Type a Message」で検索してみると予想通りsrc/app/chat-app/chat-input/chat-input.component.html
のみがヒットした。
<div class="rounded p-2 mt-2 bg border border-dark rounded">
<div class="input-group">
<input type="text" class="form-control no-focus"
required placeholder="Type a Message"
id="message" [(ngModel)]="message" name="message"
(keyup.enter)="createNewMessage()" />
<span class="input-group-btn">
<button class="btn btn-dark" (click)="createNewMessage()" type="button">
Send <i class='ion-chatbubble-working'></i>
</button>
</span>
</div>
</div>
ボタンをクリックするとcreateNewMessage
メソッドが呼ばれるように実装されているので、ChatInputコンポーネントの動作が定義されているchat-input.component.ts
を見てみる。
import { Component, Input } from '@angular/core';
import { AppsyncService } from '../appsync.service';
import { v4 as uuid } from 'uuid';
import createMessage from '../graphql/mutations/createMessage';
import getConversationMessages from '../graphql/queries/getConversationMessages';
import { unshiftMessage, constants } from '../chat-helper';
import Message from '../types/message';
import { Analytics } from 'aws-amplify';
@Component({
selector: 'app-chat-input',
templateUrl: './chat-input.component.html',
styleUrls: ['./chat-input.component.css']
})
export class ChatInputComponent {
message = '';
@Input() conversation: any;
@Input() senderId: string;
constructor(private appsync: AppsyncService) {}
createNewMessage() {
if (!this.message || this.message.trim().length === 0) {
this.message = '';
return;
}
const id = `${new Date().toISOString()}_${uuid()}`;
const message: Message = {
conversationId: this.conversation.id,
content: this.message,
createdAt: id,
sender: this.senderId,
isSent: false,
id : id
};
console.log('new message', message);
this.message = '';
this.appsync.hc().then(client => {
client.mutate({
mutation: createMessage,
variables: message,
optimisticResponse: () => ({
createMessage: {
...message,
__typename: 'Message'
}
}),
update: (proxy, {data: { createMessage: _message }}) => {
const options = {
query: getConversationMessages,
variables: { conversationId: this.conversation.id, first: constants.messageFirst }
};
const data = proxy.readQuery(options);
const _tmp = unshiftMessage(data, _message);
proxy.writeQuery({...options, data: _tmp});
}
}).then(({data}) => {
console.log('mutation complete', data);
}).catch(err => console.log('Error creating message', err));
});
Analytics.record('Chat MSG Sent');
}
}
このコンポーネントから直接AppsyncService
を呼び出してAPIをコールしているらしい。
message
という変数に送信するメッセージの情報を構築していき、client.mutate
メソッドでGraphQLのMutationを実行している。OptimisticResponseは、キャッシュに書き込めた時点でコールされるメソッド。
まだ肌感覚だが、optimisticResponse
の部分がオフライン動作に一役買っている気がする。
Apollo Clientの公式ドキュメントのOptimistic UIの記事を読んでみる。
As explained in the mutations section, optimistic UI is a pattern that you can use to simulate the results of a mutation and update the UI even before receiving a response from the server. Once the response is received from the server, the optimistic result is thrown away and replaced with the actual result. (意訳)Mutationのセクションで説明したように、Optimistic UIパターンはMutationの結果をシミュレートしてサーバーからのレスポンスを受け取る前にUIを更新できる設計手法である。サーバーからレスポンスが返ってきた時点で、仮置きしたレスポンスは破棄されて実際のレスポンスに置き換えられる。
要は実行結果をローカルで組み立ててしまい、それを元にしてUIを更新しようという方針らしい。サーバーから結果が返ってきたらしれっと上書きすると。
ChatQLのメッセージ送信部分で、Mutationを実行している部分は以下の通り。
client.mutate({
mutation: createMessage,
variables: message,
optimisticResponse: () => ({
createMessage: {
...message,
__typename: 'Message'
}
}),
update: (proxy, {data: { createMessage: _message }}) => {
const options = {
query: getConversationMessages,
variables: { conversationId: this.conversation.id, first: constants.messageFirst }
};
const data = proxy.readQuery(options);
const _tmp = unshiftMessage(data, _message);
proxy.writeQuery({...options, data: _tmp});
}
}).then(({data}) => {
console.log('mutation complete', data);
}).catch(err => console.log('Error creating message', err));
mutate
メソッドの引数のオブジェクトの中にoptimisticResponse
というキーがある。ここでサーバーにリクエストを投げて、レスポンスが返ってくるまでの間に仮の結果として使うものを準備するらしい。
__typename
はデータをキャッシュする際に正規化するために使われるらしい。
サーバーからのレスポンスが返ってきたらupdate
メソッドの部分が実行されそう。
update: (proxy, {data: { createMessage: _message }}) => {
const options = {
query: getConversationMessages,
variables: { conversationId: this.conversation.id, first: constants.messageFirst }
};
const data = proxy.readQuery(options);
const _tmp = unshiftMessage(data, _message);
proxy.writeQuery({...options, data: _tmp});
}
OptimisticResponseとして書き込んだ内容を検索して、それを置換するような処理は無いように見える。その部分はApollo Clientで内部的にうまく処理しているのだろうか?
これは表示部分がどのような実装になっているか見てみないとわからないが、RxJSのObservableのような形で購読できて、変更が流れてくるのならApollo Clientがやってくれていると見ていいだろう。
optimisticResponse
の戻り値を以下のように変更して試してみる。Chromeのデベロッパーツールを使って通信が遅い場合と完全にオフラインの場合で動作を検証する。
optimisticResponse: () => ({
createMessage: {
...message,
__typename: 'Message'
}
}),
optimisticResponse: () => ({
createMessage: {
conversationId: this.conversation.id,
content: 'OptimisticResponseの本文',
createdAt: id,
sender: this.senderId,
isSent: false,
id : id,
__typename: 'Message'
}
}),
どちらの場合にしても、まずoptimisticResponse
の内容がUIに反映された。
サーバーとの通信が終わったタイミングで、レスポンスに含まれる内容に切り替わった。
ここまで試してみてわかったのが、Apollo Clientによってオフラインのときの動作が実現していて、オンラインに復帰したら通信を再試行して同期してくれるということ。だが、オフライン時の通信要求が大量にあったとき、オンラインになったときに連続でAPIを叩き続ける自体にならないのか疑問に思った。
また、ローカルで保持できるデータにも限りがあると思うので、どこまでオフラインで機能を提供し続けられるのかも気になる。
Apollo ClientのMutationで、OptimisticResponseを利用してオフラインの動作を実現している部分を調査した。次は、ローカルのキャッシュに書き込んだデータや、実際にサーバーに通信したレスポンスがどのようにUIに反映されるのかを表示部分を調査して学ぶ。