Koa框架API接口开发

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后台允许跨域的方法主要有两种:

  1. jsonp
  2. koa2-cors让后台允许跨域直接就可以在客户端使用ajax请求数据。
  • 下载koa2-corsnpm 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,查看效果

image.png

koa-jsonp

  • 下载koa-jsonpnpm 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,查看效果如下

image.png

  • 浏览器输入xxx/detail?callback=test,查看效果如下

image.png

GraphQL

GraphQL是一种新的API 的查询语言,它提供了一种更高效、强大和灵活API 查询。它弥补了RESTful API(字段冗余,扩展性差、无法聚合api、无法定义数据类型、网络请求次数多)等不足。

GraphQL的优点:

  • 吸收了RESTful API的特性
  • 所见即所得
  • 客户端可以自定义Api聚合
  • 代码即是文档
  • 参数类型强校验
  • 下载graphql koa-graphql koa-mountnpm 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 } } }

创作不易,若本文对你有帮助,欢迎打赏支持作者!

 分享给好友: