实体自动生成增删改查方法_简单封装 eggjs 自动生成增删改查接口

eggjs 作为国内做的最好的企业级 nodejs 框架,整体设计以及很多细节功能个人都非常喜欢。我在 2017 年负责组织腾讯 IMWebConf 时,就曾经邀请 eggjs 的核心开发者天猪分享关于 eggjs 的一些经验。

自从 eggjs 发布后,我个人做的一些项目也逐步从 restify 切换至 eggjs。由于我使用的基本是 Mongodb,配合 mongoose 使用,随着项目的增多,针对数据表的增删改查接口成为其中最常用的一个需求。

按照 eggjs 的约定,如果要实现一个表的增删改查需求,例如用户表,我们需要新建 3 个文件:

  • schema/user.js 描述用户表结构
  • service/user.js 针对用户表的增删改查操作,引用 schema/user.js,其中根据 mongoose 的规则定义 model。
  • controller/user.js 针对增删改查请求做一些参数的校验以及处理后,调用 server/user.js 中的接口完成对应操作,并作出响应。

此外,还需要修改 router.js,如果 controller 中定义的方法是按照 router 的约定来定义的话,则只需要添加一行:

// router.post('/api/user', controller.user.create)
// router.delete('/api/user/:id', controller.user.destroy)
// router.put('/api/user/:id', controller.user.update)
// router.get('/api/user/:id', controller.user.show)
// router.get('/api/user', controller.user.index)

router.resources('user', '/api/user', controller.user); // 用户

如果按照这个模式,当数据表增加至十几个甚至几十个时,重复的文件和代码就太多了。当然,我们可以定义增删改查的基类,其他表的 service 和 controller 都继承自该基类,但还是有些繁琐,很多文件看着也不便管理。

所以我期望的最终效果是:

  • 只需要定义 schema/user.js,不需要新建任何文件,即可自动生成对应的 service 和 controller,之后只需要根据需求修改 router.js,暴露相关的接口。
  • 可以根据需求的变更自定义 service 和 controller。例如新建 service/user.js 即可自动继承基础的增删改查方法,同时可以自定义一些其他的方法。

在查看 eggjs 的官方文档后,我们需要做的第一件事情是能够自动加载 schema。这里赞一下 eggjs 的文档,写的非常详细,对于如何扩展框架也有非常详细的描述。

首先新建一个 eggjs 的框架项目,添加文件 lib/framework.js ,添加基础代码,使用 loader 加载 app/schema 目录的所有文件。

class AppWorkerLoader extends egg.AppWorkerLoader {
  loadRouter(opt) {
    this.loadSchema();
    super.loadRouter(opt);
  }
  loadSchema() {
    const { app } = this;
    const schemaPaths = this.getLoadUnits().map(unit => path.join(unit.path, 'app/schema'));

    // 先加载schema
    if (app.config.schema) {
      this.loadToApp(schemaPaths, 'schema');
    }
  }
}

这样既可通过 app.schema 访问到所有定义的表结构。

第二步,自动生成 service,以及支持自定义 service。这里,我翻阅了 eggjs 的源码,其中加载 service 的部分,核心是把 service 挂载到 app.serviceClasses这个对象上。所以我们可以直接把需要自动生成的 service 挂到 app.serviceClasses 对象即可。

我们在 loadSchema 方法下面添加如下代码:

// 根据schema生成对应的service和controller
Object.keys(app.schema).forEach(name => {
  const customService = this.app.serviceClasses[name]; // 自定义的 service
  this.app.serviceClasses[name] = this.createService(name);
  // 如果有自定义 service,合并原型方法。
  if (customService) {
    Object.assign(this.app.serviceClasses[name].prototype, customService);
  }
});

然后在 AppWorkerLoader 类添加一个创建自定义 service 的 createService 方法:

createService(name) {
  class NewService extends this.app.serviceClasses.base {
    get name() { return name; }
  }
  return NewService;
}

其中只定义了一个 name 属性的 getter 方法,方便 service 可以知道自己对应的是哪个 schema,可以通过 this.app.schema[this.name] 访问到 schema。

第三步,自动生成 controller,并支持继承。这里查看 eggjs 源码后,发现在完成 loader 加载 controller 之后,会对 controller 对一些特殊处理,以适应 router.js 中对 controller 的调用。所以我这里没有考虑太复杂,只是简单的复制同样的代码对新建的 controller 做同样处理。

