10
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

マルチ通信:クロスタブ通信の多様な選択肢の探究

Posted at

Ptengine

背景

現在のWeb開発では、複雑なアプリケーションの要件や、複数のタブの使用の普及に伴い、フロントエンドエンジニアは異なるタブ間のデータ通信や共同作業の問題を解決する必要があります。これには、データの共有、リアルタイム通信の実現、あるタブでの操作が他のタブに即座に反映されることの確保などが含まれます。オンラインのチームコラボレーションツールでは、複数のタブで同時にドキュメントを編集することができるようにしたり、電子商取引サイトでは、ユーザーが1つのタブで商品をカートに追加し、別のタブでカートのリアルタイム更新を確認できるようにする必要があります。特に、Ptengine では、図1に示すような視覚的なイベント設定プロセスにおいて、タブ間通信が必要です。

本文では、さまざまなタブ間通信の方法について詳しく説明し、実際の課題の解決方法を理解するのに役立ちます。初心者でも経験豊富なプロフェッショナルでも、これらの方法を理解することで、さまざまなアプリケーションでの通信ニーズに応え、ユーザーエクスペリエンスを向上させ、機能性を高めることができます。

さまざまなタブ間通信のニーズに柔軟に対応するために、これらの技術について詳しく理解しましょう。次に、8つのタブ間通信の方法を紹介し、さまざまなシナリオに対応するための解決策を提供します。

BroadCast Channel:シンプルでパワフルなタブ間通信方法

BroadCast Channelは、モダンなWeb開発におけるコミュニケーションツールであり、同一オリジンの異なるブラウザのタブ、フレーム、もしくはiframe間で簡単な通信を実現することができます。名前付きのチャンネルを作成することにより、複数のページはメッセージを送受信し合うことができ、双方向の通信を実現します。

BroadCast Channelの仕組みは、名前付きのチャンネルを作成し、同一オリジンの異なるブラウザウィンドウ、タブ、フレーム、もしくはiframe内の異なるドキュメント間で相互通信を行うことを可能にします。各BroadCast Channelオブジェクトは、チャンネルを識別するために一意の名前を使用する必要があります。同一ドメイン内の異なるページ間では、この名前は一意である必要があります。messageイベントをトリガーすることにより、メッセージはそのチャンネルをリッスンしているすべてのBroadCast Channelオブジェクトにブロードキャストされ、情報の共有が実現されます。

postMessageとは異なり、BroadCast Channelはブロードキャストのような手法であり、複数のページが同じチャンネルにサブスクライブし、メッセージを群発することができます。これは単なるピアツーピアの通信だけでなく、マルチページの情報共有やリアルタイムな更新が必要なアプリケーションのシナリオに非常に適しています。異なるページはインデックスを維持する必要はなく、単純に特定のチャンネルを「サブスクライブ」することで、複数のページ間での情報共有が実現できます。

以下は、BroadCast Channelを使用して2つのタブ間で通信する方法を示す簡単な例です:

タブA - メッセージの送信

// BroadCast Channelを作成する
const channel = new BroadcastChannel('my_channel');

// メッセージを送信する
channel.postMessage('Hello from Page A!');

タブB - メッセージの受信

// BroadCast Channelを作成する
const channel = new BroadcastChannel('my_channel');

// メッセージ受信 - 方法A
channel.onmessage = function(event) {
  console.log('Received message in Page B:', event.data);
};

// メッセージ受信 - 方法B
channel.addEventListener('message', function(event){
	console.log('Received message in Page B:', event.data);
})

メッセージを閉じる

// メッセージを閉じる - 方法A
// この方法では、channelとの接続が切断され、ブラウザはそのオブジェクトを回収しようとします
channel.close();

// メッセージを閉じる - 方法B
// この方法では、ページがブロードキャストメッセージに応答しないようにするだけで、channelはまだ存在します
channel.removeEventListener('message', function(){})

上記の例では、タブAは my_channel という名前のBroadcast Channelを作成し、メッセージを送信しました。タブBも同じ名前のブロードキャストチャンネルを作成し、メッセージを監視しています。タブAがメッセージを送信すると、タブBは即座にメッセージを受け取り、内容をプリントします。また、上記の方法を使用して接続を閉じることもできます。

注意

Broadcast Channelはセキュリティを確保するのため、**CORS**ポリシーによって制限されています。

Service Worker:強力なオフライン通信ツール

Service Worker は、Web開発における革新的な技術であり、ブラウザの背後で独立して実行されるスクリプトを開発者が利用できるようにします。ネットワークリクエストやキャッシュなどのタスクを処理するために使用されます。オフラインアクセス、パフォーマンスの最適化、プッシュ通知などの機能を提供し、複数のタブ間の通信や協調操作を実現できます。ユーザーがウェブページを閉じても、Service Worker は動作し続けるため、複数ページの協調操作や通信を実現するための強力なツールとなります。

以下は、2つのタブ間で通信するためにService Workerを使用する方法を示す例です:

タブA - Service Worker登録

// Service Worker登録
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw.js')
    .then(function(registration) {
      console.log('Service Worker registered with scope:', registration.scope);
    })
    .catch(function(error) {
      console.error('Service Worker registration failed:', error);
    });
}

タブ B - Service Workderによるメッセージ送信

// メッセージ送信
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.controller.postMessage('Hello from Page B!');
}

Service Worker(sw.js) - メッセージの受信と処理

self.addEventListener('message', function(event) {
  console.log('Received message in Service Worker:', event.data);
  // **メッセージ処理**
});

上の例では、タブAはService Workerに登録し、タブBはService Workerを使ってメッセージを送信し、Service Workerはmessageイベントをリッスンすることでメッセージを受信して処理します。 このアプローチでは、アクティブかどうかにかかわらず、タブ間のリアルタイム通信が可能です。 これは、即時のデータ更新、メッセージプッシュ、オフライン通信を可能にするのに便利です。

注意

Service Workerは、セキュリティを確保するためにHTTPS環境で実行する必要があります。

LocalStorage と window.onstorage: シンプルなローカル・ストレージ通信方法

LocalStorage は、ブラウザページが同じドメイン下でデータを共有できるようにする、シンプルなローカルストレージの形式です。 関連するイベントwindow.onstorageにより、開発者はタブ間の基本的なデータ通信を実装することができます。

タブA - データをLocalStorageに書き込む

// データをLocalStorageに書き込む
localStorage.setItem('message', 'Hello from Page A!');

タブB - LocalStorageの変更をリッスンする

// LocalStorageの変更をリッスンする
window.addEventListener('storage', function(event) {
  if (event.key === 'message') {
    console.log('Received message in Page B:', event.newValue);
  }
});

上記の例では、タブAはデータをLocalStorageに書き込みます。一方、タブBはstorageイベントを監視してLocalStorageの変更をキャッチし、タブAで書き込まれたメッセージを受け取ります。

注意

  • storage イベントはデータ変更を引き起こしたページで発生しません。例えば、ブラウザが同じドメイン内で複数のページを開いている場合、他のページのみが storage イベントを発生させますが、元のページは発生しません。
  • localStoragesessionStorage オブジェクトの両方が storage イベントを発生させます。変更を監視する際には、それらのソースを区別する必要があります。
  • sessionStorage は現在のセッション内でのみ有効であり、ウィンドウやタブを閉じるとデータが消去されます。同じドメインの異なるウィンドウやタブでも、それぞれ独自の sessionStorage データが相互に隔離され、直接共有することはできません。各ウィンドウやタブには独自の sessionStorage オブジェクトがあり、それらのデータは干渉しません。
  • 変更された値が変更されていない場合、 storage イベントは発生しません。
  • 一部のブラウザでは、プライベートモードで localStorage を設定できない場合があります。例えば、Safari では注意が必要です。

window.openとwindow.postMessage:異なるソース間通信の強力なツール

window.postMessage は強力なAPIであり、異なるソースからのスクリプトが非同期で効果的に通信することを可能にします。これにより、テキストドキュメント、複数のウィンドウ、およびクロスオリジンメッセージングの有効な解決策となり、ウィンドウ間のデータ通信によく利用されます。

