首页 > 代码库 > 8.1:SportsStore:Orders and Administration

8.1:SportsStore:Orders and Administration

本章,作者将通过收集和验证购物明细,来完成SportsStore应用,并在Deployd服务器上存储该订单。作者也构建了一个管理应用,允许认证用户查看订单,和管理产品分类。

1、准备实例项目

2、获取产品明细

在给用户显示购物车中的产品汇总后,作者将购物明细用于订单。这需要使用AngularJS的表单特性,你会在大多数web应用中需要。作者已经创建Views/placeOrder.html文件,捕获用户的购物明细。作者将介绍一些与表单相关度的特性,来避免重复大量相似的代码。作者首先添加一组数据属性(用户的名字和街道地址),然后再其他添加的属性。下面是placeOrder.html文件。

<h2>Check out now</h2><p>Please enter your details, and we‘ll ship your goods right away!</p><div class="well"><h3>Ship to</h3><div class="form-group"><label>Name</label><input class="form-control" ng-model="data.shipping.name" /></div><h3>Address</h3><div class="form-group"><label>Street Address</label><input class="form-control" ng-model="data.shipping.street" /></div><div class="text-center"><button class="btn btn-primary">Complete order</button></div></div>

第一件事情,是作者没有使用ng-controller指令,来为视图指定一个控制器。这意味着视图会被顶级controller ,sportsStoreCtrl支持,该控制器管理包含ng-view指令的视图。当视图不需要任何额外控制器行为时,不用为部分视图定义控制器。

这里,AngularJS的一个重要特性是,在input元素上,使用ng-model指令。

<input class="form-control" ng-model="data.shipping.name"/>

该ng-model指令,设置了一个双向数据绑定。作者会在第10章深入解释数据绑定,但这里简短介绍。它使用{{}},是单向绑定,意味着简单地从scope显示一个值。该单向绑定的值可以被过滤,它可以是一个表达式,而不仅仅是一个数据值,单他是一个只读关系。如果scope改变,显示的值也会变。它只能从scope到binding。

双向数据绑定用于表单元素,允许用户输入值,改变scope。更新流在scope和data binding之间。通过一个JavaScript功能,执行更新scope数据属性。这里只要知道,如果用户在input元素中输入了一个值,该值被指派到通过ng-model指令指定的scope属性上,在本例中是,data.shipping.name属性和data.shipping.street属性。

提示:注意作者没有必要更新控制器,让他在scope上定义一个data.shipping对象,或单独的name或street属性。如果该属性不存在,AngularJS scopes会假设你想动态地定义一个属性。作者将在第13章深入讲。

 

2.1、添加表单校验

如果你写过任何种类的web应用,使用表单元素,你会知道用户会在input字段中输入任何东西。要确保你得到预期的数据,AngularJS支持form validation,允许检查值。

AngularJS表单校验,基于标准HTML属性,应用到表单元素,例如type和required。表达那校验会自动执行,但必须显示校验feedback给用户。

提示:HTML5在input元素上定义了一组新的type属性的值,能被用于该值是一个e-mail地址或一个数字。作者会在第12章解释。

 

2.1.1、准备校验

设置表单校验的第一步,是添加一个form元素到视图,并添加校验属性到作者的input元素。下面是placeOrder.html文件。

<h2>Check out now</h2><p>Please enter your details, and we‘ll ship your goods right away!</p><form name="shippingForm" novalidate><div class="well"><h3>Ship to</h3><div class="form-group"><label>Name</label><input class="form-control" ng-model="data.shipping.name" required /></div><h3>Address</h3><div class="form-group"><label>Street Address</label><input class="form-control" ng-model="data.shipping.street" required /></div><div class="text-center"><button class="btn btn-primary">Complete order</button></div></div></form>

form元素有三个目的,作者没有使用浏览器为提交表单提供的内建支持的。

第一个目的,是启用校验。AngularJS使用自定义的指令,重定义了一些HTML元素,来启用制定特性,例如form。没有form元素,AngularJS不验证元素的内容,如input,select,textarea等。

第二个谜底,form元素禁用浏览器可能会尝试执行的任何校验,它通过novalidate属性。该属性是标准的HTML5特性,它确保只有AngularJS检查数据。如果你忽略了novalidate属性,那么用户可能会得到冲突,或多次校验feedback,基于浏览器的会被使用。

最后的目的,form元素会定义一个变量,用于报告表单校验。它是通过name属性做的,作者将它设为shippingForm。当用户点击button按钮,用户表单的内容被验证时,该值用于显示feedback。

 

2.1.2、显示验证Feedback

一旦form元素和校验属性放置好,AngularJS开始校验用户提供的数据,作者要为用户显示feedback。作者将在第12章详细讲解,但这里作者会用两种类型的feedback:定义CSS样式给AngularJS通过验证或没有通过验证的表单元素。可以使用scope变量控制feedback消息显示的元素的可见性。placeOrder.html文件:

