🍝

Supabase + Vue.js でチャットアプリを作ってみる

2021/12/10に公開

この記事は LAPRAS アドベントカレンダー 2021 の 10 日目の記事です。
https://qiita.com/advent-calendar/2021/lapras
おもしろい記事がたくさん上がってるので、要チェキです!


話題の BaaS Supabase を使って、チャットアプリを作ってみます。
React 版は既に公式が出しているので、今回は Vue 版を作ります。

作るもの

こんな感じのものを作ります。
名前とメッセージを入れて送信ボタンを押すと、下に発言が表示されます。
更新はリロードしなくても自動的に行われます。

Image from Gyazo

作り方

Supabase の設定

最初に、 Supabase にアクセスできるデータベースを用意していきましょう。
まずは https://supabase.com/Start your project から登録して、新しいプロジェクトを作成します。

基本は初期値のままで大丈夫ですが、 Region は us-east-1 から Northeast Asia (Tokyo) に変えておくことをおすすめします。

次に、 API のキーと URL をメモしておきます。左の歯車アイコンの Settings から、 API をクリックし、 Project API keys の public キーと、 config の URL をメモしておきます。

次にチャットのメッセージを保存しておくテーブルを作成します。
作るテーブルは messages テーブル一つだけです。
左のデータベースのアイコンから、 Tables と進み、新しく作成します。
スキーマはこんな感じにしました。

カラム名 説明
id int8 メッセージのID(最初からあった項目)
createdAt timestamptz 作成日(最初からあった項目)
name text 発言した人の名前
message text メッセージのテキスト

Vueの開発

まずは vue create supabase-chat をして、 Vue のプロジェクトを作成します。
vue create コマンドが使えない場合は、こちらを読んでインストールしてください。

生成された src/App.vue を編集していきます。

まずはテンプレートを書きます。

<template>
  <div id="app">
    <!-- form を submit すると sendMessage が呼ばれます -->
    <form @submit.prevent="sendMessage">
      <!-- 名前とメッセージの各 input タグの入力値は、 それぞれ inputName, inputMessage と同期されます -->
      <input type="text" v-model="inputName" placeholder="名前" /><br />
      <input type="text" v-model="inputMessage" placeholder="メッセージ" /><br />
      <button type="submit">送信</button>
    </form>
    <ul>
      <!-- messages に入っているメッセージをループして出力します -->
      <li v-for="message in messages" :key="message.id">
        <!-- message はオブジェクトで、 name, message, id, ここにはないですが createdAt 属性があることを期待しています -->
        <b>{{ message.name }}</b> {{ message.message }}
      </li>
    </ul>
  </div>
</template>

次に、スクリプトの方を書き込んでいきます。 methods の中は、一旦必要な機能だけわかるように TODO で機能を書いていきます。

<script>
export default {
  name: "App",
  data() {
    return {
      messages: [],
      inputName: "",
      inputMessage: "",
    };
  },
  methods: {
    sendMessage() {
      // TODO: メッセージを送信
    },
    getMessages() {
      // TODO: メッセージを取得
    },
    subscribeMessage() {
      // TODO: メッセージを購読
    },
    unsubscribeMessage() {
      // TODO: メッセージを購読をやめる
    },
  },
  mounted() {
    this.getMessages()
    this.subscribeMessage()
  },
  beforeDestroy() {
    this.unsubscribeMessage()
  },
}
</script>

supabase を導入する

npm install -S @supabase/supabase-js で、 Supabase が使えるようにします。
App.vue 内で createClient をインポートして、先ほど Supabase の画面からメモした Public Key と URL を入れると、 Supabase にアクセスできるようになります。

<script>
import { createClient } from "@supabase/supabase-js";

const supabase = createClient(
  "https://xxxxx.supabase.co",
  "xxxxx"
);

送信機能を実装する ( Supabase のデータベースに行を挿入)

sendMessage() の中を実装します。
supabase.from("[テーブル名]").insert() で、テーブルに行を挿入します。
発言し終わったら入力中のメッセージは必要なくなるので、クリアしておきましょう。

ここの実装が終わると、実際にテキストを送信してみて、 Supabase の管理画面の table editor から行が挿入されていることが確認できます。

new Vue(
  methods:{
    // ...
    async sendMessage() {
      await supabase.from("messages").insert({
        message: this.inputMessage,
        name: this.inputName,
      });
      this.inputMessage = "";
    },
    // ...
  },
}

