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

WP-Cumulus(4)

by WebSurfer 2010年6月24日 23:00

WP-Clumulus(3D タグクラウドを表示する Flash)を実装した後、ブラウザが IE(特に IE6)の場合、それ以前には出ていなかったスクリプトエラーが出るのに気がつきました。

スクリプトエラーは、外部スクリプトファイル recaptcha_ajax.js, blog.js, widget.js に定義されているメソッド等を参照できないというもので、そのため (1) Recaptcha の表示、(2) コメントの書き込み、(3) AMPL フィルターの起動、(4) Widget の移動、編集、削除ができませんでした。

いろいろと調べた結果、上記の外部スクリプトファイルを定義する script タグには defer="defer" 属性が追加されており、これと Flash Player 起動との関連で不具合が起きていることが分かりました。

defer="defer" 属性が追加されていると、当該スクリプトファイルの取得・解析を待たずに後続の HTML コードの解析・レンダリングが進められるそうです。その後 Flash Player が起動されると何かの不具合が起こり、IE のスクリプトエンジンが当該スクリプトファイルを取り込めなくなり(または解析できなくなり?)、それに定義されているメソッドを参照できなくなるようです。

対応策は、(a) Flash Player を起動しない、(b) defer="defer" 属性を削除する、(c) Flash Player を起動した後で defer="defer" 属性付の script タグを読むようスクリプトの順序を変更する・・・のいずれかになります。

検討の結果、(a) 案は 3D タグクラウドの実装を諦めるということなので問題外、(b) 案の defer="defer" 属性の削除は IE でレンダリングの遅れが目立つようになるので NG ということで、結局 (c) 案の対応を取りました。

recaptcha_ajax.js に対しては、RecaptchaControl.cs の RenderContents メソッドを書き換えました。問題のスクリプトのみ RegisterStartupScript メソッドを使用してページの最後に書き込むようにしました。コードは以下のとおりです。なお、この変更は reCaptcha を実装しなければ不要です。

protected override void RenderContents(HtmlTextWriter output)
{
  output.AddAttribute(HtmlTextWriterAttribute.Id, "spnCaptchaIncorrect");
  output.AddAttribute(HtmlTextWriterAttribute.Style, "color:Red;display:none;");
  output.RenderBeginTag("span");
  output.WriteLine("The captcha text was not valid. Please try again.");
  output.RenderEndTag();

  output.AddAttribute(HtmlTextWriterAttribute.Id, "recaptcha_placeholder");
  output.RenderBeginTag(HtmlTextWriterTag.Div);
  output.RenderEndTag();

  string csname = "ReCaptchaEnablingScript";
  Type cstype = this.GetType();
  ClientScriptManager cs = Page.ClientScript;

  if (!cs.IsStartupScriptRegistered(cstype, csname))
  {
    StringBuilder sb = new StringBuilder();
    sb.AppendLine("<script type=\"text/javascript\"" + 
        "src=\"http://api.recaptcha.net/js/recaptcha_ajax.js\" defer=\"defer\"></script>");
    sb.AppendLine("<script type=\"text/javascript\">");
    sb.AppendLine("//<![CDATA[");
    sb.AppendLine("  function showRecaptcha() {");
    sb.AppendLine("    Recaptcha.create('" + publicKey + "', 'recaptcha_placeholder', {");
    sb.AppendLine("      theme: '" + Theme + "',");
    sb.AppendLine("      tabindex: " + TabIndex.ToString());
    sb.AppendLine("      })");
    sb.AppendLine("  }");
    sb.AppendLine("  var rc_oldonload = window.onload;");
    sb.AppendLine("  if (typeof window.onload != 'function') {");
    sb.AppendLine("    window.onload = showRecaptcha;");
    sb.AppendLine("  }");
    sb.AppendLine("  else {");
    sb.AppendLine("    window.onload = function() {");
    sb.AppendLine("      rc_oldonload();");
    sb.AppendLine("      showRecaptcha();");
    sb.AppendLine("    }");
    sb.AppendLine("  }");
    sb.AppendLine("//]]>");
    sb.AppendLine("</script>");
    cs.RegisterStartupScript(cstype, csname, sb.ToString(), false);
  }
}

