この記事はNuxt/UnJS Advent Calendar 2024 23日目の記事です。
はじめに
はじめまして!添田です!
簡単な自己紹介をさせていただきます。
私はTypeScriptが一番好きなフロントエンドエンジニアで、TSKaigi2024から運営スタッフメンバーとして活動しています!業務でもVue.jsを使用していますが、今回は趣味で作成したラズパイアプリをNuxt.jsで構築した際の知見を共有したいと思います。
ざっくり概要
ラズパイを使用したIoTデバイスとWebアプリケーションを連携させ、リアルタイムな状態監視と制御を実現しています。
使用している技術スタックは以下の通りです。
フロントエンド:Nuxt.js 3.x
バックエンド:Node.js on Raspberry Pi
通信プロトコル:WebSocket (Socket.IO)
設計
シーケンス図
Webアプリケーションの設計を進める際、WebSequenceDiagrams を使用して作りたい機能ごとにシーケンス図を作成しました。
以下はサンプルのシーケンス図です(本記事のテキストはダミーです)。
sequenceDiagram
participant User
participant Frontend
participant Raspi(webAPI)
participant Raspi(WebSocket)
participant Locker
Frontend ->> User: テキスト
User ->> Locker: イベント
alt 扉を1秒以上開けた場合
Raspi(webAPI) ->> Locker: テキスト
Raspi(WebSocket) ->>+ Frontend:イベントを知らせる
Raspi(WebSocket) ->> Frontend:イベントを知らせる
alt 特定のイベントをした場合
Frontend ->>- User:XXの画面
else 特定のイベントをした場合
Frontend ->> User:30秒後にXXの画面(タイムアウト)
end
else 特定のイベントをした場合
Raspi(webAPI) ->> Raspi(webAPI):何もしない
end
このように簡単なテキストを書くことで、シーケンス図を素早く作成できるのでとても便利です。
アーキテクチャ説明
このアプリケーションでは、以下の理由からWebSocket(Socket.IO)を採用しています。
- リアルタイム性の要件:デバイスの状態変化を即座にUI上に反映する必要がある
- 双方向通信:サーバーからクライアントへのプッシュ通知が必要
- 効率的なリソース利用:継続的なポーリングを避けたい
WebSocket概要
WebSocketとSocket.IOの違い
WebSocketは、リアルタイム性が求められるイベントを即座に処理できる技術です。
HTTPリクエストとは異なり、クライアントとサーバー間で継続的な双方向通信が可能です。
これにより、リアルタイムでの通知や更新が求められるアプリケーションに適しています。
WebSocketは低レベルのプロトコルで、Socket.IOはその上に構築された高レベルのライブラリです。
Socket.IOの主な特徴は、
- 自動再接続機能
- フォールバック機能(WebSocket未対応の場合)
- イベントベースの通信
- 部屋(Room)という概念によるブロードキャスト機能
Nuxt.jsでのWebSocket導入
ライブラリのインストール
WebSocketをNuxt.jsで簡単に利用するために、nuxt-socket-ioをインストールします。
https://classic.yarnpkg.com/en/package/nuxt-socket-io
yarn add nuxt-socket-io
.envファイル作成
以下のように作成しました!
API_BASE_URL=http://XX.XX.XX.XXX/v1
WS_BASE_URL=ws://XX.XX.XX.XXX
WS_PATH=/socket.io
Nuxt.jsの設定
nuxt.config.ts
に以下の設定を追加します。
export default defineNuxtConfig({
~~~他の記述
modules: ['nuxt-socket-io'],
io: {
sockets: [{
name: 'main',
url: process.env.WS_BASE_URL,
default: true
}]
},
nitro: {
output: {
publicDir: path.join(__dirname, "output", "prod"),
},
experimental: {
websocket: true,
},
},
~~~他の記述
})
公式ドキュメントも参考にしています:https://socket.io/how-to/use-with-nuxt
WebSocket用のComposable作成
/composables/useWebSockets.ts
に以下のコードを実装しました。
import { ref, onUnmounted } from 'vue'
import type { Socket } from 'socket.io-client'
import io from 'socket.io-client'
interface OrderItem {
item_id: number
box_no?: number
}
interface WebSocketEvents {
onMessage?: (data: any) => void
onError?: (error: Error) => void
onReconnect?: () => void
}
export default function useWebSocket() {
const socket = ref<Socket | null>(null)
const isConnected = ref(false)
const lastError = ref<Error | null>(null)
const config = useRuntimeConfig()
const wsBaseUrl = config.public.WS_BASE_URL
const wsPath = config.public.WS_PATH
const setupConnection = ({
orderItems,
events = {},
}: {
orderItems: OrderItem[]
events?: WebSocketEvents
}) => {
if (socket.value) {
closeConnection()
}
try {
const _socket = io(wsBaseUrl, {
path: wsPath,
timeout: 1000,
transports: ['websocket'],
reconnectionAttempts: 5,
reconnectionDelay: 1000,
})
_socket.on('connect', () => {
console.log('WebSocket connected!')
isConnected.value = true
})
_socket.on('disconnect', () => {
console.log('WebSocket disconnected')
isConnected.value = false
})
_socket.on('error', (error: Error) => {
console.error('WebSocket error:', error)
lastError.value = error
events.onError?.(error)
})
_socket.on('reconnect', () => {
console.log('WebSocket reconnected')
events.onReconnect?.()
})
_socket.on('message', (data: any) => {
events.onMessage?.(data)
})
socket.value = _socket
} catch (error) {
console.error('Failed to setup WebSocket connection:', error)
lastError.value = error as Error
events.onError?.(error as Error)
}
}
const closeConnection = () => {
if (socket.value) {
socket.value.disconnect()
socket.value = null
isConnected.value = false
}
}
// 自動クリーンアップ
onUnmounted(() => {
closeConnection()
})
return {
setupConnection,
closeConnection,
socket,
isConnected,
lastError,
}
}
WebSocketの使用例
上記のComposableを以下のように利用します。
<script setup lang="ts">
import useWebSocket from '~/composables/useWebSocket';
const { setupConnection, closeConnection, isConnected } = useWebSocket();
onMounted(async () => {
setupConnection({
orderItems: orderItems,
events: {
onMessage: (data) => {
// メッセージ処理
},
onError: (error) => {
// エラー処理
},
onReconnect: () => {
// 再接続時の処理
}
}
});
// タイムアウト処理
const timeout = setTimeout(() => {
router.push("/");
}, 65 * 1000);
});
onBeforeUnmount(() => {
closeConnection();
});
</script>
<template>
<div>
<div v-if="!isConnected" class="error-message">
接続が切断されました。再接続を試みています...
</div>
</div>
</template>
セキュリティ考慮事項
WebSocket実装時は以下のセキュリティポイントに注意が必要です
- 認証・認可
- WebSocket接続時の認証処理
- 適切なトークン管理
- データバリデーション
- 受信データの検証
- 不正なデータの除外
- レート制限
- メッセージ送信の制限
- 接続数の制限
パフォーマンス最適化
- 接続管理
- 不要な接続はすぐに切断
- 再接続の適切な間隔設定
- メッセージ設計
- データ構造の最適化
- 必要最小限のデータ送信
- エラーハンドリング
- タイムアウトの適切な設定
- 再接続ロジックの実装
まとめ
Nuxt.jsとWebSocketを組み合わせることで、リアルタイム性の高いアプリケーションを効率的に構築できます。
今回の記事が皆さんのプロジェクトの参考になれば幸いです。