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を必ず確かめる