更新组件,兼容MES PullRequest #924

This commit is contained in:
陈梓阳 2025-04-29 15:58:53 +08:00
parent fa8b468c32
commit 18582016b2
11 changed files with 204 additions and 88 deletions

View File

@ -45,4 +45,37 @@
## 注意事项
1. 主项目和子项目需要同源或是配置好了CORS策略
2. 该库对Vue2.7的支持有限
2. 该库对Vue2.7的支持有限
3. 使用引入模块的方式渲染带有插槽的组件会导致错误,原因未知:
组件`JumpBtn.vue`
```vue
<template>
<div>
<button @click="HandleClick">
<slot></slot>
</button>
</div>
</template>
```
渲染函数
```ts
import JumpBtn from './JumpBtn.vue';
hh(JumpBtn, null, "插槽内容"); // 会导致错误
```
若注册为全局组件并在调用渲染函数时使用组件标签名,则不会错误
`main.ts`
```ts
import JumpBtn from './JumpBtn.vue';
app.component('jump-btn', JumpBtn);
```
渲染函数
```ts
h('jump-btn', null, "插槽内容"); // 并不会报错
```

View File

@ -1,11 +1,17 @@
import Invoker from "./src/components/Invoker";
import { hh } from './src/components/DemiHelper';
import { InvokerItem } from "./src/components/InvokerItem";
import { InvokerContext } from "./src/types/InvokerContext";
import { Vue2Instance } from "./src/types/Vue2Instance";
import { ModContext } from "./src/types/ModContext";
import Receiver from "./src/components/Receiver";
export {
Invoker,
Receiver,
type InvokerItem,
type InvokerContext,
type Vue2Instance,
type ModContext,
hh,
}

View File

