前端 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 并将 token 和 target(即 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。
  • 它首先获取 container 中 Field 类型的 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

相关推荐

  • 如何在Oinone 根据主题实现自定义组件样式

    在页面交互中,样式的变化是前端核心工作之一。本文介绍如何在Oinone平台中根据主题变化自定义组件样式。 介绍 Oinone平台提供了六种不同的主题设置,浅色大主题、浅色中主题、浅色小主题、深色大主题、深色中主题、深色小主题,默认采用浅色中主题。本文旨在指导如何在线或通过代码修改这些主题,以满足个性化需求。 基础知识 Oinone平台的默认主题为浅色中主题,用户可以根据喜好选择以下六种主题中的任何一种: 浅色大主题 浅色中主题 浅色小主题 深色大主题 深色中主题 深色小主题 在线修改主题 用户可以通过进入系统配置应用,并切换到系统风格配置菜单来在线修改主题。选择喜欢的主题并保存即可轻松更换。 代码修改主题 步骤示例 新建theme.ts文件 在项目的src目录下新建一个theme.ts文件。 定义主题变量 在theme.ts文件中定义主题名称和CSS变量,示例中将主色系替换为黑色。 export const themeName = ‘OinoneTheme’; export const themeCssVars = { ‘primary-color’: ‘black’, ‘primary-color-hover’: ‘black’, ‘primary-color-rgb’: ‘0, 0, 0’, ‘primary-color-focus’: ‘black’, ‘primary-color-active’: ‘black’, ‘primary-color-outline’: ‘black’, }; 在main.ts注册 import { registerTheme, VueOioProvider } from ‘@kunlun/dependencies’; // 引入注册主题组件 import { themeName, themeCssVars } from ‘./theme’; // 引入theme.ts registerTheme(themeName, themeCssVars);// 注册 VueOioProvider( { …other config theme: [themeName] // 定义的themeName传入provider中 }, [] ); 4: 刷新页面看效果 注意事项 确保在定义CSS变量时遵循主题设计规范。 正确引入theme.ts文件以避免编译错误。 总结 本文详细介绍了在Oinone平台中修改主题的两种方法:在线修改和代码修改。这些步骤允许开发者和用户根据个人喜好或项目需求,自定义界面的主题风格。

    2024年2月26日
    1.8K00
  • oio-modal 对话框

    API 参数 说明 类型 默认值 版本 cancelText 取消按钮文字 string| slot 取消 closable 是否显示右上角的关闭按钮 boolean true closeIcon 自定义关闭图标 VNode | slot – confirmLoading 确定按钮 loading boolean 无 destroyOnClose 关闭时销毁 Modal 里的子元素 boolean false footer 底部内容,当不需要默认底部按钮时,可以设为 :footerInvisible="true" slot 确定取消按钮 getTriggerContainer 指定 Modal 挂载的 HTML 节点 (instance): HTMLElement () => document.body keyboard 是否支持键盘 esc 关闭 boolean true mask 是否展示遮罩 boolean true maskClosable 点击蒙层是否允许关闭 boolean true enterText 确认按钮文字 string 确定 title 标题 string|slot 无 visible(v-model:visible) 对话框是否可见 boolean 无 width 宽度 string|number 520 wrapClassName 对话框外层容器的类名 string – zIndex 设置 Modal 的 z-index number 1000 cancelCallback 点击遮罩层或右上角叉或取消按钮的回调, return true则关闭弹窗 function(e) enterCallback 点击确定回调 function(e)

    2023年12月18日
    2.5K00
  • 界面设计器 扩展字段的查询上下文

    默认情况下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.6K00
  • oio-grid 栅格

    24 栅格系统。 <oio-row :gutter="24"> <oio-col :span="12"></oio-col> <oio-col :span="12"></oio-col> </oio-row> 概述 布局的栅格化系统,我们是基于行(row)和列(col)来定义信息区块的外部框架,以保证页面的每个区域能够稳健地排布起来。下面简单介绍一下它的工作原理: 通过\row\在水平方向建立一组\column\(简写 col) 你的内容应当放置于\col\内,并且,只有\col\可以作为\row\的直接元素 栅格系统中的列是指 1 到 24 的值来表示其跨越的范围。例如,三个等宽的列可以使用 \<a-col :span="8" />\ 来创建 如果一个\row\中的\col\总和超过 24,那么多余的\col\会作为一个整体另起一行排列 Flex 布局 我们的栅格化系统支持 Flex 布局,允许子元素在父节点内的水平对齐方式 – 居左、居中、居右、等宽排列、分散排列。子元素与子元素之间,支持顶部对齐、垂直居中对齐、底部对齐的方式。同时,支持使用 order 来定义元素的排列顺序。 Flex 布局是基于 24 栅格来定义每一个『盒子』的宽度,但不拘泥于栅格。 API Row 成员 说明 类型 默认值 align flex 布局下的垂直对齐方式:top middle bottom string top gutter 栅格间隔,可以写成像素值或支持响应式的对象写法来设置水平间隔 { xs: 8, sm: 16, md: 24}。或者使用数组形式同时设置 [水平间距, 垂直间距](1.5.0 后支持)。 number/object/array 0 justify flex 布局下的水平排列方式:start end center space-around space-between string start wrap 是否自动换行 boolean false Col 成员 说明 类型 默认值 版本 flex flex 布局填充 string|number – offset 栅格左侧的间隔格数,间隔内不可以有栅格 number 0 order 栅格顺序,flex 布局模式下有效 number 0 pull 栅格向左移动格数 number 0 push 栅格向右移动格数 number 0 span 栅格占位格数,为 0 时相当于 display: none number – xxxl ≥2000px 响应式栅格,可为栅格数或一个包含其他属性的对象 number|object – xs <576px 响应式栅格,可为栅格数或一个包含其他属性的对象 number|object – sm ≥576px 响应式栅格,可为栅格数或一个包含其他属性的对象 number|object – md ≥768px 响应式栅格,可为栅格数或一个包含其他属性的对象 number|object – lg ≥992px 响应式栅格,可为栅格数或一个包含其他属性的对象 number|object – xl ≥1200px 响应式栅格,可为栅格数或一个包含其他属性的对象 number|object – xxl ≥1600px 响应式栅格,可为栅格数或一个包含其他属性的对象 number|object –

    2023年12月18日
    1.2K00
  • 主题设置-设置表格全局样式

    在启动工程的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.6K00

Leave a Reply

Please Login to Comment