安全にクロスオリジン通信を実現できます。通常、異なるページのスクリプトは、それらのページが同じプロトコル、ポート、およびホストを持っている場合にのみ相互通信できます。しかし、window.postMessageは、この制限を回避する制御されたメカニズムを提供し、正しく使用すれば非常に安全な方法です。

タブA - ポップアップウィンドウを開いてメッセージを送信する

const popup = window.open('popup.html', 'popupWindow');
popup.postMessage('Hello from the opener!', '<https://example.com>');

popup window(popup.html) - メッセージを受信して返信する

window.addEventListener('message', function(event) {
  if (event.origin === '<https://example.com>') {
    console.log('Received message in popup:', event.data);
    event.source.postMessage('Hello from the popup!', event.origin);
  }
});

Shared Worker:複数のタブで共有されるJavaScriptスレッド

Shared Worker は、複数のブラウザのタブ間で共有されるJavaScriptスレッドです。異なるブラウザのタブ間でデータを共有したり、コードを実行したりするために使用できます。通常のWorkerとは異なり、Shared Workerは通常のWorkerとは異なるインタフェースを持ち、異なるグローバルスコープであるSharedWorkerGlobalScopeを実装しています。これにより、複数のブラウザのタブが同じバックグラウンドスレッドを共有でき、各タブごとに独立した作業スレッドを作成する必要がありません。

タブAタブBはリアルタイムにデータを同期する必要があります。共有されるデータはユーザーのオンライン状態であり、ユーザーが1つのタブでオンライン状態を変更した場合、もう1つのタブも即座に更新される必要があります。

タブA - メッセージ送信

// ページAはShared Workerを作成し、そのWorkerにメッセージを送信する
const worker = new SharedWorker('shared-worker.js');

// Shared Workerからのメッセージを監視する
worker.port.onmessage = function(event) {
  const messageFromWorker = event.data;
  console.log('Received message in A page:', messageFromWorker);
};

// Shared Workerにメッセージを送信する
const messageToWorker = 'Hello from A page!';
worker.port.postMessage(messageToWorker);

タブB - メッセージ受信

// ページBも同じShared Workerに接続し、メッセージを監視します
const worker = new SharedWorker('shared-worker.js');

// Shared Worker からのメッセージを監視する
worker.port.onmessage = function(event) {
  const messageFromWorker = event.data;
  console.log('Received message in B page:', messageFromWorker);
};

shared-worker.js

// shared-worker.js内のコード、メッセージを処理し、それを接続されたすべてのページにブロードキャストするためのもの

// 複数のタブ間の通信を処理するための SharedWorkerGlobalScope オブジェクトを作成します
const globalScope = self;

// ページからの接続イベントを監視します
globalScope.onconnect = function(event) {
  const port = event.ports[0];

	// ページからのメッセージを監視します
  port.onmessage = function(event) {
    const message = event.data;

		// Shared Worker内でメッセージを処理します
    // ここでは、単純にメッセージを接続されたすべてのページにそのままブロードキャストします。
    globalScope.clients.matchAll().then(clients => {
      for (const client of clients) {
        client.postMessage(message);
      }
    });
  };

// ポートを開始する
  port.start();
};

注意

  1. 同源制約: Shared Workerを複数の異なるページに接続する場合、これらのページは同じオリジンである必要があります。つまり、通常はhttpsプロトコル、同じホスト、ポートを持っている必要があります。そうでない場合、ブラウザによってクロスオリジン通信が制限されます。
  2. 通知の問題: Shared Workerはすべてのページに対してアクティブに通知することができないため、クロスページ通信を実現するには、通常はポーリングを使用して新しいメッセージがあるかどうかをチェックする必要があります。これは、リアルタイム通信が必要な場合などにパフォーマンスに影響を与える可能性があります。
  3. イベントリスナーの問題: 一部のブラウザでは、Shared Workerは .addEventListener を使用して message イベントをリッスンすることができません。そのため、Shared Workerのコードを記述する際には、このメソッドの使用に注意し、互換性を確保する必要があります。
  4. 互換性の問題: Shared Workerは一般的に互換性がありますが、異なるブラウザでのサポートレベルは異なります。Shared Workerを使用する際には、異なるブラウザでの互換性をテストし、必要に応じて代替手段を提供する必要があります。

