JS&CSS文件請求合併及壓縮處理研究(五)

接上篇。在咱們最終調用 @Html.RenderResFile(ResourceType.Script) 或者 @Html.RenderResFile(ResourceType.StyleSheet) 將頁面中添加的文件路徑合併成相似如下格式後:javascript

<script type="text/JavaScript" src="Resource/script?href=[Scripts/common/jquery][Scripts/functionA/A1,A2][Scripts/functionB/B1,B2]&compress"></script>

接下來須要作的就是在 Resource/script 中接收壓縮後的路徑href參數,按順序讀取服務器存取的資源文件,合併,壓縮(若是有compress參數傳入的話),輸出至客戶端。有了基本的流程和思路,代碼實現其實沒有什麼難度。看代碼:css

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using Mcmurphy.Common;

namespace Mcmurphy.Web.Controllers
{
    /// <summary>
    /// 資源文件請求處理
    /// </summary>
    public class ResourceController : Controller
    {
        /// <summary>
        /// 處理腳本文件
        /// </summary>
        /// <returns></returns>
        public void Script()
        {
            //原始Url連接
            var rawUri = new Uri(Request.Url.Scheme + "://" + Request.Url.Host + Request.RawUrl);
            //是否壓縮參數
            var isCompressJs = rawUri.Query.IndexOf("&compress", StringComparison.OrdinalIgnoreCase) >= 0 
                || rawUri.Query.IndexOf("?compress", StringComparison.OrdinalIgnoreCase) >= 0;
            var queryHref = Request["href"];
            //若是參數爲空或者參數中包含有危險字符
            if (string.IsNullOrEmpty(queryHref) || !UrlFilter.FiltUrl(Server.UrlDecode(rawUri.Query)))
            {
                Response.Write("請求參數錯誤!");
            }
            try
            {
                //請求參數拆分紅文件列表
                var jsPaths = PathHelper.QueryToFileList(queryHref, ".js");
                //轉成成物理文件列表
                jsPaths = PathHelper.GetPhysicPaths(jsPaths, Request);
                //獲取合併後的文件內容(若是標識isCompressJs == true,則須要壓縮)
                var finalJsContent = JsCombiner.CombineJs(jsPaths, Server.MapPath("~/"), isCompressJs);
                //替換內容中模板(就簡單的替換了一下時間)
                finalJsContent = finalJsContent.Replace("{{now}}", DateTime.Now.ToString("yyyyMMddHHmmss"));
                //輸出字節流
                Response.ContentType = "text/javascript";
                Response.BinaryWrite(EncodingHelper.ConvertToUTF8BomEncodingStringBytes(finalJsContent));
                Response.AddHeader("Vary", " Accept-Encoding");
            }
            catch (Exception ex)
            {
                Response.Write(ex.StackTrace);
            }
        }

        /// <summary>
        /// 處理樣式文件
        /// </summary>
        /// <returns></returns>
        public void Style()
        {
            var rawUri = new Uri(Request.Url.Scheme + "://" + Request.Url.Host + Request.RawUrl);
            //是否壓縮參數
            var isCompressJs = rawUri.Query.IndexOf("&compress", StringComparison.OrdinalIgnoreCase) >= 0
                             || rawUri.Query.IndexOf("?compress", StringComparison.OrdinalIgnoreCase) >= 0;
            var queryHref = Request["href"];
            if (string.IsNullOrEmpty(queryHref) || !UrlFilter.FiltUrl(Server.UrlDecode(rawUri.Query)))
            {
                Response.Write("請求參數錯誤!");
            }
            try
            {
                //請求參數拆分紅文件列表
                var cssPaths = PathHelper.QueryToFileList(queryHref, ".css");
                //轉成成物理文件列表
                cssPaths = PathHelper.GetPhysicPaths(cssPaths,Request);
                //獲取合併後的文件內容
                var finalCssContent = CssCombiner.CombineAndCompressCss(cssPaths, Server.MapPath("~/"), isCompressJs);
                //輸出字節流
                Response.ContentType = "text/css";
                Response.BinaryWrite(EncodingHelper.ConvertToUTF8BomEncodingStringBytes(finalCssContent));
                Response.AddHeader("Vary", " Accept-Encoding");
                
            }
            catch (Exception ex)
            {
                Response.Write(ex.StackTrace);
            }
        }
    }
}

