如何在 ASP.NET MVC 中集成 AngularJS(2)

如何在 ASP.NET MVC 中集成 AngularJS(1)中,我們介紹了 ASP.NET MVC 捆綁和壓縮、應用程序版本自動刷新和工程構建等內容。

下面介紹如何在 ASP.NET MVC 中集成 AngularJS 的第二部分。

ASP.NET 捆綁和壓縮

CSS 和 JavaScript 的捆綁與壓縮功能是 ASP.NET MVC 最流行和有效的特性之一。捆綁和壓縮降低了 HTTP 請求和有效載荷的大小,結果是可以更快和更好的執行 ASP.NET MVC 的網站。有許多可以減少 CSS 和 JavaScript 合併的大小的方法。

捆綁可以很容易地將多個文件合併或捆綁到一個文件中。您可以創建 CSS,JavaScript 和其他包。壓縮可以優化腳本和 CSS 代碼,如去除不必要的空格和註釋,縮短變量名到一個字符。由於捆綁和壓縮降低你的 JavaScript 和 CSS 文件的大小,發送的 HTTP 的字節也會顯著降低。

當配置包文件時,你需要考慮一個捆綁策略以及如何組織你的包文件。下面的 BundleConfig 類是內置的 ASP.NET 捆綁功能的配置文件。在 BundleConfig 類,我決定通過功能模塊來組織我的文件。我爲工程中的每一個文件設置了一個獨立的捆綁,包括對腳本的單獨捆綁,Angular 的核心文件,共享的 JavaScript 文件和主目錄單,客戶目錄和產品目錄。

我創建了客戶和產品目錄的獨立包,帶着這種想法,當用戶請求應用程序的這些源文件時,應以將會動態的加載這些捆綁。由於 AngularJS 是一個純客戶端框架,可以動態加載 ASP.NET 包和服務器端技術,所以這兩項技術相結合,成爲了這個要求具有發佈調試模塊的實例應用的最大開發挑戰。

複製代碼
// BundleConfig.cs
using System.Web;
using System.Web.Optimization;

public class BundleConfig
{
    // For more information on bundling, visit http://go.microsft.com/fwlink/?LinkId=301862
    public static void RegisterBundles(BundleCollection bundles)
    {
        bundles.Add(new ScriptBundle("~/bundles/jquery").Include(
            "~/Scripts/jquery-{version}.js"));

        bundles.Add(new ScriptBundle("~/bundles/bootstrap").Include(
            "~/Scripts/bootstrap.js",
            "~/Scripts/respond.js"
        ));

        bundles.Add(new StyleBundle("~/Content/css").Include(
           "~/Content/bootstrap.css",
           "~/Content/site.css",
           "~/Content/SortableGrid.css",
           "~/Content/angular-block-ui.min.css",
           "~/Content/font-awesome.min.css"
        ));

        bundles.Add(new ScriptBundle("~/bundles/angular").Include(
           "~/Scripts/angular.min.js",
           "~/Scripts/angular-route.min.js",
           "~/Scripts/angular-sanitize.min.js",
           "~/Scripts/angular-ui.min.js",
           "~/Scripts/angular-ui/ui-bootstrap.min.js",
           "~/Scripts/angular-ui/ui-bootstrap-tpls.min.js",
           "~/Scripts/angular-ui.min.js",
           "~/Scripts/angular-block-ui.js"
        ));

        bundles.Add(new ScriptBundle("~/bundles/shared").Include(
           "~/Views/Shared/CodeProjectBootstrap.js",
           "~/Views/Shared/AjaxService.js",
           "~/Views/Shared/AlertService.js",
           "~/Views/Shared/DataGridService.js",
           "~/Views/Shared/MasterController.js"
        ));

        bundles.Add(new ScriptBundle("~/bundles/routing-debug").Include(
           "~/Views/Shared/CodeProjectRouting-debug.js"
        ));

        bundles.Add(new ScriptBundle("~/bundles/routing-production").Include(
           "~/Views/Shared/CodeProjectRouting-production.js"
        ));

        bundles.Add(new ScriptBundle("~/bundles/home").Include(
           "~/Views/Home/IndexController.js",
           "~/Views/Home/AboutController.js",
           "~/Views/Home/ContactController.js",
           "~/Views/Home/InitializeDataController.js"
        ));

 
        bundles.Add(new ScriptBundle("~/bundles/customers").Include(
           "~/Views/Customers/CustomerMaintenanceController.js",
           "~/Views/Customers/CustomerInquiryController.js"
        ));

 
        bundles.Add(new ScriptBundle("~/bundles/products").Include(
           "~/Views/Products/ProductMaintenanceController.js",
           "~/Views/Products/ProductInquiryController.js"
        ));
    }
}
複製代碼

