划重点
看这篇文章你可以学到的:
- HTTP 状态码
- 200 和 304 缓存
- memory cache 和 dist cache
- axios 对 response 的处理
- koajs 模拟实践 Http 状态码
- Postman mock server 使用
起因
前段时间,后端和测试提出,我们接口报错的错误提示总是 "网络错误,请稍后再试",没有对不同的错误给出不同的提示。之前也没有太在意。一般我们认为接口应该保持稳定可用,不应该存在报错等现象,但是也不能排除确实存在错误的情况,那么这个时候的友好且有用的错误提示可以提示用户体验,且对快速排查问题有起到帮助作用。
一般在项目中,我们会对接口请求做统一的拦截处理,比如在request的header中统一加token,再比如对response的错误做统一的页面提示等。 这次我们主要关注接口报错时的错误处理是怎么做的。
现在的处理
首先来看下我们用axios封装的对接口响应的拦截的统一处理的简化版(删除了登录过期等业务处理)
const api = axios.create({
timeout: process.env.VUE_APP_REQUEST_TIMEOUT || 30 * 1000,
headers: {}
});
// 响应拦截
api.interceptors.response.use((res = {}) => {
try {
const status = res.status;
if (/^2\d{2}/.test(status)) {
const data = res && res.data;
// 错误提示
if (data && +data.code !== 0 && data.message) {
Message({ // 页面提示
message: data.message,
type: 'warning'
});
return Promise.reject(data);
}
return Promise.resolve(data && data.data);
}
} catch (e) {
return Promise.reject(e);
}
},
// 非 2xx 状态响应
e => {
if (e && e.response && e.response.status >= 400) {
// 错误提示
if (e.response.data && e.response.data.message) {
Message({
message: e.response.data && e.response.data.message,
type: 'warning'
});
} else {
Message({
message: '网络错误,请稍后再试',
type: 'warning'
});
}
} else {
Message({
message: '网络错误,请稍后再试',
type: 'warning'
});
}
return Promise.reject(e);
}
);
看以上代码我们就能知道,为什么我们的提示都是 "网络错误,请稍后再试" 了。我们把 除了 "200" 以外的错误处理,几乎都处理成了 "网络错误,请稍后再试"。
axios源码对response的处理
首先我们要搞清楚axios对response是如何处理的,直接上相关的源码。
axios的处理是在最终都调用了settle函数,settle中的validateStatus方法决定了是resolve还是reject,所以,如果我们不改写validateStatus方法,那么2**的状态码都是resolve,其余都是reject。
另外除了接口请求返回了status之外,还存在status不存在的情况,比如接口超时,网络错误,接口取消,如下:
优化结果
我们可以看出,对于resolve的处理没有问题,对于reject的处理是有点问题, 对于3** 的状态,和其他状态码处理方式不一样,直接进入了else的逻辑,对于4**,5**,1** 的状态码也没有直接的提示出返回的状态码是什么。
那我们稍微改改,status存在时,抛出message或者当前的错误,status不存在则抛出当前的错误。如下:
// 非 2xx 状态响应
e => {
if (e && e.response && e.response.status ) {
Message({
message: e.response.data && e.response.data.message || e,
type: 'warning'
});
} else {
Message({
message: e,
type: 'warning'
});
}
return Promise.reject(e);
}
这个优化就这么简单的结束了!! 但是我们学习的步伐不能停!!
看到这里有人是不是要发出一个疑问:304状态码是被处理成错误了吗?
那我们来重温一下HTTP状态码吧,并且测试一下所有的http状态码,看看实际情况是怎么样的
HTTP 状态码模拟
HTTP 状态码
其实很多HTTP 状态码我们都很少碰到,最常见的状态码就是200,400,404,500,502,503等几个。
- 1xx
- 100 "continue"
- 101 "switching protocols"
- 102 "processing"
- 2xx
- 200 "ok"
- 201 "created"
- 202 "accepted"
- 203 "non-authoritative information"
- 204 "no content"
- 205 "reset content"
- 206 "partial content"
- 207 "multi-status"
- 208 "already reported"
- 226 "im used"
- 3xx
- 300 "multiple choices"
- 301 "moved permanently"
- 302 "found"
- 303 "see other"
- 304 "not modified"
- 305 "use proxy"
- 307 "temporary redirect"
- 308 "permanent redirect"
- 4xx
- 400 "bad request"
- 401 "unauthorized"
- 402 "payment required"
- 403 "forbidden"
- 404 "not found"
- 405 "method not allowed"
- 406 "not acceptable"
- 407 "proxy authentication required"
- 408 "request timeout"
- 409 "conflict"
- 410 "gone"
- 411 "length required"
- 412 "precondition failed"
- 413 "payload too large"
- 414 "uri too long"
- 415 "unsupported media type"
- 416 "range not satisfiable"
- 417 "expectation failed"
- 418 "I'm a teapot"
- 422 "unprocessable entity"
- 423 "locked"
- 424 "failed dependency"
- 426 "upgrade required"
- 428 "precondition required"
- 429 "too many requests"
- 431 "request header fields too large"
- 5xx
- 500 "internal server error"
- 501 "not implemented"
- 502 "bad gateway"
- 503 "service unavailable"
- 504 "gateway timeout"
- 505 "http version not supported"
- 506 "variant also negotiates"
- 507 "insufficient storage"
- 508 "loop detected"
- 510 "not extended"
- 511 "network authentication required"
那么用什么方法可以模拟不同的http状态码呢,毕竟真实的后端接口环境是不太可能出现所有状态码的,那么我们就需要自己模拟,那我们可以用什么来模拟呢?
Postman 模拟
首先想到的是用Postman,Postman 有个 mock server的功能,可以在本地启动一个mock服务器。步骤如下,非常的简单。
步骤一:
步骤二: 输入多个需要模拟的Request URL和Response code
步骤三: 输入服务器名称
步骤四: 生成Mock URL
我们把最后生成的Mock URL 在项目中使用就可以了。 但是呢,一共60来个状态码,配置起来感觉好麻烦啊,懒人想懒办法,那我们还是自己写个server吧。
如果只是测试一下某几个状态码,那么Postman不失为一种简便的方法。
koajs 模拟
我们用koa写一个最简单的server,一个接口接收一个code参数,并设置给status,如下
const koa = require('koa');
const koaRouter = require('koa-router' );
const compose = require('koa-compose');
const app = new koa();
const router = new koaRouter();
router.get('/status/test', async (ctx, next) => {
ctx.body = 'test'
ctx.status = +ctx.query.code
next();
});
const middlewares = compose([
router.routes()
])
app.use(middlewares);
app.listen(3000);
另外我们写一个简单的页面,可以输入状态码,作为入参传给测试接口。
测试效果
测试几个常见的状态码及错误
- 200
- 500
- 404
- 304
304真的报错了,那确实要处理一下吧??接着往下看
- 超时
- 断网
那么设置不在规范里的状态码是否可以呢?
- 800
- 8000
还这是可以的呢,只是koa对状态码做了校验,设置的status必须是数字,且需要在100-999之间,否则抛出错误,所以前台会返回500的状态码。
缓存:200 和 304
面试题经常会问到,浏览器的缓存,强缓存和协商缓存等,那我们就来试验一下。
强缓存
强缓存是通过Expires和Cache-Control两个字段来控制的。
Expires
Expires是http1.0规范,格式是GMT格式的时间字符串。
router.get('/status/test', async (ctx, next) => {
ctx.body = 'test'
ctx.set({
'Expires':new Date('2021-09-16 14:37:30'),
})
next();
});
第一次请求:200 ok
第一次请求:200 from disk cache
时间超过 Expires 时间后,再次请求 再次变成200 ok
Cache-Control
Cache-Control是http1.1规范,通过max-age来设置缓存时间
接口中设置上Cache-Control,设置成60秒。
router.get('/status/test', async (ctx, next) => {
ctx.body = 'test'
ctx.set({
'Cache-Control':'max-age=60'
})
next();
});
第一次请求:200 ok
第一次请求:200 from disk cache
60秒后再次请求 再次变成200
Expires和Cache-Control优先级
Cache-Control > Expires,当存在Cache-Control时,Expires会被忽略
以下是试验同时设置Expires和Cache-Control的情况:
- Cache-Control比Expires早
router.get('/status/test', async (ctx, next) => {
ctx.body = 'test'
//ctx.status = +ctx.query.code,
ctx.set({
'Cache-Control':'max-age=20', // 20秒后
'Expires':new Date('2021-09-16 20:37:30'), // 晚上8点,当前是下午3点
})
next();
});
20秒后缓存已经失效
- Cache-Control比Expires晚
router.get('/status/test', async (ctx, next) => {
ctx.body = 'test'
//ctx.status = +ctx.query.code,
ctx.set({
'Cache-Control':'max-age=300', // 300秒 5分钟后
'Expires':new Date('2021-09-16 15:02:00'), // 当前下午3点,2分钟后
})
next();
});
3分钟后缓存还存在,5分钟后缓存失效
from memory cache 和 from dist cache
缓存也分为memory cache 和dist cache。 memeroy cache 比 dist cache 读取速度快,但时效短,进程结束后 memory cache就释放了。
浏览器首先读取的是memory cache,再次才是dist cache。并不是所有需要缓存的内容都会缓存在memory cache中,一般图片,js等资源文件会在同时在memory cache和dist cache中缓存,而接口类的缓存都存在dist cache中,css 很特殊,有时会存在memory cache中,有时在dist cache。(这个可能会涉及到chrome底层的缓存策略了,没有研究)
我们来看下下面的例子,我们用koa-static来处理静态资源,并且设置Cache-Control 60秒,页面中引入img、css和js来试一下
const static = require('koa-static');
const middlewares = compose([
router.routes(),
static(__dirname + '/static',{
setHeaders:(res,path,stats)=>{
res.setHeader('Cache-Control','max-age=60')
}
})
])
如果把chrome关闭,然后再次打开访问页面,此时memory cache已经释放,那么资源就会从dist cache中获取,如下:
未设置Expires和Cache-Control的缓存时间
如果我们未设置Expires和Cache-Control,那么强缓存还是有可能存在的。
缓存时间是 Date的值减去Last-Modified的值除以10。
协商缓存
当强缓存失效时,请求会发送到后端,由后端进行对比,判断新鲜度,如果内容不变则返回304,读取本地缓存,如果内容变化,则返回200
Etag 和 if-none-matched
Etag一般是每个文件的hash,与Etag对应的是if-none-matched,当请求中带有Etag时,下一次请求会在request中带上if-none-matched,其值就是上一次请求的Etag,并发送给服务端,服务端根据目前的Etag和if-none-matched进行对比,如果相同则表示文件没有改动。
Last-Modified 和 if-modified-since
Last-Modify是文件的最后修改时间,与Last-Modify对应的是if-modified-since,当请求中带有Last-Modify时,下一次请求会带上if-modified-since,时间则为上一次Last-Modify的值,根据Last-Modified 和 if-modified-since的对比,来表示文件是否改动
实例
我们继续在原来的接口上改一下,设置上Etag和Last-Modified,如下
router.get('/status/test', async (ctx, next) => {
ctx.body = 'test'
ctx.set({
'Etag': '1234',
'Last-Modified': new Date('2021-09-17')
})
if(ctx.fresh){
ctx.status = 304
}
});
服务端需要检测文件是否改动等来设置304的状态码。这里我们使用的是ctx.fresh来判断,koa使用的是fresh来处理是否需要更新,可以看下源码,如下
看下效果:
这个时候我们发现304并没有报错,在页面获取结果时获取到的是200,为什么呢?
因为304 会读取本地缓存,本地缓存的数据是200的,所以最后结果还是resolve。
我们测试一下,返回里加上random数
router.get('/status/test', async (ctx, next) => {
const num = Math.random();
ctx.body = num;
ctx.set({
'Etag': '12341234',
})
if(ctx.fresh){
ctx.status=304
}
console.log(num,ctx.response)
next();
});
-
第一次请求 服务端返回200,body中带生成的random数
浏览器端的显示与服务器端保持一致
-
第二次请求 服务器端返回304,body为空
浏览器端接口304,返回值却是有的,打印的值是前一次的结果
因此实际情况中304 的状态码不需要单独处理
缓存总结
直接看图
写在最后
最后的一些感悟:死记硬背不可取 实践检验是真理。共勉!
以上内容如有问题,欢迎沟通指正!