@ -11,6 +11,7 @@ import { onMounted, version, ref, h } from 'vue-demi';
import Invoker from './components/Invoker'
import JumpBtn from './JumpBtn.vue';
import SlotTester from './SlotTester.vue';
import { InvokerItem } from './components/InvokerItem';
// Vue3
const dom = ref(h(SlotTester, null, { default: () => [
@ -30,44 +31,49 @@ const vueVer = version;
const items = [
//
{
tag: 'div', data: { ref: test, style: { backgroundColor: 'gray', padding: '10px' } }, children: '字符串渲染测试' // -- OK
tag: 'div', data: { ref: test, style: { backgroundColor: 'gray', padding: '10px' } }, children: '1. 字符串渲染测试'
},
{
tag: 'div', data: { style: { background: 'cyan', padding: '10px' } }, children: [
{ tag: 'span', data: null, children: "嵌套渲染测试1" },
{ tag: 'span', data: null, children: "嵌套渲染测试2" }
] // -- OK
{ tag: 'div', data: null, children: "2. 嵌套渲染测试 I" },
{ tag: 'div', data: null, children: "3. 嵌套渲染测试 II" }
] //
},
//
{
tag: 'slot-tester', data: { padding: '10px', style: { backgroundColor: 'red' } }, children: {
default: () => {
return [
{ tag: 'div', data: { style: { height: '25px', padding: '10px', backgroundColor: 'green' } }, children: "默认插槽嵌套渲染" }, //
{ tag: 'div', data: { style: { height: '25px', padding: '10px', backgroundColor: 'green' } }, children: "4. 默认插槽嵌套渲染" }, //
{ tag: 'div', data: { style: { height: '25px', padding: '10px', backgroundColor: 'blue' } }, children: [
{ tag: 'div', data: { style: { height: '25px', backgroundColor: 'yellow' } }, children: "默认插槽嵌套二层渲染" } //
{ tag: 'div', data: { style: { height: '25px', backgroundColor: 'yellow' } }, children: "5. 默认插槽嵌套二层渲染" } //
] } //
]
},
title: () => "具名插槽渲染测试"
title: () => "6. 具名插槽渲染测试"
} //
},
// {
// tag: JumpBtn,
// data: { url: 'https://ai.com'}, //
// children: "7. " //
// },
{
tag: JumpBtn,
data: { url: 'https://ai.com'} // -- OK
tag: 'div',
children: '7.1 标签插槽测试'
},
{
tag: 'test-parent',
data: {
message: 'test-parent',
message: '8. 事件调用测试',
style: 'color: red',
onCallback: (msg) => { // NativeOn?-- OK
onCallback: (msg) => { // NativeOn?
alert(msg);
console.log(test.value)
}
}
}
];
] as InvokerItem[];
</script>

View File

@ -1,6 +1,6 @@
<template>
<div>
<button @click="HandleClick">事件测试</button>
<button @click="HandleClick"><slot></slot></button>
</div>
</template>

View File

@ -1,4 +1,5 @@
import * as Vue3 from "vue";
// @ts-ignore
import { h, isVue2, isVue3, type VNode, type VNodeChildren, type VNodeData } from "vue-demi";
import { InvokerItem } from "./InvokerItem";
import { ComponentInternalInstance } from "vue-demi/lib/v2/index.js";

View File

@ -4,15 +4,14 @@ import {
onUnmounted,
watch,
} from "vue-demi";
import { ModContext } from "../types/ModContext";
import { InvokerItem } from "./InvokerItem";
let idCount = 0 ;
let idCount = 0;
export const UrlFunc = null as ((name: string) => string) | null;
export default defineComponent({
props: {
name: {
type: String,
default: () => null,
},
height: {
type: String,
default: () => "auto",
@ -23,49 +22,32 @@ export default defineComponent({
},
items: {
type: Array ,
default: () => undefined,
default: () => undefined as InvokerItem[] | undefined,
},
url: {
type: String,
default: () => null,
},
}
},
setup(props, { slots, expose }) {
// const { proxy } = getCurrentInstance();
//
// const id = modService.addInvoker(proxy);
emits: ["destroyed"],
setup(props, { slots, expose, emit }) {
/** Invoker实例ID */
const id = ++idCount;
const isLocalInvoker = false;
let modContext: any = null;
const invokeMethods: { method: string; args: any[] }[] = [];
let isTemplate = false;
/** vue实例上下文此处获取的是Receiver的vue组件实例 */
let receiver: ModContext | null = null;
window["MES_MOD_INVOKERS"] ||= {};
window["MES_MOD_INVOKERS"][id] = {
getRenderContext,
initFinish,
modContext,
receiver,
};
/** 主动更新 */
function updateComponent() {
if (modContext) {
// 判断子页面vue对象是否存在
modContext.renderVersion += 1;
modContext.$forceUpdate(); // 强制渲染
// 重新计算el-button的computed
const list = [modContext];
while (list.length != 0) {
const item = list.pop();
list.push(...item.$children);
const tag = item.$options._componentTag;
if (tag == "el-button" || tag == "ElButton") {
if (item.disabled) item._computedWatchers.buttonDisabled.run();
console.log("[Invoker]: force recalculate computed property...");
}
}
}
// 判断子页面vue对象是否存在
receiver?.Update(); // 强制渲染
}
// 子组件调用
@ -76,32 +58,17 @@ export default defineComponent({
break;
}
}
function initFinish(context) {
modContext = context;
let m: { method: string; args: any[] } | undefined;
while ((m = invokeMethods.pop())) {
invokeMethod(m.method, m.args);
}
console.log("invoker init finish");
}
function invokeMethod(
method: string,
args: any[] = [],
callback: ((result: any) => void) | null = null
) {
// if(!this.component) throw new Error('子组件不支持此方法调用');
if (modContext) {
console.log("method", modContext.$children);
const result = modContext.$children[0][method](...args);
if (callback) {
callback(result);
}
} else {
invokeMethods.push({ method, args });
}
/** 当Receiver初始化完毕后触发 */
function initFinish(context: ModContext) {
receiver = context;
console.log('[Mod-Invoker]initFinish')
}
function getRefs() {
return modContext.$refs;
const refs = receiver?.refs;
if (!refs) throw new Error("[Mod-Invoker] Receiver not found");
return refs;
}
// vue3 渲染上下文
@ -124,19 +91,15 @@ export default defineComponent({
);
onBeforeUpdate(() => {
console.log("invoker-beforeUpdate", slots.default);
if (isTemplate) {
// 只更新slots变更
updateComponent();
}
});
onUnmounted(() => {
console.log("invoker-destroyed");
modContext && modContext.$destroy();
console.log("[Mod-Invoker] invoker-destroyed");
emit('destroyed');
});
return (h) => {
if (isLocalInvoker && slots.default) {
if (slots.default) {
return <div>{slots.default()}</div>;
}

View File

@ -1,6 +1,6 @@
export interface InvokerItem {
/** 组件的标签或是Component对象 */
tag: string,
tag: string | object,
/** 组件数据对象包含attr, props, class, style, 事件等对象 */
data: Record<string, any> | null,
/** 子节点和插槽 */

View File

@ -1,6 +1,6 @@
import { defineComponent, nextTick, onMounted, ref, getCurrentInstance, isVue3, h } from 'vue-demi';
import { defineComponent, nextTick, onMounted, ref, getCurrentInstance, isVue3 } from 'vue-demi';
import { hh } from './DemiHelper';
import SlotTester from '../SlotTester.vue';
import { InvokerContext } from '../types/InvokerContext';
console.log('receiver-loaded');
@ -95,23 +95,44 @@ export default defineComponent({
parentId: {
type: [Number, String],
default: () => 0
},
parentKey: {
type: String,
default: () => 'modInvoker'
}
},
setup(props) {
const instance = getCurrentInstance();
let invokerContext: any = null;
let invokerContext: InvokerContext = null!;
const renderVersion = ref(0);
if (window.parent != window) {
const invoker = window.parent['MES_MOD_INVOKERS'][props.parentId];
const invoker = window.parent[props.parentKey][props.parentId];
invokerContext = invoker;
}
else {
throw new Error("[Receiver] 组件必须在iframe中使用");
}
const renderItems = ref(invokerContext.getRenderContext() ?? []);
onMounted(() => {
nextTick().then(() => {
console.log('mounted-finish');
if (!invokerContext['modContext']) {
invokerContext.initFinish(instance?.proxy);
invokerContext.initFinish({
get renderVersion() {
return renderVersion.value;
},
Update: function (): void {
console.log('[Receiver] Force Update', renderVersion.value);
renderVersion.value += 1;
renderItems.value = invokerContext.getRenderContext() ?? [];
instance?.proxy?.$forceUpdate();
},
get refs() {
return instance?.proxy?.$refs;
}
});
}
});
});
@ -120,7 +141,7 @@ export default defineComponent({
console.log('receiver-update', renderVersion.value);
if (renderVersion.value < 0) return;
if (invokerContext.getRenderContext) {
const itemList = invokerContext.getRenderContext();
const itemList = invokerContext.getRenderContext() ?? [];
// const list = renderContext(itemList);
// console.log('item-list', list);

7
src/types/InvokerContext.d.ts vendored Normal file
View File

@ -0,0 +1,7 @@
import { ModContext } from "./ModContext";
export type InvokerContext = {
getRenderContext: () => unknown[] | undefined;
initFinish: (ctx: ModContext) => void;
receiver: ModContext | null;
}

16
src/types/ModContext.d.ts vendored Normal file
View File

@ -0,0 +1,16 @@
import { DefineComponent } from "vue";
import { Vue2Instance } from "./Vue2Instance";
export type ModContext = {
/** 渲染版本号 */
get renderVersion(): number;
/** 强制组件进行更新 */
Update(): void;
get refs(): {
[key: string]:
| Vue2Instance
| DefineComponent
| Element
| undefined
}
}

63
src/types/Vue2Instance.d.ts vendored Normal file
View File

@ -0,0 +1,63 @@
import { ComponentOptions, nextTick, VNode, WatchOptions } from "vue-demi"
export interface Vue2Instance<
Data = Record<string, any>,
Props = Record<string, any>,
Instance = never,
Options = never,
Emit = (event: string, ...args: any[]) => Vue2Instance
> {
// properties with different types in defineComponent()
readonly $data: Data
readonly $props: Props
readonly $parent: NeverFallback<Instance, Vue2Instance> | null
readonly $root: NeverFallback<Instance, Vue2Instance>
readonly $children: NeverFallback<Instance, Vue2Instance>[]
readonly $options: NeverFallback<Options, ComponentOptions<Vue2Instance>>
$emit: Emit
// Vue 2 only or shared
readonly $el: Element
readonly $refs: {
[key: string]:
| NeverFallback<Instance, Vue2Instance>
| Vue2Instance
| Element
| (NeverFallback<Instance, Vue2Instance> | Vue2Instance | Element)[]
| undefined
}
readonly $slots: { [key: string]: VNode[] | undefined }
readonly $scopedSlots: { [key: string]: NormalizedScopedSlot | undefined }
readonly $isServer: boolean
readonly $ssrContext: any
readonly $vnode: VNode
readonly $attrs: Record<string, string>
readonly $listeners: Record<string, Function | Function[]>
$mount(elementOrSelector?: Element | string, hydrating?: boolean): this
$forceUpdate(): void
$destroy(): void
$set: Function
$delete: Function
$watch(
expOrFn: string,
callback: (this: this, n: any, o: any) => void,
options?: WatchOptions
): () => void
$watch<T>(
expOrFn: (this: this) => T,
callback: (this: this, n: T, o: T) => void,
options?: WatchOptions
): () => void
$on(event: string | string[], callback: Function): this
$once(event: string | string[], callback: Function): this
$off(event?: string | string[], callback?: Function): this
$nextTick: typeof nextTick
$createElement: Function
}
type NeverFallback<T, D> = [T] extends [never] ? D : T
export type NormalizedScopedSlot = (props: any) => ScopedSlotChildren
export type ScopedSlotChildren = VNode[] | undefined