开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第1天,点击查看活动详情
好久没发文了,刚好最近做了一个项目用到koa,当处理请求响应的时候,我一直对code,message,status等这些东西混淆不清,今天就来好好捋一下。本文基于Axios模拟网络请求,koa作为后端服务器。
一、认识响应结构response
{
// `data` 由服务器提供的响应
data: {},
// `status` 来自服务器响应的 HTTP 状态码
status: 200,
// `statusText` 来自服务器响应的 HTTP 状态信息
statusText: 'OK',
// `headers` 是服务器响应头
// 所有的 header 名称都是小写,而且可以使用方括号语法访问
// 例如: `response.headers['content-type']`
headers: {},
// `config` 是 `axios` 请求的配置信息
config: {},
// `request` 是生成此响应的请求
// 在node.js中它是最后一个ClientRequest实例 (in redirects),
// 在浏览器中则是 XMLHttpRequest 实例
request: {}
}
通过例子测试来学习来的最快:
服务端代码:
// userRouter.post('/', create) 接口配置
class UserController {
async create(ctx, next) {
// ctx.status=404
ctx.status=201
ctx.body = 'ctx.body'
ctx.message = 'ctx.message'
}
}
module.exports = new UserController()
首先一个请求由客户端发送到服务器,服务器会根据不同情况进行相应,以Axios发送网络请求为例:
axios.post("http://localhost:6666/user",{
username:"coder li",
password:"123456"
}).then(( res ) => {
console.log("请求成功")
console.log("res.data:",res.data)
console.log("res.status:",res.status)
console.log("res.statusText:",res.statusText)
}).catch(( err ) => {
console.log("请求失败",err.response.status,err.response.statusText,err.response.data)
console.log("err.response.status:",err.response.status)
console.log("err.response.statusText:",err.response.statusText)
console.log("err.response.data:",err.response.data)
})
ctx.status代表了http状态码,这个状态码有什么作用呢?我们后面再来分析。
- ctx.status=201时走的是then,此时返回结果对应关系为:
ctx.body----res.data
ctx.status----res.status
ctx.message----res.statusText
//响应头信息
headers: AxiosHeaders {
'content-type': 'text/plain; charset=utf-8',
'content-length': '8',
date: 'Tue, 22 Nov 2022 10:58:38 GMT',
connection: 'close',
[Symbol(defaults)]: null
},
//config是axios请求配置信息,包含了
config:{
//包含了headers(同上)
headers: AxiosHeaders{...},
method: 'post',
url: 'http://localhost:6666/user,
// data中是post请求中带的数据
data: '{"username":"coder li","password":"123456"}'
},
// request是生成此响应的请求
request:{}
- ctx.status=404时走的是catch,此时返回结果对应关系为:
err.response.data----ctx.body
err.response.status----ctx.status
err.response.statusText----ctx.message
如果服务端没有设置ctx.message,则会使用默认的message
二、AxiosError
我们看一下err:它是一个AxiosError对象,里面的属性有:
- code
- config
- request
- response
请求失败 404 ctx.message ctx.body
AxiosError: Request failed with status code 404
at settle (/Users/lijing/coder/node/koa-test-learn/node_modules/axios/dist/node/axios.cjs:1268:12)
at IncomingMessage.handleStreamEnd (/Users/lijing/coder/node/koa-test-learn/node_modules/axios/dist/node/axios.cjs:2446:11)
at IncomingMessage.emit (node:events:525:35)
at endReadableNT (node:internal/streams/readable:1359:12)
at process.processTicksAndRejections (node:internal/process/task_queues:82:21) {
code: 'ERR_BAD_REQUEST',
config: {
...,
headers: AxiosHeaders {
'Content-Type': 'application/json',
'User-Agent': 'axios/1.1.3',
'Content-Length': '43',
'Accept-Encoding': 'gzip, deflate, br',
[Symbol(defaults)]: [Object]
},
method: 'post',
url: 'http://localhost:6666/user',
data: '{"username":"coder li","password":"123456"}'
},
request: <ref *1> ClientRequest {
...
},
response: {
status: 404,
statusText: 'ctx.message',
headers: AxiosHeaders {
'content-type': 'text/plain; charset=utf-8',
'content-length': '8',
date: 'Tue, 22 Nov 2022 11:15:42 GMT',
connection: 'close',
[Symbol(defaults)]: null
},
config: {
...,
headers: [AxiosHeaders],
method: 'post',
url: 'http://localhost:6666/user',
data: '{"username":"coder li","password":"123456"}'
},
request: <ref *1> ClientRequest {
method: 'POST',
path: '/user',
host: 'localhost',
protocol: 'http:',
},
data: 'ctx.body'
}
}
我初步观察了一下,感觉err.response这个对象和then返回中的res对象是一样的,深入一下AxiosError(引用axios官方例子):
axios.get('/user/12345')
.catch(function (error) {
if (error.response) {
// 1.请求成功发出且服务器也响应了状态码,但状态代码超出了 2xx 的范围
console.log(error.response.data);
console.log(error.response.status);
console.log(error.response.headers);
} else if (error.request) {
// 2.请求已经成功发起,但没有收到响应
// `error.request` 在浏览器中是 XMLHttpRequest 的实例,
// 而在node.js中是 http.ClientRequest 的实例
console.log(error.request);
} else {
// 3.发送请求时出了点问题
console.log('Error', error.message);
}
console.log(error.config);
});
三种错误发生的时机
第一种情况
如果error.response有值,即非undefined,说明请求成功了且服务器也响应了状态码,但是状态码超出了2XX的范围!读到这里,我知道了http状态码的作用,就是用来标识一个服务的响应到底走then还是走catch,这个响应的response响应体里面的内容是由服务器提供的。这种情况下,不管走的then还是catch,其实都是服务器响应成功了!
第二种情况:
error.response为undefined,说明了服务器没有响应,但是如果request有值,说明请求是成功发起了,可以拿到请求的配置信息,有可能是服务器宕机了。
第三种情况
error.request和error.response都没有值,说明,请求都没有发起成功.
获取error更多信息的方法toJSON
可以使用error.toJSON()方法获取更多关于HTTP错误的信息
{
message: 'Request failed with status code 404',
name: 'AxiosError',
description: undefined,
number: undefined,
fileName: undefined,
lineNumber: undefined,
columnNumber: undefined,
stack: 'AxiosError: Request failed with status code 404\n' +
' at settle (/Users/lijing/coder/node/koa-test-learn/node_modules/axios/dist/node/axios.cjs:1268:12)\n' +
' at IncomingMessage.handleStreamEnd (/Users/lijing/coder/node/koa-test-learn/node_modules/axios/dist/node/axios.cjs:2446:11)\n' +
' at IncomingMessage.emit (node:events:525:35)\n' +
' at endReadableNT (node:internal/streams/readable:1359:12)\n' +
' at process.processTicksAndRejections (node:internal/process/task_queues:82:21)',
config: {
transitional: {
silentJSONParsing: true,
forcedJSONParsing: true,
clarifyTimeoutError: false
},
adapter: [Function: httpAdapter],
transformRequest: [ [Function: transformRequest] ],
transformResponse: [ [Function: transformResponse] ],
timeout: 0,
xsrfCookieName: 'XSRF-TOKEN',
xsrfHeaderName: 'X-XSRF-TOKEN',
maxContentLength: -1,
maxBodyLength: -1,
env: { FormData: [Function], Blob: [class Blob] },
validateStatus: [Function: validateStatus],
headers: AxiosHeaders {
'Content-Type': 'application/json',
'User-Agent': 'axios/1.1.3',
'Content-Length': '43',
'Accept-Encoding': 'gzip, deflate, br',
[Symbol(defaults)]: [Object]
},
method: 'post',
url: 'http://localhost:6666/user',
data: '{"username":"coder li","password":"123456"}'
},
code: 'ERR_BAD_REQUEST',
status: 404
}
可以看出,这里面有一个code信息,'ERR_BAD_REQUEST',暂时不知道有什么用,从来没用过也懒得去查了。
三、业务上的code状态码和http状态码不是一个东西
一般服务端返回给前端的数据都会封装为如下的形式:
{
code: 200,
message: "这是一串提示信息,可能在前端进行显示",
data: {
//这里才是真正的数据
}
}
注意这里的code状态码是业务上的状态码,内部自己约定的。有些地方会把code命名为status,message也会简写为msg,我觉得都可以,没有什么唯一的标准,不过这里再用status的话,感觉可能有些同志会把它和response对象中的status状态码(http状态码)相混淆,个人建议使用code。一定要注意这里返回的整个对象实际是ctx.body的内容,通过response对象的data属性可以拿到。
四、koa中的错误处理封装
统一使用错误事件监听器来处理,app上监听(on),有错误也在app上派发错误事件(emit)
// 普通侦听
app.on('error', err => {
log.error('server error', err)
});
// 如果错误发生在 请求/响应 环节,并且其不能够响应客户端时,
// Contenxt 实例也会被传递到 error 事件监听器的回调函数里。
app.on('error', (err, ctx) => {
log.error('server error', err, ctx)
});
封装步骤
1.先封装一下返回数据的统一格式
这里采用的思路是通过中间件来实现,具体思路是在ctx对象身上增加success和failure两个方法,通过调用这两个方法来为ctx中的status、body等赋值,从而返回给客户端,具体做法如下:
class RespBean {
async respBean(ctx, next) {
//默认http状态码为200,前端请求走的是.then( res => {} )
ctx.success = function (code = 200, msg = "请求成功", data, httpStatus = 200) {
ctx.status = httpStatus
ctx.type = "json"
ctx.body = {
code,
msg,
data
}
}
// 默认http状态码是400,前端请求走的是.catch( err => {})
ctx.failure = function (code = 400, msg = "请求失败", httpStatus=400) {
ctx.status = httpStatus
ctx.type = "json"
ctx.body = {
code,
msg
}
}
await next()
}
}
module.exports = new RespBean()
因为之前java写多了,统一的返回数据格式用的就是respBean命名,这里也这样命名了,注意:这里的code和httpStatus的区别,一个是你的业务逻辑层面的code码;另一个是http的状态码,用来决定走then还是catch的。
接下来在app/index.js文件(该文件中创建的app)中插入该中间件,放到其他中间件前面保障其他中间件能够正常调用这两个方法实现返回统一数据格式。
const {respBean} = require('../common/respBean')
//统一数据返回格式,插入一个中间件,为ctx增加success和failure方法
app.use(respBean)
这样做完之后,在其它中间件中只要调用ctx下的success或failure方法,即可实现返回给定的数据格式,你也可以在其中自定义自己的一些返回数据格式,可定制性较强。
2.在app/index.js主入口文件中放入监听,并编写错误回调函数
app.on('error', errorHandler)
errorHandler是需要我们接下来做的一个回调函数,根据前面的知识,我们知道它的参数有error和ctx两个,我们需要在该回调函数中做的事情只有一个,就是根据error信息采用上面封装好的统一的格式给客户端发送相应的数据。
因此我们需要做的有两步,一是如何判断错误信息?二是返回何种数据?
关于错误信息的判断,我们采用Error构造错误信息对象,通过它其中的message来做判断
message都是一些字符串,因为怕多处使用会写错,我们使用一个文件来专门记录这些常量字符串,这也是看了很多coderwhy大佬的视频学到的东西。
我们先搞一个文件来记录常量如下:
const NAME_OR_PASSWORD_IS_REQUIRED = "name_or_password_is_required"
module.exports = {
NAME_OR_PASSWORD_IS_REQUIRED
}
返回数据,我们采用上面为ctx封装好的方法
const errorTypes = require('./error-types')
const errorHandler = (error, ctx) => {
let code, message, httpStatus
switch (error.message) {
case errorTypes.NAME_OR_PASSWORD_IS_REQUIRED:
code = 1001
httpStatus = 400
message = "用户名或密码不能为空."
break
default:
code = 404
httpStatus = 404
message = "NOT FOUND."
}
ctx.failure(code, message, httpStatus)
}
module.exports = errorHandler
可以看到,判断完成后,直接使用ctx.failure()方法,就可以很轻松的将错误信息返回给客户端
3.在其它中间件中运用
const errorTypes = require('../app/error-handler/error-types')
class UserController {
async create(ctx, next) {
// 假装这里发生了错误,用户名或密码为空
const error = new Error(errorTypes.NAME_OR_PASSWORD_IS_REQUIRED)
// 提前返回
return ctx.app.emit("error", error, ctx)
}
}
module.exports = new UserController()
我们模拟一个客户端发送请求
const axios = require('axios')
axios.post("http://localhost:6666/user", {
username: "coder li",
password: "123456"
}).then((res) => {
}).catch((err) => {
console.log("请求失败")
console.log("http状态码:", err.response.status)
console.log("statusText使用默认值:", err.response.statusText)
console.log("请求返回数据(msg可以用于客户端的展示):", err.response.data)
})
得到的输出是:
请求失败
http状态码: 400
statusText使用默认值: Bad Request
请求返回数据(msg可以用于客户端的展示): { code: 1001, msg: '用户名或密码不能为空.' }
可以看到,错误处理已经
关于http状态码我们可以查表(见附表),关于code编码,那是我们自己约定的一些编码。
至此,关于koa的请求响应错误处理就讲完了。
五、附表-http状态码
100 "continue"
101 "switching protocols"
102 "processing"
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"
300 "multiple choices"
301 "moved permanently"
302 "found"
303 "see other"
304 "not modified"
305 "use proxy"
307 "temporary redirect"
308 "permanent redirect"
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"
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"
如果有帮助到你,欢迎大家点赞、收藏、关注,作为一个默默无闻的编程业余爱好者,写的不足的地方还请大家批评指点。