Express、ReactとTwilio Syncでオンラインステータスをアプリ内で共有できる機能を実装する

August 04, 2021
レビュー担当者

Let users share their online status in your app with Express, React, and Twilio Sync

この記事はTwilio Developer VoicesチームのAshley Boucherが執筆したこちらの記事(英語)を日本語化したものです。

Twilio Syncは、Twilio ConversationsなどのTwilio APIの基盤となる技術です。Twilio Syncを利用すると、クラウドに保存されたステータスをアプリに追加でき、リアルタイムのコラボレーションの新しい機会を生み出します。

本稿では、Twilio Syncを使ってアプリでユーザーのオンラインステータスを表示する方法をご紹介します。

必要なツール

  • インストール済のNode.js
  • Twilioアカウント

アプリと環境を設定する

今回のアプリはフロントエンドとバックエンドの2つの部分で構成されています。フロントエンドはユーザーが実際に操作するアプリの部分です。フロントエンドはTwilio Sync JavaScript SDKを使用します。このSDKはSync APIと対話するための許可を得るためにアクセストークンを必要とします。バックエンド側でこのアクセストークンを生成します。

まずはファイル構造やアプリの環境を整えましょう。

ターミナルまたはコマンドプロンプトを開き、以下のコマンドを実行してください。フロントエンドとバックエンドの両方の親ディレクトリとなる新しいディレクトリを作成します。

mkdir react-sync

作成したディレクトリに移動します。

cd react-sync

 次にバックエンドの設定をします。バックエンドはExpressで構築します。

mkdir token-service
cd token-service
npm init -y
npm install express twilio dotenv cors

上記のコマンドは、新しいNode.jsアプリを作成し、以下の4つのdependenciesをインストールします。

  • サーバー構築に使用するexpress
  • Twilio Node Helper Libraryを利用し、アクセストークンを生成するためのtwilio
  • 環境変数を読み込むために使うdotenv
  • React.jsのフロントエンドからExpressアプリへのリクエストを送信するためのcors

token-serviceディレクトリで、.envindex.jsという2つの新しいファイルを以下のコマンドで作成します。テキストエディターで直接作成しても構いません。

touch .env index.js

.envファイルは環境変数を追加する場所で、index.jsファイルはバックエンドのコードを書く場所です。

Twilioの認証情報を取得する

お使いのテキストエディターで.envファイルを開きます。

以下の変数とその値をコピーして、ファイルに貼り付けます。

TWILIO_ACCOUNT_SID=XXXXX
TWILIO_API_KEY=XXXXX
TWILIO_API_SECRET=XXXXX
SYNC_SERVICE_SID=XXXXX

XXXXXの文字列は、Twilioの認証情報のプレースホルダーを表しています。これらの認証情報を取得し、.envファイルに追加する方法をご紹介します。

Account SID

Twilio Consoleに移動し、Account SIDを確認します。Account SIDの値をコピーして、.envファイルのTWILIO_ACCOUNT_SID変数に貼り付けてください。

APIキーとAPIシークレット

次に、Twilio ConsoleのAPI Keyセクションにアクセスします。赤いプラス記号をクリックして、新しいAPIキーを作成します。FRIENDLY NAMEのテキストフィールドにキーの名称を入力し、KEY TYPEStandardに設定します。Create API Keyをクリックします。

画面にSIDSECRETを含むデータが表示されます。SIDはあなたのAPIキーです。

SID.envファイルのTWILIO_API_KEYの値として、SECRETTWILIO_API_SECRETの値として貼り付けてください。

この画面を離れると、あなたのSECRETには二度とアクセスできません。値のバックアップを取っておくことを推奨します。

Service SIDを同期する

Twilio ConsoleのSyncセクションに移動します。Create new Sync Serviceをクリックし、新しいSyncサービスを作成します。ポップアップが表示されたら、「react-sync」のような判別しやすい名前を入力してください。Createをクリックし、次の画面で画面の右上のService SIDをコピーします。

Screenshot of sync service configuration page with red rectangle blurring out a service SID

Service SIDの値を.envファイルのSYNC_SERVICE_SID変数に貼り付けてください。

このファイルを保存して閉じると、アプリの設定が完了します。

