基于 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.
一切的一切,初衷都是为了简化代码,提高开发效率。