为 Vue3 提供一个可媲美 Angular 的 ioc 容器,Nodejs 开发者来助力

发布于 1周前 作者 caililin 来自 nodejs/Nestjs

为什么要为 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](/user/Local)装饰的 class 就是匿名 bean。此类 bean 仅在模块内部使用,不存在命名冲突的问题,定义和使用都很便捷
  2. 具名 bean:除了[@Local](/user/Local)之外,其他装饰器函数装饰的 class 都是具名 bean。Zova 为此类 bean 提供了命名规范,既可以避免命名冲突,也有利于跨模块使用

注入机制

Zova 通过[@Use](/user/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](/user/Use) 中通过containerScope选项覆盖默认的注入范围

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

1. app

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

// in module: test-module1
[@Store](/user/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](/user/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](/user/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](/user/Use)({ containerScope: 'host' })
  $$modelTabs: ModelTabs;
}
  • 由于父组件已经注入了 ModelTabs 的 bean 实例,因此子组件可以直接查找并注入
  • 层级注入同样支持所有注入机制:Bean Class/Bean 标识/注册名/属性名

5. skipSelf

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

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


为 Vue3 提供一个可媲美 Angular 的 ioc 容器,Nodejs 开发者来助力

30 回复

我建议先看 1 坤月之前发布在掘金的相同文章的评论

https://juejin.cn/post/7369113568573292556#comment


不理解你要表达的是什么

Vue/React 社区为什么不引入 IoC 容器是有原因的

在前端框架领域没有 IoC 的占有率反而吊打有 IoC 的,前端真的需要 IoC 吗

node 后台用 ioc 可以理解,前端页面为什么要用 ioc 呢,会想 springboot 一样随便打开一个页面就会扫描所有的 class 么?

你还别说, 我还真做了一个这样的东西, 叫做 componentScan

特定场景下 componentScan 很好用, 但是 OP 的 IoC 就有点本末倒置

在 Zova 中,装饰过的 class 在初始化时就自动注册到系统中了,不需要扫描

前端是异步体系,许多模块都是按需异步加载的,采用 componentScan 不能解决所有问题。在 Zova 中,装饰过的 class 在初始化时就自动注册到系统中了,不需要扫描

感谢,这边看见了《我们团队是如何用好 vue3 setup 组合式 API 的?》感觉很实用,之前一直以 vue3 的组合式 API 开发 vue2 的选项式代码。感觉才有点理解了组合式 API

对于 vue 来说 ioc 没有意义, 我可以使用他自身或者第三方的 store,他还可以构造 dispatch action 等概念。等于原生 js 或者 ts 来说 那第三方的 ioc 可选择余地就更多了。 而如果你希望引入 class 概念对于业务逻辑进行封装… emmm, 其实也没错, 但是 vue 目前趋势是 hooks+对应 api 去解耦 和限制数据。 所以你的 ioc 来说,是某种程度上个可以代替相关逻辑 但是契合度太低了

scope 概念很好 但是感觉 class 契合度太低了。

为什么 class 契合度太低了?可否再详细说说?

因为 hooks 某种程度上就是通过函数式的方式代替 class 的业务逻辑的。而且因为有约束处理的好的话 入参出参 副作用这些一定程度上能代替 class,而且如果我不用到 scope 的话 为啥我不直接用第三方的 store 来存储数据 这样数据流单向而且更清晰, 所以我觉得除非有一些案例,能很简单的说非这样注入不可的,那我觉得很难引入到里面。 如果纯 js, ts 底层库的话,而不关联 vue,react 这类框架使用的话 ,应该有一定价值

太可怕了,java 人

很多页面仔根本不需要也不理解什么是大型项目,什么是解耦合,也没什么业务逻辑要写。
页面里最复杂的逻辑基本都是做做动效,拉拉数据完了,强行加入 ioc 确实是增加复杂度。

vue 这些框架,感觉只是提供了一个视图层面的规范和约束,但是在业务逻辑方面,没啥最佳实践。
hook 和 class 在代码在组织业务逻辑方面,感觉没啥区别,只是写法不同而已。
反倒是 class 实际更契合 js 的语法特性,写起来也更方便。

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

说的挺中肯

