document.writeの遅延実行

html中にアクセス解析用だったり広告表示用だったりで他ホスト上のjavascript外部ファイルを読み込む必要がある場合がありますが、そのjavascriptの読み込みや実行時間に引っぱられて自ページの表示が遅れるのはいやだって事はよくあります。

例えば以下のような構成の場合

|.
|-- index.html
`-- js
    `-- other-hosts-js1.js

index.html

<html>
<head>
	<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
	<title>as-lazy-writter</title>
</head>
<body style="background-color:#f0f0f0;">
	<p>start</p>
	<p><script type="text/javascript">document.write(new Date());</script></p>
	<hr />
	<script type="text/javascript" src="js/other-hosts-js1.js"></script>
	<hr />
	<p><script type="text/javascript">document.write(new Date());</script></p>
	<p>end</p>
</body>
</html>

othre-hosts-js1.js

for (var i = 0; i < 1000000000; i++) {
}
document.write('<p>other-hosts-js1.jsから出力</p>');

index.htmlの表示結果は以下のようになります。

startを描画してからendを描画するまでに5秒ほどの待ち時間が出ました。

ここでは「other-hosts-js1.js」を同ホスト上に置いてますが、実際は他ホスト上にありこちらからは編集出来ないケースを想定してますので、何とか意図するページの描画を優先させる為には「other-hosts-js1.js」の読み込み位置を下にずらす事にします。
けど単純に下に移動させただけではjs内で出力されている「other-hosts-js1.jsから出力」という文字列の描画位置まで変わってしまいます。
ので、「document.write」をオーバーライドして、どこで実行しても任意の箇所に描画可能なように変更してやろうかと。

変更後のファイル構成はこんなんで

.
|-- index.html
`-- js
    |-- as-lazy-writter.js
    `-- other-hosts-js1.js

index.html

<html>
<head>
	<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
	<title>as-lazy-writter</title>
	<script type="text/javascript" src="js/as-lazy-writter.js"></script>
</head>
<body style="background-color:#f0f0f0;">
	<p>start</p>
	<p><script type="text/javascript">document.write(new Date());</script></p>
	<hr />
	<div id="output1"></div>
	<hr />
	<p><script type="text/javascript">document.write(new Date());</script></p>
	<p>end</p>
	<script type="text/javascript">asLazyWriteId = "output1";</script>
	<script type="text/javascript" src="js/other-hosts-js1.js"></script>
</body>
</html>

as-lazy-writter.js

var asLazyWriteId = "";

var asLazyWrite = function() {
	var str = "";
	for (var i = 0; i < asLazyWrite.arguments.length; i++) {
		str += asLazyWrite.arguments[i];
	}
	var elm = document.getElementById(asLazyWriteId);
	if (!elm) {
		elm = document.body;
	}
	elm.innerHTML += str;
};

window.addEventListener("load", function(event) {
	document.write = document._write;
	document.writeln = document._writeln;
}, false);

document._write = document.write;
document._writeln = document.writeln;
document.write = asLazyWrite;
document.writeln = asLazyWrite;

とかってしてやると

って風にメインのコンテンツが表示された後に

document.writeを通した内容が描画される。

けどこれ手抜きしてinnerHTMLとかで文字列を挿入したけど、やっぱり問題ありでした。
実行する際にはもうDOMの構築が完了してるから、構築に汲んで欲しい文字列をdocument.write中に記述してても全く解釈してくれない。。

構成を

.
|-- index.html
`-- js
    |-- as-lazy-writter.js
    |-- other-hosts-js1.js
    `-- other-hosts-js2.js

に変更して例えばother-hosts-js1.jsを

for (var i = 0; i < 1000000000; i++) {
}
document.write('<p>other-hosts-js1.jsから出力</p>');
document.write('<script type="text/javascript" src="js/other-hosts-js2.js"></script>');

ってして、other-hosts-js2.jsを

document.write('<p>other-hosts-js2.jsから出力</p>');

って内容で追加しても

other-hosts-js2.js中でwriteってる内容が描画されない。
むしろブラウザはother-hosts-js2.jsを取得にすら行ってない。

innerHTMLじゃダメでした。

なので、簡易にでもHTMLを解析してappendChildしてやる事に。

as-lazy-writter.jsを以下に変更。

var asLazyWriteId = "";
var asLazyWriteIndex = 0;
var asLazyWriteStack = new Array();
var asLazyFunctions = new Array();

var asLazyWrite = function() {
	var str = "";
	for (var i = 0; i < asLazyWrite.arguments.length; i++) {
		str += asLazyWrite.arguments[i];
	}
	var elm = document.getElementById(asLazyWriteId);
	if (!elm) {
		elm = document.body;
	}

	asLazyAppend(elm, str);
};

