☕️

Nuxt3 + piniaでサブウィンドウ間のstoreを共有する方法

2022/05/22に公開

目標

以下のような感じで、親窓と小窓のstore共有、および小窓間でのstore共有ができるようにします。
browser-sync

本記事の対象

  • サブウィンドウやタブ間で、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"

結論

具体的なコードはこちらに記載してます。
https://github.com/pilefort/pinia-browser-sync-sample

ブラウザ間の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の引数に使用したいプラグインを指定して使います)。
https://github.com/pilefort/pinia-browser-sync-sample/blob/2caafdbbb6e6db1b12826242fa7e3e2ab01285e7/plugins/pinia-sync-browser.ts

参考

https://pinia.vuejs.org/core-concepts/plugins.html

pinia-shared-stateは何をやっているか?

このライブラリの中身を見てみます。

このファイルがやりたいことを実現してる部分です。
https://github.com/wobsoriano/pinia-shared-state/blob/2f256dc8a556980cd514e36a9abe93f872cc9fcb/packages/plugin/src/index.ts

型定義を書いている declare module 'pinia' {}
piniaのstoreを読み出している PiniaSharedState
ブラウザ間のstore共有をしている share
の3つから構成されています。

以下でそれぞれについて見ていきます。

型定義部分

まず型定義部分です。
https://github.com/wobsoriano/pinia-shared-state/blob/2f256dc8a556980cd514e36a9abe93f872cc9fcb/packages/plugin/src/index.ts#L119-L149

ここではstoreのファイルに追加できるpiniaのオプションを書いてます。

share?: {
  omit?: Array<keyof S>
  enable?: boolean
  initialize?: boolean
}

これを定義することで、以下のようにstate, actions以外のオプションを追加できるようになります。

store/count.ts
export const useCountStore = defineStore('countStore', {
  state: () => ({ ... }),
  actions: {
    countPlus() { ... },
  },
  share: {
    omit: [...],
    enable: ...,
    initialize: ...,
  }
})

PiniaSharedState部分

これはstoreやshared.omitなどのオプションを確認するための部分です。
https://github.com/wobsoriano/pinia-shared-state/blob/2f256dc8a556980cd514e36a9abe93f872cc9fcb/packages/plugin/src/index.ts#L103-L117

大事な部分だけ残すと以下のような感じです。
この部分では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を共有する部分です。
https://github.com/wobsoriano/pinia-shared-state/blob/2f256dc8a556980cd514e36a9abe93f872cc9fcb/packages/plugin/src/index.ts#L26-L80

メッセージを送受信するために、チャンネルを作成し、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などにも利用できそうなので、参考になれば幸いです。

https://github.com/kusakabe-t/pinia-browser-sync-sample/blob/master/plugins/pinia-sync-browser.ts
以上

GitHubで編集を提案

Discussion