在外围添加对 controller 处理的方法,然后继续修改 loadSchema 方法:

function callFn(fn, args, ctx) {
  args = args || [];
  return ctx ? fn.call(ctx, ...args) : fn(...args);
}

// wrap the class, yield a object with middlewares
function wrapClass(Controller) {
  let proto = Controller.prototype;
  const ret = {};
  // tracing the prototype chain
  while (proto !== Object.prototype) {
    const keys = Object.getOwnPropertyNames(proto);
    for (const key of keys) {
      // getOwnPropertyNames will return constructor
      // that should be ignored
      if (key === 'constructor') {
        continue;
      }
      // skip getter, setter & non-function properties
      const d = Object.getOwnPropertyDescriptor(proto, key);
      // prevent to override sub method
      if (typeof d.value === 'function' && !ret.hasOwnProperty(key)) {
        ret[key] = methodToMiddleware(Controller, key);
      }
    }
    proto = Object.getPrototypeOf(proto);
  }
  return ret;

  function methodToMiddleware(Controller, key) {
    return function classControllerMiddleware(...args) {
      const controller = new Controller(this);
      if (!this.app.config.controller || !this.app.config.controller.supportParams) {
        args = [ this ];
      }
      return callFn(controller[key], args, controller);
    };
  }
}

class AppWorkerLoader extends egg.AppWorkerLoader {
  createController(name) {
    class NewController extends this.app.BaseController {
      constructor(ctx) {
        super(ctx);
        this.name = name;
      }
    }
    return NewController;
  }
  loadSchema() {
    const { app } = this;
    const schemaPaths = this.getLoadUnits().map(unit => path.join(unit.path, 'app/schema'));

    // 先加载schema
    if (app.config.schema) {
      this.loadToApp(schemaPaths, 'schema');
    }
    // 根据schema生成对应的service和controller
    Object.keys(app.schema).forEach(name => {
      const customService = this.app.serviceClasses[name]; // 自定义的 service
      this.app.serviceClasses[name] = this.createService(name);
      // 如果有自定义 service,合并原型方法。
      if (customService) {
        Object.assign(this.app.serviceClasses[name].prototype, customService);
      }
      // 生成 controller
      this.app.controller[name] = wrapClass(this.createController(name));
    });
  }
}

至此,我的大部分需求就已经实现了,除了自定义 controller 之外(这个等会再讲)。

那么,到现在,应该如何使用这个框架呢?

  • 把这个 framework 发布到 npm ,例如叫做 eggooo
  • 在新项目中,npm i eggoo —save,并在 package.json 中添加 egg 属性:"egg": { "framework": "eggooo" }
  • 添加 service/base.js 作为所有表的 service 基类,其中定义增删改查方法。
  • 添加 controller/base.js 作为 controller 的基类。这里使用了一个简单的技巧:
const egg = require('egg');

module.exports = app => {
  class Controller extends egg.Controller {
    constructor(ctx) {
      super(ctx);
      this.name = 'base';
    }

    getService() {
      const service = this.service[this.name];
      if (!service) throw new Error(`没有找到对应的service:${this.name}`);
      return service;
    }

    // 创建
    async create() {
    }

    // 删除单个
    async destroy() {
    }

    // 修改
    async update() {
    }

    // 获取单个
    async show() {
    }

    // 获取所有(分页/模糊)
    async index() {
    }

    // 删除所选(条件id[])
    // {id: ["5a452a44ab122b16a0231b42","5a452a3bab122b16a0231b41"]}
    async removes() {
    }
  }
  app.BaseController = Controller;
};

由于我们把 controller 挂到了 app.BaseController ,所以如果需要自定义 controller 的话,新建的 controller 继承这个 app.BaseController 即可。例如自定义 user 的 controller:

module.exports = app => {
  class Controller extends app.BaseController {
    constructor(ctx) {
      super(ctx);
      this.name = 'user';
    }
    // 自定义的方法
    async customMethod(){
    }
  }
  return Controller;
};

OK,到这里就大概差不多了,完整代码可以在这里看到:https://github.com/yisbug/ooo

当然,由于时间关系,其中对 controller 的处理其实不太优雅(复制源码,更新版本可能导致问题),有兴趣的同学可以进一步研究,也欢迎留言交流。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值