在阅读本篇文章之前,您需要学习以下知识点:
前端开发者在使用 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
和targe
t(即 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低代码应用平台体验