JavaScriptで動的にscriptタグを差し込んだり、実行する方法
こんにちは、@s_tsukaです。今回は珍しくJavaScriptネタでいこうと思います。
自分のJS力はそれほど高くないですし、ブラウザやW3Cの仕様に詳しくないですが、scriptタグはハマりポイントだと思うので、書いておきます。(といっても今回のは一般的ではないです)
今回は生のJSを扱います。(最近はAngularや、Reactが流行っていますが、未だに生JSのニーズもあるんですよ)
結論から言うと、単一scriptタグならappendChild、複雑なタグならdocument.write + iframeです。
scriptタグ
このページにたどり着いた読者には説明不要かと思いますが・・・
scriptタグをHTMLファイル上に記述し、ブラウザに読み込ませると、そのscriptが起動します。例えばこんな感じで。
<html>
<head>
<script>
alert(1);
</script>
</head>
<body>
sample
</body>
</html>
このようなHTMLを開くと画面上にブラウザのダイアログが出現して1と表示されます。
ブラウザはHTMLをDLすると上からparseを開始します。parseを行いつつ、DOM構築を行いますので、通常scriptタグはDOM構築をブロックしないようにbodyの最下部に記述します。ですが、jsファイルをDLしないとページが動作しない、あるいはなるべく早くJSを動かしたい、という場合はheadタグ内に<script src=“http://foo.js…(略) というように記述します。
ブロック考慮=body最下部 早めにDLしておきたい=head内
という感じですね。
動的にscriptタグを生成、追加する方法
通常であればscriptタグはHTML上に直接記述するか、あるいは何らかのテンプレートエンジンによって(scriptタグを含む)HTMLを生成し、それをブラウザへ返却するか、scriptのsrc属性を使って外部ファイルを読み込むと思います。
つまり、(一度ページが描画された後で)動的にscriptタグを生成して、実行する、などはあまりないと思います。(一部では常識的に使われますが)
前述の動的でないケースであれば特に問題はありませんが、場合によってはそのブラウザにDLさせたHTMLからscriptを起動し、そのJavaScriptコードによって、scriptタグをHTML上に挿入する、(そしてそのscriptを起動)ということを行うかと思います。
文章だと難しいですが、イメージだとこんな感じです。id=abcの部分にelementを挿入したい、という感じです。
<html>
<head>
</head>
<body>
<span>Insert here! [start]</span>
<div id="abc">
</div>
<span>Insert here! [end]</span>
<script>
var ele = document.getElementById("abc");
ele.innerHTML = "<script>alert(123);<\/script>";
</script>
</body>
</html>
ちなみにこれはダメな例です。正しく動きません。
意図はわかっていただけるかと思います。動的にscriptタグを挿入して、実行したいのです。
こういう場合はappendChildメソッドを利用します。script elementを生成し、これをappendChildするとうまく動きます。
<html>
<head>
</head>
<body>
<span>Insert here! [start]</span>
<div id="abc">
</div>
<span>Insert here! [end]</span>
<script>
var s = document.createElement("script");
s.innerHTML = "alert(123)";
var ele = document.getElementById("abc");
ele.appendChild(s);
</script>
</body>
</html>
実際に実行すると123が出力されると思います。
余談ですが、この静的に記述されている部分のscriptをheadの部分に記述するとscriptが動くタイミングではまだDOM parseができておらず、id 123が見つからなくてエラーになります。なのでbodyの下の方に書いています。
動的にscriptタグを含むような複雑なDOMを生成、追加する方法
上記のscriptタグが1つだけの場合は簡単ですが、scriptタグが複数あったり、divタグの中にscriptタグがあったり、そもそもそれらの構造が一定じゃなくてどうDOM構築すりゃいいんだ・・・的な状況になった場合、一気に難しくなります。
例えば上記の例を真似て、以下のコードを動かしてみます。
<html>
<head>
</head>
<body>
<span>Insert here! [start]</span>
<div id="abc">
</div>
<span>Insert here! [end]</span>
<script>
var d = document.createElement("div");
d.innerHTML = "<script>alert(123);<\/script>";
var ele = document.getElementById("abc");
ele.appendChild(d);
</script>
</body>
</html>
これは動的にdivを生成して、appendChildしています。みなさんは既にappendChildでscriptが動くことを知っています・・・が!残念ながらこの場合はscriptは動きません。
これに対応する方法は2つあります。(実際はもう少し他にもあると思いますが)
- appendするタグの中身を解析して、scriptがあった場合、逐一appendChildで1つずつ挿入する(構造を壊さないように注意しつつ)
- document.writeを使ってHTML片を出力する
appendの方はdivのparseなんてしてられるか!という感じですね。
document.writeの場合は、そのscriptタグの場所にdocumentを書く、という挙動になる上に、document.writeはブロッキングしますので、画面描画が一気に遅くなります。
なかなか良い方法は無いのですが、
「一時的にiframeを作成し、そこにdocument.writeでHTMLを書き出し、実行させる」という方法が良いかと思います。
例えばこんな風に。
<html>
<head>
</head>
<body>
<span>Insert here! [start]</span>
<div id="abc">
</div>
<span>Insert here! [end]</span>
<script>
var iframe = document.createElement('iframe');
iframe.setAttribute("style", "display:none");
document.getElementById("abc").appendChild(iframe);
var doc = iframe.contentWindow.document;
doc.open();
doc.write("<div><script>alert(123);<\/script><\/div>");
doc.close();
</script>
</body>
</html>
iframeは不可視なので表示されず、scriptを実行することができました。
ちなみにこの例は簡略化しています。もし「id=abcの部分にdivを挿入したいのであって、iframeは挿入したくない!」という人がいたら、適宜elementの移動などで調整してください。
iframeにはonLoadを付けることができますので、iframe内のscriptなどの実行完了を待って何かを行うことも可能です。
本来はこういうコードは書きたくない・・・と思う人もいるかと思いますが、諸事情により書かなくてはならないときもあります。そんなときに今回のような方法で実現すると良いかと思います。
以上です。