var asLazyAppend = function(elm, str) {
	var name, attrs, value, matches;
	var node, attr;
	var wrapper;
	var loop = true;
	while (loop) {
		name = "";
		attrs = "";
		value = "";
		if (matches = str.match(/^<([0-9a-z_\-]+)((?: +[0-9a-z_\-]+(?:=(?:\\?"[^"]*\\?"|\\?'[^']*\\?'|[0-9a-z_\-]+))?)*) *\\?\/?>(?:(.*?)<\\?\/\1>)?/i)) {
			name = matches[1];
			attrs = matches[2];
			if (matches[3]) {
				value = matches[3];
			}
		} else if (matches = str.match(/^[^<]+/i)) {
			value = matches[0];
		}
		if (name) {
			node = document.createElement(name);
			attr = "";
			while (attr = attrs.match(/ *([0-9a-z_\-]+)(?:=(\\?"[^"]*\\?"|\\?'[^']*\\?'|[0-9a-z_\-]+))?/i)) {
				if (attr[2]) {
					attr[2] = attr[2].replace(/^\\?["']?(.*?)\\?["']?$/i, '$1');
					switch (attr[1]) {
					case "class":
						node["className"] = attr[2];
						break;
					case "style":
						node["style"]["cssText"] = attr[2];
						break;
					default:
						node[attr[1]] = attr[2];
						break;
					}
				} else {
					node[attr[1]] = attr[1];
				}
				attrs = attrs.replace(attr[0], '');
			}
			if (name == "script") {
				wrapper = document.createElement("span");
				wrapper["id"] = "as-lazy-write-index-" + asLazyWriteIndex;
				elm.appendChild(wrapper);
				if (!value) {
					node.onload = function(event) {
						asLazyDeploy();
					};
					asLazyWriteStack.push({id: wrapper["id"], elm: node});
				} else {
					wrapper.appendChild(node);
					node.appendChild(document.createTextNode(value));
				}
				asLazyWriteIndex++;
			} else {
				elm.appendChild(node);
				if (value) {
					asLazyAppend(node, value);
				}
			}
		} else if (value) {
			elm.appendChild(document.createTextNode(value));
		}
		if (matches) {
			str = str.replace(matches[0], '');
		} else {
			loop = false;
		}
	}
};

var asLazyDeploy = function() {
	if (asLazyWriteStack.length > 0) {
		var hash = asLazyWriteStack.shift();
		document.getElementById(asLazyWriteId = hash.id).appendChild(hash.elm);
	} else {
		asLazyClose();
	}
};

var asLazyClose = function() {
	document.write = document._write;
	document.writeln = document._writeln;

	for (var i = 0; i < asLazyFunctions.length; i++) {
		asLazyFunctions[i]();
	}
}

var asLazyEventListener = function() {
	var type = asLazyEventListener.arguments[0];
	var listener = asLazyEventListener.arguments[1];
	var useCapture = false;
	if (asLazyEventListener.arguments.length > 2) {
		useCapture = asLazyEventListener.arguments[2];
	}
	if (type == "load") {
		asLazyFunctions.push(listener);
	} else {
		window._addEventListener(type, listener, useCapture);
	}
};

var asLazyAttachEvent = function() {
	var type = asLazyAttachEvent.arguments[0];
	var listener = asLazyAttachEvent.arguments[1];
	if (type == "onload") {
		asLazyFunctions.push(listener);
	} else {
		window._attachEvent(type, listener);
	}
};

if (window.addEventListener) {
	window.addEventListener("load", function(event) {
		asLazyDeploy();
	}, false);

	window._addEventListener = window.addEventListener;
	window.addEventListener = asLazyEventListener;
} else if (window.attachEvent){
	window.attachEvent("onload", function(event) {
		asLazyDeploy();
	});

	window._attachEvent = window.attachEvent;
	window.attachEvent = asLazyAttachEvent;
}

document._write = document.write;
document._writeln = document.writeln;
document.write = asLazyWrite;
document.writeln = asLazyWrite;

ある程度単純なカタチでHTMLタグがネストされててもそういうのが連続して記述されてても描画されるようにはしました。
これで実行した結果はこれ

まとめると

  • as-lazy-writter.jsを読み込んだ後のdocument.writeは、実行時に変数[asLazyWriteId]にて指定済みのelementが存在する場合はそのelement中に描画する
  • 上記にて[asLazyWriteId]が未指定または指定してるがそのelementが存在しない場合は通常のdocument.writeと同様に実行箇所に描画する
  • 外部js中のdocument.writeの中にインラインのjavascriptタグを記述したりしてると意図する箇所に出力されないことがある
  • その他の不具合の可能性?

という適当なものになりました。

単純な使い方しかしてないので今のとこ想定通りには動いてますがjavascriptめったに書かないので自信無しです。
改善点やバグがあったら連絡お待ちしてます。