Twilio ConversationsとVue.jsでチャットアプリケーションを作る(後編)

August 26, 2021
執筆者
レビュー担当者

Twilio ConversationsとVueでチャットアプリケーションを作る

本稿は前編と後編に分かれており、前編ではプロジェクトのセットアップからバックエンドの構築までを、後編ではフロントエンド側の構築とアプリケーションの動作検証についてご紹介いたします。

前編はこちら:「Twilio ConversationsとVue.jsでチャットアプリケーションを作る(前編)

フロントエンドを構築する

フロントエンド側はVueコンポーネントを使って構築します。このチャットアプリケーションで使用するコンポーネントはシングルファイルコンポーネント(Single file component)と呼ばれる、.vue拡張子のコンポーネントです。シングルファイルコンポーネントを使うと、HTMLテンプレート、JavaScriptのロジック、CSSのスタイリングを一つのファイルにまとめることができます。

まずはVue CLIがデフォルトで作成するHelloWorld.vueコンポーネントを削除し、Chat.vueConversation.vueコンポーネントを作成します。ターミナルで新しいウィンドウを開き、プロジェクトのルートディレクトリから以下のコマンドを実行してください。

cd src/components
rm HelloWorld.vue
touch Chat.vue Conversation.vue

Chat.vueを作成する

まずは、チャット画面全体を構成するコンポーネントとなるChat.vueを構築します。

シングルファイルコンポーネントは<template><script><style>の3つの部分に分かれています。まずはチャット画面のHTMLを<template>で定義します。

テキストエディタでChat.vueを開き、以下のコードをペーストしてください。

<template>
 <div id="chat">
   <h1>Welcome to the Vue chat app<span v-if="nameRegistered">, {{ name }}</span>!</h1>
   <p>{{ statusString }}</p>
   <div v-if="!nameRegistered">
     <input @keyup.enter="registerName" v-model="name" placeholder="Enter your name">
     <button @click="registerName">Register name</button>
   </div>
   <div v-if="nameRegistered && !activeConversation && isConnected">
     <button @click="createConversation">Join chat</button>
   </div>
   <Conversation v-if="activeConversation" :active-conversation="activeConversation" :name="name" />
 </div>
</template>

次に、HTMLに<style>を使ってCSSスタイルを追加します。<template>ブロックの下に以下のコードを追加してください。

<style scoped>
ul {
 list-style-type: none;
 padding: 0;
}

li {
 display: inline-block;
 margin: 0 10px;
}

a {
 color: #42b983;
}
</style>

<style scoped>scoped属性は現在のコンポーネントの要素にのみCSSを適用するために使用されます。

次に、JavaScriptのロジックを<script>を使って追加していきます。<template>ブロックと<style>ブロックの間に以下のコードを追加します。

<script>
import {Client as ConversationsClient} from "@twilio/conversations"
import Conversation from "./Conversation"

export default {
   components: { Conversation },
   data() {
       return {
           statusString: "",
           activeConversation: null,
           name: "",
           nameRegistered: false,
           isConnected: false
       }
   },
}
</script>

このコードでは、Twilio Conversationsの機能にアクセスするためのスタート地点となる@twilio/conversationsClientオブジェクトと、子コンポーネントとして使用するConversationコンポーネントをインポートしています。

また、dataメソッドでアプリケーション上で随時変化するデータをオブジェクトとして登録することにより、変更が起こるたびにVue.jsが検知して再レンダリングが行われるようにしています。

次に、これらのデータを操作するための関数をmethodsオブジェクトで定義します。methodsはユーザーのアクションなどDOM上で発生するイベントに対する処理を行う関数です。

まず、Clientオブジェクトを初期化するためのinitConversationsClientメソッドを作成します。initConversationsClientはユーザー名が入力されたタイミングで実行されるようにします。export defaultの配下、data()メソッドの下に、以下のコードをペーストしてください。

