Microsoft.AspNet.Web.Optimization.Bundle的完美替換方案

known發表於2018-07-14

Web應用程式中包含大量的樣式(css)和指令碼(js)檔案,這些檔案的引用、管理和釋出有很多解決方案。在Asp.Net MVC應用程式中,大家最熟悉的解決方案應屬Microsoft.AspNet.Web.Optimization這個package。這個package的使用也挺方便,對我來說,它依賴太多package,這點不合我胃口,我是比較崇尚精簡的那種。接下來介紹這個package的使用及如何將它完美的替換。

1. Microsoft.AspNet.Web.Optimization的Bundle使用

將要合併的檔案新增到BundleTable.Bundles集合中即可,樣式檔案使用StyleBundle類,指令碼檔案使用ScriptBundle類。示例如下:

public class BundleConfig
{
    public static void RegisterBundles(BundleCollection bundles)
    {
        var style = new StyleBundle("~/Content/login")
            .Include("~/Content/common.css", "~/Content/login.css");
        bundles.Add(style);

        var script = new ScriptBundle("~/Scripts/login")
            .Include("~/Scripts/common.js", "~/Scripts/login.js");
        bundles.Add(script);
    }
}

View頁面使用Styles和Scripts兩個類來呈現。示例如下:

@Styles.Render("~/Content/login")
@Scripts.Render("~/Scripts/login")

這裡只簡單介紹一下Bundle的使用。個人覺得主要有如下問題:

  • 依賴過多的package,有WebGrease、Antlr、Newtonsoft.Json;
  • 不同資料夾的樣式檔案不能同時輸出一個min檔案,若包在一起時,有些樣式檔案引用的圖片無法顯示,這個問題我沒想去解決,有了上面那一條,也不想去解決它。

2. 完美的替換方案

為了完美替換Microsoft.AspNet.Web.Optimization的Bundle,我採用了Bundler & Minifier這個VS的擴充套件,它可以方便的配置和生成樣式及指令碼的min檔案。這個擴充套件只能生成min檔案,而沒有Bundle那樣可以根據開發環境和生產環境來輸出對應的原始檔和min檔案,不過這個問題很好解決,下面來介紹如何實現。

  • 安裝Bundler & Minifier擴充套件及配置
    在VS中點選“工具-擴充套件和更新-聯機”,再輸入Bundler搜尋,下載,重啟VS完成安裝。
  • Bundle的配置
    它的配置很簡單,配一個outputFileName和inputFiles集合即可,inputFiles可以是檔案,也可以是資料夾。開啟bundleconfig.json檔案,配置示例如下:
[
  {
    "outputFileName": "static/modules/login/index.min.css",
    "inputFiles": [
      "static/modules/login/index.css"
    ]
  },
  {
    "outputFileName": "static/modules/login/index.min.js",
    "inputFiles": [
      "static/libs/jquery.min.js",
      "static/libs/jquery.md5.js",
      "static/modules/core/js",
      "static/modules/login/index.js"
    ]
  }
]
  • 解決開發環境和生產環境輸出特性
    我們知道Web.config檔案有如下節點,可以設定當前程式的環境,可以通過HttpContextBase類的IsDebuggingEnabled屬性來獲取。
<configuration>
  <system.web>
    <compilation debug="true" />
  </system.web>
</configuration>

根據這個節點,我們來實現不同環境下樣式和指令碼檔案的輸出,即開發時輸出原始檔,生產環境下輸出min檔案。我們新增HtmlHelper類的擴充套件方法,一個是MinStyle輸出樣式,一個是MinScript輸出指令碼。View頁面使用如下:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    @Html.MinStyle("static/modules/login/index.min.css")
</head>
<body>
    <div class="login">
        ...
    </div>
    @Html.MinScript("static/modules/login/index.min.js")
</body>
</html>

下面是這兩個擴充套件方法的具體實現:

public static class HtmlExtension
{
    public static IHtmlString MinStyle(this HtmlHelper helper, string path)
    {
        var format = "<link rel="stylesheet" href="/{0}">";
        var html = GetHtmlString(helper, format, path);
        return new HtmlString(html);
    }

    public static IHtmlString MinScript(this HtmlHelper helper, string path)
    {
        var format = "<script src="/{0}"></script>";
        var html = GetHtmlString(helper, format, path);
        return new HtmlString(html);
    }

    private static string GetHtmlString(HtmlHelper helper, string format, string path)
    {
        var random = DateTime.Now.ToString("yyMMddss");
        var html = string.Format(format, $"{path}?r={random}");
        var httpContext = helper.ViewContext.RequestContext.HttpContext;
        if (httpContext.IsDebuggingEnabled)
        {
            var bundle = BundleInfo.GetBundle(httpContext, path);
            if (bundle != null && bundle.HasInputFiles)
            {
                var rootPath = httpContext.Server.MapPath("~/");
                var paths = new List<string>();
                foreach (var inputFile in bundle.inputFiles)
                {
                    var inputPath = rootPath + inputFile;
                    if (File.Exists(inputPath))
                    {
                        paths.Add(string.Format(format, $"{inputFile}?r={random}"));
                    }
                    else if (Directory.Exists(inputPath))
                    {
                        var files = Directory.GetFiles(inputPath);
                        foreach (var file in files)
                        {
                            var filePath = file.Replace(rootPath, "").Replace("\", "/");
                            paths.Add(string.Format(format, $"{filePath}?r={random}"));
                        }
                    }
                }
                html = string.Join(Environment.NewLine, paths);
            }
        }

        return html;
    }

    class BundleInfo
    {
        public string outputFileName { get; set; }
        public List<string> inputFiles { get; set; }

        public bool HasInputFiles
        {
            get { return inputFiles != null && inputFiles.Count > 0; }
        }

        public static BundleInfo GetBundle(HttpContextBase httpContext, string outputFile)
        {
            var jsonFile = httpContext.Server.MapPath("~/bundleconfig.json");
            if (!File.Exists(jsonFile))
                return null;

            var json = File.ReadAllText(jsonFile);
            if (string.IsNullOrWhiteSpace(json))
                return null;

            var bundles = json.FromJson<List<BundleInfo>>();
            if (bundles == null || bundles.Count == 0)
                return null;

            return bundles.FirstOrDefault(b => b.outputFileName == outputFile);
        }
    }
}

相關文章