购物车Demo,前端使用AngularJS,后端使用ASP.NET Web API(2)--前端,以及前后端Session
?
chsakell分享了前端使用AngularJS,后端使用ASP.NET Web API的購物車案例,非常精彩,這里這里記錄下對此項目的理解。
文章:
http://chsakell.com/2015/01/31/angularjs-feat-web-api/
http://chsakell.com/2015/03/07/angularjs-feat-web-api-enable-session-state/
?
源碼:
https://github.com/chsakell/webapiangularjssecurity
?
本系列共三篇,本篇是第二篇。
購物車Demo,前端使用AngularJS,后端使用ASP.NET Web API(1)--后端
購物車Demo,前端使用AngularJS,后端使用ASP.NET Web API(2)--前端,以及前后端Session
購物車Demo,前端使用AngularJS,后端使用ASP.NET Web API(3)--Idetity,OWIN前后端驗證
?
HomeController用來展示主頁面,并接受前端傳來的Order的編號。
?
public calss HomeCOntroller : Controller {public ActionReuslt Index(){retun View();}public ActionResult ViewOrder(int id){using(var context = new SotreContext()){//這時候Order的導航屬性Gadgets還沒有加載出來呢var order = context.Orders.Find(id);//根據Order編號獲取中間表var gadgetOrders = context.GadgetOrders.Where(go => go.OrderID == id);foreach(GadgetOrder gadgetOrder in gadgetOrders){//加載中間表某個記錄中對應的導航屬性context.Entry(gadgetOrder).Reference(g => g.Gadget).Load();order.Gadgets.Add(gadgetOrder.Gadget);}return View(order);}} }?
Home/Index.cshtml視圖。
?
<html ng-app="gadgetsStore">...<body ng-controller='gadgetStoreCtrl'><div ng-hide="checkoutComplete()"><div ng-show="showFilter()"><form><input type="text" ng-model="searchItem"></form></div><cart-details></cart-details></div><div ng-show="data.error" ng-cloak>{{data.error.status}}</div><ng-view /><script src="../../Scripts/angular.js" type="text/javascript"></script><script src="../../Scripts/angular-route.js" type="text/javascript"></script><script src="../../app/mainApp.js"></script><script src="../../app/controllers/gadgetsStore.js" type="text/javascript"></script><script src="../../app/filters/storeFilters.js" type="text/javascript"></script><script src="../../app/controllers/gadgetsControllers.js" type="text/javascript"></script><script src="../../app/components/cartCmp.js" type="text/javascript"></script><script src="../../app/controllers/checkoutController.js" type="text/javascript"></</body> </html>?
以上,ng-hide="checkoutComplete()"決定著是否顯示所在div,ng-show="data.error" 決定是否顯示報錯,<ng-view />根據路由顯示不同視圖,ng-cloak用來避免在切換視圖時頁面的閃爍,<cart-details></cart-details>是自定義的directive,和angularjs有關的js文件放在頂部,applicaiton相關js文件放在其下面,在mainApp.js文件中坐落著一個頂級module名稱是gadgetStore,而頂級controller被放在了gadgetsStoreCtrl.js這個js文件中了。
最終的界面如下:
?
main.js 聲明頂級module,以及配置路由。
?
angular.module("gadgetsStore", ["storeFilters", "storeCart", "ngRoute"]).config(function($routeProvider){$routeProvider.when("/gadgets",{templateUrl: "app/views/gadgets.html"});$routeProvider.when("/checkout",{templateUrl: "app/views/checkout.html"});$routeProvider.when("/submitorder",{templateUrl: "app/views/submitOrder.html"});$routeProvider.when("/complete",{templateUrl: "app/views/orderSubmitted.html"});$routeProvider.otherwise({templateUrl: "app/views/gadgets.html"});});?
storeFilters, storeCart是我們自定義的,這里注入進來。
?
有了gadgetsStore這個module,現在就為這個module添加controller等。
?
angular.module('gadgetsStore').constant('gadgetsUrl', 'http://localhost:8888/api/gadgets').constant('ordersUrl', 'http://localhost:8888/api/orders').constant('categoreisUrl', 'http://localhost:8888/api/categories').controller('gadgetStore', function($scope, $http, $location, gadgetsUrl, categoresUrl, ordersUrl, cart){//因為gadgetsStore依賴引用了storeCart,所以這里可以引用cart//這里的data被用在主視圖上,所以data的數據會被其它部分視圖共享// $scope.data.gadgets// scope.data.erro// $scope.data.categories// $scope.data.OrderLocation// $scope.data.OrderID// $scope.data.orderError$scope.data = {};$http.get(gadgetsUrl).success(function(data){$scope.data.gadgets = data;}).error(function(error){$scope.data.error = error;});$http.get(categoresUrl).success(function(data){$scope.data.categories = data;}).error(function(error){$scope.data.error = error;});$scope.sendOrder = function(shippingDetails){var order = angular.copy(shippingDetails);order.gadgets = cart.getProducts();$http.post(ordersUrl, order).success(function(data, status, headers, config){$scope.data.OrderLocation = headers('Location');$scope.data.OrderID = data.OrderID;cart.getProducts().length = 0;}).error(function(error){$scope.data.orderError = error;}).finally(function(){$location.path("/complete");});}$scope.showFilter = function(){return $location.path() == '';}$scope.checkoutComplete = function(){return $location.path() == '/complete';}});?
以上,為gadgetsStore這個module定義了常量以及controller。把一些規定的uri定義成某個moudule的常量是很好的習慣。通過$location.path方法可以獲取或設置當前窗口的uri。
?
好了,頂級的module和頂級的controller有了,Gadget部分如何顯示呢?
?
根據路由$routeProvider.when("/gadgets",{templateUrl: "app/views/gadgets.html"}), Gadget的視圖被放在了app/views/gadgets.html中了,來看gadgets.html這個視圖。
?
<div ng-controller="gadgetsCtrl" ng-hide="data.error"><!--左側導航部分--><div><!--這里的selectCategory方法實際是把controller內部的一個變量selectedCategory設為null--><a ng-click="selectCategory()">Home</a><a ng-repeat="item in data.categoires | orderBy: 'CategoryID'" ng-click="selectCategory(item.CategoryID)" ng-class="getCategoryClass(item.CategoryID)">{{item.Name}}</a></div><!--右側Gadgets部分--><div><div ng-repeat="item in data.gadgets | filter: categoryFilterFn | filter: searchItem | range:selectedPage:pageSize">{{item.Name}}{{item.Price | currency}}<img ng-src="../../images/{{item.Images}}" />{{item.Description}}<a ng-click="addProductToCart(item)">Add To Cart</a></div><!--分頁部分--><div><a ng-repeat="page in data.gadgets | filter:categoryFilterFn | filter:searchItem | pageCount:pageSize" ng-click="selectPage($index + 1)" ng-class="getPageClass($index + 1)">{{$index + 1}}</a></div></div> </div>?
以上,把視圖的來源交給了gadgetsCtrl這個controller, 這個controller也被定義在了gadgetsStore這個module中。
?
gadgetsCtr.js
?
angular.module("gadgetsStore").constant("gadgetsActiveClass", 'btn-primary').constant('gadgetsPageCount', 3).controller("gadgetsCtrl", function($scope, $filter, gadgetsActiveClass, gadgetsPageCount, cart){//存儲Category的主鍵CategoryIDvar selectedCategory = null;//這里是傳給range和pageCount過濾器的$scope.selectedPage = 1;$scope.pageSise = gadgetsPageCount;//實際就是未selectedPage這個變量賦新值$scope.selectPage = function(newPage){$scope.selectedPage = newPage;}//這里把Category的編號CategoryID傳了進來$scope.selecteCategory = function(newCategory){$selectedCategory = newCategory;$scope.selectedPage = 1; }//這里的product實際就是Gadget//過濾出Gadget的CategoryID和這里的selectedCateogory一致的那些Gadgets$scope.categoryFilterFn = fucntion(product){return selectedCategory == null || product.CategoryID == selectedCategory;}//category實際是Category的主鍵CategoryID$scope.getCategoryClass = function(category){return selectedCategory == category ? gadgetsActiveClass : "";}$scope.getPageClass = function(page){return $scope.selectedPage = page ? gadgetsActiveClass : "";}$scope.addProductToCart = function(product){cart.addProduct(product.GadgetID, product.Name, product.Price, product.CategoryID);}});?
在顯示Gadget列表的時候,<div ng-repeat="item in data.gadgets | filter: categoryFilterFn | filter: searchItem | range:selectedPage:pageSize">,這里用到了一個自定的過濾器range,這個過濾器被定義在了storeFilters.js中。
?
var storeFilters = angular.module('storeFilters',[]);storeFitlers.filter("range", function($filter){return function(data, page, size){if(angular.isArray(data) && angular.isNumber(page) && angular.isNumber(size)){var start_index = (page - 1)*size;if(data.legnth < start_index){return [];} else {return $filter("limitTo")(data.splice(start_index), size);}} else{return data;}} });sortFilters.filter("pageCount", function(){return function(data, size){if(angular.isArray(data)){var result = [];for(var i = 0; i < Math.ceil(data.length/size); i++){ result.push(i);}} else {return data;}} });?
再來看$routeProvider.when("/checkout",{templateUrl: "app/views/checkout.html"});這個路由,checkout.html這個部分視圖如下:
?
<div ng-controller = "cartDetailsController"><div ng-show="cartData.length==0">no item in the shopping cart</div><div ng-hide="cartData.length == 0">{{item.count}}{{item.Name}}{{item.Price | currency}}{{(item.Price * item.count) | currency}}<button ng-click="remove(item.GadgetID)"></button>{{total() | currency}}<a href="#">Continue shopping</a><a href="#/submitorder">Place order now</a></div> </div>?
對應的界面如下:
?
?
cartDetailsController這個controller也被放在了頂級module里。如下:
?
?
angular.module("gadgetsStore").controller("cartDetailsController", function($scope, cart){$scope.cartData = cart.getProducts();$scope.total = function(){var total = 0;for(var i = 0; i < $scope.cartData.length;i++){total += ($scope.cartData[i].Price * $scope.cartData[i].count);}return total;}$scope.remove = function(id){cart.removeProduct(id);}});?
我們注意到,我們已經在多個地方注入cart這個服務 ,這個自定義的服務可以以factory的方式來創建,如果要用這個cart服務,它所在的module就要被其它module所引用。下面來創建cart服務:
?
var storeCart = angular.module('storeCart',[]);storeCart.factory('cart', function(){var cartData = [];return {addProduct: function(id, name, price, category){//用來標記是否已經向購物車里加了產品var addedToExistingItem = false;for(var i=0; i < cartData.length;i++){if(cartData[i].GadgetID == id){cartData[i].count++;addedToExistingItem = true;break;}}if(!addedToExistingItem){cartData.push({count:1, GadgetID: id, Price: price, Name: name, CategoryID:category});}},removeProduct: function(id){for(var i = 0; i < cartData.legnth; i++){if(cartData[i].GadgetID == id){cartData.splice(i, 1);break;}}},getProducts:function(){return cartData;}}; });?
關于購物車部分,我們還記得,在主視圖用了<cart-details></cart-details>這個自定義的directive,實際也是在storeCart這個module中定義的。
?
sortCart.directive("cartDetails", function(cart){return {restrict: "E",templateUrl: "app/components.cartDetails.html",controller: function($scope){var cartData = cart.getProducts();$scope.total = function(){var total =0;for(var i = 0; i < cartData.legnth; i++){total += (cartData[i].Price * cartData[i].count);}return total;}$scope.itemCount = function(){var total = 0;for(var i = 0; i < cartData.length; i++){total += cartData[i].count;}return total;}}}; });?
以上,對應的視圖為:
?
Your cart: {{itemCount()}} items {total() | currency} <a href="#/checkout">Checkout</a>?
在顯示購物車明細的時候,給出了提交訂單的鏈接:
<a href="#/submitorder">Place order now</a>
?
根據路由$routeProvider.when("/submitorder",{templateUrl: "app/views/submitOrder.html"}),是會加載app/views/submitOrder.html部分視圖,界面如下:
?
?
對應的html為:
?
<form name="shippingForm" novalidate><input name="companyName" ng-model="data.shipping.CompanyName" required /><span ng-show="shippingForm.companyName.$error.required"></span><input name="name" ng-model="data.shipping.OwnerName" required /><span ng-show="shippingorm.name.$error.required"></span>...<button ng-disabled="shippingForm.$invalid" ng-click="sendOrder(data.shipping)">Complete Order</button> </form>?
sendOrder被定義在了頂級module中:
?
$scope.sendOrder = function (shippingDetails) {var order = angular.copy(shippingDetails);order.gadgets = cart.getProducts();$http.post(ordersUrl, order).success(function (data, status, headers, config) {$scope.data.OrderLocation = headers('Location');$scope.data.OrderID = data.OrderID;cart.getProducts().length = 0;}).error(function (error) {$scope.data.orderError = error;}).finally(function () {$location.path("/complete");});}?
/complete會路由到$routeProvider.when("/complete",{templateUrl: "app/views/orderSubmitted.html"}), app/views/orderSubmitted.html部分視圖如下:
?
?
其html部分為:
?
<div ng-show="data.orderError">{{data.orderError.status}}the order could not be placed, <a href="#/submitorder">click here to try again</a> </div> <div ng-hide="data.orderError">{{data.OrderID}}<a href="#">Back to gadgets</a><a href="{{data.OrderLocation}}">View Order</a> </div>?
■ 實現購物車的Session
?
現在為止,還存在的問題是:當刷新頁面的時候,購物車內的產品就會消失,即還么有Session機制。
?
與ASP.NET Web API路由相關的HttpControllerRouteHandler, HttpControllerHandler, IRequireSessionState。
?
首先一個繼承內置的HttpControllerHandler,并實現內置的IRequiresSessionState接口。
?
public class SessionEnabledControllerHandler : HttpControllerHandler, IRequiresSessionState {public SessionEnabledControllerHandler(RouteData routeData): base(routeData){ } }?
然后實現一個內置HttpControllerRouteHandler的繼承類。
?
public class SessionEnabledHttpControllerRouteHandler : HttpControllerRouteHandler {protected override IHttpHandler GetHttpHandler(RequestContext requestContext){return new SessionEnabledControllerHandler(requestContext.RouteData);} }?
注釋掉WebApiConfig.cs中的代碼:
?
public static class WebApiConfig {public static void Register(HttpConfiguration config){// Web API configuration and services// Web API routes config.MapHttpAttributeRoutes();// Moved to RouteConfig.cs to enable Session/*config.Routes.MapHttpRoute(name: "DefaultApi",routeTemplate: "api/{controller}/{id}",defaults: new { id = RouteParameter.Optional });*/} }?
在RouteConfig中配置如下:
?
public class RouteConfig {public static void RegisterRoutes(RouteCollection routes){routes.IgnoreRoute("{resource}.axd/{*pathInfo}");#region Web API Routes// Web API Session Enabled Route Configurations routes.MapHttpRoute(name: "SessionsRoute",routeTemplate: "api/sessions/{controller}/{id}",defaults: new { id = RouteParameter.Optional }).RouteHandler = new SessionEnabledHttpControllerRouteHandler(); ;// Web API Stateless Route Configurations routes.MapHttpRoute(name: "DefaultApi",routeTemplate: "api/{controller}/{id}",defaults: new { id = RouteParameter.Optional });#endregion#region MVC Routesroutes.MapRoute(name: "Default",url: "{controller}/{action}/{id}",defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional });#endregion} }?
以上,需要引用System.Web.Http。
?
現在,如果希望ItemsController中使用Session,那就這樣請求:
http://localhost:61691/api/sessions/items
?
如果不想用Session,那就這樣請求:
http://localhost:61691/api/items
?
現在,在前端,向購物車添加產品相關代碼為:
?
addProduct: function (id, name, price, category) {var addedToExistingItem = false;for (var i = 0; i < cartData.length; i++) {if (cartData[i].GadgetID == id) {cartData[i].count++;addedToExistingItem = true;break;}}if (!addedToExistingItem) {cartData.push({count: 1, GadgetID: id, Price: price, Name: name, CategoryID: category});} }?
類似地,創建一個模型:
?
public class CartItem {public int Count { get; set; }public int GadgetID { get; set; }public decimal Price { get; set; }public string Name { get; set; }public int CategoryID { get; set; } }?
對應的控制器為:
?
public class TempOrdersController : ApiController {//get api/TempOrderspublic List<CartItem> GetTempOrders(){List<CartItem> cartItems = null;if(System.Web.HttpContext.Current.Session["Cart"] != null){cartItems = (List<CartItem>)System.Web.HttpContext.Current.Session["Cart"];}return cartItems;}//post api/TempOrders [HttpPost]public HttpResponseMessage SaveOrder(List<CarItem> cartItems){if (!ModelState.IsValid){return new HttpResponseMessage(HttpStatusCode.BadRequest);}System.Web.HttpContext.Current.Session["Cart"] = cartItems;return new HttpResponseMessage(HttpStatusCode.OK);} }?
再回到前端,首先在gadgetsStore這個頂級module中增加有關緩存API的uri常量。
?
angular.module('gadgetsStore').constant('gadgetsUrl', 'http://localhost:61691/api/gadgets').constant('ordersUrl', 'http://localhost:61691/api/orders').constant('categoriesUrl', 'http://localhost:61691/api/categories').constant('tempOrdersUrl', 'http://localhost:61691/api/sessions/temporders').controller('gadgetStoreCtrl', function ($scope, $http, $location, gadgetsUrl, categoriesUrl, ordersUrl, tempOrdersUrl, cart) { // Code omitted?
重新定義cart這個服務:
?
storeCart.factory('cart', function(){var cartData = [];return {addProduct: function(id, name, price, category){var addedToExistingItem = false;for(var i = 0; i < cartData.length; i++){if(cartData[i].GadgetID == id){cartData[i].count++;addedToExistingItem = true;break;}}if(!addedToExistingItem){cartData.push({count:1, GadgetID: id, Price: price, Name: name, Category: category});}},removeProduct: fucntion(id){for(var i = 0; i < cartData.length; i++){if(cartData[i].GadgetID == id){cartData.splice(i, 1);break;}}},getProducts: fucntion(){return cartData;},pushItem: function(item){cartData.push({count: item.Count, GadgetID:item.GadgetID, Price: Item.Price, Name: item.Name, CategoryID: item.CategoryID})}}; });?
為了在頁面每次刷新的時候保證Session的狀態,在主module中添加如下方法:
?
//用來把每次更新保存到后端的Session中 $scope.saveOrder = function () {var currentProducts = cart.getProducts();$http.post(tempOrdersUrl, currentProducts).success(function (data, status, headers, config) {}).error(function (error) {}).finally(function () {}); }//用來每次刷新向后端Session要數據 $scope.checkSessionGadgets = function(){$http.get(tempOrdersUrl).success(function(data){if(data){for(var i = 0; i < data.length; i++){var item = data[i];cart.pushItem(item);}}}).error(function(error){console.log('error checking session: ' + error) ;}); }?
然后checkSessionGadgets這個方法就要被運用到主視圖上去,當頁面每次加載的時候調用它。
?
<body ng-controller='gadgetStoreCtrl' class="container" ng-init="checkSessionGadgets()">?
每次向購車添加的時候需要重新更新后端的Session狀態。
?
$scope.addProductToCart = function (product) {cart.addProduct(product.GadgetID, product.Name, product.Price, product.CategoryID);$scope.saveOrder(); }?
每次從購物車一處的時候需要重新更新后端的Session狀態。
?
$scope.remove = function (id) {cart.removeProduct(id);$scope.saveOrder(); }?
在用戶提交訂單的時候,需要一出購物車內的產品,再更新后端的Session狀態。
?
$scope.sendOrder = function (shippingDetails) {var order = angular.copy(shippingDetails);order.gadgets = cart.getProducts();$http.post(ordersUrl, order).success(function (data, status, headers, config) {$scope.data.OrderLocation = headers('Location');$scope.data.OrderID = data.OrderID;cart.getProducts().length = 0;$scope.saveOrder();}).error(function (error) {$scope.data.orderError = error;}).finally(function () {$location.path("/complete");}); }?
待續~~
?
總結
以上是生活随笔為你收集整理的购物车Demo,前端使用AngularJS,后端使用ASP.NET Web API(2)--前端,以及前后端Session的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Stack Overflow 上人气爆表
- 下一篇: mysql proxy 读写分离 1