【前端】IOC容器(v4)

什么是IOC容器?

IOC不是一种技术,只是一种思想,一个重要的面向对象编程的法则,它能指导我们如何设计出松耦合,更优良的程序。传统应用程序都是由我们在类内部主动创建依赖对象,从而导致类与类之间高耦合,难于测试;有了IOC容器后,把创建和查找依赖对象的控制权交给了容器,由容器进行注入组合对象,所以对象与对象之间是松散耦合,这样也方便测试,利于功能复用,更重要的使程序的整个体系结构变得非常灵活。在运行期,在外部容器动态的将依赖对象注入组件,当外部容器启动后,外部容器就会初始化。创建并管理对象实例,以及销毁,这种应用本身不负责依赖对象的创建和维护,依赖对象的创建和维护是由外部容器负责的称为控制反转

IOC(控制反转)和DI(依赖注入)

IOC(Inversion of Control, 控制反转):通过外部容器管理对象实例的一种思想。
DI(Dependency Injection, 依赖注入):IOC的一种实现方式。

作者简述

IOC是Spring框架(一种以Java为语言开发的框架)的核心,并贯穿始终。其面向接口的开发能力,使得服务调用方和服务提供方可以做到完全解耦,只要遵循接口定义的规则进行调用,具体服务的实现可以是多样化的。

对于前端,我们使用inversify进行了IOC的实现。其强大的解耦能力可以使得平台进行大量的抽象,而无需关系具体的实现。

接下来,我们将介绍IOC在开发中的基本运用。

API

为了方便起见,我们将IOC相关功能与组件SPI的调用方式放在了一起。(更高版本的平台版本将自动获得该能力)

export class SPI {
  /**
   * register singleton service
   */
  public static Service;

  /**
   * autowired service property/parameter in service
   */
  public static Autowired;

  /**
   * service construct after execute method
   */
  public static PostConstruct;

  /**
   * autowired service in widget
   */
  public static Instantiate;

  /**
   * autowired services in widget
   */
  public static Instantiates;

  /**
   * service construct after execute method in widget
   */
  public static InstantiatePostConstruct;
}

创建第一个服务

service/ProductService.ts
import { ServiceIdentifier } from '@kunlun/dependencies';

/**
 * 产品
 */
export interface Product {
  id: string;
  name: string;
}

/**
 * 产品服务
 */
export interface ProductService {
  /**
   * 获取产品列表
   */
  getProducts(): Promise<Product[]>;

  /**
   * 通过ID获取产品
   * @param id 产品ID
   */
  getProductById(id: string): Promise<Product | undefined>;
}

/**
 * 产品服务Token
 */
export const ProductServiceToken = ServiceIdentifier<ProductService>('ProductService');
service/impl/ProductServiceImpl.ts
import { SPI } from '@kunlun/dependencies';
import { Product, ProductService, ProductServiceToken } from '../ProductService';

@SPI.Service(ProductServiceToken)
export class ProductServiceImpl implements ProductService {
  public async getProducts(): Promise<Product[]> {
    // request api get products
    return [];
  }

  public async getProductById(id: string): Promise<Product | undefined> {
    // request api get product by id
    return undefined;
  }
}
service/impl/index.ts
export * from './ProductServiceImpl';
service/index.ts
export * from './impl';
export * from './ProductService';

这样,我们便创建了一个ProductService服务,它定义了获取产品列表通过ID获取产品两个方法。接下来,我们要在可能需要的地方进行调用。

Class Component(ts)中使用ProductService服务

ProductListWidget.ts
import { BaseElementWidget, SPI, Widget } from '@kunlun/dependencies';
import { ProductService, ProductServiceToken } from '../service';
import ProductList from './ProductList.vue';

@SPI.ClassFactory(BaseElementWidget.Token({ widget: 'Product' }))
export class ProductListWidget extends BaseElementWidget {
  @SPI.Instantiate(ProductServiceToken)
  protected productService: ProductService | undefined;

  public initialize(props) {
    super.initialize(props);
    this.setComponent(ProductList);
    return this;
  }

  @Widget.Method()
  public loadProducts() {
    return this.productService?.getProducts();
  }

  @Widget.Method()
  public loadProduct(id: string) {
    return this.productService?.getProductById(id);
  }
}

在其他Service实现中使用ProductService服务

OtherServiceImpl.ts
import { SPI } from '@kunlun/dependencies';
import { OtherService, OtherServiceToken } from '../OtherService';
import { ProductService, ProductServiceToken } from '../ProductService';

@SPI.Service(OtherServiceToken)
export class OtherServiceImpl implements OtherService {
  @SPI.Autowired(ProductServiceToken)
  protected productService!: ProductService;

  public async do() {
    const products = await this.productService.getProducts();
    // do something
  }
}

