JS&CSS文件请求合并及压缩处理研究(三)

上篇我们进行了一些代码方面的准备工作。接下来的逻辑是:在View页面解析时,通过 Html.AppendResFile 方法添加的资源文件,我们需要按照分组、优先级,文件名等条件,对其路径进行合并。具体的合并规则如下:

(1),优先级高的文件优先渲染。

假如页面中有以下文件添加代码:

//添加样式文件A
Html.AppendResFile(ResourceType.StyleSheet, "[Content/Styles/styleA]"); 
//添加样式文件B,但设置了高优先级
Html.AppendResFile(ResourceType.StyleSheet, "[Content/Styles/styleB]","",PriorityType.High);

对于 styleB 来说,虽然较 styleA 后添加,但由于指定其优先级为 High(styleA为默认优先级:Normal),则路径合并之后,其位置反而提前。

合并后路径示例如下:

<link href="/Resource/style?href=[Content/Styles/styleB,styleA]&compress" type="text/css" rel="stylesheet" charset="utf-8" />

PS:在Resource/style处理程序中,我们会按照这个顺序对文件进行合并及压缩。

(2), 不同的分组分开渲染。

假如页面中:

Html.AppendResFile(ResourceType.StyleSheet, "[Content/Styles/styleA]","groupA"); 
Html.AppendResFile(ResourceType.StyleSheet, "[Content/Styles/styleB]","groupB"); 

经过路径合并后渲染为:

<link href="/Resource/style?href=[Content/Styles/styleA]&compress" type="text/css" rel="stylesheet" charset="utf-8" />
<link href="/Resource/style?href=[Content/Styles/styleB]&compress" type="text/css" rel="stylesheet" charset="utf-8" />

(3),相同文件夹下的文件,合并渲染。

这一点在上述(1)中就有所体现,同属文件夹 Content/Styles 下的styleA,styleB,路径合并后会位于同一个[]中。

(4),可同时添加多个资源文件。

Html.AppendResFile(ResourceType.Style, "[folderA/A1,A2,A3],[folderB/B1],[folderC/C1]");

路径合并后为:

<link href="/Resource/style?href=[folderA/A1,A2,A3][folderB/B1][folderC/C1]&compress" type="text/css" rel="stylesheet" charset="utf-8" />

通过上述合并规则,结合 Resource/style 端的处理,可以对资源文件的加载进行智能且精确的控制。

下面我们参照上述规则,继续完善 CombineResourceExt 类。在开始之前,需要先解决一个问题:

我们调用Html.AppendResFile方法添加的文件,肯定需求临时保存起来,以供最后统一合并路径并输出。那么我们将其保存在何处?

这里我们的选择是:HtmlHelper 对象的 HttpContext.Items,(对应ASP.NET为HttpContext.Current.Items)。
HttpContext.Items 仅在一次Http会话中存活。对于ASP.NET应用程序,多用于IHttpModule 和 IHttpHandler 之间的共享数据。
我们可以方便的以键值对的形式向其存放数据。当然,由于 HttpContext.Items 的流动性,体积大的数据其实是不适合保存在其中的。

