はじめに
以前、「Vue.js×GraphQL(AppSync)×AmplifyでTODOリストを作る」という記事を書きました。
この記事をベースにチームメンバーにハンズオンをしてみると、割と簡単にGraphQLを使うことができると好評でした。
前回は Mutation
と Query
を使ってTODOリストチックなメモアプリを構築しました。
今回はGraphQLの Subscription
を使ってチャットアプリを作ってみました。
まとめ
いきなりまとめます。
というかこの記事で伝えたいことを先に書いておきます。
- GraphQLはRESTAPIに比べてハードルは高いかもしれない…
-
Subscription
はGraphQLの中で一番便利!(120%主観) - Messangerなどのチャットアプリの裏側をなんとなく知ることができる
ソースコード
ソースコード一式はgithubにpushしています。
ソースコードの一部を参照しながら説明していきますが、詳細はこちらから確認していただければと思います。
注意事項として、Vueファイルの構成は pug + Typescript + scss となっています。
vue-graphql-chat-source
アーキテクチャ
基本的にクライアント(Vue.js)からAWSリソースへのリクエストはAWS Amplify経由で行います。
Cognitoでログインをして、そのTokenをAppSyncの認証に利用して、DynamoDBにメッセージを書き込んだり、取得したりします。
つくったもの
左右のブラウザは違うユーザでログインしています。
メッセージを送信したら、リアルタイムで自分のタイムラインにも反映されます。あと自分のメッセージは削除することができるようになってます。
ログイン関係の実装
AWS AmplifyのVuejs版である aws-amplify-vue
を使って実装しています。
aws-amplify-vue
にはログインフォームなどのComponentが用意されています。
詳しいことはaws-amplifyを利用してログイン画面を爆速で実装する .ver Vue.jsを見てください。この記事にまとめてます。
チャット部分の実装
本題はここからです。
GraphQLを使って、チャットアプリのコア部分を実装していきます。
チャットの仕組み
そもそもチャットアプリの大きな機能としては以下が挙げられます。
- メッセージを送信する
- メッセージを受信する
すごく雑ですが、この2つの機能があれば、とりあえずチャットアプリと言えるのではないでしょうか?
メッセージの送信はクライアントサイドからのトリガーなので、RESTでも簡単に実装できます。
ですがメッセージ受信をRESTで実現しようとするとどうでしょう?
一番最初に思い浮かぶのが、ポーリングですかね。数秒または数分ごとにデータを取得して、変更があれば画面の更新をかける。みたいな流れになります。
ですがこの場合、本当のリアルタイムに更新がかかるとは言えません…特にチャットの場合はメッセージ送信されたら即時に画面の更新をかけてほしいですよね。
そんな悩みを解決してくれるのがGraphQLの Subscription
です。
Subscription
簡単に説明すると、GraphQL(ここではAppSync)からクライアントサイドにデータを送信する仕組みです。
データを送信する と書きましたが、実際はセッションを確立してポーリングしているようにも感じます…(ココらへん詳しい方教えてください…)
私の主観ですが、この Subscription
がRESTとの違いの一つであり、GraphQLの特徴の一つだと思っています。
GraphQL側では特定の Mutation
が実行されたのをトリガーに、クライアントサイドに Subscription
としてメッセージを送信します。
{
"onCreateChatMessage": {
"create_time": "2019-02-05 15:51:43",
"message_body": "テスト投稿",
"user_id": "sato"
}
}
Vue.jsでAppSync(GraphQL)の基本的な使い方
Amplifyの初期設定
main.ts
にモジュールをimportして、Vueに読み込ませて、Amplifyの初期設定を行います。
※ここの aws-exports.js
はCognitoやAppSyncの情報が書かれているファイルです。githubにはpushしていないのでご自身で用意していただくか、AmplifyCLIで生成してください。
import Amplify, * as AmplifyModules from "aws-amplify";
import { AmplifyPlugin } from "aws-amplify-vue";
import aws_exports from "./aws-exports";
Amplify.configure(aws_exports);
Vue.use(AmplifyPlugin, AmplifyModules);
const awsmobile = {
"aws_project_region": "",
"aws_cognito_identity_pool_id": "",
"aws_cognito_region": "",
"aws_user_pools_id": "",
"aws_user_pools_web_client_id": "",
"aws_appsync_graphqlEndpoint": "",
"aws_appsync_region": "",
"aws_appsync_authenticationType": ""
};
export default awsmobile;
AppSyncを使う
Amplifyに用意されている API
モジュールを利用します。
import { API, graphqlOperation } from "aws-amplify";
async appSyncFunction() {
await API.graphql(graphqlOperation(gqlParams));
}
AppSyncのSchema定義(一部)
AppSyncのSchema定義の一部です。殆どはAppSyncがデフォルトで生成してくれるものですが、一部追加したりしています。
type ChatMessage {
user_id: ID!
create_time: String!
message_body: String!
}
type Mutation {
createChatMessage(input: CreateChatMessageInput!): ChatMessage
updateChatMessage(input: UpdateChatMessageInput!): ChatMessage
deleteChatMessage(input: DeleteChatMessageInput!): ChatMessage
}
type Query {
getChatMessage(user_id: ID!, create_time: String!): ChatMessage
listChatMessages(filter: TableChatMessageFilterInput, limit: Int, nextToken: String): ChatMessageConnection
}
type Subscription {
onCreateChatMessage: ChatMessage
@aws_subscribe(mutations: ["createChatMessage"])
onCreateChatMessageByUserId(user_id: ID, create_time: String, message_body: String): ChatMessage
@aws_subscribe(mutations: ["createChatMessage"])
onUpdateChatMessage(user_id: ID, create_time: String, message_body: String): ChatMessage
@aws_subscribe(mutations: ["updateChatMessage"])
onDeleteChatMessage: ChatMessage
@aws_subscribe(mutations: ["deleteChatMessage"])
onDeleteChatMessageByUserId(user_id: ID, create_time: String, message_body: String): ChatMessage
@aws_subscribe(mutations: ["deleteChatMessage"])
}
メッセージ送信処理(Mutation)
メッセージの送信は Mutation
を使います。
/**
* チャットメッセージ送信
*/
public async createMessage() {
const gqlParams: string = `
mutation put {
createChatMessage(
input: {
user_id: "${VueStore.state.userID}",
create_time: "${dayjs().format("YYYY-MM-DD HH:mm:ss")}",
message_body: "${this.putMessage}"
}
) {
user_id,
create_time,
message_body
}
}
`;
await API.graphql(graphqlOperation(gqlParams));
}
メッセージ取得処理(Query)
メッセージの取得は Query
を使います。
/**
* 最新50件分チャットメッセージを取得
*/
private async getMessages() {
const gqlParams: string = `
query list {
listChatMessages(limit: 50) {
items {
user_id,
create_time,
message_body
}
}
}
`;
const result: any = await API.graphql(graphqlOperation(gqlParams));
// 取得したメッセージ
const messages: ChatMessagesType[] = result.data.listChatMessages.items;
// scanで取得してくるので時系列をソートする必要がある
this.chatMessages = messages.sort((a, b) => {
return dayjs(a.create_time).unix() - dayjs(b.create_time).unix();
});
}
Subscribe処理(Subscription)
Subscription
を使っていきます。
この例ではすべてのユーザが送信したメッセージにトリガーして、クライアント画面の更新処理を行っています。
/**
* メッセージ新規作成のsubscribe
*/
private async createGqlSubscriber() {
const gqlParams: string = `
subscription subCreateChatMessage {
onCreateChatMessage {
user_id,
create_time,
message_body
}
}
`;
this.subCreateChatMessageObservable = await API.graphql(graphqlOperation(gqlParams, {
deleted: false
})) as Observable<object>;
// subscribeセッションを確立する
this.subCreateChatMessageClient = this.subCreateChatMessageObservable.subscribe({
// GraphQLからメッセージを受け取ったときの処理
next: (result: any) => {
console.log(result.value.data);
this.getMessages();
},
error: (err: any) => {
console.error(err);
}
});
}
特定のユーザだけのメッセージ送信に絞ることもできます。
下記の例は user_id
が sato
がメッセージを送信したときをトリガーにしたい場合です。
const gqlParams: string = `
subscription subCreateChatMessage {
onCreateChatMessageByUserId (
user_id: "sato"
) {
user_id,
create_time,
message_body
}
}
`;
さいごに
GraphQLの Subscription
はポーリングを自前で書くよりも、簡単に実装することができる上に、リクエストを投げ続けているわけではないので、リソースに対する負荷が少なくなります。
今までリアルタイムでグラフを更新するアプリを作ってきましたが、一定間隔でデータを取得するAPIを叩いているだけだったので、間隔が短ければ短いほどDBの負荷が高まるという問題がありました。GraphQLの Subscription
を使えば、そういった問題も解消されるのではないか?と思って検証しています。
ただGraphQLは冒頭にも書いたように、RESTに比べてハードルが高いように思います。もっとGraphQLが流行ってハードルが低くなる日を待っています。
ではまた!!