一次axios错误提示优化引发的学习-HTTP状态码及缓存

2,132 阅读10分钟

划重点

看这篇文章你可以学到的:

  • 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 的状态码不需要单独处理

缓存总结

直接看图

写在最后

最后的一些感悟:死记硬背不可取 实践检验是真理。共勉!

以上内容如有问题,欢迎沟通指正!