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

基础知识

1: 面向对象

面向对象编程是 JavaScript 中一种重要的编程范式,它帮助开发者通过类和对象组织代码,提高代码的复用性。

2: 装饰器

装饰器在 JavaScript 中是用于包装 class 或方法的高阶函数

为了统一术语,下面的内容会把装饰器讲成注解

在 oinone 平台中,无论是字段还是动作,都是通过 ts + vue 来实现的,ts 中是面向对象的写法,所有的属性、方法建议都在对应的 class 中,如果有通用的属性跟方法,可以放在一个公共的 class 中,然后通过继承来实现,这样便于维护。

<!-- FormString.vue -->
<template>
  <div>
    <p>用户名: {{userName}}</p>
    <button @click="updateUser({name: '王五'})">修改用户名</button>
  </div>
</template>
<script lang="ts">
  import { defineComponent } from 'vue';

  export default defineComponent({
    props: {
      userName: {
        type: String,
        default: ''
      },
      userInfo: {
        type: Object,
        default: () => ({})
      },
      updateUser: {
        type: Function,
        default: () => () => ({})
      }
    }
  });
</script>
import FormString from './FormString.vue'

@SPI.ClassFactory(
  FormFieldWidget.Token({
    viewType: [ViewType.Form, ViewType.Search],
    ttype: ModelFieldType.String
  })
)
export class FormCustomStringFieldWidget extends FormFieldWidget {
  public initialize(props) {
    super.initialize(props); // 调用父类方法,确保继承的属性和方法正常初始化
    this.setComponent(FormString); // 注册 Vue 组件,这样该 Widget 就会渲染 FormString 组件
    return this;
  }

  public otherInfo = {
    name:'张三'
  }

  @Widget.Reactive()
  public userInfo = {
    name:'李四'
  }

  @Widget.Reactive()
  public get userName() {
    return this.userInfo.name
  }

  @Widget.Method()
  public updateUser userName(user) {
     this.userInfo = user
  }

  public updateOtherUser userName(user) {
     this.otherUser = user
  }
}

这段代码定义了一个 FormCustomStringFieldWidget 类,用于处理表单中 String 类型字段的展示和交互。该类继承自 FormFieldWidget,并使用了多种注解和特性来实现不同功能。下面是对代码的详细讲解。

SPI 讲解

@SPI.ClassFactory()

无论是自定义字段还是动作,或者是自定义 mask、layout,都会用到@SPI.ClassFactory来注册,@SPI.ClassFactory 是一个注解,它标记了该类是通过工厂模式注册的。

在前端中,所有的注解(装饰器)本质上还是高阶函数,下面是一段伪代码。

const SPI = {
  ClassFactory(token) {
    return (targetClass) => {
      // todo something
    };
  }
};

class MyClass {}

SPI.ClassFactory(注册条件)(MyClass);

所以我们在使用@SPI.ClassFactory的时候,就会根据注册条件把对应的 class 注册到对应的工厂中。

注册条件

FormFieldWidget.Token({
  viewType: [ViewType.Form, ViewType.Search],
  ttype: ModelFieldType.String
});

这段代码调用FormFieldWidget.Token函数,生成一个 token,这个 token 会作为参数传递给@SPI.ClassFactory,然后@SPI.ClassFactory会根据这个 token 来注册对应的 class。

不同类型的 widget 调用的 token 函数不同:

注册条件集合

字段注册条件

  • 表单(详情、搜索、画廊)字段: FormFieldWidget.Token
  • 表格字段: BaseFieldWidget.Token

下面是参数描述和含义:

属性 类型 说明
viewType ViewType | ViewType[] 当前视图类型,支持单一或多个视图类型。
widget string | string[] 组件名称,可以是单个字符串或多个组件名称。
ttype ModelFieldType | ModelFieldType[] 字段业务类型,支持单一或多个业务类型。
multi boolean 是否多值,true 表示字段支持多个值。
model string | string[] 指定模型名称,可以是单一或多个模型。
viewName string | string[] 指定视图名称,可以是单一或多个视图名称。
name string 指定字段的 name 属性,用于业务逻辑识别。

当我们注册 字段 SPI 的时候,对应注册条件基本只会用到viewTypettypewidget、这三个属性,其他属性都是可选的。

