NuxtでSocket.ioを使う

# はじめに
WebSocket使いたいけどわざわざバックエンドを別で立てるのめんどくさい時ありますよね。
そんな時はNuxt.jsかNext.jsあたりでフロントエンド開発と同じ感覚で開発したいものですが、
例によってNext.jsの記事はあってもNuxt.jsの記事はあんまりないので書こうと思いました。
※基本的に[Socket.ioの公式ドキュメント](https://socket.io/how-to/use-with-nuxt)通りですが、一部自分の追加した記述も載せます。

あとNuxt.jsで調べると出てくる下記モジュールの開発が止まっているのと、自分の環境で試したらうまく動かなかったのも理由です

https://nuxt.com/modules/socket-io

# 対象読者
- Vue/Nuxtを使っていてWebSocketやりたい人
- Vue + バックエンドよりもNuxtだけでWebSocketやりたい人

# 実装
## ライブラリ追加
Nuxt.jsのプロジェクトは作成できている前提で、まずはライブラリを追加してください。

```
npm install socket.io socket.io-client
```

## nitro設定
現在nitro上において、WebSocketを利用した機能はまだ`Experimental`なので、下記のように書く必要があります。（今後は変わるかも。[こちら](https://github.com/unjs/nitro/issues/2171)のマージを待ちましょう）

```nuxt.config.ts
export default defineNuxtConfig({
    ~~~他の設定~~~
    nitro: {
        experimental: {
            websocket: true
        },
    }
})
```

## plugin作成
バックエンドフレームワーク内に配置することの多いSocket.ioの設定を、Nuxtでは`server/plugins`に配置します。
Socket.ioの公式ドキュメントにはないですが、私はアクティブなデバイスの判別をしたかったのでタイムアウトしたデバイスの削除処理などを追加しています。

```server/plugins/socket.io.ts
import type { NitroApp } from "nitropack";
import { Server as Engine } from "engine.io";
import { Server } from "socket.io";
import { defineEventHandler } from "h3";

export default defineNitroPlugin((nitroApp: NitroApp) => {
    const engine = new Engine();
    const io = new Server();

    io.bind(engine);

    const activeDevices = new Map()

    io.on("connection", (socket) => {
        console.log('Client connected')

        // ここで下記のように機能追加する
        socket.on('sample', (data) => {
            // 他のクライアントにブロードキャスト
            socket.broadcast.emit('sampleUpdate', data)
        })

　　　　　// アクティブなデバイスのリセット
        socket.on('disconnect', () => {
            console.log('Client disconnected')
            for (const [deviceId, device] of activeDevices.entries()) {
                if (device.socket === socket) {
                    activeDevices.delete(deviceId)
                    io.emit('deviceDisconnected', deviceId)
                }
            }
        })
    });

    // アクティブなデバイスの管理
    setInterval(() => {
        const now = Date.now()
        for (const [deviceId, device] of activeDevices.entries()) {
            if (now - device.lastUpdate > 10000) { // 10 seconds timeout
                activeDevices.delete(deviceId)
                io.emit('deviceDisconnected', deviceId)
            }
        }
    }, 5000)

    nitroApp.router.use("/socket.io/", defineEventHandler({
        handler(event) {
            engine.handleRequest(event.node.req, event.node.res);
            event._handled = true;
        },
        websocket: {
            open(peer) {
                const nodeContext = peer.ctx.node;
                const req = nodeContext.req;

                // @ts-expect-error private method
                engine.prepare(req);

                const rawSocket = nodeContext.req.socket;
                const websocket = nodeContext.ws;

                // @ts-expect-error private method
                engine.onWebSocket(req, rawSocket, websocket);
            }
        }
    }));
});

```

## クライアント側
utilsにクライアント側共通で利用するSocketを作成します。

```utils/socket/socket.ts
import { io } from "socket.io-client";

export const socket = io();
```

WebSocketを利用するコンポーネントは下記のように記述します。
公式ドキュメントだと`.client.vue`ファイルで記述されていましたが、普通のvueファイルでもライフサイクルに気をつけていれば問題なく動くはずです。
```components/socket-component.vue
<script setup lang="ts">
import {ref, watch} from "vue";
import {socket} from "~/utils/socket/socket";

const isConnected = ref(false);
const transport = ref("N/A");

if (socket.connected) {
  onConnect();
}

function onConnect() {
  isConnected.value = true;
  transport.value = socket.io.engine.transport.name;
  socket.io.engine.on("upgrade", (rawTransport) => {
    transport.value = rawTransport.name;
  });
}

function onDisconnect() {
  isConnected.value = false;
  transport.value = "N/A";
}

socket.on("connect", onConnect);
socket.on('sampleUpdate', (data: {id:string, lastUpdate: number}) => {
  // コンポーネント側の処理
})

socket.on('deviceDisconnected', (deviceId: string) => {
  // 接続が切断された時の処理
})
socket.on("disconnect", onDisconnect);

onBeforeUnmount(() => {
  socket.off("connect", onConnect);
  socket.off("disconnect", onDisconnect);
});
</script>

<template>
  <div>
    <p>Status: {{ isConnected ? "connected" : "disconnected" }}</p>
  　<p>Transport: {{ transport }}</p>
　</div>
</template>
```

以上です。あとはいい感じにサービスへ取り込んでみてください！

# 参考文献

https://socket.io/how-to/use-with-nuxt