後からロードした外部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メソッドが走るので、その後のファイルの実行は失敗する気がする。
う゛〜ん、これは許して。

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

※1157010937の回答の3/4

ファイルの読み込み後にロードした外部JSファイルでdocument.writeするときの問題点を洗います。


【writer.js】
// document.writeする
var str = '<div style="background-color:orange;">外部JSファイルによって作成されたHTML要素です。</div>';
document.write(str);



// HTML側からのチェック用
function writer() {
  alert("./writer.jsが読み込まれています。");
}



// ステータスバーでのチェック用
status += " Load!";


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



function viewSource(id) {
  if(document.body) {
    alert("ソースビュー " + id + "\n" + document.body.parentNode.innerHTML);
  } else {
    alert("ソースビュー " + id + "\n" + "<body>がまだ出現していない");
  }
}



viewSource(0);



function test(str) {
  document.writeln(str);
}



function writeScriptTag() {
  test('<script type="text/javascript" src="./writer.js" charset="Shift_JIS"></' + 'script>');
}



function writeAfterLoad() {
  test('あいう');
  // writeScriptTag();
}



function writeInInnerHTML() {
  // ↓擬似受信電文
  var str = '<div>scriptタグの直前<script type="text/javascript" src="./writer.js" charset="Shift_JIS"><' + '/script>scriptタグの直後</div>';
  document.getElementById("target").innerHTML = str;
}



viewSource(1);



test('<title>てすと</title>');



viewSource(2);



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



<div><input type="button" onclick="viewSource();" value="ソースチェック" /></div>



<hr />
※HTMLのタグを直接書いてある場合
<div>外部JSファイルの前</div>
<script type="text/javascript" src="./writer.js" charset="Shift_JIS"></script>
<div>外部JSファイルの後</div>



<hr />
※HTMLのタグをdocument.writeした場合
<script type="text/javascript"><!--



  viewSource(10);
test('<div>外部JSファイルの前</div>');
  viewSource(11);
writeScriptTag();
  viewSource(12);
test('<div>外部JSファイルの後</div>');
  viewSource(13);



//--></script>



<hr />



<div onclick="viewSource(20);writeAfterLoad();viewSource(21);">クリックでdocument.write();</div>



<hr />



<div onclick="viewSource(30);writeInInnerHTML();viewSource(31);">クリックで質問と同じ状況</div>



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



</body>
</html>

※このサンプルは、1157010937の回答の2/4のおさらいができるようになっているので、その部分の説明は省略します。

まず、HTMLのタグが直接書いてあり、外部JSファイルを読み込む<script>タグも直接書いてある場合は、外部JSファイルのdocument.writeも意図した場所に書かれています。

次に、外部JSファイルを読み込む<script>タグを、HTMLのロード中に他のタグと一緒にdocument.writeした場合、一緒に書いた他のタグの後に外部JSファイルのdocument.writeの内容が書かれています。

また、HTMLのロード後にdocument.writeするとページが変わってしまうのでNGです。

さらに、innerHTMLの中に<script>タグがあっても動作しません。

外部JSファイルの動的ロード

※1157010937の回答の2/4

スクリプト処理の中でロードしたいJSファイルを決定し、スクリプトで外部スクリプトファイルの読み込みを行う方法を説明します。

document.writeで読み込む

1157010937の回答の1/4のサンプルの中で、外部JSファイルdispTime.jsを動的に読み込んでいる通り、可能です。
ただし、ストリームが閉じてしまってからは無理です。

innerHTMLで読み込む

この方法は動作しません。
1157010937の回答の3/4のサンプルの中で、「クリックで質問と同じ状況」のクリックがその例となります。
innerHTMLに含まれる<script>タグは動作しないようです。

document.createElementで読み込む

まず、ダミーの<div>タグをdocument.createElementします。
そのinnerHTMLに<script>タグを含むソースを流し込み、ダミーごと本体にDOMでくっつけます。
→この方法では、InternetExplorerではNGでしたが、FireFoxでは動作しました。
1157010937の回答の4/4のサンプルの中で、「クリックで動的ロード」のクリックがその例となります。

では、IEでも動的ロードを可能にするにはどうするかというと、<script>タグをdocument.createElementします。
作成したscript要素の属性に、type,language,src,charset,deferなどを付けて、そのまま本体にくっつけます。
そうするとファイルの読み込み後でも、IEFireFoxも外部JSファイルがロードされます。

document.writeとストリーム

