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

相关推荐

  • 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) => {…

    2024年9月21日
    2.1K00
  • 表格页自定义按钮如何获取搜索区域的查询条件

    介绍 在使用 Oinone 平台开发过程中,开发者可能会遇到自定义动作需要获取搜索条件并传递给后端的情况。本文将介绍如何利用 Oinone平台 实现此功能。 技术基础知识 当我们在自定义一个动作的时候要先明确自定义的动作类型是什么样的,在Oinone平台中,分为了如下几个动作: 1: 视图动作2: 服务端动作3: 客户端动作3: URL动作 功能步骤或代码示例 案例1、服务端动作,动作点击时候要拿到搜索内容,然后传递给后端。 import { ActionType, ActionWidget, SPI, ServerActionWidget } from '@kunlun/dependencies'; @SPI.ClassFactory( ActionWidget.Token({ name: 'name', model: 'model', actionType: ActionType.Server }) ) export class MyServerActionWidget extends ServerActionWidget { protected async clickAction() { const rst = this.getSearchRsqlAndQueryParams(); } } 案例2、视图动作点击的时候把搜索内容带到另外一个视图或者弹窗 import { ActionType, ActionWidget, SPI, ServerActionWidget } from '@kunlun/dependencies'; @SPI.ClassFactory( ActionWidget.Token({ name: 'name', model: 'model' }) ) export class MyDialogViewActionWidget extends DialogViewActionWidget { // 继承当前动作原本的class protected async clickAction() { const { queryData } = this.getSearchRsqlAndQueryParams(); this.action.context = queryData super.clickAction() return true } } 在上述代码中,我们自定义了一个服务器动作,并在点击触发函数中调用了getSearchRsqlAndQueryParams方法,该方法会返回一个对象: { rsql: String, // 搜索内容对应的rsql queryData: Object, // 搜索的数据 condition: Condition, // 搜索内容对应的数据结构 queryDataToString: Function // 将搜索内容转成JSON字符串 } 这样我们就可以根据业务场景使用对应的值。 注意事项 1: 确保正确导入所需的依赖包。2: 理解并适当修改代码以满足特定业务需求。 总结 本文介绍了在 Oinone 平台中如何自定义一个服务端动作,并获取搜索条件传递给后端的方法。通过合理利用这些功能,开发者可以更灵活地定制应用程序,满足不同的业务需求。 实践案例 如何自定义点击导出动作绑定指定模板

    2024年3月6日
    1.6K00
  • 如何自定义 GraphQL 请求

    在开发过程中,有时需要自定义 GraphQL 请求来实现更灵活的数据查询和操作。本文将介绍两种主要的自定义 GraphQL 请求方式:手写 GraphQL 请求和调用平台 API。 方式一:手写 GraphQL 请求 手写 GraphQL 请求是一种直接编写查询或变更语句的方式,适用于更复杂或特定的业务需求。以下分别是 query 和 mutation 请求的示例。 1. 手写 Query 请求 以下是一个自定义 query 请求的示例,用于查询某个资源的语言信息列表。 const customQuery = async () => { const query = `{ resourceLangQuery { queryListByEntity(query: {active: ACTIVE, installState: true}) { id name active installState code isoCode } } }`; const result = await http.query('resource', query); this.list = result.data['resourceLangQuery']['queryListByEntity']; }; 说明: query 语句定义了一个请求,查询 resourceLangQuery 下的语言信息。 查询的条件是 active 和 installState,只返回符合条件的结果。 查询结果包括 id、name、active、installState 等字段。 2. 手写 Mutation 请求 以下是一个 mutation 请求的示例,用于创建新的资源分类信息。 const customMutation = async () => { const code = Date.now() const name = `测试${code}` const mutation = `mutation { resourceTaxKindMutation { create(data: {code: "${code}", name: "${name}"}) { id code name createDate writeDate createUid writeUid } } }`; const res = await http.mutate('resource', mutation); console.log(res); }; 说明: mutation 语句用于创建一个新的资源分类。 create 操作的参数是一个对象,包含 code 和 name 字段。 返回值包括 id、createDate 等字段。 方式二:调用平台的 API 平台 API 提供了简化的 GraphQL 调用方法,可以通过封装的函数来发送 query 和 mutation 请求。这种方式减少了手写 GraphQL 语句的复杂性,更加简洁和易于维护。 1. 调用平台的 Mutation API 使用平台的 customMutation 方法可以简化 Mutation 请求。 /** * 自定义请求方法 * @param modelModel 模型编码 * @param method 方法名或方法对象 * @param records 请求参数,可以是单体对象或者对象的列表 * @param requestFields…

    2024年9月21日
    2.0K00
  • 「前端」获取系统配置

    「前端」获取系统配置 简介 系统配置对于前端开发至关重要,它包含了许多关键信息,通过调用「systemMajorConfig」API,可以轻松地获取这些关键配置信息。除了主要的系统配置外,底层还提供了一些快捷的API,比如获取当前主题、当前主题大小、登录页面主题、版权状态和默认浏览器信息。 使用步骤 调用「systemMajorConfig」API获取系统配置数据。 使用返回的数据对象来访问特定的系统配置参数,如企业名称、企业官网等。 使用底层提供的快捷API来获取与系统配置相关的特定信息。 系统配置参数 logo (string): 应用logo(未折叠状态) appSideLogo (string): 应用logo(折叠状态) smallLogo (string): 小型logo slogan (string): 企业slogan favicon (string): 浏览器logo browserTitle (string): 浏览器标题 loginPageLogo (string): 登录页logo loginBackground (string): 登录页背景 loginLayoutType (any): 登录页布局主题 mode (any): 主题模式 size (string): 主题大小 快捷API列表 getCurrentTheme: 获取当前主题信息。 getCurrentThemeSize: 获取当前主题大小。 getLoginTheme: 获取登录页面主题信息。 getCopyrightStatus: 获取版权状态信息。 getDefaultBrowser: 获取默认浏览器信息。 示例代码 import { systemConfig, getCurrentTheme } from ‘@kunlun/dependencies’ // 访问特定系统配置参数 console.log(systemConfig.logo); // 输出企业名称 // 使用快捷API获取特定信息 console.log(getCurrentTheme());

    2023年11月1日
    1.3K00
  • 界面设计器 扩展字段的查询上下文

    默认情况下oinone平台对于查询条件,只提供的当前登录用户这一个配置,但是允许开发者扩展 开发者可以在前端代码的main.ts进行扩展 import { SessionContextOptions, ModelFieldType } from '@kunlun/dependencies'; const currentDeptOption = { ttype: ModelFieldType.String, value: '$#{currentDept}', displayName: '当前登录部门', label: '当前登录部门' }; SessionContextOptions.push(currentDeptOption as any); 加上上面的代码,然后再去界面设计器,我们就会发现,多了一个配置

    2023年11月8日
    2.3K00

Leave a Reply

Please Login to Comment