トークンサービスの構築

お使いのテキストエディターでindex.jsファイルを開き、以下のコードを追加してください。

const express = require('express');
const cors = require('cors');
require('dotenv').config();

const app = express();
app.use(express.urlencoded({ extended: false }));
app.use(cors());

const port = 3000;

app.get('/token', cors({origin:'http://localhost:3001'}), (req, res) => {
  // generate access token here
});

app.listen(port, () => {
  console.log(`Token Service listening at http://localhost:${port}`)
});

 このコードでExpressアプリを作成し、オリジンのhttp://localhost:3001から/tokenエンドポイントへのGETリクエストの送信ルートを確立させます。フロントエンド側がTwilio Sync APIに接続する必要があるときは、このエンドポイントにリクエストを行います。現時点では/tokenルートにはまだコードがなく、generate access token hereというコメントがあるだけです。次のステップでコードを追加していきます。
/tokenルートの、コメントの下に次のコードを追加します。

const AccessToken = require('twilio').jwt.AccessToken;
const SyncGrant = AccessToken.SyncGrant;

const token = new AccessToken(
  process.env.TWILIO_ACCOUNT_SID,
  process.env.TWILIO_API_KEY,
  process.env.TWILIO_API_SECRET
);

const syncGrant = new SyncGrant({
  serviceSid: process.env.SYNC_SERVICE_SID,
});

token.addGrant(syncGrant);
token.identity = req.query.identity;

res.send({
  accessToken: token.toJwt()
});

本稿でご紹介するアクセスの生成方法は、デモンストレーションを目的としており、本質的に安全な方法ではありません。本番アプリではクライアントにアクセストークンを付与する前に、ユーザーを検証し、認証してください。Syncを使用するアプリをリリースする前に、How to secure your Twilio Sync appsを参照してください。

ファイルを保存すれば、バックエンドの準備は完了です。

次に進む前に、ターミナルのtoken-serviceディレクトリから以下のコマンドを実行して、Expressサーバーを起動してください。

node index.js

サーバーが起動すると、次のようなメッセージが表示されます。

Token Service listening at http://localhost:3000

このプロセスは動作させたまま次のステップに進んでください。

Reactのフロントエンドを構築するための基盤を作る

新しくターミナルまたはコマンドプロンプトウィンドウを開き、react-syncディレクトリに移動して、以下のコマンドでフロントエンドの枠組みを作ります。

npx create-react-app client
npm install twilio-sync

上記のコマンドを実行すると、react-sync/clientディレクトリが作成されます。この新しいフォルダの中には、いくつかのファイルとサブフォルダがありますが、そのうちのひとつがsrcです。

このプロジェクトでは、Appというコンポーネントと、OnlineUsersという子コンポーネントの2つを作ります。Appコンポーネントが主要コンポーネントとなります。このコンポーネントは、ローカルユーザーのサインインを制御します。OnlineUsersコンポーネントは、すべてのオンラインユーザーのリストをレンダリングし、リモートユーザーのサインインやログアウトなどのイベントに応答します。

Appコンポーネント

srcフォルダを開いて、App.jsファイルを探します。

App.jsのコードをすべて削除し、以下に置き換えます。

import {useState} from 'react';
import OnlineUsers from './OnlineUsers.js';
var SyncClient = require('twilio-sync');

このコードでは、ReactからuseState()フックをインポートし、先ほど述べたOnlineUsersという子コンポーネントをインポートし、さらにTwilio Syncクライアントオブジェクトをインポートしています。まだOnlineUsersは作成していないので、Reactサーバーがすでに稼働している場合はエラーが表示されます。

コンポーネント構造の作成

次のステップでは、機能的なコンポーネントを作成します。先ほど追加したコードの下に、以下のコードを貼り付けます。

function App() {
  const [identity, setIdentity] = useState('');
  const [localUser, setLocalUser] = useState(null);
  const [onlineUsersSyncList, setOnlineUsersSyncList] = useState(null);
}

export default App;

