Posted at

Vue.js と AWS AppSync(GraphQL)を使ってチャットアプリを作った


はじめに

以前、「Vue.js×GraphQL(AppSync)×AmplifyでTODOリストを作る」という記事を書きました。

この記事をベースにチームメンバーにハンズオンをしてみると、割と簡単にGraphQLを使うことができると好評でした。

前回は MutationQuery を使って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にメッセージを書き込んだり、取得したりします。

vue-graphql-chat.png


つくったもの

左右のブラウザは違うユーザでログインしています。

メッセージを送信したら、リアルタイムで自分のタイムラインにも反映されます。あと自分のメッセージは削除することができるようになってます。

vue-graphql-chat.gif


ログイン関係の実装

AWS AmplifyのVuejs版である aws-amplify-vue を使って実装しています。

aws-amplify-vue にはログインフォームなどのComponentが用意されています。

詳しいことはaws-amplifyを利用してログイン画面を爆速で実装する .ver Vue.jsを見てください。この記事にまとめてます。


チャット部分の実装

本題はここからです。

GraphQLを使って、チャットアプリのコア部分を実装していきます。


チャットの仕組み

そもそもチャットアプリの大きな機能としては以下が挙げられます。


  1. メッセージを送信する

  2. メッセージを受信する

すごく雑ですが、この2つの機能があれば、とりあえずチャットアプリと言えるのではないでしょうか?

メッセージの送信はクライアントサイドからのトリガーなので、RESTでも簡単に実装できます。

ですがメッセージ受信をRESTで実現しようとするとどうでしょう?

一番最初に思い浮かぶのが、ポーリングですかね。数秒または数分ごとにデータを取得して、変更があれば画面の更新をかける。みたいな流れになります。

ですがこの場合、本当のリアルタイムに更新がかかるとは言えません…特にチャットの場合はメッセージ送信されたら即時に画面の更新をかけてほしいですよね。

そんな悩みを解決してくれるのがGraphQLの Subscription です。


Subscription

簡単に説明すると、GraphQL(ここではAppSync)からクライアントサイドにデータを送信する仕組みです。

データを送信する と書きましたが、実際はセッションを確立してポーリングしているようにも感じます…(ココらへん詳しい方教えてください…)

私の主観ですが、この Subscription がRESTとの違いの一つであり、GraphQLの特徴の一つだと思っています。

graphql_pubsub.png

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で生成してください。


main.ts

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);



aws-exports.js

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 モジュールを利用します。


script

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 を使います。


script

/**

* チャットメッセージ送信
*/

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 を使います。


script

/**

* 最新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 を使っていきます。

この例ではすべてのユーザが送信したメッセージにトリガーして、クライアント画面の更新処理を行っています。


script

/**

* メッセージ新規作成の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_idsato がメッセージを送信したときをトリガーにしたい場合です。


script

const gqlParams: string = `

subscription subCreateChatMessage {
onCreateChatMessageByUserId (
user_id: "sato"
) {
user_id,
create_time,
message_body
}
}
`
;


さいごに

GraphQLの Subscription はポーリングを自前で書くよりも、簡単に実装することができる上に、リクエストを投げ続けているわけではないので、リソースに対する負荷が少なくなります。

今までリアルタイムでグラフを更新するアプリを作ってきましたが、一定間隔でデータを取得するAPIを叩いているだけだったので、間隔が短ければ短いほどDBの負荷が高まるという問題がありました。GraphQLの Subscription を使えば、そういった問題も解消されるのではないか?と思って検証しています。

ただGraphQLは冒頭にも書いたように、RESTに比べてハードルが高いように思います。もっとGraphQLが流行ってハードルが低くなる日を待っています。

ではまた!!