Nuxt3でwebsocketを使う

NuxtJS
https://pixabay.com/images/id-1452987/
この記事は約9分で読めます。

Nuxt3のリリースを見ると、SummerからAutumnになっていましたね。リリースが待ち遠しい。。

さて、Nuxt3でwebsocketを使うにはどうするかを調べていたところ、Nuxt2とは少し異なるようでした。本記事では、Nuxt3でwebsockt(今回はsocket.io)を使う方法について書きます。

注:記事作成時点ではRC版で開発も活発なため(githubにもwebsocktについてのissueがある)内容は変わる可能性がありますのでご注意ください。

 本記事作成時、Nuxt3はまだベータリリースのため動作が変わる可能性もあります。

Nuxtとsocket.ioのインストール

まずは、最新のnuxtとsocket.ioをインストールします。

npx nuxi init nuxt-app
cd nuxt-app
npm i socket.io socket.io-client

今回の環境は以下のようになっています。

  • macOS 10.15.7
  • NodeJS 16.15.0
  • npm 8.19.2
  • nuxt3 rc11
    • builder vite
    • preset node-server
    • ssr
  • socket.io 4.5.2

socket.ioの初期化ポイント

nuxt2と異なり、nuxt3ではlistenフックが開発モード時にしか呼ばれないようです。ビルドするとnitroのpresetのエントリーポイントが生成されますが、node-serverプリセットには探す限りlistenフックなるものが見あたりませんでした。よって、本番用ビルドでは別のsocket.ioの初期化ポイントを模索する必要がありそうです。

以下のdiscussionsが参考になります。

https://github.com/nuxt/framework/discussions/1189

上記のdiscussionsを見ると、productionモードではAPIのミドルウェアの中で初期化できそうだとなっているように見えます(ここについては今後変わる可能性もありそうなのでご注意ください)。

以下、presetはnode-serverとして試してみます。

ビューの準備

App.vueを以下のように準備します。

<script setup lang="ts">
import { io } from "socket.io-client";

const text = ref("");
const socket = ref();
const messages = ref([]);

const sendMessage = () => {
  socket.value.emit("message", { date: Date.now(), message: text.value });
  messages.value.push({
    from: "me",
    date: Date.now(),
    message: text.value,
  });
  text.value = "";
};
onMounted(() => {
  socket.value = io();
  socket.value.on("message", (payload) => {
    messages.value.push(payload);
  });
});

onBeforeUnmount(() => {
  if (socket.value) {
    socket.value.close();
  }
});
</script>

<template>
  <div>
    <input type="text" v-model="text" />
    <button type="button" @click="sendMessage">send</button>
    <div class="container">
      <ul v-for="data in messages">
        <li>
          <div class="chat">
            <span class="chat__from">@{{ data.from }}</span>
            <span class="chat__datetime">{{
              new Date(data.date).toLocaleString()
            }}</span>
            <div class="chat__body">{{ data.message }}</div>
          </div>
        </li>
      </ul>
    </div>
  </div>
</template>

<style lang="scss" scoped>
.container {
  padding-top: 1rem;
  ul {
    margin: 0;
    padding: 0;
    li {
      list-style: none;
      margin: 0;
    }
  }

  .chat {
    padding-bottom: 0.5rem;
  }

  .chat__from,
  .chat__datetime {
    display: inline-block;
    font-size: 0.8rem;
  }
  .chat__datetime {
    color: grey;
    margin-left: 0.5rem;
  }
  .chat__body {
    padding: 0 0.4rem;
  }
}
</style>

簡素ですが、今回作成するビューは以下のような見た目になります。

devモード時

devモード時はlistenフックが効くので、moduleでlistenフックを使って初期化してみます。以下のようにしてみます。

import defu from "defu";
import type { ManagerOptions, SocketOptions } from "socket.io-client";
import type { ServerOptions } from "socket.io";
import { Server } from "socket.io";
import consola from "consola";
import { defineNuxtModule } from "@nuxt/kit";

interface ModuleOptions {
  client?: Partial<ManagerOptions & SocketOptions>;
  server?: Partial<ServerOptions>;
}

export default defineNuxtModule<ModuleOptions>({
  meta: {
    name: "websocket",
    configKey: "websocket",
    compatibility: {
      nuxt: "^3.0.0-rc.11",
    },
  },
  defaults: {
    client: {
      transports: ["websocket"], // 今回pollingは外すとします
    },
    server: {
      serveClient: false,
    },
  },
  hooks: {},
  setup(options, nuxt) {
    const opts = defu(
      nuxt.options.runtimeConfig.public.websocket,
      nuxt.options.runtimeConfig.websocket,
      options,
    );

    nuxt.hook("listen", async (server) => {
      const io = new Server(server, opts.server);
      nuxt.hook("close", () => io.close());
      consola.info("websocket listen", server.address(), server.eventNames());

      io.on("connection", (socket) => {
        socket.on("message", (payload) => {
          socket.broadcast.emit("message", { from: socket.id, ...payload });
        });
      });
    });
  },
});

declare module "@nuxt/schema" {
  interface ConfigSchema {
    publicRuntimeConfig?: {
      websocket?: {
        client?: Partial<ManagerOptions & SocketOptions>;
      };
    };

    privateRunimeConfig?: {
      websocket?: {
        client?: Partial<ManagerOptions & SocketOptions>;
        server?: Partial<ServerOptions>;
      };
    };
  }
}

続いてnuxt.config.tsでモジュールのロードを設定します。

export default defineNuxtConfig({
  modules: ["~/modules/websocket/module.ts"],
});

productionモード時

こちらはグローバルにアクセスできるオブジェクト参照としてglobalThisを使うこととします。

import { Server } from "socket.io";
import consola from "consola";

export default defineEventHandler((event) => {
  if (process.env.NODE_ENV !== "production") {
    return;
  }

  if (!globalThis.$io) {
    // @ts-expect-error
    const server = event.res.socket?.server;
    const runtimeConfig = useRuntimeConfig();
    const io = new Server(server, runtimeConfig.websocket?.server);
    globalThis.$io = io;

    io.on("connection", (socket) => {
      socket.on("disconnect", () => {
        consola.info(disconnect: ${socket.id});
      });

      socket.on("message", (payload) => {
        socket.broadcast.emit("message", { from: socket.id, ...payload });
      });
    });
  }
});

参考リンク

コメント

タイトルとURLをコピーしました