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

(0)
汤乾华的头像汤乾华数式员工
上一篇 2024年9月26日 am9:19
下一篇 2024年9月26日 pm4:20

相关推荐

  • 自定义字段的数据联动

    某种情况下,开发人员期望自定以的字段发生变化后,需要修改其他的字段,这篇文章从两个维度来讲解如果处理数据的联动 界面设计器配置 1: 在界面设计器页面中的的组件区域找到自定义的字段,设计元件 2: 在模型区域,搜索提交方式,如果找到了,就把该字段拖拽进来, 如果找不到,就在元件中的组件区域,拖拽一个文本字段,按照下面的配置进行配置,然后保存 图一是找到了对应的字段图二是找不到对应的字段 【图一】 【图二】 图二的字段编码必须是constructDataTrigger 3: 从模型区搜索联动函数,将其拖拽进来 3: 从模型区搜索提交数据,将其拖拽进来4: 从模型区搜索提交字段,将其拖拽进来 5: 发布 (记得刷新页面哦) 最后再到对应的设计器页面,选中该字段,进行配置 提交方式为blur或者change , 需要开发者手动调用该方法 this.blur()或者this.change(value) // 字段对应的ts文件 class MyField extends FormFieldWidget { onChangeValue(val) { // this.change(val) // this.blur() } } 联动函数就是要调用的后端函数 提交数据分为:变更字段 -> 发生变化后的字段当前视图字段 -> 当前视图所有的字段指定字段 -> 指定字段,如果配置的指定字段,那么提交字段的配置就要输入对应的字段 代码配置 平台也支持通过代码的方式修改字段 // 字段对应的ts文件 class MyField extends FormFieldWidget { onChangeValue(val) { // 修改字段本身的值 this.change(val) // 修改其他字段的值 this.formData.otherField = 'value' this.reloadFormData$.subject.next(true); } }

    2023年11月9日
    1.3K00
  • 表格主题配置(v4)

    TableThemeConfig /** * 表格主题配置 */ export interface TableThemeConfig { border: boolean | string; stripe: boolean; isCurrent: boolean; isHover: boolean; /** * 表格列主题配置 */ column: Partial<TableColumnThemeConfig>; } /** * 表格列主题配置 */ export interface TableColumnThemeConfig { /** * <h3>最小宽度</h3> * <ul> * <li>boolean: enabled column width auto compute</li> * <li>number: using css width (default: px)</li> * <li>string: using css width</li> * <li> * object: auto compute width for label by default function * <ul> * <li>min: min min width (default: 120)</li> * <li>max: max min width (default: 432)</li> * <li>chineseWidth: chinese width (default: 14 -> fontSize: 14px)</li> * <li>otherWidth: non chinese width (default: 9 -> fontSize: 14px)</li> * <li>sortableFixWidth: sortable handle width (default: 40)</li> * <li>nonSortableFixWidth: non sortable fix width (default: 22)</li> * </ul> * </li> * <li>function: auto compute width for label by function</li> * </ul> */ minWidth: boolean | number | string | Partial<TableColumnMinWidthComputeConfig> | TableColumnMinWidthComputeFunction; /** * 操作列 */ operation: { /** * 宽度 (default: 165) */ width?: number | string; /** * 最小宽度 (default: 120) */ minWidth?: number | string; }; } export interface TableColumnMinWidthComputeConfig { min: number;…

    2023年11月1日
    85300
  • 主题设置-设置表格全局样式

    在启动工程的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.1K00
  • 如何自定义表格单元格样式

    介绍 OinOne的表格是基于Vxe-Table实现的,我们将Vxe-table内置的关于单元格样式的方法、属性开放到了表格组件TableWidget上 Vxe-Table相关文档 vxe-table的单元格样式 vxe-table的单元格动态样式 单元格样式 行的样式、单元格样式,表头的样式、表尾的样式、全部都可以完全自定义,通过设置 cellClassName、headerCellClassName、rowClassName …等参数 (注:当自定义样式之后可能会覆盖表格的样式,比如选中行..等,记得自行处理好相关样式) 单元格动态样式 行的动态样式、单元格动态样式,表头的动态样式、表尾的动态样式、可以通过设置 cellStyle、headerCellStyle、rowStyle …等参数 (注:当自定义样式之后可能会覆盖表格的样式,比如选中行..等,记得自行处理好相关样式) 示例代码 这里仅演示cellClassName和cellStyle,其他方法的出入参数请参考上面的Vxe-Table文档 import { BaseElementWidget, SPI, TableWidget, ViewType, Widget } from '@kunlun/dependencies'; @SPI.ClassFactory(BaseElementWidget.Token({ viewType: ViewType.Table, widget: 'CustomStyleTableWidget', })) export class CustomStyleTableWidget extends TableWidget { @Widget.Method() protected cellClassName({ row, rowIndex, $rowIndex, column, columnIndex, $columnIndex }) { if (column.field === 'field00019') { return `demo-cell-${column.field}`; } return ''; } @Widget.Method() protected cellStyle({ row, rowIndex, $rowIndex, column, columnIndex, $columnIndex }) { if (column.field === 'field00019') { return { backgroundColor: '#f60', color: '#ffffff' }; } return ''; } } 效果预览

    2024年10月30日
    1.1K00
  • 如何通过浏览器开发者工具提高调试效率

    1.通过vue devtool查看vue组件和oinone组件的信息 平台字段、动作、视图组件都是以Widget结尾的,可以通过关键字快速找到组件 安装vue devtool插件 chrome安装最新版的vue devtool插件 谷歌应用商店插件地址,隐藏窗口需要在扩展程序的详情页额外设置才能使用该插件 安装好插件后,可以通过插件选中html页面中的元素查看相关信息 相关特性了解 组件自动创建的vue组件以组件的class类名命名,据此特性可以得出自定义组件的时候,可继承该命名的父类 一般oinone的组件以Widget后缀命名,也推荐大家也以此为后缀,方便在vue调试面板快速看出哪些是oinone组件 选中oinone组件后可以在右侧面板看到相关的组件信息,部分属性介绍 template属性为dsl的配置 activeRecords、formData、rootData、parentData、dataSource等属性为常用数据属性 action为动作的元数据 field为字段的元数据 2.直接在浏览器开发者工具的源码处调试源码 打开浏览器开发者工具,进入标签页源代码/来源(英文版为Source),win系统用快捷键ctrl+O,mac系统用快捷键cmd+O, 可以打开文件搜索面板,通过关键字搜索文件名找到代码文件,可直接在里面debug调试或者查看执行逻辑

    2024年9月9日
    1.0K00

Leave a Reply

登录后才能评论