65.9K
CodeProject 正在变化。 阅读更多。
Home

合并、压缩、精简 ASP.NET ScriptResource 和 HTML 标记

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.76/5 (20投票s)

2008年10月17日

CPOL

5分钟阅读

viewsIcon

219589

downloadIcon

2244

本文介绍了如何合并、压缩和精简 ASP.NET ScriptResource 和 HTML 标记。

引言

在过去的几个月里,我们一直在使用 Microsoft AJAX 开发一个大型的 ASP.NET 3.5 项目。在开始在测试环境中进行测试时,我们发现大量的ScriptResource.axd条目正在进行 HTTP 调用以下载 JavaScript 以使其在页面上可用。在这里,我们有大约 80/90 行占用了 HTTP 处理程序,所以在这种情况下,如果我们有 100 个并发用户,将会有大约 800/900 个并发 HTTP 调用。为了优化性能/可伸缩性并减少 HTTP 调用,我们找到了一种方法来最小化这些调用,并希望与社区分享,因为他们可能遇到相同的性能问题。

解决方案

我们在此提出的解决方案将您 HTML 中的多个 JavaScript 文件声明合并到一个声明中,这意味着我们 80/90 个 JavaScript 文件引用合并为 1 个。

脚本分析器

此项目中包含一个非常酷的实用程序,可以列出页面上使用的所有脚本。因此,在运行项目之前,只需将 enableProfiler 设置为 true ,您就可以在页面上看到脚本列表。这些脚本必须放置在 optimizerSection 标记下,这样它才能帮助 ScriptManager 找到脚本并进行合并。