methods: {
   initConversationsClient: async function() {
       window.conversationsClient = ConversationsClient
       const token = await this.getToken(this.name)
       this.conversationsClient = await ConversationsClient.create(token)
       this.statusString = "Connecting to Twilio…"
       this.conversationsClient.on("connectionStateChanged", (state) => {
           switch (state) {
           case "connected":
               this.statusString = "You are connected."
               this.isConnected = true
               break
           case "disconnecting":
               this.statusString = "Disconnecting from Twilio…"
               break
           case "disconnected":
               this.statusString = "Disconnected."
               break
           case "denied":
               this.statusString = "Failed to connect."
               break
           }
       })
   },
}

一番最初にユーザー名を入力したユーザーが「メインユーザー」として扱われ、ConversationsClient.create(token)でクライアントを割り当てます。

Conversationsクライアントを作成するには、アクセストークンが必要です。アクセストークンを取得するgetToken()メソッドの処理が終わってからクライアントを作成する必要があるため、initConversationsClientメソッドはasync/awaitを使って非同期関数として定義しています。

また、conversationsClient.onイベントリスナーを使って、チャットへの接続ステータスが変更する度にUIに表示するメッセージを変更します。

次に、サーバー側で設定した/auth/user/:userエンドポイントにGETリクエストを送り、ユーザーごとのアクセストークンを取得するためのgetTokenメソッドを作成します。

 initConversationsClientメソッドの下に、以下のコードを追加します。

getToken: async function(identity) {
   const response = await fetch(`http://localhost:5000/auth/user/${identity}`)
   const responseJson = await response.json()
   return responseJson.token
},

getTokenメソッドも/auth/user/:userエンドポイントからのレスポンスが返ってくるまで待つ必要があるため、async/awaitの非同期処理を使用します。

次にユーザー名を登録するためのregisterNameメソッドを作成します。getTokenメソッドの下に以下のコードを追加します。

registerName: async function() {
   this.nameRegistered = true
   await this.initConversationsClient()
},

このメソッドは、initConversationsClientの処理が終わった後にユーザーがユーザー名を入力したことを表すnameRegisteredプロパティを更新します。

次に、新しい会話を作成するためのcreateConversationメソッドを作成します。registerNameメソッドの下に以下のコードを追加します。

 createConversation: async function() {
   // Ensure User1 and User2 have an open client session
   try {
       await this.conversationsClient.getUser("User1")
       await this.conversationsClient.getUser("User2")
   } catch {
       console.error("Waiting for User1 and User2 client sessions")
       return
   }
   // Try to create a new conversation and add User1 and User2
   // If it already exists, join instead
   try {
       const newConversation = await this.conversationsClient.createConversation({uniqueName: "chat"})
       const joinedConversation = await newConversation.join().catch(err => console.log(err))
       await joinedConversation.add("User1").catch(err => console.log("error: ", err))
       await joinedConversation.add("User2").catch(err => console.log("error: ", err))
       this.activeConversation = joinedConversation
   } catch {
       this.activeConversation = await (this.conversationsClient.getConversationByUniqueName("chat"))
   }
}

このメソッドでは、try...catchでメインユーザー以外のユーザーである「User1」、「User2」がクライアントを保持しているかをgetUserメソッドで確認します。もしまだ保持していない場合、「Waiting for User1 and User2 client sessions」エラーメッセージを出力します。
次にnewConversation.join()を使って非同期処理で新しい会話を作成し、「User1」と「User2」をjoinedConversation.addで会話に追加します。すでに会話が存在する場合は、activeConversationgetConversationByUniqueNameで取得した既存の会話に設定します。

この会話への参加、ユーザーの追加方法はデモンストレーションのために簡略化されており、本質的に安全ではありません。本番アプリケーションでは、ユーザーの身元をどのように確認するか、どのような権限を持たせるか、アプリケーションのセキュリティをどのように確保するかを慎重に検討してください。

これでChat.vueコンポーネントが完成しました!Chat.vueコンポーネントが正しく表示されるよう、App.vueルートコンポーネントを編集しましょう。

テキストエディターでApp.vueコンポーネントを開き、内容を以下のコードに変更します。

<template>
  <Chat />
</template>

