我们知道Bean
是Spring
最基础的核心构件,大多数逻辑代码都通过Bean
进行管理。NestJS
基于TypeScript
和依赖注入
也实现了类似于Spring Bean
的机制:服务提供者( Provider )
CabloyJS 则是在原生 JS ( Vanilla JS )
上实现了更轻量、更灵活的 Bean 容器
CabloyJS 在设计 Bean 容器机制时,遵循了以下 3 个理念:
我们绝大多数逻辑代码都通过 Bean 组件进行管理,比如:Controller 、Service 、Model 、Middleware 、Event 、Queue 、Broadcast 、Schedule 、Startup 、Flow 、Flow Task,等等
CabloyJS 4.0 在实现了 Bean 容器之后,基本上所有核心组件都以 Bean 为基础进行了重构。比如基于 EggJS 的 Controller 、Service 、Middleware,也实现了 Bean 组件化
所有 Bean 组件都可以通过 AOP 组件进行逻辑扩展
AOP 组件既然也是 Bean,那么也可以通过其他 AOP 组件进行逻辑扩展
这种递归设计,为系统的可定制性和延展性,提供了强大的想象空间
CabloyJS 约定了两种定义 Bean 的模式:app 和 ctx 。由于 Bean 被容器托管,可以很方便的跨模块调用。因此,为了清晰的辨识 Bean 被应用的场景,一般约定:如果 Bean 只被本模块内部调用,那么就使用 app 模式;如果大概率会被其他模块调用,那么就使用 ctx 模式
比如: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;
};
比如: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 经常被其他模块调用,那么可以通过此属性取得调用方模块的名称
对于大多数组件,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 组件都被分配了全称,具体规则如下
注册名称 | 场景 | 所属模块 | 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 的用途
可以直接通过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 });
通过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;
}
本地 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;
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 编程
请参见:cabloy-aop
1
codespots 2021-01-06 10:50:54 +08:00 13
搞 Java 的这帮人比 GPL 还有传染性
|
3
foxcell 2021-01-06 11:06:51 +08:00
定义 Bean 还要想想 什么地方用到,这还是 bean 么,跟类有啥区别
|
4
BDC017 2021-01-06 11:15:14 +08:00 2
感觉写 Java 都写魔怔了。
|
5
wobuhuicode 2021-01-06 11:19:08 +08:00
这……有点无语
|
6
zhoudaiyu 2021-01-06 12:13:39 +08:00 via iPhone
有内味了兄弟们
|
7
kidlj 2021-01-06 12:17:44 +08:00 via iPhone 1
求放过
|
8
CODEWEA 2021-01-06 12:22:29 +08:00
有啥用?我就写个图片轮播用得着吗?
|
9
zjsxwc 2021-01-06 12:28:36 +08:00
都是容器古老的 require.js 作为容器 也比楼主这个 bean 好用,
但我还是建议使用 angular 基于 ts 的容器。 |
10
ychost 2021-01-06 15:16:55 +08:00
至少也搞个装饰器封装下
|
11
zhennann OP @foxcell 不需要思考呐。任何地方访问 bean,只需要`ctx.bean.beanFullName`即可。
你所说的应该是 app 和 ctx 两种 bean 模式。由于 CabloyJS 是模块化的隔离体系,一个 bean 组件既可以在本模块用,也可以跨模块使用。 |
12
zhennann OP 如果用装饰器,任何地方如果要使用 Bean,都需要再用装饰器声明一下。而基于原生 JS 的 Bean,在任何地方只需要`ctx.bean.beanFullName`引用 Bean,告别了装饰器满天飞的模式
|
13
zhennann OP @BDC017 不是非要向 Java 靠拢。Bean 容器,一方面方便管理和使用组件,另一方面可以使用 AOP 机制进行逻辑的扩展。如果一个项目既包括自定义业务模块,也包括系统模块,使用 aop 机制就可以轻易对“系统模块”做逻辑扩展和变更
|
14
zhennann OP @CODEWEA 这篇文章刚好写到 bean 容器。其实,CabloyJS 还包括权限管理、数据生命周期管理(草稿->归档->历史)
,而且自带 NodeJS 工作流引擎,全新的布局自适应方案 pc=mobile+pad,可以官网或者 github 查看相关内容 |
15
zhennann OP @kidlj 谢谢。其实被围剿的感觉也不错,至少有人关注了。这只是 CabloyJS 生态其中的一篇文章而已,如果只是孤立的看这篇文章,确实没啥意义,当然也就错过了一片还算不错的森林
|
18
xcstream 2021-01-08 00:06:31 +08:00
不喜欢 java 的原因就是因为 aop
|
19
zhennann OP @xcstream 你看是否可以这样理解。aop 是一种能力( bean 容器的副作用),如果不想用就当他不存在。因此,你不喜欢 java 的原因,可能是在 java 里面,不论是 bean 组件的定义还是使用,注解漫天飞的情况。
这里提到的基于原生 js 的 bean 容器,不再需要注解。比如,系统提供了一个 bean 组件:user, 那么在项目的任何地方,只需使用`ctx.bean.user`直接引用该 bean 组件,不再需要像 java 通过注解的方式声明一个变量了 |