<style>.ng-invalid { background-color: lightpink; }.ng-valid { background-color: lightgreen; }span.error { color: red; font-weight: bold; }</style><h2>Check out now</h2><p>Please enter your details, and we‘ll ship your goods right away!</p><form name="shippingForm" novalidate><div class="well"><h3>Ship to</h3><div class="form-group"><label>Name</label><input name="name" class="form-control"ng-model="data.shipping.name" required /><span class="error" ng-show="shippingForm.name.$error.required">Please enter a name</span></div><h3>Address</h3><div class="form-group"><label>Street Address</label><input name="street" class="form-control"ng-model="data.shipping.street" required /><span class="error" ng-show="shippingForm.street.$error.required">Please enter a street address</span></div><div class="text-center"><button class="btn btn-primary">Complete order</button></div></div></form>

AngularJS会给表单元素制定ng-valid和ng-invalid classes。Form元素总是会持有这些样式中的一个。

该CSS样式表明校验的效果。所以作者必须给每个元素添加一个name属性,使用AngularJS添加到scope的校验数据,控制error消息的可见性。

<input name="street"class="form-control" ng-model="
data.shipping.street" required /><span class="error" ng-show="
shippingForm.street.$error.required">Please enter a street address</span>

input元素,用于捕获用户的街道,将name属性指派为street。AngularJS在scope上创建一个shippingForm.street对象(它是form元素的name和input元素的name的结合)。该对象定义一个$error属性,该属性是一个对象,为每个校验属性提供一个属性,表示input元素失败的原因。如果shippingForm.street.$error.required属性为真,知道street input元素通过没有校验通过,用于显示error消息给用户的空间,会执行ng-show指令。

记住:作者用的简单,但AngularJS可以用于创建更复杂的校验。

 

2.1.3、将按钮链接到校验

在多数web应用,用户在提供所有表单数据,并校验通过后,才能进入下一步。当表单没有通过校验,作者想禁用Complete order按钮,并在用户完成表单属性后,自动启用它。

要做到这点,作者要改进AngularJS添加到scope的校验信息。作者可以得到整个表单的状态。当input元素为没有通过校验,shippingForm.$invalid属性为true,作者将基于此,使用ng-disabled指令,管理按钮元素的状态。作者将在第11章描述ng-disable指令。

<div class="text-center"><button ng-disabled="shippingForm.$invalid"class="btn btn-primary">Complete order</button></div>

 

2.2、添加剩下的表单字段

现在你知道AngularJS是怎么进行表单校验的了,作者要将剩下的input元素添加到表单。

<style>.ng-invalid { background-color: lightpink; }.ng-valid { background-color: lightgreen; }span.error { color: red; font-weight: bold; }</style><h2>Check out now</h2><p>Please enter your details, and we‘ll ship your goods right away!</p><form name="shippingForm" novalidate><div class="well"><h3>Ship to</h3><div class="form-group"><label>Name</label><input name="name" class="form-control"ng-model="data.shipping.name" required /><span class="error" ng-show="shippingForm.name.$error.required">Please enter a name</span></div><h3>Address</h3><div class="form-group"><label>Street Address</label><input name="street" class="form-control"ng-model="data.shipping.street" required /><span class="error" ng-show="shippingForm.street.$error.required">Please enter a street address</span></div><div class="form-group"><label>City</label><input name="city" class="form-control"ng-model="data.shipping.city" required /><span class="error" ng-show="shippingForm.city.$error.required">Please enter a city</span></div><div class="form-group"><label>State</label><input name="state" class="form-control"ng-model="data.shipping.state" required /><span class="error" ng-show="shippingForm.state.$error.required">Please enter a state</span></div><div class="form-group"><label>Zip</label><input name="zip" class="form-control"ng-model="data.shipping.zip" required /><span class="error" ng-show="shippingForm.zip.$error.required">Please enter a zip code</span></div><div class="form-group"><label>Country</label><input name="country" class="form-control"ng-model="data.shipping.country" required /><span class="error" ng-show="shippingForm.country.$error.required">Please enter a country</span></div><h3>Options</h3><div class="checkbox"><label><input name="giftwrap" type="checkbox"ng-model="data.shipping.giftwrap" />Gift wrap these items</label></div><div class="text-center"><button ng-disabled="shippingForm.$invalid"class="btn btn-primary">Complete order</button></div></div></form>

提示:你可能会尝试使用ng-repeat指令,来生成input元素。这样生成的,不能很好滴工作,因为有些指令属性值,如ng-model,ng-show,是计算过的。作者建议这样做,单你想用更先进的技术,看第15-17章,作者会描述创建自定义指令的方式。

 