IndexedDB: 効率的なクライアント側データベース

IndexedDB は、クライアント側で大量の構造化データやファイル、バイナリ大きなオブジェクト(blob)を保存するための低レベルAPIです。従来のWeb Storage(localStorageやsessionStorageなど)は少量のデータを保存するために適していますが、IndexedDBは大量の構造化データを扱うための解決策を提供しています。

IndexedDBは、リレーショナルデータベース管理システム(RDBMS)に似ており、トランザクションを使用してデータの読み書きを管理します。JavaScriptベースのオブジェクト指向データベースであり、キーにインデックス付けされたオブジェクトを保存および取得できます。また、構造化クローンアルゴリズムをサポートする任意のオブジェクトを保存できます。データベースのスキーマを指定し、データベース接続を開き、データの取得や更新のための一連のトランザクションを実行するだけです。

IndexedDBの操作は非同期で実行されるため、アプリケーションの実行をブロックすることなくデータベース操作を実行できます。つまり、他のタスクを実行しながらデータベース操作を行うことができ、操作の完了を待つ必要はありません。

タブA:データを書き込む

// IndexedDBデータベースを開く
const request = indexedDB.open('myDatabase', 1);

request.onsuccess = function (event) {
  const db = event.target.result;

	// トランザクションを作成し、オブジェクトストアを取得します
  const transaction = db.transaction('myStore', 'readwrite');
  const store = transaction.objectStore('myStore');

  // データを書き込む
  store.put('Hello from Tab A', 'message');
}

タブB:変更を監視しデータを読み取る

// IndexedDB データベースを開く
const request = indexedDB.open('myDatabase', 1);

request.onsuccess = function (event) {
  const db = event.target.result;

	// トランザクションを作成し、オブジェクトストアを取得します
  const transaction = db.transaction('myStore', 'readwrite');
  const store = transaction.objectStore('myStore');

	// データ変更を監視します
  store.onsuccess = function (event) {
    // IndexedDB からデータを読み取る
    store.get('message').onsuccess = function (event) {
      const message = event.target.result;
      console.log('Received message in Tab B:', message);
    };
  };
}

注意

  1. 同源ポリシー: IndexedDBは同一オリジンポリシーに従います。そのため、同じオリジンのタブのみが同じIndexedDBデータベースにアクセスできます。
  2. データベース名とバージョン番号: すべてのタブは、同じデータベース名とバージョン番号を使用する必要があります。これにより、それらは同じデータにアクセスできます。例では、データベース名は 'myDatabase' でバージョン番号は 1 です。
  3. トランザクション管理: IndexedDBはデータの読み書きを管理するためにトランザクションを使用しているため、トランザクションのスコープやロックを慎重に管理する必要があります。これにより、データの競合や予期せぬ動作を回避できます。
  4. イベントリスナー: IndexedDBのイベントリスナーを使用してデータの変更を検知できます。この例では、 store.onsuccess イベントを使用してデータ変更を監視しています。
  5. ブラウザサポート: IndexedDBはほとんどのモダンブラウザでサポートされていますが、一部の古いバージョンのブラウザでは互換性の問題が発生する可能性があります。

Cookie: シンプルなクライアント側の保存機構

Cookieは、ユーザーのコンピュータに保存され、ブラウザが読み取り、サーバーに送信できる小さなテキストデータの一種です。Cookieは主にクライアントとサーバーの間でセッション状態を維持するために使用されますが、異なるタブ間でデータを伝達するためにも使用することができます。

タブA:データを書き込む

document.getElementById('writeCookie').addEventListener('click', function () {
	// Cookie へのデータの書き込み
  document.cookie = 'sharedData=Hello from Page A';
});

タブB:変更を監視しデータを読み取る

