アナログCPU:5108843109

ゲームと音楽とプログラミング(酒と女とロックンロールのノリで)

('ω') < イザユケエンジニャー

iframeのデメリットを無理矢理消化した実装(JS版)

iframeの二大デメリットといえば
「URLが切り替わらない(常にフレームの外側ページのもの)」
「内側ページのURLを直接叩くとフレームが出ない」
なんですが
これを解決しつつもiframeしたいと言われたのでちょっと考えました。

しかも新規開発ではなく既存のサイトにiframeを付けるという改修だったので
既存機能への影響や諸々のコストを考えて
JSのみで解決したいな…と思ってやってみたバージョン。

…ただ、あんまり格好良くなかったので、実務ではボツにしました。
JS+PHPSmarty版で実装したのがこちら。

JS版も使えなくはなさそうなので残しておきます。

JavaScriptは基本的にjQuery前提で書いています

まずは完成サンプル

フレームページ(以下、親)と
iframe内側のコンテンツページ2つ(以下、子1/子2)の
計3ファイルを用意します。

親(frame_parent.html)

ベタ書きでスタイル付けているのは見やすくするためだけのものなので消してOK。

<HTML>

<!-- header -->
<div style="height: 100px; background-color: #CCC">
  <div>
    <span><a href="frame_parent.html">親の再読み込み</a></span>
    <span><a href="frame_child_1.html" target="content_iframe">子1</a></span>
    <span><a href="frame_child_2.html" target="content_iframe">子2</a></span>
  </div>
</div>

<!-- iframe -->
<iframe id="content_iframe" name="content_iframe" src="frame_child_1.html" style="width: 100%; height: calc(100% - 100px); border: 0;"></iframe>

<JS>

$('#content_iframe').load(function(){
    var contents = $("#content_iframe").contents();
    var url = contents[0].URL;
    var domain = "//" + contents[0].domain;
    var url_child = url.slice(url.indexOf(domain) + domain.length);

    history.replaceState("", "", url_child);
});

var child = getParam("child");
if (child != null && child != ""){
    $("#content_iframe")[0].contentDocument.location.replace(child);
}

function getParam(name) {
    var url = window.location.href;
    name = name.replace(/[\[\]]/g, "\\$&");
    var regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)");
    var results = regex.exec(url);
    if (!results) return null;
    if (!results[2]) return '';
    return decodeURIComponent(results[2].replace(/\+/g, " "));
}
子1/子2(frame_child_1.html / frame_child_2.html)

<HTML 子1>

<div>子1</div>
<div><a href="frame_child_2.html">→子2へフレーム内遷移</a></div>

<HTML 子2>

<div>子2</div>
<div><a href="frame_child_1.html">→子1へフレーム内遷移</a></div>

<JS 共通>

if(window == window.parent) {
    var childUri = location.pathname + location.search;
    location.href = "frame_parent.html?child=" + childUri;
}


以下は解説・メイキングです。

とりあえず親となるiframeページを用意する

HTML部分は完成品と同じなので省略します。そちらを参照。

ポイントは以下。

  • iframeにname(content_iframe)を付けている
    • 同名のidも付けている。あとで使う。
  • 共通部分からの子1・子2へのリンクtargetにはiframeの名前を指定
    • これでフレームの中に表示する。省略するとページ遷移が起こる(親へのリンク部分参照)
  • iframe内にはデフォルトで子1を表示するよう指定

子となるページを用意する

これもHTML部分は完成品と同じなので省略。
こちらは区別がつけば何でもOK。
一応、確認用にリンクタグを設置。target省略でフレーム内遷移します。

親と子が揃ったら簡単に動作を確認。
各リンクを踏んで、意図通りであればOK。

親のURLを子のものに書き換える

リンク遷移は正常ですが、
当然ながらブラウザに表示されているURLは常に親のものです。

これを、ページ遷移が起こるごとに子のものに書き換えてみます。

まず、iframeとか関係なく
URLを書き換えるだけのJSはこちら。ルートを意識して書きます。

// example.com/test/fuga.html → example.com/test/hoge.html
history.replaceState("", "", "hoge.html");