解决了数据存放的问题,下面看代码:

     /// <summary>
        /// 添加资源文件
        /// </summary>
        /// <param name="htmlHelper"></param>
        /// <param name="resType">资源类型</param>
        /// <param name="url">文件路径</param>
        /// <param name="group">文件分组名称</param>
        /// <param name="order">文件同组中的优先级。默认:Normal</param>
        public static void AppendResFile(this HtmlHelper htmlHelper, ResourceType resType, string url, string group = "", PriorityType order = PriorityType.Normal)
        {
            var pathArray = QueryToFileList(url);
            AppendResFile(htmlHelper, resType, pathArray, group, order);
        }

        /// <summary>
        /// 添加资源文件
        /// </summary>
        /// <param name="htmlHelper"></param>
        /// <param name="resType">资源类型</param>
        /// <param name="urls">文件的路径列表,如“channel/fanbuxie/urlcommon”,不支[]限定符</param>
        /// <param name="group">文件的分组名称</param>
        /// <param name="order">文件同组中的优先级,默认:Normal</param>
        public static void AppendResFile(this HtmlHelper htmlHelper, ResourceType resType, string[] urls, string group = "", PriorityType order = PriorityType.Normal)
        {
            Dictionary<string, ResourceInfo> resFiles;
            var urlArray = urls.Where(s => !string.IsNullOrWhiteSpace(s)).ToArray();
            if (urlArray.Length == 0)
            {
                return;
            }
            var key = String.Format("{0}_{1}", resType, ConAppendFileKey);
            if (htmlHelper.ViewContext.HttpContext.Items.Contains(key))
            {
                resFiles = htmlHelper.ViewContext.HttpContext.Items[key] as Dictionary<string, ResourceInfo>;
            }
            else
            {
                resFiles = new Dictionary<string, ResourceInfo>();
                htmlHelper.ViewContext.HttpContext.Items.Add(key, resFiles);
            }
            foreach (var urlItem in urlArray)
            {
                if (resFiles.Keys.Contains(urlItem)) //如果已添加,则更新分组和优先级信息
                {
                    resFiles[urlItem].Group = @group;
                    resFiles[urlItem].Order = order;
                }
                else
                {
                    resFiles.Add(urlItem, new ResourceInfo { Url = urlItem, Group = @group, Order = order });
                }
            }
            htmlHelper.ViewContext.HttpContext.Items[key] = resFiles;
        }


        /// <summary>
        /// 请求参数拆分成为文件列表
        /// </summary>
        /// <param name="queryContent"></param>
        /// <returns></returns>
        private static string[] QueryToFileList(string queryContent)
        {
            var originQuery = queryContent.Replace("\", "/").Replace(" ", "");
            var pathSegments = new List<string>();
            var filePathBuilder = new StringBuilder(50);
            var isCurrentCharInPathGroup = false;
            foreach (char currentChar in originQuery)
            {
                if (currentChar == '[')
                {
                    isCurrentCharInPathGroup = true;
                    filePathBuilder.Append(currentChar);
                    continue;
                }
                if (currentChar == ']')
                {
                    isCurrentCharInPathGroup = false;
                    filePathBuilder.Append(currentChar);
                    pathSegments.Add(filePathBuilder.ToString());
                    filePathBuilder.Clear();
                    continue;
                }
                if (isCurrentCharInPathGroup)
                {
                    filePathBuilder.Append(currentChar);
                    continue;
                }
                if (currentChar == ',')
                {
                    if (filePathBuilder.Length > 0)
                    {
                        pathSegments.Add(filePathBuilder.ToString());
                        filePathBuilder.Clear();
                    }
                    continue;
                }
                filePathBuilder.Append(currentChar);
            }
            if (filePathBuilder.Length > 0)
            {
                pathSegments.Add(filePathBuilder.ToString());
                filePathBuilder.Clear();
            }

            var paths = new List<string>();
            foreach (var pathSegment in pathSegments)
            {
                if (pathSegment.StartsWith("[") && pathSegment.EndsWith("]"))
                {
                    var clearedPathSegment = pathSegment.TrimStart('[').TrimEnd(']');
                    var splitterIndex = clearedPathSegment.LastIndexOf('/');
                    var pathPrefix = clearedPathSegment.Substring(0, splitterIndex + 1);
                    var pathContents = clearedPathSegment.Substring(splitterIndex + 1).Split(',');

                    paths.AddRange(pathContents.Select(pathContent => pathPrefix + pathContent));
                }
                else
                {
                    paths.Add(pathSegment);
                }
            }

            return paths.Distinct().ToArray();
        }

有了上述逻辑,假如我们在页面中这样调用:

Html.AppendResFile(ResourceType.Style, "[folderA/A1,A2,A3],[folderB/B1],[folderC/C1]");

则会首先将url参数分拆成独立路径的数组:

["folderA/A1","folderA/A2","folderA/A3","folderB/B1","folderC/C1"]

然后在 AppendResFile 的重载方法中,我们对每一个单独的文件路径,构建 ResourceInfo 对象,并存放于一个字典。最后将包含所有 ResourceInfo 信息的字典,存放于

htmlHelper.ViewContext.HttpContext.Items 中。以供最后统一的路径合并。

细心的朋友可能已经发现,我们为 Append 方法定义了一个 ConAppendFileKey 常量。假如我们现在Append的是脚本资源,则htmlHelper.ViewContext.HttpContext.Items 中存放字典的键为:

String.Format("{0}_{1}", "Script", ConAppendFileKey);

Append样式文件则为:

String.Format("{0}_{1}", "StyleSheet", ConAppendFileKey);

之所以定义这个键,是因为我们在开发中有可能会在需要的时候移除之前添加的某个资源或资源分组,那么我们就需要定义一个键标识移除资源字典。另外,我们还定义了一个键标记添加的代码块。代码如下:

private const string ConAppendFileKey = "AppendFileKey";
private const string ConRemoveFileKey = "RemoveFileKey";
private const string ConRemoveGroupKey = "RemoveGroupKey";
private const string ConScriptBlockKey = "ScriptBlockKey";

我们继续添加向CombineResourceExt类中添加移除资源的代码:

     /// <summary>
        /// 移除资源文件
        /// </summary>
        /// <param name="htmlHelper"></param>
        /// <param name="resType">资源类型</param>
        /// <param name="url">移除文件路径,可以为空或null</param>
        /// <param name="group">移除文件所在分组,可以为null</param>
        public static void RemoveResFile(this HtmlHelper htmlHelper, ResourceType resType, string url, string group = "")
        {
            var urlArray = QueryToFileList(url);
            RemoveResFile(htmlHelper, resType, urlArray, group);
        }

        /// <summary>
        /// 移除资源文件
        /// </summary>
        /// <param name="resType">资源类型</param>
        /// <param name="htmlHelper"></param>
        /// <param name="urls">移除文件列表,可以为空或则null </param>
        /// <param name="group">移除文件所在的组可以为null</param>
        public static void RemoveResFile(this HtmlHelper htmlHelper, ResourceType resType, string[] urls, string group = "")
        {
            var urlArray = urls.Where(s => !string.IsNullOrWhiteSpace(s)).ToArray();

            //按照地址移除
            if (urlArray.Length > 0)
            {
                List<string> removeFileKeys;
                var key = string.Format("{0}_{1}", resType.ToString(), ConRemoveFileKey);

                if (htmlHelper.ViewContext.HttpContext.Items.Contains(key))
                {
                    removeFileKeys = htmlHelper.ViewContext.HttpContext.Items[key] as List<string>;
                }
                else
                {
                    removeFileKeys = new List<string>();
                    htmlHelper.ViewContext.HttpContext.Items.Add(key, removeFileKeys);
                }
                foreach (var urlItem in urlArray)
                {
                    var url = urlItem.Trim().ToLower();
                    if (!removeFileKeys.Contains(url))
                    {
                        removeFileKeys.Add(url);
                    }
                }
            }

            //按照group移除
            if (!string.IsNullOrEmpty(group))
            {
                List<string> removeGroupKeys;
                var keyGroup = string.Format("{0}_{1}", resType.ToString(), ConRemoveGroupKey);
                if (htmlHelper.ViewContext.HttpContext.Items.Contains(keyGroup))
                {
                    removeGroupKeys = htmlHelper.ViewContext.HttpContext.Items[keyGroup] as List<string>;
                }
                else
                {
                    removeGroupKeys = new List<string>();
                    htmlHelper.ViewContext.HttpContext.Items.Add(keyGroup, removeGroupKeys);
                }
                if (!removeGroupKeys.Contains(group))
                {
                    removeGroupKeys.Add(group);
                }
            }
        }

可以看到,我们依然将标记为移除的资源字典存放于htmlHelper.ViewContext.HttpContext.Items中,同时,允许按url地址移除和分组移除。

对于添加代码块的功能:

     /// <summary>
        /// 添加内嵌脚本或样式
        /// </summary>
        /// <param name="htmlHelper"></param>
        /// <param name="resType"></param>
        /// <param name="script"></param>
        public static void AppendScriptBlock(this HtmlHelper htmlHelper, ResourceType resType, string script)
        {
            var key = string.Format("{0}_{1}", ConScriptBlockKey, resType);
            if (htmlHelper.ViewContext.HttpContext.Items.Contains(key))
            {
                var sb = htmlHelper.ViewContext.HttpContext.Items[key] as StringBuilder;
                sb.Append(script);
            }
            else
            {
                var sb = new StringBuilder();
                sb.Append(script);
                htmlHelper.ViewContext.HttpContext.Items[key] = sb;
            }
        }

在最终的合并之前,让我们先整理一下HttpContext.Items中都存放了哪些数据(以脚本文件为例):

  • Items["Script_AppendFileKey"],value为一个字典,存放了所有调用AppendResFile方法添加的脚本资源信息。
  • Items["Script_RemoveFileKey"],value为一个字典,存放了所有标记为已移除脚本资源的信息。
  • Items["Script_RemoveGroupKey"],value为一个字典,存放了所有标记为已移除脚本分组的信息。
  • Items["Script_ScriptBlockKey"],value为一个StringBuilder,存放了所有添加的代码块信息。

接下来,我们按照之前列出的合并规则进行路径合并。首先需要取出 Items["Script_AppendFileKey"] 中所有的资源信息,然后过滤掉 Items["Script_RemoveFileKey"] 和 Items["Script_RemoveGroupKey"],再对过滤后的数据进行合并处理。代码如下:

     /// <summary>
        /// 输出合并后路径
        /// </summary>
        /// <param name="htmlHelper"></param>
        /// <param name="resType">资源文件类型</param>
        /// <returns></returns>
        public static MvcHtmlString RenderResFile(this HtmlHelper htmlHelper, ResourceType resType)
        {
            var keyAppend = string.Format("{0}_{1}", resType, ConAppendFileKey);
            var keyRemove = string.Format("{0}_{1}", resType, ConRemoveFileKey);
            var keyRemoveGroup = string.Format("{0}_{1}", resType, ConRemoveGroupKey);
            var keyScriptBlock = string.Format("{0}_{1}", ConScriptBlockKey, resType);

            var content = new StringBuilder();
            if (htmlHelper.ViewContext.HttpContext.Items.Contains(keyAppend))
            {
                var resFiles = htmlHelper.ViewContext.HttpContext.Items[keyAppend] as Dictionary<string, ResourceInfo>;
                //取出已标记的移除的资源文件
                var removeFileKey = new List<string>();
                if (htmlHelper.ViewContext.HttpContext.Items.Contains(keyRemove))
                {
                    removeFileKey = htmlHelper.ViewContext.HttpContext.Items[keyRemove] as List<string>;
                }
                //取出已标记的移除的资源分组
                var removeGroupKey = new List<string>();
                if (htmlHelper.ViewContext.HttpContext.Items.Contains(keyRemoveGroup))
                {
                    removeGroupKey = htmlHelper.ViewContext.HttpContext.Items[keyRemoveGroup] as List<string>;
                }
                //过滤资源文件(不包含标记为已移除的文件和分组)
                var files = resFiles.Select(x => x.Value)
                    .Where(x => !removeFileKey.Contains(x.Url) && !removeGroupKey.Contains(x.Group)).ToList();


                var jsGroupFiles = files.OrderByDescending(x => x.Group).GroupBy(x => x.Group);

                //按分组输出合并后的资源文件
                foreach (IGrouping<string, ResourceInfo> item in jsGroupFiles)
                {
                    var resPath = CombinePath(item.ToArray());
                    switch (resType)
                    {
                        case ResourceType.Script:
                            content.Append(string.Format("<script type="text/JavaScript" src="Resource/script?href={0}&compress"></script>", resPath));
                            break;
                        case ResourceType.StyleSheet:
                            content.Append(string.Format("<link href="Resource/style?href={0}&compress" type="text/css" rel="stylesheet" charset="utf-8" />", resPath));
                            break;
                    }
                }
            }
            //输出样式或者脚本文件代码块
            if (htmlHelper.ViewContext.HttpContext.Items.Contains(keyScriptBlock))
            {
                var script = htmlHelper.ViewContext.HttpContext.Items[keyScriptBlock] as StringBuilder;
                switch (resType)
                {
                    case ResourceType.Script:
                        content.Append(string.Format("<script type="text/JavaScript">{0}</script>", script));
                        break;
                    case ResourceType.StyleSheet:
                        content.Append(string.Format("<style type="text/css">{0}</style>", script));
                        break;
                }
            }
            return new MvcHtmlString(content.ToString());
        }

     /// <summary>
        /// 合并资源文件路径 
        /// </summary>
        /// <param name="items"></param>
        /// <returns></returns>
        private static string CombinePath(ResourceInfo[] items)
        {
            if (!items.Any()) return String.Empty;
            //按优先级分组,控制优先级高的先输出
            var orderGroup = items.OrderBy(x => x.Order).GroupBy(x => x.Order).ToArray();
            var sb = new StringBuilder();
            foreach (var item in orderGroup)
            {
                var order = 1;
                var files = item.Select(x =>
                {
                    var lastIndex = x.Url.LastIndexOf('/');
                    var prefix = x.Url.Substring(0, lastIndex);
                    var fileName = x.Url.Substring(lastIndex + 1);
                    return new { Prfx = prefix, FileName = fileName, FileOrder = order++ };
                }).OrderBy(x => x.FileOrder);

                //按资源所属文件夹分组
                //假如有两个文件:A/a.js,A/b.js,属于同一个文件夹
                //则最终输出为:[A/a,b]
                var keysGroup = files.GroupBy(x => x.Prfx).ToArray();
                foreach (var key in keysGroup)
                {
                    var list = files.Where(x => x.Prfx.Equals(key.Key)).ToArray();
                    sb.Append("[" + list[0].Prfx + "/" + list[0].FileName);
                    for (var i = 1; i < list.Length; i++)
                    {
                        sb.Append("," + list[i].FileName);
                    }
                    sb.Append("]");
                }
            }
            return sb.ToString();
        }

关键的地方已添加了注释。

最终,我们在页面中调用:

@Html.RenderResFile(ResourceType.Script)  
//或者  
@Html.RenderResFile(ResourceType.StyleSheet)

则会相应的输出合并后的资源文件路径。

简单的测试一下:

Html.AppendResFile(ResourceType.Script, "[Scripts/common/jquery]");
Html.AppendResFile(ResourceType.Script, "[Scripts/functionA/A1],[Scripts/functionB/B1]");
Html.AppendResFile(ResourceType.Script, "[Scripts/functionA/A2],[Scripts/functionB/B2]");

合并后的路径为:

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

CombineResourceExt 的完整代码为:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web.Mvc;
using Mcmurphy.Component;
using Mcmurphy.Component.Enumeration;

namespace Mcmurphy.Extension
{
    /// <summary>
    /// 资源文件合并扩展
    /// </summary>
    public static class CombineResourceExt
    {
        private const string ConAppendFileKey = "AppendFileKey";
        private const string ConRemoveFileKey = "RemoveFileKey";
        private const string ConRemoveGroupKey = "RemoveGroupKey";
        private const string ConScriptBlockKey = "ScriptBlockKey";

        /// <summary>
        /// 合并资源文件路径 
        /// </summary>
        /// <param name="items"></param>
        /// <returns></returns>
        private static string CombinePath(ResourceInfo[] items)
        {
            if (!items.Any()) return String.Empty;
            //按优先级分组,控制优先级高的先输出
            var orderGroup = items.OrderBy(x => x.Order).GroupBy(x => x.Order).ToArray();
            var sb = new StringBuilder();
            foreach (var item in orderGroup)
            {
                var order = 1;
                var files = item.Select(x =>
                {
                    var lastIndex = x.Url.LastIndexOf('/');
                    var prefix = x.Url.Substring(0, lastIndex);
                    var fileName = x.Url.Substring(lastIndex + 1);
                    return new { Prfx = prefix, FileName = fileName, FileOrder = order++ };
                }).OrderBy(x => x.FileOrder);

                //按资源所属文件夹分组
                //假如有两个文件:A/a.js,A/b.js,属于同一个文件夹
                //则最终输出为:[A/a,b]
                var keysGroup = files.GroupBy(x => x.Prfx).ToArray();
                foreach (var key in keysGroup)
                {
                    var list = files.Where(x => x.Prfx.Equals(key.Key)).ToArray();
                    sb.Append("[" + list[0].Prfx + "/" + list[0].FileName);
                    for (var i = 1; i < list.Length; i++)
                    {
                        sb.Append("," + list[i].FileName);
                    }
                    sb.Append("]");
                }
            }
            return sb.ToString();
        }

        /// <summary>
        /// 请求参数拆分成为文件列表
        /// </summary>
        /// <param name="queryContent"></param>
        /// <returns></returns>
        private static string[] QueryToFileList(string queryContent)
        {
            var originQuery = queryContent.Replace("\", "/").Replace(" ", "");
            var pathSegments = new List<string>();
            var filePathBuilder = new StringBuilder(50);
            var isCurrentCharInPathGroup = false;
            foreach (char currentChar in originQuery)
            {
                if (currentChar == '[')
                {
                    isCurrentCharInPathGroup = true;
                    filePathBuilder.Append(currentChar);
                    continue;
                }
                if (currentChar == ']')
                {
                    isCurrentCharInPathGroup = false;
                    filePathBuilder.Append(currentChar);
                    pathSegments.Add(filePathBuilder.ToString());
                    filePathBuilder.Clear();
                    continue;
                }
                if (isCurrentCharInPathGroup)
                {
                    filePathBuilder.Append(currentChar);
                    continue;
                }
                if (currentChar == ',')
                {
                    if (filePathBuilder.Length > 0)
                    {
                        pathSegments.Add(filePathBuilder.ToString());
                        filePathBuilder.Clear();
                    }
                    continue;
                }
                filePathBuilder.Append(currentChar);
            }
            if (filePathBuilder.Length > 0)
            {
                pathSegments.Add(filePathBuilder.ToString());
                filePathBuilder.Clear();
            }

            var paths = new List<string>();
            foreach (var pathSegment in pathSegments)
            {
                if (pathSegment.StartsWith("[") && pathSegment.EndsWith("]"))
                {
                    var clearedPathSegment = pathSegment.TrimStart('[').TrimEnd(']');
                    var splitterIndex = clearedPathSegment.LastIndexOf('/');
                    var pathPrefix = clearedPathSegment.Substring(0, splitterIndex + 1);
                    var pathContents = clearedPathSegment.Substring(splitterIndex + 1).Split(',');

                    paths.AddRange(pathContents.Select(pathContent => pathPrefix + pathContent));
                }
                else
                {
                    paths.Add(pathSegment);
                }
            }

            return paths.Distinct().ToArray();
        }

        /// <summary>
        /// 添加资源文件
        /// </summary>
        /// <param name="htmlHelper"></param>
        /// <param name="resType">资源类型</param>
        /// <param name="url">文件路径</param>
        /// <param name="group">文件分组名称</param>
        /// <param name="order">文件同组中的优先级。默认:Normal</param>
        public static void AppendResFile(this HtmlHelper htmlHelper, ResourceType resType, string url, string group = "", PriorityType order = PriorityType.Normal)
        {
            var pathArray = QueryToFileList(url);
            AppendResFile(htmlHelper, resType, pathArray, group, order);
        }

        /// <summary>
        /// 添加资源文件
        /// </summary>
        /// <param name="htmlHelper"></param>
        /// <param name="resType">资源类型</param>
        /// <param name="urls">文件的路径列表,如“channel/fanbuxie/urlcommon”,不支[]限定符</param>
        /// <param name="group">文件的分组名称</param>
        /// <param name="order">文件同组中的优先级,默认:Normal</param>
        private static void AppendResFile(this HtmlHelper htmlHelper, ResourceType resType, string[] urls, string group = "", PriorityType order = PriorityType.Normal)
        {
            Dictionary<string, ResourceInfo> resFiles;
            var urlArray = urls.Where(s => !string.IsNullOrWhiteSpace(s)).ToArray();
            if (urlArray.Length == 0)
            {
                return;
            }
            var key = String.Format("{0}_{1}", resType, ConAppendFileKey);
            if (htmlHelper.ViewContext.HttpContext.Items.Contains(key))
            {
                resFiles = htmlHelper.ViewContext.HttpContext.Items[key] as Dictionary<string, ResourceInfo>;
            }
            else
            {
                resFiles = new Dictionary<string, ResourceInfo>();
                htmlHelper.ViewContext.HttpContext.Items.Add(key, resFiles);
            }
            foreach (var urlItem in urlArray)
            {
                if (resFiles.Keys.Contains(urlItem)) //如果已添加,则更新分组和优先级信息
                {
                    resFiles[urlItem].Group = @group;
                    resFiles[urlItem].Order = order;
                }
                else
                {
                    resFiles.Add(urlItem, new ResourceInfo { Url = urlItem, Group = @group, Order = order });
                }
            }
            htmlHelper.ViewContext.HttpContext.Items[key] = resFiles;
        }

        /// <summary>
        /// 移除资源文件
        /// </summary>
        /// <param name="htmlHelper"></param>
        /// <param name="resType">资源类型</param>
        /// <param name="url">移除文件路径,可以为空或null</param>
        /// <param name="group">移除文件所在分组,可以为null</param>
        public static void RemoveResFile(this HtmlHelper htmlHelper, ResourceType resType, string url, string group = "")
        {
            var urlArray = QueryToFileList(url);
            RemoveResFile(htmlHelper, resType, urlArray, group);
        }

        /// <summary>
        /// 移除资源文件
        /// </summary>
        /// <param name="resType">资源类型</param>
        /// <param name="htmlHelper"></param>
        /// <param name="urls">移除文件列表,可以为空或则null </param>
        /// <param name="group">移除文件所在的组可以为null</param>
        public static void RemoveResFile(this HtmlHelper htmlHelper, ResourceType resType, string[] urls, string group = "")
        {
            var urlArray = urls.Where(s => !string.IsNullOrWhiteSpace(s)).ToArray();

            //按照地址移除
            if (urlArray.Length > 0)
            {
                List<string> removeFileKeys;
                var key = string.Format("{0}_{1}", resType.ToString(), ConRemoveFileKey);

                if (htmlHelper.ViewContext.HttpContext.Items.Contains(key))
                {
                    removeFileKeys = htmlHelper.ViewContext.HttpContext.Items[key] as List<string>;
                }
                else
                {
                    removeFileKeys = new List<string>();
                    htmlHelper.ViewContext.HttpContext.Items.Add(key, removeFileKeys);
                }
                foreach (var urlItem in urlArray)
                {
                    var url = urlItem.Trim().ToLower();
                    if (!removeFileKeys.Contains(url))
                    {
                        removeFileKeys.Add(url);
                    }
                }
            }

            //按照group移除
            if (!string.IsNullOrEmpty(group))
            {
                List<string> removeGroupKeys;
                var keyGroup = string.Format("{0}_{1}", resType.ToString(), ConRemoveGroupKey);
                if (htmlHelper.ViewContext.HttpContext.Items.Contains(keyGroup))
                {
                    removeGroupKeys = htmlHelper.ViewContext.HttpContext.Items[keyGroup] as List<string>;
                }
                else
                {
                    removeGroupKeys = new List<string>();
                    htmlHelper.ViewContext.HttpContext.Items.Add(keyGroup, removeGroupKeys);
                }
                if (!removeGroupKeys.Contains(group))
                {
                    removeGroupKeys.Add(group);
                }
            }
        }

        /// <summary>
        /// 添加内嵌脚本或样式
        /// </summary>
        /// <param name="htmlHelper"></param>
        /// <param name="resType"></param>
        /// <param name="script"></param>
        public static void AppendScriptBlock(this HtmlHelper htmlHelper, ResourceType resType, string script)
        {
            var key = string.Format("{0}_{1}", ConScriptBlockKey, resType);
            if (htmlHelper.ViewContext.HttpContext.Items.Contains(key))
            {
                var sb = htmlHelper.ViewContext.HttpContext.Items[key] as StringBuilder;
                sb.Append(script);
            }
            else
            {
                var sb = new StringBuilder();
                sb.Append(script);
                htmlHelper.ViewContext.HttpContext.Items[key] = sb;
            }
        }

        /// <summary>
        /// 输出合并后路径
        /// </summary>
        /// <param name="htmlHelper"></param>
        /// <param name="resType">资源文件类型</param>
        /// <returns></returns>
        public static MvcHtmlString RenderResFile(this HtmlHelper htmlHelper, ResourceType resType)
        {
            var keyAppend = string.Format("{0}_{1}", resType, ConAppendFileKey);
            var keyRemove = string.Format("{0}_{1}", resType, ConRemoveFileKey);
            var keyRemoveGroup = string.Format("{0}_{1}", resType, ConRemoveGroupKey);
            var keyScriptBlock = string.Format("{0}_{1}", ConScriptBlockKey, resType);

            var content = new StringBuilder();
            if (htmlHelper.ViewContext.HttpContext.Items.Contains(keyAppend))
            {
                var resFiles = htmlHelper.ViewContext.HttpContext.Items[keyAppend] as Dictionary<string, ResourceInfo>;
                //取出已标记的移除的资源文件
                var removeFileKey = new List<string>();
                if (htmlHelper.ViewContext.HttpContext.Items.Contains(keyRemove))
                {
                    removeFileKey = htmlHelper.ViewContext.HttpContext.Items[keyRemove] as List<string>;
                }
                //取出已标记的移除的资源分组
                var removeGroupKey = new List<string>();
                if (htmlHelper.ViewContext.HttpContext.Items.Contains(keyRemoveGroup))
                {
                    removeGroupKey = htmlHelper.ViewContext.HttpContext.Items[keyRemoveGroup] as List<string>;
                }
                //过滤资源文件(不包含标记为已移除的文件和分组)
                var files = resFiles.Select(x => x.Value)
                    .Where(x => !removeFileKey.Contains(x.Url) && !removeGroupKey.Contains(x.Group)).ToList();


                var jsGroupFiles = files.OrderByDescending(x => x.Group).GroupBy(x => x.Group);

                //按分组输出合并后的资源文件
                foreach (IGrouping<string, ResourceInfo> item in jsGroupFiles)
                {
                    var resPath = CombinePath(item.ToArray());
                    switch (resType)
                    {
                        case ResourceType.Script:
                            content.Append(string.Format("<script type="text/JavaScript" src="Resource/script?href={0}&compress"></script>", resPath));
                            break;
                        case ResourceType.StyleSheet:
                            content.Append(string.Format("<link href="Resource/style?href={0}&compress" type="text/css" rel="stylesheet" charset="utf-8" />", resPath));
                            break;
                    }
                }
            }
            //输出样式或者脚本文件代码块
            if (htmlHelper.ViewContext.HttpContext.Items.Contains(keyScriptBlock))
            {
                var script = htmlHelper.ViewContext.HttpContext.Items[keyScriptBlock] as StringBuilder;
                switch (resType)
                {
                    case ResourceType.Script:
                        content.Append(string.Format("<script type="text/JavaScript">{0}</script>", script));
                        break;
                    case ResourceType.StyleSheet:
                        content.Append(string.Format("<style type="text/css">{0}</style>", script));
                        break;
                }
            }
            return new MvcHtmlString(content.ToString());
        }
    }
}
View Code

接下来鄙人会创建一个简单的单元测试项目,对之前列出的合并规则进行相应的验证。

原文地址:https://www.cnblogs.com/mcmurphy/p/3342440.html