动作注册条件

  • 动作: ActionWidget.Token

下面是参数描述和含义:

属性 类型 说明
viewType ViewType | ViewType[] 当前视图类型,支持单一或多个视图类型。
actionType ActionType | AiewType[] 当前动作类型,支持单一或多个动作类型。
widget string | string[] 组件名称,可以是单个字符串或多个组件名称。
target ViewActionTarget | ViewActionTarget[] 打开方式 (视图动作专属)
viewName string | string[] 指定视图名称,可以是单一或多个视图名称。
model string | string[] 指定模型名称,可以是单一或多个模型。
name string 指定动作的 name 属性,用于业务逻辑识别。

当我们注册 动作 SPI 的时候,对应注册条件基本只会用到actionTypemodelname、这三个属性,其他属性都是可选的。

视图、layout 注册条件

  • 动作: BaseElementWidget.Token

下面是参数描述和含义:

属性 类型 说明
viewType ViewType | ViewType[] 当前视图类型,支持单一或多个视图类型。
inline boolean 当前是否为内嵌视图。
widget string | string[] 组件名称,可以是单个字符串或多个组件名称。
viewName string | string[] 指定视图名称,可以是单一或多个视图名称。
model string | string[] 指定模型名称,可以是单一或多个模型。

当我们注册 layout 或者视图 SPI 的时候,对应注册条件基本只会用到viewTypewidget、这两个个属性,其他属性都是可选的。

mask 注册条件

  • 动作: MaskWidget.Token

下面是参数描述和含义:

属性 类型 说明
widget string | string[] 组件名称,可以是单个字符串或多个组件名称。

当我们注册 mask SPI 的时候,对应注册条件只会用到widget

路由注册条件

  • 动作: RouterWidget.Token

下面是参数描述和含义:

属性 类型 说明
widget string | string[] 组件名称,可以是单个字符串或多个组件名称。

当我们注册 路由 SPI 的时候,对应注册条件只会用到widget

SPI 覆盖

在开发中,有时我们需要对平台底层已注册的 SPI 进行覆盖。这种情况通常出现在需要修改或扩展某些功能时。下面将介绍如何安全地覆盖 SPI。

以字段为例,加入我们需要覆盖平台默认的字段,这个字段是 Form 表单里面的String类型,我们希望这个字段可以支持业务扩展的能力,那么我们可以通过如下方式来扩展这个字段:

在 5.0.x 版本中,平台部分源码是开放的,所以我们可以看到平台默认的字段源码,FormStringFieldSingleWidget就是 Form 表单里面的String类型对应的 class,它的 SPI 是:

@SPI.ClassFactory(
  BaseFieldWidget.Token({
    viewType: [ViewType.Form, ViewType.Search],
    ttype: ModelFieldType.String
  })
)

如果想覆盖它,只需要 SPI 注册条件一致就行。

@SPI.ClassFactory(
  FormFieldWidget.Token({
    viewType: [ViewType.Form, ViewType.Search],
    ttype: ModelFieldType.String
  })
)
class ExistingStringFieldWidget extends FormStringFieldSingleWidget {
  // 平台已有实现
}

注册 vue 组件

FormCustomStringFieldWidget中,我们定义了initialize函数里面写了一点代码。

import FormString from './FormString.vue'

class FormCustomStringFieldWidget{
  public initialize(props) {
    super.initialize(props);
    this.setComponent(FormString);
    return this;
  }
}

initialize函数是固定写法,super.initialize(props)是用来调用父类的方法,this.setComponent(FormString)是将 vue 组件注册到当前 widget 中,当自定义的字段不需要重写 vue 组件的时候,那么就不需要重写initialize函数,只需要继承对应的 class 重写对应的属性、方法即可

当自定义的字段、动作、视图、mask 需要使用自己的 vue 组件时,才需要重写initialize函数

ts 与 vue 之间的联动

Widget.Reactive()

FormCustomStringFieldWidget中,我们定义了userInfouserName两个属性,通过Widget.Reactive()注解,将这两个属性变成响应式属性,这样在 vue 组件中中,就可以通过 props 来接受两个属性,otherInfo属性没有打上任何注解,说明它是一个普通的属性,在对应的 vue 文件里面是获取不到的。

