基于 koa 1.x 实现 api_generator
背景
在实际项目中,我们采用前后端分离的方式进行开发,即前端需要负责浏览器端、以及 nodejs 端,后台负责提供服务。因此,一个浏览器的 API 请求如下
- 浏览器通过请求,调用 nodejs 提供的 API
- nodejs 拿到请求后,进行必要的数据包装,然后调用后台提供的 API
- nodejs 拿到结果后,进行必要的数据处理,执行必要的操作,然后将结果返回给浏览器
目前代码分析
以 根据 id 获取用户详情
这一个接口为例,在 nodejs 端我们的代码如下
router
router.get('/apis/user/:id', apiAuthCheck, controller.getUser);
controller
exports.getUser = function*() { const ctx = this; const id = ctx.params.id; const result = userService.getOneById(id, ctx); console.log(`[获取单个用户详情]: ${result}`); return messageHelper.success(ctx, result); // messageHelper 是返回辅助类,此处不细说 }
service
exports.getOneById = (id, ctx) => request.send('get', `/user/${id}`, null, null, ctx, false);
utils
// 通过 cookie 判断是否登陆 exports.apiAuthCheck = function*() { if (this.cookies.get('token')) { ... yield next; } } // 请求辅助类,request 使用 superagent 库 exports.send = function (method, path, query, data, ctx, isForm) => { return new Promise((resolve, reject) => { const q = request[method](`${serverConfig.url}${path}`) .query(Object.assign({ }, query || {})); if (ctx.state.accessToken) { q.set('access-token', ctx.state.accessToken); } if (data) { q.send(data); } if (isForm) { q.type('form'); } q.end((err, result) => { if (err) { if (err.code === 'ECONNREFUSED') { reject({ errCode: 50000, errMsg: '服务器连接失败,请重试' }); } if (err.response) { reject({ errCode: err.response.body.status || 50000, errMsg: err.response.body.message || '服务器连接失败,请重试' }); } else { reject({ errCode: 50000, errMsg: '服务器连接失败,请重试' }); } } else { if (result.body) { resolve(result.body); } else { resolve(result.text); } } }); }); }
可以看到,就算有提取了一些辅助类,每添加一个接口,我们还是需要在 router
controller
service
中分别添加代码,而且几乎都是复制粘贴!而且,随着接口的增多,这些文件将会不可避免地产生代码量爆炸
思路
仔细分析上面的代码会发现,其实每一个接口,变化的只有下面的东西:
- node 路由的 path
- 后台接口的 path
- 调用方法
- 参数传递
- 是否 form 提交(后台提供的接口分为 form 提交和 json 提交)
- 打印
根据之前写的 action_generator,entity_generator 的经验,可以实现一个 api_generator
,将变化的东西变成配置,将不变的东西抽取出来,一劳永逸
我们定义 api_generator 接受一个 Array,该 Array 的每一个元素都是一个 json 配置,根据该配置可以生成一个对应的接口
Talk is cheap. Show me the code.
// 获取调用方法,其中,将 delete 转为 koa 路由识别的 del
const getMethod = (serverMethod, apiMethod) => {
const method = serverMethod || apiMethod;
return method === 'delete' ? 'del' : method;
};
// 获取路径中的参数
const getParams = path => (path.match(/:\w*/g) || []).map(param => param.substring(1));
// 通过参数替换,获取最终调用的后台地址
const getRealServerPath = (serverPath, serverParams, params) => {
let result = serverPath;
serverParams.forEach(param => {
result = result.replace(`:${param}`, params[param]);
});
return result;
};
module.exports = apis => router => {
apis.forEach(api => {
/** api 本身的调用方式 */
const method = api.method;
/** api 的显示名称 */
const name = api.name;
/** api 本身的 path */
const apiPath = api.apiPath;
/** 后台接口的调用方式 */
const serverMethod = api.serverMethod;
/** superagent 请求后台接口时调用方法 */
const requestMethod = getMethod(serverMethod, method);
/** superagent 请求后台接口时是否使用 form 提交 */
const isForm = api.isForm || false;
router[method](apiPath,
function *() {
const ctx = this;
const params = ctx.params || {};
const query = ctx.request.query || null;
const body = (ctx.request.body || {}).fields || null;
const file = (ctx.request.body || {}).files || null;
/** 获取后台接口的 path,可以是直接配置,也可以通过请求参数转化得到,若不配置,默认和 api 一致 */
const serverPath = api.serverPath ||
api.mapServerPath && api.mapServerPath(params, query, body, ctx) ||
apiPath;
/** 根据 params,将参数填充,得到真正的 path */
const serverParams = getParams(serverPath);
const realServerPath = getRealServerPath(serverPath, serverParams, params);
ctx.logger.log(`/******************** ${name} send to server ***********************/`);
ctx.logger.log(JSON.stringify({
path: realServerPath,
query,
body,
isForm
}));
ctx.logger.log(`/*******************************************************************/`);
try {
const result = yield request.send({
url,
method: requestMethod,
path: realServerPath,
query,
data: body,
ctx,
isForm
});
ctx.logger.log(`/****************** ${name} response from server ********************/`);
ctx.logger.log(JSON.stringify(result));
ctx.logger.log(`/********************************************************************/`);
return messageHelper.success(ctx, result.data);
} catch(e) {
ctx.logger.log(`/****************** ${name} error from server ***********************/`);
ctx.logger.log(JSON.stringify(e));
ctx.logger.log(`/********************************************************************/`);
return messageHelper.response(ctx, e);
}
});
});
};
通过以上的 api_generator,我们只需要以下的配置,就可以添加一开始我们想要的获取用户详情的接口了!
{
name: '获取单个用户详情',
method: 'get',
apiPath: '/user/:id'
}
兼容所有 API
参数转换,返回结果转换
// 添加一个 noWrap 函数表示不需要转换,作为参数转换的默认函数
const noWrap = data => data;
// 添加参数转换配置
const wrapQuery = api.wrapQuery || noWrap;
const wrapBody = api.wrapBody || noWrap;
const wrapParams = api.wrapParams || noWrap;
const wrapResult = api.wrapResult || noWrap;
// 在获取参数之后,对参数进行转换
/** 包装后台接口的参数 */
const wrappedParams = wrapParams(params, query, body, ctx);
const wrappedQuery = wrapQuery(query, params, body, ctx);
const wrappedBody = wrapBody(body, params, query, ctx);
// 对返回值做转换
return messageHelper.success(ctx, wrapResult(result.data));
// 使用示例
{
method: 'post',
name: '添加用户',
apiPath: '/user',
wrapBody: (body, params, query, ctx) => Object.assign({}, body, {
id: query.id,
time: moment(body.time).format('YYYY-MM-DD HH:mm:ss')
})
}
调用前后执行自定义业务
// 添加业务配置
/** 调用后台接口前执行的业务 */
const businessBefore = api.businessBefore;
/** 调用后台接口后,或者不调用接口时执行的业务 */
const business = api.business;
// 在获取参数后,添加前置业务
/** 执行调用后台接口前的逻辑 */
if (businessBefore) yield businessBefore(ctx, params, query, body, file);
// 调用接口后,如果有后置业务,则将接口返回结果暂存至 ctx 中,并执行后置业务
if (business) {
ctx.apiResult = wrapResult(result.data);
yield business(ctx, params, query, body, file);
} else {
return messageHelper.success(ctx, wrapResult(result.data));
}
// 使用示例
{
method: 'post',
name: '添加用户',
apiPath: '/user',
wrapBody: (body, params, query, ctx) => Object.assign({}, body, {
id: query.id,
time: moment(body.time).format('YYYY-MM-DD HH:mm:ss')
}),
businessBefore: function* (ctx, params, query, body, file) {
// do something.
},
business: function* (ctx, params, query, body, file) {
if (ctx.apiResult.errCode) {
// do something.
} else {
return messageHelper.success(ctx.apiResult);
}
}
}
普通接口,不需要调用后台
对于这样的接口,只需要加一个配置 needRequestServer
,默认为 true,如果配置为 false 就不需要调用后台接口,直接执行 business 即可,此处代码忽略
最终形态
const getMethod = (serverMethod, apiMethod) => {
const method = serverMethod || apiMethod;
return method === 'delete' ? 'del' : method;
};
const noWrap = data => data;
const getParams = path => (path.match(/:\w*/g) || []).map(param => param.substring(1));
const getRealServerPath = (serverPath, serverParams, params) => {
let result = serverPath;
serverParams.forEach(param => {
result = result.replace(`:${param}`, params[param]);
});
return result;
};
const requestAuthCheck = function *(next) {
// 校验 cookie
yield next;
}
module.exports = apis => router => {
apis.forEach(api => {
/** api 本身的调用方式 */
const method = api.method;
/** api 的显示名称 */
const name = api.name;
/** api 本身的 path */
const apiPath = api.apiPath;
/** 是否需要请求后台接口 */
const needRequestServer = api.needRequestServer === false ? false : true;
/** 校验中间件 */
const requestAuthCheck = api.requestAuthCheck || requestAuthCheck;
/** 后台接口的调用方式 */
const serverMethod = api.serverMethod;
/** superagent 请求后台接口时调用方法 */
const requestMethod = getMethod(serverMethod, method);
/** superagent 请求后台接口时是否使用 form 提交 */
const isForm = api.isForm || false;
/** 后台接口的参数和返回值包装 */
const wrapQuery = api.wrapQuery || noWrap;
const wrapBody = api.wrapBody || noWrap;
const wrapParams = api.wrapParams || noWrap;
const wrapResult = api.wrapResult || noWrap;
/** 调用后台接口前执行的业务 */
const businessBefore = api.businessBefore;
/** 调用后台接口后,或者不调用接口时执行的业务 */
const business = api.business;
router[method](apiPath, requestAuthCheck,
function *() {
const ctx = this;
const params = ctx.params || {};
const query = ctx.request.query || null;
const body = (ctx.request.body || {}).fields || null;
const file = (ctx.request.body || {}).files || null;
/** 执行调用后台接口前的逻辑 */
if (businessBefore) yield businessBefore(ctx, params, query, body, file);
if (needRequestServer) {
/** 包装后台接口的参数 */
const wrappedParams = wrapParams(params, query, body, ctx);
const wrappedQuery = wrapQuery(query, params, body, ctx);
const wrappedBody = wrapBody(body, params, query, ctx);
/** 获取后台接口的 path,可以是直接配置,也可以通过请求参数转化得到,若不配置,默认和 api 一致 */
const serverPath = api.serverPath ||
api.mapServerPath && api.mapServerPath(params, query, body, ctx) ||
apiPath;
/** 根据 params,将参数填充,得到真正的 path */
const serverParams = getParams(serverPath);
const realServerPath = getRealServerPath(serverPath, serverParams, wrapParams);
ctx.logger.log(`/******************** ${name} send to server ***********************/`);
ctx.logger.log(JSON.stringify({
path: realServerPath,
query: wrappedQuery,
body: wrappedBody,
isForm
}));
ctx.logger.log(`/*******************************************************************/`);
try {
const result = yield request.send({
url,
method: requestMethod,
path: realServerPath,
query: wrapQuery,
data: wrapBody,
ctx,
isForm
});
ctx.logger.log(`/****************** ${name} response from server ********************/`);
ctx.logger.log(JSON.stringify(result));
ctx.logger.log(`/********************************************************************/`);
if (business) {
ctx.apiResult = wrapResult(result.data);
yield business(ctx, params, query, body, file);
} else {
return messageHelper.success(ctx, wrapResult(result.data));
}
} catch(e) {
ctx.logger.log(`/****************** ${name} error from server ***********************/`);
ctx.logger.log(JSON.stringify(e));
ctx.logger.log(`/********************************************************************/`);
if (business) {
ctx.apiResult = e;
yield business(ctx, params, query, body, file);
} else {
return messageHelper.response(ctx, e);
}
}
} else {
if (business) {
yield business(ctx, params, query, body, file);
}
}
});
});
};
结语
记住,Don’t repeat yourself.
一切的一切,初衷都是为了简化代码,提高开发效率。