Hello World
Spiga

深入Atlas系列 - 浅析ASP.NET Beta 2中令人疑惑的脚本引入方式

2006-11-08 02:36 by 老赵, 5082 visits
  似乎已经有不少朋友在作了ASP.NET AJAX Beta 1到Beta 2的转移之后遇到了这样的问题:如果使用了ScriptManager引入了自定义的JavaScript脚本文件后会发生JavaScript错误。例如:
<asp:ScriptManager ID="ScriptManager1" runat="server" EnablePartialRendering="false">
    
<Scripts>
        
<asp:ScriptReference Path="MyScript.js" />
    
</Scripts>
</asp:ScriptManager>

上面的代码会抛出如下的异常:“Sys.ScriptLoadFailedException: The script 'MyScript.js' could not be loaded or does not contain the Sys.Application.notifyScriptLoaded() callback.”,意思就是说我们的MyScript.js缺少了对于Sys.Application.notifyScriptLoaded函数的调用。

这表示了什么?为什么突然发生了这种状况?我们一点点地来看。


一、代码逻辑简析:

我们从页面的代码看起。上面的使用方式会产生如下的代码:
<script src="/ScriptResource.axd?..." type="text/javascript"></script>

……

<script type="text/javascript">
<!--
Sys.Application.queueScriptReference('MyScript.js');
Sys.Application.initialize();
// -->
</script>

通过ScriptResource.axd文件引入的是“MicrosoftAjax.js”文件,不用多说。我们可以发现,和Beta 1的行为不同,ScriptManager并没有为添加在其中的引用添加<script />元素,而是使用了Sys._Application.queueScriptReference()将需要引入的脚本路径添加进一个队列中,最后通过Sys._Application.initialize()函数真正地引入这些脚本文件。我们来仔细查看这些方法:

首先Sys._Application.queueScriptReference函数会使用Sys._ScriptLoader类的Singleton对象的queueScriptReference函数,将脚本路径添加到那个Sys_ScriptLoader类的一个队列中。代码如下:
function Sys$_Application$queueScriptReference(scriptUrl) {
    
/// <param name="scriptUrl" type="String" mayBeNull="false"></param>
    var e = Function._validateParams(arguments, [
        {name: 
"scriptUrl", type: String}
    ]);
    
if (e) throw e;

    Sys._ScriptLoader.getInstance().queueScriptReference(scriptUrl);
}

Sys._ScriptLoader.getInstance 
= function Sys$_ScriptLoader$getInstance() {
    
var sl = Sys._ScriptLoader._activeInstance;
    
if(!sl) {
        sl 
= Sys._ScriptLoader._activeInstance = new Sys._ScriptLoader();
    }

    
return sl;
}

function Sys$_ScriptLoader$queueScriptReference(scriptUrl) {
    
/// <param name="scriptUrl" type="String" mayBeNull="false"></param>
    var e = Function._validateParams(arguments, [
        {name: 
"scriptUrl", type: String}
    ]);
    
if (e) throw e;

    
if(!this._scriptsToLoad) {
        
this._scriptsToLoad = [];
    }

    Array.add(
this._scriptsToLoad, {src: scriptUrl});
}

在Sys._Application.initialize函数中,会调用_loadScripts函数,而该函数事实上调用的是Sys._ScriptLoader的Singleton实例的loadScripts函数。如下:
function Sys$_Application$initialize() {
    
if (!this._initialized && !this._initializing) {
        
this._initializing = true;
        
this._loadScripts();
    }
}

function Sys$_Application$_loadScripts() {
    debug.assert(
!this._scriptLoaderExecuted, "Cannot load scripts more than once.");
    
this._scriptLoaderExecuted = true;        

    Sys._ScriptLoader.getInstance().loadScripts(
        
this.get_scriptLoadTimeout(), // 30 by default
        Function.createDelegate(
thisthis._allScriptsLoadedHandler),
        Function.createDelegate(
thisthis._scriptLoadFailedHandler),
        Function.createDelegate(
thisthis._scriptLoadTimeoutHandler));
}

