Vue 3での使用方法
このガイドでは、Vue 3 アプリケーション内で Socket.IO を使用する方法を説明します。
サンプル
構成
src
├── App.vue
├── components
│   ├── ConnectionManager.vue
│   ├── ConnectionState.vue
│   └── MyForm.vue
├── main.js
└── socket.js
Socket.IO クライアントは、src/socket.js ファイルで初期化されます。
src/socket.js
import { reactive } from "vue";
import { io } from "socket.io-client";
export const state = reactive({
  connected: false,
  fooEvents: [],
  barEvents: []
});
// "undefined" means the URL will be computed from the `window.location` object
const URL = process.env.NODE_ENV === "production" ? undefined : "https://:3000";
export const socket = io(URL);
socket.on("connect", () => {
  state.connected = true;
});
socket.on("disconnect", () => {
  state.connected = false;
});
socket.on("foo", (...args) => {
  state.fooEvents.push(args);
});
socket.on("bar", (...args) => {
  state.barEvents.push(args);
});
開発中は、サーバーで CORS を有効にする必要があります。
const io = new Server({
  cors: {
    origin: "https://:8080"
  }
});
参考: CORS の処理
イベントリスナーは `src/socket.js` ファイルに登録されます。コンポーネントにリスナーを登録することは、強くお勧めしません。詳細は以下を参照してください。
その後、コンポーネント内で使用できます。
- src/components/ConnectionState.vue
<template>
  <p>State: {{ connected }}</p>
</template>
<script>
import { state } from "@/socket";
export default {
  name: "ConnectionState",
  computed: {
    connected() {
      return state.connected;
    }
  }
}
</script>
- src/components/ConnectionManager.vue
<template>
  <button @click="connect()">Connect</button>
  <button @click="disconnect()">Disconnect</button>
</template>
<script>
import { socket } from "@/socket";
export default {
  name: "ConnectionManager",
  methods: {
    connect() {
      socket.connect();
    },
    disconnect() {
      socket.disconnect();
    }
  }
}
</script>
また、autoConnect オプションを使用して、すぐに接続せずに socket オブジェクトを初期化することもできます。
export const socket = io(URL, {
  autoConnect: false
});
これは、たとえば、ユーザーが接続する前に資格情報を提供する必要がある場合に役立ちます。
- src/components/MyForm.vue
<template>
  <form @submit.prevent="onSubmit">
    <input v-model="value" />
    <button type="submit" :disabled="isLoading">Submit</button>
  </form>
</template>
<script>
import { socket } from "@/socket";
export default {
  name: "MyForm",
  data() {
    return {
      isLoading: false,
      value: ""
    }
  },
  methods: {
    onSubmit() {
      this.isLoading = true;
      socket.timeout(5000).emit("create-something", this.value, () => {
        this.isLoading = false;
      });
    },
  }
}
</script>
参考: https://vuejs.org/guide/scaling-up/state-management.html
重要な注意事項
これらの注意点は、どのフロントエンドフレームワークにも当てはまります。
ホットモジュールリローディング
Socket.IO クライアントの初期化を含むファイル (上記の例では src/socket.js ファイル) のホットリローディングを行うと、以前の Socket.IO 接続がアクティブなままになる場合があります。これは、
- Socket.IO サーバーに複数の接続が存在する可能性があることを意味します。
- 以前の接続からのイベントを受信する可能性があることを意味します。
唯一の既知の回避策は、この特定のファイルが更新されたときに**ページ全体をリロード**することです (または、ホットリローディングを完全に無効にすることもできますが、それは少し極端かもしれません)。
参考: https://vue-loader.vuejs.org/guide/hot-reload.html
子コンポーネントのリスナー
子コンポーネントにイベントリスナーを登録することは、UI の状態とイベントの受信時間を結びつけるため、強くお勧めしません。コンポーネントがマウントされていない場合、メッセージが見落とされる可能性があります。
src/components/MyComponent.vue
<script>
import { socket } from "@/socket";
export default {
  name: "MyComponent",
  data() {
    return {
      fooEvents: []
    }
  },
  mounted() {
    // BAD
    socket.on("foo", (...args) => {
      this.fooEvents.push(args);
    });
  }
}
</script>
ただし、ルートコンポーネントでは問題ありません (常にマウントされているため)。
一時的な切断
WebSocket 接続は非常に強力ですが、常に稼働しているとは限りません。
- ユーザーと Socket.IO サーバーの間のあらゆるものが、一時的な障害が発生したり、再起動されたりする可能性があります。
- サーバー自体が、自動スケーリングポリシーの一環として強制終了される場合があります。
- モバイルブラウザの場合、ユーザーが接続を失ったり、Wi-Fi から 4G に切り替えたりする可能性があります。
つまり、ユーザーに優れたエクスペリエンスを提供するために、一時的な切断を適切に処理する必要があります。
幸いなことに、Socket.IO には、役立つ機能がいくつか含まれています。以下を確認してください。
Pinia を使用する場合
Pinia は Vue 用のストアライブラリであり、コンポーネント/ページ間で状態を共有できます。
詳細情報はこちらにあります。
Pinia のストアと Socket.IO 接続は、次のパターンで同期できます。
import { defineStore } from "pinia";
import { socket } from "@/socket";
export const useItemStore = defineStore("item", {
  state: () => ({
    items: [],
  }),
  actions: {
    bindEvents() {
      // sync the list of items upon connection
      socket.on("connect", () => {
        socket.emit("item:list", (res) => {
          this.items = res.data;
        });
      });
      // update the store when an item was created
      socket.on("item:created", (item) => {
        this.items.push(item);
      });
    },
    createItem(label) {
      const item = {
        id: Date.now(), // temporary ID for v-for key
        label
      };
      this.items.push(item);
      socket.emit("item:create", { label }, (res) => {
        item.id = res.data;
      });
    },
  },
});
import { defineStore } from "pinia";
import { socket } from "@/socket";
export const useConnectionStore = defineStore("connection", {
  state: () => ({
    isConnected: false,
  }),
  actions: {
    bindEvents() {
      socket.on("connect", () => {
        this.isConnected = true;
      });
      socket.on("disconnect", () => {
        this.isConnected = false;
      });
    },
    connect() {
      socket.connect();
    }
  },
});
そして、ルートコンポーネントで
<script setup>
import { useItemStore } from "@/stores/item";
import { useConnectionStore } from "@/stores/connection";
import { socket } from "@/socket";
const itemStore = useItemStore();
const connectionStore = useConnectionStore();
// remove any existing listeners (after a hot module replacement)
socket.off();
itemStore.bindEvents();
connectionStore.bindEvents();
</script>