当定义 userName 时,我们为其加上了 get 属性,这会将它转换为一个计算属性,方便在 Vue 组件中动态更新。

所有可以理解成:

 @Widget.Reactive()
 public userInfo = {}
 等价于 ↓
 const  userInfo = ref({})

  @Widget.Reactive()
  public get userName() {
    return this.userInfo.name
  }
   等价于  ↓
  const  userName = computed(() => userInfo.name)

Widget.Method()

在上述代码中,我们还定义了updateUserupdateOtherUser方法,通过updateUser上使用了Widget.Method()注解,将这个方法变成一个响应式方法,这样在 vue 组件中,就可以通过 props 来接受这个方法,调用这个方法,实现数据的更新,updateOtherUser是一个普通的方法,在 vue 文件中是获取不到的。

最终我们可以得出一个结论,在 ts 中,如果定义的属性跟方法需要在 vue 中获取,那么就要加上Widget.Reactive()注解,如果是方法,就加上Widget.Method()注解,如果该属性是一个计算属性,就加上get属性。

伪代码实现:

import { ref, computed } from 'vue';

const Widget = {
  Reactive(target, key, description) {
    if (description.get) {
      target[key] = computed(description.get);
    } else {
      target[key] = ref(target[key]);
    }
  },

  Method(target, key) {
    target[key] = ref(target[key]);
  }
};

完整案例:用户信息管理

场景:我们将创建一个用户信息管理的小模块,允许用户查看和修改他们的个人信息,包括姓名和邮箱地址。我们将用 Vue 组件展示这些信息,并使用 TS 中的类来管理逻辑。此案例将展示如何使用 @Widget.Reactive() 和 @Widget.Method() 注解来确保数据和方法的响应式更新。

第一步:Vue 组件定义

我们首先定义一个简单的 Vue 组件,用于显示和更新用户信息。

<!-- UserInfo.vue -->
<template>
  <div>
    <p>姓名: {{ userName }}</p>
    <p>邮箱: {{ userEmail }}</p>
    <button @click="updateUserInfo({ name: '李四', email: 'li.si@example.com' })">修改用户信息</button>
  </div>
</template>

<script lang="ts">
  import { defineComponent } from 'vue';

  export default defineComponent({
    props: {
      userName: {
        type: String,
        default: ''
      },
      userEmail: {
        type: String,
        default: ''
      },
      updateUserInfo: {
        type: Function,
        default: () => () => ({})
      }
    }
  });
</script>

第二步:TypeScript 类定义

我们在 TypeScript 中定义一个类 FormStringUserFieldWidget,用于管理用户信息。我们使用 @Widget.Reactive() 和 @Widget.Method() 来确保数据的响应式更新和方法的可用性。

import UserInfo from './UserInfo.vue';

@SPI.ClassFactory(
  FormFieldWidget.Token({
    viewType: [ViewType.Form],
    ttype: ModelFieldType.String,
    widget: 'FormStringUserFieldWidget'
  })
)
export class FormStringUserFieldWidget extends FormFieldWidget {
  // 注册 Vue 组件
  public initialize(props: any) {
    super.initialize(props);
    this.setComponent(UserInfo); // 将 UserInfo.vue 组件注册到当前 Widget
    return this;
  }

  // 用户基础信息
  @Widget.Reactive()
  public userInfo = {
    name: '张三',
    email: 'zhang.san@example.com'
  };

  // 返回用户姓名的计算属性
  @Widget.Reactive()
  public get userName() {
    return this.userInfo.name;
  }

  // 返回用户邮箱的计算属性
  @Widget.Reactive()
  public get userEmail() {
    return this.userInfo.email;
  }

  // 更新用户信息的方法
  @Widget.Method()
  public updateUserInfo(newInfo: { name: string; email: string }) {
    this.userInfo = { ...this.userInfo, ...newInfo };
  }
}

