koa 后台基础及使用

311 阅读5分钟

杂项

  • 任何看似复杂的设计,都是为了让系统更加简单化
  • 为什么有bin/www这个文件,因为按照分离原则app.js内应该只包含业务代码;所以需要有个文件单独包含服务相关的代码
  • 接口如果没有任何返回,默认响应404
  • 获取用户 ip 开启配置app.proxy = true
  • 使用process捕获全局错误
    • 监听unhandledRejection。当promisereject但没有对应的catch处理方法时触发
process.on('uncaughtException', (err) => {
  logger.error('## system ERROR uncaughtException ## %o', err);
});
process.on('unhandledRejection', (reason) => {
  logger.error('## system ERROR unhandledRejection ## %o', reason);
});
  • 可以在koa-body前通过修改ctx.headers内相关信息,以达到想要的类型/编码等;
    • 如:防止参数及类型错误,导致koaBody解析异常
  • koa-compose 用于组合中间间,使用时注意组件件顺序和依赖关系。
  • 配合 koa-respond 可简化响应方法 ctx.status / ctx.body
  • 日志文件 koa-log4

基本 api

  • app.env
  • app.proxy 设置为 true 支持 X-Forwarded-Host 才能获取入口 ip
  • app.listen(port?, hostname?, () => {}) 监听
  • app.callback() 返回的是个基本的function(req, res) {/** koa 自定义方法 **/}方法,包含所挂载的路由,的处理函数
    • 配合 nodejs 原生的http.createServer(app.callback()).listen(3000)理解
  • app.use([middleware]) 注册中间件
  • app.on('error', (err, ctx) => {}) 错误处理
  • app.keys= 用于配置cookie/session配置的 key
  • app.context 整个app的实例,可以在上面添加方法及属性
    • app.context._localData = {} -> ctx._localData
    • app 本身上也能挂载方法;在通过ctx.app.xxx使用

Context 上下文

  • ctx.req/.res 对应 node 中原生的 request/response 对象
  • ctx.app app 实例
  • ctx.accepts(types...) 检查请求的accept对给定的type(s)是否可以接受;接受返回匹配值,失败返回false
  • ctx.is(types...) 检查请求是否包含Content-Type头字;有返回匹配值,没有返回null,失败返回false
  • ctx.throw([status], [msg], [properties]) 用于抛出错误本质是new Error()
    • status 只能是 4xx/5xx 的
    • 调用后阻止整个程序运行,且再调用 ctx.body 无效
  • ctx.attachment([filename]) 请求头为附件类型,提示浏览器下载
  • ctx.cookies.get()/.set(key, value, opts) cookie相关方法
    • 设置汉字/Object时需要转化
{
  domain: 'localhost',  // 写cookie所在的域名
  path: '/',       // 写cookie所在的路径
  maxAge: 10 * 60 * 1000, // cookie有效时长
  expires: new Date('2017-02-15'),  // cookie失效时间
  httpOnly: false,  // 是否只用于http请求中获取
  overwrite: false  // 是否允许重写
}
  • ctx.get() 获取request.headers属性
  • ctx.set(key, value)/.set({}) 设置response.headers属性
  • ctx.redirect(url, [alt]) 重定向
    • 不能设置 body 体,所以传递数据只能通过 URLParams
  • ctx.type 设置响应的content-type
  • ctx.status 设置响应状态码
  • ctx.body= 设置响应体

参数获取

  • get 方式
    • /xxx?a=1&b=2使用ctx.query
    • 动态路径router.get(/abc/:a/:b):配置koa-router后使用使用ctx.params.a/b
  • post 方式:配置koa-body后使用ctx.request.body
  • file 获取 ctx.request.files 是个对象

中间件

全局错误

  • 基于 koa 中间件原理:要放在洋葱模型的最外层,这样才能拦截所有的错误
module.exports = function errorHandler(app) {
  // 响应处理
  app.use(async (ctx, next) => {
    try {
      await next();
    } catch (err) {
      let { status } = err;

      if (status === 422 && err.errno && err.errno === 2001) {
        // validate 校验错误
        ctx.type = 'json';
        ctx.body = ctx.resError(err);
      } else {
        ctx.type = 'json';
        ctx.body = ctx.resError(ctx.errorInfo.systemError);
      }

      ctx.app.emit('error', err, ctx);
    }
  });
	
  // 错误信息输出
  app.on('error', (err) => {
    let { status } = err;
    app.context.logger.error('## ERROR details ## %s', 'statusCode:' + status, err);
  });

  process.on('uncaughtException', (err) => {
    app.context.logger.error('## system ERROR uncaughtException ## %j', err);
  });
  process.on('unhandledRejection', (reason) => {
    app.context.logger.error('## system ERROR unhandledRejection ## %j', reason);
  });
};

