【前端】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低代码应用平台体验

(0)
nation的头像nation数式员工
上一篇 2023年6月20日 pm4:07
下一篇 2023年11月2日 pm1:58

相关推荐

  • 表单字段API

    FormFieldWidget 表单字段的基类,包含了表单字段通用的属性跟方法 示例 class MyFieldClass extends FormFieldWidget{ } 字段属性 属性名 说明 类型 可选值 默认值 value 当前字段的值 any – null formData 当前表单视图的数据 Object – {} rootData 跟视图的数据,如果当前只有一个视图,那么与formData是一样的 Array – [] metadataRuntimeContext 当前视图运行时的上下文,可以获取当前模型、字段、动作、视图等所有的数据 Object – – urlParameters 当前url参数 Object – – field 当前字段的元数据 Object – – model 当前模型 Object – – view 当前视图 Object – – disabled 是否禁用 Boolean – false invisible 当前字段是否不可见 Boolean – false required 当前字段是否必填,如果当前字段是在详情页,那么是false Boolean – false readonly 当前字段是否只读,如果当前字段是在详情页、搜索,那么是false Boolean – false placeholder 占位符 String – 当前字段的displayName label 字段的标题 String – 当前字段的displayName 方法 方法名 说明 参数 例子 getDsl 获取当前字段所有的配置 – change 修改当前字段的值 any focus 获取焦点触发的方法 – blur 失去焦点触发的方法 – executeValidator 执行当前字段的校验,异步的 – submit 重写当前字段的提交逻辑 – submit() { return ‘value’ } reloadActiveRecords 替换当前视图的数据 Array this.reloadActiveRecords([{code: xxx, name: 111}]) reloadRootData 替换根视图的数据 Array this.reloadRootData([{code: xxx, name: 111}])

    2023年11月15日
    85400
  • 表格如何支持表尾合计计算

    介绍 可以通过扩展TableWidget.ts实现 示例代码 import { BaseElementWidget, DslDefinitionType, SPI, TableWidget, ViewType, Widget } from '@kunlun/dependencies'; @SPI.ClassFactory( BaseElementWidget.Token({ type: ViewType.Table, widget: 'table', model: 'resource.k2.Model0000000109', viewName: '移动端品牌_TABLE_0000000000021513' }) ) export class FooterStatisticsTable extends TableWidget { public initialize(props) { if (props.template) { props.template?.widgets?.forEach((a) => { if (a.dslNodeType === DslDefinitionType.FIELD && this.statisticsFieldList.includes(a.name)) { a.statistics = true; } }); } super.initialize(props); return this; } // 需要表尾做合并的字段名称 public statisticsFieldList = ['fansNum']; @Widget.Reactive() protected get showFooter(): boolean | undefined { return true; } } 效果预览

    2024年10月14日
    66700
  • 【界面设计器】自定义字段组件实战——表格字段内嵌表格

    阅读之前 此文章为实战教程,已假定你熟悉了【界面设计器】较为完整的【自定义组件】相关内容。 如果在阅读过程中出现的部分概念无法理解,请自行学习相关内容。【前端】文章目录 业务背景 表格中的一对多(O2M)或多对多(M2M)字段使用表格展开。 演示内容:在【商品】的表格中存在【库存信息】这一列,这一列的内容通过表格展示【尺码】和【数量】两列。 业务分析及实现思路 从需求来看,我们需要实现一个【内嵌表格】组件,并且该组件允许在【表格】视图中使用。与之前不同的是,这里我们需要支持两个业务类型一对多(O2M)和多对多(M2M),即一个组件中包含两个元件。 在【内嵌表格】组件的属性面板中,我们需要再定义一个【内嵌表格配置】组件,用来选择内嵌表格中需要哪些字段进行组合,以及为每个组合提供一些基础配置。 这里需要理解一个基本概念,即【内嵌表格】的属性面板是【内嵌表格配置】的【执行页面】。所有组件的属性面板在【执行页面】时都是【表单】视图。 因此我们可以实现一个【内嵌表格配置】组件,并且该组件允许在【表单】视图中使用。其业务类型使用【文本】,我们在保存配置数据时,可以使用JSON数据结构来存储复杂结构。(这里的实现思路并非是最符合协议设定的,但可以满足绝大多数组件场景) 在【内嵌表格配置】组件中,我们可以允许用户添加/移除组合,并且每个组合有两个属性,【标题】和【字段】。 一些解释 看过【界面设计器】自定义字段组件实战——表格字段组合展示文章的读者可能很熟悉这一实现思路,会想当然的尝试将两个组件进行合并。这里我觉得有必要作出一些实现思路上的解释。 虽然在表面上看起来【组合列配置】和【内嵌表格配置】用到的属性完全一样,但在实现上,由于关联关系的查询需要在组件中特殊处理【透出字段(选项字段列表)】字段(【界面设计器】组件开发常见问题中对该属性进行了解释),才能查询到相应的关联数据。 不仅如此,这两个组件所代表的含义也完全不同。【组合列配置】是在一列中配置需要展示的字段,它在未来可能会增加【颜色(根据条件判断展示不同的颜色)】、【动作(可点击的行为)】等等诸多与之相关的属性。【内嵌表格配置】是在一列中配置表格中的多列,它在未来可能会增加【行高(控制表格行高)】、【支持排序(表格列支持排序)】等等诸多与之相关的属性。 在这里希望读者可以理解一点:相似并不代表相关。组件的抽象与归纳整理的不同点在于,抽象更需要关心其本身所代表的含义,而不是仅关注其相似程度。将多个相似度高但含义不同的组件进行归纳整理得到的只是一个含义不明,无法适应变化的组件。 因此,我们仍然使用两个不同的组件进行实现。 准备工作 此处你应该已经准备好了【商品】和【库存】两个模型,并且可以完整执行【商品】模型的全部【增删改查】操作。 业务模型定义(此处模型定义并非业务中正常使用的模型定义,仅作为演示使用) (以下仅展示本文章用到的模型字段,忽略其他无关字段。) 关联字段:-左侧表示当前模型中的字段API名称,右侧表示关联模型中的字段API名称。 商品(Item) 名称 API名称 业务类型 是否多值 长度(单值长度) 关联模型 关联字段 ID id 整数 否 – – – 编码 code 文本 否 128 – – 名称 name 文本 否 128 – – 库存信息 inventoryInfo 一对多 是 – 库存(Inventory) id – itemId 库存(Inventory) 名称 API名称 业务类型 是否多值 长度(单值长度) 关联模型 关联字段 ID id 整数 否 128 – – 商品 item 多对一 否 – 商品(Item) itemId – id 商品ID itemId 整数 否 – – – 尺码 size 文本 否 128 – – 库存 count 整数 否 – – – PS:如果是使用【模型设计器】创建这两个模型,在创建关联关系字段时必须使用双向关联,才能正确建立关联关系。 实现页面效果展示 表格视图 表单视图 库存信息配置 创建组件、元件 准备工作完成后,我们需要根据【业务背景】确定【组件】以及【元件】相关信息,并在【界面设计器】中进行创建。 以下操作过程将省略详细步骤,仅展示可能需要确认的关键页面。 创建内嵌表格组件 创建内嵌表格元件(一对多) 创建内嵌表格元件(多对多) 创建内嵌表格配置组件 创建内嵌表格配置元件 设计内嵌表格元件属性面板 (两个元件的属性面板可以完全一致) 创建tableConfig字段,并切换至【内嵌表格配置】组件。 再拖入【透出字段(选项字段列表)】,并设置为隐藏。 设计内嵌表格配置元件属性面板 启动SDK工程进行组件基本功能开发 开发步骤参考 打开【表格】视图,将【库存信息】字段的组件切换为【内嵌表格】 在属性面板中看到【内嵌表格配置】组件,并优先实现【内嵌表格配置】组件。这里的属性面板就是【内嵌表格配置】组件对应的【执行页面】。 当【内嵌表格配置】组件可以按照预先设计的数据结构正确保存tableConfig属性时,可以在【内嵌表格】组件中的props定义中直接获取该属性,接下来就可以进行【内嵌表格】组件的开发。 代码实现参考 工程结构 typing.ts export interface InlineTableConfig { key: string; label?: string; field?: string; } FieldService.ts import { GenericFunctionService, IModelField, QueryPageResult } from '@kunlun/dependencies'; export class FieldService { private static readonly FIELD_MODEL = 'base.Field'; public static async fetchFieldsByModel(model: string, filter?: string): Promise<IModelField[]> { let rsql = `model == "${model}"`; if (filter) {…

    2023年11月1日
    55800
  • 「前端」 获取当前用户信息以及语言

    概述 TopBarService 是用于管理用户信息和多语言配置的工具类,提供以下核心功能: 用户信息查询(含缓存机制) 当前语言状态获取 可用语言列表查询 语言切换操作 快速开始 // 基础使用示例 import { TopBarService } from '@kunlun/dependencies'; // 获取用户信息(自动缓存) const userInfo = await TopBarService.getUserInfo(); // 获取当前语言配置 const currentLang = await TopBarService.getCurrentLang(); // 查询支持的语言列表 const languages = await TopBarService.queryLanguageList(); // 激活指定语言 await TopBarService.activeLang('zh_CN'); API 详解 方法功能说明 方法名称 描述 参数 返回值 ​getUserInfo 获取当前用户信息(自动缓存) 无 用户信息对象 ​queryUserInfo 强制获取最新用户信息 无 最新用户信息对象 ​getCurrentLang 获取当前激活的语言配置 无 语言配置对象 / undefined ​queryLanguageList 获取所有可用语言列表 无 语言列表数组 ​activeLang 激活指定语言 语言 ID (string) 更新后的语言配置对象

    2025年3月21日
    46500
  • 上下文在字段和动作中的应用

    上下文在字段和动作中的应用 在业务场景中,常常需要在打开弹窗或跳转到新页面时携带当前页面数据。此时,我们需要配置相关「动作」中的上下文信息。 在 oinone 平台中,上下文主要分为以下三种: activeRecord:当前视图数据 rootRecord:主视图数据 openerRecord:触发弹窗的对象 参考文档:oinone内的主视图数据和当前视图数据使用介绍 activeRecord 表示当前视图的数据。例如,若动作配置在表单上,则指代当前表单的数据;若配置在 o2m、m2m 字段表格上,则指代选中的行数据。 rootRecord 表示根视图的数据。若当前视图是表单页,则代表表单的数据;若为表格页,则代表表格的数据。 openerRecord 表示触发弹窗的对象。例如,在弹窗内的字段或动作中,可通过 openerRecord 获取触发弹窗的信息。 这三者均为对象 (Object) 类型。 界面设计器配置 在 o2m、m2m 表格字段弹窗中携带当前视图数据 假设我们设计了一个包含 o2m、m2m 表格字段的表单页面。打开相关弹窗时,需将表单中的 code 数据传递至弹窗中。 选择相应的「动作」,如创建或添加。在右侧属性面板底部找到「上下文」,添加格式为对象 {} 的上下文信息。 以键值对的格式添加上下文信息:{code: rootRecord.code}。 设计弹窗时,将 code 字段拖入弹窗中。 完成设计后保存并发布。 大家可以看到,上下文中的key是 code,但是value是rootRecord.code,这里取的是rootRecord而不是activeRecord,因为我们上面讲过如果当前动作配置在o2m、m2m的字段表格上面,那么activeRecord就是表格选中的行,我们现在要取的是表单上的code字段,所以需要用rootRecord。 注意点:key需要是提交模型【前端视图】存在的字段才能传递。

    2023年11月8日
    1.4K10

Leave a Reply

登录后才能评论