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

加油,为 Vue3 提供一个可媲美 Angular 的 ioc 容器

  •  
  •   zhennann · 110 天前 · 2444 次点击
    这是一个创建于 110 天前的主题,其中的信息可能已经有所发展或是发生改变。

    为什么要为 Vue3 提供 ioc 容器

    Vue3 因其出色的响应式系统,以及便利的功能特性,完全胜任大型业务系统的开发。但是,我们不仅要能做到,而且要做得更好。大型业务系统的关键就是解耦合,从而减缓 shi 山代码的生长。而 ioc 容器是目前最好的解耦合工具。Angular 从一开始就引入了 ioc 容器,因此在业务工程化方面一直处于领先地位,并且一直在向其他前端框架招手:“我在前面等你们,希望三年后能再见”。那么,我就试着向前走两步,在 Vue3 中引入 ioc 容器,并以此为基础扩充其他工程能力,得到一个新框架:Zova。诸君觉得是否好用,欢迎拍砖、交流:

    IOC 容器分类

    在 Zova 中有两类 ioc 容器:

    1. 全局 ioc 容器:在系统初始化时,会自动创建唯一一个全局 ioc 容器。在这个容器中创建的 Bean 实例都是单例模式
    2. 组件实例 ioc 容器:在创建 Vue 组件实例时,系统会为每一个 Vue 组件实例创建一个 ioc 容器。在这个容器中创建的 Bean 实例可以在组件实例范围之内共享数据和逻辑

    Bean Class 分类

    在 Zova 中有两类 Bean Class:

    1. 匿名 bean:使用@Local装饰的 class 就是匿名 bean。此类 bean 仅在模块内部使用,不存在命名冲突的问题,定义和使用都很便捷
    2. 具名 bean:除了@Local之外,其他装饰器函数装饰的 class 都是具名 bean。Zova 为此类 bean 提供了命名规范,既可以避免命名冲突,也有利于跨模块使用

    注入机制

    Zova 通过@Use装饰器函数注入 Bean 实例,提供了以下几种注入机制:

    1. Bean Class

    通过Bean Class在 ioc 容器中查找并注入 Bean 实例,如果不存在则自动创建。这种机制一般用于同模块注入

    import { ModelTodo } from '../../bean/model.todo.js';
    
    class ControllerTodo {
      @Use()
      $$modelTodo: ModelTodo;
    }
    

    2. Bean 标识

    通过Bean 标识在 ioc 容器中查找并注入 Bean 实例,如果不存在则自动创建。这种机制一般用于跨模块注入层级注入

    import type { ModelTabs } from 'zova-module-a-tabs';
    
    class ControllerLayout {
      @Use('a-tabs.model.tabs')
      $$modelTabs: ModelTabs;
    }
    
    • 通过a-tabs.model.tabs查找并注入 Bean 实例
    • 因此,只需导入 ModelTabs 的 type 类型,从而保持模块之间的松耦合关系

    3. 注册名

    通过注册名在 ioc 容器中查找并注入 Bean 实例,如果不存在则返回空值。这种机制一般用于同模块注入层级注入

    import type { ModelTodo } from '../../bean/model.todo.js';
    
    class ControllerTodo {
      @Use({ name: '$$modelTodo' })
      $$modelTodo: ModelTodo;
    }
    
    • 通过注册名$$modelTodo查找并注入 Bean 实例。一般而言,应该确保在 ioc 容器中已经事先注入过 Bean 实例,否则就会返回空值

    4. 属性名

    通过属性名在 ioc 容器中查找并注入 Bean 实例,如果不存在则返回空值。这种机制一般用于同模块注入层级注入

    import type { ModelTodo } from '../../bean/model.todo.js';
    
    class ControllerTodo {
      @Use()
      $$modelTodo: ModelTodo;
    }
    
    • 通过属性名$$modelTodo查找并注入 Bean 实例。一般而言,应该确保在 ioc 容器中已经事先注入过 Bean 实例,否则就会返回空值

    注入范围

    匿名 bean的默认注入范围都是ctx具名 bean可以在定义时指定默认注入范围,不同的场景(scene)有不同的默认注入范围。 此外,在实际注入时,还可以在 @Use 中通过containerScope选项覆盖默认的注入范围

    Zova 提供了以下几种注入范围:app/ctx/new/host/skipSelf

    1. app

    如果注入范围是 app ,那么就在全局 ioc 容器中注入 bean 实例,从而实现单例的效果

    // in module: test-module1
    @Store()
    class StoreCounter {}
    
    // in module: test-module2
    import type { StoreCounter } from 'zova-module-test-module1';
    
    class Test {
      @Use('test-module1.store.counter')
      $$storeCounter: StoreCounter;
    }
    
    • Store 的注入范围默认是 app ,因此通过 Bean 标识test-module1.store.counter在全局 ioc 容器中查找并注入 bean 实例

    2. ctx

    如果注入范围是 ctx ,那么就在当前组件实例的 ioc 容器中注入 bean 实例

    // in module: a-tabs
    @Model()
    class ModelTabs {}
    
    // in module: test-module2
    import type { ModelTabs } from 'zova-module-a-tabs';
    
    class ControllerLayout {
      @Use('a-tabs.model.tabs')
      $$modelTabs: ModelTabs;
    }
    
    • Model 的注入范围默认是 ctx ,因此通过 Bean 标识a-tabs.model.tabs在当前组件实例的 ioc 容器中查找并注入 bean 实例

    3. new

    如果注入范围是 new ,那么就直接创建新的 bean 实例

    // in module: a-tabs
    @Model()
    class ModelTabs {}
    
    // in module: test-module2
    import type { ModelTabs } from 'zova-module-a-tabs';
    
    class ControllerLayout {
      @Use({ beanFullName: 'a-tabs.model.tabs', containerScope: 'new' })
      $$modelTabs: ModelTabs;
    }
    
    • 由于指定 containerScope 选项为 new ,因此通过 Bean 标识a-tabs.model.tabs直接创建新的 bean 实例

    层级注入

    注入范围除了支持app/ctx/new,还支持层级注入:host/skipSelf

    4. host

    如果注入范围是 host ,那么就在当前组件实例的 ioc 容器以及所有父容器中依次查找并注入 bean 实例,如果不存在则返回空值

    // in parent component
    import type { ModelTabs } from 'zova-module-a-tabs';
    
    class Parent {
      @Use('a-tabs.model.tabs')
      $$modelTabs: ModelTabs;
    }
    
    // in child component
    import type { ModelTabs } from 'zova-module-a-tabs';
    
    class Child {
      @Use({ containerScope: 'host' })
      $$modelTabs: ModelTabs;
    }
    
    • 由于父组件已经注入了 ModelTabs 的 bean 实例,因此子组件可以直接查找并注入
    • 层级注入同样支持所有注入机制:Bean Class/Bean 标识/注册名/属性名

    5. skipSelf

    如果注入范围是 skipSelf ,那么就在所有父容器中依次查找并注入 bean 实例,如果不存在则返回空值

    Zova 已开源: https://github.com/cabloy/zova

    29 条回复    2024-10-08 10:17:10 +08:00
    lisongeee
        1
    lisongeee  
       109 天前   ❤️ 1
    我建议先看 1 坤月之前发布在掘金的相同文章的评论

    https://juejin.cn/post/7369113568573292556#comment
    zhennann
        2
    zhennann  
    OP
       109 天前
    @lisongeee 不理解你要表达的是什么
    LuckyLauncher
        3
    LuckyLauncher  
       109 天前
    Vue/React 社区为什么不引入 IoC 容器是有原因的

    在前端框架领域没有 IoC 的占有率反而吊打有 IoC 的,前端真的需要 IoC 吗
    wuyiccc
        4
    wuyiccc  
       109 天前
    node 后台用 ioc 可以理解,前端页面为什么要用 ioc 呢,会想 springboot 一样随便打开一个页面就会扫描所有的 class 么?
    shimada666
        5
    shimada666  
       109 天前
    qrobot
        6
    qrobot  
       109 天前
    @wuyiccc 你还别说, 我还真做了一个这样的东西, 叫做 componentScan
    qrobot
        7
    qrobot  
       109 天前
    @qrobot 特定场景下 componentScan 很好用, 但是 OP 的 IoC 就有点本末倒置
    zhennann
        8
    zhennann  
    OP
       109 天前
    @wuyiccc 在 Zova 中,装饰过的 class 在初始化时就自动注册到系统中了,不需要扫描
    zhennann
        9
    zhennann  
    OP
       109 天前
    @qrobot 前端是异步体系,许多模块都是按需异步加载的,采用 componentScan 不能解决所有问题。在 Zova 中,装饰过的 class 在初始化时就自动注册到系统中了,不需要扫描
    calmbinweijin
        10
    calmbinweijin  
       109 天前
    @lisongeee 感谢,这边看见了《我们团队是如何用好 vue3 setup 组合式 API 的?》感觉很实用,之前一直以 vue3 的组合式 API 开发 vue2 的选项式代码。感觉才有点理解了组合式 API
    ZGame
        11
    ZGame  
       109 天前
    @zhennann 对于 vue 来说 ioc 没有意义, 我可以使用他自身或者第三方的 store,他还可以构造 dispatch action 等概念。等于原生 js 或者 ts 来说 那第三方的 ioc 可选择余地就更多了。 而如果你希望引入 class 概念对于业务逻辑进行封装... emmm, 其实也没错, 但是 vue 目前趋势是 hooks+对应 api 去解耦 和限制数据。 所以你的 ioc 来说,是某种程度上个可以代替相关逻辑 但是契合度太低了
    ZGame
        12
    ZGame  
       109 天前
    scope 概念很好 但是感觉 class 契合度太低了。
    zhennann
        13
    zhennann  
    OP
       108 天前
    @ZGame 为什么 class 契合度太低了?可否再详细说说?
    ZGame
        14
    ZGame  
       108 天前
    @zhennann 因为 hooks 某种程度上就是通过函数式的方式代替 class 的业务逻辑的。而且因为有约束处理的好的话 入参出参 副作用这些一定程度上能代替 class,而且如果我不用到 scope 的话 为啥我不直接用第三方的 store 来存储数据 这样数据流单向而且更清晰, 所以我觉得除非有一些案例,能很简单的说非这样注入不可的,那我觉得很难引入到里面。 如果纯 js, ts 底层库的话,而不关联 vue,react 这类框架使用的话 ,应该有一定价值
    KuroNekoFan
        15
    KuroNekoFan  
       108 天前
    太可怕了,java 人
    juzisang
        16
    juzisang  
       108 天前   ❤️ 1
    很多页面仔根本不需要也不理解什么是大型项目,什么是解耦合,也没什么业务逻辑要写。
    页面里最复杂的逻辑基本都是做做动效,拉拉数据完了,强行加入 ioc 确实是增加复杂度。
    juzisang
        17
    juzisang  
       108 天前
    vue 这些框架,感觉只是提供了一个视图层面的规范和约束,但是在业务逻辑方面,没啥最佳实践。
    hook 和 class 在代码在组织业务逻辑方面,感觉没啥区别,只是写法不同而已。
    反倒是 class 实际更契合 js 的语法特性,写起来也更方便。

    在维护一个长期项目上,由于前端框架更迭太快了,时间跨度可能有好几年。
    这个时候,耦合太多这些视图框架的特性在业务代码里,后续想更新甚至更换视图框架,都会很麻烦。
    WispZhan
        18
    WispZhan  
       108 天前
    @juzisang 说的挺中肯

    对于大部分前端切图仔而言,完全没必要用 IoC ,甚至他们都不理解为啥要弄得这么复杂。
    novaline
        19
    novaline  
       108 天前
    强行 IoC
    qrobot
        20
    qrobot  
       108 天前
    @zhennann #9L

    举个例子
    ```
    import c1 from "xxx";


    class Demo {
    @Autowired
    private: b2;
    }

    class Demo2 {

    }
    ```
    qrobot
        21
    qrobot  
       108 天前
    与以下的本质上有什么区别?

    ```
    class Demo2 {
    constructor(b2) {
    this.b2 = b2
    }
    }
    ```

    相对于 IoC, 这几点非常蛋疼


    1. 会导致 tree shaking 完全失效
    2. 多一个 runtime 开销
    3. 增加调试的复杂度
    unco020511
        22
    unco020511  
       107 天前
    前端需要 ioc 的理由是什么?
    gogozs
        23
    gogozs  
       107 天前
    JAVA ptsd 了
    zhennann
        24
    zhennann  
    OP
       107 天前
    @qrobot ts 与 java 装饰器的不同:ts 装饰器不仅仅是装饰,而且可以在代码初始化时,执行一段初始化逻辑,从而主动在系统中注册资源。而 java 装饰器没有这个主动初始化的阶段,因此需要扫描

    1. Zova 提供了模块化体系,以模块为单位实现独立的打包,从而也是以模块为单位实现异步加载。这确实存在 tree shaking 失效的问题,但是可以避免打包产物碎片化严重的问题,同时也能避免初始包过大的问题。对于小项目,tree shaking 可能优先于碎片化,对于中大项目,碎片化和初始包大小可能优先于 tree shaking 。这是一个 trade-off 问题
    2. 多一个 runtime 开销是否值得,也和项目的规模有关
    3. 调试是否复杂跟代码结构有关。Zova 提供了更多的代码规范,代码更加清晰,或许更容易调试一些。反之,原始的 Vue3 并没有对业务架构做出更多的约定,也没有提供现成的最佳实践,代码风格反而难以统一。
    zhennann
        25
    zhennann  
    OP
       107 天前
    @unco020511 请参见这篇文档:为什么需要 Vue3+IOC: https://zova.js.org/zh/guide/start/why.html
    zhennann
        26
    zhennann  
    OP
       107 天前
    @gogozs Zova 与 Java 的代码风格有显著的不同,体现在以下两个方面:
    1. 更少的装饰器函数:Zova 采用依赖注入与依赖查找相结合的策略,优先使用依赖查找,从而大量减少装饰器函数的使用
    2. 更少的类型标注:Zova 优先使用依赖查找可以达到化类型于无形的开发体验,也就是不需要标注类型就可以享受到类型编程的诸多好处,从而让我们的代码始终保持简洁和优雅,进而显著提升开发效率,保证代码质量
    qrobot
        27
    qrobot  
       75 天前
    @zhennann 你说了这么多我实在是看不到任何优点, 无非是把 Spring 这一套强行拿到 前端来, 这非常过度设计.

    因为项目足够大, 一个 runtime 的开销非常恐怖, 你自己看看, 现在主流的都是想着怎么去 runtime , 反而你还在里面加 runtime 反其道而行之


    webpack 之初就是为了 tree shaking 和 code splitting, 现在你把 tree shaking 的功能完全丢弃了.

    其次你的想法很好, 站在项目工程角度上来考虑这个事情, 这些东西我觉得这完全是将简单的东西复杂化.
    qrobot
        28
    qrobot  
       75 天前
    @zhennann 两年之后你可以在回过头来看我说的这句话, 越简单的代码往往越可靠
    zhennann
        29
    zhennann  
    OP
       41 天前
    @qrobot 小型项目与大型项目的诉求不同,对框架设计的要求也就不同。对于大型项目而言,通过一个精炼的 runtime 把常用的开发范式内聚成一个核心,不仅有利于规范团队开发,也可以大量减少重复性代码,让精力更加聚焦于业务领域本身。我这里可以举两个例子:
    第一个例子:在实际开发当中,会遇到三个场景的状态共享:组件内部状态共享、组件之间状态共享、全局状态共享。在传统的 Vue3 当中,分别采用不同的机制来实现,而在 Zova 中只需要采用统一的 IOC 容器机制即可。参见:[简洁而强大的 IOC 容器]( https://zova.js.org/zh/guide/essentials/ioc/introduction.html)
    第二个例子:在实际开发当中,会遇到四种全局状态数据:异步数据(一般来自服务端)、同步数据。同步数据又分为三种:localstorage 、cookie 、内存。在传统的 Vue3 当中,分别采用不同的机制来处理这些状态数据,那么可否采用统一的机制进行管理呢?此外,对于大型项目,用户需要长时间进行界面交互的场景,如果存在过多的全局状态数据,就会导致内存占用过多,有什么破解之道呢? Zova 提供的 Model 机制可以用更优雅、更简洁的代码解决以上问题,参见:[Model: 统一数据源]( https://zova.js.org/zh/guide/techniques/model/introduction.html)
    此外,经过近半年的进化,Zova 的整体架构得到进一步精简,并且提供了 VSCode 插件,通过右键菜单提供大量工具,显著提升开发体验,包括四大类能力:Create/Init/Refactor/Tools 。若有空可以一试。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   5584 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 24ms · UTC 09:03 · PVG 17:03 · LAX 01:03 · JFK 04:03
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.