WebSurfer's Home

トップ > Blog 1   |   ログイン
APMLフィルター

defer 属性つき script 定義と IE の問題

by WebSurfer 2012年12月3日 21:37

外部スクリプトファイルを定義する script 要素 に defer="defer" 属性を追加すると、あるケースで、internet explorer (IE) がそのスクリプトファイルを解析できなくなるという問題の紹介です。

defer 属性つき script 定義と 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 タグを読むよう順序を変更する、のいずれかしかなさそうです。

Tags: , ,

JavaScript

キャプチャリングとバブリング

by WebSurfer 2012年12月1日 00:14

JavaScript や jQuery を使ったプログラミングで、DOM イベントのバブリングという言葉を耳にします。本などを読んでもピンと来なかったので、理解するためにサンプルを作って動かしてみました。あまり面白くないかもしれませんが、せっかく作ったので書いておきます。

イベントのキャプチャリングとバブリング

上の画像のサンプルは、実際に動かして試すことができるよう 実験室 にアップしました。興味のある方は試してみてください。ソースコードはこの記事の下の方に記載しています。

DOM イベントは、イベントの原因となったオブジェクトで発生するだけでなく、キャプチャリングとバブリングというイベントの伝播があります。ここが .NET Framework のイベントとは異なっていて、自分が理解に苦しんだところです。

どこかの DOM オブジェクトでイベントが発生すると、window オブジェクト(ブラウザによっては document オブジェクト)とイベントが起きたオブジェクトの間を、DOM ツリーの親子関係を順にたぐって、イベントが伝播していきます。

伝播は 3 つのフェーズに分かれており、Capturing Phase(捕捉フェーズ)⇒ Target Phase(対象フェーズ)⇒ Bubbling Phase(浮上フェーズ)という順番になります。それぞれのフェーズの説明は以下の通りです。

  1. Capturing Phase では、window ⇒ document ⇒ その中の親 ⇒ 子 ⇒ 孫 ⇒ ひ孫 ・・・といった具合に、親子関係を親側から順にたぐって、イベントが起きたオブジェクトの親まで(「親まで」という点に注意)の各オブジェクトにイベントが送信されていきます。
  2. Target Phase では、イベントの原因となったオブジェクトにイベントが送信されます。
  3. Bubbling Phase では、イベントを発生させたオブジェクトの親から(「親から」という点に注意)順に浮上していき、window に達するまで各オブジェクトに順にイベントが送信されていきます。

window からイベントを発生させたオブジェクトの間の、親子関係にある任意のオブジェクトにリスナーを登録しておけば、そのオブジェクトにイベントが送信されたタイミングで必要な処置ができます。

オブジェクトにリスナーを��録する方法は、(1) 当該オブジェクトの addEventListener メソッド を呼び出す、(2) HTML 要素の属性を利用する、(3) 当該オブジェクトのプロパティを利用する、の 3 つがあります。

注意しなければならないのは、上記の (1) 以外の方法では、Capturing Phase でイベントを捕捉できないことです。

さらに注意しなければならないのは、IE8 以前では addEventListener メソッドはサポートされていないことです。代わりに attachEvent メソッドが使えますが、これは Capturing Phase でイベントを捕捉できません。(attachEvent メソッドの詳細は後述します)

IE9 は DOM Level 3 Events をサポートしているそうなので(詳しくは MSDN の IEBlog DOM Level 3 Events support in IE9 を参照)、addEventListener メソッド を使用可能です。

addEventListener メソッドを利用してリスナーを登録し、Capturing Phase、Target Phase、Bubbling Phase でイベントを補足するサンプルを書いてみました。そのソースコードをアップしておきます。

なお、Target Phase では、addEventListener メソッドで第 3 引数 useCapture を false に指定して登録したリスナーのみが呼び出されることになっているそうですが、自分が試した限りでは、useCapture を true に指定して登録したリスナーまでもが呼び出されてしまいました。

検証に使ったブラウザは、IE9、Firefox 17.0, Chrome 23.0.1271.95 m, Opera 12.02, Safari 5.1.7 で、すべて同じ結果になりました。

