🍌

ブラウザの新規タブへのデータ転送する方法

に公開3

ウェブ開発をしていると、新しいタブを開きながらデータを転送する必要がある状況によく遭遇します。最近のプロジェクトでも似たような要件がありました。商品一覧ページで、ユーザーが選択した最大500個の商品IDを新しいタブの詳細ページに転送する必要がありました。各IDは10桁の文字列で、新しいタブではこれらのIDを使ってAPIを呼び出し、詳細情報を取得する必要がありました。

簡単に見えるこの作業は、想像以上に多くの悩みをもたらしました。500個のID、最大で約5,000文字になるデータを、どのように転送するのが最も効率的でしょうか?

検討過程:各手法の限界

複数の手法を検討する中で、それぞれの限界を発見しました。

URLパラメータ

誰もが最初に思い浮かべる方法は、URLにデータを載せて送ることです。実装も簡単で直感的です。

const selectedIds = ['0000000001', '0000000002', ... /* 498個 */];
const url = `/detail?ids=${selectedIds.join(',')}`;
window.open(url, '_blank');

主要ブラウザのURL長制限は以下の通りです:

ブラウザ URL最大長 表示制限 出典
Chrome 2MB[1] 32kB (アドレスバー) Chromium公式文書
Edge 2MB[1:1] 32kB (アドレスバー) Chromiumベース
Firefox 無制限[2] 65,536文字 (アドレスバー) Mozillaコミュニティ
Safari ~80,000文字[3] - テストベース

RFC 3986ではURL長に対する明示的な制限を設けていませんが[4]、実務では互換性のためURL全体を2,048文字以下に保つことが推奨されています[5]。最大500個のIDは約5,000文字で、この推奨値を2倍以上超える長さでした。

さらに重要な問題もありました:

  • セキュリティとプライバシーの問題: 長いURLがサーバーログ、ブラウザ履歴、分析ツールにそのまま記録され、ビジネスロジックが含まれたIDリストが様々な場所に露出される
  • サーバー互換性の問題: HTTP標準の414 (URI Too Long)ステータスコード発生の可能性[6]とウェブサーバー(Apache、Nginx)別のURL長制限設定の違い

URLパラメータは少量の識別子転送にのみ適した方法でした。5,000文字というデータサイズには別のアプローチが必要でした。

ブラウザストレージ:localStorage vs sessionStorage

次に思い浮かんだのは、ブラウザのストレージを活用する方法でした。Web Storage APIには2つのオプションがあります。

localStorageは同じドメインのすべてのタブからアクセス可能な永続ストレージで、ほとんどのブラウザで5-10MBまで保存可能です[7]sessionStorageはタブ別に隔離されたストレージで、タブを閉じるとデータが削除されます[8]。500個のID(約5KB)は両方のストレージで十分に収容できるサイズです。

しかし、ここで重要なセキュリティ問題を発見しました。商品IDはビジネスに敏感な情報だという事実です。競合他社が我々の商品リストを把握したり、ユーザーの閲覧パターンを追跡できるデータです。ブラウザストレージは開発者ツールで簡単にアクセス可能で、XSS攻撃に露出されるリスクがあるため、敏感なデータ保存には不適切でした。社内管理サービスであってもこのような危険な方法は選択したくありませんでした。

特にsessionStorageは新しいタブからアクセスできないという致命的な制限がありました。window.openで開いた新しいタブは、親タブのsessionStorageにアクセスできないため、我々の用途には使用できませんでした。

// localStorage使用例(セキュリティ上不適切)
// Aページでの保存
localStorage.setItem('selectedProducts', JSON.stringify(selectedIds));
window.open('/detail', '_blank');

// Bページでの読み取り
const ids = JSON.parse(localStorage.getItem('selectedProducts'));
if (ids) {
  loadProductDetails(ids);
}

// sessionStorageは新しいタブからアクセス不可
// 新しいタブは独立したセッションコンテキストを持つため、親タブのsessionStorageにアクセスできない

また、ユーザーが詳細ページのURLをブックマークしたり直接アクセスしたりすると、データがなくて空の画面が表示される問題もありました。もし敏感でないデータなら、localStorageも良い選択肢になれたでしょうが、セキュリティが優先の状況では他の方法を探す必要がありました。

postMessage:安全だがタイミングの問題

ストレージ方式のセキュリティ問題を避けたくて、直接通信方法を探しました。postMessageはHTML5で導入された安全なクロスオリジン通信APIで、ウィンドウ間で直接メッセージを転送できます[9]。Structured Clone Algorithm[10]を使用して複雑なオブジェクトも安全に送信でき、理論的にはデータサイズ制限はありません。

// Aページ
const detailWindow = window.open('/detail', '_blank');
setTimeout(() => {
  detailWindow.postMessage(selectedIds, window.location.origin);
}, 300);

// Bページ
window.addEventListener('message', (event) => {
  if (event.origin !== window.location.origin) return;
  if (Array.isArray(event.data)) {
    loadProductDetails(event.data);
  }
});

しかし、タイミングの問題が大きな懸念事項でした。新しいタブが完全にロードされる前にメッセージを送ると受信できず、遅すぎるとユーザーが空の画面を見ることになります。

これはウェブでよく発生する**レースコンディション(Race Condition)**問題です。2つ以上の非同期作業(新しいタブのロードとメッセージ送信)が同時に実行される時、実行順序によって結果が変わる状況を指します。我々の場合は「新しいタブがいつロード完了するか」と「メッセージをいつ送るか」のタイミングを正確に合わせることが困難という問題でした。

BroadcastChannel:簡単だがブラウザサポート制限

