杂项
- 任何看似复杂的设计,都是为了让系统更加简单化
- 为什么有
bin/www这个文件,因为按照分离原则app.js内应该只包含业务代码;所以需要有个文件单独包含服务相关的代码
- 接口如果没有任何返回,默认响应
404
- 获取用户 ip 开启配置
app.proxy = true
- 使用
process捕获全局错误
- 监听
unhandledRejection。当promise被reject但没有对应的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相关方法
{
domain: 'localhost',
path: '/',
maxAge: 10 * 60 * 1000,
expires: new Date('2017-02-15'),
httpOnly: false,
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) {
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 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.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' })
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
}
const genValidator = require('../../middlewares/validator')
router.post('/register', genValidator({/rule/}), async (ctx, next) => {})
单元测试
const app = require('../src/app')
const request = require('supertest')
module.exports = request(app.callback())
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) 数据模型;定义数据关系模型