<script>
import Chat from "./components/Chat.vue"
import "@twilio/conversations"
export default {
	name: "App",
	components: {
		Chat
	}
}
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
#app input {
  padding: 12px 20px;
  margin: 8px 0;
  display: inline-block;
  border: 1px solid #ccc;
  border-radius: 4px;
  box-sizing: border-box;
  margin-right: 5px;
  width: 300px;
}
#app button {
  background-color: #21cfbc;
  color: white;
  padding: 14px 20px;
  margin: 8px 0;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}
</style>

Chat.vueの表示を確認しましょう。ターミナルの二つ目のウィンドウで、以下のコマンドを実行してください。

npm run serve

一つ目のターミナルのウィンドウでサーバーが引き続き動いていることを確認してからhttp://localhost:8080/をブラウザで開いてください。以下のような画面が表示されます。

チャットアプリケーションのウェルカムページ

あなたの名前を入力して「Register name」をクリックしてみてください。すると、以下のように画面がアップデートされます。

チャットに接続

これでユーザーが入場するまでの画面の準備ができました。次にチャット部分を構築するConversation.vueにコードを追加していきましょう。

Conversation.vueを作成する

テキストエディターでConversation.vueを開きます。まずはChat.vueと同様にHTMLテンプレートを追加します。以下のコードをペーストしてください。

<template>
 <div id="conversation">
   <div class="conversation-container">
     <div
       v-for="message in messages" :key="message.index"
       class="bubble-container"
       :class="{ myMessage: message?.state?.author === name }"
     >
       <div class="bubble">
         <div class="name">{{ message?.state?.author }}:</div>
         <div class="message">{{ message?.state?.body }}</div>
       </div>
     </div>
   </div>
   <div class="input-container">
     <input @keyup.enter="sendMessage" v-model="messageText" placeholder="Enter your message">
     <button @click="sendMessage">Send message</button>
   </div>
 </div>
</template>

次に、CSSを追加します。<template>ブロックの下に以下のコードをペーストしてください。

<style scoped>
.conversation-container {
 margin: 0 auto;
 max-width: 400px;
 height: 600px;
 padding: 0 20px;
 border: 3px solid #f1f1f1;
 overflow: scroll;
}

.bubble-container {
 text-align: left;
}

.bubble {
 border: 2px solid #f1f1f1;
 background-color: #fdfbfa;
 border-radius: 5px;
 padding: 10px;
 margin: 10px 0;
 width: 230px;
 float: right;
}

.myMessage .bubble {
 background-color: #abf1ea;
 border: 2px solid #87E0D7;
 float: left;
}

.name {
 padding-right: 8px;
 font-size: 11px;
}

::-webkit-scrollbar {
 width: 10px;
}

::-webkit-scrollbar-track {
 background: #f1f1f1;
}

::-webkit-scrollbar-thumb {
 background: #888;
}

::-webkit-scrollbar-thumb:hover {
 background: #555;
}
</style>

次にJavaScriptロジックを追加します。<template>ブロックと<style>ブロックの間に以下のコードを追加します。

<script>
export default {
 props: ["activeConversation", "name"],
 data() {
   return {
     messages: [],
     messageText: "",
     isSignedInUser: false
   }
 },
}
</script>

ここでは、propsをインポートしています。Propsは親コンポーネントから子コンポーネントにデータを渡すために使われます。このチャットアプリケーションでは、Chat.vueが親コンポーネント、Conversations.vueが子コンポーネントです。Chat.vueを確認すると、<template>で以下のように現在ユーザーが入場しているチャットactiveConversationとユーザー名のnameを受け渡しています。

   <Conversation :active-conversation="activeConversation" :name="name" />

次に、過去に受信したユーザーからのメッセージを表示し、新しく送信されるメッセージに対するイベントリスナーを設定する処理を追加します。export defaultの配下、data()メソッドの下に、以下のコードをペーストしてください。

 mounted() {
   this.activeConversation.getMessages()
     .then((newMessages) => {
       this.messages = [...this.messages, ...newMessages.items]
     })
   this.activeConversation.on("messageAdded", (message) => {
     this.messages = [...this.messages, message]
   })
 },