以處理腳本文件的Script Action爲例:html

1,首先對接收的參數進行常規的空值及危險字符檢查。java

檢查是否存在危險字符的UrlFilter.FiltUrl方法代碼爲jquery

namespace Mcmurphy.Common
{
    public class UrlFilter
    {
        /// <summary>
        /// 判斷是否有特殊字符
        /// </summary>
        /// <param name="url"></param>
        /// <returns></returns>
        public static bool FiltUrl(string url)
        {
            if (url.Contains("&_="))
            {
                return false;
            }
            if (url.Contains("(") || url.Contains(")"))
            {
                return false;
            }
            if (url.Contains("{") || url.Contains("}"))
            {
                return false;
            }
            if (url.Contains(";") || url.Contains(":"))
            {
                return false;
            }

            return true;
        }
    }
}

2,將獲取到的諸如「[Scripts/common/jquery][Scripts/functionA/A1,A2][Scripts/functionB/B1,B2]」類型的路徑參數,拆分爲單獨的路徑數組,並獲取其真實的物理路徑,以便讀取內容。數組

3,調用JsCombiner.CombineJs方法,傳入物理路徑集合,對腳本文件進行合併。若是標記壓縮,則須要進行壓縮處理。
其中,JsCombiner.CombineJs 方法代碼爲:瀏覽器

namespace Mcmurphy.Common
{
    public class JsCombiner
    {
        /// <summary>
        /// 拼接js文件內容
        /// </summary>
        /// <param name="jsPaths"></param>
        /// <param name="serverPath"></param>
        /// <param name="isCompress"></param>
        /// <returns></returns>
        public static string CombineJs(string[] jsPaths, string serverPath, bool isCompress)
        {
            var jsBuilder = new StringBuilder();
            foreach (string path in jsPaths)
            {
                if (File.Exists(path))
                {
                    var sourceFileContent = AutoDetectEncodingFileReader.ReadFileContent(path, Encoding.Default);
                    if (string.IsNullOrEmpty(sourceFileContent) == false)
                    {
                        if (isCompress)
                        {
                            var compressor = new JavaScriptCompressor();
                            //ASCII, BigEndianUnicode, Unicode, UTF32, UTF7, UTF8, Default (default).
                            compressor.Encoding = Encoding.UTF8;
                            //True (default) | False.  True => Obfuscate function and variable names
                            compressor.ObfuscateJavascript = true;
                            //True | False (default).  True => compress any functions that contain 'eval'. Default is False, which means a function that contains
                            compressor.IgnoreEval = false;
                            //True | False (default).
                            compressor.DisableOptimizations = false;
                            //True | False (default).  True => preserve redundant semicolons (e.g. after a '}'
                            compressor.PreserveAllSemicolons = false;
                            //The position where a line feed is appened when the next semicolon is reached. 
                            compressor.LineBreakPosition = -1;
                            sourceFileContent = compressor.Compress(sourceFileContent);
                        }
                        jsBuilder.Append(sourceFileContent);
                    }
                }
                else
                {
                    jsBuilder.Append(string.Format("/*未找到文件{0}*/", path.Replace(serverPath, "")));
                }
            }
            return jsBuilder.ToString();
        }
    }
}

由上述代碼能夠看到,對腳本文件的壓縮,咱們調用了 YUI Compressor for .Net 這個第三方類庫。緩存

YUI Compressor for .Net 爲腳本的壓縮提供了豐富的選項,好比是否進行代碼混淆,是否自動在'}'後添加分號,是否進行代碼優化,錯誤日誌記錄,文件編碼等。上面的代碼僅僅是最常規的應用。更多信息能夠參考其開源地址:http://yuicompressor.codeplex.com/,在該頁面的 documentation 標籤下,能夠查看到更多的使用說明。服務器

須要說明的是,在調用最新的YUI Compressor for .Net 對腳本文件進行壓縮時,須要添加三個引用:網絡

EcmaScript.NET.dll
Yahoo.Yui.Compressor.Build.MsBuild.dll
Yahoo.Yui.Compressor.dll

4,將合併[壓縮]後的內容,以UTF-8 BOM的字符流形式輸出至客戶端。

