前端 SPI 注册 + 渲染

在阅读本篇文章之前,您需要学习以下知识点:

1: TS 结合 Vue 实现动态注册和响应式管理

前端开发者在使用 oinone 平台的时候会发现,不管是自定义字段还是视图,对应的 typescript 都会用到@SPI.ClassFactory(参数),然后在对用的class中重写initialize方法`:

@SPI.ClassFactory(参数)
export class CustomClass extends xxx {
  public initialize(props) {
    super.initialize(props);
    this.setComponent(FormString);
    return this;
  }
}

本文将带您熟悉 oinone 前端的 SPI 注册机制以及 TS + Vue 的渲染过程。

不管是自定义字段还是视图@SPI.ClassFactory(参数)都是固定写法,区别在于参数不同,这篇文章里面详细描述了参数的定义

SPI 注册机制

有自定义过字段、视图经验的开发者可能会发现,字段(表单字段)SPI 注册用的是FormFieldWidget.Token生成对应的参数,视图 SPI 注册用的是BaseElementWidget.Token,那么为什么要这样定义呢?

大家可以想象成现在有一个大的房子,房子里面有很多房间,每个房间都有自己的名字,比如FormFieldWidget就是房间的名字,BaseElementWidget也是房间的名字,这样一来我们就可以根据不同的房间存放不同的东西。

下面给大家展示下伪代码实现:


class SPI {
  static container = new Map<string, WeakMap<object, object>>()

  static ClassFactory(token) {
    return (target) => {
      if(!SPI.container.get(token.type)) {
        SPI.container.set(token.type, new WeakMap())
      }

      const services = SPI.container.get(token.type)
      services?.set(token, target)
    }
  }
}

class FormFieldWidget {
  static Token(options) {
    return {
      ...options,
      type: 'Field'
    }
  }

  static Selector(options) {
   const fieldWidgets =  SPI.container.get('Field')

   if(fieldWidgets) {
    return fieldWidgets.get(options)!
   }

   return null
  }
}

@SPI.ClassFactory(FormFieldWidget.Token({
  viewType: 'Form',
  ttype: 'String',
  widget: 'Input'
}))
class StringWidget {
}

// 字段元数据
const fieldMeta = {
  name: "name",
  field: "name",
  mode: 'demo.model',
  widget: 'Input',
  ttype: 'String',
  viewType: 'Form'
}

// 找到对应的widget
const widget = FormFieldWidget.Selector({
  viewType: fieldMeta.viewType,
  ttype: fieldMeta.ttype,
  widget: fieldMeta.widget,
})

在上述代码中,我们主要是做了这么写事情:

1.SPI class

class SPI {
  static container = new Map<string, WeakMap<object, object>>()
}
  • SPI 类是一个静态类,用于管理服务的注册和获取。
  • container 是一个静态属性,类型是 Map,它的键是字符串,值是 WeakMap。这个结构允许我们为每个服务类型(例如,Field)管理多个服务实例。

2.ClassFactory 方法

  static ClassFactory(token) {
    return (target) => {
      if (!SPI.container.get(token.type)) {
        SPI.container.set(token.type, new WeakMap())
      }

      const services = SPI.container.get(token.type)
      services?.set(token, target)
    }
  }
  • ClassFactory 是一个静态方法,接受一个 token 作为参数,返回一个函数.
  • 当这个返回的函数被调用时,它会检查 SPI.container 中是否存在对应 token.type 的条目:
    • 如果不存在,则创建一个新的 WeakMap 并将其存入 container 中。
  • 然后,它会从 container 中获取该服务类型的 WeakMap 并将 tokentarget(即 clas)存入其中。这样,服务的注册就完成了。

3.FormFieldWidget class

class FormFieldWidget {
  static Token(options) {
    return {
      ...options,
      type: 'Field'
    };
  }
}
  • FormFieldWidget 是一个用于定义表单字段的类。
  • Token 是一个静态方法,接受 options 作为参数,返回一个对象,并在其中添加 type 属性,默认为 'Field'。这个方法用于创建服务的唯一标识。
  static Selector(options) {
    const fieldWidgets = SPI.container.get('Field')

    if (fieldWidgets) {
      return fieldWidgets.get(options)!
    }

    return null
  }
  • Selector 是一个静态方法,用于从 SPI.container 中获取与给定 options 对应的字段 Widget。
  • 它首先获取 containerField 类型的 WeakMap,然后尝试获取与 options 关联的实例。如果找到了,返回该实例;否则返回 null。

4. 注册一个 Widget

@SPI.ClassFactory(
  FormFieldWidget.Token({
    viewType: 'Form',
    ttype: 'String',
    widget: 'Input'
  })
)
class StringWidget {}
  • 这里使用了装饰器 @SPI.ClassFactory(...) 来注册一个名为 StringWidget 的类。
  • FormFieldWidget.Token 生成的 token 包含 viewType、ttype 和 widget 等信息,用于唯一标识这个 Widget。
  • StringWidget 类被定义时,ClassFactory 会被调用,StringWidget 将被注册到 SPI.container 中,作为 'Field' 类型的一部分。

5. 获取 Widget

// 字段元数据
const fieldMeta = {
  name: 'name',
  field: 'name',
  mode: 'demo.model',
  widget: 'Input',
  ttype: 'String',
  viewType: 'Form'
};

// 找到对应的widget
const widget = FormFieldWidget.Selector({
  viewType: fieldMeta.viewType,
  ttype: fieldMeta.ttype,
  widget: fieldMeta.widget
});
  • fieldMeta 是一个字段元数据对象,它包含字段的类型、视图类型、字段类型等。
  • FormFieldWidget.Selector 方法使用 fieldMeta 中的信息来查找对应的 Widget。

这段代码实现了一个简单的依赖注入,允许开发者通过 SPI 类注册和获取表单字段的 Widget。通过使用 WeakMap,它确保在不再需要服务时可以有效地回收内存。FormFieldWidget 提供了定义服务 token 和选择服务实例的方法,装饰器用于简化服务的注册过程。

如果您想要更深入的学习依赖注入,可参考这篇文章inversify

渲染机制

当我们使用 ts+vue 自定义一个字段或者视图的时候,oinone 底层会将 ts 中的 class 渲染成 vue 组件,然后再将 setComponent 中的组件作为子组件,大家可以使用 vue 调试功能看到对应的功能,那么这功能是怎么实现的呢?

在日常的 Vue 开发中,我们通常会使用 .vue 文件,这些文件中包含模板语法或 TSX 写法。无论采用哪种方式,在运行时都会被转换为 render 函数,本质上是将模板或 TSX 语法转化为 JavaScript 代码,以便在浏览器中运行。

// myComponent.ts

const myComponent = defineComponent({
  setup() {
    const name = ref('');

    return { name };
  },
  render() {
    return createVNode('div', null, this.name);
  }
});

在上面的代码中,我们创建了 myComponent.ts 文件,通过 defineComponent 定义了一个组件。在 setup 函数中,我们定义了一个响应式变量 name,最后在 render 函数中返回一个 div 标签,内容为 name 的值。可以看到,这个文件并不是 .vue 或 .tsx 文件,而是一个普通的 TS 文件。

接下来,我们定义一个 VueWidget 类,以便在其中使用组件

class VueWidget {
  component = null;
  props = {};

  setComponent() {
    this.component = myComponent;
  }

  render() {
    return defineComponent({
      setup() {
        const name = ref('');

        return { name };
      },
      render() {
        return createVNode(this.component, this.props);
      }
    });
  }
}

const widget = new VueWidget();

const vnode = widget.setComponent(myComponent).render();

// 这个时候就可以拿到这个vnode去做渲染了

在上述代码中,我们定义了 VueWidget 类,其中包含一个 setComponent 方法来设置当前组件。通过调用 render 方法,我们可以创建一个新的组件实例,并获取其虚拟节点(vnode)进行渲染。

oinone 平台的前端代码,所有的 class 基层都会继承 VueWidget,这就是为什么 ts 中的 class 会被渲染成 vue 组件的原因。

响应式数据传递

当我们写好了对应的 class + vue 时,通常会遇到属性传递的问题,在前端代码中,我们会使用 @Widget.Reactive | @Widget.Method 来定义一个响应式的属性、方法,这样对应的 vue 组件里面只需要定义对应的 Props 来接收,然后就可以直接使用。

@SPI.ClassFactory(参数)
export class CustomClass extends xxx {
  public initialize(props) {
    super.initialize(props);
    this.setComponent(FormString);
    return this;
  }

  @Widget.Reactive()
  private userName = '张三'

  @Widget.Method()
  private updateUserName(userName:string) {
    this.userName = userName
  }
}

// FormString.vue
<template>
  <div>{{ userName }}</div>
  <button @click="updateUserName('李四')">更新用户名</button>
</template>

<script>
export default {
  props: ['userName', 'updateUserName']
}
</script>

当开发者在class中使用 @Widget.xxx 注解时,底层会收集依赖,并将数据传递到对应的 Vue 组件。在 Vue 组件中,开发者可以直接使用这些属性和方法,无需自行定义。这种方式大大简化了数据管理和组件间的交互。

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

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

Like (0)
汤乾华's avatar汤乾华数式员工
Previous 2024年9月26日 am9:19
Next 2024年9月26日 pm4:20

相关推荐

  • oio-empty-data 空数据状态

    何时使用 当目前没有数据时,用于显式的用户提示。 初始化场景时的引导创建流程。 API 参数 说明 类型 默认值 版本 description 自定义描述内容 string | v-slot – image 设置显示图片,为 string 时表示自定义图片地址 string | v-slot false imageStyle 图片样式 CSSProperties –

    2023年12月18日
    1.6K00
  • 如何通过 Oineone 平台自定义视图

    在 Oineone 平台上,自定义视图允许用户替换默认提供的页面布局,以使用自定义页面。本文将指导您如何利用 Oineone 提供的 API 来实现这一点。 默认视图介绍 Oineone 平台提供了多种默认视图,包括: 表单视图 表格视图 表格视图 (左树右表) 详情视图 画廊视图 树视图 每种视图都有其标准的 layout。自定义视图实际上是替换这些默认 layout 的过程。 默认的表单视图 layout <view type="FORM"> <element widget="actionBar" slot="actionBar" slotSupport="action"> <xslot name="actions" slotSupport="action" /> </element> <element widget="form" slot="form"> <xslot name="fields" slotSupport="pack,field" /> </element> </view> 内嵌的的表单视图 layout <view type="FORM"> <element widget="form" slot="form"> <xslot name="fields" slotSupport="pack,field" /> </element> </view> 默认的表格 <view type="TABLE"> <pack widget="group"> <view type="SEARCH"> <element widget="search" slot="search" slotSupport="field" /> </view> </pack> <pack widget="group" slot="tableGroup"> <element widget="actionBar" slot="actionBar" slotSupport="action"> <xslot name="actions" slotSupport="action" /> </element> <element widget="table" slot="table" slotSupport="field"> <element widget="expandColumn" slot="expandRow" /> <xslot name="fields" slotSupport="field" /> <element widget="rowActions" slot="rowActions" slotSupport="action" /> </element> </pack> </view> 内嵌的的表格 <view type="TABLE"> <view type="SEARCH"> <element widget="search" slot="search" slotSupport="field" /> </view> <element widget="actionBar" slot="actionBar" slotSupport="action"> <xslot name="actions" slotSupport="action" /> </element> <element widget="table" slot="table"> <element widget="expandColumn" slot="expandRow" /> <xslot name="fields" slotSupport="field" /> <element widget="rowActions" slot="rowActions" /> </element> </view> 左树右表 <view type="table"> <pack title="" widget="group"> <view type="search"> <element slot="search" widget="search"/> </view> </pack> <pack title="" widget="group"> <pack widget="row" wrap="false"> <pack widget="col" width="257"> <pack title="" widget="group"> <pack widget="col"> <element slot="tree" widget="tree"/> </pack> </pack> </pack> <pack mode="full" widget="col"> <pack widget="row"> <element justify="START" slot="actionBar"…

    2024年4月3日
    1.6K00
  • 【界面设计器】自定义字段组件实战——表格字段内嵌表格

    阅读之前 此文章为实战教程,已假定你熟悉了【界面设计器】较为完整的【自定义组件】相关内容。 如果在阅读过程中出现的部分概念无法理解,请自行学习相关内容。【前端】文章目录 业务背景 表格中的一对多(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日
    1.5K00
  • 【前端】项目开发前端知识要点地图

    概述 下面整理了目前现有的所有文章,并提供了基本的学习路径。所有使用*标记的文章属于推荐必读文章。 目录 基础篇 【路由】浏览器地址栏url参数介绍 母版-布局-DSL 渲染基础(v4)* 组件SPI机制(v4)* 组件数据交互基础(v4)* 组件生命周期(v4) 入门篇 自定义视图组件(v4)* 如何通过浏览器开发者工具提高调试效率* 如何提高自定义组件的开发效率* 自定义组件之自动渲染(组件插槽的使用)(v4)* GraphQL请求详解(v4)* 上下文在字段和动作中的应用 如何实现页面间的跳转 如何自定义指定页面的样式 进阶篇 自定义组件之手动渲染基础(v4) 自定义组件之手动渲染弹出层(v4) 自定义组件之手动渲染任意视图(v4) 【前端】IOC容器(v4) 最佳实践篇 【前端】工程结构最佳实践(v4)* 【前端】移动端工程结构最佳实践(v4)* 界面设计器实战篇 基础篇 【界面设计器】模型增删改查基础 【界面设计器】他表字段 【界面设计器】左树右表 【界面设计器】树形表格 【界面设计器】树下拉/级联选择 【界面设计器】自定义字段组件基础 展示篇 【界面设计器】自定义字段组件实战——轮播图 【界面设计器】自定义字段组件实战——表格字段组合展示 【界面设计器】自定义字段组件实战——表格字段内嵌表格 交互篇 【界面设计器】自定义字段组件实战——千分位输入框 其他 前端低无一体使用教程 如何自定义表格字段? 【界面设计器】组件开发常见问题 【前端】低无一体部署常见问题 【前端】生产环境性能调优 API文档 OioProvider详解(v4.3.0)* 前端环境配置(v4)* 默认布局模板(v4) 表格主题配置(v4) 运行时上下文API文档(v4) Class Component(ts)(v4)

    2024年5月25日
    5.1K00
  • 自定义组件之手动渲染基础(v4)

    阅读之前 你应该: 了解DSL相关内容。母版-布局-DSL 渲染基础(v4) 了解SPI机制相关内容。组件SPI机制(v4.3.0) 了解组件相关内容。 Class Component(ts)(v4) 自定义组件之自动渲染(组件插槽的使用)(v4) 为什么需要手动渲染 在自定义组件之自动渲染(组件插槽的使用)(v4)文章中,我们介绍了带有具名插槽的组件可以使用DSL模板进行自动化渲染,并且可以用相对简单的方式与元数据进行结合。 虽然自动化渲染在实现基本业务逻辑的情况下,有着良好的表现,但自动化渲染方式也有着不可避免的局限性。 比如:当需要多个视图在同一个位置进行切换。 在我们的平台中,界面设计器的设计页面,在任何一个组件在选中后,需要渲染对应的右侧属性面板。每个面板的视图信息是保存在对应的元件中的。根据元件的不同,找到对应的视图进行渲染。在单个视图中使用自动化渲染是无法处理这一问题的,我们需要一种可以局部渲染指定视图的方式,来解决这一问题。 获取一个视图 使用ViewCache获取视图 export class ViewCache { /** * 通过模型编码和名称获取视图 * @param model 模型编码 * @param name 名称 * @param force 强制查询 * @return 运行时视图 */ public static async get(model: string, name: string, force = false): Promise<RuntimeView | undefined> /** * 通过模型编码、自定义名称和模板获取编译后的视图(此视图非完整视图,仅用于自定义渲染使用) * @param model 模型编码 * @param name 名称(用作缓存key) * @param template 视图模板 * @param force 强制查询 * @return 运行时视图 */ public static async compile( model: string, name: string, template: string, force = false ): Promise<RuntimeView | undefined> } ViewCache#get:用于服务端定义视图,客户端直接获取完整视图信息。 ViewCache#compile:用于客户端定义视图,通过服务端编译填充元数据相关信息,但不包含视图其他信息。 自定义一个带有具名插槽的组件,并提供切换视图的相关按钮 以下是一个自定义组件的完整示例,其使用ViewCache#compile方法获取视图。 view.ts const template1 = `<view> <field data="id" invisible="true" /> <field data="code" label="编码" /> <field data="name" label="名称" /> </view>`; const template2 = `<view> <field data="id" invisible="true" /> <field data="name" label="名称" /> <field data="code" label="编码" /> </view>`; export const templates = { template1, template2 }; ManualDemoWidget.ts import { BaseElementWidget, createRuntimeContextForWidget, FormWidget, RuntimeView, SPI, ViewCache, ViewType, Widget } from '@kunlun/dependencies'; import ManualDemo from './ManualDemo.vue'; import { templates } from './view'; @SPI.ClassFactory(BaseElementWidget.Token({ widget: 'ManualDemo' })) export class ManualDemoWidget extends BaseElementWidget { private formWidget: FormWidget | undefined; public…

    2023年11月1日
    1.2K00

Leave a Reply

Please Login to Comment