<!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>Event Capturing and Bubbling</title>
    <style type="text/css">
        #div1 { border: 1px solid black; width: 200px; 
            height: 100px; }
        #table1 { border: 1px solid red; width: 150px; }
        td { border: 1px solid green; }
        #span1 { background-color: yellow; }
    </style>
    <script type="text/javascript">
    //<![CDATA[

        // IE9, Mozzilla 用リスナー。
        function listener1(event) {
            // イベントが発生すると event オブジェクトが生成
            // され、その参照が第一引数に渡される。それから
            // target プロパティでイベントを発生させたオブジ
            // ェクトを、eventPhase プロパティでフェーズ情
            // 報を取得できる。this はアタッチしたオブジェク
            // トへの参照となる。
            var e = document.getElementById("result");
            var phase = "";
            if (event.eventPhase === 1) {
                phase = "capturing phase";
            } else if (event.eventPhase === 2) {
                phase = "target phase";
            } else if (event.eventPhase === 3) {
                phase = "bubbling phase";
            }
            var str = "fired by: " + event.target.id + 
                      ", listened at: " + this.id + 
                      ", during " + phase + "<br />";
            e.innerHTML += str;
        }

        // IE6-8 用リスナー。
        function listener3(element) {
            // attachEvent を使うと、this が参照するオブジェ
            // クトは window になってしまい、アタッチしたオ
            // ブジェクトへの参照は取得できない。止むを得な
            // いので、オブジェクトへの参照は引数として渡す。
            // Mozilla 系ではリスナーの第一引数には event オ
            // ブジェクトへの参照が渡されるので注意。
            var e = document.getElementById("result");
            var str = 
                "fired by: " + window.event.srcElement.id +
                ", listened at: " + element.id + "<br />";
            e.innerHTML += str;
        }

        // Bubbling Phase のリスナーをアタッチ。
        function attachForBubbling(element) {
            var e = document.getElementById("result");
            if (element.addEventListener) {
                // Bubbling Phase のリスナーをアタッチするに
                // は第三引数を false に設定する。
                element.addEventListener('click', listener1, 
                    false);
                e.innerHTML += "listener1 attached to " +
                    element.id +
                    " by addEventListener" + 
                    " w/ useCapture=false<br />";
            } else if (element.attachEvent) {
                // アタッチするオブジェクトへの参照をリスナ
                // ーの引数に渡すため、以下のように匿名関数
                // を使う。ただし、匿名関数を使うとデタッチ
                // できなくなることに注意。
                element.attachEvent('onclick', 
                    function () { listener3(element) });
                e.innerHTML += "listener3 attached to " + 
                    element.id + " by attachEvent<br />";
            }
        }

        // Capturing Phase のリスナーをアタッチ。
        // (IE9, Mozilla のみ) 
        function attachForCapturing(element) {
            var e = document.getElementById("result");
            if (element.addEventListener) {
                // Capturing Phase 用リスナーをアタッチするに
                // は第三引数を true に設定する。
                element.addEventListener('click', listener1, 
                    true);
                e.innerHTML += "listener1 attached to " +
                    element.id +
                    " by addEventListener" + 
                    " w/ useCapture=true<br />";
            }
        }

        // リスナーを各オブジェクトにアタッチ。
        window.onload = function () {
            var element = document.getElementById("div1");
            attachForBubbling(element);
            attachForCapturing(element);

            element = document.getElementById("table1");
            attachForBubbling(element);
            attachForCapturing(element);

            element = document.getElementById("td1");
            attachForBubbling(element);
            attachForCapturing(element);

            element = document.getElementById("span1");
            attachForBubbling(element);
            attachForCapturing(element);
        }
    //]]>
    </script>    
</head>
<body>
    <div id="div1">
        <table id="table1">
            <tr>
                <td id="td1">
                    <span id="span1">span1</span>
                </td>
            </tr>
            <tr>
                <td id="td2">
                    <span id="span2">span2</span>
                </td>
            </tr>
        </table>
    </div>
    <input type="button" value="Clear Results" 
        onclick="javascript:result.innerHTML = '';"/>
    <br />
    <div id="result"></div>
</body>
</html>

上でも述べましたように、IE6-8 では、addEventListener メソッドはサポートされていませんが、代わりに attachEvent メソッド がリスナーを登録するのに使えます。

IE6-8 をサポートするためには以下のようにします。上のサンプルコードでもこのようにして IE6-8 でリスナーを登録しています。

