譯自《AngularJS》
?
與服務器通信
?
目前,我們已經接觸過下面要談的主題的主要內容,這些內容包括你的Angular應用如何規劃設計、不同的angularjs部件如何裝配在一起并正常工作以及AngularJS中的模板代碼運行機制的一小部分內容。把它們結合在一起,你就可以搭建一些簡潔優雅的Web應用,但他們的運作主要還是限制在客戶端.在前面第二章,我們接觸了一點用$http
服務做與服務器端通信的內容,但是在這一章,我們將會深入探討如何在現實世界的真實應用中使用它($http
).
在這一章,我們將討論一下AngularJS如何幫你與服務器端通信,這其中包括在最低抽象等級的層面或者用它提供的優雅的封裝器。而且我們將會深入探討AngularJS如何用內建緩存機制來幫你加速你的應用.如果你想用SocketIO
開發一個實時的Angular應用,那么第八章有一個例子,演示了如何把·SocketIO·封裝成一個指令然后如何使用這個指令,在這一章,我們就不涉及這方面內容了.
目錄
- 通過$http進行通行
- 進一步配置你的請求
- 設定HTTP頭信息(Headers)
- 緩存響應數據
- 對請求(Request)和響應(Response)的數據所做的轉換
- 單元測試
- 使用RESTful資源
- resource資源的聲明
- 定制方法
- 不要使用回調函數機制!(除非你真的需要它們)
- 簡化的服務器端操作
- 對ngResource做單元測試
- $q和預期值(Promise)
- 響應攔截處理
- 安全方面的考慮
- JSON的安全脆弱性
- 跨站請求偽造(XSRF)
通過$http進行通行
從Ajax應用(使用XMLHttpRequests)發動一個請求到服務器的傳統方式包括:得到一個XMLHttpRequest對象的引用、發起請求、讀取響應、檢驗錯誤代碼然后最后處理服務器響應。它就是下面這樣:
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();
對于這樣一個簡單、常用且經常重復的任務,上面這個代碼量比較大.如果你想重復性地做這件事,你最終可能會做一個封裝或者使用現成的庫.
AngularJS XHR(XMLHttpRequest) API遵循Promise接口.因為XHRs是異步方法調用,服務器響應將會在未來一個不定的時間返回(當然希望是越快越好).Promise接口保證了這樣的響應將會如何處理,它允許Promise接口的消費者以一種可預計的方式使用這些響應.
假設我們想從我們的服務器取回用戶的信息.如果接口在/api/user地址可用,并且接受id作為url參數,那么我們的XHR請求就可以像下面這樣使用Angular的核心$http服務:
$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核心服務$http提供的眾多簡便方法之一.類似的,如果你想使用AngularJS向相同URL帶一些POST請求數據發起一個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為大多數常用請求類型都提供了類似的簡便方法,他們包括:
- GET
- HEAD
- POST
- DELETE
- PUT
- JSONP
進一步配置你的請求
有時,工具箱提供的標準請求配置還不夠,它可能是因為你想做下面這些事情:
- 你可能想為請求添加權限驗證的頭信息
- 改變請求數據的緩存方式
- 在請求被發送或者響應返回時,對數據以一些方式做一定的轉換處理
在上面這樣的情況之下,你可以進一步配置請求,通過可選的傳遞進請求的配置對象.在之前的例子中,我們使用配置對象來標明可選的URL參數,即便我們哪兒演示的GET和POST方法是簡便方法。內部的原生方法可能看上面像相面這樣:
$http(config)
下面演示的是一個調用這個方法的偽代碼模板:
$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和其它的簡便方法已經設置了請求的method類型,所以不需要再設置這個,config配置對象是傳給·$http.get·、·$http.post·方法的最后一個參數,所以當你使用任何簡便方法的時候,你任何能用這個config配置對象.
你也可以通過傳入含有下面這些鍵的屬性集config對象來改變已有的request對象
- method : 一個表示http請求類型的字符串,比如GET,或者POST
- url : 一個URL字符串代表要請求資源的絕對或相對URL
- params : 一個對象(準確的說是鍵值映射)包含字符串到字符串內容,它代表了將會轉換為URL參數的鍵值對,比如下面這樣: [{key1: 'value1', key2: 'value2'}] 它將會被轉換為: ?key1=value&key2=value2 這串字符將會加在URL后面,如果在value的位置你用一個對象取代字符串或數字,那這個對象將會轉換為JSON字符串.
- data :一個字符串或一個對象,它將會被作為請求消息數據被發送.
- timeout : 這是請求被認定為過期之前所要等待的毫秒數.
還有部分另外的選項可以被配置,在下面的章節中,我們將會深度探索這些選項.
設定HTTP頭信息(Headers)
AngularJS有一個默認的頭信息,這個頭信息將會對所有的發送請求使用,它包含以下信息: 1.Accept: application/json, text/plain, / 2.X-Requested-With:XMLHttpRequest
如果你想設置任何特定的頭信息,這兒有兩種方法來做這件事:
第一種方法,如果你相對所有的發送請求都使用這些特定頭信息,那你需要把特定有信息設置為Angular默認頭信息的一部分.可以在$httpProvider.defaults.headers
配置對象里面設置這個,這個步驟通常會在你的app設置config部分來做.所以如果你想移除"Requested-With"頭信息且對所有的GET請求啟用"DO NOT TRACK"設置,你可以簡單地通過以下代碼來做:
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';
});
如果你只想對某個特定的請求設置頭信息,而不是設置默認頭信息.那么你可以通過給$http服務傳遞包含指定頭信息的config對象來做.相同的定制頭信息可以作為第二個參數傳遞給GET請求,第一個參數是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 });
如何在應用中處理權限驗證頭信息的成熟示例將會在第八章的Cheetsheets示例部分給出.
緩存響應數據
AngularJS為HTTP GET請求提供了一個開箱即用的簡單緩存系統.缺省情況下,它對所有的請求都是禁用的,但是如果你想對你的請求啟用緩存系統,你可以使用以下代碼:
$http.get('http://server/myapi', {cache: true
}).success(function() { // Handle success });
這段代碼啟用了緩存系統,然后AngularJS將會緩存來自Server的響應數據.但對相同的URL的請求第二次發出時,AngularJS將會從緩存里面取出前一次的響應數據作為響應返回.這個緩存系統也很智能,即使你同時對相同URL發出多個請求,只有一個請求會發向Server,這個請求的響應數據將會反饋給所有(同時發起的)請求。
然而這種做法從可用性的角度看可能是有所沖突的,當一個用戶首先看到舊的結果,然后新的結果突然冒出來,比如一個用戶可能即將單擊一個數據項,而實際上這個數據項后臺已經發生了變化.
注意所有響應(即使是從緩存里取出的)本質上仍舊是異步響應.換句話說,期望你的利用緩存響應時的異步代碼運行仍舊和他向后臺服務器發出請求時的代碼運行機制是一樣的.
對請求(Request)和響應(Response)的數據所做的轉換
AngularJS對所有$http
服務發起的請求和響應做一些基本的轉換,它們包括:
- 請求(Request)轉換: 如果請求的Cofig配置對象的data屬性包含一個對象,將會把這個對象序列化為JSON格式.
- 響應(Response)轉換: 如果探測到一個XSRF頭,把它剝離掉.如果響應數據被探測為JSON格式,用JSON解析器把它反序列化為JSON對象.
如果你需要部分系統默認提供的轉換,或者想使用你自己的轉換,你可以把你的轉換函數作為Config配置對象的一部分傳遞進去(后面有細述).這些轉換函數得到HTTP請求和HTTP響應的數據主體以及它們的頭信息.然后把序列化的修改后版本返回出來.在Config對象里面配置這些函數需要使用·transformRequest·鍵和·transformResponse·鍵,這些都可以通過使用`$httpProvider·服務在模塊的config函數里面配置它.
我們什么時候使用這些哪?讓我假設我們有一個服務器,它更習慣于jQuery運行的方式.它可能希望我們的POST數據以key1=val1&key2=val2
字符串的形式傳遞,而不是以{key1:val1,key2:val2}
這樣的JSON格式.這個時候,我們可能相對每個請求做這樣的轉換,或者單個地增加transformRequest轉換函數,為了達成這個示例這樣的目標,我們將要設置一個通用transformRequet轉換函數,以便對所有的發出請求,這個函數都可以把JSON格式轉換為鍵值對字符串,下面代碼演示了如何做這個工作:
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);};
});
單元測試
目前為止,我們已經了解如何使用$http
服務以及如何以可能的方式做你需要的配置.但是如何寫一些單元測試來保證這些夠真實有效的運行哪?
正如我們曾經三番五次的提到的那樣,AngularJS一直以測試為先的原則而設計.所以Angualr有一個模擬服務器后端,在單元測試中,它可以幫你就可以測試你發出的請求是否正確,甚至可以精確控制模擬響應如何得到處理,什么時候得到處理.
讓我們探索一下下面這樣的單元測試場景:一個控制向服務器發起請求,從服務器得到數據,把它賦給作用域內的模型,然后以具體的模板格式顯示出來.
我們的NameListCtrl
控制器是一個非常簡單的控制器.它的存在只有一個目的:訪問names
API接口,然后把得到數據存儲在作用域scope模型內.
function NamesListCtrl($scope, $http) {$http.get('http://server/names', {params: {filter: ‘none’}}).success(function(data) {$scope.names = data;});
}
怎樣對這個控制器做單元測試?在我們的單元測試中,我們必須保證下面這些條件:
NamesListCtrl
能夠找到所有的依賴項(并且正確的得到注入的這些依賴)》- 當控制器加載時盡可能快地立刻發情請求從服務器得到names數據.
- 控制器能夠正確地把響應數據存儲到作用域scope的
names
變量屬性中.
在我們的單元測試中構造一個控制器時,我們給它注入一個scope作用域和一個偽造的HTTP服務,在構建測試控制器的方式和生產中構建控制器的方式其實是一樣的.這是推薦方法,盡管它看上去上有點復雜。讓我看一下具體代碼:
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·服務提供一個比較底層的實現來幫你發起XHR請求,但是同時也給提供了很強的可控性和彈性.在大多數情況下,我們處理的是對象集或者是封裝有一定屬性和方法的對象模型,比如帶有個人資料的自然人對象或者信用卡對象.
在上面這樣的情況下,如果我們自己構建一個js對象來表示這種較復雜對象模型,那做法就有點不夠nice.如果我們僅僅想編輯某個對象的屬性、保存或者更新一個對象,那我們如何讓這些狀態在服務器端持久化.
$resource
正好給你提供這種能力.AngularJS resources可以幫助我們以描述的方式來定義對象模型,可以定義一下這些特征:
- resource的服務器端URL
- 這種請求常用參數的類型
- (你可以免費自動得到get、save、query、remove和delete方法),除了那些方法,你可以定義其它的方法,這些方法封裝了對象模型的特定功能和業務邏輯(比如信用卡模型的charge()付費方法)
- 響應的期望類型(數組或者一個獨立對象)
- 頭信息
什么時候你可以用Angular Resources組件?
只有你的服務器端設施是以RESTful方式提供服務的時候,你才應該用Angular resources組件.比如信用卡那個案例,我們將用它作為本章這一部分的例子,他將包括以下內容:
- 給地址
/user/123/card
發送一個GET請求,將會得到用戶123的信用卡列表. - 給地址
/user/123/card/15
發送一個GET請求,將會得到用戶123本人的ID為15的信用卡信息 - 給地址
/user/123/card
發送一個在POST請求數據部分包含信用卡信息的POST請求,將會為用戶123新創建一個信用卡 - 給地址
/user/123/card/15
發送一個在POST請求數據部分包含信用卡信息的POST請求,將會更新用戶123的ID為5的信用卡的信息. - 給地址
/user/123/card/15
一個方法為DELETE類型的請求,將會刪除掉用戶123的ID為5的信用卡的數據.
除了按照你的要求給你提供一個查詢服務器端信息的對象,$resource
還可以讓你使用返回的數據對象就像他們是持久化數據模型一樣,可以修改他們,還可以把你的修改持久化存儲下來.
ngResource
是一個單獨的、可選的模塊.要想使用它,你看需要做以下事情:
- 在你的HTML文件里面引用angular-resource.js的實際地址
- 在你的模塊依賴里面聲明對它的依賴(例如,angular.module('myModule',['ngResource'])).
- 在需要的地方,注入$resource這個依賴項.
在我們看怎樣用ngResource方法創建一個resource資源之前,我們先看一下怎樣用基本的$http服務做類似的事情.比如我們的信用卡resource,我們想能夠讀取、查詢、保存信用卡信息,另外還要能為信用卡還款.
這兒是上述需求一個可能的實現:
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}});}};
}]);
取代以上方式,你也可以輕松創建一個在你的應用中始終如一的Angular資源服務,就像下面代碼這樣:
myAppModule.factory('CreditCard', ['$resource', function($resource) {return $resource('/user/:userId/card/:cardId',{userId: 123, cardId: '@id'},{charge: {method:'POST', params:{charge:true}, isArray:false});
}]);
做到現在,你就可以任何時候從Angular注入器里面請求一個CreditCard依賴,你就會得到一個Angular資源,默認情況下,這個資源會提供一些初始的可用方法.表格5-1列出了這些初始方法以及他們的運行行為,這樣你就可以知道在服務器怎樣配置來配合這些方法了.
表格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'}
});
前面這個樣例代碼里面發生了很多事情,所以我們將會一個一個地認真講解其中的重要部分:
resource資源的聲明
聲明你自己的$resource
非常簡單,只要調用注入的$resource函數,并給他傳入正確的參數即可。(你現在應該已經知道如何注入依賴,對吧?)
$resource函數只有一個必須參數,就是提供后臺資源數據的URL地址,另外還有兩個可選參數:默認request參數信息和其它的想在資源上要配置的動作.
請注意:第一個URL地址參數中的的變量數據是參數化可配置的(:冒號是參數變量的語法符號,比如:userId
以為這個參數將會被實際的userId參數變量取代(譯者注:寫過參數化SQL語句的人應該很熟悉),而:cardId
將會被cardId參數變量的值所取代),如果沒有給函數傳遞這些參數變量,那那個位置將會被空字符取代.
函數的第二個參數則負責提供所有請求的默認參數變量信息.在這個案例中,我們給userId參數傳遞一個常量:123,cardId參數則更有意思,我們給cardId參數傳遞了一個"@id"字符串.這意味著如果我們使用一個從服務器返回的對象而且我們可以調用這個對象的任何方法(比如$save),那么這個對象的id屬性將會被取出來賦給cardId字段.
函數的第三個參數是一些我們想要暴露的其它方法,這些方法是對定制資源做操作的方法.在下面的章節,我們將會深度討論這個話題
定制方法
$resource函數的第三個參數是可選的,主要用來傳遞要在resource資源上暴露的其它自定義方法。
在這個案例中,我們自定義了一個方法charge.這個自定義方法可以通過傳遞一個對象而被配置上.這個對象里有個鍵值,表明了此方法的暴露名稱.這個配置需要頂一個request請求的方法類型(GET,POST等等),以及該請求中需要的參數也要被傳遞(比如charge=true),并且聲明返回對象是數組還是單個普通對象。這一切到搞定之后,你就可以在有這個業務實際需要求的時候,自由地調用CreditCard.charge()
方法.
不要使用回調函數機制!(除非你真的需要它們)
第三個需要注意的事情是資源調用的返回類型.讓我們再次關注一下CreditCard.query()
這個函數.你將會看到不是在回調函數中給cards賦值,而是直接把它賦給card變量.在異步服務器請求的情況下唉,這樣的代碼是如何運作的哪?
你擔心代碼是否正常工作是對的,但是代碼實際上是可以正常工作的.這里實際發生的是AngularJS賦值了一個引用(是普通對象的還是數組的取決于你期望的返回類型),這個引用將會在未來服務器請求響應返回時被填充.在這期間,這個引用是個空應用.
因為在AngularJS應用中的大多數通用過程都是從服務器端取數據,把它賦給一個變量,然后在模版上顯示它,而上面這樣的簡化機制非常優雅.在你的控制器代碼中,你所有需要去做的就是發出服務器端請求,把返回值賦給正確的作用域(scope)變量.然后剩下的合適渲染這些數據就由模板系統去操心了.
如果你想對返回值做一些業務邏輯處理,拿著匯總方法就不能奏效了.在這種情況下,你就得依賴回調函數機制了,比如在Credit.get()調用中使用的那種機制.
簡化的服務器端操作
無論你使用資源簡化函數機制還是回調函數,關于返回對象都有幾點問題需要我們注意.
返回的對象不是一個普通JS對象,實際上,他是“resource”類型的對象.這就意味著對象里除了包含服務器返回的數據以外,還有一些附加的行為函數(在這個案例中如$save()和$charge函數).這樣我們就可以很方便的執行服務器端操作,比如取數據、修改數據并把修改在服務器端持久化保存下來(其實也就是一般CURD應用里面的通用操作).
對ngResource做單元測試
ngResource依賴項是一個封裝,它以Angular核心服務$http
為基礎.因此,你可能已經知道如何對它做單元測試了.它和我們看到的對$http
做單元測試的樣例比起來基本沒什么真正的變化.你只需要知道最終的服務器端請求應該由resource發起,告訴模擬$http
服務關于請求的信息.其他的基本都一樣.下面我們來看看如何本節測試前面的代碼:
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
單元測試非常相似,除了一些細微區別.注意在我們的expect語句里面,取代了簡單的"equals"方法,哦我們用的是特殊的"toEqualData"方法.這種eapect語句會智能地省略ngResource添加到對象上的附加方法.
$q
和預期值(Promise)
目前為止,我們已經看到了AngulrJS是如何實現它的異步延遲API機制.
預期值建議(Promise proposal)是AngularJS構建異步延遲API的底層基礎.作為底層機制,預期值建議(Promise proposal)為異步請求做了下面這些事:
- 異步請求返回的是一個預期(promise)而不是一個具體數據值.
- 預期值有一個
then
函數,這個函數有兩個參數,一個參數函數響應"resolved“或者"sucess"事件,另外一個參數函數響應"rejected”或者"failure"事件.這些函數以一個結果參數調用,或者以一個拒絕原因參數調用. - 確保當結果返回的時候,兩個參數函數中有一個將會被調用
大多數的延遲機制和Q(詳見$q API文檔)是以上面這種方法實現的,AngularJS為什么這樣實現具體是因為以下原因:
- $q對于整個AngularJS是可見的,因此它被集成到作用域數據模型里面。這樣返回數據就能快速傳遞,UI上的閃爍更新也就更少.
- AngularJS模板也能識別$q預期值,因為預期值可以被當作結果值一樣對待,而不是把它僅僅當作結果的預期.這種預期值會在響應返回時被通知提醒.
- 更小的覆蓋范圍:AngularJS僅僅實現那些基本的、對于公共異步任務的需求來說最重要的延遲函數機制.
你也許會問這樣的問題:為什么我們會做如此瘋狂激進的實現機制?讓我們先看一個在在異步函數使用方面的標準問題:
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時經常抱怨的令人恐懼的深層嵌套縮進椎體的噩夢.返回值異步本質與實際問題的同步需求之間產生矛盾:導致多級函數包含關系,在這種情況下要想準確跟蹤里面某句代碼的執行上下文環境就很難.
另外,這種情況對錯誤處理也有很大影響.錯誤處理的最好方法是什么?在每次都做錯誤處理?那代碼結構就會非常亂.
為了解決上面這些問題,預期值建議(Promise proposal)機制提供了一個then函數的概念,這個函數會在響應成功返回的時候調用相關的函數去執行,另一方面,當產生錯誤的時候也會干相同的事,這樣整個代碼就有嵌套結構變為鏈式結構.所以之前那個例子用預期值API機制(至少在AngularJS中已經被實現的)改造一下,代碼結構會平整許多:
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
});
那個完全的橫椎體代碼一下子被優雅地平整了,而且提供了鏈式的作用域,以及一個單點的錯誤處理.你在你自己的應用中處理異步請求回調時也可以用相同的代碼,只要調用Angular的$q服務.這種機制可以幫我做一些很酷的事情:比如響應攔截.
響應攔截處理
我們的講解已經覆蓋了怎樣調用服務器端服務、怎樣處理響應、怎樣把響應優雅地抽象化封裝、怎樣處理異步回調.但是在真實世界的Web應用中,你最終還不得不對每個服務器端請求調用做一些通用的處理操作,比如錯誤處理、權限認證、以及其它考慮到安全問題的處理操作,比如對響應數據做裁剪處理(譯注:有的Ajax響應為了安全需要,會添加一定約定好的噪聲數據).
有著現在已經對$q API的深入理解,我們目前就可以利用響應攔截器機制來做上面所有提出過的功能.響應攔截器(正如其名)可以在響應數據被應用使用之前攔截他它,并且對它做數據轉換處理,比如錯誤處理以及其它任何處理,包括廚房洗碗槽.(估計是指數據清洗)
讓我們看一個代碼例子,這個例子中的代碼攔截響應,并對響應數據做了輕微的數據轉換.
// 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');
安全方面的考慮
目前我們開發Web應用的時候,安全是一個非常重要的關注點,在我們的考慮維度直中,它必須作為首位被考慮.AngularJS給我們提供了一些幫助,同時也帶來了兩個安全攻擊的角度,下面這一節我們將會講解這些內容.
JSON的安全脆弱性
當我們對服務器發送一個請求JSON數組數據的GET請求時(特別是當這些數據是敏感數據且需要登錄驗證或讀取授權時),就會有一個不易察覺的JSON安全漏洞被暴露出來.
當我們使用一個惡意站點時,站點可能會用<script>標簽發起同樣的請求而得到相同的信息.因為你仍舊是登錄狀態,惡意站點利用了你的驗證信息而請求了JSON數據,并且得到了它.
你或許驚奇是如何做到的,因為信息仍舊在你客戶端,服務器也得不到這個數組信息的引用.并且通常作為請求腳本返回響應JSO對象會導致一個執行錯誤,雖然數組是個列外.
但是漏洞真正的切入點是:在JavaScript里,你是可以對內建對象做重定義的.在這個漏洞里面,數組的構造函數可以被重定義,通過這種重定義,惡意站點腳本就可以得到對響應數據的引用,然后就可以把響應數據發回它自己的服務器嘍.
有兩種方法可以防止這個漏洞:一是通常要確保敏感數據信息只作為POST請求的響應被返回,二是返回一個不合法的JSON表達式,然后客戶端用約定好的邏輯把不合法數據轉換為可用的真實數據.
AngulaJS中你可以兩種方法都用來阻止這個漏洞.在你的應用中,你可以而且應該選擇敏感JSON信息只通過POST請求來獲取.
進一步,你可以在服務器端給JSON響應數據配置一個前綴字符串:
")]}`,\n"
那么一個正常JSON響應比如:
['one','two']
通過前綴字符串設置,這個JSON響應就會變為
")]}'",
['one', 'two']
AngularJS將會自動的把前綴字符串過濾掉,然后僅僅處理真實JSON數據.
跨站請求偽造(XSRF)
跨站請求偽造攻擊主要有以下特征:
- 它們影響的站點通常依賴于授權或者用戶認證.
- 它們往往利用漏洞站點保存登錄或者授權信息這個事實.
- 它們發起以假亂真的HTTP或者XMLHTTPRequest請求來制造副作用,這種副作用通常是有害的.
考慮依稀下面這個跨站請求偽造攻擊的案例:
- 用戶A登錄進他的銀行帳號(http://www.examplebank.com/)
- 用戶B意識到這點,然后誘導用戶A訪問用戶B的個人主頁
- 主頁上有一個特殊手工生成的圖片連接地址,這個圖片的的指向地址將會導致一次跨站請求偽造攻擊,比如如下代碼:
<img src="http://www.examplebank.com/xfer?from=UserA&amount=10000&to=UserB" />
如果用戶A的銀行站點把授權信息保存在cookie里,且Cookie還沒過期.當用戶A打開用戶B的站點時,就會導致非授權的用戶A給用戶B轉賬行為.
那么AngularJS是怎么幫助你防止這種事情發生?它提供一種雙步機制來防止跨站請求偽造攻擊.
在客戶端,當發起XHR異步請求時,$http服務會從一個叫XSRF-TOKEN的cookie中讀取令牌值,然后把它設置成X-XSRF-TOKEN頭信息的值,因為只有你自己域的請求才能讀取和設置這個令牌,你可以保證XHR請求只來自你自己的域.
同時,服務器端代碼也需要一點輕微的修改,以便于你收到你的第一個HTTP GET請求時就設置一個可讀取的對話Cookie,這個對話Cookie鍵叫XSRF-TOKEN。后續客戶端發往服務器的請求就可以通過對比請求頭信息的令牌值和之前第一個請求設置的Cookie令牌值來達到驗證的目的.當然,令牌必須是一個用戶一個唯一的令牌值.這個令牌值必須在服務器端驗證(以防止惡意腳本捏造假令牌).