第三步:案例解析

  • initialize 方法:
    • 我们使用 initialize 方法来注册 Vue 组件 UserInfo.vue。当这个类被实例化时,Vue 组件会被渲染,并且与类中的响应式数据和方法建立连接。
  • userInfo 属性:
    • 这是用户的基本信息对象,包含 name 和 email 两个字段。我们使用 @Widget.Reactive() 注解将 userInfo 变成响应式数据,这样在 Vue 组件中可以随时获取和更新。
  • userName 和 userEmail 计算属性:
    • 我们使用 get 关键字为 userName 和 userEmail 定义了计算属性,并使用 @Widget.Reactive() 注解,这样 Vue 组件可以根据 userInfo 的变化自动更新显示内容。
  • updateUserInfo 方法:
    • 这是用于更新用户信息的响应式方法。通过 @Widget.Method() 注解,我们确保 Vue 组件能够调用此方法。点击 Vue 组件中的按钮后,会触发 updateUserInfo 方法更新用户的姓名和邮箱。
  • Vue 组件
    • 我们通过 props 接收了从 TypeScript 类传递过来的 userName、userEmail 和 updateUserInfo 方法。点击按钮后,updateUserInfo 方法会被调用,用户信息将被更新。

通过这个完整的案例,我们展示了如何将 TypeScript 与 Vue 结合,通过 @Widget.Reactive() 和 @Widget.Method() 实现数据的响应式联动和方法调用。

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

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

Like (0)
汤乾华's avatar汤乾华数式员工
Previous 2024年9月21日 am10:32
Next 2024年9月21日 pm5:51