if (element.addEventListener) {
    element.addEventListener('click', listener, false);
} else if (element.attachEvent){
    element.attachEvent('onclick', listener);
}

attachEvent メソッドには以下のデメリットがあるので注意してください。

  1. リスナー内の this で取得できるのが、window オブジェクトへの参照になる。(addEventListener メソッドの場合はリスナーをアタッチしたオブジェクトへの参照になる)
  2. Capturing Phase ではイベントを補足できない。(Bubbling Phase と Target Phase では補足可能)

上記 1 の問題に対応するため、サンプルコードでは、リスナーの引数にアタッチするオブジェクトへの参照を渡しています。さらに、attachEvent メソッドの引数に匿名関数を使って、リスナーを登録しています。

Tags: , , ,

JavaScript

ToolkitScriptManager と gzip 圧縮

by WebSurfer 2012年11月21日 21:37

Internet Explorer (IE) の「インターネットオプション」の「詳細設定」タブで、「HTTP 1.1 を使用する」のチェックを外すと、Ajax Control Toolkit が動かないという話の紹介です。(プロキシサーバー経由になる場合は「プロキシ接続で HTTP 1.1 を使用する」のチェックを外す)

「HTTP 1.1 を使用する」のチェックマークを外す

ググって見つけた CodePlex のページ ToolkitScriptManager ignores accept-encoding http header を読んで原因がわかりました。

簡単に言うと、原因は、ToolkitScriptManager が常に gzip で圧縮したスクリプトファイルを送信するのに、IE は「HTTP 1.1 を使用する」にチェックを入れないと解凍しないことです。

詳しく書くと、以下の通りです。

  1. ToolkitScriptManager は、Microsoft Ajax Library を外部スクリプトファイルとして HTTP ハンドラ経由でブラウザにダウンロードさせるため、script 要素の src 属性に ScriptResource.axd?d=...&t=... と設定した初期ページをブラウザに返します。

    クエリ文字列のパラメータ d には、HTTP ハンドラが取得すべきファイルの指定と、送信時に圧縮するか否かの指示が含まれています。(パラメータ d, t の詳細については MSDN マガジンの ASP.NET AJAX アプリケーションの国際化 を見てください)
  2. IE の設定で「HTTP 1.1 を使用する」にチェックが入っている場合、要求ヘッダには Accept-Encoding: gzip, deflate が含まれます。チェックを外すと Accept-Encoding は含まれなくなります。

    ASP.NET 標準の ScriptManager の場合は、Accept-Encoding の有無を見て、初期ページに指定される ScriptResource.axd?d=...&t=... のパラメータ d を変えてきます。(Accept-Encoding が無ければ非圧縮、Accept-Encoding: gzip, deflate が含まれれば gzip 圧縮がパラメータ d に指定される)
  3. ところが、Ajax Control Toolkit の ToolkitScriptManager の場合は、要求ヘッダに Accept-Encoding があってもなくても、初期ページに指定する ScriptResource.axd?d=...&t=... は同じです。パラメータ d は gzip 圧縮の指定となります。

    つまり、IE の設定で「HTTP 1.1 を使用する」のチェックを外しても/外さなくても、サーバーからHTTP ハンドラ経由で受け取るスクリプトファイルは gzip で圧縮されたものになります。
  4. 一方、IE の設定で「HTTP 1.1 を使用する」のチェックを外すということは、IE では圧縮ファイルは解凍されないということを意味します。(応答ヘッダに Content-Encoding: gzip が含まれていても無視されます)。
  5. 結果、IE は Microsoft Ajax Library のスクリプトを解析できず、Ajax Control Toolkit が動かないということになります。(「文字が正しくありません。」とか「'Sys' は宣言されていません。」などのエラーが出ます)

IE8, IE9 で検証してみましたが、同じ問題が出ます。

いまどき「HTTP 1.1 を使用する」のチェックを外すことはないでしょうから実際に問題が出ることはなさそうですが、こういう問題もあるということは ASP.NET Web アプリケーションの開発者なら知っておいたほうがよさそうだと思って書いてみました。

Tags: ,

AJAX

About this blog

2010年5月にこのブログを立ち上げました。主に ASP.NET Web アプリ関係の記事です。

Calendar

<<  2024年4月  >>
31123456
78910111213
14151617181920
21222324252627
2829301234
567891011

View posts in large calendar