Node.js 基于原生 JS 实现的 Bean 容器和 AOP 编程

zhennann · January 06, 2021 · Last by pynix replied at January 07, 2021 · 472 hits

Bean 是什么

我们知道BeanSpring最基础的核心构件,大多数逻辑代码都通过Bean进行管理。NestJS基于TypeScript依赖注入也实现了类似于Spring Bean的机制:服务提供者(Provider)

CabloyJS 则是在原生JS(Vanilla JS)上实现了更轻量、更灵活的 Bean 容器

理念

CabloyJS 在设计 Bean 容器机制时,遵循了以下 3 个理念:

1. 几乎所有事物都是 Bean

我们绝大多数逻辑代码都通过 Bean 组件进行管理,比如:Controller、Service、Model、Middleware、Event、Queue、Broadcast、Schedule、Startup、Flow、Flow Task,等等

CabloyJS 4.0 在实现了 Bean 容器之后,基本上所有核心组件都以 Bean 为基础进行了重构。比如基于 EggJS 的 Controller、Service、Middleware,也实现了 Bean 组件化

2. Bean 支持 AOP

所有 Bean 组件都可以通过 AOP 组件进行逻辑扩展

3. AOP 也是一种 Bean

AOP 组件既然也是 Bean,那么也可以通过其他 AOP 组件进行逻辑扩展

这种递归设计,为系统的可定制性和延展性,提供了强大的想象空间

定义 Bean

CabloyJS 约定了两种定义 Bean 的模式:app 和 ctx。由于 Bean 被容器托管,可以很方便的跨模块调用。因此,为了清晰的辨识 Bean 被应用的场景,一般约定:如果 Bean 只被本模块内部调用,那么就使用 app 模式;如果大概率会被其他模块调用,那么就使用 ctx 模式

1. app 模式

比如:Controller、Service 都采用 app 模式

src/module/test-party/backend/src/bean/test.app.js

module.exports = app => {

  class appBean extends app.meta.BeanBase {

    actionSync({ a, b }) {
      return a + b;
    }

    async actionAsync({ a, b }) {
      return Promise.resolve(a + b);
    }

  }

  return appBean;
};

2. ctx 模式

比如:ctx.bean.atomctx.bean.userctx.bean.role都采用 ctx 模式

src/module/test-party/backend/src/bean/test.ctx.js

module.exports = ctx => {
  class ctxBean {

    constructor(moduleName) {
      this._name = moduleName || ctx.module.info.relativeName;
    }

    get name() {
      return this._name;
    }

    set name(value) {
      this._name = value;
    }

    actionSync({ a, b }) {
      return a + b;
    }

    async actionAsync({ a, b }) {
      return Promise.resolve(a + b);
    }

  }

  return ctxBean;
};

ctx.module.info.relativeName: 由于 ctx 模式的 Bean 经常被其他模块调用,那么可以通过此属性取得调用方模块的名称

注册 Bean

对于大多数组件,EggJS 采用约定优先的策略,会在指定的位置查找资源,并自动加载。而 CabloyJS 采用显式注册,从而 Webpack 可以收集所有后端源码,实现模块编译的特性

src/module/test-party/backend/src/beans.js

const testApp = require('./bean/test.app.js');
const testCtx = require('./bean/test.ctx.js');

module.exports = app => {
  const beans = {
    // test
    'test.app': {
      mode: 'app',
      bean: testApp,
    },
    testctx: {
      mode: 'ctx',
      bean: testCtx,
      global: true,
    },
  };
  return beans;
};
名称 说明
mode 模式:app/ctx
bean bean 组件
global 是否是全局组件

使用 Bean

1. beanFullName

每一个注册的 Bean 组件都被分配了全称,具体规则如下

注册名称 场景 所属模块 global beanFullName
test.app test test-party false test-party.test.app
testctx test-party true testctx

全局 Bean(global:true): 当一个 Bean 组件可以作为一个核心的基础组件的时候,可以设置为全局 Bean,方便其他模块的调用,比如:atomuserroleflowflowTask,等等

本地 Bean(global:false): 当一个 Bean 组件一般只用于本模块时,可以设置为本地 Bean,从而避免命名冲突

场景:对于本地Bean,我们一般为其分配一个场景名称作为前缀,一方面便于 Bean 的分类管理,另一方面也便于辨识 Bean 的用途

2. 基本调用

可以直接通过this.ctx.bean取得 Bean 容器,然后通过beanFullName获取 Bean 实例

src/module/test-party/backend/src/controller/test/feat/bean.js


// global: false
this.ctx.bean['test-party.test.app'].actionSync({ a, b }); 
await this.ctx.bean['test-party.test.app'].actionAsync({ a, b });

// global: true
this.ctx.bean.testctx.actionSync({ a, b });
await this.ctx.bean.testctx.actionAsync({ a, b });

3. 新建 Bean 实例

通过this.ctx.bean获取 Bean 实例,那么这个实例对当前ctx而言是单例的。如果需要新建 Bean 实例,可以按如下方式进行:

ctx.bean._newBean(beanFullName, ...args)

比如我们要新建一个 Flow 实例:

src/module-system/a-flow/backend/src/bean/bean.flow.js

_createFlowInstance({ flowDef }) {
  const flowInstance = ctx.bean._newBean(`${moduleInfo.relativeName}.local.flow.flow`, {
    flowDef,
  });
  return flowInstance;
}

4. 跨模块调用本地 Bean

本地 Bean 也可以被跨模块调用

跨模块调用的本质:新建一个 ctx 上下文环境,该 ctx 的 module 信息与本地 Bean 一致,然后通过新容器ctx.bean来调用本地 Bean

await ctx.executeBean({ locale, subdomain, beanModule, beanFullName, context, fn, transaction })
名称 可选 说明
locale 可选 默认等于 ctx.locale
subdomain 可选 默认等于 ctx.subdomain
beanModule 必需 本地 Bean 所属模块名称
beanFullName 必需 本地 Bean 的全称
context 可选 调用本地 Bean 时传入的参数
fn 必需 调用本地 Bean 的方法名
transaction 可选 是否要启用数据库事务

比如我们要调用模块a-file的本地 Bean: service.file,直接上传用户的 avatar,并返回 downloadUrl

src/module-system/a-base-sync/backend/src/bean/bean.user.js

// upload
const res2 = await ctx.executeBean({
  beanModule: 'a-file',
  beanFullName: 'a-file.service.file',
  context: { fileContent: res.data, meta, user: null },
  fn: '_upload',
});
// hold
profile._avatar = res2.downloadUrl;

5. app.bean

ctx.bean是每个请求初始化一个容器,而app.bean则可以实现整个应用使用一个容器,从而实现 Bean 组件的应用级别的单例模式

src/module/test-party/backend/src/controller/test/feat/bean.js

app.bean['test-party.test.app'].actionSync({ a, b }); 
await app.bean['test-party.test.app'].actionAsync({ a, b });

AOP 编程

限于篇幅,关于AOP编程请参见:cabloy-aop

相关链接

ruby 圈子哪有什么 AOP

You need to Sign in before reply, if you don't have an account, please Sign up first.