function getCookie(name) {
  const value = `; ${document.cookie}`;
  const parts = value.split(`; ${name}=`);
  if (parts.length === 2) return parts.pop().split(';').shift();
}

// Cookieの変更を監視する
let previousCookie = document.cookie;
setInterval(function () {
  const currentCookie = document.cookie;
  if (currentCookie !== previousCookie) {
    const sharedData = getCookie('sharedData');
    document.getElementById('output').textContent = sharedData;
    previousCookie = currentCookie;
  }
}, 1000); // Cookieを毎秒チェックする

注意

  1. サイズ制限: Cookieには通常、数KBの制限があります。そのため大量のデータを保存するのに適していません。
  2. セキュリティ: Cookie内のデータはクライアント側で閲覧および変更可能であり、敏感な情報を保存するのには適していません。
  3. クロスオリジン制限: 通常、Cookieは同一オリジンポリシーの制限を受けるため、クロスオリジンでの共有ができません。異なるドメイン間での使用はできません。同じドメイン内の異なるタブ間でのみ使用可能です。
  4. 有効期限: Cookieの有効期限を設定できますが、設定しない場合はセッション終了時に削除されます。
  5. クロスドメイン通信: デフォルトでは、Cookieは同じドメイン内でのみ共有されます。異なるドメイン間でCookieを使用する場合は、適切なドメインとパスを設定し、Cookieが必要なドメインとパスでアクセスできるようにする必要があります。
  6. セキュリティ上の考慮事項: Cookie内のデータはユーザーや他のスクリプトからアクセスおよび変更可能です。そのため、敏感な情報を保存するのには適していません。クロスタブ通信を行う際には、データのセキュリティを確保するために、データの暗号化や署名が必要な場合があります。

WebSocket:強力な双方向通信プロトコル

WebSocketは、ブラウザとサーバー間で全双方向通信チャネルを確立するプロトコルです。これにより、ブラウザとサーバー間でリアルタイムで双方向のデータ転送が可能になります。WebSocketは、クロスタブ通信に非常に適しています。低遅延で効率的な通信手段を提供しています。

タブA: データを送信する

// WebSocket接続を作成します
const socket = new WebSocket('ws://example.com/socket');

// Bページにデータを送信します
document.getElementById('sendData').addEventListener('click', function () {
  const data = { message: 'Hello from Page A' };
  socket.send(JSON.stringify(data));
});

タブB:データを受信する

// WebSocket接続を作成します
const socket = new WebSocket('ws://example.com/socket');

// WebSocketメッセージを監視します
socket.addEventListener('message', function (event) {
  const data = JSON.parse(event.data);
  document.getElementById('output').textContent = data.message;
});

使用シナリオ:

WebSocketは、リアルタイムな通信が必要な場面で利用されます。例えば、オンラインチャット、リアルタイムゲーム、共同編集、リアルタイムデータの更新などが挙げられます。また、WebSocketはタブ間通信の一種として、異なるタブにデータをリアルタイムで同期するために使用することもできます。

注意

  • サーバーサイドでWebSocketサーバーを設定して、WebSocket接続を処理する必要があります。
  • WebSocket接続はセキュリティを確保するために通常、暗号化された通信を行うためにwssプロトコルを使用します。
  • WebSocketは長期の接続を行うため、接続の管理には慎重に対応する必要があり、不必要なリソースの使用を避ける必要があります。
  • WebSocket通信は双方向のデータ転送を可能にしますが、データの形式や解析には適切な処理が必要です。
  • ネットワークの不安定性や切断時の再接続メカニズムを考慮する必要があります。

まとめ

使用シナリオの比較

通信手段 使用シナリオ
Broadcast Channel 全ての開いているタブにリアルタイムにメッセージを配信する必要があるシナリオに適しています
Service Worker オフラインアクセス、プッシュ通知、および複数のタブ間通信に適しています
LocalStorage データ共有とリアルタイムな更新が必要なシナリオに適しています
window.open / postMessage データやメッセージを直接送信する必要がある場面に適しています
Shared Worker リアルタイムな更新とインタラクションが必要なアプリケーションに適しています
IndexedDB 複数のタブ間で複雑なデータ共有を行う場合に適していますが、リアルタイム性が低いシナリオです
Cookie リアルタイム性が低い、ユーザー設定などの小規模なデータ共有に適しています
WebSocket リアルタイム通信と複数のタブでの協調操作に適しています

