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

2021-01-06 10:42:12 +08:00
 zhennann

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

相关链接

2457 次点击
所在节点    推广
19 条回复
codespots
2021-01-06 10:50:54 +08:00
搞 Java 的这帮人比 GPL 还有传染性
morrieati
2021-01-06 11:05:45 +08:00
@codespots 确实
foxcell
2021-01-06 11:06:51 +08:00
定义 Bean 还要想想 什么地方用到,这还是 bean 么,跟类有啥区别
BDC017
2021-01-06 11:15:14 +08:00
感觉写 Java 都写魔怔了。
wobuhuicode
2021-01-06 11:19:08 +08:00
这……有点无语
zhoudaiyu
2021-01-06 12:13:39 +08:00
有内味了兄弟们
kidlj
2021-01-06 12:17:44 +08:00
求放过
CODEWEA
2021-01-06 12:22:29 +08:00
有啥用?我就写个图片轮播用得着吗?
zjsxwc
2021-01-06 12:28:36 +08:00
都是容器古老的 require.js 作为容器 也比楼主这个 bean 好用,
但我还是建议使用 angular 基于 ts 的容器。
ychost
2021-01-06 15:16:55 +08:00
至少也搞个装饰器封装下
zhennann
2021-01-06 15:55:35 +08:00
@foxcell 不需要思考呐。任何地方访问 bean,只需要`ctx.bean.beanFullName`即可。
你所说的应该是 app 和 ctx 两种 bean 模式。由于 CabloyJS 是模块化的隔离体系,一个 bean 组件既可以在本模块用,也可以跨模块使用。
zhennann
2021-01-06 15:58:06 +08:00
如果用装饰器,任何地方如果要使用 Bean,都需要再用装饰器声明一下。而基于原生 JS 的 Bean,在任何地方只需要`ctx.bean.beanFullName`引用 Bean,告别了装饰器满天飞的模式
zhennann
2021-01-06 16:01:27 +08:00
@BDC017 不是非要向 Java 靠拢。Bean 容器,一方面方便管理和使用组件,另一方面可以使用 AOP 机制进行逻辑的扩展。如果一个项目既包括自定义业务模块,也包括系统模块,使用 aop 机制就可以轻易对“系统模块”做逻辑扩展和变更
zhennann
2021-01-07 08:14:39 +08:00
@CODEWEA 这篇文章刚好写到 bean 容器。其实,CabloyJS 还包括权限管理、数据生命周期管理(草稿->归档->历史)
,而且自带 NodeJS 工作流引擎,全新的布局自适应方案 pc=mobile+pad,可以官网或者 github 查看相关内容
zhennann
2021-01-07 08:27:18 +08:00
@kidlj 谢谢。其实被围剿的感觉也不错,至少有人关注了。这只是 CabloyJS 生态其中的一篇文章而已,如果只是孤立的看这篇文章,确实没啥意义,当然也就错过了一片还算不错的森林
kidlj
2021-01-07 13:10:26 +08:00
@zhennann 嗯嗯,别在意,我并不懂 Java 的 aop 是什么,凑个热闹开个玩笑!
zhennann
2021-01-07 14:49:57 +08:00
@kidlj 👌 我也是借题发挥一下,爱惜羽毛嘛,没忍住,多说了几句😄😄
xcstream
2021-01-08 00:06:31 +08:00
不喜欢 java 的原因就是因为 aop
zhennann
2021-01-08 08:21:26 +08:00
@xcstream 你看是否可以这样理解。aop 是一种能力( bean 容器的副作用),如果不想用就当他不存在。因此,你不喜欢 java 的原因,可能是在 java 里面,不论是 bean 组件的定义还是使用,注解漫天飞的情况。
这里提到的基于原生 js 的 bean 容器,不再需要注解。比如,系统提供了一个 bean 组件:user, 那么在项目的任何地方,只需使用`ctx.bean.user`直接引用该 bean 组件,不再需要像 java 通过注解的方式声明一个变量了

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/742094

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX