基于原生 JS 实现的 Bean 容器和 AOP 编程
Bean 是什么
我们知道Bean是Spring最基础的核心构件,大多数逻辑代码都通过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.atom、ctx.bean.user、ctx.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,方便其他模块的调用,比如:atom、user、role、flow、flowTask,等等
本地 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
相关链接
- 官网: https://cabloy.com/
- GitHub: https://github.com/zhennann/cabloy