緩存與 ASP.NET 捆綁

使用 ASP.NET 捆綁的優勢是它的“cache busting”的輔助方法,一旦你改變了 CSS 和 JavaScript 的緩存方式,這種方法將會使用自動引導的方式使捆綁的文件能夠更容易的進行緩存。下面的代碼示例是在一個 MVC 的 Razor 視圖中執行的(通常情況下,是在 _Layout.cshtml 母版頁)。所述的 Scripts.Render 方法將會在客戶端渲染,並且當在非調試模式下執行時,它將會產生包的虛擬路徑和結束包的序列號。當你更改包的內容並重新發布你的應用程序時,包將會生成一個新的版本號,這有助於客戶端上的瀏覽器緩存,並生成一個新的下載包。

// _Layout.cshtml
@Scripts.Render("~/bundles/customers")
@Scripts.Render("~/bundles/products")

該 Scripts.Render 功能是一個很好的功能,但在此示例應用程序,我想使用在客戶端一側動態加載的客戶和產品,所以我不能用渲染功能來渲染我的一些包,這是挑戰的開始。這個問題是以如何使用 AngularJS 從客戶端 JavaScript 渲染服務器端的 ASP.NET 包開始的?

_Layout.cshtml - 服務器端啓動代碼

一個使用 ASP.NET MVC 來引導 AngularJS 應用程序的好處是,你可以通過 _Layout.cshtml 主頁中服務器端的代碼,來加載和執行 AngularJS 的代碼。這是第一步,幫助解決我通過客戶端代碼渲染服務器端捆綁的窘境。當然,你可以簡單地嵌入腳本來標記客戶端的代碼,但我需要一種方法來渲染一個包和引用,並維護被追加到清除了緩存的包的目的自動版本號。

開始的時候,我在 _Layout.cshtml 母版頁的頂部編寫了一些服務器端代碼。我所做的頭兩件事情就是讓從程序集信息類中獲取應用的序列號,從應用程序設置中獲取檢索的基本 URL。這兩個都將被之後 HTML 中的 Razor 視圖引擎所解析。

下面的代碼段,產生了我想根據需求動態加載的一些包,我不想當應用啓動時加載所有的前期的包。我需要的信息中的最重要一塊是虛擬路徑和每一次捆綁的長版本號。幸運的是,訪問捆綁信息的方法,本身就是一種捆綁的功能。

下面的代碼行的關鍵行引用了 BundleTable。這行代碼執行了 ResolveBundleUrl 返回了該方法的虛擬路徑以及每個引用的捆綁和版本號。這些代碼基本上生成一個包的列表並且將該列表轉換成一個 JSON 集合。後來這個 JSON 集被添加到 AngularJS。有一個 JSON 集合中的包的信息是,允許從客戶端 AngularJS 應用程序加載服務器端捆綁的最初的方法。

複製代碼
// _Layout.cshtml
@using CodeProject.Portal.Models
@{
    string version = typeof(CodeProject.Portal.MvcApplication).Assembly.GetName().Version.ToString();
    string baseUrl = System.Configuration.ConfigurationManager.AppSettings["BaseUrl"].ToString();

    List<CustomBundle> bundles = new List<CustomBundle>();
    CodeProject.Portal.Models.CustomBundle customBundle;

    List<string> codeProjectBundles = new List<string>();
    codeProjectBundles.Add("home");
    codeProjectBundles.Add("customers");
    codeProjectBundles.Add("products");

    foreach (string controller in codeProjectBundles)
    {
        customBundle = new CodeProject.Portal.Models.CustomBundle();
        customBundle.BundleName = controller;
        customBundle.Path = BundleTable.Bundles.ResolveBundleUrl("~/bundles/" + controller);
        customBundle.IsLoaded = false;
        bundles.Add(customBundle);
    }

    BundleInformation bundleInformation = new BundleInformation();
    bundleInformation.Bundles = bundles;
    string bundleInformationJSON = Newtonsoft.Json.JsonConvert.SerializeObject(
    bundleInformation, Newtonsoft.Json.Formatting.None);

}
複製代碼

ASP.NET 的捆綁類有很多的功能。例如,如果你想通過捆綁所有文件進行迭代,你可以執行 EnumerateFiles 方法,返回一個特定的包內的每個文件的虛擬路徑。

