代码改变世界

ScriptManager的ResolveScriptReference事件的重要作用

2007-01-23 16:10 Jeffrey Zhao 阅读(...) 评论(...) 编辑 收藏

还是对那个个人门户进行优化时遇到的问题。

那是个较为大型的网站(假设其域名为http://www.sample.com),其中有多个频道。它的个人门户为(http://spaces.sample.com),里面的每个模块都都由其它一个频道提供(例如音乐频道:http://music.sample.com)。每个频道维护自己的逻辑,然后只要根据个人门户制定的协议就能够在门户上建造一个Web Part。目前使用这种方式将门户的压力分担给其它频道,门户用于维护内容分离的信息与逻辑。

具体的实现方式,还有其细节我不便多说。这里遇到的问题是,由于某些频道还会提供独立的页面内嵌在个人门户的管理页面的IFrame中(例如,用户通过个人门户的页面来访问音乐频道提供的页面,用以管理自己的音乐信息)。这样,外部页面和内部页面两个不同站点均使用了ASP.NET AJAX,其造成的结果就是当打开一个页面时,很可能会需要将一些庞大的AJAX库下载两次。例如MicrosoftAjax.js,会通过http://spaces.sample.com/ScriptResouce.axdhttp://music.sample.com/ScriptResource.axd下载。这直接导致了缓存的失效,用户需要下载大量的代码。

哎,还好,查看了ScriptManager的代码之后,发现了一个非常简单的解决方案。我们先来看一下ScriptManager的相关代码:

private void RegisterScripts()
{
    List<ScriptReference> list1 = this.CollectScripts();

    ScriptReference reference1 = 
        new ScriptReference("MicrosoftAjax.js", this, this);
    ScriptManager.AddFrameworkScript(reference1, list1, 0);

    if (this.PageRequestManager.IncludingWebFormsScript)
    {
        ScriptReference reference2 = 
            new ScriptReference("MicrosoftAjaxWebForms.js", this, this);
        ScriptManager.AddFrameworkScript(reference2, list1, 1);
    }

    foreach (ScriptReference reference3 in list1)
    {
        this.OnResolveScriptReference(new ScriptReferenceEventArgs(reference3));
    }

    List<ScriptReference> list2 = ScriptManager.RemoveDuplicates(list1);
    bool flag1 = false;
    foreach (ScriptReference reference4 in list2)
    {
        string text1 = reference4.GetUrl(this, this.Control, this.Zip);
        this.RegisterClientScriptIncludeInternal(
            reference4.ContainingControl, typeof(ScriptManager), text1, text1);
        if (!flag1 && reference4.IsFrameworkAssembly())
        {
            this.ConfigureApplicationServices();
            flag1 = true;
        }
    }
}

 

这就是ScriptManager用来注册脚本的方法。幸运的是,它没多对MicrosoftAjax.js和MicrosoftAjaxWebForms.js(如果需要的话)两个程序集的资源文件进行特殊处理,而是与其它用户指定的脚本文件“一视同仁”地进行注册。换句话说,它一样会通过ResolveScriptReference事件进行处理,我们有机会在这个时候改变它!当然,具体有很多细节方面的内容就只能请大家自己去阅读代码和分析了。

我编写了一个ScriptManager类似的控件StaticScriptManager,会响应ScriptManager的ResolveScriptReference事件,在这里我们可以改变它所引用的脚本文件路径。StaticScriptManager的代码非常简单,在这里就全部贴出来了。

[PersistChildren(false)]
[ParseChildren(true)]
[NonVisualControl]
public class StaticScriptManager : Control
{
    public static StaticScriptManager GetCurrent(Page page)
    {
        return (page.Items[typeof(StaticScriptManager)] as StaticScriptManager);
    }

    private bool _StaticScriptEnabled = true;

    public bool StaticScriptEnabled
    {
        get { return _StaticScriptEnabled; }
        set { _StaticScriptEnabled = value; }
    }

    protected override void OnInit(EventArgs e)
    {
        base.OnInit(e);

        if (!this.DesignMode)
        {
            if (StaticScriptManager.GetCurrent(this.Page) != null)
            {
                throw new InvalidOperationException("One ContentPageManager per Page!");
            }

            this.Page.Items[typeof(StaticScriptManager)] = this;

            ScriptManager.GetCurrent(this.Page).ResolveScriptReference += 
                new EventHandler<ScriptReferenceEventArgs>(OnResolveScriptReference);
        }
    }

    private void OnResolveScriptReference(
        object sender, ScriptReferenceEventArgs e)
    {
        if (!this.StaticScriptEnabled)
        {
            return;
        }

        ScriptReference script = e.Script;

        if (script.Name != "MicrosoftAjax.js"
            && script.Name != "MicrosoftAjaxWebForms.js"
            && script.Name != "MicrosoftAjaxTimer.js"
            && !String.IsNullOrEmpty(script.Assembly))
        {
            return;
        }

        string scriptPath = ConfigurationManager.AppSettings["Atlas_StaticScriptPath"];
        if (String.IsNullOrEmpty(scriptPath))
        {
            return;
        }

        script.Path = scriptPath.EndsWith("/") ?
            scriptPath + script.Name : scriptPath + "/" + script.Name;
    }
}

 

关键在于OnResolveScriptReference方法,将会判断当前脚本是否是程序集的内嵌脚本。如果是的话,则读取配置信息,并将脚本文件地址修改为指定的路径。例如,我们如果在配制文件里面设置Atlas_StaticScriptPath为http://static.sample.com/scripts/atlas/,则在浏览器里查看HTML则会发现页面引入下面这个路径的脚本文件:

http://static.sample.com/scripts/atlas/MicrosoftAjax.js

如果在ScriptManager或者ScriptReference里设置ScriptMode为Debug,那么则会引入下面这个路径的脚本文件:

http://static.sample.com/scripts/atlas/MicrosoftAjax.debug.js

现在,只要在页面放入了StaticScriptManager这个控件,就可以让大量的应用访问同一个地址,用户就不会下载多次了。事实上,如果有一系列的大型应用会使用相同脚本的话,都应该让他们指向同一个脚本地址,尽可能减少用户的下载数据量。

其实这只是ScriptManager类ResolveScriptReference事件的一个简单实用实例。试想一下,如果再结合一些详细的配置,成为一个专业的详细的脚本库也不是件困难的事情了。