相关推荐

  • 动作API

    ActionWidget 动作组件的基类,包含了动作组件的通用属性和方法 示例 class MyActionWidget extends ActionWidget { } 动作属性 属性名 说明 类型 可选值 默认值 label 动作的名称 String – 当前动作的displayName action 当前动作的元数据 RuntimeAction – model 运行时模型 RuntimeModel – viewAction 运行时视图动作 RuntimeViewAction – view 运行时视图 RuntimeViewAction – initialValue 视图初始值 ActiveRecord[] – initialContext 视图初始上下文 Object – urlParameters 获取url参数 UrlQueryParameters – scene 场景 String – loading 动作加载状态 Boolean – false disabled 是否禁用 Boolean – false disabledTitle 禁用时的按钮名称 String – – invisible 当前字段是否不可见 Boolean – false validateForm 点击动作后是否校验表单 Boolean – false actionDomain 动作的domain查询条件 String – undefined goBack 点击动作后是否返回上一页 Boolean – false isDialog 是否为弹窗内动作 Boolean – 弹窗下的动作默认为true closeDialog 点击动作后是否关闭弹窗 Boolean – 默认为isDialog的值 isDrawer 是否为抽屉内动作 Boolean – 抽屉下的动作默认为true closeDrawer 点击动作后是否关闭抽屉 Boolean – 默认为isDrawer的值 isInnerPopup 是否为页内弹出层动作 Boolean – 页内弹出层下的动作默认为true isAsync 是否为异步动作 Boolean – true refreshRoot 是否刷新根视图 Boolean – false refreshData 是否刷新数据 Boolean – true type 动作的类型 ButtonType – 行内动作默认为ButtonType.link,其他动作为ButtonType.primary bizStyle 动作的业务类型 ButtonBizStyle – ButtonBizStyle.default icon 动作的图标 String – – enableConfirm 是否开启二次确认 Boolean – true confirmType 二次确认的类型 ConfirmType – – confirm 二次确认的内容 String – – confirmText 二次确认的提示内容 String – – confirmPosition 二次确认提示的展示位置 PopconfirmPlacement – PopconfirmPlacement.BM enterText 二次确认的确定按钮文字 String – – cancelText 二次确认的取消按钮文字 String – – searchBody 列表页的动作可以拿到搜索区域的搜索条件 ActiveRecord…

    2024年3月8日
    1.4K00
  • 如何编写自定义字段组件的校验逻辑

    介绍 自定义字段组件的时候,我们可能会遇到有复杂校验规则或者业务上特殊的校验提示信息的场景,这时候可以通过覆写字段的校验方法validator来实现。 示例代码 import { SPI, ValidatorInfo, FormStringFieldWidget, isEmptyValue, isValidatorSuccess, FormFieldWidget, ViewType, ModelFieldType } from '@kunlun/dependencies' @SPI.ClassFactory(FormFieldWidget.Token({ viewType: [ViewType.Form], ttype: ModelFieldType.String, widget: 'DemoPhone' })) export class DemoFormPhoneFieldWidget extends FormStringFieldWidget { // 字段校验方法 public async validator(): Promise<ValidatorInfo> { // 建议先调用平台内置的通用校验逻辑 const res = await super.validator(); if (!isValidatorSuccess(res)) { // 校验失败直接返回 return res; } // 编写自有校验逻辑 if (!isEmptyValue(this.value) && !/^1[3456789]\d{9}$/.test(this.value as string)) { // 通过内置的validatorError方法提示校验提示信息 return this.validatorError('手机号格式错误'); } // 无异常,用内置的validatorSuccess返回校验通过的信息 return this.validatorSuccess(); } } 扩展学习 自定义字段组件如何处理vue组件内的表单校验

    2024年8月23日
    2.3K00
  • 如何自定义表格字段?

    目录 一、表单字段注册vue组件实现机制 二、表格字段注册vue组件实现机制 三、机制对比分析 四、表格字段完整案例 表单字段注册vue组件实现机制 核心代码 public initialize(props) { super.initialize(props); this.setComponent(VueComponent); return this; } 表格字段注册vue组件实现机制 核心代码 @Widget.Method() public renderDefaultSlot(context: RowContext) { const value = this.compute(context); return [createVNode(CustomTableString, { value })]; } 因为表格有行跟列,每一列都是一个单独的字段(对应的是TS文件),但是每列里面的单元格承载的是Vue组件,所以通过这种方式可以实现表格每个字段对应的TS文件是同一份,而单元格的组件入口是通过renderDefaultSlot函数动态渲染的vue组件,只需要通过createVNode创建对应的vue组件,然后将props传递进去就行 上下文接口 interface RowContext<T = unknown> { /** * 当前唯一键, 默认使用__draftId, 若不存在时,使用第三方组件内置唯一键(如VxeTable使用{@link VXE_TABLE_X_ID}) */ key: string; /** * 当前行数据 */ data: Record<string, unknown>; /** * 当前行索引 */ index: number; /** * 第三方组件原始上下文 */ origin: T; } 机制对比分析 表单字段 vs 表格字段渲染机制对比表 对比维度 表单字段实现方案 表格字段实现方案 绑定时机 initialize 阶段静态绑定 renderDefaultSlot 阶段动态创建 组件声明方式 this.setComponent(Component) createVNode(Component, props) 上下文传递 通过类成员变量访问 显式接收 RowContext 参数 渲染控制粒度 字段级(表单控件) 单元格级 表格字段完整案例 import { SPI, ViewType, BaseFieldWidget, Widget, TableNumberWidget, ModelFieldType, RowContext } from '@kunlun/dependencies'; import CustomTableString from './CustomTableString.vue'; import { createVNode } from 'vue'; @SPI.ClassFactory( BaseFieldWidget.Token({ ttype: ModelFieldType.String, viewType: [ViewType.Table], widget: 'CustomTableStringWidget' }) ) export class CustomTableStringWidget extends BaseTableFieldWidget { @Widget.Method() public renderDefaultSlot(context:RowContext) { const value = this.compute(context); // 当前字段的值 const rowData = context.data // 当前行的数据 const dataSource = this.dataSource // 表格数据 if (value) { // 自定义组件入口在此处 return [createVNode(CustomTableString, { value })]; } return []; } } <template> <div>当前值: {{value}}</div> </template> <script lang="ts"> import { defineComponent } from 'vue'…

    2023年11月6日
    2.5K00
  • 表格如何支持表尾合计计算

    介绍 可以通过扩展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日
    1.9K00
  • oinone的GraphQL使用指南

    如果之前没了解过GraphQL,可以先查看GraphQL的文档 为什么oinone要选用GraphQL? 我们先看一下oinone独特的元数据设计 介绍信息来源于Oinone 7天从入门到精通,如提示无权限,则需要申请 再看一下GraphQl的介绍 我们可以看出,GraphQL提供的特性可以满足我们对元数据的描述需求,因此我们选用GraphQL。 相关工具推荐 可视化gql请求工具: 官方下载地址 oinone工具包-win版 oinone工具包-mac版 模拟登录请求 点击“Refresh Schema”按钮手动同步后端的gql文档数据 点击“show Documentation”按钮查看gql文档,可以在搜索框内输入关键字查询相关文档

    2023年11月1日
    1.9K00

Leave a Reply

Please Login to Comment