之前做Java的时候,一旦遇到要登录拦截的情形,总会用到Filter。请求在到达controller之前,先经过filter,进行登录验证,然后再决定放行。前端开发也会有类似的场景,Http请求中通用的附加处理就是一种,而Angular原生的interceptor已经提供了支持。我在工作中也有一些用到它的地方,于是上来做个笔记。

HttpProvider和它的Interceptor

Angular的HttpProvider原生支持拦截器(interceptor),可以在请求发出和被接受之前做相应处理。在官网的API文档介绍中,有如下示例

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
// register the interceptor as a service
$provide.factory('myHttpInterceptor', function($q, dependency1, dependency2) {
return {
// optional method
'request': function(config) {
// do something on success
return config;
},

// optional method
'requestError': function(rejection) {
// do something on error
if (canRecover(rejection)) {
return responseOrNewPromise
}
return $q.reject(rejection);
},

// optional method
'response': function(response) {
// do something on success
return response;
},

// optional method
'responseError': function(rejection) {
// do something on error
if (canRecover(rejection)) {
return responseOrNewPromise
}
return $q.reject(rejection);
}
};
});

$httpProvider.interceptors.push('myHttpInterceptor');

实际中我主要用到过 requestresponseresponseError

Request使用

API请求路径的通用修正

也不知道为什么,从接触到interceptor的那天起,第一个想到的场景就是ajax请求中的URL。在最开始写ajax的时候,借助的是jQuery, 请求的URL直接放到调用参数中去,觉得这一切,怎么说呢,还挺好的哈?但随着前后端的分离,后端工程师在开发或后面维护API的过程中,或者由于其他的原因,URL会发生变更。拿我的经历来说,后端是一个独立的系统,同时服务于APP和web,后端的API命名是和业务直接关联的,在命名上,只会停留在 /user/login, 而我们在调用的时候,这样是不太直观的,所以会变成 /api/user/login,将来也有可能变成 /somethingelse/user/login

而这样做还有一个好处,就是方便Nginx做跳转。通过proxy_pass, 将所有 /api/ 的请求代理到真实的后台API。

后台API的URL甚至可能有端口,但这一切都被Nginx处理掉了,对于前后段分离来说,这样既可以避免跨域请求,又可以让请求显得更加规范

封装共有请求参数

这个是比较常见的,在和后台请求过程中除了每个API单独的参数,还会附带一些基本参数,比如认证的token(对于基于Token而不是Session的后端)和用于统计的其他信息。如果是每个单独加相信大家都不会愿意,于是我们可能写一个共有的方法,在里面附件共有参数(也可以更改API的请求路径),这些不借助Angular也可以做到,只是我们要记得每次都调用这个公用方法。

设置请求超时时间

为ajax请求设置通用的超时时间,而且还可以为上传请求单独配置更长的超时时间

这些都可以放到 request 中来,如下代码是只是一个例子:

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
angular.module('app.common')
.factory('RequestInterceptor', RequestInterceptor)

RequestInterceptor.$inject = ['$rootScope', 'Constants']

function RequestInterceptor($rootScope, Constants) {

//过滤掉所有类似 /static/**/** 的路径,只对API请求作过滤
var _notStaticUrlReg = /^\/(?!static\/).*$/i
var interceptor = {
'request': function(config) {
if (config.url.match(_notStaticUrlReg)) {
var data = config.data
if (!data) {
data = config.data = {}
}

//重新组装API,将所有的请求加入API的项目基本路径,如果已经包含路径则不再添加
//from '/user/login' TO '/api/url/login'
if (config.url && config.url.indexOf(Constants.ApiBasePath) < 0) {
config.url = Constants.ApiBasePath + config.url
}

// 注入认证信息
data.authentication = {}

// 设置请求超时时间
config.headers['cache-control'] = 'no-cache'
config.timeout = 30000

//如果是上传api,超时时间加长
if (config.url.indexOf('/file/upload') > -1) {
config.timeout = 100000
}
}

return config
}
}

return interceptor
}

