AngularJS使用XMLHttpRequest(XHR)和JSONP请求来与后端通信,它用通用的$http服务来发布XHR和JSONP请求。
$http请求
$http APIs 实例如下:
首先有一个专用的方法发出XHR GET请求,有几种同样的其他类型的XHRrequests方法如下:
Get:$http.get(url,config)
POST:$http.post(url,data,config)
Put:$http.put(url,data,config)
Delete:$http.delete(url,config)
Head:$http.head
JSONP请求:$http.jsonp(url, config)
$http方法接受的参数依赖于使用的HTTP方法。对于可以在body中添加数据的方法(如POST和PUT),参数如下:
url:将要调用的目标URL
data:将要发送的数据
config:js对象,包含其他的影响请求和响应的配置选项。
对于其他方法(GET,DELETE,HEAD,JSONP),在请求body中没有数据发送,参数变得简单,可以简化为两个参数:url和config。
从$http方法返回的对象允许我们注册success和error回调函数。
http响应处理
请求可能会成功或者失败,AngularJS提供两种方法来注册回调函数处理两种结果:success和error.。所有的方法都接受包含以下参数的回调函数:
data:实际返回数据
status:响应的HTTP状态
headers:接触到HTTP响应头的函数
config:当请求触发时提供的配置对象
success的返回码是:200-299,其他的返回码是error。如果不注册回调函数,响应将会忽略。
同源策略
1、JSONP
浏览器可以使用script标签从外部服务器拉取js。JSONP不触发XHR请求,但是产生一个<script>标签,源指向外部资源。一旦产生的script标签出现在DOM中,浏览器执行其功能,并调用服务器,服务器在我们的web应用中使用函数调用。JSONP实例如下:
调用$http.jsonp函数,AngularJS将动态创建一个新的<script>DOM元素如下:
只要这个script标签添加到DOM中,浏览器将会请求URL,响应一旦返回,将会有内容如下:
一个JSONP响应是一个正常的JS函数调用。AngularJS产生angular.callbacks._k函数,这个函数一旦调用将会触发success回调函数。提供给$http.jsonp函数调用的URL必须包含JSON_CALLBACK请求参数。AngularJS将这个字符串转为动态生成的函数名。
JSONP的限制:
1)仅能获取HTTP请求,错误处理将成问题,因为浏览器不会在返回给js的script标签中暴露HTTP响应状态。实际上记录http状态错误并触发出错回调函数很困难。
2)JSONP也会暴露我们的web应用产生安全问题。除了XSS攻击,最大的问题是一个服务器能在JSONP响应中产生任意抽象的js。这个js将会被加载到浏览器中并在用户的session中执行,引起不同危害,从简单的网页奔溃到窃取敏感数据,所以我们应该通过jsonp请求谨慎选择服务并且仅使用可信任的服务器。
2、使用CORS
建立于XMLHttpRequest对象之上以一种已定义并可控的方式实现跨域AJAX请求,CORS的整体思想是:浏览器和外部服务器需要协作(通过发送合适的请求和响应头)来有条件的使用跨域请求。所以,外部服务器需要配置。浏览器必须能发送合适的请求头,并对跨域请求成功的服务器响应进行解释。
CORS请求分为simple(GET POST HEAD请求) 和 non-simple(使用HTTP动词或者允许集合之外的请求头)。
对于non-simple的请求,浏览器必须在发送最初请求之前发送探测的OPTION请求,并等待服务器的批准。仔细观察HTTP就会发现OPTIONS请求。一个删除用户的例子如下:
检查HTTP通信可以看到两个请求(OPTIONS和DELETE)指向同一个URL
3、服务器端代理
JSONP不是一个理想的跨域请求技术。CORS规范使情况好转,但仍需要在服务器端进行额外的配置,并且浏览器需要支持标准。如果无法使用CORS或JSONP技术,就是永远的选择完全避免跨域请求的问题。我们可以通过配置本地服务器作为对外部服务器的代理实现这一目标。通过采用正确的服务器配置我们可以通过我们的服务器代理跨域请求,从而让浏览器的目标只有我们的服务器。这种技术适用于所有的浏览器,并且不需要提前发生OPTIONS请求。此外,它不会添加任何附加的安全风险。这种方法的缺点是,我们需要相应地配置服务器。
promises 和 $q
$q基础
举个例子:我们通过电话预订pizza并让其快递到我们的家。虽然预定pizza只需要一个很短的电话,实际交货(订单执行)需要一定的时间,是异步的。为了感受Promise API,我们使用$q来对pizza订单及其成功交付进行建模。首先,定义一个Person,可以吃掉pizza,或者当订单未送达时感到饥饿,如下所示:
可以有个eat和beHungry方法当作success和error的回调函数。
现在对pizza预定建模并完成整个过程如下:
$q.defer()返回一个deferred对象,这个对象有两个作用:
1)包含一个promise对象(在promise属性中)。promises是对一个deferred任务执行结果(成功或失败)的占位符
2)暴露方法来触发任务完成(resolve)或者失败(reject)
promise API有两个作用:控制未来任务执行、执行结果。
控制任务执行的entity(在我们的例子中是restaurant)暴露一个promise对象(pizzaOrderFulfillment.promise)给对任务结果感兴趣的实体。例子中,Pawel对order感兴趣,通过在promise对象上注册回调函数来表达他的兴趣。为了注册回调函数,使用了then(successCallBack,errorCallBack)方法。这个方法接受回调函数将会在任务结果中调用。为了标志任务执行结束,需要在deferred对象上调用resolve方法。传递给resolve方法的参数将会被用来作为提供给success回调函数的值。当success回调函数调用后,任务结束,promise被resolved。通常reject方法调用将会触发error回调和promise rejection。
Promises是first-class js对象
我们可以通过他们(first-class js对象)周围的参数和函数调用返回它们。这让我们易封装异步操作的服务。如下:
restaurant服务封装了异步任务,从takeOrder方法返回一个promise,返回的promise可以被restaurant顾客使用来放置promised结果,当结果可用时notified。rejecting promises和触发error回调函数例子如下:
收集回调函数
一个promise对象可以被用来注册多个callbacks,如下所示:
多个成功的callbacks被注册,当一个promise resolve后所有这些都被触发,同样,promise rejection也会触发所有注册的error callbacks。
注册callbacks和promise生命周期
一旦promise resolved或者rejected,将不会改变其状态。仅有一次机会提供promised结果,不会出现以下情况:
resolve一个rejected promise
用一个不同的结果resolve一个已经resolved promise
reject一个resolved promise
用一个不同的rejection理由reject一个rejected promise
例如如果我们的pizza已经成功送到或者可能吃掉了再告知我们订单有问题,那就没有任何意义了。
任何注册的回调函数在一个promise resolved(rejected)后将会以同样的结果resolved(或者同样的失败缘由rejected)。
异步操作链
Promise API真正有用的地方是在异步环境中模仿同步函数调用。
继续pizza例子,想象下现在我们被朋友邀请吃pizza,我们的hosts将会预定一个比萨,一旦order到来,他们将会切块吃。有一系列异步事件:首先一个pizza需要被delivered,只有这样才能准备serving。在我们开始享用之前两个promise需要resolved:restaurant承诺送货,hosts承诺会给pizza切块并served。建模如下:
以上例子中,我们可以看到promises链(then方法)。这个结构非常类似于下面的同步代码:
同时它很容易处理错误条件。让我们来看看错误传播实例:
这里从restaurant得到的rejection结果传播到对最终结果感兴趣的person上。这正是异步环境下的异常处理:抛出的异常将会冒泡给第一个catch块。
在Promise API,error回调函数作为catch块,作为标准的catch块-我们已经得到几个处理异常情况的选项。我们可以:
1、recover(从一个catch 块中返回值)
2、传播failure(重新抛出异常)
有Promise API,很容易在catch块中模拟recovery。例如假定如果需要的pizza不可用,我们的hosts试图预定另一个pizza
、
另一种情况是如果recovery不行,我们需要考虑的是重新抛出异常。在这种情况下,唯一的选项是触发另一个error,$q服务有专用的方法($q.reject):
$ q.reject方法在异步环境中等价于抛出异常。该方法返回一个被拒绝的新的promise。
$q
$q有两个额外有用的方法:$q.all和$q.when.
聚集promises
$q.all方法使开启多个异步任务成为可能,当所有任务都完成后notified。从几个异步actions有效聚集promises,返回一个组合的promise可以作为一个连接点。为了说明$q.all方法的实用性,我们考虑从多个restaurants订食品的例子。我们要等到两个订单都到货才能享用:
其中一个action失败了,promise也会被reject
封装值作为promises
有时同样的API需要与从同步和异步actions的结果中获取。在这种情况下,通常将所有结果都视为异步。$q.when方法可以使其将js对象包装成promise。
如下所示
$q的集成
$q不仅是promise的api实现,也是与AngularJS渲染机制紧密连接的。promises可以直接暴露在scope上,并且只要promise resolved就可以自动渲染,这使我们将promise当成model值。
如下,给定以下模版:
和控制器中的代码:
Hello,world将会在两秒后自动渲染,无需人工编程干预。
$http
现在,我们已经涉及到多个promises,可以去揭秘从$http 方法调用返回的响应对象。$http调用返回一个对象,这个对象上注册了success和error回调函数。实际上,返回的对象一个完全成熟的promise,这个promise有两个额外方便的方法(success和error)。作为任何一个promise,一个从$HTTP调用返回的还具有then方法,允许我们以如下形式重写callback注册代码:
由于$HTTP服务从其方法调用上返回promise,当与后端交互时可以很容易地聚集回调,链和连接请求,以及在异步环境下可以使用复杂的错误处理。
英文原文:Mastering Web Application Development with AngularJS