javascript
AngualrJS之服务器端通信
譯自《AngularJS》
?
與服務(wù)器通信
?
目前,我們已經(jīng)接觸過下面要談的主題的主要內(nèi)容,這些內(nèi)容包括你的Angular應用如何規(guī)劃設(shè)計、不同的angularjs部件如何裝配在一起并正常工作以及AngularJS中的模板代碼運行機制的一小部分內(nèi)容。把它們結(jié)合在一起,你就可以搭建一些簡潔優(yōu)雅的Web應用,但他們的運作主要還是限制在客戶端.在前面第二章,我們接觸了一點用$http服務(wù)做與服務(wù)器端通信的內(nèi)容,但是在這一章,我們將會深入探討如何在現(xiàn)實世界的真實應用中使用它($http).
在這一章,我們將討論一下AngularJS如何幫你與服務(wù)器端通信,這其中包括在最低抽象等級的層面或者用它提供的優(yōu)雅的封裝器。而且我們將會深入探討AngularJS如何用內(nèi)建緩存機制來幫你加速你的應用.如果你想用SocketIO開發(fā)一個實時的Angular應用,那么第八章有一個例子,演示了如何把·SocketIO·封裝成一個指令然后如何使用這個指令,在這一章,我們就不涉及這方面內(nèi)容了.
目錄
- 通過$http進行通行
- 進一步配置你的請求
- 設(shè)定HTTP頭信息(Headers)
- 緩存響應數(shù)據(jù)
- 對請求(Request)和響應(Response)的數(shù)據(jù)所做的轉(zhuǎn)換
- 單元測試
- 使用RESTful資源
- resource資源的聲明
- 定制方法
- 不要使用回調(diào)函數(shù)機制!(除非你真的需要它們)
- 簡化的服務(wù)器端操作
- 對ngResource做單元測試
- $q和預期值(Promise)
- 響應攔截處理
- 安全方面的考慮
- JSON的安全脆弱性
- 跨站請求偽造(XSRF)
通過$http進行通行
從Ajax應用(使用XMLHttpRequests)發(fā)動一個請求到服務(wù)器的傳統(tǒng)方式包括:得到一個XMLHttpRequest對象的引用、發(fā)起請求、讀取響應、檢驗錯誤代碼然后最后處理服務(wù)器響應。它就是下面這樣:
var xmlhttp = new XMLHttpRequest(); xmlhttp.onreadystatechange = function() {if (xmlhttp.readystate == 4 && xmlhttp.status == 200) {var response = xmlhttp.responseText;} else if (xmlhttp.status == 400) { // or really anything in the 4 series// Handle error gracefully} }; // Setup connection xmlhttp.open(“GET”, “http://myserver/api”, true); // Make the request xmlhttp.send();對于這樣一個簡單、常用且經(jīng)常重復的任務(wù),上面這個代碼量比較大.如果你想重復性地做這件事,你最終可能會做一個封裝或者使用現(xiàn)成的庫.
AngularJS XHR(XMLHttpRequest) API遵循Promise接口.因為XHRs是異步方法調(diào)用,服務(wù)器響應將會在未來一個不定的時間返回(當然希望是越快越好).Promise接口保證了這樣的響應將會如何處理,它允許Promise接口的消費者以一種可預計的方式使用這些響應.
假設(shè)我們想從我們的服務(wù)器取回用戶的信息.如果接口在/api/user地址可用,并且接受id作為url參數(shù),那么我們的XHR請求就可以像下面這樣使用Angular的核心$http服務(wù):
$http.get('api/user', {params: {id: '5'} }).success(function(data, status, headers, config) { // Do something successful. }).error(function(data, status, headers, config) { // Handle the error });如果你來自jQuery世界,你可能會注意到:AngularJS和jquery處理異步需求的方式很相似.
我們上面例子中使用的$htttp.get方法僅僅是AngularJS核心服務(wù)$http提供的眾多簡便方法之一.類似的,如果你想使用AngularJS向相同URL帶一些POST請求數(shù)據(jù)發(fā)起一個POST請求,你可以像下面這樣做:
var postData = {text: 'long blob of text'}; // The next line gets appended to the URL as params // so it would become a post request to /api/user?id=5 var config = {params: {id: '5'}}; $http.post('api/user', postData, config ).success(function(data, status, headers, config) { // Do something successful }).error(function(data, status, headers, config) { // Handle the error });AngularJS為大多數(shù)常用請求類型都提供了類似的簡便方法,他們包括:
- GET
- HEAD
- POST
- DELETE
- PUT
- JSONP
進一步配置你的請求
有時,工具箱提供的標準請求配置還不夠,它可能是因為你想做下面這些事情:
- 你可能想為請求添加權(quán)限驗證的頭信息
- 改變請求數(shù)據(jù)的緩存方式
- 在請求被發(fā)送或者響應返回時,對數(shù)據(jù)以一些方式做一定的轉(zhuǎn)換處理
在上面這樣的情況之下,你可以進一步配置請求,通過可選的傳遞進請求的配置對象.在之前的例子中,我們使用配置對象來標明可選的URL參數(shù),即便我們哪兒演示的GET和POST方法是簡便方法。內(nèi)部的原生方法可能看上面像相面這樣:
$http(config)下面演示的是一個調(diào)用這個方法的偽代碼模板:
$http({method: string,url: string,params: object,data: string or object,headers: object,transformRequest: function transform(data, headersGetter) or an array of functions,transformResponse: function transform(data, headersGetter) or an array of functions,cache: boolean or Cache object,timeout: number,withCredentials: boolean });GET、POST和其它的簡便方法已經(jīng)設(shè)置了請求的method類型,所以不需要再設(shè)置這個,config配置對象是傳給·$http.get·、·$http.post·方法的最后一個參數(shù),所以當你使用任何簡便方法的時候,你任何能用這個config配置對象.
你也可以通過傳入含有下面這些鍵的屬性集config對象來改變已有的request對象
- method : 一個表示http請求類型的字符串,比如GET,或者POST
- url : 一個URL字符串代表要請求資源的絕對或相對URL
- params : 一個對象(準確的說是鍵值映射)包含字符串到字符串內(nèi)容,它代表了將會轉(zhuǎn)換為URL參數(shù)的鍵值對,比如下面這樣: [{key1: 'value1', key2: 'value2'}] 它將會被轉(zhuǎn)換為: ?key1=value&key2=value2 這串字符將會加在URL后面,如果在value的位置你用一個對象取代字符串或數(shù)字,那這個對象將會轉(zhuǎn)換為JSON字符串.
- data :一個字符串或一個對象,它將會被作為請求消息數(shù)據(jù)被發(fā)送.
- timeout : 這是請求被認定為過期之前所要等待的毫秒數(shù).
還有部分另外的選項可以被配置,在下面的章節(jié)中,我們將會深度探索這些選項.
設(shè)定HTTP頭信息(Headers)
AngularJS有一個默認的頭信息,這個頭信息將會對所有的發(fā)送請求使用,它包含以下信息: 1.Accept: application/json, text/plain, / 2.X-Requested-With:XMLHttpRequest
如果你想設(shè)置任何特定的頭信息,這兒有兩種方法來做這件事:
第一種方法,如果你相對所有的發(fā)送請求都使用這些特定頭信息,那你需要把特定有信息設(shè)置為Angular默認頭信息的一部分.可以在$httpProvider.defaults.headers配置對象里面設(shè)置這個,這個步驟通常會在你的app設(shè)置config部分來做.所以如果你想移除"Requested-With"頭信息且對所有的GET請求啟用"DO NOT TRACK"設(shè)置,你可以簡單地通過以下代碼來做:
angular.module('MyApp',[]).config(function($httpProvider) {// Remove the default AngularJS X-Request-With headerdelete $httpProvider.default.headers.common['X-Requested-With'];// Set DO NOT TRACK for all Get requests$httpProvider.default.headers.get['DNT'] = '1'; });如果你只想對某個特定的請求設(shè)置頭信息,而不是設(shè)置默認頭信息.那么你可以通過給$http服務(wù)傳遞包含指定頭信息的config對象來做.相同的定制頭信息可以作為第二個參數(shù)傳遞給GET請求,第一個參數(shù)是URL字符串:
$http.get('api/user', { // Set the Authorization header. In an actual app, you would get the auth // token from a service headers: {'Authorization': 'Basic Qzsda231231'}, params: {id: 5} }).success(function() { // Handle success });如何在應用中處理權(quán)限驗證頭信息的成熟示例將會在第八章的Cheetsheets示例部分給出.
緩存響應數(shù)據(jù)
AngularJS為HTTP GET請求提供了一個開箱即用的簡單緩存系統(tǒng).缺省情況下,它對所有的請求都是禁用的,但是如果你想對你的請求啟用緩存系統(tǒng),你可以使用以下代碼:
$http.get('http://server/myapi', {cache: true }).success(function() { // Handle success });這段代碼啟用了緩存系統(tǒng),然后AngularJS將會緩存來自Server的響應數(shù)據(jù).但對相同的URL的請求第二次發(fā)出時,AngularJS將會從緩存里面取出前一次的響應數(shù)據(jù)作為響應返回.這個緩存系統(tǒng)也很智能,即使你同時對相同URL發(fā)出多個請求,只有一個請求會發(fā)向Server,這個請求的響應數(shù)據(jù)將會反饋給所有(同時發(fā)起的)請求。
然而這種做法從可用性的角度看可能是有所沖突的,當一個用戶首先看到舊的結(jié)果,然后新的結(jié)果突然冒出來,比如一個用戶可能即將單擊一個數(shù)據(jù)項,而實際上這個數(shù)據(jù)項后臺已經(jīng)發(fā)生了變化.
注意所有響應(即使是從緩存里取出的)本質(zhì)上仍舊是異步響應.換句話說,期望你的利用緩存響應時的異步代碼運行仍舊和他向后臺服務(wù)器發(fā)出請求時的代碼運行機制是一樣的.
對請求(Request)和響應(Response)的數(shù)據(jù)所做的轉(zhuǎn)換
AngularJS對所有$http服務(wù)發(fā)起的請求和響應做一些基本的轉(zhuǎn)換,它們包括:
- 請求(Request)轉(zhuǎn)換: 如果請求的Cofig配置對象的data屬性包含一個對象,將會把這個對象序列化為JSON格式.
- 響應(Response)轉(zhuǎn)換: 如果探測到一個XSRF頭,把它剝離掉.如果響應數(shù)據(jù)被探測為JSON格式,用JSON解析器把它反序列化為JSON對象.
如果你需要部分系統(tǒng)默認提供的轉(zhuǎn)換,或者想使用你自己的轉(zhuǎn)換,你可以把你的轉(zhuǎn)換函數(shù)作為Config配置對象的一部分傳遞進去(后面有細述).這些轉(zhuǎn)換函數(shù)得到HTTP請求和HTTP響應的數(shù)據(jù)主體以及它們的頭信息.然后把序列化的修改后版本返回出來.在Config對象里面配置這些函數(shù)需要使用·transformRequest·鍵和·transformResponse·鍵,這些都可以通過使用`$httpProvider·服務(wù)在模塊的config函數(shù)里面配置它.
我們什么時候使用這些哪?讓我假設(shè)我們有一個服務(wù)器,它更習慣于jQuery運行的方式.它可能希望我們的POST數(shù)據(jù)以key1=val1&key2=val2字符串的形式傳遞,而不是以{key1:val1,key2:val2}這樣的JSON格式.這個時候,我們可能相對每個請求做這樣的轉(zhuǎn)換,或者單個地增加transformRequest轉(zhuǎn)換函數(shù),為了達成這個示例這樣的目標,我們將要設(shè)置一個通用transformRequet轉(zhuǎn)換函數(shù),以便對所有的發(fā)出請求,這個函數(shù)都可以把JSON格式轉(zhuǎn)換為鍵值對字符串,下面代碼演示了如何做這個工作:
var module = angular.module('myApp'); module.config(function ($httpProvider) {$httpProvider.defaults.transformRequest = function(data) {// We are using jQuery’s param method to convert our// JSON data into the string formreturn $.param(data);}; });單元測試
目前為止,我們已經(jīng)了解如何使用$http服務(wù)以及如何以可能的方式做你需要的配置.但是如何寫一些單元測試來保證這些夠真實有效的運行哪?
正如我們曾經(jīng)三番五次的提到的那樣,AngularJS一直以測試為先的原則而設(shè)計.所以Angualr有一個模擬服務(wù)器后端,在單元測試中,它可以幫你就可以測試你發(fā)出的請求是否正確,甚至可以精確控制模擬響應如何得到處理,什么時候得到處理.
讓我們探索一下下面這樣的單元測試場景:一個控制向服務(wù)器發(fā)起請求,從服務(wù)器得到數(shù)據(jù),把它賦給作用域內(nèi)的模型,然后以具體的模板格式顯示出來.
我們的NameListCtrl控制器是一個非常簡單的控制器.它的存在只有一個目的:訪問namesAPI接口,然后把得到數(shù)據(jù)存儲在作用域scope模型內(nèi).
function NamesListCtrl($scope, $http) {$http.get('http://server/names', {params: {filter: ‘none’}}).success(function(data) {$scope.names = data;}); }怎樣對這個控制器做單元測試?在我們的單元測試中,我們必須保證下面這些條件:
- NamesListCtrl能夠找到所有的依賴項(并且正確的得到注入的這些依賴)》
- 當控制器加載時盡可能快地立刻發(fā)情請求從服務(wù)器得到names數(shù)據(jù).
- 控制器能夠正確地把響應數(shù)據(jù)存儲到作用域scope的names變量屬性中.
在我們的單元測試中構(gòu)造一個控制器時,我們給它注入一個scope作用域和一個偽造的HTTP服務(wù),在構(gòu)建測試控制器的方式和生產(chǎn)中構(gòu)建控制器的方式其實是一樣的.這是推薦方法,盡管它看上去上有點復雜。讓我看一下具體代碼:
describe('NamesListCtrl', function(){var scope, ctrl, mockBackend;// AngularJS is responsible for injecting these in testsbeforeEach(inject(function(_$httpBackend_, $rootScope, $controller) {// This is a fake backend, so that you can control the requests// and responses from the servermockBackend = _$httpBackend_;// We set an expectation before creating our controller,// because this call will get triggered when the controller is createdmockBackend.expectGET('http://server/names?filter=none').respond(['Brad', 'Shyam']);scope = $rootScope.$new();// Create a controller the same way AngularJS would in productionctrl = $controller(PhoneListCtrl, {$scope: scope});}));it('should fetch names from server on load', function() {// Initially, the request has not returned a responseexpect(scope.names).toBeUndefined();// Tell the fake backend to return responses to all current requests// that are in flight.mockBackend.flush();// Now names should be set on the scopeexpect(scope.names).toEqual(['Brad', 'Shyam’]);}); });使用RESTful資源
·$http·服務(wù)提供一個比較底層的實現(xiàn)來幫你發(fā)起XHR請求,但是同時也給提供了很強的可控性和彈性.在大多數(shù)情況下,我們處理的是對象集或者是封裝有一定屬性和方法的對象模型,比如帶有個人資料的自然人對象或者信用卡對象.
在上面這樣的情況下,如果我們自己構(gòu)建一個js對象來表示這種較復雜對象模型,那做法就有點不夠nice.如果我們僅僅想編輯某個對象的屬性、保存或者更新一個對象,那我們?nèi)绾巫屵@些狀態(tài)在服務(wù)器端持久化.
$resource正好給你提供這種能力.AngularJS resources可以幫助我們以描述的方式來定義對象模型,可以定義一下這些特征:
- resource的服務(wù)器端URL
- 這種請求常用參數(shù)的類型
- (你可以免費自動得到get、save、query、remove和delete方法),除了那些方法,你可以定義其它的方法,這些方法封裝了對象模型的特定功能和業(yè)務(wù)邏輯(比如信用卡模型的charge()付費方法)
- 響應的期望類型(數(shù)組或者一個獨立對象)
- 頭信息
什么時候你可以用Angular Resources組件?
只有你的服務(wù)器端設(shè)施是以RESTful方式提供服務(wù)的時候,你才應該用Angular resources組件.比如信用卡那個案例,我們將用它作為本章這一部分的例子,他將包括以下內(nèi)容:
除了按照你的要求給你提供一個查詢服務(wù)器端信息的對象,$resource還可以讓你使用返回的數(shù)據(jù)對象就像他們是持久化數(shù)據(jù)模型一樣,可以修改他們,還可以把你的修改持久化存儲下來.
ngResource是一個單獨的、可選的模塊.要想使用它,你看需要做以下事情:
- 在你的HTML文件里面引用angular-resource.js的實際地址
- 在你的模塊依賴里面聲明對它的依賴(例如,angular.module('myModule',['ngResource'])).
- 在需要的地方,注入$resource這個依賴項.
在我們看怎樣用ngResource方法創(chuàng)建一個resource資源之前,我們先看一下怎樣用基本的$http服務(wù)做類似的事情.比如我們的信用卡resource,我們想能夠讀取、查詢、保存信用卡信息,另外還要能為信用卡還款.
這兒是上述需求一個可能的實現(xiàn):
myAppModule.factory('CreditCard', ['$http', function($http) {var baseUrl = '/user/123/card';return {get: function(cardId) {return $http.get(baseUrl + '/' + cardId);},save: function(card) {var url = card.id ? baseUrl + '/' + card.id : baseUrl;return $http.post(url, card);},query: function() {return $http.get(baseUrl);},charge: function(card) {return $http.post(baseUrl + '/' + card.id, card, {params: {charge: true}});}}; }]);取代以上方式,你也可以輕松創(chuàng)建一個在你的應用中始終如一的Angular資源服務(wù),就像下面代碼這樣:
myAppModule.factory('CreditCard', ['$resource', function($resource) {return $resource('/user/:userId/card/:cardId',{userId: 123, cardId: '@id'},{charge: {method:'POST', params:{charge:true}, isArray:false}); }]);做到現(xiàn)在,你就可以任何時候從Angular注入器里面請求一個CreditCard依賴,你就會得到一個Angular資源,默認情況下,這個資源會提供一些初始的可用方法.表格5-1列出了這些初始方法以及他們的運行行為,這樣你就可以知道在服務(wù)器怎樣配置來配合這些方法了.
表格5-1 一個信用卡reource Function Method URL Expected Return CreditCard.get({id: 11}) GET /user/123/card/11 Single JSON CreditCard.save({}, ccard) POST /user/123/card with post data “ccard” Single JSON CreditCard.save({id: 11}, ccard) POST /user/123/card/11 with post data “ccard” Single JSON CreditCard.query() GET /user/123/card JSON Array CreditCard.remove({id: 11}) DELETE /user/123/card/11 Single JSON CreditCard.delete({id: 11}) DELETE /user/123/card/11 Single JSON
讓我們看一個信用卡resource使用的代碼樣例,這樣可以讓你理解起來覺得更淺顯易懂.
// Let us assume that the CreditCard service is injected here // We can retrieve a collection from the server which makes the request // GET: /user/123/card var cards = CreditCard.query(); // We can get a single card, and work with it from the callback as well CreditCard.get({cardId: 456}, function(card) {// each item is an instance of CreditCardexpect(card instanceof CreditCard).toEqual(true);card.name = "J. Smith";// non-GET methods are mapped onto the instancescard.$save();// our custom method is mapped as well.card.$charge({amount:9.99});// Makes a POST: /user/123/card/456?amount=9.99&charge=true// with data {id:456, number:'1234', name:'J. Smith'} });前面這個樣例代碼里面發(fā)生了很多事情,所以我們將會一個一個地認真講解其中的重要部分:
resource資源的聲明
聲明你自己的$resource非常簡單,只要調(diào)用注入的$resource函數(shù),并給他傳入正確的參數(shù)即可。(你現(xiàn)在應該已經(jīng)知道如何注入依賴,對吧?)
$resource函數(shù)只有一個必須參數(shù),就是提供后臺資源數(shù)據(jù)的URL地址,另外還有兩個可選參數(shù):默認request參數(shù)信息和其它的想在資源上要配置的動作.
請注意:第一個URL地址參數(shù)中的的變量數(shù)據(jù)是參數(shù)化可配置的(:冒號是參數(shù)變量的語法符號,比如:userId以為這個參數(shù)將會被實際的userId參數(shù)變量取代(譯者注:寫過參數(shù)化SQL語句的人應該很熟悉),而:cardId將會被cardId參數(shù)變量的值所取代),如果沒有給函數(shù)傳遞這些參數(shù)變量,那那個位置將會被空字符取代.
函數(shù)的第二個參數(shù)則負責提供所有請求的默認參數(shù)變量信息.在這個案例中,我們給userId參數(shù)傳遞一個常量:123,cardId參數(shù)則更有意思,我們給cardId參數(shù)傳遞了一個"@id"字符串.這意味著如果我們使用一個從服務(wù)器返回的對象而且我們可以調(diào)用這個對象的任何方法(比如$save),那么這個對象的id屬性將會被取出來賦給cardId字段.
函數(shù)的第三個參數(shù)是一些我們想要暴露的其它方法,這些方法是對定制資源做操作的方法.在下面的章節(jié),我們將會深度討論這個話題
定制方法
$resource函數(shù)的第三個參數(shù)是可選的,主要用來傳遞要在resource資源上暴露的其它自定義方法。
在這個案例中,我們自定義了一個方法charge.這個自定義方法可以通過傳遞一個對象而被配置上.這個對象里有個鍵值,表明了此方法的暴露名稱.這個配置需要頂一個request請求的方法類型(GET,POST等等),以及該請求中需要的參數(shù)也要被傳遞(比如charge=true),并且聲明返回對象是數(shù)組還是單個普通對象。這一切到搞定之后,你就可以在有這個業(yè)務(wù)實際需要求的時候,自由地調(diào)用CreditCard.charge()方法.
不要使用回調(diào)函數(shù)機制!(除非你真的需要它們)
第三個需要注意的事情是資源調(diào)用的返回類型.讓我們再次關(guān)注一下CreditCard.query()這個函數(shù).你將會看到不是在回調(diào)函數(shù)中給cards賦值,而是直接把它賦給card變量.在異步服務(wù)器請求的情況下唉,這樣的代碼是如何運作的哪?
你擔心代碼是否正常工作是對的,但是代碼實際上是可以正常工作的.這里實際發(fā)生的是AngularJS賦值了一個引用(是普通對象的還是數(shù)組的取決于你期望的返回類型),這個引用將會在未來服務(wù)器請求響應返回時被填充.在這期間,這個引用是個空應用.
因為在AngularJS應用中的大多數(shù)通用過程都是從服務(wù)器端取數(shù)據(jù),把它賦給一個變量,然后在模版上顯示它,而上面這樣的簡化機制非常優(yōu)雅.在你的控制器代碼中,你所有需要去做的就是發(fā)出服務(wù)器端請求,把返回值賦給正確的作用域(scope)變量.然后剩下的合適渲染這些數(shù)據(jù)就由模板系統(tǒng)去操心了.
如果你想對返回值做一些業(yè)務(wù)邏輯處理,拿著匯總方法就不能奏效了.在這種情況下,你就得依賴回調(diào)函數(shù)機制了,比如在Credit.get()調(diào)用中使用的那種機制.
簡化的服務(wù)器端操作
無論你使用資源簡化函數(shù)機制還是回調(diào)函數(shù),關(guān)于返回對象都有幾點問題需要我們注意.
返回的對象不是一個普通JS對象,實際上,他是“resource”類型的對象.這就意味著對象里除了包含服務(wù)器返回的數(shù)據(jù)以外,還有一些附加的行為函數(shù)(在這個案例中如$save()和$charge函數(shù)).這樣我們就可以很方便的執(zhí)行服務(wù)器端操作,比如取數(shù)據(jù)、修改數(shù)據(jù)并把修改在服務(wù)器端持久化保存下來(其實也就是一般CURD應用里面的通用操作).
對ngResource做單元測試
ngResource依賴項是一個封裝,它以Angular核心服務(wù)$http為基礎(chǔ).因此,你可能已經(jīng)知道如何對它做單元測試了.它和我們看到的對$http做單元測試的樣例比起來基本沒什么真正的變化.你只需要知道最終的服務(wù)器端請求應該由resource發(fā)起,告訴模擬$http服務(wù)關(guān)于請求的信息.其他的基本都一樣.下面我們來看看如何本節(jié)測試前面的代碼:
describe('Credit Card Resource', function(){var scope, ctrl, mockBackend;beforeEach(inject(function(_$httpBackend_, $rootScope, $controller) {mockBackend = _$httpBackend_;scope = $rootScope.$new();// Assume that CreditCard resource is used by the controllerctrl = $controller(CreditCardCtrl, {$scope: scope});}));it('should fetched list of credit cards', function() {// Set expectation for CreditCard.query() callmockBackend.expectGET('/user/123/card').respond([{id: '234', number: '11112222'}]);ctrl.fetchAllCards();// Initially, the request has not returned a responseexpect(scope.cards).toBeUndefined();// Tell the fake backend to return responses to all current requests// that are in flight.mockBackend.flush();// Now cards should be set on the scopeexpect(scope.cards).toEqualData([{id: '234', number: '11112222'}]);}); });這個測試看上去和簡單的$http單元測試非常相似,除了一些細微區(qū)別.注意在我們的expect語句里面,取代了簡單的"equals"方法,哦我們用的是特殊的"toEqualData"方法.這種eapect語句會智能地省略ngResource添加到對象上的附加方法.
$q和預期值(Promise)
目前為止,我們已經(jīng)看到了AngulrJS是如何實現(xiàn)它的異步延遲API機制.
預期值建議(Promise proposal)是AngularJS構(gòu)建異步延遲API的底層基礎(chǔ).作為底層機制,預期值建議(Promise proposal)為異步請求做了下面這些事:
- 異步請求返回的是一個預期(promise)而不是一個具體數(shù)據(jù)值.
- 預期值有一個then函數(shù),這個函數(shù)有兩個參數(shù),一個參數(shù)函數(shù)響應"resolved“或者"sucess"事件,另外一個參數(shù)函數(shù)響應"rejected”或者"failure"事件.這些函數(shù)以一個結(jié)果參數(shù)調(diào)用,或者以一個拒絕原因參數(shù)調(diào)用.
- 確保當結(jié)果返回的時候,兩個參數(shù)函數(shù)中有一個將會被調(diào)用
大多數(shù)的延遲機制和Q(詳見$q API文檔)是以上面這種方法實現(xiàn)的,AngularJS為什么這樣實現(xiàn)具體是因為以下原因:
- $q對于整個AngularJS是可見的,因此它被集成到作用域數(shù)據(jù)模型里面。這樣返回數(shù)據(jù)就能快速傳遞,UI上的閃爍更新也就更少.
- AngularJS模板也能識別$q預期值,因為預期值可以被當作結(jié)果值一樣對待,而不是把它僅僅當作結(jié)果的預期.這種預期值會在響應返回時被通知提醒.
- 更小的覆蓋范圍:AngularJS僅僅實現(xiàn)那些基本的、對于公共異步任務(wù)的需求來說最重要的延遲函數(shù)機制.
你也許會問這樣的問題:為什么我們會做如此瘋狂激進的實現(xiàn)機制?讓我們先看一個在在異步函數(shù)使用方面的標準問題:
fetchUser(function(user) {fetchUserPermissions(user, function(permissions) {fetchUserListData(user, permissions, function(list) {// Do something with the list of data that you want to display});}); });上面這個代碼就是人們使用JavaScirpt時經(jīng)常抱怨的令人恐懼的深層嵌套縮進椎體的噩夢.返回值異步本質(zhì)與實際問題的同步需求之間產(chǎn)生矛盾:導致多級函數(shù)包含關(guān)系,在這種情況下要想準確跟蹤里面某句代碼的執(zhí)行上下文環(huán)境就很難.
另外,這種情況對錯誤處理也有很大影響.錯誤處理的最好方法是什么?在每次都做錯誤處理?那代碼結(jié)構(gòu)就會非常亂.
為了解決上面這些問題,預期值建議(Promise proposal)機制提供了一個then函數(shù)的概念,這個函數(shù)會在響應成功返回的時候調(diào)用相關(guān)的函數(shù)去執(zhí)行,另一方面,當產(chǎn)生錯誤的時候也會干相同的事,這樣整個代碼就有嵌套結(jié)構(gòu)變?yōu)殒準浇Y(jié)構(gòu).所以之前那個例子用預期值A(chǔ)PI機制(至少在AngularJS中已經(jīng)被實現(xiàn)的)改造一下,代碼結(jié)構(gòu)會平整許多:
var deferred = $q.defer(); var fetchUser = function() {// After async calls, call deferred.resolve with the response valuedeferred.resolve(user);// In case of error, calldeferred.reject(‘Reason for failure’); } // Similarly, fetchUserPermissions and fetchUserListData are handleddeferred.promise.then(fetchUser).then(fetchUserPermissions).then(fetchUserListData).then(function(list) {// Do something with the list of data}, function(errorReason) {// Handle error in any of the steps here in a single stop });那個完全的橫椎體代碼一下子被優(yōu)雅地平整了,而且提供了鏈式的作用域,以及一個單點的錯誤處理.你在你自己的應用中處理異步請求回調(diào)時也可以用相同的代碼,只要調(diào)用Angular的$q服務(wù).這種機制可以幫我做一些很酷的事情:比如響應攔截.
響應攔截處理
我們的講解已經(jīng)覆蓋了怎樣調(diào)用服務(wù)器端服務(wù)、怎樣處理響應、怎樣把響應優(yōu)雅地抽象化封裝、怎樣處理異步回調(diào).但是在真實世界的Web應用中,你最終還不得不對每個服務(wù)器端請求調(diào)用做一些通用的處理操作,比如錯誤處理、權(quán)限認證、以及其它考慮到安全問題的處理操作,比如對響應數(shù)據(jù)做裁剪處理(譯注:有的Ajax響應為了安全需要,會添加一定約定好的噪聲數(shù)據(jù)).
有著現(xiàn)在已經(jīng)對$q API的深入理解,我們目前就可以利用響應攔截器機制來做上面所有提出過的功能.響應攔截器(正如其名)可以在響應數(shù)據(jù)被應用使用之前攔截他它,并且對它做數(shù)據(jù)轉(zhuǎn)換處理,比如錯誤處理以及其它任何處理,包括廚房洗碗槽.(估計是指數(shù)據(jù)清洗)
讓我們看一個代碼例子,這個例子中的代碼攔截響應,并對響應數(shù)據(jù)做了輕微的數(shù)據(jù)轉(zhuǎn)換.
// register the interceptor as a service myModule.factory('myInterceptor', function($q, notifyService, errorLog) {return function(promise) {return promise.then(function(response) {// Do nothingreturn response;}, function(response) {// My notify service updates the UI with the error messagenotifyService(response);// Also log it in the console for debug purposeserrorLog(response);return $q.reject(response);});} });// Ensure that the interceptor we created is part of the interceptor chain $httpProvider.responseInterceptors.push('myInterceptor');安全方面的考慮
目前我們開發(fā)Web應用的時候,安全是一個非常重要的關(guān)注點,在我們的考慮維度直中,它必須作為首位被考慮.AngularJS給我們提供了一些幫助,同時也帶來了兩個安全攻擊的角度,下面這一節(jié)我們將會講解這些內(nèi)容.
JSON的安全脆弱性
當我們對服務(wù)器發(fā)送一個請求JSON數(shù)組數(shù)據(jù)的GET請求時(特別是當這些數(shù)據(jù)是敏感數(shù)據(jù)且需要登錄驗證或讀取授權(quán)時),就會有一個不易察覺的JSON安全漏洞被暴露出來.
當我們使用一個惡意站點時,站點可能會用<script>標簽發(fā)起同樣的請求而得到相同的信息.因為你仍舊是登錄狀態(tài),惡意站點利用了你的驗證信息而請求了JSON數(shù)據(jù),并且得到了它.
你或許驚奇是如何做到的,因為信息仍舊在你客戶端,服務(wù)器也得不到這個數(shù)組信息的引用.并且通常作為請求腳本返回響應JSO對象會導致一個執(zhí)行錯誤,雖然數(shù)組是個列外.
但是漏洞真正的切入點是:在JavaScript里,你是可以對內(nèi)建對象做重定義的.在這個漏洞里面,數(shù)組的構(gòu)造函數(shù)可以被重定義,通過這種重定義,惡意站點腳本就可以得到對響應數(shù)據(jù)的引用,然后就可以把響應數(shù)據(jù)發(fā)回它自己的服務(wù)器嘍.
有兩種方法可以防止這個漏洞:一是通常要確保敏感數(shù)據(jù)信息只作為POST請求的響應被返回,二是返回一個不合法的JSON表達式,然后客戶端用約定好的邏輯把不合法數(shù)據(jù)轉(zhuǎn)換為可用的真實數(shù)據(jù).
AngulaJS中你可以兩種方法都用來阻止這個漏洞.在你的應用中,你可以而且應該選擇敏感JSON信息只通過POST請求來獲取.
進一步,你可以在服務(wù)器端給JSON響應數(shù)據(jù)配置一個前綴字符串:
")]}`,\n"那么一個正常JSON響應比如:
['one','two']通過前綴字符串設(shè)置,這個JSON響應就會變?yōu)?/p> ")]}'", ['one', 'two']
AngularJS將會自動的把前綴字符串過濾掉,然后僅僅處理真實JSON數(shù)據(jù).
跨站請求偽造(XSRF)
跨站請求偽造攻擊主要有以下特征:
- 它們影響的站點通常依賴于授權(quán)或者用戶認證.
- 它們往往利用漏洞站點保存登錄或者授權(quán)信息這個事實.
- 它們發(fā)起以假亂真的HTTP或者XMLHTTPRequest請求來制造副作用,這種副作用通常是有害的.
考慮依稀下面這個跨站請求偽造攻擊的案例:
- 用戶A登錄進他的銀行帳號(http://www.examplebank.com/)
- 用戶B意識到這點,然后誘導用戶A訪問用戶B的個人主頁
- 主頁上有一個特殊手工生成的圖片連接地址,這個圖片的的指向地址將會導致一次跨站請求偽造攻擊,比如如下代碼:<img src="http://www.examplebank.com/xfer?from=UserA&amount=10000&to=UserB" />
如果用戶A的銀行站點把授權(quán)信息保存在cookie里,且Cookie還沒過期.當用戶A打開用戶B的站點時,就會導致非授權(quán)的用戶A給用戶B轉(zhuǎn)賬行為.
那么AngularJS是怎么幫助你防止這種事情發(fā)生?它提供一種雙步機制來防止跨站請求偽造攻擊.
在客戶端,當發(fā)起XHR異步請求時,$http服務(wù)會從一個叫XSRF-TOKEN的cookie中讀取令牌值,然后把它設(shè)置成X-XSRF-TOKEN頭信息的值,因為只有你自己域的請求才能讀取和設(shè)置這個令牌,你可以保證XHR請求只來自你自己的域.
同時,服務(wù)器端代碼也需要一點輕微的修改,以便于你收到你的第一個HTTP GET請求時就設(shè)置一個可讀取的對話Cookie,這個對話Cookie鍵叫XSRF-TOKEN。后續(xù)客戶端發(fā)往服務(wù)器的請求就可以通過對比請求頭信息的令牌值和之前第一個請求設(shè)置的Cookie令牌值來達到驗證的目的.當然,令牌必須是一個用戶一個唯一的令牌值.這個令牌值必須在服務(wù)器端驗證(以防止惡意腳本捏造假令牌).
總結(jié)
以上是生活随笔為你收集整理的AngualrJS之服务器端通信的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: android 反射调用方法可不可以重载
- 下一篇: android uid systemui