有时候我们需要为OpenAPI设置一些描述信息,比如说设置API的版本,联系人信息。我们可以通过设置Router的api选项来预设一些信息。更多支持预设的字段以及数据结构请参考OpenAPI2.0 Schema。
// index.js
import Koa from 'koa';
import { OreoRouter } from 'oreo-router';
const app = new Koa();
const router = new OreoRouter({
api: {
// API基础信息
info: {
// API文档的标题
title: 'title',
// API文档的描述
description: 'description',
// API文档的版本信息
version: '1.0.0',
// API文档的联系人信息
contact: {
// 联系人/组织的姓名
name: 'API Support',
// 联系人/组织的主页地址
url: 'http://www.swagger.io/support',
// 联系人/组织的邮箱
email: 'support@swagger.io',
},
},
},
apiExplorerEnable: true,
middleware: {
dir: './controllers',
},
});
app.use(router.routes());
app.listen(3000, () => {
console.log(`
Listening Port : 3000
Api-Explorer : http://127.0.0.1:3000/api-explorer
`);
});
// controllers/hello.js
import {
Middleware, // Mean this is a api class.
Get, // A definition of a GET operation on this path.
Summary, // A short summary of what the operation does.
Description, // A verbose explanation of the operation behavior.
Tags, // A list of tags for API documentation control. Tags can be used for logical
Query, // Parameters that are appended to the URL. For example, in /items?id=###, the query parameter is id.
Responses, // The list of possible responses as they are returned from executing this operation.
} from 'oreo-router';
@Middleware('/hello')
export default class ExampleController {
@Get('/')
@Summary('hello')
@Description('oreo-router is awesome!')
@Tags(['example'])
@Query({
name: {
type: 'string',
required: true,
},
})
@Responses({
200: {
description: 'Request success.',
},
})
hello(ctx, next) {
ctx.response.body = `hello world! ${ctx.request.query.name}`;
}
}
现在你就可以使用api-explorer,可以看到预设的API基础信息以及API信息了。
> curl -X GET "http://127.0.0.1:3000/api/hello?name=bitebit" -H "accept: application/json"
> hello world! bitebit
在项目实践中,我们经常需要使用表单校验功能,检查某个入参是否符合特定的条件,检验通过之后才交由后续中间件处理。比如说我们现在需要增加query表单校验功能,那么我们可以为Query装饰器设置处理器中间件,来进行表单校验。提示:如果想在最开始的时候校验表单,那么Query装饰器应当使用的尽量靠前。关于装饰器中间件的执行顺序问题,请参考后文《装饰器中间件的执行顺序》。
// index.js
import Koa from 'koa';
import { OreoRouter } from 'oreo-router';
import queryMiddlewareWrapper from './decorator-middleware/query';
const app = new Koa();
const router = new OreoRouter({
apiExplorerEnable: true,
internalDecoratorHandlers: {
// 为内置装饰器Query配置处理器中间件
Query: queryMiddlewareWrapper,
},
middleware: {
dir: './controllers',
},
});
app.use(router.routes());
app.listen(3000, () => {
console.log(`
Listening Port : 3000
Api-Explorer : http://127.0.0.1:3000/api-explorer
`);
});
// controllers/hello.js
import {
Middleware,
Get,
Summary,
Description,
Tags,
Query,
Responses,
} from 'oreo-router';
@Middleware('/hello')
export default class ExampleController {
@Get('/')
@Summary('hello')
@Description('oreo-router is awesome!')
@Tags(['example'])
// 装饰器Query的参数将会被传递给上一步配置的中间件处理器中。
// 接口入参必须包含一个name字段。
@Query({
name: {
type: 'string',
required: true,
},
})
@Responses({
200: {
description: 'Request success.',
},
})
// 返回hellow world {{name}}
hello(ctx, next) {
ctx.response.body = `hello world! ${ctx.request.query.name}`;
}
}
// querySchema 为装饰器Query的参数
// 如果name是必须字段且query入参不包含name,那么提示“Please enter name”
// 否者校验通过
export default function queryMiddlewareWrapper(querySchema) {
return async (ctx, next) => {
if (querySchema.name.required === true) {
if (!ctx.request.query.name) {
ctx.throw(400, 'Please enter name');
} else {
await next();
}
} else {
await next();
}
};
}
> curl -X GET "http://127.0.0.1:3000/api/hello?name=bitebit" -H "accept: application/json"
> hello world! bitebit
提示”hello world! bitebit“,说明表单校验通过,业务中间件处理了请求。
> curl -X GET "http://127.0.0.1:3000/api/hello" -H "accept: application/json"
> Please enter name
提示“Please enter name“,说明表单校验未通过,我们成功了。
Koa生态有大量好用的中间件,如果不能使用的话,那是非常遗憾的。我们提供了UseBefore,Use,UseAfter装饰器可以便捷的将koa原生中间件引入进来。UseBefore,Use,UseAfter不仅可以应用在类的方法上来挂载到指定方法的前后,还可以应用在类上来挂载到该类所有的方法前后。
// index.js
import Koa from 'koa';
import { OreoRouter } from 'oreo-router';
const app = new Koa();
const router = new OreoRouter({
apiExplorerEnable: true,
middleware: {
dir: './controllers',
},
});
app.use(router.routes());
app.listen(3000, () => {
console.log(`
Listening Port : 3000
Api-Explorer : http://127.0.0.1:3000/api-explorer
`);
});
// ./middlewares/logger.js
export default async function (ctx, next) {
const path = ctx.path;
const method = ctx.method;
console.log(`-> ${new Date().toISOString()} ${method} ${path}`);
await next();
console.log(`<- ${new Date().toISOString()} ${method} ${path}`);
}
// ./controllers/hello.js
import {
Middleware,
Use,
Get,
Summary,
Description,
Tags,
Query,
Responses,
} from 'oreo-router';
import logger from '../middlewares/logger';
@Middleware('/hello')
export default class ExampleController {
@Get('/log')
@Summary('hello')
@Description('oreo-router is awesome!')
@Tags(['example'])
@Query({
name: {
type: 'string',
required: true,
},
})
@Responses({
200: {
description: 'Request success.',
},
})
@Use(logger)
log(ctx, next) {
ctx.response.body = `hello world! ${ctx.request.query.name}`;
}
@Get('/no-log')
@Summary('hello but no log')
@Description('oreo-router is awesome!')
@Tags(['example'])
@Query({
name: {
type: 'string',
required: true,
},
})
@Responses({
200: {
description: 'Request success.',
},
})
nolog(ctx, next) {
ctx.response.body = `hello world! ${ctx.request.query.name}`;
}
}
> curl curl -X GET "http://127.0.0.1:3000/api/hello/no-log?name=bitebit" -H "accept: application/json"
> hello world! bitebit
服务器控制台未输出任何请求日志
>
> curl -X GET "http://127.0.0.1:3000/api/hello/log?name=bitebit" -H "accept: application/json"
>
服务器控制台输出请求如下日志
> -> 2017-12-15T13:18:12.678Z GET /api/hello/log
> <- 2017-12-15T13:18:12.681Z GET /api/hello/log
// index.js
import Koa from 'koa';
import { OreoRouter } from 'oreo-router';
const app = new Koa();
const router = new OreoRouter({
apiExplorerEnable: true,
middleware: {
dir: './controllers',
},
});
app.use(router.routes());
app.listen(3000, () => {
console.log(`
Listening Port : 3000
Api-Explorer : http://127.0.0.1:3000/api-explorer
`);
});
// ./decorators/logger.js
import { toDecorator } from 'oreo-router';
export default toDecorator({
name: 'Log',
middleware: (label) => {
return async (ctx, next) => {
const path = ctx.path;
const method = ctx.method;
console.log(`${label} -> ${new Date().toISOString()} ${method} ${path}`);
await next();
console.log(`${label} <- ${new Date().toISOString()} ${method} ${path}`);
};
},
});
// ./controllers/hello.js
import {
Middleware,
Get,
Summary,
Description,
Tags,
Query,
Responses,
} from 'oreo-router';
import Log from '../decorators/logger';
@Middleware('/hello')
export default class ExampleController {
@Get('/')
@Summary('hello')
@Description('oreo-router is awesome!')
@Tags(['example'])
@Query({
name: {
type: 'string',
required: true,
},
})
@Responses({
200: {
description: 'Request success.',
},
})
@Log('hello logger...')
hello(ctx, next) {
ctx.response.body = `hello world! ${ctx.request.query.name}`;
}
}
> curl http://127.0.0.1:3000/api/hello?name=name
> hello logger... -> 2017-12-15T13:28:50.398Z GET /api/hello
> hello logger... <- 2017-12-15T13:28:50.399Z GET /api/hello
装饰器中间件的调用顺序分为外序和内序,但是无论是外序还是内序,都是由上至下先后调用的。 外序指的是UseBefore、Use、UseAfter、Decorator(泛指其他装饰器) 四者之间的顺序关系。 内序指的是UseBefore、Use、UseAfter、Decorator(泛指其他装饰器) 同时装饰一个方法时与自己的顺序关系,比如两个UseBefore同时装饰一个方法的时候,在前面的UseBefore先执行。
// index.js
import Koa from 'koa';
import { OreoRouter } from 'oreo-router';
import { queryMiddlewareWrapper } from './decorators/query';
const app = new Koa();
const router = new OreoRouter({
apiExplorerEnable: true,
middleware: {
dir: './controllers',
},
internalDecoratorHandlers: {
Query: queryMiddlewareWrapper,
},
});
app.use(router.routes());
app.listen(3000, () => {
console.log(`
Listening Port : 3000
Api-Explorer : http://127.0.0.1:3000/api-explorer
`);
});
// ./middlewares/log.js
export default function log(label) {
return (ctx, next) => {
console.log(label);
return next();
};
}
// ./decorators/cache.js
import { toDecorator } from 'oreo-router';
export default toDecorator({
name: 'Cache',
maxDecorate: 10,
middleware: (label) => {
return (ctx, next) => {
console.log(label);
return next();
};
},
});
// ./decorators/query.js
import { toDecorator } from 'oreo-router';
function queryMiddlewareWrapper(querySchema) {
return async (ctx, next) => {
console.log('Decorator Query', querySchema);
await next();
};
}
const Query = toDecorator({
name: 'Query',
maxDecorate: 1,
middleware: queryMiddlewareWrapper,
});
export {
queryMiddlewareWrapper,
Query,
};
// ./controllers/hello.js
import {
Middleware,
Get,
Summary,
Description,
Tags,
Query,
Responses,
UseBefore,
Use,
UseAfter,
} from 'oreo-router';
import Cache from '../decorators/cache';
import logger from '../middlewares/log';
@Middleware('/hello')
@Cache('Decorator Class')
@UseBefore(logger('UseBefore Class '))
@Use(logger('Use Class'))
@UseAfter(logger('UseAfter Class'))
export default class ExampleController {
@Get('/')
@Summary('hello')
@Description('oreo-router is awesome!')
@Cache('Decorator function 0')
@Tags(['example'])
@Query({
name: {
type: 'string',
required: true,
},
})
@Cache('Decorator function 1')
@Responses({
200: {
description: 'Request success.',
},
})
@Cache('Decorator function 2')
@UseBefore(logger('UseBefore Class function'))
@Use(logger('Use Class function'))
@UseAfter(logger('UseAfter Class function'))
hello(ctx, next) {
console.log('hello world!');
ctx.response.body = `hello world! ${ctx.request.query.name}`;
return next();
}
}
> curl -X GET "http://127.0.0.1:3000/api/hello?name=bitebit" -H "accept: application/json"
> hello world! bitebit
服务器控制台输出如下:
> UseBefore Class
> Decorator Class
> Use Class
> UseBefore Class function
> Decorator function 0
> Decorator Query { name: { type: 'string', required: true } }
> Decorator function 1
> Decorator function 2
> Use Class function
> hello world!
> UseAfter Class function
> UseAfter Class