[ASP.NET] TypeScript + AngularJs 使用 ASP.NET Bundle 與自動判斷載入 JS

介紹使用 TypeScript + AngularJs 於 ASP.NET MVC 上整合使用 ASP.NET Bundle 與自動判斷 js 是否需要載入

前言


這篇就來介紹可以透過怎樣做法來整合 ASP.NET Bundle 綁定你所撰寫的 angularJs 檔案。

 

ASP.NET Bundle


什麼是 ASP.NET Bundle ? 不知道的朋友可以先參考我之前寫的一篇文章 [ASP.NET] Bundling Script and Style 加速網站效能, 簡單的說其主要功用在於綑綁網站上的 js 與 css 檔案,讓網站可以減少針對 js 與 css 檔案的請求數量與壓縮這些檔案的內容。

 

在新建立 MVC 專案時其實也會預設將 Bundle 關聯的套件加入,如果你的專案裡面沒有 ASP.NET Bundle 的套件也沒關係,可以透過 NuGet 搜尋「Web.Optimization」來安裝,如下

 

設定 BundleConfig.cs


在安裝好 ASP.NET Bundle 後,首先需要設定一下 BundleConfig 檔案,在 App_Start 資料夾下,新增一個 BundleConfig.cs 檔案並增加一個 RegisterBundles 的靜態方法用於設計基本需要進行 Bundle 的檔案,如下

public static void RegisterBundles(BundleCollection bundles)
{
    // bundling styles.
    bundles.Add(
        new StyleBundle("~/bundles/css/base")
        .Include(
            "~/Content/angular-block-ui.css",
            "~/Content/bootstrap.css",
            "~/Content/Site.css"
        ));

    // bundling script.
    bundles.Add(
        new ScriptBundle("~/bundles/script/base")
        .Include(
            "~/Scripts/jquery/jquery-2.1.4.js",
            "~/Scripts/angular/angular.js",
            "~/Scripts/angular/angular-block-ui.js"
        ));
          
}

在這裡額外講一件事,就是當你綑綁了 css 檔案時,在 Release 模式時你可能會碰到在 css 檔案內指定路徑的圖片都找不到檔案了,例如當 css 內寫了這樣...

.bg-image {
    border: 1px solid #000000;
    width: 300px;
    height: 300px;
    background-image: url(../Content/Images/bg.jpg);
}

但是在執行 Bundle 之後出現...

其原因就是因為在進行了 Bundle 之後,因為路徑改變了導致 "../content/images/bg.jpg" 這種寫法的去尋找對應路徑的檔案時該位置並不存在,那該怎麼解決呢?

第一種方法就是將 bundle 的路徑減少一個階層,但這種作法不是最佳解,難保以後不會忘記又犯同樣的錯誤。

new StyleBundle("~/bundles/css")

第二種方法就是使用 CssRewriteUrlTransform 類別,在 Include css 檔案的同時,指定預設提供的 CssRewriteUrlTransform 後就會將 css 檔案內的圖片路徑都轉換成網站根目錄為起始的相對路徑 "/Content/Images/bg.jpg",但是這種作法在當你的應用程式是掛在某個站台底下的子應用程式時就會一樣碰到路徑問題。

var styleBundle = new StyleBundle("~/bundles/css/base")
    .Include("~/Content/angular-block-ui.css")
    .Include("~/Content/bootstrap.css")
    .Include("~/Content/Site.css", new CssRewriteUrlTransform());

第三種方法就是站在巨人的肩膀,使用 BundleTransformer 套件所提供的 StyleTransformer,他可以判斷網站或是應用程式進而轉換成對應的相對路徑。

// bundling styles.
var styleBundle = new StyleBundle("~/bundles/css/base")
    .Include("~/Content/angular-block-ui.css")
    .Include("~/Content/bootstrap.css")
    .Include("~/Content/Site.css");
styleBundle.Transforms.Add(new StyleTransformer());
bundles.Add(styleBundle);