对于大部分前端切图仔而言,完全没必要用 IoC ,甚至他们都不理解为啥要弄得这么复杂。

#9L

举个例子
<br>import c1 from "xxx";<br><br><br>class Demo {<br> <br> private: b2;<br>}<br><br>class Demo2 {<br><br>}<br>

与以下的本质上有什么区别?

<br>class Demo2 {<br> constructor(b2) {<br> this.b2 = b2<br> }<br>}<br>

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


1. 会导致 tree shaking 完全失效
2. 多一个 runtime 开销
3. 增加调试的复杂度

前端需要 ioc 的理由是什么?

ts 与 java 装饰器的不同:ts 装饰器不仅仅是装饰,而且可以在代码初始化时,执行一段初始化逻辑,从而主动在系统中注册资源。而 java 装饰器没有这个主动初始化的阶段,因此需要扫描

1. Zova 提供了模块化体系,以模块为单位实现独立的打包,从而也是以模块为单位实现异步加载。这确实存在 tree shaking 失效的问题,但是可以避免打包产物碎片化严重的问题,同时也能避免初始包过大的问题。对于小项目,tree shaking 可能优先于碎片化,对于中大项目,碎片化和初始包大小可能优先于 tree shaking 。这是一个 trade-off 问题
2. 多一个 runtime 开销是否值得,也和项目的规模有关
3. 调试是否复杂跟代码结构有关。Zova 提供了更多的代码规范,代码更加清晰,或许更容易调试一些。反之,原始的 Vue3 并没有对业务架构做出更多的约定,也没有提供现成的最佳实践,代码风格反而难以统一。

请参见这篇文档:为什么需要 Vue3+IOC: https://zova.js.org/zh/guide/start/why.html

Zova 与 Java 的代码风格有显著的不同,体现在以下两个方面:
1. 更少的装饰器函数:Zova 采用依赖注入与依赖查找相结合的策略,优先使用依赖查找,从而大量减少装饰器函数的使用
2. 更少的类型标注:Zova 优先使用依赖查找可以达到化类型于无形的开发体验,也就是不需要标注类型就可以享受到类型编程的诸多好处,从而让我们的代码始终保持简洁和优雅,进而显著提升开发效率,保证代码质量

你说了这么多我实在是看不到任何优点, 无非是把 Spring 这一套强行拿到 前端来, 这非常过度设计.

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


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

其次你的想法很好, 站在项目工程角度上来考虑这个事情, 这些东西我觉得这完全是将简单的东西复杂化.

两年之后你可以在回过头来看我说的这句话, 越简单的代码往往越可靠

小型项目与大型项目的诉求不同,对框架设计的要求也就不同。对于大型项目而言,通过一个精炼的 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 。若有空可以一试。

为 Vue3 提供一个可媲美 Angular 的 IoC(控制反转)容器,我们可以利用 Node.js 的模块系统以及一些流行的依赖注入库(如 inversifyjs,尽管它主要用于 TypeScript,但原理相似)来实现一个基础的 IoC 容器。以下是一个简化版的实现思路,展示如何在 Vue3 中使用 IoC 容器进行依赖注入。

首先,我们需要一个基础的服务类和一个容器类:

// services/myService.js
class MyService {
  getData() {
    return 'Hello from MyService!';
  }
}

// iocContainer.js
class IoCContainer {
  constructor() {
    this.services = {};
  }

  register(name, service) {
    this.services[name] = new service();
  }

  get(name) {
    return this.services[name];
  }
}

// main.js (Vue3 entry point)
import { createApp } from 'vue';
import App from './App.vue';
import { IoCContainer } from './iocContainer';
import MyService from './services/myService';

const container = new IoCContainer();
container.register('myService', MyService);

const app = createApp(App);
app.config.globalProperties.$container = container;

app.mount('#app');

在 Vue 组件中,你可以通过 this.$container.get('myService') 来获取服务实例:

export default {
  mounted() {
    const myService = this.$container.get('myService');
    console.log(myService.getData()); // Outputs: Hello from MyService!
  }
}

这个简化版示例展示了如何在 Vue3 中实现一个基本的 IoC 容器。实际应用中,可能需要更复杂的注册和解析逻辑,以及支持装饰器、异步服务等高级特性。

回到顶部