<optimizerSection enable="true" enableScriptCompression="true" 
	enableHtmlCompression="true" enableScriptMinification="true" 
	enableHtmlMinification="true" enableProfiler="false">
     <add key="1" name="MicrosoftAjax.js"  
         assembly="System.Web.Extensions, Version=3.5.0.0, 
	Culture=neutral, PublicKeyToken=31bf3856ad364e35" path="" />
     <add key="2" name="MicrosoftAjaxWebForms.js"  
         assembly="System.Web.Extensions, Version=3.5.0.0, 
	Culture=neutral, PublicKeyToken=31bf3856ad364e35" path="" />
     <add key="3" name="" assembly="" path="~/Scripts/Script01.js" />
     <add key="4" name="" assembly="" path="~/Scripts/Script02.js" />
     <add key="5" name="" assembly="" path="~/Scripts/Script03.js" />
     <add key="6" name="AspNetPerformanceOptimizer.Controls.Control01Client.js"  
         assembly="AspNetPerformanceOptimizer, Version=1.0.0.0, 
	Culture=neutral, PublicKeyToken=null" path="" />
     <add key="7" name="AspNetPerformanceOptimizer.Controls.Control02Client.js"  
         assembly="AspNetPerformanceOptimizer, Version=1.0.0.0, 
	Culture=neutral, PublicKeyToken=null" path="" />
     <add key="8" name="AspNetPerformanceOptimizer.Controls.Control03Client.js"  
         assembly="AspNetPerformanceOptimizer, Version=1.0.0.0, 
	Culture=neutral, PublicKeyToken=null" path="" />
     <add key="9" name="AspNetPerformanceOptimizer.Controls.Control04Client.js"  
         assembly="AspNetPerformanceOptimizer, Version=1.0.0.0, 
	Culture=neutral, PublicKeyToken=null" path="" />
     <add key="10" name="AspNetPerformanceOptimizer.Controls.Control05Client.js"  
         assembly="AspNetPerformanceOptimizer, Version=1.0.0.0, 
	Culture=neutral, PublicKeyToken=null" path="" />
     <add key="11" name="AspNetPerformanceOptimizer.Controls.Control06Client.js"  
         assembly="AspNetPerformanceOptimizer, Version=1.0.0.0, 
	Culture=neutral, PublicKeyToken=null" path="" />
     <add key="12" name="AspNetPerformanceOptimizer.Controls.Control07Client.js"  
         assembly="AspNetPerformanceOptimizer, Version=1.0.0.0, 
	Culture=neutral, PublicKeyToken=null" path="" />
     <add key="13" name="AspNetPerformanceOptimizer.Controls.Control08Client.js"  
         assembly="AspNetPerformanceOptimizer, Version=1.0.0.0, 
	Culture=neutral, PublicKeyToken=null" path="" />
     <add key="14" name="AspNetPerformanceOptimizer.Controls.Control09Client.js"  
         assembly="AspNetPerformanceOptimizer, Version=1.0.0.0, 
	Culture=neutral, PublicKeyToken=null" path="" />
     <add key="15" name="AspNetPerformanceOptimizer.Controls.Control10Client.js"  
         assembly="AspNetPerformanceOptimizer, Version=1.0.0.0, 
	Culture=neutral, PublicKeyToken=null" path="" />
     <add key="16" name="AspNetPerformanceOptimizer.Controls.Control11Client.js"  
         assembly="AspNetPerformanceOptimizer, Version=1.0.0.0, 
	Culture=neutral, PublicKeyToken=null" path="" />
     <add key="17" name="AspNetPerformanceOptimizer.Controls.Control12Client.js"  
         assembly="AspNetPerformanceOptimizer, Version=1.0.0.0, 
	Culture=neutral, PublicKeyToken=null" path="" />
     <add key="18" name="AspNetPerformanceOptimizer.Controls.Control13Client.js"  
         assembly="AspNetPerformanceOptimizer, Version=1.0.0.0, 
	Culture=neutral, PublicKeyToken=null" path="" />
     <add key="19" name="AspNetPerformanceOptimizer.Controls.Control14Client.js"  
         assembly="AspNetPerformanceOptimizer, Version=1.0.0.0, 
	Culture=neutral, PublicKeyToken=null" path="" />
     <add key="20" name="AspNetPerformanceOptimizer.Controls.Control15Client.js"  
         assembly="AspNetPerformanceOptimizer, Version=1.0.0.0, 
	Culture=neutral, PublicKeyToken=null" path="" />
     <add key="21" name="AspNetPerformanceOptimizer.Controls.Control16Client.js"  
         assembly="AspNetPerformanceOptimizer, Version=1.0.0.0, 
	Culture=neutral, PublicKeyToken=null" path="" />
     <add key="22" name="AspNetPerformanceOptimizer.Controls.Control17Client.js"  
         assembly="AspNetPerformanceOptimizer, Version=1.0.0.0, 
	Culture=neutral, PublicKeyToken=null" path="" />
     <add key="23" name="AspNetPerformanceOptimizer.Controls.Control18Client.js"  
         assembly="AspNetPerformanceOptimizer, Version=1.0.0.0, 
	Culture=neutral, PublicKeyToken=null" path="" />
     <add key="24" name="AspNetPerformanceOptimizer.Controls.Control19Client.js"  
         assembly="AspNetPerformanceOptimizer, Version=1.0.0.0, 
	Culture=neutral, PublicKeyToken=null" path="" />
</optimizerSection>

配置

我们有一个名为 ScriptCombinerSection.cs 的文件,它从 web.config 文件加载必要的配置信息,并通过一个名为 OptimizerConfig 的 static 类使其可用。

