Nuxt3 + piniaでサブウィンドウ間のstoreを共有する方法
目標
以下のような感じで、親窓と小窓のstore共有、および小窓間でのstore共有ができるようにします。
本記事の対象
- サブウィンドウやタブ間で、storeを共有したい方
- wobsoriano/pinia-shared-stateのコードリーディングをしたい方
環境
簡単のため、SPAのアプリで実験しました。
使用ライブラリのバージョンは以下です。
"nuxt": "3.0.0-rc.3",
"pinia": "^2.0.14"
"@pinia/nuxt": "^0.1.9",
"broadcast-channel": "^4.2.0"
結論
具体的なコードはこちらに記載してます。
ブラウザ間のstoreの同期はBroadcast Channel APIとvueのwatchメソッドを併用して実現します。
また複数ブラウザに対応するために、Broadcast Channel APIはpubkey/broadcast-channelというライブラリで代用できます。
wobsoriano/pinia-shared-stateについて
https://www.npmjs.com/search?q=pinia%20share
で検索すると、pinia-shared-stateというライブラリが公開されていると分かります。
これは、vue2/3でpiniaを用いてブラウザ間のstoreを共有するためのライブラリです。
このライブラリはplugins配下で、piniaにpluginを追加することで利用できます (pinia.useの引数に使用したいプラグインを指定して使います)。
参考
pinia-shared-stateは何をやっているか?
このライブラリの中身を見てみます。
このファイルがやりたいことを実現してる部分です。
型定義を書いている declare module 'pinia' {}
piniaのstoreを読み出している PiniaSharedState
ブラウザ間のstore共有をしている share
の3つから構成されています。
以下でそれぞれについて見ていきます。
型定義部分
まず型定義部分です。
ここではstoreのファイルに追加できるpiniaのオプションを書いてます。
share?: {
omit?: Array<keyof S>
enable?: boolean
initialize?: boolean
}
これを定義することで、以下のようにstate, actions以外のオプションを追加できるようになります。
export const useCountStore = defineStore('countStore', {
state: () => ({ ... }),
actions: {
countPlus() { ... },
},
share: {
omit: [...],
enable: ...,
initialize: ...,
}
})
PiniaSharedState部分
これはstoreやshared.omitなどのオプションを確認するための部分です。
大事な部分だけ残すと以下のような感じです。
この部分ではstoreで定義されているstateやオプションを取得し、ブラウザ間で共有するstoreのstateをフィルタリングしています。
return ({ store, options }: PiniaPluginContext) => {
Object.keys(store.$state).forEach((key) => {
if (omittedKeys.includes(key)) return
share(key, store)
})
}
share部分
ここが実際にブラウザ間でstoreのstateを共有する部分です。
メッセージを送受信するために、チャンネルを作成し、onmessageでメッセージを受け取れるようにします。
const channelName = `${store.$id}-${key.toString()}`
// typeについては以下を参考。古いブラウザでも対応したいなら、type = localStorageとする
// cf. https://github.com/pubkey/broadcast-channel#methods
const channel = new BroadcastChannel(channelName, {
type,
})
// ...
channel.onmessage = (evt) => { /* ... */ }
storeが更新されたときに、異なるウィンドウにstoreを更新するように指示する部分がこの部分です。
channel.postMessageで変化したstateを渡します。
watch(
() => store[key],
(state) => {
if (!externalUpdate) {
timestamp = Date.now()
channel.postMessage({
timestamp,
state: JSON.parse(safeStringify(state)),
})
}
externalUpdate = false
},
{ deep: true },
)
受け取り側では以下のように、store[key] = evt.state
でstateを更新します。
channel.onmessage = (evt) => {
if (evt === undefined) {
channel.postMessage({
timestamp,
state: JSON.parse(safeStringify(store[key])),
})
return
}
if (evt.timestamp <= timestamp)
return
externalUpdate = true
timestamp = evt.timestamp
store[key] = evt.state
}
以下の部分は親窓が新しくウィンドウを開いたときに、新しく開いたウィンドウにstateを渡すために追加しています。
if (evt === undefined) {
channel.postMessage({
timestamp,
state: JSON.parse(safeStringify(store[key])),
})
return
}
上記はonmessageでメッセージを受け取らないと実行されません。
そのため以下のように、ウィンドウが作成されたときにpluginが読み込まれることを利用して、メッセージを送ります。
const sync = () => channel.postMessage(undefined)
const unshare = () => {
return channel.close()
}
// fetches any available state
if (initialize)
sync()
wobsoriano/pinia-shared-stateを簡素に書いた場合
上記のライブラリを簡素に書いて、オプションを修正したものが以下になります。
同じような仕組みはReactなどにも利用できそうなので、参考になれば幸いです。
以上
Discussion