初回の取得機能を実装する ( Supabase のデータベースから取得する)

sendMessage() の中を実装します。
supabase.from("[テーブル名]").select('*') で、テーブルの全行を取得できます。
作成日で並び替えるのと、さすがに全件は必要ないので limit() を使って適当な数で制限しておきましょう。

const MAX_MESSAGE_COUNT = 10
new Vue(
  methods:{
    // ...
    async getMessages() {
      const { data } = await supabase
        .from("messages")
        .select("*")
        .limit(MAX_MESSAGE_COUNT)
        .order("createdAt", { ascending: false });
      this.messages = data;
    },
    // ...
  },
}

リアルタイムの取得機能を実装する ( Supabase のデータベースの変更を購読する)

subscribeMessage() の中を実装します。
supabase.from("[テーブル名]").on('INSERT', () => {}).subscribe() で、テーブルの挿入を購読できます。
ちなみに INSERT の箇所を * にすると、 UPDATE や DELETE なども購読できますが、今回は INSERT 以外されることがないので、 INSERT のみ購読します。

const MAX_MESSAGE_COUNT = 10
new Vue(
  methods:{
    // ...
    subscribeMessage() {
      supabase
        .from("messages")
        .on("INSERT", ({ new: newMessage }) => {
          this.messages = [newMessage, ...this.messages].slice(
            0,
            MAX_MESSAGE_COUNT
          );
        })
        .subscribe();
    },
    // ...
  },
}

リアルタイムの取得の解除を実装する

unsubscribeMessage() の中を実装します。

new Vue(
  methods:{
    // ...
    unsubscribeMessage() {
      supabase.removeAllSubscriptions();
    },
    // ...
  },
}

以上で完成です 🎉

完成

<template>
  <div id="app">
    <!-- form を submit すると sendMessage が呼ばれます -->
    <form @submit.prevent="sendMessage">
      <!-- 名前とメッセージの各inputタグの入力値は、 それぞれ inputName, inputMessage と同期されます -->
      <input type="text" v-model="inputName" placeholder="名前" /><br />
      <input type="text" v-model="inputMessage" placeholder="メッセージ" /><br />
      <button type="submit">送信</button>
    </form>
    <ul>
      <!-- messages に入っているメッセージをループして出力します -->
      <li v-for="message in messages" :key="message.id">
        <!-- message はオブジェクトで、 name, message, id, ここにはないですが createdAt 属性があることを期待しています -->
        <b>{{ message.name }}</b> {{ message.message }}
      </li>
    </ul>
  </div>
</template>

<script>
import { createClient } from "@supabase/supabase-js";

const supabase = createClient(
  "https://xxxxxxx.supabase.co",
  "xxxxxxx"
);
const MAX_MESSAGE_COUNT = 10;

export default {
  name: "App",
  data() {
    return {
      messages: [],
      inputName: "",
      inputMessage: "",
    };
  },
  methods: {
    async sendMessage() {
      await supabase.from("messages").insert({
        message: this.inputMessage,
        name: this.inputName,
      });
      this.inputMessage = "";
    },
    async getMessages() {
      const { data } = await supabase
        .from("messages")
        .select("*")
        .limit(MAX_MESSAGE_COUNT)
        .order("createdAt", { ascending: false });
      this.messages = data;
    },
    subscribeMessage() {
      supabase
        .from("messages")
        .on("INSERT", ({ new: newMessage }) => {
          this.messages = [newMessage, ...this.messages].slice(
            0,
            MAX_MESSAGE_COUNT
          );
        })
        .subscribe();
    },
    unsubscribeMessage() {
      supabase.removeAllSubscriptions();
    },
  },
  mounted() {
    this.getMessages()
    this.subscribeMessage()
  },
  beforeDestroy() {
    this.unsubscribeMessage()
  },
}
</script>

Supabase を触ってみて

Firebase Alternative を謳っているだけあって、導入がとても簡単でした。
あとは Firebase のデータベースがスキーマレスなのに対して、 Supabase はリレーショナルデータベースなので、降ってくる型がわからないという不安がなくなるのが嬉しいですね。
中身が PostgreSQL なので、馴染みのある SQL を実行して操作できることも、ポイント高いなーと思いました。

まずは個人開発から使っていきたいです◎

Discussion