AngularJS入门笔记-21-Ajax和Promise
ajax是现代web应用的基础,当需要让浏览器加载新内容而不刷新页面时就需要用到ajax,ng内置了ajax请求和异步promises。
- 发起ajax请求 使用$http服务
- 接收ajax请求响应 使用success,error或then方法在$http方法返回的对象上注册回调函数
- 处理非json数据,若是xml可使用jqLite处理
- 请求或预处理响应配置 使用转换函数
- 默认ajax配置 使用$httpProvider
- 拦截请求或响应 使用$httpProvider注册拦截器工厂函数
- 表示在未来某刻的活动 使用promises,由deferred对象和promises对象组成
- 获取deferred对象 调用由$q服务提供的defer方法
- 获取promises对象 使用由deferred对象定义的promise值
- 链接所有promises 使用then方法注册回调,then返回另一个promises,当回调函数被执行时该promises将被resolved
- 等待多个promises 使用$q.all方法创建promises,所有promises被resolved后整体才会resolved
ajax
一般通过ng内置的$http服务发起ajax请求,它是被异步执行的标准http请求(这也是$http服务名称的来源吧)。ajax异步刷新能向后台请求内容和数据,创建富客户端的重要手段。
使用jquery中的ajax与ng中的ajax差别就是,ng中将从服务器获取的数据应用到作用域中会自动更新所有绑定,而jquery中需要显示处理数据,并且操纵dom。
$http服务产生请求有两种方法,一种是使用如下的一些快捷方法:
- get(url, config) 执行GET请求
- post(url, data, config) 执行POST请求
- delete(url, config) 执行DELETE请求
- put(url, data, config) 执行PUT请求
- head(url, config) 执行HEAD请求
- jsonp(url, config) 执行跨域js请求,JSONP(JSON with Padding表示JSON和填充)能绕过浏览器对js代码被载入的限制的工作方式。
另一个是将$http服务对象当做函数传入配置对象。1
2
3
4
5
6
7
8angular.module("exampleApp", [])
.controller("defaultCtrl", function ($scope, $http) {
$scope.loadData = function () {
$http.get("productData.json").success(function (data) {
$scope.products = data;
});
}
});
GET和POST,应该如何选择
一般的经验是GET请求仅被用于所有只读的信息检索,而POST请求被用于改变应用状态的操作,所以GET是安全的,除了检索它没有任何副作用,而POST是不安全的(有可能会改变一些东西)。
同时GET请求是可寻址的,即所有信息都被包含在URL中,所以它适合放入书签,链接这些地址。GET请求不应该用于改变状态。若该GET请求是一个删除操作,因为url很有可能会被爬虫访问,而此时则会造成很大的破坏。
接收响应
发起ajax请求只是第一部分,当它响应时,我们需要接收响应,对返回的数据进行处理,由$http服务方法返回的承诺对象所定义的方法:
- success(fn) 当http请求完成时,调用fn
- error(fn) 当http请求无法完成时,调用fn
- then(fn1, fn2) 注册成功函数fn1和失败函数fn2
success和error方法接收简化后的响应,success接收服务器正常返回数据,error接收问题的描述字符串。
then方法提供了更详细的响应信息,如下是then传入处理函数的对象的属性 - data 请求返回数据
- status 返回http状态码
- headers 返回指定的http头部信息
- config 配置对象,该对象是在发起ajax前配置
1
2
3
4
5
6
7$http.get("productData.json").then(function (response) {
console.log("Status: " + response.status);
console.log("Type: " + response.headers("content-type"));
console.log("Length: " + response.headers("content-length"));
console.log(response.config);
$scope.products = response.data;
});
使用then方法时,ng自动处理json数据
配置ajax请求
由$http服务定义的方法都接收一个可选参数,即配置对象,绝大多数情况下使用默认配置即可,但也可显式设置。
- data 设置发送到服务器的数据,若设置了该值,则会被ng序列化为JSON格式
- headers 用于设置请求头部
- method 设置请求所使用的HTTP方法
- params 用于设置URL属性
- timeout 设置请求过期时间
- transformRequest 用于在请求发送到服务器前操作数据
- transformResponse 用于在请求发送到服务器后操作数据
- url 设置请求url
- withCredentials 为true时,表示底层浏览器请求对象上的withCredentials选项可用,包含在请求中验证cookie
- xsrfHeaderName,xsrfCookieName 这些属性用来防跨域请求伪造攻击
在ng中,内置的转换就是将传出的数据序列化为JSON,传入的JSON解析成js对象。
将配置对象上的transformRequest属性来转换响应,该函数负责返回更换后的数据,通常是发送给服务器的反序列化版本,比如若与服务器约定的格式是xml格式,那么将请求转换为xml,将响应转换为json。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48// 获取xml响应
$scope.loadData = function () {
var config = {
transformResponse: function (data, headers) {
if(headers("content-type") == "application/xml"
&& angular.isString(data)) {
products = [];
var productElems = angular.element(data.trim()).find("product");
for (var i = 0; i < productElems.length; i++) {
var product = productElems.eq(i);
products.push({
name: product.attr("name"),
category: product.attr("category"),
price: product.attr("price")
});
}
return products;
} else {
return data;
}
}
}
$http.get("productData.xml", config).success(function (data) {
$scope.products = data;
});
}
// 发送xml请求
$scope.sendData = function() {
var config = {
headers: {
"content-type": "application/xml"
},
transformRequest: function (data, headers) {
var rootElem = angular.element("<xml>");
for (var i = 0; i < data.length; i++) {
var prodElem = angular.element("<product>");
prodElem.attr("name", data[i].name);
prodElem.attr("category", data[i].category);
prodElem.attr("price", data[i].price);
rootElem.append(prodElem);
}
rootElem.children().wrap("<products>");
return rootElem.html();
}
}
$http.post("ajax.html", $scope.products, config);
}
配置默认ajax
通过$http服务提供器$httpProvider为ajax请求定义默认设置,该提供器定义了一些属性:
- defaults.headers.common 定义用于所有请求的默认头部
- defaults.headers.post 定义用于POST请求的默认头部
- defaults.headers.put 定义用于PUT请求的默认头部
- defaults.transformRequest 定义用于所有请求的转换函数的数组
- defaults.transformResponse 定义用于所有响应的转换函数的数组
- interceptors 拦截器工厂函数数组,拦截器是转换函数的复杂形式
- withCredentials 为所有请求设置withCredentials项,该属性常常用于发起需要验证的跨域请求
defaults.transformRequest和defaults.transformResponse属性是数组,必须用push方法添加。
$httpProvider.interceptors属性是一个数组,插入数组中的每一个元素都有一些属性,而拦截器是转换函数的最佳复杂替代品就是由于这些属性中的request和response。
- request 在产生请求并传入配置对象前调用拦截器函数
- requestError 在上一个请求拦截器抛出错误时调用的拦截器函数
- response 在响应并传入配置对象前调用拦截器函数
- responseError 在上一个响应拦截器抛出错误时调用的拦截器函数
1
2
3
4
5
6
7
8
9
10
11
12$httpProvider.interceptors.push(function () {
return {
request: function (config) {
config.url = "productData.json";
return config;
},
response: function (response) {
console.log("Data Count: " + response.data.length);
return response;
}
}
});
在上述代码中,在工厂方法产生的对象定义了request和response属性,request拦截器将配置对象的url修改为productData.json,然后返回config对象将其传给下一个拦截器。response拦截器也是这样,不管前面你如何操作/修改,最后都需要返回response对象给下一个拦截器。
使用promises
promises是一个对未来发生的事情的注册方式,如ajax请求。promises需要的对象有两个,promise对象用于接收响应的通知,deferred对象用于发送通知。
ng内置的$q服务来获取和管理promises,$q服务定义了一些方法:
- all(promises) 当指定数组中所有promises被解决或其中任一被拒绝时返回promises
- defer() 创建deferred对象
- reject(reason) 返回被拒绝的promises
- when(value) 在被解决的promises中封装一个值作为结果
获取和使用deferred对象
通过$q.defer方法获取deferred对象,该对象有如下一些方法和属性
- resolve(result) 带有指定值的延迟活动完成的信号
- reject(result) 延迟活动失败或由于特定原因将不被完成的信号
- notify(result) 提供来自延迟活动的临时结果
- promise 返回接收其他方法信号的promise对象
基本使用流程是获取deferred对象,然后使用活动结果作为信号调用resolve或reject方法,可选择性通过notify方法提供临时更新。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52<html ng-app="exampleApp">
<head>
<title>Promises</title>
<script src="angular.js"></script>
<link href="bootstrap.css" rel="stylesheet" />
<link href="bootstrap-theme.css" rel="stylesheet" />
<script>
angular.module("exampleApp", [])
.directive("promiseWorker", function($q) {
var deferred = $q.defer();
return {
link: function(scope, element, attrs) {
element.find("button").on("click", function (event) {
var buttonText = event.target.innerText;
if (buttonText == "Abort") {
deferred.reject("Aborted");
} else {
deferred.resolve(buttonText);
}
});
},
controller: function ($scope, $element, $attrs) {
this.promise = deferred.promise;
}
}
})
.directive("promiseObserver", function() {
return {
require: "^promiseWorker",
link: function (scope, element, attrs, ctrl) {
ctrl.promise.then(function (result) {
element.text(result);
}, function (reason) {
element.text("Fail (" + reason + ")");
});
}
}
})
.controller("defaultCtrl", function ($scope) {
});
</script>
</head>
<body ng-controller="defaultCtrl">
<div class="well" promise-worker>
<button class="btn btn-primary">Heads</button>
<button class="btn btn-primary">Tails</button>
<button class="btn btn-primary">Abort</button>
Outcome: <span promise-observer></span>
</div>
</body>
</html>
See the Pen promises by XmoyKing (@xmoyking) on CodePen.
promiseWorker指令依赖$q服务,在工厂函数中调用$q.defer方法获取新的deferred对象,然后在link函数和控制器中能够使用它。
link函数使用jqLite定位button元素并绑定click事件,在收到事件中,检查被单击元素的文本并调用deferred对象的resolve方法(为Heads和Tails按钮)或者reject方法(Abort按钮)两者之一,控制器定义promise属性来映射deferred对象和promise属性,通过控制器暴露该属性,可以允许其他指令获取与deferred对象有关的promise对象,并接收关于结果的信号。
defered对象用于标识用户单击按钮的结果,然后创建一个新的指令promiseObserver,监控和更新span元素的内容。
promiseObserver指令使用require定义属性从其他指令中取得控制器,并获取promise对象,该对象定义如下方法:
- then(success, error, notify) 注册被函数以响应deferred对象的resolve,reject和notify方法,该函数所传参数是用于调用deferred对象的方法
- catch(error) 仅注册错误处理函数
- finally(fn) 注册无论解决还是拒绝都会被调用的函数,
理解promises
感觉好像promises并没有什么出彩的地方,甚至还不如ajax理解起来那么容易,在上例中,promises表示一个活动的单一实例,一旦被解决或拒绝,promises无法再次使用,比如,单击Heads按钮,结果显示Heads,然后单击Tails按钮无效,因为promises已经被解决,无法再次使用,一旦设置,结果不变。
这意味这给观察者信号是“第一次用户选择Heads/Tails/Aborts”,如使用常规js的click事件,那其中每个仅能反映“用户单击按钮”,而不管用户单击的顺序和方式,即click事件可以重复,promises不能重复,一旦确定,即发出单一的活动结果作为信号。
链接多个then函数解决多个promises顺序使用1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35angular
.module("exampleApp", [])
.directive("promiseWorker", function($q) {
var deferred = $q.defer();
return {
link: function(scope, element, attrs) {
element.find("button").on("click", function(event) {
var buttonText = event.target.innerText;
if (buttonText == "Abort") {
deferred.reject("Aborted");
} else {
deferred.resolve(buttonText);
}
});
},
controller: function($scope, $element, $attrs) {
this.promise = deferred.promise;
}
};
})
.directive("promiseObserver", function() {
return {
require: "^promiseWorker",
link: function(scope, element, attrs, ctrl) {
ctrl.promise
.then(function(result) {
return "Success (" + result + ")";
})
.then(function(result) {
element.text(result);
});
}
};
})
.controller("defaultCtrl", function($scope) {});
See the Pen promises链 by XmoyKing (@xmoyking) on CodePen.
通过$q.all解决多个promises协同使用1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63<html ng-app="exampleApp">
<head>
<title>Promises</title>
<script src="angular.js"></script>
<link href="bootstrap.css" rel="stylesheet" />
<link href="bootstrap-theme.css" rel="stylesheet" />
<script>
angular.module("exampleApp", [])
.directive("promiseWorker", function ($q) {
var deferred = [$q.defer(), $q.defer()];
var promises = [deferred[0].promise, deferred[1].promise];
return {
link: function (scope, element, attrs) {
element.find("button").on("click", function (event) {
var buttonText = event.target.innerText;
var buttonGroup = event.target.getAttribute("data-group");
if (buttonText == "Abort") {
deferred[buttonGroup].reject("Aborted");
} else {
deferred[buttonGroup].resolve(buttonText);
}
});
},
controller: function ($scope, $element, $attrs) {
this.promise = $q.all(promises).then(function (results) {
return results.join();
});
}
}
})
.directive("promiseObserver", function () {
return {
require: "^promiseWorker",
link: function (scope, element, attrs, ctrl) {
ctrl.promise.then(function (result) {
element.text(result);
}, function (reason) {
element.text(reason);
});
}
}
})
.controller("defaultCtrl", function ($scope) {
});
</script>
</head>
<body ng-controller="defaultCtrl">
<div class="well" promise-worker>
<div class="btn-group">
<button class="btn btn-primary" data-group="0">Heads</button>
<button class="btn btn-primary" data-group="0">Tails</button>
<button class="btn btn-primary" data-group="0">Abort</button>
</div>
<div class="btn-group">
<button class="btn btn-primary" data-group="1">Yes</button>
<button class="btn btn-primary" data-group="1">No</button>
<button class="btn btn-primary" data-group="1">Abort</button>
</div>
Outcome: <span promise-observer></span>
</div>
</body>
</html>
See the Pen 多个promises协同 by XmoyKing (@xmoyking) on CodePen.