postMessageのタイミング問題を解決する別の方法として、BroadcastChannel APIも検討しました。これは同じオリジンのすべてのブラウジングコンテキスト(タブ、ワーカー、iframe)間でメッセージをブロードキャストできるAPIです[11]。postMessageと異なり特定のターゲットを指定する必要がなく、pub-subパターンを簡単に実装できます。

// Aページでブロードキャスト
const channel = new BroadcastChannel('product-data');
channel.postMessage(selectedIds);

// Bページで受信
const channel = new BroadcastChannel('product-data');
channel.onmessage = (event) => {
  if (Array.isArray(event.data)) {
    loadProductDetails(event.data);
  }
};

Chrome 54(2016年)から完全サポートされているため、大体互換性問題はないと見ることができ、タイミング問題も簡単な遅延で解決できます。

IndexedDB:強力だが過度な複雑性

最後にIndexedDBも検討しました。これはW3Cで標準化されたブラウザ内蔵NoSQLデータベースで、構造化された大容量データを非同期的に保存できます[12]。トランザクションをサポートし、GB単位のデータも安定的に処理できます。

// IndexedDBにID配列を保存
const request = indexedDB.open('ProductDatabase', 1);
request.onsuccess = (event) => {
  const db = event.target.result;
  const transaction = db.transaction(['products'], 'readwrite');
  const store = transaction.objectStore('products');
  store.put({ id: 'selectedIds', data: selectedIds });
};

しかし、500個のID(5KB)を保存するためにトランザクションとスキーマを管理するのは過度な設計(Over-engineering)に該当しました。またIndexedDBもクライアントストレージなので、同じセキュリティの懸念がありました。

最終選択:BroadcastChannel API

各方法を検討した結果、Chrome環境ではBroadcastChannelが最も適切な解決策でした。セキュリティはpostMessageと同等でありながら実装ははるかに簡単で、タイミング問題も簡単に解決できます。

簡単で安全な実装

// Aページ:ID転送
function openDetailPageWithIds(selectedIds) {
  const channel = new BroadcastChannel('product-data');
  window.open('/detail', '_blank');
  
  setTimeout(() => {
    channel.postMessage(selectedIds);
    channel.close();
  }, 500);
}

// Bページ:ID受信とAPI呼び出し
const channel = new BroadcastChannel('product-data');

channel.onmessage = async (event) => {
  const ids = event.data;
  
  if (Array.isArray(ids)) {
    const response = await fetch('/api/products/details', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ ids })
    });
    
    const products = await response.json();
    renderProductList(products);
  }
  
  channel.close();
};

// 使用例
// Aページで500個のID転送
const selectedIds = ['0000000001', '0000000002', /* ... 498個 */];
openDetailPageWithIds(selectedIds);

BroadcastChannel方式の核心的な利点

  1. セキュリティ優先: 敏感な商品IDがブラウザストレージに露出されない
  2. 実装簡素: 複雑な再試行ロジックやウィンドウ参照管理が不要
  3. 同一オリジンセキュリティ: 同じドメインでのみ通信可能
  4. 拡張性: 複数のタブに同時転送可能
  5. Chrome完全サポート: Chrome 54(2016年)から安定サポート

選択の基準と学んだこと

500個のIDを転送する道のりを通じて、いくつかの重要な教訓を得ました。

まずデータサイズの重要性です。500個 × 10文字 = 5,000文字はURLでは無理ですが、他の方法では十分に可能なサイズでした。問題を正確に定義することが解決策選択の第一歩です。

次に完璧な方法はないという点です。URLパラメータは簡単ですが長さ制限があり、localStorageは容量は十分ですがセキュリティの懸念があり、postMessageは安全ですがタイミング問題があります。各方法にはトレードオフがあり、状況に応じて選択または組み合わせる必要があります。

過度なエンジニアリングを警戒すべきという点も重要でした。5KBデータにIndexedDBは過剰で、簡単な問題には簡単な解決策の方が適しています。

最後にデータの機密性を見過ごしてはいけないという点です。最初はlocalStorageが便利に見えましたが、商品IDがビジネスに敏感な情報だと気づき、セキュリティを優先したBroadcastChannel方式を選択することになりました。技術的便利性よりもセキュリティが優先です。

ブラウザタブ間のデータ転送は単純に見えますが、実際には多くの考慮事項があります。URLパラメータの限界、localStorageのセキュリティリスク、postMessageのタイミング問題、BroadcastChannelの互換性まで。しかし、Chrome環境で敏感なデータを扱うなら、BroadcastChannelが最もバランスの取れた選択だと思います。


参考文献

脚注
  1. Guidelines for URL Display - Chromium - Chrome/Edge公式URL長制限(2MB) ↩︎ ↩︎

  2. What is the maximum length for a URL query string in Firefox? - Mozilla Support - Firefox URL長情報 ↩︎

  3. What is the maximum length of a URL in different browsers? - Stack Overflow - ブラウザ別URL長テスト結果 ↩︎

  4. RFC 3986 - Uniform Resource Identifier (URI): Generic Syntax - URI標準規格 ↩︎

  5. What is the maximum possible length of a query string? - Stack Overflow - 2,048文字推奨の根拠 ↩︎

  6. 414 URI Too Long - MDN Web Docs ↩︎

  7. Web Storage API - MDN Web Docs ↩︎

  8. Window.sessionStorage - MDN Web Docs - タブ別独立ストレージ、新しいタブからはアクセス不可 ↩︎

  9. Window.postMessage() - MDN Web Docs ↩︎

  10. Structured Clone Algorithm - MDN Web Docs - オブジェクトを安全に複製・転送するためのブラウザ内蔵アルゴリズム ↩︎

  11. Broadcast Channel API - MDN Web Docs ↩︎

  12. IndexedDB API - MDN Web Docs ↩︎

Discussion