API封装
格式化输出一
- 在
middleware/
目录下新建jsonResponse.js
文件
function jsonResponse(option = {}) {
return async (ctx, next) => {
ctx.success = function(data) {
ctx.type = option.type || 'json'
ctx.body = {
code: option.successCode || 200,
msg: option.successMsg || 'success',
data: data
}
}
ctx.fail = function(code, msg) {
ctx.type = option.type || 'json'
ctx.body = {
code: code || option.failCode || 99,
msg: msg || option.successMsg || 'fail',
data: null
}
}
await next()
}
}
module.exports = jsonResponse
- 修改
app.js
文件,配置自定义的中间件
const jsonResponse = require('./middleware/jsonResponse')
app.use(jsonResponse())
- 在代码中使用
router.get('/test', async ctx => {
try {
// ...
const res = await asyncFun()
ctx.success(res)
} catch (e) {
ctx.fail(-1, e.message)
}
})
API异常处理一
可参考Koa框架日志和错误日志中的全局错误捕捉
部分
这里我们把它写到一个单独的中间件文件中,换种方式写(手动抛出异常以前用ctx.error
,这里改用throw
),并添加一些常用的异常
- 在
utils/
目录下添加http-exception.js
,定义常用的异常类
// 默认的异常,其他异常类都是从该类继承
class HttpException extends Error {
constructor(msg = '错误请求', code = 10000, status = 400) {
super()
this.code = code
this.msg = msg
this.status = status
}
}
class ParameterException extends HttpException {
constructor(msg, code) {
super()
this.status = 400
this.msg = msg || '参数错误'
this.code = code || 10000
}
}
class AuthFailed extends HttpException {
constructor(msg, code) {
super()
this.status = 401
this.msg = msg || '授权失败'
this.code = code || 10004
}
}
class NotFound extends HttpException {
constructor(msg, code) {
super()
this.status = 404
this.msg = msg || '未找到该资源'
this.code = code || 10005
}
}
class Forbidden extends HttpException {
constructor(msg, code) {
super()
this.status = 403
this.msg = msg || '禁止访问'
this.code = code || 10006
}
}
class Oversize extends HttpException {
constructor(msg, code) {
super()
this.status = 413
this.msg = msg || '上传文件过大'
this.code = code || 10007
}
}
class InternalServerError extends HttpException {
constructor(msg, code) {
super()
this.status = 500
this.msg = msg || '服务器出错'
this.code = code || 10008
}
}
module.exports = {
HttpException,
ParameterException,
AuthFailed,
NotFound,
Forbidden,
Oversize,
InternalServerError
}
- 在
middleware/
目录下新建exception.js
文件,定义中间件
const errors = require('../utils/http-exception')
// 全局异常监听
const catchError = () => {
return async(ctx, next) => {
try {
ctx.errs = errors
await next()
} catch(error) {
// 已知异常
const isHttpException = error instanceof errors.HttpException
if (isHttpException) {
ctx.fail(error.code, error.msg)
ctx.response.status = error.status
} else {
ctx.fail(9999, '未知错误')
ctx.response.status = 500
}
}
}
}
module.exports = catchError
- 修改
app.js
文件,配置中间件
const jsonResponse = require('./middleware/response')
const catchError = require('./middleware/exception')
app.use(jsonResponse())
app.use(catchError())
- 在代码中使用
router.get('/test2', async ctx => {
try {
dddcc()
} catch (e) {
// 手动捕捉异常
// {"code":9999,"msg":"未知错误","data":null}
// throw(new Error())
// 手动捕捉异常
// {"code":1111,"msg":"ddd","data":null}
// throw(new ctx.errs.AuthFailed())
// 手动捕捉异常
// {"code":10004,"msg":"授权失败","data":null}
throw(new ctx.errs.AuthFailed())
}
ctx.success({ name: 'ddd', age: 16 })
})
router.get('/test3', async ctx => {
// 这样子也会自动捕捉到异常;
// {"code":9999,"msg":"未知错误","data":null}
ddddww()
ctx.success({ name: 'ddd', age: 16 })
})
- 在
middleware/
目录下新建exception.js
文件。优化下:开发环境下不是HttpException
抛出异常
const errors = require('../utils/http-exception')
// 全局异常监听
const catchError = () => {
return async(ctx, next) => {
try {
ctx.errs = errors
await next()
} catch(error) {
// 已知异常
const isHttpException = error instanceof errors.HttpException
// 开发环境
const isDev = process.env.NODE_ENV === 'development'
// 在控制台显示未知异常信息:开发环境下,不是HttpException 抛出异常
if (isDev && !isHttpException) {
throw error
}
if (isHttpException) {
ctx.fail(error.code, error.msg)
ctx.response.status = error.status
} else {
ctx.fail(9999, '未知错误')
ctx.response.status = 500
}
}
}
}
module.exports = catchError
格式化输出二
上面是给ctx
添加了一个公共方法success()
和fail()
这里我们还是用原来的方式ctx.body = xxx
下格式化输出
- 在
middleware/
目录下新建responseFormatter.js
文件
const responseFormatter = async (ctx, next) => {
// 先去执行路由
await next()
// 直接用ctx.body返回的话,不手动设置会默认使用ctx.status=200
ctx.status = ctx.response.status
// 如果有返回值,将返回数据添加到data中
if (ctx.body) {
ctx.body = {
code: 200,
msg: 'success',
data: ctx.body
}
} else {
ctx.body = {
code: 200,
msg: 'success'
}
}
}
module.exports = responseFormatter
- 修改
app.js
文件,配置自定义的中间件
const responseFormatter = require('./middleware/responseFormatter')
// 添加格式化处理响应结果的中间件,在添加路由之前调用
app.use(responseFormatter)
- 在代码中使用
router.get('/test', async ctx => {
ctx.body = {
name: 'wmm66',
age: 18
}
})
优化:
上面的方法是所有的路由响应输出都会进行格式化输出
实际使用中我们要对URL
进行过滤,通过过滤的才对他进行格式化处理
- 修改
middleware/responseFormatter.js
文件
const responseFormatter = async ctx => {
// 直接用ctx.body返回的话,不手动设置会默认使用ctx.status=200
ctx.status = ctx.response.status
// 如果有返回值,将返回数据添加到data中
if (ctx.body) {
ctx.body = {
code: 200,
msg: 'success',
data: ctx.body
}
} else {
ctx.body = {
code: 200,
msg: 'success'
}
}
}
const urlFilter = pattern => {
return async (ctx, next) => {
const reg = new RegExp(pattern)
// 先去执行路由
await next()
// 通过正则的url进行格式化处理
if (reg.test(ctx.originalUrl)) {
responseFormatter(ctx)
}
}
}
module.exports = urlFilter
- 修改
app.js
文件
const responseFormatter = require('./middleware/responseFormatter')
// 添加格式化处理响应结果的中间件,在添加路由之前调用
// 仅对/api开头的url进行格式化处理
app.use(responseFormatter('^/api'))
API异常处理二
- 在
utils/
目录下添加apiError.js
文件,创建一个API
异常类
// 自定义API异常
class apiError extends Error {
constructor(errName, errCode, errMessage) {
super()
this.name = errName
this.code = errCode
this.message = errMessage
}
}
module.exports = apiError
- 在
utils/
目录下添加apiErrorNames.js
文件。封装API
异常信息,并可以通过API
错误名称获取异常信息
// API错误名称
const apiErrorNames = {}
apiErrorNames.UNKONW_ERROR = 'unknowError'
apiErrorNames.USER_NOT_EXIST = 'userNotExist'
// API错误名称对应的错误信息
const errMap = new Map()
errMap.set(apiErrorNames.UNKONW_ERROR, { code: -1, message: '未知错误' })
errMap.set(apiErrorNames.USER_NOT_EXIST, { code: 101, message: '用户不存在' })
// 根据错误名称获取错误信息
apiErrorNames.getErrorInfo = errName => {
let errInfo
if (errName) {
errInfo = errMap.get(errName)
}
// 如果没有对应的错误信息,默认'未知错误'
if(!errInfo) {
errName = apiErrorNames.UNKONW_ERROR
errInfo = errMap.get(errName)
}
return errInfo
}
module.exports = apiErrorNames
- 修改
utils/apiError.js
文件,引入apiErrorNames
const apiErrorNames = require('./apiErrorNames')
// 自定义API异常
class apiError extends Error {
constructor(errName) {
super()
const errInfo = apiErrorNames.getErrorInfo(errName)
this.name = errName
this.code = errInfo.code
this.message = errInfo.message
}
}
module.exports = apiError
- 修改
middleware/responseFormatter.js
文件,处理API
异常
const apiError = require('../utils/apiError')
// ...
const urlFilter = pattern => {
return async (ctx, next) => {
const reg = new RegExp(pattern)
try {
// 先去执行路由
await next()
// 通过正则的url进行格式化处理
if (reg.test(ctx.originalUrl)) {
responseFormatter(ctx)
}
} catch (error) {
// 如果异常类型是API异常并且通过正则验证的url,将错误信息添加到响应体中返回
if (error instanceof apiError && reg.test(ctx.originalUrl)) {
ctx.status = 200
ctx.body = {
code: error.code,
msg: error.message
}
}
// 继续抛,让外层中间件处理日志
throw error
}
}
}
module.exports = urlFilter
- 在代码中使用
const apiError = require('../utils/apiError')
const apiErrorNames = require('../utils/apiErrorNames')
router.get('/test', async ctx => {
// 如果 id != 1 抛出API异常
if (ctx.query.id != 1) {
throw new apiError(apiErrorNames.USER_NOT_EXIST)
}
ctx.body = {
name: 'wmm66',
age: 18
}
})
koa2-cors
koa2
后台允许跨域的方法主要有两种:
jsonp
koa2-cors
让后台允许跨域直接就可以在客户端使用ajax
请求数据。
-
下载
koa2-cors
:npm i koa2-cors --save
-
修改
app.js
文件,添加跨域配置
const cors = require('koa2-cors')
// app.use(cors())
app.use(cors({
origin: function (ctx) {
// if (ctx.url === '/test') {
// return "*"; // 允许来自所有域名请求
// }
return '*'
// 这样就能只允许 http://localhost:8080 这个域名的请求了
// return 'http://localhost:8080';
},
exposeHeaders: ['WWW-Authenticate', 'Server-Authorization'],
maxAge: 5,
credentials: true,
allowMethods: ['GET', 'POST', 'DELETE'],
allowHeaders: ['Content-Type', 'Authorization', 'Accept'],
}))
jsonp
jsonp
的机制是:
- 我们传给服务器一个
callback
参数,值是我们要调用的函数名字,- 然后服务器返回一个字符串,这个字符串不仅仅是需要返回的数据,而且这个数据要用这个函数名字包裹。
我们需要做如下事情:
- 解析请求所带的参数,并且读取
callback
参数的值。解决方法是,我们用ctx.request.query
获得请求所带的所有参数,然后读取出callback
参数:ctx.request.query.callback
。- 把数据转化为字符串,并用这个函数名包裹。这个很简单,字符串连接即可。
JSONP
跨域输出的数据是可执行的JavaScript
代码- ctx输出的类型应该是
'text/javascript'
- ctx输出的内容为可执行的返回数据
JavaScript
代码字符串- 需要有回调函数名
callbackName
,前端获取后会通过动态执行JavaScript
代码字符,获取里面的数据
原生实现
router.get('/detail', async ctx => {
let cb = ctx.query.callback || 'callback'
// ctx.type = 'text'
// ctx.body = cb + '(' + '"数据"' + ')'
let returnData = {
success: true,
data: {
text: 'this is a jsonp api',
time: new Date().getTime(),
}
}
// jsonp的script字符串
let jsonpStr = `;${cb}(${JSON.stringify(returnData)})`
// 用text/javascript,让请求支持跨域获取
ctx.type = 'text/javascript'
// 输出jsonp字符串
ctx.body = jsonpStr
})
- 浏览器输入
xxx/detail?callback=test
,查看效果
koa-jsonp
-
下载
koa-jsonp
:npm i koa-jsonp --save
-
修改
app.js
,使用koa-jsonp
const jsonp = require('koa-jsonp')
app.use(jsonp())
// 处理部分请求
router.get('/detail2', async ctx => {
let returnData = {
success: true,
data: {
text: 'this is a jsonp api',
time: new Date().getTime(),
}
}
// 直接输出JSON
ctx.body = returnData
})
- 浏览器输入
xxx/detail
,查看效果如下
- 浏览器输入
xxx/detail?callback=test
,查看效果如下
GraphQL
GraphQL
是一种新的API 的查询语言,它提供了一种更高效、强大和灵活API
查询。它弥补了RESTful API
(字段冗余,扩展性差、无法聚合api
、无法定义数据类型、网络请求次数多)等不足。
GraphQL
的优点:
- 吸收了
RESTful API
的特性- 所见即所得
- 客户端可以自定义
Api
聚合- 代码即是文档
- 参数类型强校验
-
下载
graphql koa-graphql koa-mount
:npm i graphql koa-graphql koa-mount --save
-
在根目录下新建
schema/
目录 -
在
schema/
目录下新建default.js
文件
const DB = require('../model/db.js')
const {
GraphQLObjectType,
GraphQLString,
GraphQLInt,
GraphQLSchema,
GraphQLList
} = require('graphql');
// 定义导航Schema类型
var GraphQLNav = new GraphQLObjectType({
name:'nav',
fields:{
title:{ type: GraphQLString },
url:{ type: GraphQLString },
sort:{ type: GraphQLInt },
status:{ type:GraphQLInt },
add_time:{ type: GraphQLString }
}
})
// 定义根
var Root = new GraphQLObjectType({
name: 'RootQueryType',
fields: {
navList: {
type: GraphQLList(GraphQLNav),
async resolve(parent, args) {
var navList = await DB.find('nav', {});
console.log(navList)
return navList;
}
}
}
})
// 增加数据
const MutationRoot = new GraphQLObjectType({
name: "Mutation",
fields: {
addNav: {
type: GraphQLNav,
args: {
title: { type: new GraphQLNonNull(GraphQLString) },
description:{ type: new GraphQLNonNull(GraphQLString) },
keywords:{ type: GraphQLString },
pid:{ type: GraphQLString },
add_time:{ type: GraphQLString },
status:{ type: GraphQLID }
},
async resolve(parent, args) {
var cateList = await DB.insert('nav',{
title: args.title,
description: args.description,
keywords: args.keywords,
pid: 0,
add_time: '',
status: 1
});
console.log(cateList.ops[0]);
return cateList.ops[0];
}
}
}
})
module.exports = new GraphQLSchema({
query: QueryRoot,
mutation: MutationRoot
})
- 修改
app.js
,配置中间件
const mount = require('koa-mount')
const graphqlHTTP = require('koa-graphql')
const GraphQLSchema = require('./schema/default.js')
app.use(mount('/graphql', graphqlHTTP({
schema: GraphQLSchema,
graphiql: true
})))
- 使用
Graphql
增加数据
mutation{
addNav(title: "测试导航", description: "描述") {
title
}
}
- 使用
Graphql
查询数据
{ articleList { title, cateList { title, description } } }
发表评论