在Sys._ScriptLoader.loadScripts函数中,会接受四个参数(关键不在这里):
  1. scriptTimeout:加载所有Script文件时所用的超时时间,单位为秒。
  2. allScriptsLoadedCallback:当所有脚本被加载完毕后使用的回调函数。
  3. scriptLoadFailedCallback:脚本加载失败后使用的回调函数。
  4. scriptLoadTimeoutCallback:脚本加载超时后使用的回调函数。
  Sys._ScriptLoader.loadScripts函数代码如下:
function Sys$_ScriptLoader$loadScripts(scriptTimeout, allScriptsLoadedCallback, scriptLoadFailedCallback, scriptLoadTimeoutCallback) {
    
/// <param name="scriptTimeout" type="Number" integer="true"></param>
    /// <param name="allScriptsLoadedCallback" type="Function" mayBeNull="true"></param>
    /// <param name="scriptLoadFailedCallback" type="Function" mayBeNull="true"></param>
    /// <param name="scriptLoadTimeoutCallback" type="Function" mayBeNull="true"></param>    
    var e = Function._validateParams(arguments, [
        {name: 
"scriptTimeout", type: Number, integer: true},
        {name: 
"allScriptsLoadedCallback", type: Function, mayBeNull: true},
        {name: 
"scriptLoadFailedCallback", type: Function, mayBeNull: true},
        {name: 
"scriptLoadTimeoutCallback", type: Function, mayBeNull: true}
    ]);
    
if (e) throw e;

    
if(this._loading) {
        
throw Error.invalidOperation(Sys.Res.scriptLoaderAlreadyLoading);
    }
    
this._loading = true;
    
this._allScriptsLoadedCallback = allScriptsLoadedCallback;
    
this._scriptLoadFailedCallback = scriptLoadFailedCallback;
    
this._scriptLoadTimeoutCallback = scriptLoadTimeoutCallback;
    
    
if(scriptTimeout > 0) {
        
// 监听超时
        this._timeoutCookie = window.setTimeout(
            Function.createDelegate(
thisthis._scriptLoadTimeoutHandler),
            scriptTimeout 
* 1000);
    }
        
    
this._loadScriptsInternal();
}

在this._loadScriptsInternal函数中会使用Sys._ScriptLoaderTack类来将<script />元素添加到Header中。
function Sys$_ScriptLoader$_loadScriptsInternal() {
    
if (this._scriptsToLoad && this._scriptsToLoad.length > 0) {
        
// 出队列
        var nextScript = Array.dequeue(this._scriptsToLoad);
        
// 构造一个<script />元素,
        var scriptElement = this._createScriptElement(nextScript);
            
        
if (scriptElement.text && Sys.Browser.agent === Sys.Browser.Safari) {
            scriptElement.innerHTML 
= scriptElement.text;
            
delete scriptElement.text;
        }            

        
if (typeof(nextScript.src) === "string") {
            
this._currentTask = new Sys._ScriptLoaderTask(
                scriptElement,
                
// this._scriptLoadedDelegate = Function.createDelegate(this, this._scriptLoadedHandler);
                this._scriptLoadedDelegate);

            
this._currentTask.execute();
        }
        
else { 
            document.getElementsByTagName('HEAD')[
0].appendChild(scriptElement);
            
this._loadScriptsInternal();
        }
    }
    
else {
        
// 加载完了,清除所有的回调函数
        var callback = this._allScriptsLoadedCallback;

        
this._allScriptsLoadedCallback = null;
        
this._scriptLoadFailedCallback = null;
        
this._scriptLoadTimeoutCallback = null;

        
this._loading = null;

        
// 调用回调函数
        if(callback) {
            callback(
this);
        }
    }
}