Response的使用

通用的请求错误判断

如果在请求过程中认证信息过期(这个其实是可以避免的,在发起ajax请求前的页面应该做hasLogin的验证,目前没有这个验证才加上来),会返回403的错误码,或者一些其他代表认证信息过期的错误码,在response中通过这个来进行判断。

在上面的代码基础上,增加 response: function () 模块

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
var interceptor = {
'request': function (config) {}, // 此处省略
'response': function (_response) { // **为和不就叫response?此处有坑**

var status = _response.status
if (status === 403 || status === 401) {
var _path = $location.path()
$rootScope.$emit('root.needsLogin')
} else {
var url = _response.config.url
var data = _response.data

// 依然只对api做处理,静态资源的URL不做处理
if (url && url.match(_notStaticUrlReg)) {

// 如果请求过程中发现信息过期,广播该事件
if (data && data.code === 'auth_failed' ) {
$rootScope.$emit('root.needsLogin')
}

}
}

return _response
}
}

_response 的坑
前面提到过,在response的参数命名中为何不直接取名response 就好呢?这是在使用 ng-file-upload-shim的时候遇到的。 ng-file-upload-shim 在IE8、IE9下会通过 FileAPI 调用Flash来上传,可喜的是,这个请求仍然会被Angular的http处理,故而interceptor也是可以用在这里的。但是我遇到了一个奇怪的问题,在IE上面,FileAPI的请求,其response(拦截器中的参数)为undefined,然而实际上后台是有返回的,而且FileAPI其自己也可以获取到。但是断点进去,response就是undefined。但我看到debug面板老是出现”_response”的变量,以为是浏览器自己的重命名。抱着试一试的态度,我把参数改成_response,有值了! 难道在这时候,response变量和浏览器发生了冲突,被浏览器直接写为undefined?

ResponseError的使用

自动重新请求

最开始需要用到这个的时候,是因为一个莫名奇妙的请求被丢弃错误,一个普通的ajax请求,在某一台手机上,经常莫名奇妙的发生请求被丢弃故而报错,其请求完全没有到达超时的限制(请求超时后会被丢弃),尝试过不少但都没能找到根本原因,最后想到了一个暴力的方法,自动重新请求!

通过判断返回的状态码是否等于0 来判断该请求是否被丢弃,重试指定的次数。这个次数放在哪呢?我是放在response的config对象中,方便每次都能得到上一次的状态,而且只和这个response有关。

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
// 还是接上面的构造代码,只是加入 `responseError:function() {}`
var interceptor = {
'request': function (config) {}, // 此处省略
'response': function (_response) {}, // 此处省略
'responseError': function (_response) {
if (_response.status === 0) {
return abortingHandler(_response)
}

//对aborted的request 进行重新请求
function abortingHandler(_response) {

//如果是静态资源,则不作处理
if (!_response.config.url.match(_notStaticUrlReg)) {
return $q.reject(_response)
}

var $http = $injector.get('$http')
var _config = _response.config

if (typeof _config.triedTimes !== 'undefined') {
_config.triedTimes++
} else {
_config.triedTimes = 1
}

if (_config.triedTimes < 3) {
return $http(_config)
} else {
return $q.reject(_response)
}
}

}
}

后话

在实践 angular interceptor的过程中,这篇博客帮助了我很多,推荐大家可以去看看, 传送门。以上只是我自己用的一些简单配置,如有局限或不对之处,还请见谅和指正。

最近也开始使用react,感觉虽然react自己说学习他只需要一个react,可是在实际项目,尤其是企业级的项目中,只靠react一个是远远不够的。这时候,redux,react-router放上来都是最基本的配置。然后在实际过程中,对于类似angular interceptor的内容,我用的就是一个共有方法封装=_=,对于Http Mock使用的是mockjax