roymondchen 873a51fc87 docs: 升级 VitePress 至 v2 alpha,类型引用改为源码片段同步
- 升级 vitepress 到 ^2.0.0-alpha.17
- vite.optimizeDeps.rolldownOptions.transform.define 迁移至 vite.define 以适配 v2 API
- 同步升级 vitest/rolldown/vue/vite 等周边依赖
- 文档中类型链接统一改为 <<< 片段引用源码 region,避免 commit hash 链接失效
- packages/{core,editor,form-schema,schema,stage} 相关类型加 // #region 锚点
- 移除已废弃的 docs/guide/advanced/tmagic-ui.md 及侧栏入口

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-18 11:47:03 +08:00

295 lines
7.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 3.[DSL](../conception.md#dsl) 解析渲染
tmagic 提供了 vue/react 两个版本的解析渲染组件,可以直接使用。基础渲染组件以 container 为核心,配合 page、button、img、text 等多个独立的 npm 包,分别发布在 `vue-components/``react-components/` 下:
vue 版本:
- [@tmagic/vue-container](https://www.npmjs.com/package/@tmagic/vue-container)
- [@tmagic/vue-page](https://www.npmjs.com/package/@tmagic/vue-page)
- [@tmagic/vue-button](https://www.npmjs.com/package/@tmagic/vue-button)
- [@tmagic/vue-img](https://www.npmjs.com/package/@tmagic/vue-img)
- [@tmagic/vue-text](https://www.npmjs.com/package/@tmagic/vue-text)
- 其他:`@tmagic/vue-overlay``@tmagic/vue-qrcode``@tmagic/vue-page-fragment``@tmagic/vue-page-fragment-container``@tmagic/vue-iterator-container`
react 版本:
- [@tmagic/react-container](https://www.npmjs.com/package/@tmagic/react-container)
- [@tmagic/react-page](https://www.npmjs.com/package/@tmagic/react-page)
- [@tmagic/react-button](https://www.npmjs.com/package/@tmagic/react-button)
- [@tmagic/react-img](https://www.npmjs.com/package/@tmagic/react-img)
- [@tmagic/react-text](https://www.npmjs.com/package/@tmagic/react-text)
- 其他:`@tmagic/react-overlay``@tmagic/react-qrcode``@tmagic/react-page-fragment``@tmagic/react-page-fragment-container``@tmagic/react-iterator-container`
接下来是以 vue 为基础,来讲述如何实现一个类似 [@tmagic/vue-container](https://www.npmjs.com/package/@tmagic/vue-container) 的渲染器
## 准备工作
### 创建项目
将[上一教程](./runtime.md)中的[editor-runtime](https://github.com/jia000/tmagic-tutorial/tree/master/course2/editor-runtime)和[hello-editor](https://github.com/jia000/tmagic-tutorial/tree/master/course2/hello-editor)复制过来
## 基础概念
### 节点Node
每一个组件最终都是由一个节点来描述每个节点至少拥有id,type两个属性
id: 节点的唯一标识,不可重复
type: 节点的类型,有业务自行定义
### 容器Container
容器也是节点的一种容器可以包含多个节点并且是保存在items属性下
items: 容器下包含的节点组成的数组items中不能有page,app
### 页面Page)
页面是容器的一种type固定为pageitems中不能有page
### 根Root)
根节点也是一个容器type固定为appitems只能是page
## 实现
创建hello-ui目录
```
.
└─editor-runtime
└─hello-editor
└─hello-ui
```
### 渲染节点
在hello-ui下创建 Component.vue 文件
由于节点的type是由业务自行定义的所以需要使用动态组件渲染在vue下可以使用[component](https://cn.vuejs.org/api/built-in-special-elements.html#component)组件来实现
[component](https://cn.vuejs.org/api/built-in-special-elements.html#component) 是通过is参数来决定哪个组件被渲染所以将type与组件做绑定
例如有组件 HelloWorld可以将组件全局注册
```js
app.component('hello-world', HelloWorld);
```
然后将'hello-world'作为type那么is="hello-world"就会渲染 HelloWorld 组件
为了让组件渲染出来的dom能被编辑器识别到还需要将节点的id作为dom的id
```vue
<template>
<component v-if="config" :is="type" :data-tmagic-id="`${id}`" :style="style" :config="config"></component>
</template>
<script lang=ts setup>
import { computed } from 'vue';
import type { MNode } from '@tmagic/schema';
// 将节点作品参数传入组件中
const props = defineProps<{
config: MNode;
}>();
const type = computed(() => {
if (!props.config.type || ['page', 'container'].includes(props.config.type)) return 'div';
return props.config.type;
});
const id = computed(() => props.config.id);
</script>
```
接下来就需要解析节点的样式,在@tmagic/editor中默认会将样式配置保存到节点的style属性中,如果自行定义到了其他属性,则以实际为准
解析style需要注意几个地方
1. 数字
css中的数值有些是需要单位的例如px有些是不需要的例如opacity
@tmagic/editor中,默认都是不带单位的,所以需要将需要单位的地方补齐单位
这里做补齐px处理如果需要做屏幕大小适应 可以使用rem或者vw这个可以根据自身需求处理。
2. url
css中的[url](https://developer.mozilla.org/zh-CN/docs/Web/CSS/url)需要使用 url()所以当值为url时需要转为url(xxx)
3. transform
[transform](https://developer.mozilla.org/zh-CN/docs/Web/CSS/transform)属性可以指定为关键字值none 或一个或多个transform-function值。
```ts
const fillBackgroundImage = (value: string) => {
if (value && !/^url/.test(value) && !/^linear-gradient/.test(value)) {
return `url(${value})`;
}
return value;
};
const style = computed(() => {
if (!props.config.style) {
return {};
}
const results: Record<string, any> = {};
const whiteList = ['zIndex', 'opacity', 'fontWeight'];
Object.entries(props.config.style).forEach(([key, value]) => {
if (key === 'backgroundImage') {
value && (results[key] = fillBackgroundImage(value));
} else if (key === 'transform' && typeof value !== 'string') {
results[key] = Object.entries(value as Record<string, string>)
.map(([transformKey, transformValue]) => {
let defaultValue = 0;
if (transformKey === 'scale') {
defaultValue = 1;
}
return `${transformKey}(${transformValue || defaultValue})`;
})
.join(' ');
} else if (!whiteList.includes(key) && value && /^[-]?[0-9]*[.]?[0-9]*$/.test(value)) {
results[key] = `${value}px`;
} else {
results[key] = value;
}
});
return results;
});
```
### 渲染容器
容器与普通节点的区别就是需要多一个items的解析
新增Container.vue文件
```vue
<template>
<Component :config="config">
<Component v-for="item in config.items" :key="item.id" :config="item"></Component>
</Component>
</template>
<script lang="ts" setup>
import type { MContainer } from '@tmagic/schema';
import Component from './Component.vue';
defineProps<{
config: MContainer;
}>();
</script>
```
### 渲染页面
页面就是容器之所以单独存在是页面会自己的方法例如reload等
Page.vue文件
```vue
<template>
<Container :config="config"></Container>
</template>
<script lang="ts" setup>
import type { MPage } from '@tmagic/schema';
import Container from './Container.vue';
defineProps<{
config: MPage;
}>();
defineExpose({
reload() {
window.location.reload();
}
});
</script>
```
## 在runtime中使用 hello-ui
删除editor-runtime/src/ui-page.vue
将App.vue中的ui-page改成hello-ui中的Page
```vue
<template>
<Page v-if="page" :config="page" ref="pageComp"></Page>
</template>
<script lang="ts" setup>
// eslint-disable-next-line
import { Page } from 'hello-ui';
<script>
```
在editor-runtime/vue.config.js中加上配置
```ts
configureWebpack: {
resolve: {
alias: {
'hello-ui': path.resolve(__dirname, '../hello-ui'),
vue$: path.resolve(__dirname, './node_modules/vue'),
},
},
},
```
## 添加HelloWorld组件
在hello-ui下新增HelloWorld.vue
```vue
<template>
<div>hollo-world</div>
</template>
<script lang="ts" setup>
import type { MNode } from '@tmagic/schema';
defineProps<{
config: MNode;
}>();
</script>
```
在editor-runtime main.ts中注册HelloWorld
```ts
import { createApp } from 'vue';
import type { Magic } from '@tmagic/stage';
// eslint-disable-next-line
import { HelloWorld } from 'hello-ui';
import App from './App.vue';
declare global {
interface Window {
magic?: Magic;
}
}
const app = createApp(App);
app.component('hello-world', HelloWorld);
app.mount('#app');
```
[源码](https://github.com/vft-magic/tmagic-tutorial/tree/master/course3)