by WebSurfer
2012年12月3日 21:37
外部スクリプトファイルを定義する script 要素 に defer="defer" 属性を追加すると、あるケースで、internet explorer (IE) がそのスクリプトファイルを解析できなくなるという問題の紹介です。
「あるケース」というのは、div 要素などの innerHTML を書き換えることです。自分でそのようなコートを書かなくても、例えば、SWFObject を使って Flash を埋め込む場合に innerHTML の書き換えが行われます。
ただし、html コードを書く順番が問題で、defer 属性を追加した script タグが出現した後、innerHTML を書き換える場合に限ります。順番が反対の場合は問題は起こりません。
確証がないのではっきりしたことは言えませんが、自分が試した限りでは、スクリプトの取得に時間がかかる(サーバーの応答が遅い)と問題が発生する確率が高いようです。ブラウザの解析の速度も関係があるようで、IE6 であればほぼ 100% 問題が発生するのに対し、IE8 は微妙なタイミングで問題が発生したりしなかったりします。
検証のため、スクリプトをダウンロードする HTTP ハンドラを作って、Thread.Sleep メソッドを使って応答に時間がかかるようにしてみました。
<%@ WebHandler Language="C#" Class="JavaScriptHandler" %>
using System;
using System.Web;
using System.Text;
using System.Threading;
using System.Diagnostics;
public class JavaScriptHandler : IHttpHandler
{
public void ProcessRequest (HttpContext context)
{
Stopwatch stopWatch = new Stopwatch();
stopWatch.Start();
StringBuilder sb = new StringBuilder();
sb.Append("DateTime accessed: "
+ DateTime.Now.ToString("d MMM yyyy HH:mm:ss zzz",
System.Globalization.DateTimeFormatInfo.InvariantInfo)
+ ", ");
string delay = context.Request.QueryString["delay"];
int time;
bool result = Int32.TryParse(delay, out time);
if (result)
{
Thread.Sleep(time);
sb.Append(String.Format(
"delay time set: {0} ms", time) + ", ");
}
else
{
sb.Append("delay time set: none, ");
}
context.Response.ContentType = "text/javascript";
context.Response.Cache.VaryByHeaders["Accept-Encoding"] =
true;
context.Response.Cache.SetCacheability(
HttpCacheability.NoCache);
context.Response.Cache.SetExpires(
DateTime.Now.ToUniversalTime());
context.Response.Cache.SetMaxAge(
new TimeSpan(0, 0, 0, 0));
context.Response.AppendHeader("Pragma", "no-cache");
stopWatch.Stop();
TimeSpan ts = stopWatch.Elapsed;
sb.Append(String.Format(
"TimeSpan measured: {0:000} ms", ts.Milliseconds));
string script = sb.ToString();
script = "var msg = '" + script + "'";
context.Response.Write(script);
}
public bool IsReusable
{
get
{
return false;
}
}
}
上記の HTTP ハンドラを呼び出す際、例えば、クエリ文字列を delay=200 とすると、リクエストを受けてから約 200ms 後にアクセスした時間、クエリ文字列の設定、実際に計った時間をスクリプトとして返します。
以下のような簡単な HTML コードで試すことができます。たぶん delay はもっと少なくても問題が再現すると思います。実際に動かして試すことができるよう 実験室 にアップしました。興味のある方は試してみてください。
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title></title>
<script type="text/javascript"
src="JavaScriptHandler.ashx?delay=200" defer="defer">
</script>
<script type="text/javascript">
//<![CDATA[
function write(id){
document.getElementById(id).innerHTML =
"<h1>innerHTML changed!<\/h1>";
}
function ScriptTest() {
var x = msg;
alert(x);
}
//]]>
</script>
</head>
<body>
<div id="myContent">
<h1>This will be replaced by write method</h1>
</div>
<script type="text/javascript">
//<![CDATA[
write("myContent");
//]]>
</script>
<br />
<input type="button" value="Script Test"
id="button1" onclick="javascript:ScriptTest();" />
</body>
</html>
自分が検証した限りでは、IE6-9 で同じ問題が出ました(IE10 は未検証)。対応策は、(1) defer="defer" 属性を使用しない、または、(2) innnerHTML を書き換えた後で defer="defer" 属性付の script タグを読むよう順序を変更する、のいずれかしかなさそうです。