AngularJS 和 ASP.NET MVC 入门 - 第二部分
如何快速上手 AngularJS 在 ASP.NET MVC 中的应用:第二部分
系列文章
在第一部分中,我们启动了一个包含以下内容的 AngularJS/ASP.NET MVC 基础应用程序
- 构建一个功能齐全的单页应用程序所需的样板代码
- 一些基础路由,包括一个包含参数的路由和一个只能由经过身份验证的用户访问的路由(使用基于 Cookie 的身份验证)
- 登录和注册表单,包括使这些功能正常运行所需的 Angular Javascript 代码
对于懒惰/时间有限的用户,我已将第一部分结束时的 Visual Studio 解决方案附加到本文。但是,为了减小文件大小,我已经删除了 packages 目录,因此您需要使用 Nuget 包还原才能恢复它们。
我们出色的应用程序仍然存在很多问题,其中一些我们将在第二部分解决。本文比第一部分要短(也可能不那么令人兴奋),但将为第三部分做好一切准备。
源代码
本文随附的源代码可以在 此处找到。
我们将在第二部分解决的问题
- 丑陋的 URL - 当我们在视图之间导航时,我们的 URL 中会出现丑陋的井号(#)。我们将在第二部分通过使用 HTML5 模式(pushstate)来解决这个问题
- 丑陋的视图 - 在第一部分中,我们根本没有尝试为应用程序添加任何样式,让我们添加 Twitter Bootstrap 和 Angular UI Directives for Bootstrap 来增加功能,而无需 jQuery
- 非常基础的路由 - 在第一部分中,我们使用了 ngRoute,这对于基础路由来说还可以,但当我们需要更高级的功能时,它就显得不够了。在第二部分,我们将用 Angular UI Router 替换它
丑陋的 URL
现在我们的 URL 看起来像这样
井号(#)之后的所有内容都被 Web 服务器忽略。我们在第一部分中添加的 ngRoute 目前会获取 URL 的这一部分,检查它是否与我们设置的任何模式匹配,如果匹配,则将正确的视图加载到我们登陆页面的容器 div 中。
如果我们改为使用 AngularJS 的 HTML5 模式,我们可以获得更漂亮的 URL。这将导致 Angular 使用HTML5 History API,只需在我们这边写几行代码(一行 Javascript 和几行 C#)即可处理所有这些复杂性。
先说 Javascript,我们需要修改我们的 apps.config 函数,现在它将 Angular 的 $locationProvider 模块作为依赖项,并调用它的 hashPrefix 和 html5mode 函数。就是这样。
var configFunction = function ($routeProvider, $httpProvider, $locationProvider) { $locationProvider.hashPrefix('!').html5Mode(true); $routeProvider. when('/routeOne', { templateUrl: 'routesDemo/one' }) .when('/routeTwo/:donuts', { templateUrl: function (params) { return '/routesDemo/two?donuts=' + params.donuts; } }) .when('/routeThree', { templateUrl: 'routesDemo/three' }) .when('/login', { templateUrl: '/Account/Login', controller: LoginController }) .when('/register', { templateUrl: '/Account/Register', controller: RegisterController }); $httpProvider.interceptors.push('AuthHttpResponseInterceptor'); } configFunction.$inject = ['$routeProvider', '$httpProvider', '$locationProvider'];
我们的登陆页面上的链接也需要更新,以删除井号
<ul> <li><a href="/routeOne">Route One</a></li> <li><a href="/routeTwo/6">Route Two</a></li> <li><a href="/routeThree">Route Three</a></li> </ul> <ul> <li><a href="/login">Login</a></li> <li><a href="/register">Register</a></li> </ul>
好的,现在让我们调试网站并浏览一下
我们的 URL 看起来好多了,但尝试刷新
HTML5 模式正在工作,但只是在非常表面的程度上。刷新页面会将完整的 URL 发送到服务器(因为我们删除了井号),而服务器不知道该如何处理。我们可以通过正确地重新配置 MVC 的 RouteCollection 来解决这个问题。我们需要明确指定每个视图的路由,然后添加一个“捕获所有”的路由,将所有其他 URL 发送到我们的登陆页面,由 Angular 来处理。
在 App_Start => RouteConfig.cs 中更新 RegisterRoutes 方法,如下所示
public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); routes.MapRoute( name: "routeOne", url: "routesDemo/One", defaults: new { controller = "RoutesDemo", action = "One" }); routes.MapRoute( name: "routeTwo", url: "routesDemo/Two/{donuts}", defaults: new { controller = "RoutesDemo", action = "Two", donuts = UrlParameter.Optional }); routes.MapRoute( name: "routeThree", url: "routesDemo/Three", defaults: new { controller = "RoutesDemo", action = "Three" }); routes.MapRoute( name: "login", url: "Account/Login", defaults: new { controller = "Account", action = "Login" }); routes.MapRoute( name: "register", url: "Account/Register", defaults: new { controller = "Account", action = "Register" }); routes.MapRoute( name: "Default", url: "{*url}", defaults: new { controller = "Home", action = "Index" }); }
再次调试网站,浏览一下,然后测试后退/刷新按钮,一切都应该正常工作了。
丑陋的视图
现在我们的网站根本没有应用任何样式。让我们用 Twitter Bootstrap 来解决这个问题,我将从 Cloudflare 添加它,放在我们登陆页面的 <head> 部分,并把 Angular UI Directives for Bootstrap 添加到 </body> 闭合标签之前。
只需应用几行 CSS 类,并添加几个元素,我们就可以改变整个 Web 应用程序的外观。像这样更新你的登陆页面
<!DOCTYPE html> <html ng-app="AwesomeAngularMVCApp" ng-controller="LandingPageController"> <head> <title ng-bind="models.helloAngular"></title> <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.2.0/css/bootstrap.min.css"> @Styles.Render("~/Content/css") </head> <body> <div class="navbar navbar-default navbar-fixed-top" role="navigation"> <div class="container"> <div class="navbar-header"> <button type="button" class="navbar-toggle" ng-click="navbarProperties.isCollapsed = !navbarProperties.isCollapsed"> <span class="icon-bar"></span> <span class="icon-bar"></span> <span class="icon-bar"></span> </button> <a class="navbar-brand" href="#">Awesome Angular MVC APP</a> </div> <div class="navbar-collapse collapse" collapse="navbarProperties.isCollapsed"> <ul class="nav navbar-nav"> <li><a href="/routeOne">Route One</a></li> <li><a href="/routeTwo/6">Route Two</a></li> <li><a href="/routeThree">Route Three</a></li> </ul> <ul class="nav navbar-nav navbar-right"> <li><a href="/login">Login</a></li> <li><a href="/register">Register</a></li> </ul> </div> </div> </div> <div class="container mainContent"> <div ng-view></div> </div> <script src="//cdnjs.cloudflare.com/ajax/libs/angular.js/1.2.20/angular.min.js"></script> <script src="//cdnjs.cloudflare.com/ajax/libs/angular.js/1.2.20/angular-route.min.js"></script> <script src="//cdnjs.cloudflare.com/ajax/libs/angular-ui-bootstrap/0.10.0/ui-bootstrap-tpls.min.js"></script> @Scripts.Render("~/bundles/AwesomeAngularMVCApp") </body> </html>
将以下内容添加到 Content => Site.css
.mainContent { margin-top: 60px; }
现在,我们需要将 Angular UI Directives for Bootstrap 模块注册到我们的 Angular Application 模块中,我们在 AwesomeAngularMVCApp.js 中完成此操作
var AwesomeAngularMVCApp = angular.module('AwesomeAngularMVCApp', ['ngRoute', 'ui.bootstrap']);
最后,我们需要更新我们的 Angular 登陆页面控制器,使其包含移动导航菜单的默认状态(它将最初是折叠的)
var LandingPageController = function($scope) { ... $scope.navbarProperties = { isCollapsed: true }; }
Twitter Bootstrap 本身就是一个独立的主题。本节的重点是向你展示如何将其正确地添加到任何 AngularJS 应用程序中。
设置移动导航菜单以正确展开和折叠,而无需使用 jQuery,这会让很多人感到困惑,同样,将 Angular UI Directives for Bootstrap 正确注册到你的应用程序模块中也需要技巧,所以我特意在这里进行了介绍。
现在你可以以此为基础,用 Twitter Bootstrap 完全样式化你的网站。
非常基础的路由
我们目前使用的是 AngularJS 自带的 ngRoute 模块,这对于基础路由来说还可以,但如果我们有更高级的路由需求,我们可能会感到有些受限。
让我们用Angular UI Router 替换它。这是一个功能齐全的 Angular 路由框架,为我们提供了诸如嵌套、多视图和命名视图等强大功能。
我们现在将做的足够让我们开始使用,但如果你计划用它来构建一个完整的单页应用程序,你将需要阅读深入指南,并将API 参考保存在附近的标签页以供参考。
更新你的登陆页面,用 Angular UI Router 的脚本标签替换添加 ngRoute 的脚本标签
<script src="//cdnjs.cloudflare.com/ajax/libs/angular-ui-router/0.2.10/angular-ui-router.min.js"></script>
现在我们需要将 Angular UI Router 注册到我们的 Angular Application 模块中,并移除 ngRoute 的注册
var AwesomeAngularMVCApp = angular.module('AwesomeAngularMVCApp', ['ui.router', 'ui.bootstrap']);
Angular UI Router 是基于状态的。它基于有限状态机的数学概念,并将你的 Web 应用程序转化为相同的状态。你不是从 URL 导航到 URL,而是从状态过渡到状态,并为应用程序可能处于的每种状态设置一个路由。
使用 UI Router,我们可以在登陆页面上拥有多个容器视图,而使用 ngRoute 时我们只能有一个。我们现在将在登陆页面添加两个视图,并设置我们的应用程序为四种状态
- 当应用程序处于状态一(State One)时,我们将把路由一(route one)加载到我们的第一个容器 div 中,将路由二(route two)加载到第二个容器 div 中
- 当应用程序处于状态二(State Two)时,我们将把路由一(route one)加载到我们的第一个容器 div 中,将路由三(route three)加载到第二个容器 div 中
- 当应用程序处于状态三(State Three)时,我们将把路由二(route two)加载到我们的第一个容器 div 中,将路由三(route three)加载到第二个容器 div 中
- 当应用程序处于登录/注册状态(LoginRegister state)时,我们将把登录表单加载到第一个容器 div 中,将注册表单加载到第二个容器 div 中
更新登陆页面,使其现在有两个容器 div,并移除之前与 ngRoute 一起使用的容器 div
<div class="container mainContent"> <div class="row"> <div class="col-md-6"> <div ui-view="containerOne"></div> </div> <div class="col-md-6"> <div ui-view="containerTwo"></div> </div> </div> </div>
现在让我们修改 AwesomeAngularMVCApp.js 来告诉它在哪些状态下将哪些视图放置在哪里。我们不再依赖 ngRoute 的 $routeProvider 服务,而是有了 UI Router 的 $stateProvider 这个新依赖项
var configFunction = function ($stateProvider, $httpProvider, $locationProvider) { $locationProvider.hashPrefix('!').html5Mode(true); $stateProvider .state('stateOne', { url: '/stateOne?donuts', views: { "containerOne": { templateUrl: '/routesDemo/one' }, "containerTwo": { templateUrl: function (params) { return '/routesDemo/two?donuts=' + params.donuts; } } } }) .state('stateTwo', { url: '/stateTwo', views: { "containerOne": { templateUrl: '/routesDemo/one' }, "containerTwo": { templateUrl: '/routesDemo/three' } } }) .state('stateThree', { url: '/stateThree?donuts', views: { "containerOne": { templateUrl: function (params) { return '/routesDemo/two?donuts=' + params.donuts; } }, "containerTwo": { templateUrl: '/routesDemo/three' } } }) .state('loginRegister', { url: '/loginRegister?returnUrl', views: { "containerOne": { templateUrl: '/Account/Login', controller: LoginController }, "containerTwo": { templateUrl: '/Account/Register', controller: RegisterController } } }); $httpProvider.interceptors.push('AuthHttpResponseInterceptor'); } configFunction.$inject = ['$stateProvider', '$httpProvider', '$locationProvider'];
我们还需要更新我们的 AuthHttpResponseInterceptor ,以便在服务器返回 401 响应时转到 loginRegister 状态。为了实现这一点,我们需要注入 UI Router 的 $state 服务。然而,由于这个库中的一个 bug,我们无法直接注入它。相反,我们注入 AngularJS 的 $injector 服务,并使用它来解析 $state 的一个实例
var AuthHttpResponseInterceptor = function($q, $location, $injector) { return { response: function (response) { if (response.status === 401) { console.log("Response 401"); } return response || $q.when(response); }, responseError: function (rejection) { if (rejection.status === 401) { $injector.get('$state').go('loginRegister', { returnUrl: $location.path() }); } return $q.reject(rejection); } } } AuthHttpResponseInterceptor.$inject = ['$q', '$location', '$injector'];
我们的 LoginController 也需要更新,因为它现在从 UI Router 的 $stateParams 对象中获取返回 URL,而不是从 ngRoute 的 $routeParams 对象中获取
var LoginController = function ($scope, $stateParams, $location, LoginFactory) { $scope.loginForm = { ...etc returnUrl: $stateParams.returnUrl, ...etc }; ...etc } LoginController.$inject = ['$scope', '$stateParams', '$location', 'LoginFactory'];
最后,我们需要更新我们的链接。有趣的是,我们的超链接不再围绕 URL,现在我们直接链接到状态本身,并使用 JSON 提供该状态的任何参数。更新登陆页面上的链接,使其看起来像这样
<div class="navbar-collapse collapse" collapse="navbarProperties.isCollapsed"> <ul class="nav navbar-nav"> <li><a ui-sref="stateOne({ donuts: 12 })">State One</a></li> <li><a ui-sref="stateTwo">State Two</a></li> <li><a ui-sref="stateThree({ donuts: 4 })">State Three</a></li> </ul> <ul class="nav navbar-nav navbar-right"> <li><a ui-sref="loginRegister">Login / Register</a></li> </ul> </div>
现在让我们调试并浏览一下。导航到状态一(State One)应该会并排返回路由一和路由二
如果我们尝试导航到状态二或状态三(States two or Three),我们的拦截器将像以前一样触发,并将我们转换到 loginRegister 状态。如果此时我们登录,我们将能够看到状态二和状态三。
嵌套视图
我们还没有介绍嵌套视图。让我们向我们的 RoutesDemo 控制器添加另一个 C# Action 方法,名为 Four,并使用 Visual Studio 创建视图。在视图中添加一些内容以唯一标识它。暂时从 Route Three 中删除 Authorize 属性,它只是为了演示一个概念。
我们将把路由四(route four)嵌套在路由一(route one)中,所以像这样更新路由一的视图
Route one <div ui-view="nestedView"></div>
现在让我们在 Angular 中更新我们的路由配置以反映这一点。当我们向状态添加嵌套视图时,我们使用命名配置 viewName@stateName。所以要为 stateOne 配置 nestedView,我们给它命名为 nestedView@stateOne,如下所示
$stateProvider .state('stateOne', { url: '/stateOne?donuts', views: { "containerOne": { templateUrl: '/routesDemo/one' }, "containerTwo": { templateUrl: function (params) { return '/routesDemo/two?donuts=' + params.donuts; } }, "nestedView@stateOne": { templateUrl: '/routesDemo/four' } } }) .state('stateTwo', ...etc
我们刚刚添加了一个新视图,所以我们需要更新 RouteConfig.cs 以反映这一点
routes.MapRoute( name: "routeFour", url: "routesDemo/Four", defaults: new { controller = "RoutesDemo", action = "Four" });
现在让我们测试一下
回顾
好的,在第二部分中我们实现了以下目标
- 启用了 AngularJS 的 HTML5 模式(pushstate),并相应配置了 MVC 以使其正常工作
- 添加了 Twitter Bootstrap 和 Angular UI directives for bootstrap。现在一切都已准备就绪,我们可以根据需要从这两个库中添加组件
- 用 Angular UI router 替换了 ngRoute,并为我们的应用程序添加了命名视图、多视图和嵌套视图。
第三部分预告
- SignalR 集成
- 指令
- 防伪令牌
评论/批评/问题等
欢迎在文章中评论任何评论/批评/问题等,我将始终回复。感谢阅读 :)