再细化下去似乎没有必要了,因为问题好像就出在了这里。Sys._ScriptLoaderTask构造函数的第二个参数是一个回调函数,它会在加载一个<script />元素后调用这个回调函数。但是我们来看一下Sys._ScriptLoader._scriptLoadedHandler函数到底做了些什么。如下:
function Sys$_ScriptLoader$_scriptLoadedHandler(loaded) {
    
var currentTask = this._currentTask;
    
var currentScriptElement = currentTask.get_scriptElement();
    currentTask.dispose();
        
    
this._currentTask = null;
    
this._scriptsToLoad = null;
    
this._loading = null;
        
    
if(this._timeoutCookie) {
        window.clearTimeout(
this._timeoutCookie);
        
this._timeoutCookie = null;
    }

    
// 为什么在这里调用scriptLoadFailedCallback这个回调函数?
    var callback = this._scriptLoadFailedCallback;

    
this._allScriptsLoadedCallback = null;
    
this._scriptLoadFailedCallback = null;
    
this._scriptLoadTimeoutCallback = null;

    
if(callback) {
        callback(
this, currentScriptElement);
    }
    
else {
        
throw Error.scriptLoadFailed(currentScriptElement.src);
    }
}

这段代码有些奇怪,它使用了this._scriptLoadFailedCallback回调函数,也就是说,它“准备”了要失败!在这个回调函数里,也就是Sys._Application._scriptLoadFailedHandler方法里就会触发文章一开始提到的异常:
function Sys$_Application$_scriptLoadFailedHandler(scriptLoader, scriptElement) {
        
if(this._disposing) {
            
return;
        }
        
var cancelled = false;
        
var handler = this.get_events().getHandler('scriptLoadFailed');
        
if(handler) {
            
var args = new Sys.ScriptElementEventArgs(scriptElement);
            handler(
this, args);
            cancelled 
= args.get_cancel();
        }
        
if(!cancelled) {
            
// 就是这里
            throw Error.scriptLoadFailed(scriptElement.src);
        }
    }

这里就涉及到了那个要求被调用的回调函数的作用。Sys._Application.notifyScriptLoaded函数会调用Sys._ScriptLoader那个Singleton对象的notifyScriptLoaded函数,而在这个函数里调用了当前Sys._ScriptLoaderTask对象的dispose方法,也就是当场“销毁”了这个对象,也就是说,在Sys_Application._scriptLoadFailedHandler函数也就不会被调用了。


二、解决方法:

解决方法倒非常简单,只要再引入的js文件中加上下面的代码就可以了:
if (typeof(Sys) !== 'undefined') Sys.Application.notifyScriptLoaded();

理论上这段代码可以加在文件的任何地方,虽然在调用了这个回调函数之后马上就会在<head />元素中加入另外一个<script />对象,但是因为浏览器保证了在同一时刻只会有一个外部脚本文件被加载,所以也就保证了脚本加载的顺序。

这也解释了官方的Known Issue:“If you are loading .js files for the Microsoft AJAX library from disk (by setting the ScriptReference property of the ScriptManager control), do not use the library that is installed by default in the %ProgramFiles%\Microsoft ASP.NET\ASP.NET 2.0 AJAX Extensions\v1.0.61025 folder. Instead, install the Microsoft AJAX Library from the http://www.asp.net Web site, copy the .js files from the new installation location to a folder of your application, and then point the ScriptReference property to that location.”。它要求开发人员从“http://ajax.asp.net/downloads/beta/default.aspx?tabid=47&subtabid=471”中下载一个另外的ASP.NET AJAX Library包,当需要从磁盘引入(相对于使用ScriptResource.axd文件引入)时,必须使用这个包内脚本文件,而不是使用安装了ASP.NET AJAX后出现在“%ProgramFiles%\Microsoft ASP.NET\ASP.NET 2.0 AJAX Extensions\v1.0.61025”文件夹里的内容。打开两者进行比较之后就会发现,关键就在于它们的最后一行,一个调用了“Sys.Application.notifyScriptLoader()”,一个没有。

