## 概要 [[BroadcastChannel]]の技術を使ってデータのドキュメント同期を行う。[[Vue]]を使うため、[[VueUse]]の[[useBroadcastChannel]]を使ってみる。 ## 実現したいこと - ウィンドウA - タブ1 (pageA) - ウィンドウB - タブ2 (pageB) - タブ3 (pageB) という構成があったときに以下を満たすこと。 - タブ1のinputフォームからデータを変更したとき - タブ1の表示部に内容が表示される (これは普通) - タブ2の表示部に内容がリアルタイムに同期して表示される - タブ3の表示部に内容がリアルタイムに同期して表示される - タブ2やタブ3を再度開きなおしたら、タブ1の状態が表示される ## プロジェクト作成 [[Nuxt]]のプロジェクトをつくる。[[toki]]を使う。 ```console toki nuxt nuxt-broadcast-channel-sandbox ``` [[ランタイム]]には[[Bun]]を選択。[[VueUse]]もインストールしておく。 ```console cd nuxt-broadcast-channel-sandbox bun add @vueuse/core ``` ## ページのソースコード作成 簡単なページをつくる。 `app.vue` ```html <template> <div> <NuxtPage /> </div> </template> ``` `pages/pageA.vue` ```html <script setup lang="ts"> const text = ref("init"); </script> <template> <h1>pageA</h1> <input v-model="text" type="text" /> <h2>{{ text }}</h2> </template> ``` `pages/pageB.vue` ```html <script setup lang="ts"> const text = ref("init"); </script> <template> <h1>pageB</h1> <h2>{{ text }}</h2> </template> ``` ![[Pasted image 20241017225935.png|frame]] *この時点ではページ間のデータ同期はされていない* ## Broadcast Channelでデータを同期する [[VueUse]]の[[useBroadcastChannel]]を使ってデータ同期するコードを実装する。まずはブロードキャストするメッセージを `types.d.ts` に定義する。 ```ts export type DataBroadcastMessage = { type: "data-broadcast"; value: string }; export type RequestBroadcastMessage = { type: "request-broadcast" }; export type BroadcastMessage = DataBroadcastMessage | RequestBroadcastMessage; ``` ブロードキャストする側の `pages/pageA.vue` は入力に変更があったらメッセージをブロードキャストする。ただ、それだけでは不十分であり、**別のページが作成されたときに、他同期ページと同様のデータを初期描画するため、ブロードキャストしたpageAから再度ブロードキャストしてもらう要求** を行う。 ```html <script setup lang="ts"> import { useBroadcastChannel } from "@vueuse/core"; import type { BroadcastMessage, DataBroadcastMessage } from "~/types"; // postでデータをブロードキャストできる const { data, post } = useBroadcastChannel< BroadcastMessage, DataBroadcastMessage >({ name: "minerva-test", }); const broadcastData = () => { post({ type: "data-broadcast", value: { id: text.value.length, name: text.value }, }); }; const text = ref("init"); // textが変更されたらブロードキャスト(post) watch( () => text.value, () => { broadcastData(); }, // WARN: immediate: true にするとmount完了前にbroadcastDataが実行されてしまうのでNG ); onMounted(async () => { // WARN: mount完了後じゃないとBroadcast Channelの初期化が完了していない // その状態でpostすると処理は実行されるがreceiverが受け取れないので回避 broadcastData(); }); // ブロードキャスト要求が来たら、textが変更されてなくてもブロードキャスト watch( () => data.value, (d) => { if (d.type === "request-broadcast") { broadcastData(); } }, ); </script> <template> <h1>pageA</h1> <input v-model="text" type="text" /> <h2>{{ text }}</h2> </template> ``` ゆえに、ブロードキャストを受ける側の `pages/pageB.vue` は ```html <script setup lang="ts"> import { useBroadcastChannel } from "@vueuse/core"; import type { BroadcastMessage, RequestBroadcastMessage } from "~/types"; // dataでブロードキャストされたデータを受け取れる const { data, post } = useBroadcastChannel< BroadcastMessage, RequestBroadcastMessage >({ name: "minerva-test" }); const message = ref(""); // DataBroadcastMessageを受信したならmessageに代入 watch( () => data.value, (d) => { if (d.type === "data-broadcast") { message.value = `${d.value.id}: ${d.value.name}`; } }, ); // ページマウント後にデータを取得するため、RequestBroadcastMessageをブロードキャストしてpageAにDataBroadcastMessageをブロードキャストしてもらう onMounted(() => { post({ type: "request-broadcast" }); }); </script> <template> <h1>pageB</h1> <h2>{{ message }}</h2> </template> ``` ## 動作確認 ![[2024-10-18-01-35-11.webm]]