🌟

Supabaseでリアルタイムチャットアプリを作る

2024/07/22に公開

SupabaseにはRealtime APIがあります。接続しているクライアントとのメッセージを送受信できるAPIになります。
このAPIを使用して簡単なリアルタイムチャットアプリを作ってみました。

コード全体はこちらを参考にしてください。
https://github.com/k-logic563/supabase-chat-app

使用モジュールのバージョンは以下の通りです。
主要なモジュールのみ載せていますので、その他はリポジトリのpackage.jsonを参考にしてください。

"vite": "^5.3.4"
"vue": "^3.4.31"
"typescript": "^5.2.2"
"tailwindcss": "^3.4.6"
"@supabase/supabase-js": "^2.44.4"

アプリケーション概要

技術

技術選定として、Viteを使用しています。
使用言語はJavaScript、テンプレートはVue+TypeScriptを使用しています。

仕様

大きく分けて2つあります。

  • 匿名ログインで使用可能になる
  • 他ユーザーが書き込んだタイミングでリアルタイム更新されて表示される

完成イメージ

実装解説

要点を絞って解説を進めます。

データベース設定

テーブルmessagesを作成します。user_idauth.users.idを外部キーに持つよう設定します。

create table
  public.messages (
    id bigint generated by default as identity,
    message text not null,
    created_at timestamp with time zone not null default now(),
    user_id uuid not null default gen_random_uuid (),
    constraint messages_pkey primary key (id),
    constraint messages_user_id_fkey foreign key (user_id) references auth.users (id) on update cascade on delete cascade
  ) tablespace pg_default;

またRLSを有効の上、ポリシー設定してください。
詳細を省きますが、selectinsertに対してauthenticatedユーザーのみ適応するような設定をすれば良いです。

匿名ログイン画面

目的として、どのユーザーがチャットしたのか判別するために使用しています。今回は何か登録する作業を省きたかったので匿名ログインとしました。
ここは他プロバイダー(Googleログインなど)でもかまいません。

ここではsignInAnonymously関数を使用し、匿名ログインをしています。

views/login.vue
<script setup lang="ts">
const handleSigninWithAnonymous = async () => {
  try {
    await supabaseClient.auth.signInAnonymously(); // ここ
  } catch (e) {
    console.log(e);
  }
}
</script>

ログイン周りのハンドリング

ここではonAuthStateChange関数を使用し、ユーザーの認証状態を監視しています。
ログアウトなどでセッションが切れた場合はログイン画面へリダイレクトします。また、セッション中ログイン画面にアクセスした場合、ホーム画面へリダイレクトします。

guards/AuthGuard.vue
<script setup lang="ts">
const { data: authListener } = supabaseClient.auth.onAuthStateChange(async (_: unknown, session) => {
  // セッション中、かつログインページ
  // トップへリダイレクト
  const isLoginPage = route.path === '/login';
  if (session && isLoginPage) {
    await router.push('/');
    return;
  }
  // サインアウト
  if (!session) {
    await router.push('/login');
    return;
  }
});

onBeforeUnmount(() => {
  authListener.subscription.unsubscribe();
});
</script>

Realtime API実装

channel関数でチャネルを作成、messagesテーブルにinsertされたタイミングでfetchMessageDataを発火するように設定しています。
その記述により、双方向通信が可能になっており別のユーザーがメッセージを登録しても即時に更新がなされます。

views/Home.vue
<script setup lang="ts">
...

const fetchMessageData = async () => {
  try {
    const { data } = await supabaseClient
      .from('messages')
      .select();
    if (data) {
      messageData.value = data;
    }
  } catch (e) {
    console.error(e);
  }
}

fetchMessageData();

const channel = supabaseClient
  .channel('chat')
  .on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'messages' }, fetchMessageData)
  .subscribe()

onBeforeUnmount(() => {
  channel.unsubscribe();
});
</script>

チャット一覧画面

ログインしているユーザーの発言では、背景が緑色で右寄りになります。
onMountedフックで現在ログインしているユーザーidを取得します。その後、messagesテーブルから取得したデータにあるuser_idで同ユーザーか判別しています。

views/Home.vue
<script setup lang="ts">
const userId = ref<string>('');

onMounted(async () => {
  // 現在ログイン中のユーザー
  const { data } = await supabaseClient.auth.getUser();
  if (data.user) {
    userId.value = data.user?.id;
  }
});
</script>

<template>
  <div class="max-w-[768px] mx-auto py-[32px]">
    <div class="h-[calc(100vh-64px)] grid grid-rows-[auto_1fr_auto]">
      ...
      <main class="p-4 bg-gray-200">
        <ul v-if="messageData.length" class="flex flex-col gap-y-4 items-start">
          <li
            v-for="data in messageData"
            :key="data.id"
            :class="['rounded-full', 'px-4', 'py-2', data.user_id === userId ? 'bg-green-400 ml-auto' : 'bg-gray-100']"
          >
          {{ data.message }}
        </li>
        </ul>
      </main>
      ...
    </div>
  </div>
</template>

コード全体を見たい人へ

再度、コード全体を見たい方向けにリポジトリリンクを貼ります。
https://github.com/k-logic563/supabase-chat-app

さいごに

チャットアプリ作成を通してRealtime APIがどのような挙動になるのか実感できました。

参考

公式 Realtime API
https://supabase.com/docs/guides/realtime

Discussion