後からロードした外部JSファイルでdocument.writeする(2)

※1157010937の回答の4/4

今まで挙げた問題点をふまえ、以下の要望を満たす方法を考えます。

  • HTMLファイルのロード後に外部JSファイルを読み込む
  • 読み込む外部JSファイルの<script>タグを含んだHTMLのソースが、おおもとのネタ
  • 外部JSファイルは、読み込まれたときにdocument.writeする
  • 外部JSファイルのdocument.writeは、その<script>タグの位置に出したい

けっこう厳しい要求ですが、以下サンプルです。(前回のwriter.jsも使います)


<html>
<head>
<script type="text/javascript"><!--



// 擬似応答電文
var text = '<div>JSファイルの直前<script type="text/javascript" src="./writer.js" charset="Shift_JIS"></' + 'script>JSファイルの直後</div>';




// 以前の方法
function AjaxWriter_Old(str) {
  var targetObj = document.getElementById("target");
  targetObj.innerHTML = str;
}



// JSファイルをHTMLロード後に読み込む方法(IE△)
function ActiveScriptLoad(str) {
  var targetObj = document.getElementById("target");
  var dummyObj = document.createElement("div");
  dummyObj.innerHTML = str;
  targetObj.appendChild(dummyObj);
}



// 解決方法
function AjaxWriter_New(str) {
  var dummyObj = document.createElement("div");
  dummyObj.innerHTML = str;
  var scripts = [];
  (function(node) {
    var children = node.childNodes;
    for(var i = 0; i < children.length; i++) {
      var child = children[i];
      if(child.nodeName.toLowerCase() == "script") {
        scripts[scripts.length] = {src : child.src, type : child.type, charset : child.charset, language : child.language, defer : child.defer};
      } else {
        arguments.callee(child);
      }
    }
  })(dummyObj);
  var name = "ScriptFilePoint";
  var tags = str.replace(/<script.*?<\/script>/ig, '<span name="' + name + '"></span>');
  var targetObj = document.getElementById("target");
  targetObj.innerHTML = tags;
  var spans;
  if(document.all) {
//  spans = document.getElementsByTagName("span");
//  for(var i = spans.length - 1; i >= 0; i--) {
//    if(spans[i].name != name) {
//      spans = spans.slice(0, i).concat(spans.slice)((i + 1), spans.length))(;
//      }
//    }

    spans = [];
    var temp = document.getElementsByTagName("span");
    for(var i = 0; i < temp.length; i++) {
      if(temp[i].name == name) {
        spans[spans.length] = temp[i];
      }
    }
    delete temp;

  } else {
    spans = document.getElementsByName(name);
  }
  for(var i = 0; i < scripts.length; i++) {
    ScriptLoadWithWrite(scripts[i].type, scripts[i].src, scripts[i].charset, spans[i]);
  }
}



// スクリプトのロード(IE○)
function ScriptLoadWithWrite(type, src, charset, baseNode) {
  // writeメソッドの隠蔽
  var nativeWriter  = document.write;
  var nativeWriteln = document.writeln;
  var stringBuffer = "";
  document.write = function(str) {
    clearTimeout(recover.timer);
    recover.timer = setTimeout(recover, 0);
    if(str) {
      stringBuffer += str;
    }
  };
  document.writeln = function(str) {
    clearTimeout(recover.timer);
    recover.timer = setTimeout(recover, 0);
    if(str) {
      stringBuffer += str;
    }
    stringBuffer += "\r\n";
  };
  // scriptタグの生成
  var node = document.createElement("script");
  node.src = src;
  node.type = type;
  node.charset = charset;
  node.onload = recover;
  var next = baseNode.nextSibling;
  if(next) {
    baseNode.parentNode.insertBefore(node, next);
  } else {
    baseNode.parentNode.appendChild(node);
  }
  // writeメソッドの戻し
  function recover() {
    clearTimeout(recover.timer);
    node.onload = null;
    document.write   = nativeWriter;
    document.writeln = nativeWriteln;
    if(stringBuffer) {
      var obj = document.createElement("span");
      obj.innerHTML = stringBuffer;
      var baseNode = node;
      var next = baseNode.nextSibling;
      if(next) {
        baseNode.parentNode.insertBefore(obj, next);
      } else {
        baseNode.parentNode.appendChild(obj);
      }
    }
  }
  recover.timer = setTimeout(recover, 3000);
}



//--></script>
</head>
<body>



<div onclick="ActiveScriptLoad(text);">クリックで動的ロード</div>



<div onclick="AjaxWriter_New(text);">クリックで解決!</div>



<div onclick="AjaxWriter_Old(text);">クリックで比較(以前のもの)</div>



<div id="target" style="border:solid 1px black;"></div>



</body>
</html>
※「クリックで解決」以外は前回までのおさらいなので、説明省略します。

AjaxWriter_Newメソッド

まず、AjaxWriter_Newメソッドでは、渡されたソースの中の<script>タグを調査します。
ダミーのエレメントを作成し、そのinnerHTMLにソースを流し込み、そこから<script>タグとその情報を抜き出します。

次に、ソースの<script>タグを別のものに置き換えて、目的の場所のinnerHTMLに挿入します。

スクリプトのロードをするためにScriptLoadWithWriteメソッドを呼んで終了です。

ScriptLoadWithWriteメソッド

まず、外部JSファイルをロードする前に、document.writeを隠蔽します。
外部JSファイルがロードされ、そこでdocument.writeされたらその文字列をバッファとして蓄積します。

次に、外部JSファイルをロードします。

最後に、外部JSファイルのロード完了を待って、recoverメソッドで隠蔽したdocument.writeを戻し、バッファの文字列を任意のオブジェクトの直後に書き出します。
recoverメソッドが呼ばれるまでは、本体側でdocument.writeが使えません。

ちなみに、ソースコードの最後のタイマーですが、外部JSファイルのロードに時間がかかったり、応答なしの場合にrecoverメソッドを実行するためのものです。
タイマーを0にすると、外部JSファイルのロードにちょっとでも時間がかかると先にrecoverメソッドが呼ばれ、うまく動きません。
ブラウザでタイムアウトするくらいの長さか、必ず帰ってくるならばその行自体をコメント化します。


個人的な感想…

なんだか、仕様の穴を抜けていくようなコードを書いた気がしてならないです。
途中で、またまたIEを無条件で強制終了させるコードを見つけてしまうし…。
ここまで書いて最後に設計バグを見つけました。
外部JSファイルが複数種類あってそのロード時間がけっこう違うとき、一番速いファイルでrecoverメソッドが走るので、その後のファイルの実行は失敗する気がする。
う゛〜ん、これは許して。