接口权限

  • 中间就需要是个可执行函数,同时 api 权限需要传递参数(角色/编码)所以使用闭包形式。外层获取 params 信息,内层获取 ctx/next 最后通过需要执行 next() 放行
/**
 * @function 用户应用权限接口判断
 * @param {string} resourceCode - 资源编码
 * @param {string} roleCode - 角色编码
 * @other 默认不传时什么都不执行,两个都穿 resourceCode 优先级高于 roleCode
 */
function apiPermissions(resourceCode, roleCode) {
  return function handle(ctx, next) {
    const { resoruceTree, roleInfo } = ctx.session.userInfo;

    if (resourceCode) {
      if (!resoruceTree[resourceCode]) {
        ctx.error(ctx.errInfo.userNoAuthError);
        return;
      }
    } else if (roleCode) {
      if (roleInfo.code !== roleCode) {
        ctx.error(ctx.errInfo.userNoAuthError);
        return;
      }
    }

    return next();
  };
}

koa-router

  • 路由的模块化规则:.../模块/方法
    • 可通过router.js管理路由,并导出
  • router.get([name], [paths], middlewares)|put|post|del|all 支持多路径和多个中间件
  • router.use([paths], middleware) 用于该路由表内添加中间件或拆分路由
  • router.prefix(string) 给该组路由添加前缀
  • router.redirect(source, destination, [code]) 重定向;支持内/外部重定向
  • router.allowedMethods([opts]) 添加默认的 header 信息。所有路由中间件执行完后,如果结果的 !ctx.status || ctx.status === 404 则添加默认的 header 信息。
  • 动态路由:/create/:type 通过 ctx.params.type 获取。相同请求方式下,注意注册的普通路由和动态路由的顺序。执行时是按照注册顺序进行触发的
  • 可以通过创建新的类去继承 koa-router 添加自己的方法,方便后续的引用**(重要)**
  • 注意事项
    • app.use(router.routes()).use(router._allowedMethods_()) 注册的路由之间是相互独立

params validate

  • 使用parameter插件配合
  • itemRule
    • required
    • type: integer/number/date/datatime/boolean/string/email/password/url/enum/object/array
    • convertType: 强制转为输入的数据格式,支持 int/number/string/boolean
    • default: 设置默认值
  • addRule('ruleName', function(rule, value, params) { return true/'error message' })
// <validator.js 校验中间件>
const Parameter = require('parameter')
const _p = new Parameter()

function genValidator(rule) {
  // 定义中间件函数
  async function validator(ctx, next) {
    const data = ctx.request.body
    const error = _p.validate(rule, data)
    if (error) {
      // 验证失败
      ctx.status = 422
      ctx.body = {
        errno: -100,
        errors: error, // 正式环境不返回
        msg: '参数错误'
      }
      return
    }
    // 验证成功,继续
    await next()
  }
  // 返回中间件
  return validator
}

// <user.js 路由>
const genValidator = require('../../middlewares/validator')

router.post('/register', genValidator({/rule/}), async (ctx, next) => {})

单元测试

  • jest配合supertest模拟请求
// server.js 提供用于测试的请求实例,包含 app.js 下的所有路由
const app = require('../src/app')
const request = require('supertest')

module.exports = request(app.callback())

// xxx.test.js 测试用例中
const server = require('../server')
server.post('/xxx').send({xxx})

完整实例

  • 注:next() 是要返回的。通过 return/await next()
  • 目录结构
    • bin/www 或者是 server.js 负责启动服务
    • app.js 业务主入口:只有单纯的业务服务
    • middleware 自定义中间件
      • 可配置 koa-compose 对公共中间件进行组合
    • extend 扩展:对 app/app.context 进行扩展,挂载常用方法等
    • router 路由层
      • api 对外接口:特定中间件预处理,调用controller
        • 中间件:基础信息检验(状态),数据特殊处理等
      • view 试图接口
    • controller 控制层
      • 参数校验、用户信息获取、组装参数
      • 调用 service 传递参数并获取数据
      • 响应给用户(可将结果转化为特定格式)
    • service 数据层
      • 具体的业务处理,通过model获取和组装数据、格式化(添加默认数据/字段转换)
      • 第三方服务的调用等
    • model(db) 数据模型;定义数据关系模型