概要
ちょっとしたデモを作る時など、PCやスマホ、IoTデバイス等の間でリアルタイムにデータをやりとりしたいときって結構あります。
そんなとき Firestore 使うと便利なんですけど、Internet につながらない環境という場合がけっこうありまして、そんなのときのためにローカル回線の中で簡易 Firestore 的なこと(データを更新したら各WebApp上のデータも自動でリアルタイムに更新されるというもの)を実現してみました。
作ったもの
同期するデータ(サーバ/クライアントで共有)
synchroData.ts
interface AllData {
aaa: number
bbb: string
ccc: string[]
}
const allData: AllData = {
aaa: 123,
bbb: 'abc',
ccc: [],
}
export default {}
export { allData, AllData }
WebSocketサーバ(node-ts)
package.json
{
"name": "synchro-data",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "ts-node src/index.ts"
},
"license": "MIT",
"devDependencies": {
"typescript": "^4.5.4"
},
"dependencies": {
"@types/lodash": "^4.14.178",
"@types/node": "^17.0.8",
"@types/ws": "^8.2.2",
"lodash": "^4.17.21",
"ts-node": "^10.4.0",
"ws": "^8.4.0"
}
}
index.ts
import { Server } from 'ws'
import { set } from 'lodash'
import { allData, AllData } from './synchroData'
const ws = new Server({ port: 5001 });
ws.on('connection', socket => {
console.log('connected!')
// 初期データ受け取り
socket.send(JSON.stringify({
allData,
}))
socket.on('message', msg => {
console.log('receive: ' + msg);
// 自身のデータ更新
const received = JSON.parse(`${msg}`)
if (received.method === 'set') {
set(allData, received.path, received.data)
console.log('allData', allData)
}
// クライアントにリレー
ws.clients.forEach(client => {
// console.log(`${msg}`);
client.send(`${msg}`)
})
})
socket.on('close', () => {
console.log('good bye.');
})
})
console.log('Start!')
tsconfig.json
{
"compilerOptions": {
"module": "commonjs",
"target": "ES2021",
"sourceMap": true,
"types": [
"node"
]
},
"include": [
"./src/**/*"
],
"exclude": [
"node_modules"
]
}
クライアント(nuxt3)
package.json
{
"private": true,
"scripts": {
"dev": "nuxi dev",
"build": "nuxi build",
"start": "node .output/server/index.mjs"
},
"devDependencies": {
"nuxt3": "latest"
},
"dependencies": {
"@types/lodash": "^4.14.178",
"lodash": "^4.17.21"
}
}
nuxt.config.ts
import { defineNuxtConfig } from 'nuxt3'
// https://v3.nuxtjs.org/docs/directory-structure/nuxt.config
export default defineNuxtConfig({
ssr: false,
})
plugins/SynchroDataPlugin.ts
import { reactive, ref } from 'vue'
import { defineNuxtPlugin } from '#app'
import { set as lodashSet } from 'lodash'
import { allData, AllData } from '~/plugins/synchroData'
interface SynchroDataJournal {
method: 'set' | 'append'
path: string
data: any
}
const initSynchroData = <SynchroData extends object> (defaultData: SynchroData) => {
type ResOfAllData = { allData: SynchroData }
const isResOfAllData = <SynchroData> (res: any): res is ResOfAllData => {
return !!res['allData']
}
const synchroData = reactive<SynchroData>(defaultData)
const synchroDataStatus = ref('')
const mutateSynchroData = (journal: SynchroDataJournal) => {
if (journal.method === 'set') {
lodashSet(synchroData, journal.path, journal.data)
// console.log('allData', allData)
}
}
const ws = new WebSocket('ws://localhost:5001')
ws.addEventListener('open', (e) => {
synchroDataStatus.value = 'Socket Success'
})
ws.addEventListener('message', (e) => {
synchroDataStatus.value = `received: ${e.data}`
const received = JSON.parse(`${e.data}`) as ResOfAllData | SynchroDataJournal
if (isResOfAllData(received)) {
Object.assign(synchroData, received.allData)
} else {
mutateSynchroData(received)
}
})
const synchroDataEmit = (journal: SynchroDataJournal) => {
ws.send(JSON.stringify(journal))
}
return {
provide: {
synchroData,
synchroDataStatus,
synchroDataEmit,
}
}
}
export default defineNuxtPlugin(nuxtApp => initSynchroData<AllData>(allData))
pages/index.vue
<template>
<div>
<div>{{ $synchroDataStatus }}</div>
<div>{{ $synchroData }}</div>
<button @click="dataChange">
dataChange
</button>
</div>
</template>
<script setup lang="ts">
const { $synchroData, $synchroDataStatus, $synchroDataEmit } = useNuxtApp()
const dataChange = () => {
$synchroDataEmit({
method: 'set',
path: 'aaa',
data: $synchroData.aaa + 1,
})
}
</script>
結果
これで $synchroData には常に最新のデータがリアクティブに入っている状態になる。