iframe 要素で読み込んだページから、外側のページ (親ページ) の要素を書き換えたり、関数を呼んだりしたいことがあります。iframe の中の高さに合わせて枠の大きさを変える、iframe の中のボタンで親ページのダイアログを閉じる、といった場面です。ここでは、そのための window.parent の使い方と、使えない場合の postMessage() を説明します。

window.parent と window.top

iframe の中のページでは、window.parent が親ページの window を指します。iframe が何重にも入れ子になっているときは、window.top がいちばん外側のページの window を指します。

// iframe の中のページで実行する
if (window.parent !== window) {
  // iframe の中で表示されている
  const title = window.parent.document.querySelector("h1");
  title.textContent = "iframe の中から書き換えました";
}

iframe の中でないときは、window.parent は自分自身を指します。上の例のように比べれば、iframe の中で表示されているかを確かめられます。逆に、親ページの側から iframe の中を操作するときは、iframe 要素の contentWindow や contentDocument を使います。iframe の中のページは、親ページとは別の文書として扱われます (詳しくは iframe の中のページについての記事を参照してください)。

オリジンが違うと、親ページの DOM には触れない

window.parent.document で親ページを操作できるのは、親ページと iframe の中のページが「同じオリジン」のときだけです。オリジンは、スキーム (https など)、ホスト名、ポート番号の組のことで、どれか 1 つでも違えば別のオリジンになります。

親ページiframe の中同じオリジンか
https://example.com/https://example.com/frame.html同じ
https://example.com/https://www.example.com/frame.html違う (ホスト名)
https://example.com/http://example.com/frame.html違う (スキーム)

オリジンが違うと、window.parent.document を読もうとした時点でエラー (SecurityError) になります。これは「同一オリジンポリシー」と呼ばれるブラウザーの安全の仕組みです。どのサイトでも iframe で読み込めるため、この制限が無いと、悪意のあるページが、利用者のログインしているサイトを iframe で開いて中身を盗み見ることができてしまいます。

古い IE で起きていたエラー

IE6 などの古い Internet Explorer では、同じオリジンでも、iframe の中から親ページを操作したときにエラーになることがありました。よくあった原因は、実行のタイミングです。

iframe の中のページは、親ページとは別に読み込まれます。iframe の中のスクリプトが動いた時点で、親ページの読み込みがまだ終わっておらず、呼ぼうとした関数や要素がまだできていない、ということが起こりえました。古い IE では、読み込みや実行の順番がほかのブラウザーと違うことがあり、ほかのブラウザーでは動くのに IE でだけエラーになる、ということが起きていました。当時は、親ページの読み込みが終わってから親の側で iframe の中を呼ぶ、関数があるかを確かめてから呼ぶ、といった回避策がとられました。

今のブラウザーでも、読み込みのタイミングによって同じことは起こりえます。どちらが先に読み込まれても動くように作ることが大切です。

今は postMessage でやりとりする

オリジンが違うページどうしでも、postMessage() を使えば、メッセージを送り合えます。相手の DOM を直接操作するのではなく、「こうしてほしい」というデータを送り、受け取った側が自分で処理する形です。同じオリジンの場合も、この形にしておくと、ページどうしの結び付きが弱くなり、作りを変えやすくなります。

iframe の中のページから、自分の高さを親ページに知らせる例です。

// iframe の中のページ (https://widget.example.com/)
function sendHeight() {
  window.parent.postMessage(
    { type: "resize", height: document.documentElement.scrollHeight },
    "https://example.com" // 受け取ってよい相手のオリジン
  );
}
new ResizeObserver(sendHeight).observe(document.body);

親ページでは、message イベントで受け取ります。

// 親ページ (https://example.com/)
const frame = document.querySelector("#widget");

window.addEventListener("message", (event) => {
  // 送り主のオリジンを必ず確かめる
  if (event.origin !== "https://widget.example.com") return;
  if (event.source !== frame.contentWindow) return;

  const data = event.data;
  if (data && data.type === "resize" && Number.isFinite(data.height)) {
    frame.style.height = data.height + "px";
  }
});

安全に使うために、次の点を守ります。

  • 受け取る側では、event.origin で送り主のオリジンを必ず確かめる。確かめないと、どのページからのメッセージでも受け入れてしまう
  • 送る側では、第 2 引数に相手のオリジンを指定する。"*" にすると、相手が意図しないページ (たとえば、このページを勝手に iframe で埋め込んだ別のサイト) でも送ってしまうので、大事な情報を送るときは使わない
  • 受け取ったデータは、形と値を確かめてから使う。innerHTML に入れたり、eval() に渡したりしない

まとめ

  • iframe の中から親ページを指すのは window.parent、いちばん外側は window.top
  • 親ページの DOM を直接操作できるのは、同じオリジンのときだけ
  • 古い IE では、読み込みのタイミングによってエラーが起きやすかった。今も、読み込みの順番に頼らない作りにする
  • オリジンが違うときは postMessage() を使い、受け取る側で event.origin を必ず確かめる

参考