public class OptimizerConfig
{
    protected static Dictionary _scripts;
    protected static bool _enable;
    protected static bool _enableProfiler;
    protected static bool _enableScriptCompression;
    protected static bool _enableHtmlCompression;
    protected static bool _enableScriptMinification;
    protected static bool _enableHtmlMinification;
     static OptimizerConfig()
    {
        _scripts = new Dictionary();
        OptimizerSection sec = null;
        try
        {
            sec = (OptimizerSection)
                  	System.Configuration.ConfigurationManager.GetSection
"optimizerSection");
             foreach (ScriptElement i in sec.Scripts)
            {
                _scripts.Add(i.Key, i);
            }
            _enable = sec.Enable;
            _enableProfiler = sec.EnableProfiler;
            _enableScriptCompression = sec.EnableScriptCompression;
            _enableHtmlCompression = sec.EnableHtmlCompression;
            _enableScriptMinification = sec.EnableScriptMinification;
            _enableHtmlMinification = sec.EnableHtmlMinification;
        }
        catch { }
    }
    public static ScriptElement GetScriptByKey(string key)
    {
        ScriptElement objElement = null;
        try
        {
            objElement = _scripts[key];
        }
        catch { }
        return objElement;
    }
    public static ScriptElement GetScriptByResource(string name, string assembly)
    {
        ScriptElement objElement = null;
        foreach (KeyValuePair element in _scripts)
        {
            if (element.Value.Name == name && element.Value.Assembly == assembly)
            {
                objElement = element.Value;
                break;
            }
        }
        return objElement;
    }
    public static ScriptElement GetScriptByPath(string path)
    {
        ScriptElement objElement = null;
        foreach (KeyValuePair element in _scripts)
        {
            if (element.Value.Path == path)
            {
                objElement = element.Value;
                break;
            }
        }
        return objElement;
    }
    public static ScriptElement GetScriptByName(string name)
    {
        ScriptElement objElement = null;
        foreach (KeyValuePair element in _scripts)
        {
            if (element.Value.Name == name)
            {
                objElement = element.Value;
                break;
            }
        }
        return objElement;
    }
    public static bool Enable
    {
        get { return _enable; }
    }
    public static bool EnableProfiler
    {
        get { return _enableProfiler; }
    }
    public static bool EnableScriptCompression
    {
        get { return _enableScriptCompression; }
    }
    public static bool EnableHtmlCompression
    {
        get { return _enableHtmlCompression; }
    }
    public static bool EnableScriptMinification
    {
        get { return _enableScriptMinification; }
    }
    public static bool EnableHtmlMinification
    {
        get { return _enableHtmlMinification; }
    }
}

优化类型

我们可以将整体性能优化分为五个不同的部分

  • 脚本合并器:将所有 scriptresource.axd 调用合并为一个调用。
  • 脚本压缩器:根据浏览器能力压缩所有客户端脚本,包括 gzip/deflate。
  • 脚本精简器:删除注释、缩进和换行符。
  • HTML 压缩器:根据浏览器能力压缩所有 HTML 标记,包括 gzip/deflate。
  • HTML 精简:将完整的 HTML 写入单行并尽可能精简(开发中)。

脚本合并器和压缩器

如前所述,开发脚本合并器的基本目的是最小化 HTTP 调用并一次性获取完整的客户端脚本。为了使其正常工作,我们需要重写 ScriptManager 类及其三个方法。必须重写的主要方法之一是 OnResolveScriptReference。每当解析一个脚本时,我们都会在这里获取它,并用 web.config 提供的脚本信息替换它。如果我们启用脚本分析,Render 方法会将分析出的脚本列表写入浏览器。