值得一提的是,SPI.Autowired默认要求服务必须存在,否则会直接报错。如果其不是必须的,可以使用以下方式进行定义:

@SPI.Autowired(ProductServiceToken, { required: false })
protected productService: ProductService | undefined;

其他API的使用

SPI.PostConstruct

在实例初始化后执行,仅用于服务中,无法用于组件中。

@SPI.Service(OtherServiceToken)
export class OtherServiceImpl implements OtherService {
  @SPI.Autowired(ProductServiceToken)
  protected productService!: ProductService;

  @SPI.PostConstruct()
  public init() {
    console.log('post construct init.', this.productService);
  }
}
SPI.Instantiates

SPI.Instantiate类似,不过其声明的类型为服务数组,当没有任何服务时,将获得空数组,而不是空。因此可以使用以下方式进行定义:

@SPI.Instantiates(ProductServiceToken)
protected productServices!: ProductService[];
SPI.InstantiatePostConstruct

SPI.PostConstruct类似,不同之处在于,其仅用于组件中,无法用于服务中。虽然,我们提供了这种方式,但其实际应用场景相当有限,由于实例化组件时调用的方法为类的constructor方法,其init方法紧跟其后执行,此时,并没有执行任何组件初始化,Vue生命周期等逻辑,因此其对于业务组件来说并不是有意义的。

@SPI.ClassFactory(BaseElementWidget.Token({ widget: 'Product' }))
export class ProductListWidget extends BaseElementWidget {
  @SPI.Instantiate(ProductServiceToken)
  protected productService: ProductService | undefined;

  @SPI.InstantiatePostConstruct()
  protected init() {
    console.log('post construct init', this.productService);
  }
}
SPI.RawInstantiate

该方法并非装饰器,而是可以直接在代码中静态调用的方法。其为SPI.Instantiate的具体实现,可以在任何地方执行。

const productService = SPI.Instantiate(ProductServiceToken);
SPI.RawInstantiates

SPI.RawInstantiate类似,其为SPI.Instantiates的具体实现,可以在任何地方执行。

const productServices = SPI.Instantiates(ProductServiceToken);

注意事项

  • 在使用IOC时,我们并不希望Class Component(ts)组件也被注册成服务,因此,在该类上使用SPI.Service是无效的,甚至可能会带来其他意想不到的问题。
  • SPI.Autowired只能用于其他服务,而不能用于Class Component(ts)组件。
  • SPI.Instantiate只能用于Class Component(ts)组件,而不能用于其他服务。
  • 所有服务实例均为单例,且无法修改。未来可能会提供多例的实现功能,目前从设计角度上考虑不建议使用。

这样设计的目的在于,IOC容器的性能虽然在整体上没有太大的问题,不过为了避免不必要的查找,所以我们并没有通过IOC容器来创建组件。我们需要尽可能的将查找范围最小化。

Oinone社区 作者:nation原创文章,如若转载,请注明出处:https://doc.oinone.top/frontend/52.html

访问Oinone官网:https://www.oinone.top获取数式Oinone低代码应用平台体验

Like (0)
nation's avatarnation数式员工
Previous 2023年6月20日 pm4:07
Next 2023年11月2日 pm1:58