※1157010937の回答の1/4

document.write(以降、writelnも含む)は、
読み込んでいるファイルのストリームの最後に内容を書き出します。

静的なdocument.writeの場合


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



var x = 10;



document.write("x = 20;");



alert(x);



//--></script>
</head>
<body>
</body>
</html>
を読み込むとき、alert(x);の直前にx = 20;を書き出すのではありません

このJavaScriptが実行されるとき、HTMLファイルはどこまで読み込まれているかというと、


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



var x = 10;



document.write("x = 20;");



alert(x);



//--></script>
までですので、この最後尾にx = 20;が追加され、

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



var x = 10;



document.write("x = 20;");



alert(x);



//--></script>x = 20;
</head>
<body>
</body>
</html>
と認識されます。

このとき、ストリームは開いていて、つまりファイルは読み込み中で、その最後尾に追加するのがポイントです。

動的なdocument.writeの場合

では、document.writeで書き出したソースの中にdocument.writeがあったら、それはどうなるのでしょうか?

まずは、最初に書き出したソースに直接document.writeがあった場合


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



function dispTime() {
  var str = '<div>現在の時刻は、';
  str += '<script type="text/javascript"><!--\r\n';
  str += 'document.write)((new Date())(.toLocaleString());\r\n';
  str += '/' + '/--></' + 'script>';
  str += 'です。</div>\r\n';
  for(var i = 0; i < 100; i++) {
    str += 'ゴミ文字列';
  }
  document.write(str);
}



//--></script>
</head>
<body>
<script type="text/javascript"><!--
dispTime();
//--></script>
</body>
</html>
これは意図通りに動作するようです。

次に、最初に書き出したソースで外部JSファイルをロードし、そこでdocument.writeしている場合


【dispTime.js】
document.write)((new Date())(.toLocaleString());

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



function dispTime() {
  var str = '<div>現在の時刻は、';
  str += '<script type="text/javascript" src="./dispTime.js" charset="Shift_JIS"></' + 'script>';
  str += 'です。</div>\r\n';
  for(var i = 0; i < 100; i++) {
    str += 'ゴミ文字列';
  }
  document.write(str);
}



//--></script>
</head>
<body>
<script type="text/javascript"><!--
dispTime();
//--></script>
</body>
</html>
意図通りに動作しません。
なぜかというと、日時を書きたいdocument.writeが動く時点では、既にストリームに「ゴミ文字列」がたくさん書き込まれてしまっているので、document.writeしてもその後に書き出すだけになります。
ただし、もともと固定でHTMLファイルに書いてある場合には、ちゃんとJSファイルはscriptタグのところで実行されます。

さらに、document.writeするタイミングを2回に分けても無駄です。


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



function dispTime() {
  var str = '<div>現在の時刻は、';
  str += '<script type="text/javascript" src="./dispTime.js" charset="Shift_JIS"></' + 'script>';
  str += 'です。</div>\r\n';
  document.write(str);
  str = '';

  for(var i = 0; i < 100; i++) {
    str += 'ゴミ文字列';
  }
  document.write(str);
}



//--></script>
</head>
<body>
<script type="text/javascript"><!--
dispTime();
//--></script>
</body>
</html>
これはJavaScriptのスレッドが(ページごとに)1本しかないためで、仮に「ゴミ文字列」をdocument.writeする前にdispTime.jsを読み込み完了していても、それらを書いたスクリプトが走っている最中です。
そのスレッドが終わってからdispTime.jsの実行の番となるのです。

※(余談)ということは、個人的には外部ファイルを使わない方があるべき姿ではないような気がします。

ファイル読み込み後のdocument.writeの場合

ファイルのロードが終わってしまうと、ストリームが閉じます。
その後でdocument.writeすると、新たなストリームが開かれます。
ブラウザの動作では、新しい白紙ページに遷移して“読み込み中”となり、そこに書き込まれます。


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



function dispTime() {
  var str = '<div>現在の時刻は、';
  str += '<script type="text/javascript" src="./dispTime.js" charset="Shift_JIS"></' + 'script>';
  str += 'です。</div>\r\n';
  for(var i = 0; i < 100; i++) {
    str += 'ゴミ文字列';
  }
  document.write(str);
}



//--></script>
</head>
<body>
<input type="button" onclick="dispTime();" value="document.writeする" />
</body>
</html>

まとめ

つまり、document.writeするときにはdocument.writeする時点でのストリームがどうなっているかを意識しなければなりません。