public class OptimizeScriptManager : ScriptManager
{
    private const string HANDLER_PATH = "~/ClientScriptCombiner.aspx?keys=";
    private const string BLOCKED_HANDLER_PATH = HANDLER_PATH + "-1";
    private Dictionary _scripts = new Dictionary();
    private List<ScriptReference> _profilerScripts = null;
     protected override void OnInit(EventArgs e)
    {
        base.OnInit(e);
        if (OptimizerConfig.EnableProfiler) _profilerScripts = 
					new List<ScriptReference>();
    }
    protected override void OnResolveScriptReference(ScriptReferenceEventArgs e)
    {
        try
        {
            base.OnResolveScriptReference(e);
             #region Profiling scripts
            if (OptimizerConfig.EnableProfiler)
            {
                bool isFound = false;
                foreach (ScriptReference reference in _profilerScripts)
                {
                    if (reference.Assembly == e.Script.Assembly && 
                         reference.Name == e.Script.Name && 
			reference.Path == e.Script.Path)
                    {
                        isFound = true;
                        break;
                    }
                }
                if (!isFound)
                {
                    ScriptReference objScrRef = new ScriptReference(e.Script.Name, 
                                                                    e.Script.Assembly);
                    if (!string.IsNullOrEmpty(e.Script.Name) && 
                        string.IsNullOrEmpty(e.Script.Assembly))
                    {
                        //TODO: if resource belongs to System.Web.Extensions.dll, 
                        //it does not provide assembly info that's why hard-coded 
                        //assembly name is written to get it in profiler
                        objScrRef.Assembly = "System.Web.Extensions, Version=3.5.0.0," +
                                             " Culture=neutral, 
					PublicKeyToken=31bf3856ad364e35";
                    }
                    objScrRef.Path = e.Script.Path;
                    objScrRef.IgnoreScriptPath = e.Script.IgnoreScriptPath;
                    objScrRef.NotifyScriptLoaded = e.Script.NotifyScriptLoaded;
                    objScrRef.ResourceUICultures = e.Script.ResourceUICultures;
                    objScrRef.ScriptMode = e.Script.ScriptMode;
                    _profilerScripts.Add(objScrRef);
                    objScrRef = null;
                }
            }
            #endregion
             #region Combining Client Scripts
             bool isAssemblyBased = ((e.Script.Assembly.Length > 0) ? true : false);
            bool isPathBased = ((e.Script.Path.Length > 0) ? true : false);
            bool isNameBased = ((e.Script.Path.Length == 0 && 
				e.Script.Assembly.Length == 0 
                                	&& e.Script.Name.Length > 0) ? true : false);
             if (OptimizerConfig.Enable && (isAssemblyBased || 
				isPathBased || isNameBased))
            {
                ScriptElement element = null;
                try
                {
                    if (isAssemblyBased)
                        element = OptimizerConfig.GetScriptByResource(e.Script.Name, 
                                                                      e.Script.Assembly);
                    else if (isPathBased)
                    {
                        element = OptimizerConfig.GetScriptByPath(e.Script.Path);
                        if (null != element)
                        {
                            if (!OptimizerHelper.IsValidExtension(element, ".js"))
                            {
                                element = null;
                            }
                            else if (!OptimizerHelper.IsAbsolutePathExists(element))
                            {
                                string absolutePath = 
				OptimizerHelper.GetAbsolutePath(element);
                                element = null;
                            }
                        }
                    }
                    else if (isNameBased)
                        element = OptimizerConfig.GetScriptByName(e.Script.Name);
                }
                catch (Exception exc)
                {
                    element = null;
                }
                 if (element != null)
                {
                    if (!_scripts.ContainsKey(element.Key))
                    {
                        try
                        {
                            _scripts.Add(element.Key, e.Script);
                            e.Script.Assembly = string.Empty;
                            e.Script.Name = string.Empty;
                             StringBuilder objStrBuilder = new StringBuilder();
                            objStrBuilder.Append(HANDLER_PATH);
                             foreach (KeyValuePair script in _scripts)
                            {
                                objStrBuilder.Append(script.Key + ".");
                            }
                            string strPath = objStrBuilder.ToString();
                            objStrBuilder = null;
                             foreach (KeyValuePair script in _scripts)
                            {
                                script.Value.Path = strPath;
                            }
                        }
                        catch { }
                    }
                    else
                    {
                        e.Script.Assembly = string.Empty;
                        e.Script.Name = string.Empty;
                        e.Script.Path = BLOCKED_HANDLER_PATH;
                    }
                }
            }
            #endregion
        }
        catch (Exception ex)
        {
            this.Page.Response.Write(ex.ToString().Replace("\n", "<br>"));
        }
    }
    protected override void Render(HtmlTextWriter writer)
    {
        try
        {
            #region Writing profiled scripts on the browser
            if (OptimizerConfig.EnableProfiler && _profilerScripts != null)
            {
                StringBuilder builder = new StringBuilder();
                int index = 1;
                foreach (ScriptReference script in _profilerScripts)
                {
                    builder.Append("<add key=\"" + index++ + "\" name=\"" + 
                                   script.Name + "\" assembly=\"" + script.Assembly + 
                                   "\" path=\"" + script.Path + "\" /><br>");
                }
                writer.WriteLine("<pre>");
                writer.WriteLine(builder.ToString());
                writer.WriteLine("</pre>");
                builder = null;
            }
            #endregion
        }
        catch (Exception ex)
        {
            this.Page.Response.Write(ex.ToString().Replace("\n", "<br>"));
        }
        finally
        {
            base.Render(writer);
        }
    }
}

一旦所有脚本都已解析,它将构建包含页面所需的所有密钥的 URL

<script src="ClientScriptCombiner.aspx?keys=
	1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24." 
         type="text/javascript"></script>

