初始创建!
This commit is contained in:
commit
fa8b468c32
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
3
.vscode/extensions.json
vendored
Normal file
3
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"recommendations": ["Vue.volar"]
|
||||||
|
}
|
48
README.md
Normal file
48
README.md
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
# Vue Mod Page
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
这是一个工具库,用于在Vue项目跨越IFrame来调用另一个Vue项目,并且同时支持了Vue2.7和Vue3的跨版本调用。
|
||||||
|
|
||||||
|
## 功能
|
||||||
|
|
||||||
|
- 基于IFrame来调用另一个Vue项目(以下称“子项目”)
|
||||||
|
- 可以在子项目中渲染原生HTML元素,或是已注册为全局组件的Vue组件
|
||||||
|
- 支持Vue2.7和Vue3的跨版本调用
|
||||||
|
- 支持属性传参(props),CSS类(classes),样式(style),事件(onXXX)的传递
|
||||||
|
- 支持子节点(children)渲染
|
||||||
|
- 支持具名插槽(slot)
|
||||||
|
- 支持作用域插槽(slot-scope)
|
||||||
|
- 支持Vue模板引用(ref)
|
||||||
|
|
||||||
|
## 使用
|
||||||
|
|
||||||
|
1. 在主项目和子项目中引入该工具库
|
||||||
|
2. 在子项目中引入`Receiver`组件,然后将该组件注册到路由上
|
||||||
|
3. 在子项目中将需要被主项目调用的Vue组件注册为全局组件
|
||||||
|
|
||||||
|
```vue
|
||||||
|
// Vue3
|
||||||
|
app.component('MyComponent', MyComponent)
|
||||||
|
// Vue2
|
||||||
|
Vue.component('MyComponent', MyComponent)
|
||||||
|
```
|
||||||
|
|
||||||
|
4. 在主项目中引入`Invoker`组件,并传入`url`和`items`属性
|
||||||
|
- `url`:子项目`Receiver`的URL
|
||||||
|
- `items`: 包含子项目渲染元素定义的列表,见`InvokerItem`的类型定义
|
||||||
|
5. 根据使用的Vue版本,在两个项目的目录下执行以下命令
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Vue2.7
|
||||||
|
npm run vue-demi-switch 2.7 vue2
|
||||||
|
# Vue3
|
||||||
|
npm run vue-demi-switch 3
|
||||||
|
```
|
||||||
|
|
||||||
|
6. 运行项目查看效果
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. 主项目和子项目需要同源,或是配置好了CORS策略
|
||||||
|
2. 该库对Vue2.7的支持有限
|
12
child.html
Normal file
12
child.html
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Vite + Vue + TS</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/child.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
12
index.html
Normal file
12
index.html
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Vite + Vue + TS</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
11
index.ts
Normal file
11
index.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import Invoker from "./src/components/Invoker";
|
||||||
|
import { hh } from './src/components/DemiHelper';
|
||||||
|
import { InvokerItem } from "./src/components/InvokerItem";
|
||||||
|
import Receiver from "./src/components/Receiver";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Invoker,
|
||||||
|
Receiver,
|
||||||
|
type InvokerItem,
|
||||||
|
hh,
|
||||||
|
}
|
36
package.json
Normal file
36
package.json
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"name": "vue-modpage",
|
||||||
|
"private": true,
|
||||||
|
"version": "1.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vue-tsc -b && vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"switch:2": "vue-demi-switch 2.7 vue2",
|
||||||
|
"switch:3": "vue-demi-switch 3",
|
||||||
|
"test:2": "vue-demi-switch 2 vue2 && jest",
|
||||||
|
"test:3": "vue-demi-switch 3 && jest"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"vue-demi": "^0.14.10"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"vue": "^2.7.0 || >=3.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^22.10.2",
|
||||||
|
"@vitejs/plugin-vue": "^5.2.1",
|
||||||
|
"@vitejs/plugin-vue-jsx": "^4.1.1",
|
||||||
|
"@vitejs/plugin-vue2": "^2.2.0",
|
||||||
|
"@vitejs/plugin-vue2-jsx": "^1.1.0",
|
||||||
|
"typescript": "~5.6.2",
|
||||||
|
"vite": "^6.0.1",
|
||||||
|
"vitest": "^2.1.8",
|
||||||
|
"vue": "^3.5.13",
|
||||||
|
"vue-template-compiler": "2.7.16",
|
||||||
|
"vue-tsc": "^2.1.10",
|
||||||
|
"vue2": "npm:vue@2.7.16"
|
||||||
|
},
|
||||||
|
"packageManager": "pnpm@9.1.1+sha1.09ada6cd05003e0ced25fb716f9fda4063ec2e3b"
|
||||||
|
}
|
2185
pnpm-lock.yaml
Normal file
2185
pnpm-lock.yaml
Normal file
File diff suppressed because it is too large
Load Diff
1
public/vite.svg
Normal file
1
public/vite.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
After Width: | Height: | Size: 1.5 KiB |
74
src/App.vue
Normal file
74
src/App.vue
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
vue-{{ vueVer }}
|
||||||
|
<Invoker url="/child.html#?id={id}" style="border:1px solid red;height: 400px;overflow-y: scroll;" :items="items" />
|
||||||
|
<component :is="dom" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, version, ref, h } from 'vue-demi';
|
||||||
|
import Invoker from './components/Invoker'
|
||||||
|
import JumpBtn from './JumpBtn.vue';
|
||||||
|
import SlotTester from './SlotTester.vue';
|
||||||
|
|
||||||
|
// Vue3
|
||||||
|
const dom = ref(h(SlotTester, null, { default: () => [
|
||||||
|
h('div', { style: { height: '25px', padding: '10px', backgroundColor: 'green' } }, "bbb"), // 一层嵌套
|
||||||
|
], title: () => "title"}));
|
||||||
|
// Vue2
|
||||||
|
// const dom = ref({ render: () => h(JumpBtn, { props: { url: '111' } }) });
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
console.log(test.value);
|
||||||
|
}, 3000);
|
||||||
|
});
|
||||||
|
|
||||||
|
const test = ref<HTMLDivElement>();
|
||||||
|
const vueVer = version;
|
||||||
|
const items = [
|
||||||
|
// 原生元素渲染测试
|
||||||
|
{
|
||||||
|
tag: 'div', data: { ref: test, style: { backgroundColor: 'gray', padding: '10px' } }, children: '字符串渲染测试' // 字符串测试 -- OK
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: 'div', data: { style: { background: 'cyan', padding: '10px' } }, children: [
|
||||||
|
{ tag: 'span', data: null, children: "嵌套渲染测试1" },
|
||||||
|
{ tag: 'span', data: null, children: "嵌套渲染测试2" }
|
||||||
|
] // 子组件列表测试 -- OK
|
||||||
|
},
|
||||||
|
// 组件渲染测试
|
||||||
|
{
|
||||||
|
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: 'blue' } }, children: [
|
||||||
|
{ tag: 'div', data: { style: { height: '25px', backgroundColor: 'yellow' } }, children: "默认插槽嵌套二层渲染" } // 二层嵌套
|
||||||
|
] } // 三层嵌套
|
||||||
|
]
|
||||||
|
},
|
||||||
|
title: () => "具名插槽渲染测试"
|
||||||
|
} // 具名插槽测试
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: JumpBtn,
|
||||||
|
data: { url: 'https://ai.com'} // 组件传参测试 -- OK
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: 'test-parent',
|
||||||
|
data: {
|
||||||
|
message: 'test-parent',
|
||||||
|
style: 'color: red',
|
||||||
|
onCallback: (msg) => { // 事件测试(可能要测试NativeOn?)-- OK
|
||||||
|
alert(msg);
|
||||||
|
console.log(test.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
21
src/JumpBtn.vue
Normal file
21
src/JumpBtn.vue
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<button @click="HandleClick">事件测试</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang='ts'>
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
url: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const HandleClick = () => {
|
||||||
|
alert('Jump To ' + props.url);
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
18
src/SlotTester.vue
Normal file
18
src/SlotTester.vue
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div style="color: dimgray">
|
||||||
|
<slot name="title">Default Title</slot>
|
||||||
|
</div>
|
||||||
|
<div style="color: indigo">
|
||||||
|
<slot>Default Content</slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang='ts'>
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
29
src/child.tsx
Normal file
29
src/child.tsx
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { createApp, defineComponent } from 'vue-demi'
|
||||||
|
|
||||||
|
import Receiver from './components/Receiver'
|
||||||
|
import SlotTester from './SlotTester.vue';
|
||||||
|
const App = defineComponent({
|
||||||
|
setup(){
|
||||||
|
return (h)=>(<Receiver parent-id={1}></Receiver>)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const app = createApp(App);
|
||||||
|
app.component('slot-tester', SlotTester);
|
||||||
|
app.component('test-parent',defineComponent({
|
||||||
|
props:{
|
||||||
|
message:{
|
||||||
|
type:String,
|
||||||
|
default:()=>'empty'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setup(props, { slots, emit }) {
|
||||||
|
return (h) => (<div>{props.message} <button onClick={() => emit('callback', 'payload-test')}>测试</button> {slots.default?.()}</div>)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
app.component('test-child',defineComponent({
|
||||||
|
setup(){
|
||||||
|
return (h)=>(<div>test-child</div>)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
app.mount('#app')
|
132
src/components/DemiHelper.ts
Normal file
132
src/components/DemiHelper.ts
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
import * as Vue3 from "vue";
|
||||||
|
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";
|
||||||
|
|
||||||
|
function splitAttrs(obj: object) : { attrs: Record<string, any>, on: Record<string, any> } {
|
||||||
|
const attrs: Record<string, any> = {};
|
||||||
|
const listeners: Record<string, any> = {};
|
||||||
|
for (const key in obj) {
|
||||||
|
if (key.indexOf('on') == 0) {
|
||||||
|
const newKey = key[2].toLowerCase() + key.substring(3);
|
||||||
|
listeners[newKey] = obj[key];
|
||||||
|
} else {
|
||||||
|
attrs[key] = obj[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log("Listener", listeners)
|
||||||
|
return { attrs, on: listeners };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将扁平化的组件数据对象分离为Vue2形式的data对象
|
||||||
|
* @param rawData
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
function splitVue2Data(rawData?: Record<string, any> | null) {
|
||||||
|
console.warn("Handling Vue2 data: ", rawData);
|
||||||
|
if (!rawData) return {};
|
||||||
|
|
||||||
|
if (isVue3) throw new Error("Vue3 data object is not supported in Vue2");
|
||||||
|
// Vue2分离式处理
|
||||||
|
else {
|
||||||
|
const on = {}; // 可能需要区分NativeOn
|
||||||
|
const { class: cls, style, attrs = {}, props = {}, ...rest } = rawData;
|
||||||
|
|
||||||
|
// 处理事件
|
||||||
|
for (const key in rest) {
|
||||||
|
if (key.indexOf('on') == 0 && key != 'on') {
|
||||||
|
const newKey = key[2].toLowerCase() + key.substring(3);
|
||||||
|
on[newKey] = rest[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const v2data = {
|
||||||
|
class: cls,
|
||||||
|
style,
|
||||||
|
attrs: attrs,
|
||||||
|
props: { ...props, ...rest }, // 未明确的属性放入props
|
||||||
|
on,
|
||||||
|
}
|
||||||
|
return v2data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 渲染具名插槽
|
||||||
|
* @param slotFn
|
||||||
|
* @param args
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
function renderSlot(slotFn: Function, args: any, ctx?: ComponentInternalInstance) {
|
||||||
|
let child: string | Array<InvokerItem> | InvokerItem = slotFn(args);
|
||||||
|
// console.warn("Rendering Slot: ", child);
|
||||||
|
// 字符串情况
|
||||||
|
if (typeof child == 'string') {
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
// Array<InvokerItem>情况
|
||||||
|
else if (Array.isArray(child)) {
|
||||||
|
return child.map((c: InvokerItem) => hh(c.tag, c.data, c.children, ctx));
|
||||||
|
}
|
||||||
|
// InvokerItem情况
|
||||||
|
else {
|
||||||
|
return hh(child.tag, child.data, child.children, ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hh(tag: string, data?: Record<string, any> | null, children?: string | Array<InvokerItem> | Record<string, Function>, ctx?: ComponentInternalInstance) {
|
||||||
|
// 适配Vue2渲染函数结构
|
||||||
|
// console.debug("Rendering", tag, data, children, `Vue Version: ${isVue2 ? 'Vue2' : 'Vue3'}`);
|
||||||
|
|
||||||
|
// 处理tag
|
||||||
|
let processedTag: string | object = tag;
|
||||||
|
if (isVue3 && typeof tag == 'string') {
|
||||||
|
// Vue 3 需要解析全局组件
|
||||||
|
// @ts-ignore - Vue 3 的 resolveComponent 需要特殊处理
|
||||||
|
const resolved = Vue3?.resolveComponent?.(tag);
|
||||||
|
processedTag = resolved || tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理data
|
||||||
|
let processedData: any = null;
|
||||||
|
if (isVue2) {
|
||||||
|
processedData = splitVue2Data(data);
|
||||||
|
}
|
||||||
|
else processedData = data;
|
||||||
|
|
||||||
|
// 处理children
|
||||||
|
let processedChildren: any = {};
|
||||||
|
// 字符串则直接返回
|
||||||
|
if (typeof children == 'string') {
|
||||||
|
processedChildren = children
|
||||||
|
}
|
||||||
|
// 子节点列表则递归处理
|
||||||
|
else if (Array.isArray(children)) {
|
||||||
|
processedChildren = children.map(child => hh(child.tag, child.data, child.children, ctx));
|
||||||
|
}
|
||||||
|
// 处理具名插槽对象
|
||||||
|
else if (children && typeof children == 'object') {
|
||||||
|
if (isVue3) {
|
||||||
|
processedChildren = Object.fromEntries(
|
||||||
|
Object.entries(children).map(([name, slotFn]) => [
|
||||||
|
name,
|
||||||
|
(args) => renderSlot(slotFn, args, ctx)
|
||||||
|
])
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// 兼容Vue2插槽
|
||||||
|
processedData['scopedSlots'] = Object.fromEntries(
|
||||||
|
Object.entries(children).map(([name, slotFn]) => [
|
||||||
|
name,
|
||||||
|
(args) => {
|
||||||
|
return renderSlot(slotFn, args);
|
||||||
|
}
|
||||||
|
])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.debug("Processed", processedTag, processedData, processedChildren);
|
||||||
|
return h(processedTag, processedData, processedChildren);
|
||||||
|
}
|
152
src/components/Invoker.tsx
Normal file
152
src/components/Invoker.tsx
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
import {
|
||||||
|
defineComponent,
|
||||||
|
onBeforeUpdate,
|
||||||
|
onUnmounted,
|
||||||
|
watch,
|
||||||
|
} from "vue-demi";
|
||||||
|
|
||||||
|
let idCount = 0 ;
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
default: () => null,
|
||||||
|
},
|
||||||
|
height: {
|
||||||
|
type: String,
|
||||||
|
default: () => "auto",
|
||||||
|
},
|
||||||
|
scroll: {
|
||||||
|
type: Boolean,
|
||||||
|
default: () => false,
|
||||||
|
},
|
||||||
|
items: {
|
||||||
|
type: Array ,
|
||||||
|
default: () => undefined,
|
||||||
|
},
|
||||||
|
url: {
|
||||||
|
type: String,
|
||||||
|
default: () => null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup(props, { slots, expose }) {
|
||||||
|
// const { proxy } = getCurrentInstance();
|
||||||
|
//
|
||||||
|
// const id = modService.addInvoker(proxy);
|
||||||
|
const id = ++idCount;
|
||||||
|
const isLocalInvoker = false;
|
||||||
|
let modContext: any = null;
|
||||||
|
const invokeMethods: { method: string; args: any[] }[] = [];
|
||||||
|
let isTemplate = false;
|
||||||
|
|
||||||
|
window["MES_MOD_INVOKERS"] ||= {};
|
||||||
|
window["MES_MOD_INVOKERS"][id] = {
|
||||||
|
getRenderContext,
|
||||||
|
initFinish,
|
||||||
|
modContext,
|
||||||
|
};
|
||||||
|
|
||||||
|
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...");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 子组件调用
|
||||||
|
function eventCallback(type: string, ...args: any[]) {
|
||||||
|
switch (type) {
|
||||||
|
case "created":
|
||||||
|
initFinish(args[0]);
|
||||||
|
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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function getRefs() {
|
||||||
|
return modContext.$refs;
|
||||||
|
}
|
||||||
|
|
||||||
|
// vue3 渲染上下文
|
||||||
|
function getRenderContext() {
|
||||||
|
return props.items;
|
||||||
|
}
|
||||||
|
|
||||||
|
expose({
|
||||||
|
getRefs,
|
||||||
|
getRenderContext,
|
||||||
|
eventCallback,
|
||||||
|
initFinish,
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.items,
|
||||||
|
() => {
|
||||||
|
updateComponent();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
onBeforeUpdate(() => {
|
||||||
|
console.log("invoker-beforeUpdate", slots.default);
|
||||||
|
if (isTemplate) {
|
||||||
|
// 只更新slots变更
|
||||||
|
updateComponent();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
onUnmounted(() => {
|
||||||
|
console.log("invoker-destroyed");
|
||||||
|
modContext && modContext.$destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
return (h) => {
|
||||||
|
if (isLocalInvoker && slots.default) {
|
||||||
|
return <div>{slots.default()}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<iframe
|
||||||
|
src={props.url.replace('{id}',id.toString())}
|
||||||
|
scrolling={props.scroll ? "yes" : "no"}
|
||||||
|
style={`border: none; width: 100%; height: ${props.height}; `}
|
||||||
|
></iframe>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
8
src/components/InvokerItem.ts
Normal file
8
src/components/InvokerItem.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
export interface InvokerItem {
|
||||||
|
/** 组件的标签或是Component对象 */
|
||||||
|
tag: string,
|
||||||
|
/** 组件数据对象,包含attr, props, class, style, 事件等对象 */
|
||||||
|
data: Record<string, any> | null,
|
||||||
|
/** 子节点和插槽 */
|
||||||
|
children: string | Array<InvokerItem> | Record<string, Function>
|
||||||
|
}
|
136
src/components/Receiver.ts
Normal file
136
src/components/Receiver.ts
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
import { defineComponent, nextTick, onMounted, ref, getCurrentInstance, isVue3, h } from 'vue-demi';
|
||||||
|
import { hh } from './DemiHelper';
|
||||||
|
import SlotTester from '../SlotTester.vue';
|
||||||
|
|
||||||
|
console.log('receiver-loaded');
|
||||||
|
|
||||||
|
|
||||||
|
function renderContext(item: object | Array<any>, toSlot: boolean = false) {
|
||||||
|
/**
|
||||||
|
* 对象为字符串的情况
|
||||||
|
* # { tag: 'div', attrs: undefined, children: 'hhh' } => h('span', undefined, 'hhh')
|
||||||
|
*/
|
||||||
|
if (typeof item === 'string')
|
||||||
|
return item;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对象为函数的情况
|
||||||
|
* # { tag: 'div', attrs: undefined, children: () => 'hhh' } => h('span', undefined, 'hhh')
|
||||||
|
*/
|
||||||
|
if (typeof (item) === "function") {
|
||||||
|
|
||||||
|
// return item;
|
||||||
|
return item();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对象为数组的情况
|
||||||
|
* # { tag: 'div', attrs: undefined, [ { tag: 'span', attrs: undefined, 'hhh' } ] } => h('div', undefined, [ h('span', undefined, 'hhh') ])
|
||||||
|
*/
|
||||||
|
if (Array.isArray(item)) {
|
||||||
|
|
||||||
|
const list = item.map(i => {
|
||||||
|
if (isVue3) {
|
||||||
|
// const comp = resolveComponent(i.tag);
|
||||||
|
return hh(i.tag, i.attrs, i.children);
|
||||||
|
// return hDemi(comp, i.attrs, renderContext(i.children, typeof (comp) !== 'string') as any); // vue3
|
||||||
|
}
|
||||||
|
|
||||||
|
// const { attrs, listeners, ref } = splitAttrs(i.attrs);
|
||||||
|
const slots = renderContext(i.children);
|
||||||
|
if (typeof (slots) === "object") {
|
||||||
|
return hh(i.tag, {
|
||||||
|
attrs: i.attrs
|
||||||
|
// attrs,
|
||||||
|
// on: listeners,
|
||||||
|
// scopedSlots: slots,
|
||||||
|
// ref
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return hh(i.tag, {
|
||||||
|
attrs: i.attrs,
|
||||||
|
}, slots);
|
||||||
|
|
||||||
|
});
|
||||||
|
if (toSlot) {
|
||||||
|
return () => list;
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 复合对象的情况
|
||||||
|
* #
|
||||||
|
*/
|
||||||
|
const children: Record<string, any> = {};
|
||||||
|
for (const key in item) {
|
||||||
|
|
||||||
|
children[key] = function (scope) {
|
||||||
|
const child = item[key](scope);
|
||||||
|
if (isVue3) {
|
||||||
|
return hh(child.tag, child.attrs, renderContext(child.children, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(child)) {
|
||||||
|
const slots = renderContext(child);
|
||||||
|
return slots;
|
||||||
|
} else {
|
||||||
|
// const { attrs, listeners, ref } = splitAttrs(child.attrs);
|
||||||
|
const slots = renderContext(child.children);
|
||||||
|
return hh(child.tag, {
|
||||||
|
attrs: child.attrs
|
||||||
|
// attrs,
|
||||||
|
// on: listeners,
|
||||||
|
// ref
|
||||||
|
}, slots);
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'Receiver',
|
||||||
|
props: {
|
||||||
|
parentId: {
|
||||||
|
type: [Number, String],
|
||||||
|
default: () => 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setup(props) {
|
||||||
|
const instance = getCurrentInstance();
|
||||||
|
let invokerContext: any = null;
|
||||||
|
const renderVersion = ref(0);
|
||||||
|
if (window.parent != window) {
|
||||||
|
const invoker = window.parent['MES_MOD_INVOKERS'][props.parentId];
|
||||||
|
invokerContext = invoker;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
nextTick().then(() => {
|
||||||
|
console.log('mounted-finish');
|
||||||
|
if (!invokerContext['modContext']) {
|
||||||
|
invokerContext.initFinish(instance?.proxy);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
try {
|
||||||
|
console.log('receiver-update', renderVersion.value);
|
||||||
|
if (renderVersion.value < 0) return;
|
||||||
|
if (invokerContext.getRenderContext) {
|
||||||
|
const itemList = invokerContext.getRenderContext();
|
||||||
|
|
||||||
|
// const list = renderContext(itemList);
|
||||||
|
// console.log('item-list', list);
|
||||||
|
return hh('div', undefined, itemList);
|
||||||
|
}
|
||||||
|
return hh('div');
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.error("[Receiver] 渲染节点过程中出现错误", e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
5
src/main.ts
Normal file
5
src/main.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { createApp, Vue2 } from 'vue-demi'
|
||||||
|
import App from './App.vue'
|
||||||
|
|
||||||
|
const app = createApp(App)
|
||||||
|
app.mount('#app')
|
1
src/vite-env.d.ts
vendored
Normal file
1
src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
27
tsconfig.app.json
Normal file
27
tsconfig.app.json
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true,
|
||||||
|
"noImplicitAny": false
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "index.ts"]
|
||||||
|
}
|
7
tsconfig.json
Normal file
7
tsconfig.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./tsconfig.app.json" },
|
||||||
|
{ "path": "./tsconfig.node.json" }
|
||||||
|
]
|
||||||
|
}
|
24
tsconfig.node.json
Normal file
24
tsconfig.node.json
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||||
|
"target": "ES2022",
|
||||||
|
"lib": ["ES2023"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
38
vite.config.ts
Normal file
38
vite.config.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
import vueJsx from '@vitejs/plugin-vue-jsx'
|
||||||
|
|
||||||
|
import vue2 from '@vitejs/plugin-vue2'
|
||||||
|
import vue2Jsx from '@vitejs/plugin-vue2-jsx'
|
||||||
|
import { isVue2, isVue3,version } from 'vue-demi'
|
||||||
|
import path from 'node:path'
|
||||||
|
import { createRequire } from 'node:module'
|
||||||
|
|
||||||
|
const resolve = (str: string) => {
|
||||||
|
return path.resolve(__dirname, str)
|
||||||
|
}
|
||||||
|
console.log('vue',version)
|
||||||
|
|
||||||
|
function getV2Compiler() {
|
||||||
|
const req = createRequire(import.meta.url);
|
||||||
|
const rt = req.resolve("./node_modules/vue2/compiler-sfc");
|
||||||
|
console.log(rt);
|
||||||
|
const c = req(rt)
|
||||||
|
console.log(c);
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: isVue3 ? [vue(), vueJsx()] : [vue2({
|
||||||
|
compiler: getV2Compiler()
|
||||||
|
}), vue2Jsx()],
|
||||||
|
resolve:{
|
||||||
|
alias:{
|
||||||
|
vue: isVue2 ? resolve('./node_modules/vue2') : resolve('./node_modules/vue'),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
optimizeDeps: {
|
||||||
|
exclude: ['vue-demi']
|
||||||
|
},
|
||||||
|
})
|
Loading…
Reference in New Issue
Block a user