CSS 綑綁的題外話說完了,最後建立一個屬於我們自己撰寫的 js 的綑綁,如下

public static void RegisterBundles(BundleCollection bundles)
{
    // bundling styles.
    var styleBundle = new StyleBundle("~/bundles/css/base")
        .Include("~/Content/angular-block-ui.css")
        .Include("~/Content/bootstrap.css")
        .Include("~/Content/Site.css");
    styleBundle.Transforms.Add(new StyleTransformer());
    bundles.Add(styleBundle);

    // bundling script.
    bundles.Add(
        new ScriptBundle("~/bundles/script/base")
        .Include(
            "~/Scripts/libs/jquery/jquery-2.1.4.js",
            "~/Scripts/libs/angular/angular.js",
            "~/Scripts/libs/angular/angular-block-ui.js"
        ));

    bundles.Add(
        new ScriptBundle("~/bundles/script/app")
        .Include(
            "~/Scripts/app/app.js",
            "~/Scripts/app/common.directive.js"
        ));

    //BundleTable.EnableOptimizations = true;
}

在上圖我們只要將基本共用的檔案綑綁到 bundle/script/app 即可,而剩下的那些 js 檔案則使用另外的手法進行。

 

自動判斷瀏覽位置載入對應的 js 檔案


現在就要進入本篇的重點,在這要做的就是當我希望我瀏覽到特定網址的網頁時,能夠自動判斷是否有需要載入的 js 檔案,進而載入到頁面或著也將這些檔案 Bundle 起來。

還記得在 TypeScript + AngularJs 專案架構 這篇文章中,我們所放置的 js 檔案路徑是比照 View 裡面的檔案位置嗎? 這個做的原因也牽扯到現在要做的這個動作。而主要的需求就是當我瀏覽到 http://localhost:8267/Home/HelloWorld 這個網址的時候,系統能夠依據這個網址的路徑去尋找 Scripts/App/Home/ 底下對應的路徑資料夾中是否存在需要載入的 js 檔案,如果有的話就將這些檔案載入至頁面。

要能夠做到這個自動判斷首先需要撰寫一個 Helper 類別,現在建立一個 HtmlRenderHelper 類別與一個 RenderControllerJs 靜態方法,如下

public static class HtmlRenderHelper
{
    /// <summary>
    /// 產生此頁面路徑所需使用的 angular scripts 檔案,並進行綑綁
    /// </summary>
    /// <param name="htmlHelper"></param>
    /// <param name="entryPointRoot">Scripts 檔案進入點,預設為 "~/Scripts" </param>
    /// <returns></returns>
    public static IHtmlString RenderControllerJs(this HtmlHelper htmlHelper, string entryPointRoot = "~/Scripts")
    {

    }
}

接著為了要能夠知道目前瀏覽的頁面的 Route 資訊,需要額外增加一個 GetRoutingInfo 方法,如下

private static RoutingInfo GetRoutingInfo(ViewContext viewContext)
{
    var area = viewContext.RouteData.DataTokens["area"] != null
        ? viewContext.RouteData.DataTokens["area"].ToString() : "Root";
    var controller = viewContext.Controller.ValueProvider.GetValue("controller").RawValue as string;
    var action = viewContext.Controller.ValueProvider.GetValue("action").RawValue as string;
    return new RoutingInfo
    {
        Area = area,
        Controller = controller,
        Action = action
    };
}

再來回到 RenderControllerJs 方法,先加入以下程式碼

// 取得 Route 路徑資訊
var routingInfo = GetRoutingInfo(htmlHelper.ViewContext);
// 預設相依頁面的 Script 子模組檔名
var moduleSubFileNames = new List<string> { "ViewModel", "Directive", "Service", "Controller" };
// 實際存在的 Modules
var hasModule = new List<string>();
// 尋找對應路徑下是否存在此 Script 檔案
foreach (var subModuleName in moduleSubFileNames)
{
    var entryPointImplementPath = $"{entryPointRoot}/App/{routingInfo.Controller}/{routingInfo.Action}.{subModuleName}.js";
    var filePath = htmlHelper.ViewContext.HttpContext.Server.MapPath(entryPointImplementPath);
    if (File.Exists(filePath))
        hasModule.Add(entryPointImplementPath); // 存在則加入集合中
}