这里查询字符串中的 keys 参数表示用点(.)分隔的脚本编号。这些脚本编号在 web.config 中指定,因此在解析脚本时,它会查找与匹配的 string 相对应的编号并构建 URL。一旦 URL 被构建并写入浏览器,处理程序将被调用并逐个获取编号以从程序集或文件中提取客户端脚本。StringBuilder 用于收集脚本流并将其写入浏览器。当处理程序被调用并完成所有脚本的合并后,它会确定浏览器的能力,看它是否支持压缩。如果支持,例如 gzip,它将创建 GZipStream 类的实例并压缩脚本,否则将原样写入。

public void ProcessRequest(HttpContext context)
{
    bool shouldProcessRequest = true;
    string[] scriptKeys = null;
    string keys = context.Server.UrlDecode(context.Request.Params["keys"]);
    string scriptResourcePath = String.Empty;
    ScriptManager objScriptManager = new ScriptManager();
    StringBuilder scriptBuilder = new StringBuilder();
    IHttpHandler handler = new ScriptResourceHandler();
    ///StringCollection _gescHdrStatus = new StringCollection();
     if (String.IsNullOrEmpty(keys) || keys.Equals("-1")) shouldProcessRequest = false;
     if (shouldProcessRequest)
    {
        scriptKeys = keys.Split('.');
        ///_gescHdrStatus.Add("incount:" + scripts.Length.ToString()); //script count
        scriptResourcePath = string.Format("{0}{1}{2}{3}{4}{5}{6}{7}", 
                context.Request.Url.Scheme, "://", context.Request.Url.Host, ":", 
                context.Request.Url.Port, "/", context.Request.ApplicationPath, 
                "/ScriptResource.axd");
        foreach (string key in scriptKeys)
        {
            ScriptElement element = OptimizerConfig.GetScriptByKey(key);
            if (element == null) continue;
             #region Generating resource URL dynamically and creating 
			WebRequest object to extract stream of script
            if (element != null)
            {
                ScriptReference reference = null;
                bool isPathBased = false;
                if (element.Path.Length > 0)
                {
                    reference = new ScriptReference(element.Path);
                    isPathBased = true;
                }
                else if (element.Assembly.Length > 0 && element.Name.Length > 0)
                {
                    reference = new ScriptReference(element.Name, element.Assembly);
                }
                try
                {
                    OptimizeScriptReference openReference = 
				new OptimizeScriptReference(reference);
                    string url = string.Empty;
                    if (!isPathBased)
                    {
                        url = context.Request.Url.OriginalString.Replace
				(context.Request.RawUrl, "") + 
                                       openReference.GetUrl(objScriptManager);
                        var queryStringIndex = url.IndexOf('?');
                        var queryString = url.Substring(queryStringIndex + 1);
                        var request = new HttpRequest("scriptresource.axd", 
					scriptResourcePath, queryString);
                         using (StringWriter textWriter = 
					new StringWriter(scriptBuilder))
                        {
                            HttpResponse response = new HttpResponse(textWriter);
                            HttpContext ctx = new HttpContext(request, response);
                              handler.ProcessRequest(ctx);
                        }
                    }
                    else
                    {
                        string absolutePath = OptimizerHelper.GetAbsolutePath(element);
                        if (OptimizerHelper.IsAbsolutePathExists(absolutePath))
                        {
                            using (StreamReader objJsReader = 
					new StreamReader(absolutePath, true))
                            {
                                scriptBuilder.Append(objJsReader.ReadToEnd());
                            }
                        }
                    }
                    scriptBuilder.AppendLine();
                }
                catch (Exception ex)
                {
                }
            }
            #endregion
        }
    }
     objScriptManager = null;
     #region Writing combine output scripts to the Response.OutputStream
      context.Response.Clear();
    context.Response.ContentType = "application/x-javascript";
     try
    {
        SetResponseCache(context.Response);
        scriptBuilder.AppendLine();
        //scriptBuilder.AppendLine("if(typeof(Sys)!=='undefined')
        // 	Sys.Application.notifyScriptLoaded();");
        string combinedScripts = scriptBuilder.ToString();
         if (shouldProcessRequest)
        {
            if (OptimizerConfig.EnableScriptMinification)
            {
                combinedScripts = JsMinifier.GetMinifiedCode(combinedScripts);
            }
             string encodingTypes = string.Empty;
            string compressionType = "none";
            if (OptimizerConfig.EnableScriptCompression)
            {
                encodingTypes = context.Request.Headers["Accept-Encoding"];
                 if (!string.IsNullOrEmpty(encodingTypes))
                {
                    encodingTypes = encodingTypes.ToLower();
                    if (context.Request.Browser.Browser == "IE")
                    {
                        if (context.Request.Browser.MajorVersion < 6)
                            compressionType = "none";
                        else if (context.Request.Browser.MajorVersion == 6 && 
                           !string.IsNullOrEmpty(context.Request.ServerVariables
							["HTTP_USER_AGENT"]) 
                           && context.Request.ServerVariables
					["HTTP_USER_AGENT"].Contains("EV1"))
                            compressionType = "none";
                    }
                    if ((encodingTypes.Contains("gzip") || 
				encodingTypes.Contains("x-gzip") 
                       || encodingTypes.Contains("*")))
                        compressionType = "gzip";
                    else if (encodingTypes.Contains("deflate"))
                        compressionType = "deflate";
                }
            }
            else
            {
                compressionType = "none";
            }
            if (compressionType == "gzip")
            {
                using (MemoryStream stream = new MemoryStream())
                {
                    using (StreamWriter writer = new StreamWriter(new GZipStream(stream, 
                                      	CompressionMode.Compress), Encoding.UTF8))
                    {
                        writer.Write(combinedScripts);
                    }
                    byte[] buffer = stream.ToArray();
                    context.Response.AddHeader("Content-encoding", "gzip");
                    context.Response.OutputStream.Write(buffer, 0, buffer.Length);
                }
            }
            else if (compressionType == "deflate")
            {
                using (MemoryStream stream = new MemoryStream())
                {
                    using (StreamWriter writer = new StreamWriter
						(new DeflateStream(stream, 
                                                      	CompressionMode.Compress), 
						Encoding.UTF8))
                    {
                        writer.Write(combinedScripts);
                    }
                    byte[] buffer = stream.ToArray();
                    context.Response.AddHeader("Content-encoding", "deflate");
                    context.Response.OutputStream.Write(buffer, 0, buffer.Length);
                }
            }
            else
            {
                //no compression plain text...
                context.Response.AddHeader("Content-Length", 
				combinedScripts.Length.ToString());
                context.Response.Write(combinedScripts);
            }
        }
        scriptBuilder = null;
    }
    catch (Exception ex)
    {
        context.Response.Write(ex.ToString().Replace("\n", "<br>"));
    }
    #endregion
}

脚本精简器

我们使用的是 **jsmin**,由 Douglas Crockford 提供。

HTML 压缩器、HTML 精简器

为了压缩和精简 HTML 标记,我们引入了一个新的流类 HtmlCompressStream,它继承自 Stream

public class HtmlCompressStream : Stream
{
    public enum CompressionType { None = 0, GZip = 1, Deflate = 2 };
    private Stream _stream;
     public HtmlCompressStream
	(Stream stream, CompressionMode mode, CompressionType type)
    {
        switch (type)
        {
            case CompressionType.GZip:
                _stream = new GZipStream(stream, mode);
                break;
            case CompressionType.Deflate:
                _stream = new DeflateStream(stream, mode);
                break;
            default:
                _stream = new StreamWriter(stream).BaseStream;
                break;
        }
    }
     public Stream BaseStream
    {
        get { return _stream; }
    }
    public override bool CanRead
    {
        get { return _stream.CanRead; }
    }
    public override bool CanSeek
    {
        get { return _stream.CanSeek; }
    }
    public override bool CanWrite
    {
        get { return _stream.CanWrite; }
    }
    public override long Length
    {
        get { return _stream.Length; }
    }
    public override long Position
    {
        get { return _stream.Position; }
        set { _stream.Position = value; }
    }
     public override IAsyncResult BeginRead(byte[] array, int offset, int count, 
                                    AsyncCallback asyncCallback, object asyncState)
    {
        return _stream.BeginRead(array, offset, count, asyncCallback, asyncState);
    }
    public override IAsyncResult BeginWrite(byte[] array, int offset, int count, 
                                    AsyncCallback asyncCallback, object asyncState)
    {
        return _stream.BeginWrite(array, offset, count, asyncCallback, asyncCallback);
    }
    protected override void Dispose(bool disposing)
    {
        _stream.Dispose();
    }
    public override int EndRead(IAsyncResult asyncResult)
    {
        return _stream.EndRead(asyncResult);
    }
    public override void EndWrite(IAsyncResult asyncResult)
    {
        _stream.EndWrite(asyncResult);
    }
    public override void Flush()
    {
        _stream.Flush();
    }
    public override int Read(byte[] array, int offset, int count)
    {
        return _stream.Read(array, offset, count);
    }
    public override long Seek(long offset, SeekOrigin origin)
    {
        return _stream.Seek(offset, origin);
    }
    public override void SetLength(long value)
    {
        _stream.SetLength(value);
    }
    public override void Write(byte[] array, int offset, int count)
    {
        if (OptimizerConfig.EnableHtmlMinification)
        {
            //TODO: HTML Minification
            _stream.Write(array, offset, count);
        }
        else
        {
            _stream.Write(array, offset, count);
        }
    }
}