ここではmounted()を使用しています。mounted()は、Vue.jsのライフサイクルフックの一つで、コンポーネントがDOMに追加されたタイミングで呼び出されます。ユーザーがチャットに入場し、チャット画面がDOMに追加されたタイミングでこれまでに送信されたメッセージを取得し、画面に表示させます。また、activeConversation.onイベントリスナーを設定し、これから送信されるメッセージを受け取る準備をします。新しく送信されたメッセージは分割代入 (Destructuring assignment)を使ってmessages配列に追加します。

次に、ユーザーがメッセージを送信した際の処理を定義します。mounted()の下に次のコードをペーストしてください。

 methods: {
   sendMessage: function() {
     this.activeConversation.sendMessage(this.messageText)
       .then(() => {
         this.messageText = ""
       })
   }
 }

methodssendMessageメソッドを定義します。メッセージの送信処理を行い、処理完了後にメッセージ入力のためのテキストエリアを空にするためにmessageTextを空文字列に設定します。

お疲れ様でした!以上でConversation.vueの設定が完了しました!それでは完成したアプリケーションを検証してみましょう。

チャットアプリケーションを動作検証する

ターミナルの一つ目のウィンドウでサーバーが引き続き動いていて、二つ目のウィンドウでVue CLIが起動していることを確認してください。


すでに開いているブラウザのウィンドウに加えて、ウィンドウをさらに2つ開き、http://localhost:8080/にアクセスします。

新しく開いた一つ目のウィンドウからユーザー名「User1」と、二つ目のウィンドウからユーザー名「User2」を入力し、それぞれ「Register name」をクリックしユーザーを作成てください。

このチュートリアルでは、デモンストレーションを簡略化する目的でダミーユーザーの「User1」と「User2」をプログラム上で自動追加しています。「User1」と「User2」以外のユーザー名をこのステップで入力するとエラーになります。

User1のユーザー名入力画面
User2のユーザー名入力画面

次に、メインユーザー、「User1」と「User2」それぞれの画面で「Join chat」をクリックしてください。

すると、チャット画面が表示されます。

ユーザー名入力後のチャット画面

もし「Join chat」をクリックしても何も起こらない場合は、メインユーザーのウィンドウでブラウザの開発者コンソールを開いてみてください。「Join chat」をクリックすると「Waiting for User1 and User2 client sessions」メッセージが表示される場合、「User1」と「User2」にクライアントが存在しないことを意味します。ウィンドウを2つ開き、「User1」と「User2」を登録してから再度「Join chat」を試してください。


それではメッセージを送ってみましょう。メインユーザーの画面からメッセージをフォームに入力し、「Send message」ボタンをクリックしてください。

メインユーザーからのメッセージ

「User1」の画面を開くと、メインユーザーからのメッセージが確認できます。

メインユーザーからのメッセージをUser1の画面で確認


「User1」からもメッセージを送ってみましょう。メッセージをフォームに入力し、「Send message」ボタンをクリックしてください。

「User2」の画面を開くと、メインユーザーと「User1」からのメッセージが確認できます。

メインユーザーとUser1からのメッセージをUser2の画面で確認


次のステップ


お疲れ様です。Twilio ConversationsとVue.jsを使ったチャットアプリケーションが完成しました。さらに開発を進めたい方は、このアプリケーションにTwilio Syncを追加してオンラインステータスを共有できるようにしてみたり、Typing Indicatorを使ってユーザーがタイピングしている時に「User1がタイピングしています」などのステータスを表示させてみてはいかがでしょうか?

これまでに使用したコードはすべてGithubリポジトリでご確認いただけます。また、Vue.jsについてもっと知りたい方は、Vue.js公式ドキュメントを参照してください。

Twilio Blogに投稿してみたい方や、フィードバック、登壇、勉強会のお誘いなど気軽にsnakajima[at]twilio.comまでご連絡ください。開発中のプロジェクトに関してはGithub(smwilk)を覗いてみて下さい。