このコードは、Appコンポーネントの枠組みを作成し、その中で前の手順でインポートしたuseState()フックを使用して以下の3つの状態変数を設定しています。

  • identityはローカルユーザーがフォームフィールドに入力した際、ユーザーの氏名を取得するために使用します。
  • localUserListItemリソースを表します。ListItemはSyncの共有状態にあるローカルユーザーを含むTwilio Syncオブジェクトです。
  • onlineUsersSyncListListリソースを表します。ListListItemリソースを順序付けしたリストで、Syncの共有状態にあるすべてのオンラインユーザーを表します。

コンポーネントに条件付きレンダリングを追加する

App関数内の状態変数の下に以下のreturnメソッドを追加します。

return (
  <div className="app">
  {
    localUser && onlineUsersSyncList
    ? <OnlineUsers localUser={localUser} onlineUsersSyncList={onlineUsersSyncList} />
    : <div>
        <input 
          type="text" 
          value={identity} 
          onChange={(event) => setIdentity(event.target.value)}
          placeholder="Enter your name"></input>
        <button onClick={getAccessToken}>Connect to App and show you're online!</button>
      </div>
  }
  </div>
);

このコードは、Appコンポーネントがロードされたときに、レンダリングされる項目を決定する役割を果たします。まず、localUseronlineUsersSyncListの状態の値が「Truthy」であるかどうか、つまりnullfalseemptyundefinedではない何らかの値を持っているかどうかを確認します。

コンポーネントが初期ロードされ、ユーザーがサインインする前の状態では前述した変数ははすべて「Falsy」な値になります。この場合、コンポーネントはユーザーの名前を入力するテキスト入力フィールドと、ユーザーがサインインするためにクリックするボタンをレンダリングします。

ユーザーがボタンをクリックすると、そのクライアントにアクセストークンが付与されます。クライアントがアクセストークンを取得すると、Syncリソースが初期化され、状態値が設定されます。これらの処理はすべて、ボタンがクリックされたときに呼び出されるgetAccessToken()という関数の中で行われます。次の工程でこの関数を追加します。

状態値が更新されると、コンポーネントが再レンダリングされ、状態の値が「Truthy」になります。これにより、入力フィールドとボタンの代わりにOnlineUsersコンポーネントがレンダリングされます。

このフローは、ユーザーを認証または確認するメカニズムを含んでいないため、安全なサインインを確証していません。匿名のユーザーでもアクセストークンを取得できてしまいます。本稿ではTwilio Syncの機能のご紹介を目的としているため、これらの設定に関する詳細は省略しています。本番環境ではこのままデプロイせず、安全なサインイン方法を導入してください。

コンポーネントにgetAccessToken()関数を追加する

return()メソッドが追加されたので、前述したgetAccessToken()メソッドを追加して Appコンポーネントを完成させます。

以下のコードをAppコンポーネント内の、状態変数宣言の下、return()の前に追加します。

const getAccessToken = async () => {
  const res = await fetch(`http://localhost:3000/token?identity=${identity}`);
  const data = await res.json();
  const syncClient = new SyncClient(data.accessToken);

  const SyncList = await syncClient.list('online-users');
  const localUser = await SyncList.push({name: identity})

  await setLocalUser(localUser);
  await setOnlineUsersSyncList(SyncList);
}

このコードは、バックグラウンドで動作しているExpressサーバの /token ルートにGETリクエストを行います。

レスポンスでアクセストークンを取得すると、SyncClientのインスタンスが作成され、SyncListlocalUserの2つのSyncオブジェクトの作成に使用されます。これらのオブジェクトにそれぞれコンポーネントの状態変数の値を割り当てます。

Appコンポーネントの説明は以上です。次にOnlineUsersコンポーネントについて説明します。

OnlineUsersコンポーネント

/client/srcフォルダにOnlineUsers.jsという新しいファイルを作成します。

OnlineUsers.jsファイルに以下のコードを追加します。

import {useState, useEffect} from 'react';

このコードでReactからuseState()useEffect()のフックをインポートします。

インポートの下に以下のコードを追加して、OnlineUsersコンポーネントの枠組みを作成します。

コンポーネントを構築する

 

function OnlineUsers({localUser, onlineUsersSyncList}) {

}

export default OnlineUsers;

このコードでは、空のコンポーネントを作成してエクスポートし、親のAppコンポーネントからのPropsオブジェクトを分割代入(Destructuring)して、2つの変数localUseronlineUsersSyncListに変換しています。