根据浏览器的能力,我们创建一个压缩的 stream 对象,让写入器写入内容。编写这个类有几个优点,一是启用压缩,二是写入时同时精简 HTML 标记。因此,当写入器想要将 stream 发送到浏览器时,它会调用 Write 方法,该方法将字节流写入浏览器。我们将此类的一个实例分配给页面请求时的 Response.Filter 属性,即写入 Global.asaxApplication_BeginRequest 事件中。

public class Global : System.Web.HttpApplication
{
    protected void Application_BeginRequest(object sender, EventArgs e)
    {
        HttpRequest request = this.Request;
        HttpResponse response = this.Response;
         if (request.RawUrl.IndexOf(".aspx") > -1 && 
			string.IsNullOrEmpty(request.Params["keys"]))
        {
            if (OptimizerConfig.EnableHtmlCompression && 
			!(request.Browser.IsBrowser("IE") &&
                request.Browser.MajorVersion <= 6))
            {
                string acceptEncoding = request.Headers["Accept-Encoding"];
                if (!string.IsNullOrEmpty(acceptEncoding))
                {
                    acceptEncoding = 
			acceptEncoding.ToLower(CultureInfo.InvariantCulture);
                     if (acceptEncoding.Contains("gzip"))
                    {
                        //response.Filter = 
			new GZipStream(response.Filter, CompressionMode.Compress);
                        response.Filter = new HtmlCompressStream(response.Filter,
                            CompressionMode.Compress,
                            HtmlCompressStream.CompressionType.GZip);
                        response.AddHeader("Content-encoding", "gzip");
                    }
                    else if (acceptEncoding.Contains("deflate"))
                    {
                        //response.Filter = new DeflateStream
		      //(response.Filter, CompressionMode.Compress);
                        response.Filter = new HtmlCompressStream(response.Filter,
                            CompressionMode.Compress,
                            HtmlCompressStream.CompressionType.Deflate);
                        response.AddHeader("Content-encoding", "deflate");
                    }
                }
            }
            else
            {
                response.Filter = new HtmlCompressStream(response.Filter,
                    CompressionMode.Compress, HtmlCompressStream.CompressionType.None);
            }
        }
    }
}

注意:HTML 精简功能仍在开发中,很快就会完成。我们期待一个优秀的 HTML 精简器。

使用示例项目

有一些 Ajax 客户端控件和 *.js 文件是为了运行和测试示例而创建的。您需要重新构建项目,然后访问 default.aspx。如果您想使用 AjaxControlToolkit 进行测试,请下载最新版本的控件工具包并将其引用到项目中。从工具箱中拖放一些控件,启用分析器,然后运行项目。它会在页面上显示脚本列表;只需复制这些脚本并将它们放在 web.config 文件中的 optimizerSection 下。这就是运行项目所需的全部。

有用的结果

之前(238KB 需要 1.55 秒)

之后(75KB 需要 62 毫秒)

结论

我们总会发现处理和网络延迟之间存在某种权衡,本文完全专注于网络延迟,而不是处理成本,因为我们假设这种实现是在高端处理服务器上完成的。最后,如果您正在使用带 AJAX 客户端控件的 ASP.NET,请确保您的网站在所有可伸缩性级别上都提供最佳性能。

© . All rights reserved.