3、存储订单

本节,我们会扩展Deployd服务器提供的数据库,使用Ajax请求发送订单数据给服务器,在流程的最后,播放一个感谢信息。

 

3.1、扩展Deployd服务器

使用dashboard,选择collection,将集合的名字设为/orders,点击创建按钮。定义如下属性:

Name Type Required
name string Yes
street string Yes
city string Yes
state string Yes
zip string Yes
country string Yes
giftwrap boolean No
products array Yes

 

多花点注意,在gifwrap和products属性的类型上。

 

3.2、定义控制器行为

下一步要定义使用Ajax请求,发送订单给Deployd服务器的控制器行为。我可以以很多不同的方式定义该功能——创建一个服务或创建一个新的控制器。你可以以你喜欢的方式,构建,没有绝对的对与错。作者将保持一切都很简单,添加行为到顶级sportsStore控制器上,该控制器已经包含用Ajax请求加载产品数据的代码。

angular.module("sportsStore").constant("dataUrl", "http://localhost:5500/products").constant("orderUrl", "http://localhost:5500/orders").controller("sportsStoreCtrl", function ($scope, $http, $location,dataUrl, orderUrl, cart) {$scope.data = {};$http.get(dataUrl).success(function (data) {$scope.data.products = data;}).error(function (error) {$scope.data.error = error;});$scope.sendOrder = function (shippingDetails) {var order = angular.copy(shippingDetails);order.products = cart.getProducts();$http.post(orderUrl, order).success(function (data) {$scope.data.orderId = data.id;cart.getProducts().length = 0;}).error(function (error) {$scope.data.orderError = error;}).finally(function () {$location.path("/complete");});}});

Depolyd会在数据库中创建一个新的对象,来相应POST请求,并返回刚刚创建的对象,包括id属性。

作者定义了一个新的constant,制定URL,你会用于POST请求,并添加一个cart服务的依赖,以得到产品明细。作者添加到控制器的行为叫做sendOrder,它将用户的购物明细,作为参数。

作者使用angular.copy工具方法,他在第5章描述过,用来创建shipping details对象的拷贝,所以他可以安全地操作它,而不影响应用的其他部分。该shipping details对象的属性,通过ng-model指令创建,代表作者的orders Deployd集合。左右要做的,就是定义一个products属性,来引用购物车中的products数组。

作者使用$http.post方法,创建一个Ajax POST请求指定URL和数据,他使用success和error方法,在第5章介绍过,来相应请求的结果。

作者也使用在$http.post方法返回的promise上,使用then方法。该then方法,持有一个功能,无论Ajax请求的结果如何,都会被调用。无论发生什么,他都想要显示相同的视图给用户,所以他使用then方法,调用$location.path方法。这是编程的方式设置URL的path组建,它将会通过第7章创建的URL配置,触发视图改变。

 

3.3、调用控制器行为

要调用心控制器行为,必须添加ng-click指令到shipping details视图的button元素。

<div class="text-center"><button ng-disabled="shippingForm.$invalid"ng-click="sendOrder(data.shipping)"class="btn btn-primary">Complete order</button></div

 

3.4、定义视图

作者定义的Ajax请求完成后的URL路径是/complete,该URL路由配置映射到/views/thankYou.html文件:

<div class="alert alert-danger" ng-show="data.orderError">Error ({{data.orderError.status}}). The order could not be placed.<a href="#/placeorder" class="alert-link">Click here to try again</a></div><div class="well" ng-hide="data.orderError"><h2>Thanks!</h2>Thanks for placing your order. We‘ll ship your goods as soon as possible.If you need to contact us, use reference {{data.orderId}}.</div>

该视图定义了两个不同的内容块,来处理Ajax请求的success和unsuccessful。如果发生error,error的明细会显示,通过一个a链接,让用户返回到shipping details视图,让他可以再试一次。如果请求成功,为用户显示thank-you消息,包含新订单对象的id。

4、做改进

以后会做改进的地方:

第一,当你加载app.html文件到浏览器,你可能注意到一个小的延迟,在视图显示和products的元素和分类被生成。这是因为Ajax请求在后台获取数据,在等待服务器返回数据旗舰,AngularJS继续执行应用,并显示视图,当数据抵达后,在更新。在第22章,作者将描述如何使用URL路由特性,让AngularJS在Ajax请求已经完成后,再显示视图。

第二,作者将产品数据中的分类,提取出来,用于导航和分页特性。在一个真实的项目中,作者会考虑,在产品数据第一次抵达时,生成该信息。在第20章,作者描述如何使用promises,来构建行为链。

最后,作者使用$animate服务,在第23章介绍,当URL路径改变时,在视图切换时使用过度动画。

4.1、避免优化陷阱