次に、以下のコードでOnlineUsers関数の中で、初期状態を設定します。

function OnlineUsers({localUser, onlineUsersSyncList}) {
  const [onlineUsers, setOnlineUsers] = useState([]);
}

export default OnlineUsers;

コンポーネントがマウントれたタイミングで処理を実行する

useEffect()フックを使って、コンポーネントのマウント後に以下の処理を実行します。

  • 現在の全員のオンラインユーザーリストをSyncから取得し、それを状態変数onlineUsersに割り当てます。
  • イベントリスナーをonlineUsersSyncListオブジェクトに設定して、ユーザーがオンラインになるタイミングを監視します。
  • イベントリスナーをonlineUsersSyncListオブジェクトに設定して、ユーザーがオフラインになるタイミングを監視します。

以下のコードをコピーして、関数内の状態宣言の下に貼り付けます。

useEffect(() => {
  getSetOnlineUsers();

  onlineUsersSyncList.on('itemAdded', event => {
    if (!event.isLocal) {
      getSetOnlineUsers();
    }
  });

  onlineUsersSyncList.on('itemRemoved', getSetOnlineUsers);

  window.addEventListener("beforeunload", removeParticipant);
}, []); 

ヘルパー関数を追加する

このコードは、まだ定義していない2つの関数、getSetOnlineUsers()removeParticipant()を呼び出しています。これからこれらの関数を追加していきます。useEffect()のフックの下に、以下のコードを貼り付けます。

// Gets current list of online users from Sync
const getSetOnlineUsers = async() => {
  const items = await onlineUsersSyncList.getItems();
  setOnlineUsers(items.items);
}

// Removes local user from Sync list
const removeParticipant = async () => {
  const list = await onlineUsersSyncList.remove(localUser.index)
  list.close();
}

コンポーネントのレンダリング

OnlineUsersコンポーネントのステートに格納されたユーザーのリストをレンダリングするために、return()メソッドを追加します。以下のコードをOnlineUsers関数の閉じかっこ(})の前に貼り付けます。

return (
  <div className="participants">
    <h2>Online users:</h2>
    {
      onlineUsers.map(p => <div key={p.index}>{p.data.name}</div>)
    }
  </div>
); 

このファイルを保存して閉じれば、コンポーネントの作成は完了です。

Syncアプリを試す

ターミナルまたはコマンドプロンプトに戻り、1つ目のウィンドウでバックエンドのExpressサーバーがポート3000で動作していることを確認します。

2つ目のターミナルウィンドウで、clientディレクトリに移動し、以下のコマンドを実行します。

npm start

これにより、ローカルのReactサーバーが起動します。Reactサーバーは通常、ポート3000で起動しますが、バックエンドがすでにこのポートで動作しているため、3001番ポートで動作しても問題ないか確認を求められます。確認のためにyを押します。

サーバーが起動し、ブラウザ上のhttp://localhost:3001に自分のアプリが表示されるようになります。

ブラウザでタブまたはウィンドウを2つ用意し、アプリを開きます。

1つのタブで、テキストフィールドに名前を入力してボタンをクリックすると、名前がオンラインユーザーリストに表示されます。

Screenshot showing online users list

2つ目のタブで、別の名前を入力し、ボタンをクリックします。今度は、両方のユーザーがリストに表示されます。

Screenshot showing both users in list

どちらかのタブを閉じると、開いているタブにオンラインのユーザーが1人になっていることが表示されます。

お疲れ様でした。Express、ReactでのTwilio Syncアプリの開発が完了しました。本稿では、Twilio Syncの使い方の一例を紹介しました。この経験を生かして、マルチプレイヤーのオンラインゲームや、SyncとVanilla JavaScriptを使ったコラボレーションノートパッドを作ってみてはいかがでしょうか。 ぜひ、あなたの作品をTwitterで共有してください。

Ashleyは、TwilioブログのJavaScriptエディターです。Ashleyと協力し、Twilioにテクニカルストーリーを紹介するには、Twitterで@ahl38までご連絡ください。TwitterでAshleyが見つからない場合は、どこかのパティオでコーヒーを飲んでいることでしょう(ワインの時間かも)。