V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
zhennann
V2EX  ›  推广

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

  •  
  •   zhennann · 2021-01-06 10:42:12 +08:00 · 2427 次点击
    这是一个创建于 1177 天前的主题,其中的信息可能已经有所发展或是发生改变。

    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

    相关链接

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