## 概要
[[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]]