FireFox で service worker から見たブラウザの URL が正しくならない現象


現象

webpush を実装して FireFox で動作確認をしていたところ、以下の現象に遭遇した。

  1. ページにアクセスする
  2. pushState で URL を変更する
  3. webpush を受信して client.url を見ると 1 の URL が参照される

Chrome では 2 の URL になってくれるのに。。。
今実装しているプロダクトでは以下の要件があるので URL を見て判定をしないといけないので困った。

  • 通知された URL をアクティブなページで表示していた場合は通知を表示しない
  • 通知をクリックした時に通知された URL が非アクティブなタブで開いていた場合は、それをアクティブにし、そうでない場合はタブを開く

環境

Mac 10.15 (Catalina)
FireFox 75.0

解決方法

client.url を見るのはやめた。

postMessage で URL を取得する方針に変更。
service worker からアプリに URL リクエストを postMessage して、
アプリ側ではそれを受けて location.href を postMessage で返信する。
実装は雰囲気こんな感じ。

sw.js

let resolveUrl = null;

const fetchUrl = client => {
  return Promise.race([
    new Promise(function (resolve) {
      resolveUrl = resolve;
      client.postMessage({ type: 'sw:requestUrl' });
    }),
    new Promise(function (resolve) {
      setTimeout(function () {
        resolve();
      }, 5000);
    })
  ]);
}

self.addEventListener('push', event => {
  if (!self.Notification || self.Notification.permission !== 'granted') {
    return;
  }

  const url = event.data.json().url;

  const notify = async () => {
    const clientList = await self.clients.matchAll({
      type: 'window'
    });

    for (let i = 0; i < clientList.length; i++) {
      const client = clientList[i]
      const clientUrl = await fetchUrl(client);
      if (
        client.visibilityState === 'visible'
        && clientUrl === url
      ) {
        return;
      }
    }
    self.registration.showNotification('通知', {});
  };
  event.waitUntil(notify());
});

self.onmessage = function(e) {
  if (e.data.type !== 'url') return;
  resolveUrl && resolveUrl(e.data.url);
};

アプリ側

const listener = (e: MessageEvent) => {
  if (e.origin !== location.origin || typeof e.data !== 'object') return;
  switch (e.data.type) {
    case 'sw:requestUrl':
      navigator.serviceWorker.controller?.postMessage({
        type: 'url',
        url: `${location.href}`
      });
      return;
    default:
      return;
  }
};
navigator.serviceWorker.addEventListener('message', listener);

これでなんとか解決。