Svelte + WebSocketでシンプルなチャットアプリを作る
Svelte と WebSocket の勉強のため簡単なチャットアプリを作ったので備忘録としてまとめます。
基本
Svelte とは?
Svelte は React や Vue.js のように宣言的 UI で Web アプリケーションを作れるツールです。Vue.js の SFC のように、.svelte
ファイルで HTML、JavaScript、CSS を単一コンポーネントとして管理し、それを組み合わせてアプリケーションを構築していきます。
React や Vue.js との大きな違いは、事前にアプリケーションコードを純粋な JavaScript ファイルとしてコンパイルすることで、クライアントではネイティブの DOM スクリプトとして実行される点です。仮想 DOM を使用しないため、ビルド後のコードには専用のランタイムライブラリが含まれず軽量です。
WebSocket とは?
WebSocket は単一の TCP コネクション上で双方向通信を実現するための通信プロトコルです。WebSocket を使うことで、一度通信を確立すれば通常の HTTP 通信では実現できないサーバー起点のデータ送信が可能になります。そのため従来は pooling などで実現していたリロードなしのリアルタイムのデータ更新が容易に実装できます。
チャットアプリの作成
以下のように別ブラウザで開いていても、リアルタイムで同期的に動くチャットアプリを作りました。
ソースコードはこちらにあります。
以下ポイントだけメモします。
サーバー側の実装
サーバー側の実装はとても簡単です。
Node.js で WebSocket を扱うためのライブラリであるwsを使って実装しています。
import WebSocket from "ws"
const wss = new WebSocket.Server({ port: 8082 });
type Chat = {
userId: string,
message: string
}
// インメモリでデータを保持
const chatBoard: Chat[] = [];
wss.on('connection', (ws) => {
ws.on('message', (payload) => {
if(typeof payload === 'string') {
chatBoard.push(JSON.parse(payload));
}
// 全ての接続先に送信
wss.clients.forEach((client) => {
client.send(JSON.stringify(chatBoard));
})
});
ws.send(JSON.stringify(chatBoard));
});
new WebSocket.Server
でサーバーのインスタンスを作り、wss.on('connect', callback)
で WebSocket 接続時の処理を。接続時にコールバックで受け取るws
を使って、ws.on('message', callback)
でリクエスト受信時の処理を書いています。
リクエスト受信時には、インメモリで保持しているチャットのメッセージ配列の更新を行うとともに、wss.clients
で全ての接続中のクライアントを取得し、それぞれのクライアントに対してsend
で新しいチャットメッセージの配列を送っています。ws.send
を使った場合、リクエストを送信したクライアントだけにメッセージを送ることになり、別ブラウザでの同期的な表示はできないので注意です。
Client側の実装
Client 側では Svelte の TypeScript テンプレートを元にチャットアプリを作っています。
TypeScript でのプロジェクト作成はこちらを参考にしました。
コンポーネントはボード全体を表示するApp.svelte
と、ひとつひとつのメッセージを表示するMessage.svelte
と、メッセージの入力フォームを表示するMessageForm.svelte
の 3 つです。
まずMessage.svelteです。
Svelte での Props は、script タグの中でexport
している変数となります。このコンポーネントでは、currentUserId
・userId
・message
の 3 つの Props を受け取り、その内容をもとにメッセージを表示しています。
その他、class:mine="{currentUserId === userId}"
の部分で、currentUserId と userId が一致する場合のみmine
という class を付与しています。
アバターの画像は、Avatarsという文字列からアバター画像を生成してくれる API を利用しています。
<script lang="ts">
export let currentUserId: string;
export let userId: string;
export let message: string;
</script>
<div class="message-container" class:mine="{currentUserId === userId}">
<img width="40px" src="{`https://avatars.dicebear.com/api/bottts/` + userId + '.svg'}" alt="Avatar" />
<div class="balloon">{message}</div>
</div>
<style>
/* ... */
</style>
次にMessageForm.svelteです。
このコンポーネントでは親コンポーネントにクリックイベントを送るために、createEventDispatcher
を使って送信ボタンのクリックで submit イベントをディスパッチしています。createEventDispatcher
は Vue でいう emit と同様ですね。また、親コンポーネントから受け取ったmessage
Props の内容をbind:value
で input 要素に割当ています。親コンポーネント側で MessageForm コンポーネントの定義時にbind:message
とすることで、透過的に message の変更を親コンポーネントに反映できます。
※ フォーム要素のコンポーネント間のデータのやり取りはこちらを参考にしました。
<script lang="ts">
import { createEventDispatcher } from 'svelte';
export let message;
const dispatch = createEventDispatcher();
const handleSubmit = () => {
dispatch('submit');
};
</script>
<input bind:value="{message}" class="input" type="text" placeholder="Your message" />
<button class="button is-link" on:click="{handleSubmit}">submit</button>
最後にApp.svelteです。
ライフサイクルフックのonMount
で WebSocket の接続とメッセージ受信時のイベントを定義しています。メッセージ受信時には、内部で保持するリアクティブな変数であるmessages
にデータを代入して、チャットデータの更新を行っています。
他、socket.onopen
・socket.onmessage
で WebSocket 接続時、メッセージ受信時の処理を登録しています。メッセージ受信時に受信データをJSON.parse
でパースしてmessages
に代入しています。
また、handleSubmit
という関数で送信ボタン押下時に、WebSocket に対して追加のメッセージを送っています。先にmessages
の値を更新しているのは、自分起因のデータ更新は WebSocket の通信を待たず反映させるためです。
<script lang="ts">
import { onMount, tick } from 'svelte';
import Message from './components/Message.svelte';
import MessageForm from './components/MessageForm.svelte';
import { getUserId } from './lib/getUserId';
let message = '';
let messages: { userId: string; message: string }[] = [];
let socket: null | WebSocket = null;
let board: null | Element;
const currentUserId = getUserId();
const scrollToBottom = async () => {
await tick();
board.scrollTop = board.scrollHeight;
};
const handleSubmit = () => {
if (!message) {
return;
}
messages = [...messages, { userId: currentUserId, message }];
socket.send(JSON.stringify({ userId: currentUserId, message }));
message = '';
scrollToBottom();
};
onMount(() => {
socket = new WebSocket('ws://localhost:8082');
socket.onopen = () => {
console.log('socket connected');
};
socket.onmessage = (event) => {
if(!event.data) { return }
messages = JSON.parse(event.data);
scrollToBottom();
};
});
</script>
<main>
<div class="wrapper">
<h1>Svelte simple chat</h1>
<div class="board" bind:this="{board}">
{#each messages as { userId, message: mg }}
<Message currentUserId="{currentUserId}" userId="{userId}" message="{mg}" />
{/each}
</div>
<div class="message-form-wrapper">
<MessageForm bind:message on:submit="{handleSubmit}" />
</div>
</div>
</main>
<style>
/* ... */
</style>
参考
- https://zenn.dev/toshitoma/articles/what-is-svelte
- https://svelte.dev/
- https://www.codegrid.net/articles/2020-svelte-1/
- https://ja.wikipedia.org/wiki/WebSocket
- http://wild-data-chase.com/index.php/2019/03/17/post-630/#outline__4
- https://www.html5rocks.com/ja/tutorials/websockets/basics/
終わりに
以上「Svelte + WebSocketでシンプルなチャットアプリを作る」でした。
Svelte も WebSocket も触りの触りですが、思いのほか使いやすかったです。また折りを見て書いていきたいです。
Discussion