// example.com/test/fuga.html → example.com/hoge.html
history.replaceState("", "", "/hoge.html");

これをベースにしつつ、実際は子のURLに書き換えたいので
必要な処理をがつがつ加えます。
<親(JS)>

// iframeの中身を取得
var contents = $("#content_iframe").contents();
// iframeの中身のURLとドメイン部分を取得
// ドメイン部分は後で文字列操作するので「//」を加えておく
var url = contents[0].URL;
var domain = "//" + contents[0].domain;
// ドメインより後の部分を切り出す(example.com/test/hoge.html → /test/hoge.html)
var url_child = url.slice(url.indexOf(domain) + domain.length);
// 切り出したURLで書き換える
history.replaceState("", "", url_child);

これを親のHTML内に置いて再読み込みしてみると、
デフォルトで表示される子1のURLに書き換わります。

さらに子が切り替わるたびにこのスクリプトを動作させたいので、
「iframeの中身が読み込まれたら実行する」という処理にします。
<親(JS)>

$('#content_iframe').load(function(){
    var contents = $("#content_iframe").contents();
    var url = contents[0].URL;
    var domain = "//" + contents[0].domain;
    var url_child = url.slice(url.indexOf(domain) + domain.length);
    history.replaceState("", "", url_child);
});

一番上と一番下の行が追加部分です。
これで各リンクを踏んでみると、意図したとおり、
子の読み込み時にURLが切り替わるようになりました。

子のURLに直接アクセスされたときにフレームを表示する

厄介なのがここ。

まずは親ページにリダイレクトさせる

これはめちゃくちゃ簡単。
子に以下のJSを設置するだけ。
<子(JS)>

if(window == window.parent) {
    location.href = "frame_parent.html";
}

「自身がiframeの中に入っていない場合、親にリダイレクト」をしています。
場合によってはこれで充分かもしれませんが、
今回は指定されたURLの子ページを表示させたいのでもう少し考えます。

子から親に、自身の情報を渡す

先ほどのJSを少し書き換えて、リダイレクト時に自身のパスを渡してみます。
<子(JS)>

if(window == window.parent) {
    // ドメインより後の部分を取得
    var childUri = location.pathname + location.search;
    location.href = "frame_parent.html?child=" + childUri;
}

リダイレクトした後に親側でReferrerで子ページのURLを取得することもできますが、
必ずしもフレームを付ける処理を行いたいとも限らないので、
明示的に渡すようにしました。
(例えば、フレームなしのログインページから
 フレームありのデフォルト親ページに遷移したいとき、
 ログインページにフレームを付けられても困る)

親側で、子の情報を受け取った場合のみiframeの中身を書き換える

子から渡したパラメータ「child」ですが、
親側でこれを確認できた場合のみ、iframeの中身をそれに書き換える、という方法をとります。

パラメータ取得関数「getParam」は詳しい解説を省略しますが、
例えば「?hoge=abc&fuga=def」のようなパラメータをうまいこと分割してくれるやつです。
今回は、「?child=frame_child_2.html」を
「childというパラメータの中身はframe_child_2.html」と認識するために使っています。

<親(JS)>

// 渡されたパラメータを取得
var child = getParam("child");
// パラメータが存在する場合、iframeの中身を書き換える
if (child != null && child != ""){
    $("#content_iframe")[0].contentDocument.location.replace(child);
}

// パラメータ取得関数(stackoverflowより拝借)
// https://stackoverflow.com/questions/901115/how-can-i-get-query-string-values-in-javascript
function getParam(name) {
    var url = window.location.href;
    name = name.replace(/[\[\]]/g, "\\$&");
    var regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)");
    var results = regex.exec(url);
    if (!results) return null;
    if (!results[2]) return '';
    return decodeURIComponent(results[2].replace(/\+/g, " "));
}
動作確認して完了

URLを直接指定したときにチラチラして鬱陶しいですが
(指定された子を表示→親にリダイレクト→親のiframe内書き換え)
通常のフレーム内遷移では問題ないので、
直接指定される機会が少ない場合はアリかなと思いました。