相关推荐

  • 打开弹窗/抽屉的动作如何在弹窗关闭后扩展逻辑

    介绍 在业务中,我们可能会遇到在弹窗关闭后执行业务逻辑的场景,这个时候可以通过自定义弹窗动作来实现 注意: oinone已经内置了弹窗内的动作触发后刷新主视图、刷新当前视图、提交数据的能力,可以通过界面设计器在动作的属性面板配置,本文档为内置能力不满足需求的场景使用 场景案例 弹窗动作组件示例 import { ActionType, ActiveRecord, BaseActionWidget, DialogViewActionWidget, SPI, ViewActionTarget, DisposeEventHandler, IPopupInstance, PopupManager, RuntimeAction, } from '@kunlun/dependencies'; /** * 弹出层销毁回调 – 建议抽到工具类中 * @param popupKey 弹出层key * @param disposeEventHandler 销毁的回调 */ function popupDisposeCallback( popupKey: string, disposeEventHandler: DisposeEventHandler, ) { const innerDisposeFn = (manager: PopupManager, instance: IPopupInstance, action?: RuntimeAction) => { if (instance.key === popupKey) { disposeEventHandler?.(manager, instance, action); } PopupManager.INSTANCE.clearOnClose(innerDisposeFn); }; PopupManager.INSTANCE.onClose(innerDisposeFn); } @SPI.ClassFactory( BaseActionWidget.Token({ actionType: [ActionType.View], target: [ViewActionTarget.Dialog], model: 'resource.k2.Model0000000109', name: 'dialogActionName001' }) ) export class CustomDialogViewActionWidget extends DialogViewActionWidget { protected createPopupWidget(data: ActiveRecord[]) { super.createPopupWidget(data); popupDisposeCallback(this.dialog.getHandle(), async (manager: PopupManager, instance: IPopupInstance, action?: RuntimeAction) => { // action为触发关闭弹窗的动作,点击动作关闭弹出层该参数才有值,如果是点击遮罩背景层则无该参数 if (action?.name === 'actionName001') { // 以下为示例代码,指定name的动作关闭弹窗后刷新当前视图的数据查询 this.refreshCallChaining?.syncCall(); } }); } } 函数式调用打开弹窗的示例 以下为在自定义字段组件中手动触发打开弹窗 import { BaseFieldWidget, Dialog, DialogWidget, DisposeEventHandler, FormStringFieldSingleWidget, IPopupInstance, ModelDefaultActionName, ModelFieldType, PopupManager, RuntimeAction, RuntimeViewAction, SPI, ViewType, Widget } from '@kunlun/dependencies'; /** * 弹出层销毁回调 – 建议抽到工具类中 * @param popupKey 弹出层key * @param disposeEventHandler 销毁的回调 */ function popupDisposeCallback( popupKey: string, disposeEventHandler: DisposeEventHandler, ) { const innerDisposeFn = (manager: PopupManager, instance: IPopupInstance, action?: RuntimeAction) => { if (instance.key === popupKey) { disposeEventHandler?.(manager, instance, action); } PopupManager.INSTANCE.clearOnClose(innerDisposeFn); }; PopupManager.INSTANCE.onClose(innerDisposeFn);…

    2024年8月22日
    1.5K00
  • 表格字段配置Switch开关

    在业务开发中,我们经常会遇到表格中有个Switch开关组件: 那么如何通过界面设计器配置这个组件呢,下面让我们一起来学习下吧。 设计布尔类型的组件 1: 首先在界面设计器的组件区域添加一个组件。 2: 我们给这个组件添加一个元件,元件的配置必须跟下面的一致 3: 给元件添加属性配置 拖拽一个单行文本字段, 字段编码必须是truthyAction,代表的是该字段为true的时候要执行的动作 再拖拽一个单行文本字段, 字段编码必须是falsyAction,代表的是该字段为false的时候要执行的动作 4: 发布元件,然后再去对应的界面设计器页面,将对应的字段切换成刚刚设计的组件 5: 发布界面设计器,染红我们可以在运行时的页面看到效果 当switch切换的时候,会执行对应的action

    2023年11月21日
    2.8K00
  • oio-modal 对话框

    API 参数 说明 类型 默认值 版本 cancelText 取消按钮文字 string| slot 取消 closable 是否显示右上角的关闭按钮 boolean true closeIcon 自定义关闭图标 VNode | slot – confirmLoading 确定按钮 loading boolean 无 destroyOnClose 关闭时销毁 Modal 里的子元素 boolean false footer 底部内容,当不需要默认底部按钮时,可以设为 :footerInvisible="true" slot 确定取消按钮 getTriggerContainer 指定 Modal 挂载的 HTML 节点 (instance): HTMLElement () => document.body keyboard 是否支持键盘 esc 关闭 boolean true mask 是否展示遮罩 boolean true maskClosable 点击蒙层是否允许关闭 boolean true enterText 确认按钮文字 string 确定 title 标题 string|slot 无 visible(v-model:visible) 对话框是否可见 boolean 无 width 宽度 string|number 520 wrapClassName 对话框外层容器的类名 string – zIndex 设置 Modal 的 z-index number 1000 cancelCallback 点击遮罩层或右上角叉或取消按钮的回调, return true则关闭弹窗 function(e) enterCallback 点击确定回调 function(e)

    2023年12月18日
    2.4K00
  • 自定义字段组件如何处理vue组件内的表单校验

    介绍 本示例以字符串字段为业务场景,将输入框用element-plus的组件实现了一遍,vue组件内在onMounted生命周期内将ElForm表单实例通过ts组件内提供到props的setFormInstance方法设置到了ts组件的属性formInstance上,这样就可以在ts组件校验方法validator()触发的时候直接调用表单组件实例formInstance的校验方法validate() 适用场景 当前字段存储了动态表单的配置json,vue组件内自行实现了一套表单渲染逻辑,需要在vue组件和ts组件内同时触发校验 参考文档 element-plus表单组件文档 如何编写自定义字段组件的校验逻辑 示例代码 ts组件 import { BaseFieldWidget, FormStringFieldSingleWidget, isValidatorSuccess, ModelFieldType, SPI, ValidatorInfo, ViewType, Widget } from '@kunlun/dependencies'; import { FormInstance } from 'element-plus'; import MyFormStringField from './MyFormStringField.vue'; @SPI.ClassFactory( BaseFieldWidget.Token({ viewType: [ViewType.Form, ViewType.Search], ttype: ModelFieldType.String, widget: 'Input', model: 'resource.k2.Model0000000109', name: 'code', }) ) export class MyFormStringFieldWidget extends FormStringFieldSingleWidget { public initialize(props) { super.initialize(props); this.setComponent(MyFormStringField); return this; } /** * ElementPlus的表单vue组件实例 * @private */ private formInstance?: FormInstance; @Widget.Method() private setFormInstance(formInstance: FormInstance | undefined) { this.formInstance = formInstance; } /** * 字段校验方法 */ public async validator(): Promise<ValidatorInfo> { const validRes = await this.formInstance?.validate((valid, fields) => {}); console.log('validRes', validRes) if (!validRes) { return this.validatorError('校验失败'); } const res = await super.validator(); if (!isValidatorSuccess(res)) { return res; } if (this.value == null) { return this.validatorSuccess(); } return this.validateLength(this.value); } } vue组件 <template> <ElForm ref="formInstance" :model="model" :rules="rules"> <ElFormItem label="编码" prop="code"> <ElInput v-model="model.code" @input="onValueChange"></ElInput> </ElFormItem> </ElForm> </template> <script lang="ts"> import { defineComponent, reactive, ref, onMounted, watch } from 'vue'; import { ElForm, ElFormItem, ElInput, FormInstance } from 'element-plus'; export default defineComponent({ name: 'MyFormStringField', components: { ElForm, ElFormItem, ElInput }, props: ['value', 'setFormInstance', 'onChange'],…

    2024年9月6日
    2.3K00
  • 前端自定义组件之单页面步骤条

    本文将讲解如何通过自定义,实现单页面的步骤条组件。其中每个步骤的元素里都是界面设计器拖出来的。 实现路径 整体的实现思路是界面设计器拖个选项卡组件,自定义这个选项卡,里面的每个选项页都当成一步渲染出来,每一步的名称是选项页的标题。 1. 界面设计器拖出页面 我们界面设计器拖个选项卡组件,然后在每个选项页里拖拽任意元素。完成后点击右上角九宫格,选中选项卡,填入组件 api 名称,作用是把选项卡切换成我们自定义的步骤条组件,这里的 api 名称和自定义组件的 widget 对应。最后发布页面,并绑定菜单。 2. 组件实现 widget 组件重写了选项卡,核心函数 renderStep,通过 DslRender.render 方法渲染界面设计器拖拽的元素,每一步的 step 又是解析选卡页得到的。 import { SPI, Widget, DefaultTabsWidget, BasePackWidget, DslDefinition, DslRender, DslDefinitionType, CallChaining, customMutation } from '@oinone/kunlun-dependencies'; import { VNode } from 'vue'; import NextStepSinglePage from './NextStepSinglePage.vue'; @SPI.ClassFactory(BasePackWidget.Token({ widget: 'NextStepSinglePage' })) export class NextStepSinglePageWidget extends DefaultTabsWidget { public initialize(props) { super.initialize(props); this.setComponent(NextStepSinglePage); return this; } @Widget.Reactive() public get invisible() { return false; } // 配置的每一步名称,解析选项页的标题 @Widget.Reactive() public get titles() { return this.template?.widgets?.map((item) => item.title) || []; } // region 上一步下一步配置 // 步骤数组,数组里的元素即步骤要渲染的内容 @Widget.Reactive() public get steps(): DslDefinition[] { // 每个 tab 是一个步骤,这里会有多个步骤 // 每个步骤里有多个元素,又是数组 // 所以这里是二维数组 const tabDsls: DslDefinition[][] = this.template?.widgets.map((item) => item.widgets) || []; // 对每个步骤的子元素们,外侧包一层 row 布局,所以变回了一维数组 return tabDsls.map((tabDsl) => { return { …(this.template || {}), dslNodeType: DslDefinitionType.PACK, widgets: tabDsl, widget: 'row', resolveOptions: { mode: 1 } }; }); } // 渲染步骤,每个步骤有多个子元素 @Widget.Method() public renderStep(step: DslDefinition): VNode | undefined { return DslRender.render(step); } // region 校验相关 // 校验的钩子 @Widget.Reactive() @Widget.Inject('validatorCallChaining') protected parentValidatorCallChaining: CallChaining<boolean> | undefined; // 校验步骤表单 @Widget.Method() public async onValidator(): Promise<boolean> { const res = await this.parentValidatorCallChaining?.syncCall(); return res…

    2025年7月8日
    1.0K00

Leave a Reply

Please Login to Comment