其中EncodingHelper.ConvertToUTF8BomEncodingStringBytes代碼爲:

namespace Mcmurphy.Common
{
    public static class EncodingHelper
    {
        /// <summary>
        /// 爲字符串添加BOM頭信息
        /// 讓客戶端正確識別UTF-8編碼
        /// </summary>
        /// <param name="originalString"></param>
        /// <returns></returns>
        public static byte[] ConvertToUTF8BomEncodingStringBytes(string originalString)
        {
            var bom = Encoding.UTF8.GetPreamble();
            var content = Encoding.UTF8.GetBytes(originalString);
            var resultBytes = new byte[bom.Length + content.Length];
            bom.CopyTo(resultBytes, 0);
            content.CopyTo(resultBytes, bom.Length);
            return resultBytes;
        }
    }
}

接下來咱們定位到Mcmurphy.Web項目,在Scripts文件夾下添加幾個腳本文件,測試一下。

Scripts/common/jquery.js   

Scripts/functionA/A1.js :   
$(function() {
    console.log("folder:functionA,name:A1.js");
});

Scripts/functionA/A2.js :
$(function() {
    console.log("folder:functionA,name:A2.js");
});

Scripts/functionB/B1.js :
$(function () {
    console.log("folder:functionB,name:B1.js");
});

Scripts/functionB/B2.js :   
$(function () {
    console.log("folder:functionB,name:B2.js");
});

而後咱們將 Views/Shared/_Layout.cshtml 佈局文件修改成:

<!DOCTYPE html>
<html>
<head>
    <title>@ViewBag.Title</title>
    @{
        Html.AppendResFile(ResourceType.StyleSheet, "[Content/common]");
        Html.AppendResFile(ResourceType.Script, "[Scripts/common/jquery]");
        
        @RenderSection("Head_Section", false)
    }
    @Html.RenderResFile(ResourceType.StyleSheet)
</head>

<body>
    @RenderBody()
    
    @RenderSection("Foot_Section", false)

    @Html.RenderResFile(ResourceType.Script)
</body>
</html>

再定位到 Views/Home/Index.cshtml 文件,修改其內容爲:

@{
    ViewBag.Title = "Index";
}
@section Head_Section{
    @{
        //添加樣式文件A
        Html.AppendResFile(ResourceType.StyleSheet, "[Content/Styles/styleA]"); 
        //添加樣式文件B,但設置了高優先級
        Html.AppendResFile(ResourceType.StyleSheet, "[Content/Styles/styleB]","",PriorityType.High);

        //添加腳本文件functionA/A1,functionB/B1
        Html.AppendResFile(ResourceType.Script, "[Scripts/functionA/A1],[Scripts/functionB/B1]");
    }
}
@section Foot_Section{
    @{
        //添加腳本文件functionA/A2,functionB/B2
        Html.AppendResFile(ResourceType.Script, "[Scripts/functionA/A2],[Scripts/functionB/B2]");
    }
}
<h2>Index</h2>

(關於ASP.NET MVC及Razor,在此再也不贅述)

Okay,Ctrl + F5 直接運行。查看瀏覽器網絡請求及控制檯

能夠看到已合併的樣式及腳本文件網絡請求,而控制檯也獲得了正確的輸出。

固然,也能夠直接在瀏覽器地址欄輸出Url,查看到合併[壓縮]後的腳本或樣式文件。

http://localhost:17509/Resource/Script?href=[Scripts/common/jquery][Scripts/functionA/A1,A2][Scripts/functionB/B1,B2]&compress

或者:

http://localhost:17509/Resource/Style?href=[Content/Styles/styleB][Content/common][Content/Styles/styleA]&compress

至此關於JS&CSS文件的請求合併及壓縮處理的實現就告一段落。固然,在此優化的基礎上其實還有不少其它方面的工做可作。好比可將Resource/[Script][Style]的處理,放到獨立的資源服務器,能夠獲得額外的CDN的好處。另外,諸如腳本樣式等文件一般都爲靜態資源,並不會常常變更,能夠考慮在服務器作一些緩存及版本號方面的控制等。

最後附上所有源碼種子:

MvcResourceHandle.rar

相關文章
相關標籤/搜索