// 如不存在任何模組則不做 Render
if (hasModule.Count == 0) return null;

以上程式碼主要的目的在於先取得 routingInfo,之後依照專案架構規範先預設幾個 module 的子檔名,再透過取得的 routingInfo 去尋找在 ~/Scripts/App 路徑底下相對於 Controller 與 Action 名稱的 js 檔案是否存在,如果存在則先加入一個暫存集合中。如果都沒有存在的檔案則不進行後續處理。

最後,要進行的就是 Bundle 的處理。加入以下程式碼,如下

var bundleRenderPath = $"~/bundles/app/{routingInfo.Controller.ToLower()}";
var bundle = BundleTable.Bundles.GetBundleFor(bundleRenderPath);
if (bundle != null) // 如果存在舊的 bundle,則先刪除後再使用新的
    BundleTable.Bundles.Remove(bundle);

bundle = new Bundle(bundleRenderPath);
foreach (var path in hasModule)
{
    bundle.Include(path); // 加入需要 bundle 的 Scripts
}

BundleTable.Bundles.Add(bundle);
return Scripts.Render(bundleRenderPath);

在以上程式碼中,預設的 Bundle 後路經將是 "/bundles/app/controllerName",例如 "/bundles/app/home" 將會依據不同的 controller name 變更,接著從原本已設定好的 Bundles 中尋找是否有一樣名稱的,如果有就先移除以確保之後加入的是正確的,最後再將之前所暫存的 module 路徑一個一個 Include 進 bundle 中後輸出。

2016/02/19 補充
由於 ASP.NET Bundle 會 Cache 該 Bundle 路徑的所綑綁的 JS,所以以上的程式碼在 Runtime 執行時候,會發生同一個路徑回傳的 JS 是舊的情況,所以以上程式碼必須要再修改一下。    

在此提供兩種方式

1.將 bundleRenderPath 每次產生的路徑都變成不一樣,也就是在字串結尾加入亂數英數字,例如:

var bundleRenderPath = $"~/bundles/app/{routingInfo.Controller.ToLower()}_{RandomHelper.GetString()}";

2.直接更新 BundleTable 的 Cache 內容,如下: 

bundle = new Bundle(bundleRenderPath);
foreach (var path in hasModule)
{
    bundle.Include(path); // 加入需要 bundle 的 Scripts
}

// 更新 Cache 中的內容
var context = new BundleContext(new HttpContextWrapper(HttpContext.Current), BundleTable.Bundles, bundle.Path);
bundle.UpdateCache(context, bundle.GenerateBundleResponse(context));

BundleTable.Bundles.Add(bundle);
return Scripts.Render(bundleRenderPath);

 

在經過了以上這些動作之後,現在你只需要在 _Layout.cshtml 樣板檔中加入 Html.RenderControllerJs() 即可自動判斷是否需要載入對應的 js 檔案,如下

 

結語


以上就是在不使用 RequireJs 的情況下,使用 ASP.NET Bundle 配合自動判斷載入的方式來降低頁面所需要載入 js 的方法,所以基本上只要依照 app 專案架構規範的方式去建立 js 檔案,就能夠套用這種模式來進行開發而不需要另外再去針對每個不同的頁面去手動載入 js 檔案,降低錯誤的發生。

但是我覺得這個方法也還不是最好的做法,更好的做法就是能夠連需要載入的第三方套件都能夠動態判斷載入,不過目前我還沒有想到更好的做法就是了,以上就是本篇的介紹。

 

範例程式碼


TypeScriptAngularJsArticleExample

 

 


以上文章敘述如有錯誤及觀念不正確,請不吝嗇指教
如有侵權內容也請您與我反應~謝謝您 :)