foreach (var file in bundle.EnumerateFiles(new BundleContext(
         new HttpContextWrapper(HttpContext.Current), BundleTable.Bundles, "~/bundles/shared")))
{
    string filePath = file.IncludedVirtualPath.ToString();
}

_Layout.cshtml - 標題

在 HTML 文檔的標題部分,有一個 RequireJS 的參考。該應用程序通過客戶端 AngularJS 代碼使用了 RequireJS 動態的加載包。RequireJS 是一個加載了 JavaScript API 模塊的異步模塊定義(AMD)。RequireJS 有許多功能,但是對於實例應用的目的,僅需要來自於 RequireJS 的請求功能以便在後面應用程序的使用。

此外,Scripts.Render 和 Styles.Render 方法將在開始部分被執行。當應用程序以調試模式執行或者 EnableOptimizations 被指爲 false 時,渲染的方法將會在每一次捆綁中生成多個腳本。當在發佈模式和啓用優化時,渲染方法將生成一個腳本標記來代表整個捆綁的版本戳。

這就導致了另外一個挑戰,那就是應用需要支持發佈模式下生成捆綁腳本標籤的能力,和調試模式下生成獨特文件的腳本標籤的能力。如果你想要在調試模式下爲 JavaScript 代碼設置斷點,這點是很重要的。因爲如果在發佈模式下,使用 JavaScript 代碼的優化捆綁版本是不可能的。

最後,在標題部分,使用 Razor 語法的基本 URL 被早早地設定爲服務器側的基本 URL 變量。

複製代碼
<!-- _Layout.cshtml -->

!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
<meta http-equiv="Pragma" content="no-cache" />
<meta http-equiv="Expires" content="0" />

<title>AngularJS MVC Code Project</titlev>

<script src="~/Scripts/require.js"></script>

@Scripts.Render("~/bundles/jquery")
@Scripts.Render("~/bundles/bootstrap")
@Scripts.Render("~/bundles/modernizr")
@Scripts.Render("~/bundles/angular")

@Styles.Render("~/Content/css")

<base href="#baseUrl" />

</head>
複製代碼

調試模式VS發佈模式

當 EnableOptimizations 被設置爲 false,或者在調試模式運行時,該 @Scripts.Render 方法會在每一次捆綁中產生多種腳本標籤。如果你想設置斷點並調試 JavaScript 文件,這是必要的。你有另一種選擇,就是在調試模式下,使用 RenderFormat 方法來選人客戶腳本標籤。

下面的代碼片段包含在 _layout.cshtml 母版頁中,當應用程序在調試模式下,RenderFormat 會被使用。在這種模式下,應用的版本序列號會被追加到捆綁中的所有JavaScript 文件的腳本標籤中。對於標準的渲染腳本標籤格式不包含追加版本號來說,這也算是個小彌補。

從 Visual Studio 中啓動應用程序時,您可能會遇到瀏覽器緩存的問題。同時也可能會花時間來猜測,你運行的是否是最新版本的 JavaScript 文件。在瀏覽器中按 F5 可以解決這個問題。爲了避免這個問題一起發生,應用程序版本號會被附加到腳本標籤中。使用自動版本插件,版本號會在每次構建中自動遞增。使用這項技術,我能夠知道每一次的編譯和運行使用的是 JavaScript 文件的最新版本,這爲我省了很多時間。