你注意到,作者说要考虑重用分类和分页数据。这是因为任何类型的优化,都要十分小心,来确保它是明智的,并避免两个主要的陷阱。

第一个陷阱是,过早地优化。

第二个陷阱是,translation优化。

 

5、管理产品分类

要完成SportsStore应用,作者要创建一个应用,来允许管理员管理产品分类的内容,和订单队列。这回允许作者,演示AngularJS如何用于执行create,read,update,delete操作。

记住:每个后端服务实现认证有不同的方式,但基本的前提是相同的:将用户的凭证,通过请求发送给制定URL,如果请求成功,浏览器会返回一个cookie,浏览器会在随后的请求中,自动发送该cookie,用以标识用户。本例中使用Deployd。

 

5.1、准备Deployd

给数据库做一些改变,有些事情只能管理员做。在这里,我们会使用Deployd定义一个管理员用户,并穿件访问策略。

Collection Admin User
products create,read,update,delete read
orders create,read,update,delete create

总之,管理员能在任何集合上执行任何操作。一般用户可以read产品集合,并创建orders集合中的新对象。

在dashboard上创建Users Collection ,设置新容器的名字为/users。

用户集合,定义了id,username,password属性,是作者需要的。创建一个新对象,用户名密码为admin,secret。

 

5.1.1、集合的安全

作者喜欢Deployd的一个特性,是他定义一个简单的JavaScript API,可以用于实现服务端功能。当在一个集合上执行操作时,一系列的事件会被触发。点击products集合的Events,你会看到一系列的tab,代表不同的集合事件:On Get,On Validate,On Post,On Put,On Delete。所有的集合都有这些事件,你可以使用JavaScript,来加强认证策略。填入一下代码到On Put和On Delete tabs:

if (me === undefined || me.username != "admin") {    cancel("No authorization", 401);}

在Deployd API中,变量me,代表当前用户,cancel功能,使用制定消息和HTTP状态吗,结束一个请求。该代码,允许有权限的用户,和管理员用户访问,单其他请求都会用401状态吗结束,这代表客户端是没有权限做请求。

提示:不要担心这些事件是什么,作者会在后面说。

重复这些步骤,在orders集合的事件里,处理On Post和On Validate。强制执行认证控制的事件:

Collection Description
products On Put, On Delete
orders On Get, On Put, On Delete
users None

 

5.2、创建管理应用

作者要为管理任务,创建一个分隔的AngularJS应用。作者可以在主应用中集成这些特性,但这将意味着所有用户都将下载admin功能的代码,即使他们永远用不上。作者添加一个新文件,叫admin.html,放到angularjs文件夹:

<!DOCTYPE html><html ng-app="sportsStoreAdmin"><head><title>Administration</title><script src="angular.js"></script><script src="ngmodules/angular-route.js"></script><link href="bootstrap.css" rel="stylesheet" /><link href="bootstrap-theme.css" rel="stylesheet" /><script>angular.module("sportsStoreAdmin", ["ngRoute"]).config(function ($routeProvider) {$routeProvider.when("/login", {templateUrl: "/views/adminLogin.html"});$routeProvider.when("/main", {templateUrl: "/views/adminMain.html"});$routeProvider.otherwise({redirectTo: "/login"});});</script></head><body><ng-view /></body></html>

作者使用Module.config方法,创建了三个路由,让ng-view指令显示body元素。

URL Path View
/login /views/adminLogin.html
/main /views/adminMain.html
All others Redirects to /login

otherwise方法定义的路由,作者使用redirectTo选项,将URL路径改变到其他路由。这将让浏览器到/login路径,用于认证用户。作者将在第22章描述。

 

5.2.1、添加占位符视图

作者要实现认证特性,需要为/views/adminMain.html视图文件创建一些占位符内容,当认证成功时会显示。下面是adminMain.html文件的内容:

<div class="well">This is the main view</div>

作者将在后面替换它。

 

5.3、实现认证

Deployd认证用户,使用标准HTTP请求。该应用发送一个POST请求到/users/login URL,包含username和password值。如果认证成功,服务器响应状态吗200,如果失败,返回401。要实现认证,作者要定义一个控制器,发起Ajax调用,并处理响应。下面是controllers/adminControllers.js文件的内容:

angular.module("sportsStoreAdmin").constant("authUrl", "http://localhost:5500/users/login").controller("authCtrl", function($scope, $http, $location, authUrl) {$scope.authenticate = function (user, pass) {$http.post(authUrl, {username: user,password: pass}, {withCredentials: true}).success(function (data) {$location.path("/main");}).error(function (error) {$scope.authenticationError = error;});}});

 

5.3.1、定义视图认证

5.4、定义主视图和控制器

5.5、实现订单特性

5.6、实现产品特性

5.6.1、定义RESTful控制器

5.6.2、定义视图

5.6.3、添加对HTML文件的引用