chsakell分享了一個前端使用AngularJS,後端使用ASP.NET Web API的項目。html
源碼: https://github.com/chsakell/spa-webapi-angularjs
文章:http://chsakell.com/2015/08/23/building-single-page-applications-using-web-api-and-angularjs-free-e-book/前端
這裏記錄下對此項目的理解。分爲以下幾篇:jquery
● 對一個前端使用AngularJS後端使用ASP.NET Web API項目的理解(1)--領域、Repository、Servicegit
● 對一個前端使用AngularJS後端使用ASP.NET Web API項目的理解(2)--依賴倒置、Bundling、視圖模型驗證、視圖模型和領域模型映射、自定義handlerangularjs
● 對一個前端使用AngularJS後端使用ASP.NET Web API項目的理解(3)--主頁面佈局github
● 對一個前端使用AngularJS後端使用ASP.NET Web API項目的理解(4)--Movie增改查以及上傳圖片web
Home/Index.cshtml視圖數據庫
建立一個有關ui的module:spa/modules/common.ui.jsbootstrap
(function () { 'use strict'; angular.module('common.ui', ['ui.bootstrap', 'chieffancypants.loadingBar']); })();
建立一個有關功能的module:spa/modules/common.ui.jscanvas
(function () { 'use strict'; angular.module('common.core', ['ngRoute', 'ngCookies', 'base64', 'angularFileUpload', 'angularValidator', 'angucomplete-alt']); })();
Home/Index.cshtml視圖摘要:
<html ng-app="homeCinema"> <body ng-controller="rootCtrl"> <top-bar></top-bar> <side-bar></side-bar> <div class="page {{ pageClass }}" ng-view></div> </body> </html>
homeCinema是一個主module,依賴common.ui和common.core這2個module,定義在了spa/app.js中,具體以下:
//config傳入名稱爲config的函數 //run傳入名稱爲run的函數 //執行順序:app.config()→app.run()→directive compile functions if found→app.controller→directive's link funciton if found angular.module('homeCinema', ['common.core', 'common.ui']) .config(config) .run(run); //爲config函數注入參數 config.$inject = ['$routeProvider']; //路由設置,讓controller和頁面匹配 function config($routeProvider) { $routeProvider .when("/", { templateUrl: "scripts/spa/home/index.html", controller: "indexCtrl" }) .when("/login", { templateUrl: "scripts/spa/account/login.html", controller: "loginCtrl" }) .when("/register", { templateUrl: "scripts/spa/account/register.html", controller: "registerCtrl" }) .when("/customers", { templateUrl: "scripts/spa/customers/customers.html", controller: "customersCtrl" }) .when("/customers/register", { templateUrl: "scripts/spa/customers/register.html", controller: "customersRegCtrl", //注入到cotroller中的依賴,controller會等到resolve的動做結束後再初始化 resolve: { isAuthenticated: isAuthenticated } }) .when("/movies", { templateUrl: "scripts/spa/movies/movies.html", controller: "moviesCtrl" }) .when("/movies/add", { templateUrl: "scripts/spa/movies/add.html", controller: "movieAddCtrl", resolve: { isAuthenticated: isAuthenticated } }) .when("/movies/:id", { templateUrl: "scripts/spa/movies/details.html", controller: "movieDetailsCtrl", resolve: { isAuthenticated: isAuthenticated } }) .when("/movies/edit/:id", { templateUrl: "scripts/spa/movies/edit.html", controller: "movieEditCtrl" }) .when("/rental", { templateUrl: "scripts/spa/rental/rental.html", controller: "rentStatsCtrl" }).otherwise({ redirectTo: "/" }); } //爲resolve的函數注入參數 isAuthenticated.$inject = ['membershipService', '$rootScope', '$location']; //resolve執行的函數 function isAuthenticated(membershipService, $rootScope, $location) { if (!membershipService.isUserLoggedIn()) { $rootScope.previousState = $location.path(); $location.path('/login'); } } //爲run函數注入參數 run.$inject = ['$rootScope', '$location', '$cookieStore', '$http']; //一些初始化工做 function run($rootScope, $location, $cookieStore, $http) { // handle page refreshes //rootScope.repository //rootScope.repository.loggedUser $rootScope.repository = $cookieStore.get('repository') || {}; if ($rootScope.repository.loggedUser) { $http.defaults.headers.common['Authorization'] = $rootScope.repository.loggedUser.authdata; } $(document).ready(function () { $(".fancybox").fancybox({ openEffect: 'none', closeEffect: 'none' }); $('.fancybox-media').fancybox({ openEffect: 'none', closeEffect: 'none', helpers: { media: {} } }); $('[data-toggle=offcanvas]').click(function () { $('.row-offcanvas').toggleClass('active'); }); }); }
side-bar
在界面中的適用方法:<side-bar></side-bar>
在common.ui這個module中自定義了一個directive。在spa/layout/sideBar.directive.js
(function(app) { 'use strict'; app.directive('sideBar', sideBar); function sideBar() { return { restrict: 'E', replace: true, templateUrl: '/scripts/spa/layout/sideBar.html' } } })(angular.module('common.ui'));
spa/layout/sidebar.html摘要:
<a ng-href="#/">Home</i> <a ng-href="#/customers/">Customers</a> <a ng-href="#/customers/register">Register customer</a> <a ng-href="#/movies/">Movies</a> <a ng-href="#/movies/add">Add movie<i class="fa fa-plus-circle fa-fw pull-right"></i></a> <a ng-href="#/rental/">Rental history<i class="fa fa-leanpub fa-fw pull-right"></i></a> <a ng-href="#/login" ng-if="!userData.isUserLoggedIn">Login</a> <a ng-click="logout();" ng-if="userData.isUserLoggedIn">Logout</a>
其中,userData.isUserLoggedIn確定是homeCinema這個module的controller,定義在了spa/home/rootCtrl.js中。
(function (app) { 'use strict'; app.controller('rootCtrl', rootCtrl); rootCtrl.$inject = ['$scope','$location', 'membershipService','$rootScope']; function rootCtrl($scope, $location, membershipService, $rootScope) { //userData對象 $scope.userData = {}; //$scope.userData.displayUserInfo方法顯示用戶 $scope.userData.displayUserInfo = displayUserInfo; //方法登出 $scope.logout = logout; //$scope.userData.isUserLoggedIn,布爾類型 //$scope.username function displayUserInfo() { $scope.userData.isUserLoggedIn = membershipService.isUserLoggedIn(); if($scope.userData.isUserLoggedIn) { //主頁面初始化的時候就定義在$rootScope.repository.loggedUser.username了 $scope.username = $rootScope.repository.loggedUser.username; } } function logout() { membershipService.removeCredentials(); $location.path('#/'); $scope.userData.displayUserInfo(); } $scope.userData.displayUserInfo(); } })(angular.module('homeCinema'));
以上,注入了membershipService這個服務,另外還有一個apiService,notificationService等服務被放在了common.core模塊中,都是以factory的方式建立的服務。
spa/services/notificationService.js這個服務基於toastr.js管理全部的通知。
(function (app) { 'use strict'; //以工廠的方式建立服務 app.factory('notificationService', notificationService); function notificationService() { toastr.options = { "debug": false, "positionClass": "toast-top-right", "onclick": null, "fadeIn": 300, "fadeOut": 1000, "timeOut": 3000, "extendedTimeOut": 1000 }; var service = { displaySuccess: displaySuccess, displayError: displayError, displayWarning: displayWarning, displayInfo: displayInfo }; return service; function displaySuccess(message) { toastr.success(message); } function displayError(error) { if (Array.isArray(error)) { error.forEach(function (err) { toastr.error(err); }); } else { toastr.error(error); } } function displayWarning(message) { toastr.warning(message); } function displayInfo(message) { toastr.info(message); } } })(angular.module('common.core'));
spa/services/apiService.js服務用來管理GET和POST請求。
(function (app) { 'use strict'; app.factory('apiService', apiService); apiService.$inject = ['$http', '$location', 'notificationService','$rootScope']; function apiService($http, $location, notificationService, $rootScope) { var service = { get: get, post: post }; function get(url, config, success, failure) { return $http.get(url, config) .then(function (result) { success(result); }, function (error) { if (error.status == '401') { notificationService.displayError('Authentication required.'); $rootScope.previousState = $location.path(); $location.path('/login'); } else if (failure != null) { failure(error); } }); } function post(url, data, success, failure) { return $http.post(url, data) .then(function (result) { success(result); }, function (error) { if (error.status == '401') { notificationService.displayError('Authentication required.'); $rootScope.previousState = $location.path(); $location.path('/login'); } else if (failure != null) { failure(error); } }); } return service; } })(angular.module('common.core'));
top bar
自定義的top bar放在了common.ui模塊中,spa/layout/topBar.directive.js
(function(app) { 'use strict'; app.directive('topBar', topBar); function topBar() { return { restrict: 'E', replace: true, templateUrl: '/scripts/spa/layout/topBar.html' } } })(angular.module('common.ui'));
spa/layout/topBar.html摘要:
<a class="navbar-brand active" href="#/">Home Cinema</a> <a href="#about">About</a> <ulng-if="userData.isUserLoggedIn"> <a href="#/">{{username}}</a> </ul>
以上,username, userData.isUserLoggedIn都是homeCinema這個模塊中rootCtrl控制器的變量。
Latest Movies
首先要寫一個繼承ApiController的類,用來處理登陸異常。
namespace HomeCinema.Web.Infrastructure.Core { public class ApiControllerBase : ApiController { protected readonly IEntityBaseRepository<Error> _errorsRepository; protected readonly IUnitOfWork _unitOfWork; public ApiControllerBase(IEntityBaseRepository<Error> errorsRepository, IUnitOfWork unitOfWork) { _errorsRepository = errorsRepository; _unitOfWork = unitOfWork; } public ApiControllerBase(IDataRepositoryFactory dataRepositoryFactory, IEntityBaseRepository<Error> errorsRepository, IUnitOfWork unitOfWork) { _errorsRepository = errorsRepository; _unitOfWork = unitOfWork; } protected HttpResponseMessage CreateHttpResponse(HttpRequestMessage request, Func<HttpResponseMessage> function) { HttpResponseMessage response = null; try { response = function.Invoke(); } catch (DbUpdateException ex) { LogError(ex); response = request.CreateResponse(HttpStatusCode.BadRequest, ex.InnerException.Message); } catch (Exception ex) { LogError(ex); response = request.CreateResponse(HttpStatusCode.InternalServerError, ex.Message); } return response; } //把錯誤報錯到數據庫中去 private void LogError(Exception ex) { try { Error _error = new Error() { Message = ex.Message, StackTrace = ex.StackTrace, DateCreated = DateTime.Now }; _errorsRepository.Add(_error); _unitOfWork.Commit(); } catch { } } } }
以上, ApiControllerBase定義了一個重要的方法CreateHttpResponse,不但能夠處理請求響應,還能夠進行異常處理,把異常記錄到數據庫。
接着定義MoviesController,繼承ApiControllerBase基類。
[Authorize(Roles = "Admin")] [RoutePrefix("api/movies")] public class MoviesController : ApiControllerBase { private readonly IEntityBaseRepository<Movie> _moviesRepository; public MoviesController(IEntityBaseRepository<Movie> moviesRepository,IEntityBaseRepository<Error> _errorsRepository, IUnitOfWork _unitOfWork) : base(_errorsRepository, _unitOfWork) { _moviesRepository = moviesRepository; } ... }
首頁展現6個Moview,針對此寫一個aciton方法。
[AllowAnonymous] [Route("latest")] public HttpResponseMessage Get(HttpRequestMessage request) { return CreateHttpResponse(request, () => { HttpResponseMessage response = null; //獲取6個 var movies = _moviesRepository.GetAll().OrderByDescending(m => m.ReleaseDate).Take(6).ToList(); //轉換成視圖模型 IEnumerable<MovieViewModel> moviesVM = Mapper.Map<IEnumerable<Movie>, IEnumerable<MovieViewModel>>(movies); //建立響應 response = request.CreateResponse<IEnumerable<MovieViewModel>>(HttpStatusCode.OK, moviesVM); return response; }); }
界面如何展現出來呢?在spa/app.js中的路由已經有了定義。
$routeProvider .when("/", { templateUrl: "scripts/spa/home/index.html", controller: "indexCtrl" }) ...
也就是在根地址下,使用indexCtrl這個controller,路由到scripts/spa/home/index.html這裏。
scripts/spa/home/index.html
<div ng-if="loadingMovies"> <label class="label label-primary">Loading movies...</label> </div> <div ng-repeat="movie in latestMovies"> <strong>{{movie.Title}} </strong> <a ng-href="../../Content/images/movies/{{movie.Image}}" title="{{movie.Description | limitTo:200}}"> <img class="media-object" height="120" ng-src="../../Content/images/movies/{{movie.Image}}" alt="" /> </a> <available-movie is-available="{{movie.IsAvailable}}"></available-movie> {{movie.Description | limitTo: 70}}...</small> <label class="label label-info">{{movie.Genre}}</label> <span component-rating="{{movie.Rating}}"></span> <a ng-href="{{movie.TrailerURI}}">Trailer<i class="fa fa-video-camera fa-fw"></i></a> </div>
以上,其實定義了2個directive,一個是available-movie,用來顯示Movie的狀態,多是Available,也多是Not Available;另外一個是component-rating,用來顯示星級,基於 raty.js文件開發。
先來看控制器:spa/home/indexCtrl.js
(function (app) { 'use strict'; app.controller('indexCtrl', indexCtrl); indexCtrl.$inject = ['$scope','apiService', 'notificationService']; function indexCtrl($scope, apiService, notificationService) { $scope.loadingMovies = true; $scope.latestMovies = []; $scope.loadData = loadData; function loadData() { apiService.get('/api/movies/latest', null, moviesLoadCompleted, moviesLoadFailed); } function moviesLoadCompleted(result) { $scope.latestMovies = result.data; $scope.loadingMovies = false; } function moviesLoadFailed(response) { notificationService.displayError(response.data); } loadData(); } })(angular.module('homeCinema'));
再來看是否顯示Available Movie的這個自定義directive,在頁面中是這樣使用的:
<available-movie is-available="{{movie.IsAvailable}}"></available-movie>
實際是在spa/directives/availableMovie.directive.js中定義的。
(function (app) { 'use strict'; //注意這裏的慣例,這裏的availableMovie至關於界面上的available-movie app.directive('availableMovie', availableMovie); function availableMovie() { return { restrict: 'E', templateUrl: "/Scripts/spa/directives/availableMovie.html", link: function ($scope, $element, $attrs) { //getAvailbleClass供html中調用 //attrs表示屬性,注意這裏的慣例:isAvailable至關於界面上的is-available $scope.getAvailableClass = function () { if ($attrs.isAvailable === 'true') return 'label label-success' else return 'label label-danger' }; //getAvailability根據屬性的布爾值返回不一樣的字符串 $scope.getAvailability = function () { if ($attrs.isAvailable === 'true') return 'Available!' else return 'Not Available' }; } } } })(angular.module('common.ui'));
最終,在spa/directives/availableMovie.html中:
<label ng-class="getAvailableClass()">{{getAvailability()}}</label>
在Movie的顯示中,還定義了一個directive,用來顯示星級,在界面中按以下:
<span component-rating="{{movie.Rating}}"></span>
在spa/directives/componentRating.directive.js中:
(function(app) { 'use strict'; app.directive('componentRating', componentRating); function componentRating() { return { restrict: 'A', //A說明directive以屬性的方式 link: function ($scope, $element, $attrs) { $element.raty({ score: $attrs.componentRating, //componentRating至關於界面中的component-rating,接收component-rating屬性值 halfShow: false, readOnly: $scope.isReadOnly,//代表是隻讀的星級 noRatedMsg: "Not rated yet!", starHalf: "../Content/images/raty/star-half.png", starOff: "../Content/images/raty/star-off.png", starOn: "../Content/images/raty/star-on.png", hints: ["Poor", "Average", "Good", "Very Good", "Excellent"], click: function (score, event) { //Set the model value $scope.movie.Rating = score; $scope.$apply(); } }); } } } })(angular.module('common.ui'));
點擊首頁Movie縮略圖,彈出窗口
關於縮略圖的html部分就在scripts/spa/home/index.html中,具體爲:
<a class="fancybox pull-left" rel="gallery1" ng-href="../../Content/images/movies/{{movie.Image}}" title="{{movie.Description | limitTo:200}}"> <img class="media-object" height="120" ng-src="../../Content/images/movies/{{movie.Image}}" alt="" /> </a>
根據類名fancybox,使用jquery語法調用jquery.fancybox.js的語法,具體調用是在app.js中調用的:
function run($rootScope, $location, $cookieStore, $http) { ... $(document).ready(function () { $(".fancybox").fancybox({ openEffect: 'none', closeEffect: 'none' }); ... }); }
待續~