複製代碼
// _Layout.cshtml
@if (HttpContext.Current.IsDebuggingEnabled)
{
    @Scripts.RenderFormat("<script type=\"text/javascript\" src=\"{0}?ver =" + @version + " \">
                           </script>", "~/bundles/shared")
    @Scripts.RenderFormat("<script type=\"text/javascript\" src=\"{0}?ver =" + @version + " \">
                           </script>","~/bundles/routing-debug")
}
else
{
    @Scripts.Render("~/bundles/shared")
    @Scripts.Render("~/bundles/routing-production")
}
複製代碼

服務器端 Razor 數據和 AngularJS 之間的橋樑

現在,我已經創建了服務器端的捆綁數據的收集,接下來的挑戰就是注入並創建服務器端和客戶端 AngularJS 代碼的橋樑。在 _Layout.cshtml 母版頁,我創建了能夠創造一個 AngularJS 供應商的匿名的 JavaScript 功能。最初我計劃創建一個常規的 AngularJS 服務或者一個包含在 _Layout.cshtml 文件中能夠使用 Razor 語法注入服務器端的方法集。

不幸的是,直到 AngularJS 配置階段完成之後,才能提供 AngularJS 服務和方法集,因此我無法在主頁中創建一個沒有 AngularJS 錯誤的服務。爲了克服這個限制,則需要創建一個 AngularJS 的提供者。提供者的功能是,能夠創建提供方法集和服務的實例。提供者允許你在 Angular 配置過程中創建和配置一個服務。

服務提供者名稱是以他們所提供工作的提供商爲開始的。下面的代碼片段中,代碼創建一個“applicationConfiguration”提供商,這個提供商正在被 applicationConfigurationProvider 引用。這個提供商將會在構造函數中被配置,來設定用於動態請求的應用所需的程序集版本號和捆綁列表。MVC Razor 代碼在構造函數中會注入服務器端的數據。

複製代碼
// _Layout.cshtml
(function () {
        var codeProjectApplication = angular.module('codeProject');
        codeProjectApplication.provider('applicationConfiguration', function () {
            var _version;
            var _bundles;
            return {
                setVersion: function (version) {
                _version = version;
            },

            setBundles: function (bundles) {
                _bundles = bundles;
            },

            getVersion: function () {
                return _version;
            },

            getBundles: function () {
                return _bundles;
            },

            $get: function () {
                return {
                    version: _version,
                    bundles: _bundles
                }
            }
       }
    });

    codeProjectApplication.config(function (applicationConfigurationProvider) {
        applicationConfigurationProvider.setVersion('@version');
        applicationConfigurationProvider.setBundles('@Html.Raw(bundleInformationJSON)');
    });
})();
複製代碼

路由產生和動態加載 MVC 捆綁

現在你可能已經看到了很多例子實現了每個內容頁硬編碼路徑的 AngularJS 示例。示例應用程序的路由使用基於約定的方法,這種方法允許路由表使用硬編碼的路由方法來實現使用基於約定的方法。所有的內容頁和相關聯的 JavaScript 文件將會遵循命名約定規則,這個規則允許該應用程序來解析路由並動態地確定每個內容頁需要哪些 JavaScript 文件。

下面的示例應用程序的路由表只需要分析出三條路線:

  • 一個用於根路徑'/'
  • 一個標準路由路徑,如'/:section/:tree'
  • 包含路由參數的路由,如'/:section/:tree/:id' 

我決定從 ASP.NET 捆綁中加載 JavaScript 文件,下面的路由配置代碼需要包含一些 applicationConfigurationProvider 引用的代碼,來用於創建保存之前的捆綁信息。捆綁信息將會被解析爲 JSON 集。捆綁信息集將會用於返回虛擬的捆綁路徑。此外,JSON 集將被用於跟蹤被加載的捆綁。一旦捆綁被加載,就不需要第二次捆綁了。

有幾件事情需要寫入路由代碼中。首先,每當用戶選擇一個頁面來加載一定功能模塊時,對於模塊綁定的所有 JavaScript 文件需要被下載。例如,當用戶選擇客戶模式中的一個內容頁面時,以下的代碼會查看模塊的捆綁是否已經通過 JSON _bundles collection 的 isLoaded 屬性被檢查了,並且如果 isLoaded 爲 false,則捆綁將會被記載, isLoaded 屬性會被設置爲 true。

當確定需要下載哪些模式的捆綁時,有兩件事情需要去加載捆綁:deferred promise 和 RequireJS。deferred promise 可以幫助你異步運行函數,當它完成執行,就會返回。

現在,最後一塊本文之謎是確定從客戶端代碼包中加載的方式。我在以前的文章 CodeProject.com 使用 RequireJS(前面提到的)來動態加載 JavaScript 文件,我使用捆綁來加載 RequireJS。使用 RequireJS“需求”的功能, 我通過捆綁的虛擬路徑進入需求功能。事實證明,需求功能將會加載任何能夠更好執行捆綁加載的路徑。

當我第一次使用 RequireJS 的路徑來下載捆綁時,我已經完成了 RequireJS 和它的所有配置。事實證明,我能夠去掉這一切,只是簡單地加載 RequireJS 庫並使用它的需求功能。我甚至沒有使用 RequireJS 定義表述來預安裝我的動態加載控制器。很多試驗和錯誤之後,我已經達到了本文的目的。我現在可以通過客戶端代碼加載服務器端的捆綁。

複製代碼
// CodeProjectRouting-production.js
​angular.module("codeProject").config(
['$routeProvider', '$locationProvider', 'applicationConfigurationProvider'
    function ($routeProvider, $locationProvider, applicationConfigurationProvider) {
        var baseSiteUrlPath = $("base").first().attr("href");
        var _bundles = JSON.parse(applicationConfigurationProvider.getBundles());
        this.getApplicationVersion = function () {
            var applicationVersion = applicationConfigurationProvider.getVersion();
            return applicationVersion;
        }
        this.getBundle = function (bundleName) {

            for (var i = 0; i < _bundles.Bundles.length; i++) {
                if (bundleName.toLowerCase() == _bundles.Bundles[i].BundleName) {
                    return _bundles.Bundles[i].Path;
                }
            }
        }
        this.isLoaded = function (bundleName) {
            for (var i = 0; i < _bundles.Bundles.length; i++) {
                if (bundleName.toLowerCase() == _bundles.Bundles[i].BundleName) {
                    return _bundles.Bundles[i].IsLoaded;
                }
            }
        }
        this.setIsLoaded = function (bundleName) {
            for (var i = 0; i < _bundles.length; i++) {
                if (bundleName.toLowerCase() == _bundles.Bundles[i].BundleName) {
                    _bundles.Bundles[i].IsLoaded = true;
                    break;
                }
            }
        }
        $routeProvider.when('/:section/:tree',
        {
            templateUrl: function (rp) { return baseSiteUrlPath + 'views/' + 
                         rp.section + '/' + rp.tree + '.html?v=' + this.getApplicationVersion(); },
            resolve: {
                load: ['$q', '$rootScope', '$location', function ($q, $rootScope, $location) {
                    var path = $location.path().split("/");
                    var parentPath = path[1];
                    var bundle = this.getBundle(parentPath);
                    var isBundleLoaded = this.isLoaded(parentPath);
                    if (isBundleLoaded == false) {
                        this.setIsLoaded(parentPath);
                        var deferred = $q.defer();
                        require([bundle], function () {
                            $rootScope.$apply(function () {
                                deferred.resolve();
                            });
                        });
                        return deferred.promise;
                    }
                }]
            }
        });
        $routeProvider.when('/:section/:tree/:id',
        {
            templateUrl: function (rp) { return baseSiteUrlPath + 'views/' + 
                         rp.section + '/' + rp.tree + '.html?v=' + this.getApplicationVersion(); },
            resolve: {
                load: ['$q', '$rootScope', '$location', function ($q, $rootScope, $location) {
                    var path = $location.path().split("/");
                    var parentPath = path[1];
                    var bundle = this.getBundle(parentPath);
                    var isBundleLoaded = this.isLoaded(parentPath);
                    if (isBundleLoaded == false) {
                        this.setIsLoaded(parentPath);
                        var deferred = $q.defer();
                        require([bundle], function () {
                            $rootScope.$apply(function () {
                                deferred.resolve();
                            });
                        });
                        return deferred.promise;
                    }
                }]
            }
        });
        $routeProvider.when('/',
        {
            templateUrl: function (rp) { 
return baseSiteUrlPath + 'views/Home/Index.html?v=' + this.getApplicationVersion(); }, resolve: { load: ['$q', '$rootScope', '$location', function ($q, $rootScope, $location) { var bundle = this.getBundle("home"); var isBundleLoaded = this.isLoaded("home"); if (isBundleLoaded == false) { this.setIsLoaded("home"); var deferred = $q.defer(); require([bundle], function () { $rootScope.$apply(function () { deferred.resolve(); }); }); return deferred.promise; } }] } }); $locationProvider.html5Mode(true); } ]);
複製代碼

以上是如何在 ASP.NET MVC 中集成 AngularJS 的第二部分內容,最後一篇內容會在近期呈現,敬請期待!

 

在本文的第二部分內容中,作者已經解決了在 ASP.NET MVC 中集成 AngularJS 遇到的大部分問題。當我們自己在進行 ASP.NET MVC 和 AngularJS 開始時,也可以藉助開發工具來助力開發過程。ASP.NET MVC開發時,可以藉助 ComponentOne Studio ASP.NET MVC 這一款輕量級控件,它與 Visual Studio 無縫集成,完全與 MVC6 和 ASP.NET 5.0 兼容,將大幅提高工作效率;AngularJS 開發時,可以藉助 Wijmo 這款爲企業應用程序開發而推出的一系列包含 HTML5 和 JavaScript 的開發控件集,無論應用程序是移動端、PC 端、還是必須要支持 IE6,Wijmo 均能滿足需求。

發佈了7 篇原創文章 · 獲贊 1 · 訪問量 8919
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章