blog.js, widget.js に対しては、widget.ascx, widget.ascx.cs を書き換えました。Flash 起動スクリプトを、タグクラウドをレンダリングする HTML コードの中に埋め込むように変更しました。コードは以下のとおりです。なお、これは 3D タグクラウドを実装するには必須です。

widget.ascx

<%@ Control Language="C#" 
    AutoEventWireup="true" 
    CodeFile="widget.ascx.cs" 
    Inherits="widgets_Tag_cloud_widget" %>
<div id="wpcumulusflashcontent">
    If you see this Flash Player (Ver. 10 or better) 
    and/or JavaScript are/is not available in your PC.
</div>
<asp:Literal id="Literal1" runat="server"></asp:Literal>
<ul runat="server" id="ulTags" />

widget.ascx.cs

public override void LoadWidget()
{
  string root = BlogEngine.Core.Utils.AbsoluteWebRoot.ToString();
  StringBuilder sb = new StringBuilder();
  sb.Append("<tags>");

  foreach (string key in WeightedList.Keys)
  {
    string path = "?tag=/" + Utils.RemoveIllegalCharacters(key);
    HtmlGenericControl li = new HtmlGenericControl("li");
    li.InnerHtml = string.Format(LINK, 
        Utils.RelativeWebRoot + path, WeightedList[key], 
        "Tag: " + key, key);
    ulTags.Controls.Add(li);

    string size = "10";
    switch (WeightedList[key])
    {
      case "biggest":
        size = "13";
        break;
      case "big":
        size = "12";
        break;
      case "medium":
        size = "11";
        break;
      case "small":
        size = "10";
        break;
      case "smallest":
        size = "9";
        break;
      default:
        size = "10";
        break;
    }
    sb.Append(String.Format("<a href='{0}' style='{1}'>{2}</a>", root + path, size, key));
  }
  sb.Append("</tags>");

  StringBuilder cs = new StringBuilder();
  cs.AppendLine("<script type=\"text/javascript\">");
  cs.AppendLine("//<![CDATA[");
  cs.AppendLine("var so = new SWFObject(\"/BlogEngine/tagcloudUnicode.swf\", \"tagcloud\", \"180\", \"170\", \"10\", \"#ffffff\");");
  cs.AppendLine("so.addParam(\"wmode\", \"transparent\");");
  cs.AppendLine("so.addVariable(\"mode\", \"tags\");");
  cs.AppendLine("so.addVariable(\"tcolor\", \"0x333333\");");
  cs.AppendLine("so.addVariable(\"tcolor2\", \"0x333333\");");
  cs.AppendLine("so.addVariable(\"hicolor\", \"0xff0000\");");
  cs.AppendLine("so.addVariable(\"tspeed\", \"100\");");
  cs.AppendLine("so.addVariable(\"tagcloud\", \"" + sb.ToString() + "\");");
  cs.AppendLine("so.write(\"wpcumulusflashcontent\");");
  cs.AppendLine("//]]>");
  cs.AppendLine("</script>");
  Literal1.Text = cs.ToString();
}

これで問題はすべて片付いたので、パラメータ設定の為のコントロールパネルを追加するまでは、上記コードが最終版・・・と思いたいです。(笑)


------------------ 2010/8/5 追記 ------------------

RecaptchaControl.cs の RenderContents メソッドですが、以下のように空白を追加してください。

protected override void RenderContents(HtmlTextWriter output)
{
  ・・・中略・・・
    // /javascript\"" → /javascript\" " としてください。
    sb.AppendLine("<script type=\"text/javascript\" " + 
        "/ src=\"http://api.recaptcha.net/js/recaptcha_ajax.js\" defer=\"defer\"></script>");

  ・・・後略・・・

空白なしでも動きますが、W3C Markup Validation Service でエラーがでます。

Tags: , ,

BlogEngine.NET

About this blog

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

Calendar

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

View posts in large calendar