# 3.[DSL](../conception.md#dsl) 解析渲染 tmagic 提供了 vue3/vue2/react 三个版本的解析渲染组件,可以直接使用 [@tmagic/ui](https://www.npmjs.com/package/@tmagic/ui) [@tmagic/ui-vue2](https://www.npmjs.com/package/@tmagic/ui-vue2) [@tmagic/ui-react](https://www.npmjs.com/package/@tmagic/ui-react) 接下来是以vue3为基础,来讲述如何实现一个[@tmagic/ui](https://www.npmjs.com/package/@tmagic/ui) ## 准备工作 ### 创建项目 将[上一教程](./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/hellow-editor)复制过来 ## 基础概念 ### 节点(Node) 每一个组件最终都是由一个节点来描述,每个节点至少拥有id,type两个属性 id: 节点的唯一标识,不可重复 type: 节点的类型,有业务自行定义 ### 容器(Container) 容器也是节点的一种,容器可以包含多个节点并且是保存在items属性下 items: 容器下包含的节点组成的数组,items中不能有page,app ### 页面(Page) 页面是容器的一种,type固定为page,items中不能有page ### 根(Root) 根节点也是一个容器,type固定为app,items只能是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 ``` 接下来就需要解析节点的样式,在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 = {}; 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) .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 ``` ### 渲染页面 页面就是容器,之所以单独存在,是页面会自己的方法,例如reload等 Page.vue文件 ```vue ``` ## 在runtime中使用 hello-ui 删除editor-runtime/src/ui-page.vue 将App.vue中的ui-page改成hello-ui中的Page ```vue ``` 在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)