【前端】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

相关推荐

  • 【前端】工程结构最佳实践(v4)

    阅读之前 你应该: 了解node与npm相关内容 了解lerna包管理工具的相关内容 官方文档 了解git仓库的相关内容 了解rollup的相关内容 工程结构包示例 Vue项目结构包下载 工程结构详解 工程结构 ├── packages │   ├── kunlun-boot │   │   ├── package.json │   │   ├── public │   │   │   ├── favicon.ico │   │   │   └── index.html │   │   ├── src │   │   │   ├── main.ts │   │   │   └── shim-vue.d.ts │   │   ├── tsconfig.json │   │   └── vue.config.js │   ├── kunlun-module-demo │   │   ├── scripts │   │   │   ├── postpublish.js │   │   │   └── prepublish-only.js │   │   ├── src │   │   │   ├── index.ts │   │   │   └── shim-vue.d.ts │   │   ├── index.ts │   │   ├── package.json │   │   ├── rollup.config.js │   │   └── tsconfig.json │   └── kunlun-modules-demo │   ├── scripts │   │   ├── build.config.js │   │   ├── postpublish.js │   │   └── prepublish-only.js │   ├── packages │   │   ├── module-demo1 │   │   │   ├── index.ts │   │   │   ├── package.json │   │   │   ├── rollup.config.js │   │   │   └── src │   │   │   ├── index.ts │   │   │   └── shim-vue.d.ts │   │   ├── module-demo2 │   │   │   ├── index.ts │   │   │   ├── package.json │   │   │   ├── rollup.config.js │   │   │  …

    前端 2023年11月1日
    1.6K00
  • 自定义组件之手动渲染任意视图(v4)

    private metadataViewWidget: MetadataViewWidget | null | undefined; private async renderCustomView(model: string, viewName: string, slotName?: string) { const view = await ViewCache.get(model, viewName); if (!view) { return; } if (this.metadataViewWidget) { this.metadataViewWidget.dispose(); this.metadataViewWidget = null; } const metadataViewWidget = this.createWidget(MetadataViewWidget, slotName, { metadataHandle: this.metadataHandle, rootHandle: this.rootHandle, internal: true, inline: true, automatic: true }); this.metadataViewWidget = metadataViewWidget; metadataViewWidget.initContextByView(view); this.forceUpdate(); }

    2025年3月6日
    1.9K00
  • 自定义视图组件(v4)

    阅读之前 你应该: 了解DSL相关内容。母版-布局-DSL 渲染基础(v4) 了解SPI机制相关内容。组件SPI机制(v4) 什么是视图组件 我们将一个视图中提供数据源的组件称为视图组件。 下面,我们将根据提供的示例布局进行进一步介绍。 示例布局(默认表格视图布局) <view type="TABLE"> <pack widget="group"> <view type="SEARCH"> <element widget="search" slot="search" /> </view> </pack> <pack widget="group" slot="tableGroup"> <element widget="actionBar" slot="actionBar"> <xslot name="actions" /> </element> <element widget="table" slot="table"> <element widget="expandColumn" slot="expandRow" /> <xslot name="fields" /> <element widget="rowActions" slot="rowActions" /> </element> </pack> </view> view: 视图标签;一个视图中的所有组件将共享数据源,视图的数据源通过视图组件进行提供。(在这个示例中,该视图的数据源通过widget="table"(TableWidget)提供) pack: 容器组件标签; element: 通用元素组件标签; xslot:dsl插槽; 根据标签性质,我们可以将这个示例布局进一步简化,只留下我们目前要关注的主要内容。 <view type="TABLE"> <element widget="table" slot="table"> <xslot name="fields" /> </element> </view> 在以上示例布局中,有且仅有一个组件会向视图提供数据源,那就是widget="table"(TableWidget)这个组件。我们接下来将对这个组件进行自定义,以实现业务中所需的列表(List)数据源展示方式。 1 平台组件简介 平台提供的基础组件有如下几种: 组件名称 描述 BaseElement element标签通用组件 BaseElementViewWidget 通用视图组件 BaseElementObjectViewWidget 对象(Object)数据源通用视图组件 BaseElementListViewWidget 列表(List)数据源通用组件 平台提供的内置组件有如下几种:(均使用element标签) 组件名称 标签 视图类型 描述 TableWidget widget="table" TABLE 内置表格组件 FormWidget widget="form" FORM 内置表单组件 DetailWidget widget="detail" DETAIL 内置详情组件 GallertWidget widget="gallery" GALLERY 内置画廊组件 TreeWidget/CardCascaderWidget widget="tree/cardCascader" TREE 内置树/卡片级联组件 我们可以根据业务场景,继承不同的组件,来实现自己的业务场景。在自定义过程中,我们建议尽可能的将逻辑控制在组件内部。如果场景是唯一且确定的,也可以进行一些特殊逻辑处理。 2 场景:实现一个虚拟滚动表格(不使用分页器) 2.1 确定组件名称 widget="VirtualTable" 通过布局设置自定义组件名称 我们将原表格布局中的widget="table"改为我们所需要的自定义组件名称即可。 多个视图可以绑定同一个布局,所以这种修改方式更适用于大范围使用相同布局的情况。 <view type="TABLE"> <pack widget="group"> <view type="SEARCH"> <element widget="search" slot="search /> </view> </pack> <pack widget="group" slot="tableGroup"> <element widget="actionBar" slot="actionBar> <xslot name="actions" /> </element> <element widget="VirtualTable" slot="table"> <element widget="expandColumn" slot="expandRow" /> <xslot name="fields" /> <element widget="rowActions" slot="rowActions" /> </element> </pack> </view> 通过DSL设置自定义组件名称 我们使用了slot="table"这个插槽,通过属性合并覆盖的方式,在DSL上直接指定我们所需要的自定义组件名称即可。 这种修改方式更适用于个别几个视图需要使用该组件的情况。 <view type="TABLE"> <template slot="table" widget="VirtualTable"> …… </template> </view> 2.2 简单实现一个基础功能的虚拟滚动表格 定义一个VirtualTable.vue文件,使用平台提供的oio-table组件。目前内部采用vxe-table封装,相关api文档 点击查看 props定义: showDataSource: 当前展示数据;通过平台内置BaseElementListViewWidget组件提供。 <template> <oio-table ref="table" border show-overflow height="400" :row-config="{ isHover: true…

    2023年11月1日
    1.8K00
  • 自定义表格支持合并或列、表头分组

    本文将讲解如何通过自定义实现表格支持单元格合并和表头分组。 点击下载对应的代码 在学习该文章之前,你需要先了解: 1: 自定义视图2: 自定义视图、字段只修改 UI,不修改数据和逻辑3: 自定义视图动态渲染界面设计器配置的视图、动作 1. 自定义 widget 创建自定义的 MergeTableWidget,用于支持合并单元格和表头分组。 // MergeTableWidget.ts import { BaseElementWidget, SPI, ViewType, TableWidget, Widget, DslRender } from '@kunlun/dependencies'; import MergeTable from './MergeTable.vue'; @SPI.ClassFactory( BaseElementWidget.Token({ viewType: ViewType.Table, widget: 'MergeTableWidget' }) ) export class MergeTableWidget extends TableWidget { public initialize(props) { super.initialize(props); this.setComponent(MergeTable); return this; } /** * 表格展示字段 */ @Widget.Reactive() public get currentModelFields() { return this.metadataRuntimeContext.model.modelFields.filter((f) => !f.invisible); } /** * 渲染行内动作VNode */ @Widget.Method() protected renderRowActionVNodes() { const table = this.metadataRuntimeContext.viewDsl!; const rowAction = table?.widgets.find((w) => w.slot === 'rowActions'); if (rowAction) { return rowAction.widgets.map((w) => DslRender.render(w)); } return null; } } 2. 创建对应的 Vue 组件 定义一个支持合并单元格与表头分组的 Vue 组件。 <!– MergeTable.vue –> <template> <vxe-table border height="500" :column-config="{ resizable: true }" :merge-cells="mergeCells" :data="showDataSource" @checkbox-change="checkboxChange" @checkbox-all="checkedAllChange" > <vxe-column type="checkbox" width="50"></vxe-column> <!– 渲染界面设计器配置的字段 –> <vxe-column v-for="field in currentModelFields" :key="field.name" :field="field.name" :title="field.label" ></vxe-column> <!– 表头分组 https://vxetable.cn/v4.6/#/table/base/group –> <vxe-colgroup title="更多信息"> <vxe-column field="role" title="Role"></vxe-column> <vxe-colgroup title="详细信息"> <vxe-column field="sex" title="Sex"></vxe-column> <vxe-column field="age" title="Age"></vxe-column> </vxe-colgroup> </vxe-colgroup> <vxe-column title="操作" width="120"> <template #default="{ row, $rowIndex }"> <!– 渲染界面设计器配置的行内动作 –> <row-action-render :renderRowActionVNodes="renderRowActionVNodes" :row="row" :rowIndex="$rowIndex" :parentHandle="currentHandle" ></row-action-render> </template> </vxe-column> </vxe-table> <!– 分页 –> <oio-pagination :pageSizeOptions="pageSizeOptions" :currentPage="pagination.current"…

    2025年1月9日
    2.6K00
  • 主题设置-设置表格全局样式

    在启动工程的main.ts通过主题配置表格全局样式 registerThemeItem('demoTheme', { 'table-config': { // 是否带有边框 default(默认), full(完整边框), outer(外边框), inner(内边框), none(无边框) border: 'full', // 是否带有斑马纹 stripe: false, // 当鼠标点击行时,是否要高亮当前行 isCurrent: true } as Partial<TableThemeConfig> }); VueOioProvider( { browser: { title: 'Oinone – 构你想象!', favicon: 'https://pamirs.oss-cn-hangzhou.aliyuncs.com/pamirs/image/default_favicon.ico' }, theme: ['demoTheme'], // 其他代码 });

    2024年8月2日
    1.5K00

Leave a Reply

Please Login to Comment