不过事实上,这两种脚本文件的区别不止这些,在“预装”的脚本中,还缺少了“Sys.Res”的定义(Sys.Res中定义了大量的字符串信息),这样的话,比如在一些异常发生时,就会因为找不到Sys.Res中的字符串而出错,这反而掩盖了真正的异常信息。

说到这里,我忽然想说,“预装”的脚本算是什么东西?


三、注意事项:

为了观察脚本的生成,我们来做一些尝试。首先,我们在aspx文件中使用如下的代码:
<body>
    
<form id="form1" runat="server">
        
<asp:ScriptManager ID="ScriptManager1" runat="server" EnablePartialRendering="false">
            
<Scripts>
                
<asp:ScriptReference Path="MyScript1.js" />
                
<asp:ScriptReference Path="MyScript2.js" />
            
</Scripts>
        
</asp:ScriptManager>
        
        
<script language="javascript">
            alert("The script inside the 
<form /> element.");
        
</script>
        
    
</form>
    
    
<script language="javascript">
        alert("The script outside the 
<form /> element")
    
</script>
</body>

然后在aspx.cs文件中添加如下的代码:
protected override void OnPreRender(EventArgs e)
{
    
base.OnPreRender(e);
    
this.ClientScript.RegisterStartupScript(
        
this.GetType(), "OnPreRender"
        
"<script language='javascript'>alert('OnPreRender');</script>");
}

protected override void OnPreRenderComplete(EventArgs e)
{
    
base.OnPreRenderComplete(e);
    
this.ClientScript.RegisterStartupScript(
        
this.GetType(), "OnPreRenderComplete"
        
"<script language='javascript'>alert('OnPreRenderComplete');</script>");
}

然后我们来看一下这些代码生成了什么样的客户端代码呢?如下(经过排版):
<body>
    
<form name="form1" method="post" action="Default.aspx" id="form1">
        ……

        
<script src="/ScriptResource.axd?..." type="text/javascript"></script>

        
<script language="javascript">
            alert(
"The script inside the <form /> element.");
        
</script>
        
        
<script language='javascript'>alert('OnPreRender');</script>

        
<script type="text/javascript">
            
<!--
            Sys.Application.queueScriptReference('MyScript1.js');
            Sys.Application.queueScriptReference('MyScript2.js');
            
// -->
        </script>

        
<script language='javascript'>alert('OnPreRenderComplete');</script>

        
<script type="text/javascript">
            
<!--
            Sys.Application.initialize();
            
// -->
        </script>
    
</form>
    
    
<script language="javascript">
        alert(
"The script outside the <form /> element")
    
</script>
</body>

从这里可以看出使用不同的方式,在不同的位置或者时刻注册脚本代码时,它们在页面中出现的顺序。需要注意的是,Sys.Application.initialize函数被调用后,在大多数情况下后面的代码会先于外部脚本中的代码执行。也就是说,紧跟在Sys.Application.initialize函数之后的代码无法使用外部脚本内的数据。因此,如果有任何需要在页面被加载时执行的代码,一般来说一定要写在pageLoad函数中。如下:
function pageLoad(sender, args)
{
    
if (!args.get_isPartialLoad())
    {
        
// 页面第一次被加载
    }
    
else
    {
        
// UpdatePanel更新
    }
}

args参数是一个Sys.ApplicationLoadEventArgs对象,它的isPartailLoad属性首次出现在ASP.NET AJAX中,它就好比服务器端的IsPostBack属性一样,能够使用它很方便地判断Page Load事件的触发,是因为页面第一次加载,还是因为UpdatePanel被更新了。
Creative Commons License

本文基于署名 2.5 中国大陆许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名赵劼(包含链接),具体操作方式可参考此处。如您有任何疑问或者授权方面的协商,请给我留言

Add your comment

12 条回复

  1. 老赵
    admin
    链接

    老赵 2006-11-08 02:40:00

    这种做法实在太不好了,居然强制开发人员编写脚本文件的规则。
    另外,为什么现在脚本引入的逻辑变得那么复杂?性能?我没有从代码里看出任何提高性能的做法,莫非利用了某个浏览器中鲜为人知的特性?

  2. GSpring
    *.*.*.*
    链接

    GSpring 2006-11-08 08:21:00

    佩服

  3. Dflying Chen
    *.*.*.*
    链接

    Dflying Chen 2006-11-08 08:32:00

    @Jeffrey Zhao
    我觉得现在这个时候还不要太深入这部分内容,都会有所改变的。
    不如趁这段时间把基础打好。

  4. sunlife[匿名]
    *.*.*.*
    链接

    sunlife[匿名] 2006-11-08 09:22:00

    @Dflying Chen 说的对,万变不离其宗,先把已经稳定下来的学好才是良策

  5. 老赵
    admin
    链接

    老赵 2006-11-08 09:54:00

    @Dflying Chen
    我也这么觉得,所以我只深入一下我觉得稳定的地方吧。像这样的文档内还没有提到的东西感觉危险。

  6. sy[匿名][未注册用户]
    *.*.*.*
    链接

    sy[匿名][未注册用户] 2006-11-08 10:23:00

    大部分都是html封装堆积而成
    可以根据需要随便整合出现在需要的ajax效果
    怎么变都是html,http交互,东西是一样的 .
    ms封装封装再封装,我们就拿东西来用,时间长了就变的不动脑了。

  7. 老赵
    admin
    链接

    老赵 2006-11-08 10:30:00

    @sy[匿名]
    其实组件讲的就是封装+复用,其实做Web开发说到底就是HTML,ASP.NET服务器控件也好,最终变成的还是HTML。不过我觉得我们还是要动脑子的,因为不可能有组件能够覆盖所有应用的需求,例如我们还是必须要知道服务器控件会生成什么样的HTML,行为是什么。:)

  8. Cat Chen
    *.*.*.*
    链接

    Cat Chen 2006-11-08 12:10:00

    我没去仔细读代码,当它提示我文件缺少加载完毕事件时,我就想这东西一定会写在文件末尾(否则对开发人员来说是个大麻烦),然后随便打开一个它自己的js,把最后一句抄到自己的js上了事,呵呵……

    我赞成Dflying说的,先不要去研究这个,如果要作项目迁移就慢慢迁移,有能力的自己解决问题,没能力的先不要迁继续用旧版本。虽然读者需要文章,他们可能需要迁移的指引,但我们不能一味的就这样追版本号。

  9. 老赵
    admin
    链接

    老赵 2006-11-08 14:10:00

    @Cat Chen
    呵呵,所以我在这个问题上浅尝辄止了,我还期望这个方面被改回来,不过似乎不可能。
    其实忽然想到这个方式也有好处,至少我们能够在引入脚本之前添加代码了,呵呵。

  10. 小蜗牛
    *.*.*.*
    链接

    小蜗牛 2006-11-08 18:38:00

    恩,基础要打好。sp。

  11. wyx[未注册用户]
    *.*.*.*
    链接

    wyx[未注册用户] 2006-11-10 12:49:00

    这个queueScriptReference好像有大问题,比如当用了componentart后,componentart的script文件在队列最后,所有的componentart 控件都不会显示正常,直到整页完全载入。这在使用它的menu时有很大问题。menu项直到最后一刻才显示。

  12. 老赵
    admin
    链接

    老赵 2006-11-10 13:09:00

    @wyx
    这个特点是可以预料到的,所以这里比较可惜,如果真要使用的话,那么就直接写<script />元素吧。:)

发表回复

登录 / 登录并记住我 ,登陆后便可删除或修改已发表的评论 (请注意保留评论内容)

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

评论内容(大于5个字符):

  1. Your Name yyyy-MM-dd HH:mm:ss

使用Live Messenger联系我