跨域サポートの比較

通信手段 主ドメインが一致 主ドメインが一致しているが、サブドメインが異なる 主ドメインが異なる メインドメインおよびサブドメインの両方が一致していな
Service Worker
Shared Worker
IndexedDB
Cookie
LocalStorage
Broadcast Channel
window.open / postMessage
WebSocket

跨域列字段说明:

ケース タブA タブB
主ドメインが一致 www.ptengine.jp www.ptengine.jp
主ドメインが一致しているが、サブドメインが異なる blog.ptengine.jp help.ptengine.jp
主ドメインが異なる www.ptengine.jp www.ptengine.com
メインドメインおよびサブドメインの両方が一致していな blog.ptengine.jp help.ptengine.com

ブラウザの互換性の比較

通信手段 Chrome Firefox Safari Edge Opera IE
Broadcast Channel 54+ 38+ 10.1+ 16+ 41+
Service Worker 40+ 44+ 17+ 24+
LocalStorage 4+ 3.5+ 4+ 8+ 10.5+ 8+
window.open / postMessage 1+ 1+ 4+ 8+ 15+ 8+
Shared Worker 4+ 29+ 17+ 24+
IndexedDB 23+ 10+ 12+ 15+
Cookie
WebSocket 4+ 11+ 12+ 15+ 10+

性能比較

性能は使用状況に応じて異なります。通信頻度、データ量、ネットワーク環境などの要因によって異なります。開発者は具体的なニーズに基づいて適切な通信手段を選択する必要があります。

通信手段 性能 comment
BroadCast Channel 高性能、リアルタイムなメッセージ配信 全ての受信者がオンラインである必要があり、メッセージの保存をサポートしていません。
Service Worker 高性能でオフライン通信をサポートしています。 バックグラウンドで実行され、ブラウザがService Workerをサポートしている必要があり、HTTPSである必要があります。登録されたバックグラウンドスレッドのライフサイクルを管理する必要があります。
LocalStorage 低性能でリアルタイム通信に適していません。 主に静的データを保存するために使用され、リアルタイム通信には適していません。
window.open / postMessage 中程度の性能で、ウィンドウ間の通信に適しています。 新しいウィンドウを開き、小規模なウィンドウ間通信に適していますが、大量のメッセージ交換には適していません。
Shared Worker 高性能で、複数のタブ間通信に適しています。 Shared Workerのブラウザサポートが必要であり、大規模なタブ間通信に適しています。リアルタイム通信を実現するためには、追加のポーリングや定期的なタスクが必要です。
IndexedDB 高性能であり、大規模なデータの保存とリアルタイム通信に適しています。 大量の構造化データの保存に適しており、非同期操作が必要で複雑です。
Cookie 低性能で、小規模なデータの保存に適しています。 シンプルなデータの保存に適していますが、リアルタイム通信には適しておらず、Cookieのサイズ制限に制約されます。
Websocket 高いパフォーマンスでリアルタイム通信に適しています。 WebSocketサーバーのサポートが必要であり、リアルタイム通信に適していますが、やや複雑です。

最後に

本文には、様々なタブ間通信手法を探究しました。それぞれの手法には独自の特性と適用シナリオがあり、異なる要求に対応することができます。タブ間通信は現代のWeb開発において重要な役割を果たしており、より豊かなユーザーエクスペリエンスと機能を実現することができます。

これらの通信手法を理解することで、具体的な状況に応じて適切な手法を選択し、より強力で協調性のあるWebアプリケーションを構築することができます。今後もWeb開発における通信ツールが進化し、ユーザーにより良いサービスとエクスペリエンスを提供していくことを期待しています。

お読みいただきありがとうございました。何かご質問やご意見がございましたら、お気軽にお知らせください。Web開発の成功を祈っています!

10
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?