w3c

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などの実行完了を待って何かを行うことも可能です。

本来はこういうコードは書きたくない・・・と思う人もいるかと思いますが、諸事情により書かなくてはならないときもあります。そんなときに今回のような方法で実現すると良いかと思います。

以上です。