创建editor项目
将上一教程中的hello-world复制过来,改名hello-editor
创建runtime项目
和hello-editor同级
pnpm create vite
删除src/components/HelloWorld.vue
按钮需要用的ts types依赖
pnpm add @tmagic/schema @tmagic/stage
实现runtime
将hello-editor中的render函数实现移植到runtime项目中
<script setup lang="ts">
import {createApp,ref, computed} from 'vue';
import type StageCore from '@tmagic/stage';
const value = ref({
type: 'app',
// 必须加上ID,这个id可能是数据库生成的key,也可以是生成的uuid
id: 1,
items: [],
});
const render = async ({renderer}:StageCore) => {
const root = window.document.createElement('div');
const page = value.value.items[0];
if (!page.value) return root;
const {width = 375, height = 1700} = page.value.style || {};
root.id = `${page.value.id}`;
root.style.cssText = `
width: ${width}px;
height: ${height}px;
`;
createApp(
{
template: '<div v-for="node in config.items" :key="node.id" :id="node.id">hello world</div>',
props: ['config'],
},
{
config: page.value,
},
).mount(root);
renderer.on('onload', () => {
const style = window.document.createElement('style');
// 隐藏滚动条,重置默认样式
style.innerHTML = `
body {
overflow: auto;
}
html,body {
height: 100%; margin: 0;padding: 0;
}
html::-webkit-scrollbar {
width: 0 !important;
display: none;
}
`;
renderer.iframe?.contentDocument?.head.appendChild(style);
renderer.contentWindow?.magic?.onPageElUpdate(root);
renderer.contentWindow?.magic?.onRuntimeReady({});
});
return root;
};
</script>
<template>
<div>
</div>
</template>
<style scoped>
</style>
新建ui-page.vue文件
<template>
<div v-if="config" :id="config.id" :style="style">
<div v-for="node in config.items" :key="node.id" :id="node.id">hello world</div>
</div>
</template>
<script lang="ts" setup>
import { computed } from 'vue';
const props = defineProps<{
config: any;
}>();
const style = computed(() => {
const { width = 375, height = 1700 } = props.config.style || {};
return {
width: `${width}px`,
height: `${height}px`,
};
});
</script>
将以下代码覆盖到src/App.vue中
<template>
<uiPage :config="page"></uiPage>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import uiPage from './ui-page.vue';
const page = ref<any>();
</script>
修改vite.config.js
启动项目
修改hello-editor
删除render props,添加runtimeUrl,修改样式app.vue
<template>
<m-editor
v-model="value"
:runtime-url="runtimeUrl"
:component-group-list="componentGroupList"
></m-editor>
</template>
<script lang="ts" setup>
import {ref, createApp, computed} from 'vue';
import {editorService} from '@tmagic/editor';
const page = computed(() => editorService.get('page'));
const value = ref({
type: 'app',
// 必须加上ID,这个id可能是数据库生成的key,也可以是生成的uuid
id: 1,
items: [],
});
const componentGroupList = ref([
{
title: '组件列表',
items: [
{
icon: 'https://vfiles.gtimg.cn/vupload/20220614/9cc3091655207317835.png',
text: 'HelloWorld',
type: 'hello-world',
},
],
},
]);
const runtimeUrl = 'http://localhost:8078/';
</script>
<style>
#app {
overflow: auto;
}
html,body,#app {
height: 100%; margin: 0;padding: 0;
}
#app::-webkit-scrollbar {
width: 0 !important;
display: none;
}
</style>
启动hello-editor
跨域问题
修改editor-runtime下的vite.config.js
server: {
port: 8078, //指定端口号
headers:{
'Access-Control-Allow-Origin': '*',
}
},
runtime与editor通信
到这里项目就可以正常访问了,但是会发现添加组件没有反应。
这是因为在runtime中无法直接获取到editor中的dsl,所以需要通过editor注入到window的magic api来交互
如出现在runtime中出现magic为undefined, 可以尝试在App.vue中通过监听message,来准备获取magic注入时机,然后调用magic.onRuntimeReady,示例代码如下
window.addEventListener('message', ({ data }) => {
if (!data.tmagicRuntimeReady) {
return;
}
window.magic?.onRuntimeReady({
// ...
});
});
注意:这里可能会出现editor抛出message的时候,runtime还没有执行到监听message的情况 编辑器只在iframe onload事件中抛出message 如果出现runtime中接收不到message的情况,可以尝试在onMounted的时候调用magic.onRuntimeReady
修改main.js为main.ts
import { createApp } from 'vue'
import type { Magic } from '@tmagic/stage';
import './style.css';
import App from './App.vue';
declare global {
interface Window {
magic?: Magic;
}
}
createApp(App).mount('#app')
新增tsconfig.json
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"strict": true,
"jsx": "preserve",
"moduleResolution": "node",
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"useDefineForClassFields": true,
"sourceMap": true,
"baseUrl": ".",
"types": [
"webpack-env"
],
"paths": {
"@/*": [
"src/*"
]
},
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
},
"include": [
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.vue",
"tests/**/*.ts",
"tests/**/*.tsx"
],
"exclude": [
"node_modules"
]
}
src下新增shims-vue.d.ts
/* eslint-disable */
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}
修改runtime下的app.vue
<template>
<uiPage :config="page"></uiPage>
</template>
<script lang="ts" setup>
import type { RemoveData, UpdateData } from '@tmagic/stage';
import type { Id, MApp, MNode } from '@tmagic/schema';
import { ref,reactive } from 'vue';
import uiPage from './ui-page.vue';
const root = ref<MApp>();
const page = ref<any>();
window.magic?.onRuntimeReady({
/** 当编辑器的dsl对象变化时会调用 */
updateRootConfig(config: MApp) {
root.value = config;
},
/** 当编辑器的切换页面时会调用 */
updatePageId(id: Id) {
page.value = root.value?.items?.find((item) => item.id === id);
},
/** 新增组件时调用 */
add({ config }: UpdateData) {
const parent = config.type === 'page' ? root.value : page.value;
parent.items?.push(config);
},
/** 更新组件时调用 */
update({ config }: UpdateData) {
const index = page.value.items?.findIndex((child: MNode) => child.id === config.id);
page.value.items.splice(index, 1, reactive(config));
},
/** 删除组件时调用 */
remove({ id }: RemoveData) {
const index = page.value.items?.findIndex((child: MNode) => child.id === id);
page.value.items.splice(index, 1);
},
});
</script>
同步页面dom给编辑器
由于组件渲染在runtime中,对于编辑器来说是个黑盒,并不知道哪个dom节点才是页面(对于dsl的解析渲染可能是千奇百怪的),所以需要将页面的dom节点同步给编辑器
修改runtime下的app.vue
const pageComp = ref<InstanceType<typeof uiPage>>();
watch(page, async () => {
// page配置变化后,需要等dom更新
await nextTick();
window?.magic?.onPageElUpdate(pageComp.value?.$el);
});
以上就是一个简单runtime实现,以及与编辑的交互,这是一个不完善的实现(会发现组件再画布中无法自由拖动,是因为没有完整的解析style),但是其中已经几乎覆盖所有需要关心的内容
当前教程中实现了一个简单的page,tmagic提供了一个比较完善的实现,将在下一节介绍