2019.02.13
ChatQLでApollo ClientのOptimistic Responseを使っている部分をちょっとだけ変更して、どのような動作をしているかを確認した。
前回のようにChromeのデベロッパーツールを使ってコードを特定していく。
card-body
とかchat-message
というクラスがついた要素を探していけば見つけられそう。
src/app/chat-app/chat-message/chat-message.component.html
に該当する箇所を発見した。
<div class="card w-75 mb-2 chat-message"
[class.float-right]="fromMe"
[class.border-primary]="fromMe"
[class.border-success]="!fromMe">
<div class="card-body p-2 border rounded">
<div class='clearfix'>
<h6 class="card-title mb-1 float-left"
[class.text-primary]="fromMe"
[class.text-success]="!fromMe">
{{user?.username}}
</h6>
<div class='float-right'>
<small class="card-subtitle mb-0 text-muted">{{createdAt | momentAgo }}</small>
<i class="ion-checkmark" [class.text-muted]="!message.isSent" [class.text-info]="message.isSent" data-pack="default" data-tags="talk"></i>
</div>
</div>
<p class="card-text mb-0">{{message.content}}</p>
</div>
</div>
import { Component, Input, Output, EventEmitter, AfterViewInit, OnInit, OnChanges, SimpleChanges } from '@angular/core';
import { AppsyncService } from '../appsync.service';
import Message from '../types/message';
import readUserFragment from '../graphql/queries/readUserFragment';
import User from '../types/user';
const USER_ID_PREFIX = 'User:';
@Component({
selector: 'app-chat-message',
templateUrl: './chat-message.component.html',
styleUrls: ['./chat-message.component.css']
})
export class ChatMessageComponent implements AfterViewInit, OnInit, OnChanges {
@Input() message: Message;
@Input() fromMe: boolean;
@Input() isLast = false;
@Input() isFirst = false;
@Output() added: EventEmitter<Message> = new EventEmitter();
user: User;
createdAt;
constructor(private appsync: AppsyncService) {}
ngOnInit() {
this.appsync.hc().then(client => {
this.user = client.readFragment({
id: USER_ID_PREFIX + this.message.sender,
fragment: readUserFragment
});
});
}
ngOnChanges(changes: SimpleChanges) {
for (let propName in changes) {
if (propName === 'message') {
const chng = changes[propName];
this.createdAt = chng.currentValue.createdAt.split('_')[0];
}
}
}
ngAfterViewInit() {
if (!this.isFirst && !this.isLast) { return; }
console.log('message: ngAfterViewChecked: ', this.message.id, this.isFirst, this.isLast);
this.added.emit(this.message);
}
}
このChatMessageComponentでは、基本的に@Input
デコレーターにて入力値を親のコンポーネントから受け取るPresentational Componentみたい。基本的に、と書いたのは、ユーザーの情報はまたAppsyncServiceをDIしてそこから手に入れているため。
このようなケースは確かに実装に迷いそうだが、出来るだけContainer ComponentとPresentational Componentに分けたほうが良いのだろうか?
ChatMessageComponentのセレクターはapp-chat-message
なので、プロジェクト全体で検索してみた。利用箇所は1箇所のみで、ChatMessageViewComponentというコンポーネントだった。
<div class="bg rounded p-2 border border-dark rounded">
<div class="p-0 bg">
<span class="d-block font-weight-bold bg-dark text-white text-right rounded p-2 mb-2">
<i class="ion-chatbubbles" data-pack="default" data-tags="talk"></i> {{conversation?.name}}
</span>
<div #scrollMe class="chat" [appInfscroll]="loadMoreMessages" [completedFetching]="completedFetching">
<app-chat-message *ngFor="let message of messages; last as isLast; first as isFirst"
[message]="message"
[fromMe]="fromMe(message)"
[isLast]="isLast"
[isFirst]="isFirst"
(added)="messageAdded(isFirst, $event)"
></app-chat-message>
</div>
</div>
</div>
変数messages
を読み込んでいる部分にApollo Clientからデータを引き出す実装があるはず。
const options = {
query: getConversationMessages,
fetchPolicy: fetchPolicy,
variables: {
conversationId: this._conversation.id,
first: constants.messageFirst
}
};
const observable: ObservableQuery<MessagesQuery> = client.watchQuery(options);
observable.subscribe(({data}) => {
console.log('chat-message-view: subscribe', data);
if (!data) { return console.log('getConversationMessages - no data'); }
const newMessages = data.allMessageConnection.messages;
this.messages = [...newMessages].reverse();
this.nextToken = data.allMessageConnection.nextToken;
console.log('chat-message-view: nextToken is now', this.nextToken ? 'set' : 'null');
});
this.subscription = observable.subscribeToMore({
document: subscribeToNewMessages,
variables: { 'conversationId': this._conversation.id },
updateQuery: (prev: MessagesQuery, {subscriptionData: {data: {subscribeToNewMessage: message }}}) => {
console.log('subscribeToMore - updateQuery:', message);
return unshiftMessage(prev, message);
}
});
client.watchQuery()
で変更を監視できるっぽい。Optimistic Responseの結果とサーバーからのレスポンスはどちらもここに流れてくるのだろうか。
下の方では、this.subscription
に何かを代入している。この変数は、このクラスの始まりの方で以下のような型で定義されている。
subscription: () => void;
この定義から関数が入ることがわかる。なぜsubscribe
とsubscribeToMore
が皆目分からないが、もしかしたらどちらかはWebSocketにてサーバー側からプッシュされた情報をハンドリングするものかもしれない。
このような疑問を持つことを想定してか(?)、いい感じにconsole.log
が仕込んである。これをデベロッパーツールで注視する。
メッセージを送信したブラウザのタブでは以下のようなログがでた。observable.subscribe
とobservable.subscribeToMore
のどちらも発火している。
別画面を開いてメッセージを送信すると、以下のようなログが出た。
あれ、、、
試しに以下の部分をコメントアウトして他の画面からメッセージを送ってみた。
this.subscription = observable.subscribeToMore({
document: subscribeToNewMessages,
variables: { 'conversationId': this._conversation.id },
updateQuery: (prev: MessagesQuery, {subscriptionData: {data: {subscribeToNewMessage: message }}}) => {
console.log('subscribeToMore - updateQuery:', message);
return unshiftMessage(prev, message);
}
});
すると、メッセージを送った画面以外の画面では、自動的に新着メッセージが届かなかった。
やはり、こちらはWebSocketプロトコルを利用してサーバーから送られてくる情報を購読してローカルのキャッシュに書き込む処理を行なっているらしい。
以前気になった、オフライン時に他の画面から送信されたメッセージがオンラインに復帰しても同期されないのはここらへんを改修することでなんとかできるかもしれない。
オンラインに復帰したタイミングを検知して、そのタイミングでデータを読み込む処理を書く必要があるのか、Apollo Clientでやってくれるのかが現時点では不明。(やってくれるなら前述の問題は発生しないはずだが...)
仮にSubscriptionがGraphQLサーバーの方でキューイングされていて、クライアントがオンラインになったら順次プッシュするような形だったとすると、再接続したタイミングでクライアントの負荷が爆上がりする気がする。
Subscriptionはあくまでアプリケーションがアクティブな状態でのデータ更新の検知として使って、基本的なデータの同期はQueryでフェッチする形が良いのかもしれない。
Apollo Clientからclient.watchQuery
でデータを引き出してビューに表示する部分のコードを追ってみた。リモートの変更はwatchQuery
メソッドの戻り値に対してsubscribeToMore
メソッドを呼ぶことで購読できる(気がする)。
次回はこのあたりをしっかりApollo Clientのドキュメントを見て裏を取りたい。
new AWSAppSyncClient()
で生成したインスタンスに対して、hydrated
メソッドを呼んでいるがどういう役割があるのかreadQuery
やwatchQuery
でcache-and-network
のような文字列をfetchPolicyとして指定しているが、指定方法によってどのような違いがあるのか