还是对那个个人门户进行优化时遇到的问题。
那是个较为大型的网站(假设其域名为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.axd和http://music.sample.com/ScriptResource.axd下载。这直接导致了缓存的失效,用户需要下载大量的代码。
哎,还好,查看了ScriptManager的代码之后,发现了一个非常简单的解决方案。我们先来看一下ScriptManager的相关代码:
private void RegisterScripts() { Listlist1 = 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 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(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事件的一个简单实用实例。试想一下,如果再结合一些详细的配置,成为一个专业的详细的脚本库也不是件困难的事情了。