2022-05-05

tmagic-editor

文档

github tmagic-editor tmagic-editor可视化开源项目是从魔方平台演化而来的开源项目, 意在提供一个供开发者快速搭建可视化搭建平台的解决方案;

npm run playground

编辑器-实例

快速开始 - editor

for vite

初始化 一个标准的 vite vue 项目

# 初始化 一个 vite vue 项目
npm init vite@latest tmagic-project --template vue
cd tmagic-project

安装相关依赖

  • 编辑器 @tmagic/editor 实现一个可视化编辑器。 @tmagic/form 实现组件在编辑器中自定义表单配置。
# 安装 @tmagic/editor 和 @tmagic/form
npm install @tmagic/editor @tmagic/form -S
  • UI 适配 由于在实际应用中项目常常会用到例如element-plus、tdesign-vue-next等UI组件库。 为了能让使用者能够选择不同UI库,@tmagic/editor将其中使用到的UI组件封装到@tmagic/design中,然后通过不同的adapter 来指定使用具体的对应的UI库,我们提供了@tmagic/element-plus-adapter来支持element-plus,所以还需要安装相关的依赖。
# 安装 UI 实现 适配
npm install @tmagic/element-plus-adapter @tmagic/design element-plus -S
  • 其他 editor 中还包含了monaco-editor,所以还需安装 monaco-editor,可以参考 monaco-editor 的配置指引Momonaco-editor是微软提供的代码编辑器, 可以嵌入到浏览器中, 方便开发?
# 代码编辑器
npm install monaco-editor -S
 
# 样式
npm install sass -D

用来AMD引入 propsConfigs, propsValues asyncLoadJs.js

function asyncLoadJs (url) {
  return new Promise((resolve, reject) => {
    let script = document.createElement('script')//没有校验 重复加载
    script.type = 'text/javascript'
    script.src = url
    document.body.appendChild(script)
    script.onload = () => {
      resolve()
    }
    script.onerror = () => {
      reject()
    }
  })
}

引入 @tmagic/editor

main.js

import { createApp } from 'vue';
import ElementPlus from 'element-plus';
import zhCn from 'element-plus/es/locale/lang/zh-cn';
 
import TMagicDesign from '@tmagic/design';
import MagicEditor from '@tmagic/editor';
import MagicElementPlusAdapter from '@tmagic/element-plus-adapter';
import MagicForm from '@tmagic/form';
 
import App from './App.vue';
 
import 'element-plus/dist/index.css';
import '@tmagic/editor/dist/style.css';
import '@tmagic/form/dist/style.css';
 
const app = createApp(App);
app.use(ElementPlus, {
  locale: zhCn,
});
app.use(TMagicDesign, MagicElementPlusAdapter);
app.use(MagicEditor);
app.use(MagicForm);
app.mount("#app");

以上代码便完成了 @tmagic/editor 的引入。需要注意的是,样式文件需要单独引入。

使用 m-editor 组件

App.vue

<template>
  <m-editor
    v-model="dsl"
    :menu="menu"
    :runtime-url="runtimeUrl"
    :props-configs="propsConfigs"
    :props-values="propsValues"
    :component-group-list="componentGroupList"
  >
  </m-editor>
</template>
 
<script>
import { defineComponent, ref } from "vue";
import { markRaw } from 'vue';
import { FolderOpened, SwitchButton, Tickets } from '@element-plus/icons-vue';
 
export default defineComponent({
    name: "App",
    setup() {
       // 组件列表
      const componentGroupList = ref([
        {
          title: '容器',
          items: [
            {
              icon: markRaw(FolderOpened),
              text: '组',
              type: 'container',
            },
          ],
        },
        {
          title: '基础组件',
          items: [
            {
              icon: markRaw(Tickets),
              text: '文本',
              type: 'text',
            },
            {
              icon: markRaw(SwitchButton),
              text: '按钮',
              type: 'button',
            },
          ],
        },
      ]);
      // 组件属性列表
      const propsConfigs = ref([])
      // 组件默认值
      const propsValues = ref([])
      asyncLoadJs(`entry/config/index.umd.cjs`).then(() => {
        propsConfigs.value = window.magicPresetConfigs;
      });
      asyncLoadJs(`entry/value/index.umd.cjs`).then(() => {
        propsValues.value = window.magicPresetValues;
      });
      
      return {
        menu: ref({
          left: [
            // 顶部左侧菜单按钮
          ],
          center: [
            // 顶部中间菜单按钮
          ],
          right: [
            // 顶部右侧菜单按钮
          ],
        }),
 
        dsl: ref({
          // 初始化页面数据
        }),
        runtimeUrl: "/runtime/playground/index.html",
        // 组件属性列表
        propsConfigs,
        // 组件默认值
        propsValues,
        // 组件列表
        componentGroupList,
      };
    },
  });
</script>
 
<style lang="scss">
  ... 略
</style>

vite 走你~

m-editor 配置项

其中,有四个需要注意的属性配置项:runtimeUrl values configs componentGroupList。这是能让我们的编辑器正常运行的关键。

runtimeUrl

该配置涉及到 runtime 概念, tmagic-editor编辑器中心的模拟器画布, 是一个 iframe(这里的 runtimeUrl 配置的, 就是你提供的 iframe 的 url), 其中渲染了一个 runtime, 用来响应编辑器中的组件增删改等操作;

如何快速得到一个 runtime?

可以使用tmagic-editor项目源码中的 runtime,在提供的三个框架 vue2/vue3/react runtime 目录中选择一个,执行 npm run build:admin 得到产物,并将 ./dist/*产物放到你的项目中 ./public/,此处的 runtimeUrl 指向你放置 runtime/playground/index.html 的路径。(注意index.html引入外部js文件路径有错误的)

runtimeUrl: "/runtime/playground/index.html"

componentGroupList

componentGroupList 是指定左侧组件库内容的配置; 此处定义了在编辑器组件库中有什么组件; 在添加的时候通过组件 type 来确定 runtime 中要渲染什么组件; componentgrouplist

// 组件列表
const componentGroupList = ref([
  {
    title: '容器',
    items: [
      {
        icon: markRaw(FolderOpened),
        text: '组',
        type: 'container',
      },
    ],
  },
  {
    title: '基础组件',
    items: [
      {
        icon: markRaw(Tickets),
        text: '文本',
        type: 'text',
      },
      {
        icon: markRaw(SwitchButton),
        text: '按钮',
        type: 'button',
      },
    ],
  },
]);

propsConfigs/propsValues

propsConfigs propsValues 和 componentGroupList 中声明的组件是一一对应的,通过 type 来识别属于哪个组件,该配置涉及的内容,就是组件的表单配置描述,在组件开发中会通过 formConfig 配置来声明这份内容。

开发一个组件

上述的 runtime 产物中,dist 目录中即包含一个 entry 文件夹,在你的项目组件初始化之后,分别异步加载里面的config/index.umd.js、value/index.umd.js。并如上面代码中,赋值给 configs/values 即可。

// 组件属性列表
const propsConfigs = ref([])
// 组件默认值
const propsValues = ref([])
asyncLoadJs(`/entry/config/assets/index.umd.cjs`).then(() => {
  propsConfigs.value = window.magicPresetConfigs;
});
asyncLoadJs(`/entry/value/assets/index.umd.cjs`).then(() => {
  propsValues.value = window.magicPresetValues;
});

其实官方[tmagic-editor]的 playground, 就是一个编辑器实例的 demo 项目

dsl

点击中间的新增页面也会发现没有反应,这是因为没有编辑器初始值,只需要给value赋上初始值就可以了离谱! 加上初始值后,点击新增页面就可以渲染出一个画布了,但是点击添加HelloWorld组件依然没有反应

const dsl = ref({
    type: 'app',
    // 必须加上ID,这个id可能是数据库生成的key,也可以是生成的uuid
    id: 1,
    items: [],
  });

踩坑

internal server error preprocessor dependency sass not found. did you install it vite [plugin:vite:css] Preprocessor dependency “sass” not found. Did you install it?

npm install sass --save-dev

Uncaught ReferenceError: global is not defined at node_modules/randombytes/browser.js (browser.js:16:14) 总有那么低级的错误!

修改其源码 ./node_modules/serialize-javascript/index.js增加: window.global = window

如果是使用vite构建工具,如果出现 Uncaught ReferenceError: global is not defined,那么需要再vite.config.js中添加如下配置:

{
  optimizeDeps: {
    esbuildOptions: {
      define: {
        global: 'globalThis',
      },
    },
  },
}

import { performance } from ‘node:perf_hooks’ SyntaxError: Cannot use import statement outside a module npm i --save-dev @types/node

基础概念

组件

组件是tmagic-editor可配置页面元素的最小单位; 组件样式、逻辑代码(即开发者写的 vue, react 等代码)。 组件 type, 是组件的类型,这是用来告诉编辑器,我们要渲染的是什么组件。每个组件在开发时就应该确定这样一个唯一、不和其他组件冲突的组件 type。

插件

插件和组件类似,但是插件的功能是作为页面逻辑行为的一种补充方式。一般不显式的在模拟器中被渲染出具体内容(除非插件中会生成组件并插入页面),通常我们会用插件实现类似登录,页面环境判断,请求拦截器等等功能。

插件一般包含如下内容:

插件逻辑代码。 插件 type,是插件的类型,和组件 type 作用相同。在开发时就应该确定这样一个唯一、不和其他组件冲突的组件 type

容器

容器是tmagic-editor编辑器中的一个基础单位, 页面本身就是一个容器, 在基础组件中称为组, tmagic-editor通过容器概念, 实现了丰富的布局方式, 因为我们的布局行为是设置在容器上的, 容器内的组件是绝对定位, 或是顺序排布, 是根据容器的配置行为改变的; tmagic-editor的容器理论上可以无限嵌套;

表单配置

配置项目都是由组件里的表单描述来决定的, 用户可以在表单配置区域里通过配置项来改变组件的行为和样式;

DSL(描述文件)

DSL 是编辑器搭建页面的最终产物(描述文件), 其中包含了所有组件信息(组件布局, 组件配置等)和插件内容, 以及其他可拓展的信息都存放在 DSL 中; 根据 DSL 的描述进行渲染 https://tencent.github.io/tmagic-editor/docs/guide/advanced/js-schema.html

页面

我们把页面统一称为 runtime,更具体的 runtime 概念可以查看页面发布。runtime 是承载tmagic-editor项目页面的运行环境。编辑器的工作区是 runtime 的一个具体实例,另一个就是我们发布上线后,用户访问的真实项目页面。

核心概念

tmagic-editor

tmagic-editor可视化开源项目是从魔方平台演化而来的开源项目,意在提供一个供开发者快速搭建可视化搭建平台的解决方案。

tmagic-editor的编辑器我们已经封装成一个 npm 包,可以直接安装使用。编辑器是使用 vue3 开发的,但使用编辑器的业务可以不限框架,可以用 vue2、react 等开发业务组件。 @tmagic/editor 实现一个可视化编辑器;

in short 可以理解为一个编辑器, 可视化界面设计器, 它的产物是DSL

runtime

runtime 的概念, 是理解tmagic-editor项目页运行的重要概念, runtime 是承载tmagic-editor项目页面的运行环境;

可视化页面需要在tmagic-editor编辑器中搭建、渲染,通过模拟器所见即所得。搭建完成后,保存配置并发布,然后渲染到真实页面。其中涉及到两个不同的 runtime:

  1. 终端打开真实页面 (runtime page) 对应的是 runtime 中的 page。即把tmagic-editor 保存后的配置进行加载、解析、渲染,然后呈现页面的过程。

  2. 编辑器中的模拟器 (runtime playground) 对应的是 runtime 中的 playground。其实仔细查看源码,playground 和 page runtime 的差异,在于 playground 中需要响应编辑器中用户的操作

runtime 的 playground 部分,和 page 做的事情几乎一致,业务方可以包含上述 page 所拥有的全部能力。 但是,因为 playground 需要被编辑器加载,作为编辑器中页面模拟器的渲染容器,和编辑器通信,接受编辑器中组件的增删改查。所以,除了保持和 page 一样的渲染逻辑之外,playground 还要额外实现一套既定通信内容和 api,才能实现和编辑器的通信功能

runtime

各个 runtime 的作用除了作为不同场景je下的渲染环境,同时也是不同环境的打包构建载体。

in short tmagic-editor负责编辑设计出DSL, 最终是给 runtime 渲染, 所以runtime是有不同实现的

官方示例的 runtime

tmagic-editor提供了三个版本的 runtime 示例,可以参考:

参考 Vue3 runtime的 App.vue 源码

....
window.magic?.onRuntimeReady({
  getApp() {
    return app;
  },
  updateRootConfig(config: MApp) {
    console.log('update config', config);
    root.value = config;
    app?.setConfig(config, curPageId.value);
  },
  updatePageId(id: Id) {
    console.log('update page id', id);
    curPageId.value = id;
    app?.setPage(id);
  },
  select(id: Id) {
    console.log('select config', id);
    selectedId.value = id;
    const el = document.getElementById(`${id}`);
    // 未在当前文档下找到目标元素,可能是还未渲染,等待渲染完成后再尝试获取
     return nextTick().then(() => document.getElementById(`${id}`) as HTMLElement);
...
 

实现一个runtime 需要提供的API

API说明参数
updateRootConfig根节点更新root: MApp
updatePageId更新当前页面 idid: string
select选中组件id: string
add增加组件{ config , root }: UpdateData
update更新组件{ config , root }: UpdateData
remove删除组件{ config , root }: UpdateData
sortNode组件在容器间排序{ src , dist, root }: SortEventData

业务逻辑写在哪? 由于 runtime 是页面渲染的承载环境,其中会加载 @tmagic/ui 以及各个业务组件,业务发布项目页也是基于 runtime,所以在 runtime 中实现业务方的自定义逻辑是最合适的。

tmagic/ui

tmagic-editor的页面渲染,是通过在载入编辑器中保存的 DSL 配置,通过 ui 渲染器渲染页面。在容器布局原理里我们提到过,容器和组件在配置中呈树状结构,所以渲染页面的时候,渲染器会递归配置内容,从而渲染出页面所有组件。

tmagic-editor的设计中,针对每个前端框架,都需要有一个对应的 @tmagic/ui 来承担渲染器职责。

in short 相当于一个UI实现层, @tmagic/ui 和 runtime 是配套出现的, runtime 必须基于 @tmagic/ui 才可以实现渲染; 因为 @tmagic/ui 需要提供 runtime 所需要的渲染器;

@tmagic/ui vue3

vue3 渲染器

在 @tmagic/ui vue3 中,我们提供了几个基础组件,可以在项目源码中找到对应内容。

page tmagic-editor的页面基础 container tmagic-editor 的容器渲染器 Component.vue tmagic-editor的组件渲染器

button/text 是一个组件开发的示例,具体组件开发相关规范可以参考组件开发。 其中 page/container/Component 是 UI 的基础,是每个框架的 UI 都应该实现的。

@tmagic/form

tmagic-editor的表单配置,核心就是使用了 @tmagic/form 来作为渲染器。@tmagic/form 是一个 npm 包,可以安装它,在你想使用的地方单独使用。

相关核心库

@tmagic/editor 实现一个可视化编辑器; @tmagic/form 实现组件在编辑器中自定义表单配置;

@tmagic/core 实现对组件进行跨框架管理与一些通用复杂逻辑的实现; 我们在 @tmagic/core 这个包中, 实现了tmagic-editor组件节点的 Node 类, 每个组件在tmagic-editor的运行环境被渲染前, 都会对应初始化一个 Node 类实例; 而这些 Node 实例上包含了一些基础功能, 包括触发指定钩子函数; 这是一个框架无关的核心库, 所以支持在各个语言框架中使用; 但是具体触发时机需要由各个框架的渲染器实现;

@tmagic/stage 实现在编辑器中对组件的位置拖动与大小拖拉;

@tmagic/ui 提供一些vue3基础组件; @tmagic/ui-vue2 提供一些vue2基础组件; @tmagic/ui-react 提供一些react基础组件;

runtime 实现在编辑器中对使用不同框架的组件的渲染; page 项目提供最终页面发布的执行环境与组件构建;

实例教程

以 vue3 的组件开发为例。运行项目中的 playground 示例,会自动加载 vue3 的 runtime。runtime会加载@tmagic/ui

1.Hello World

简单示例 Hello World

初始化 项目

# vite 项目
npm init vite@latest tmagic-components --template vue
 
cd tmagic-components
 
# tmagic/editor 依赖
npm install --save @tmagic/editor @tmagic/form @tmagic/stage @tmagic/design @tmagic/element-plus-adapter element-plus
 
 

初始化 编辑器组件

main.js

import 'element-plus/dist/index.css';
import '@tmagic/editor/dist/style.css';
import '@tmagic/form/dist/style.css';
 
import { createApp } from 'vue';
import ElementPlus from 'element-plus';
 
import TMagicDesign from '@tmagic/design';
import TMagicEditor from '@tmagic/editor';
import TMagicElementPlusAdapter from '@tmagic/element-plus-adapter';
import TMagicForm from '@tmagic/form';
 
import App from './App.vue';
 
createApp(App)
  .use(ElementPlus)
  .use(TMagicDesign, TMagicElementPlusAdapter)
  .use(TMagicEditor)
  .use(TMagicForm)
  .mount('#app');

App.vue

<template>
  <m-editor
    v-model="value"
    :render="render"
    :component-group-list="componentGroupList"
  ></m-editor>
</template>
 
<script lang="ts" setup>
import { ref } from 'vue';
 
const value = ref();
 
const componentGroupList = ref([]);
 
const render = () => window.document.createElement('div');
</script>
 
<style>
....
</style>

添加组件列表配置

const componentGroupList = ref([
  {
    title: '组件列表',
    items: [
      {
        icon: 'https://vfiles.gtimg.cn/vupload/20220614/9cc3091655207317835.png',
        text: 'HelloWorld',
        type: 'hello-world',
      },
    ],
  },
]);

初始化DSL

modelValue/v-model

const value = ref({
  type: 'app',
  // 必须加上ID,这个id可能是数据库生成的key,也可以是生成的uuid
  id: 1,
  items: [],
});

加上初始值后,点击新增页面就可以渲染出一个画布了,但是点击添加 HelloWorld 组件没有反应

这是因为这时的编辑器并能理解HelloWorld是什么,需要在render函数中处理

编辑器的渲染

中间工作区域中画布渲染的内容,通常是通过解析modelValue来渲染出DOM,return的DOM结构需要有一个根节点。 https://tencent.github.io/tmagic-editor/docs/api/editor/props.html#render

in short 这里其实就是 runtime 的实现

const render = () => {
  const root = window.document.createElement('div');//创建一个根dom
  const page = value.value.items[0];//DSL 这里第一个必定是Page配置?
  if (!page) return root;
  root.id = `${page.value.id}`;
  createApp(//动态编译Vue模板
    {
      template: '<p v-for="node in config.items" :key="node.id" :id="node.id">hello world</p>',
      props: ['config'],
    },
    {
      config: page,
    },
  ).mount(root);
 
  return root;
};
 

render函数中获取 page 是通过 value.value.items[0],这样只是表示第一个页面, 如果页面有多个页面就会有问题 可以通过editorService.get('page')获取到当前选中的页面

import { editorService } from '@tmagic/editor';
import type { MPage } from '@tmagic/schema';
 
const page = computed(() => editorService.get<MPage>('page'))

这里用到了动态编译Vue模板,所以需要在vue.config.js中添加vue alias

configureWebpack: {
  resolve: {
    alias: {
      vue$: 'vue/dist/vue.esm-bundler.js',
    },
  },
}

总而言之, render 可以接受 DSL 和 与之交互;

参考: https://tencent.github.io/tmagic-editor/docs/guide/tutorial/hello-world.html

实现 runtime for vue3

参考: https://tencent.github.io/tmagic-editor/docs/guide/tutorial/runtime.html

runtime 渲染项目

初始化

# vite 项目
npm init vite@latest editor-runtime --template vue
cd editor-runtime
 
# 依赖
npm install --save @tmagic/schema @tmagic/stage
 

新建 ui-page.vue文件 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, defineProps } from 'vue';
 
const props = defineProps(["config"]);//接受一个 config prop
const style = computed(() => {
  const { width = 375, height = 1700 } = props.config.style || {};
  return {
    width: `${width}px`,
    height: `${height}px`,
  };
});
</script>

runtime-url 入口, 使用 http url ;

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();
</script>
<style scoped>
</style>

启动: vite —port 5100

editor 编辑器项目

初始化

# vite 项目
npm init vite@latest hello-editor --template vue
cd hello-editor
 
# tmagic/editor 依赖
npm install --save @tmagic/editor @tmagic/form @tmagic/stage @tmagic/design @tmagic/element-plus-adapter element-plus
 

App.vue

<template>
    <m-editor style="width: 100%; height: 100%"
      v-model="value"
      :runtime-url="runtimeUrl"
      :component-group-list="componentGroupList"
    ></m-editor>
</template>
 
<script lang="ts" setup>
import { ref, watch } from "vue";
 
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://127.0.0.1:5100/";//runtime 的url;
</script>
 
<style>
#app {
  overflow: auto;
}
 
html,body,#app {
  height: 100%; margin: 0;padding: 0;
}
 
#app::-webkit-scrollbar {
  width: 0 !important;
  display: none;
}
</style>
 

在editor-runtime 项目下的 vue.config.js 下配置

devServer: {
  headers: {
    'Access-Control-Allow-Origin': '*',
  },
},
optimizeDeps: {
  esbuildOptions: {
    define: {
      global: 'globalThis',
    },
  },
},

main.js

import 'element-plus/dist/index.css';
import '@tmagic/editor/dist/style.css';
import '@tmagic/form/dist/style.css';
 
import { createApp } from 'vue';
import ElementPlus from 'element-plus';
 
import TMagicDesign from '@tmagic/design';
import TMagicEditor from '@tmagic/editor';
import TMagicElementPlusAdapter from '@tmagic/element-plus-adapter';
import TMagicForm from '@tmagic/form';
 
import App from './App.vue';
createApp(App)
  .use(ElementPlus)
  .use(TMagicDesign, TMagicElementPlusAdapter)
  .use(TMagicEditor)
  .use(TMagicForm)
  .mount('#app');

启动: vite

runtime 与 editor 通信

到这里项目就可以正常访问了,但是会发现添加组件没有反应。 这是因为在 runtime 中无法直接获取到editor中的dsl,所以需要通过editor注入到window的magic api来交互

如出现在runtime中出现magic为undefined, 可以尝试在App.vue中通过监听message,来准备获取magic注入时机,然后调用magic.onRuntimeReady,示例代码如下

实测 editor-runtime 接受不到, 因为是 <iframe srcdoc= 的属性问题??

import { onMounted } from 'vue'
import type { RemoveData, UpdateData } from '@tmagic/stage';
import type { Id, MApp, MNode } from '@tmagic/schema';
const root = ref<MApp>();
 
// onMounted(()=>{
console.log("editor-runtime onMounted")
window.addEventListener('message', ({ data }) => {
  console.log("editor-runtime event message",data)
  if (!data.tmagicRuntimeReady) {
    return;
  }
  console.log("editor-runtime onRuntimeReady")
  window.magic?.onRuntimeReady({
    /** 当编辑器的dsl对象变化时会调用 */
    updateRootConfig(config: MApp) {
      console.log("当编辑器的dsl对象变化时会调用");
      root.value = config;
    },
 
    /** 当编辑器的切换页面时会调用 */
    updatePageId(id: Id) {
      console.log("当编辑器的切换页面时会调用");
      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);
      console.log("新增组件时调用");
    },
 
    /** 更新组件时调用 */
    update({ config }: UpdateData) {
      const index = page.value.items?.findIndex((child: MNode) => child.id === config.id);
      page.value.items.splice(index, 1, reactive(config));
        console.log("更新组件时调用");
    },
 
    /** 删除组件时调用 */
    remove({ id }: RemoveData) {
      const index = page.value.items?.findIndex((child: MNode) => child.id === id);
      page.value.items.splice(index, 1);
      console.log("删除组件时调用");
    },
  });
})

同步页面dom给编辑器

由于组件渲染在runtime中,对于编辑器来说是个黑盒,并不知道哪个dom节点才是页面(对于dsl的解析渲染可能是千奇百怪的),所以需要将页面的dom节点同步给编辑器

watch(page, async () => {
  // page配置变化后,需要等dom更新
  await nextTick();
  window?.magic.onPageElUpdate(pageComp.value?.$el);//将对应组件的 dom 告知 editor
});

以上就是一个简单runtime实现,以及与编辑的交互,这是一个不完善的实现(会发现组件再画布中无法自由拖动,是因为没有完整的解析style)

项目归档

tmagic-editor-learn.zip

实现 @tmagic/ui

新建一个 hello-ui 项目文件夹, 用作ui渲染实现

.
└─editor-runtime
└─hello-editor
└─hello-ui

DSL 描述数据

节点(Node)

每一个组件最终都是由一个节点来描述,每个节点至少拥有id,type两个属性: id: 节点的唯一标识,不可重复 type: 节点的类型,有业务自行定义

容器(Container)

容器也是节点的一种,容器可以包含多个节点并且是保存在items属性下 items: 容器下包含的节点组成的数组,items中不能有page,app

页面(Page)

页面是容器的一种,type固定为page,items中不能有page

根(Root)

根节点也是一个容器,type固定为 app,items只能是page

DSL 渲染实现

节点渲染

在 hello-ui 下创建 Component.vue 文件

由于节点的type是由业务自行定义的,可以使用component 组件通过is参数来决定哪个组件被渲染,所以将type与组件做绑定

<template>
  <component v-if="config" :is="type" :id="`${id}`" :style="style" :config="config"></component>
</template>
 
<script lang=ts setup>
import { computed } from 'vue';
 
import type { MNode } from '@tmagic/schema';
 
// 将节点作品参数传入组件中 <!> 外部传入的节点 DSL 
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);
 
 
const fillBackgroundImage = (value: string) => {
  if (value && !/^url/.test(value) && !/^linear-gradient/.test(value)) {
    return `url(${value})`;
  }
  return value;
};

style 的解析, 需要注意几个地方

  1. 数字: css中的数值有些是需要单位的,例如px,有些是不需要的,例如opacity, 在tmagic/editor中,默认都是不带单位的,所以需要将需要单位的地方补齐单位 这里做补齐px处理,如果需要做屏幕大小适应, 可以使用rem或者vw,这个可以根据自身需求处理。

  2. url: css中的url需要是用url(),所以当值为url时,需要转为url(xxx)

  3. transform : transform属性可以指定为关键字值none 或一个或多个transform-function值。

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;
});
</script>
 

容器渲染

容器与普通节点的区别,就是需要多一个items的解析

在 hello-ui 下新增Container.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>

页面渲染

新增Page.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>
 

添加HelloWorld组件

在hello-ui下新增 HelloWorld.vue

<template>
  <div>hollo-world</div>
</template>
 
<script lang="ts" setup>
import type { MNode } from '@tmagic/schema';
 
defineProps<{
  config: MNode;
}>();
</script>

在 editor-runtime 中

  1. 将App.vue中的 ui-page 改成 hello-ui中的Page
<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>
  1. 注册 HelloWorld 组件
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
 
import { HelloWorld } from 'hello-ui';
let app = createApp(App)
app.mount('#app');
app.component('hello-world', HelloWorld);
  1. 配置解析路径 editor-runtime/vue.config.js
configureWebpack: {
  resolve: {
    alias: {
      'hello-ui': path.resolve(__dirname, '../hello-ui'),
      vue$: path.resolve(__dirname, './node_modules/vue'),
    },
  },
},

自此三个项目(参考上 [核心概念]): 一个编辑器项目(hello-editor): 编辑, 页面设计器 (设计生成DSL) 一个runtime项目(editor-runtime): 一切和业务相关的逻辑,都应该在 runtime 中实现 (还负责响应编辑器, 引用ui渲染) 一个ui渲染项目(hello-ui) : 承担的是业务逻辑无关的,基础组件渲染的功能 (负责渲染 节点组件, 容器, 页面)

项目归档

tmagic-editor-learn_02.zip

二次开发

官方 tmagic-editor (https://tencent.github.io/tmagic-editor/)项目中 ./playground 目录源码对应 页面设计器; ./runtime/vue3 目录源码对应 runtime-vue3 ./packages/ui 目录源码对应 @tmagic/ui

node.js >= 18 pnpm >= 9

  1. 先安装 pnpm
npm install -g pnpm
  1. 然后安装依赖
pnpm bootstrap
  1. 运行项目, 执行命令
pnpm playground

编辑器分离

按照架构设计 编辑器与UI渲染无关, 不加功能, 就用官方实现即可, 不做扩展, 把它打包处理变成一个纯的编辑器

  1. 关键三个地方需要为修改外部注入

1.playground

runtimeUrl路径

// runtimeUrl  和 预览 的地址
const { VITE_RUNTIME_PATH, VITE_ENTRY_PATH } = import.meta.env;
const runtimeUrl = `${VITE_RUNTIME_PATH}/playground/index.html`;
 

预览的DSL 参数传递 playground\src\pages\Editor.vue

const previewUrl = computed(
  () => `${VITE_RUNTIME_PATH}/page/index.html?localPreview=1&page=${editor.value?.editorService.get('page')?.id}`,
);

在 runtime 预览获取 DSL 的地方 runtime\vue3\page\main.ts

const app = new Core({
  ua: window.navigator.userAgent,
  config: ((getUrlParam('localPreview') ? getLocalConfig() : window.magicDSL) || [])[0] || {},
  curPage: getUrlParam('page'),
});

左边的组件列表配置

// 组件列表
import componentGroupList from '../configs/componentGroupList';
// componentGroupList.ts
import { FolderOpened, Grid, PictureFilled, SwitchButton, Tickets } from '@element-plus/icons-vue';
export default [
  {
    title: '示例容器',
    items: [
      {
        icon: FolderOpened,//直接引用组件 要改下, 才能外部注入
        text: '组',
        type: 'container',
      },
      {
        icon: FolderOpened,
        text: '蒙层',
        type: 'overlay',
      },
    ],
  },
  .....
 

playground的index.html 引用这个js EDITOR_CONFIG.js 直接定义为全局变量, 好调试 也好修改

const EDITOR_CONFIG = {
    componentGroupList: [
        {
          title: '示例容器',
          items: [
            {
              icon: "FolderOpened",
              text: '组',
              type: 'container',
            },
            {
              icon: "FolderOpened",
              text: '蒙层',
              type: 'overlay',
            },
          ],
        },
        //右边列表
    ,
	// runtime 地址, 原总的项目开发环境(得看 runtime 项目 dev.vite.config 打包后的目录; 执行的是runtime打包不是rund ev);  npm run build:playground 
    // runtimeUrl: "http://localhost:8888/playground/runtime/vue3/",
    // previewUrl: "http://localhost:8888/playground/runtime/vue3/",
    
	// runtime 地址, 执行本地 public 资源; npm run build:playground 
    runtimePath: "runtime/vue3",
    entryPath: "./entry/vue3",
}
window.__G_EDITOR_CONFIG = EDITOR_CONFIG;

修改为读取全局变量 ./playground/src/pages/Editor.vue

//预览DSL数据,  改为外部js注入
// import componentGroupList from '../configs/componentGroupList';
import {getList} from '../configs/componentGroupList';
let componentGroupList = getList(__G_EDITOR_CONFIG.componentGroupList)//处理 ico组件
// const { VITE_RUNTIME_PATH, VITE_ENTRY_PATH } = import.meta.env;
// console.log(" run %s, %s", VITE_RUNTIME_PATH, VITE_ENTRY_PATH) //   ///tmagic-editor/playground/runtime/vue3, ./entry/vue3
// 指向的 runtime 的地址
let VITE_RUNTIME_PATH = __G_EDITOR_CONFIG.runtimePath;
let VITE_ENTRY_PATH = __G_EDITOR_CONFIG.entryPath;

npm run build:playground

2.编辑器的 props

分离编辑器需要

  1. runtimeUrl 属性: (runtime 编译出来的 page)
  2. entryPath 属性: (runtime 编译出来的 entry)

编辑器默认内部dom只要有id都可被选中,可通过 canSelect 控制!!! https://tencent.github.io/tmagic-editor/docs/api/editor/props.html#canselect

playground\src\pages\Editor.vue

const canSelect = (el :any) => {
  const inn = ["page", "container", "button", "text", "img", "qrcode", "overlay"]
  for (let index = 0; index < inn.length; index++) {
    const element = inn[index];
    if(el?.id.startsWith(element)) return true;
  }
  return (el?.id.startsWith("sny"))
};

document.getElementsByTagName(“iframe”)[0].contentWindow.appInstance

3.为组件添加统一配置项

编辑器的 ‘属性’, ‘样式’ 等 来源于: import propsService from './services/props'; 内部 fillConfig 实际来源于./tmagic-editor\packages\editor\src\utils\props.ts 文件定义的;

官方还预留了扩展可以使用 usePlugin, 在 fillConfig 之后调用 afterFillConfig, 填充自己定义的属性

propsService.usePlugin({
  /**
   oldConfig: 系统生成的配置
   componentConfig: 组件内定义的formConfig
   **/
  afterFillConfig(oldConfig, componentConfig) {
    return yourConfig
  }
})

runtime & ui 项目定制

runtime 修改

删除没用的, 只保留 runtime (runtime\vue3) , packages\ui(ui) 及其依赖

├─packages
│  ├─cli
│  ├─core
│  ├─data-source
│  ├─schema
│  ├─stage
│  └─ui //ui 项目
├─runtime
│  └─vue3
│      ├─page
│      │  └─assets
│      └─playground
│          └─assets
└─

runtime: 项目 可增加业务代码, 供ui组件使用, 使用DSL调用ui, 作为中介者; packages ui: 项目纯作为符合 tmagic-editor 组件规范的, 可作为组件资产项目;

  1. main.ts 修改加载 DSL 和 page id的方式, 注意 localPreview 是作为设计器预览的参数, 方便发布嵌入 main.ts
const app = new Core({
  ua: window.navigator.userAgent,
  // config: ((getUrlParam('localPreview') ? getLocalConfig() : window.magicDSL) || [])[0] || {},
  config: (getUrlParam('localPreview') ? getLocalConfig()[0] : getConfig()),//返回DSL 
  curPage: getUrlParam('page')//返回 page id
});
  1. runtime\vue3\build.vite.config.ts

修改 base, 不然发布后的js引用路径有问题

if (['page', 'playground', 'page:admin', 'playground:admin'].includes(mode)) {
    const [type, isAdmin] = mode.split(':');
    //    const base = isAdmin ? `/static/vue3/runtime/${type}/` : `/tmagic-editor/playground/runtime/vue3/${type}`;
    const base = `tmagic-test/${type}`; //ctx路径!
    const outDir = isAdmin
      ? path.resolve(process.cwd(), `./dist/runtime/${type}`)
      : path.resolve(process.cwd(), `../../playground/public/runtime/vue3/${type}`);//这里可配置 runtime 编译输出的 entry 和 runtime
    return {
      .....
  1. dev.vite.config.ts
export default defineConfig({
  plugins: [vue(), vueJsx()],
 
  resolve: {
    alias: [
      { find: /^@\/assets\//, replacement: path.join(__dirname, '../../packages/ui/src') },//<!> 增加导入别名, 方便组件引用资源; ui 项目新增一个 assets文件夹
      { find: /^vue$/, replacement: path.join(__dirname, 'node_modules/vue/dist/vue.esm-bundler.js') },
      {
        find: /^@tmagic\/utils\/resetcss.css/,
        replacement: path.join(__dirname, '../../packages/utils/src/resetcss.css'),
      },
 
  ....
  root: './',
 
  // base: '/tmagic-editor/playground/runtime/vue3/',
  base: '/runtime/',// <!> 改下路径 太长了
 
  publicDir: 'te',//<!>修改开放资源目录, 将打包后的playground放到这个目录, 方便开发
cd runtime\vue3
pnpm install

runtime 编译新增的组件

runtime 项目的入口 runtime\vue3\page\main.ts

 
import { createApp, defineAsyncComponent } from 'vue';
 
import Core from '@tmagic/core';
import { getUrlParam } from '@tmagic/utils';
 
import components from '../.tmagic/async-comp-entry';//关键这个文件是 tmagic entry && npm run build:config && npm run build:value && npm run build:event 编译出来的
....
 
Object.entries(components).forEach(([type, component]: [string, any]) => {//这里导入注册的组件
  magicApp.component(`magic-ui-${type}`, defineAsyncComponent(component));
});
 

tmagic entry && npm run build:config && npm run build:value && npm run build:event 看下编译配置 runtime\vue3\build.vite.config.ts

  if (['value', 'config', 'event', 'value:admin', 'config:admin', 'event:admin'].includes(mode)) {
    const [type, isAdmin] = mode.split(':');
    const capitalToken = type.charAt(0).toUpperCase() + type.slice(1);
    return {
      publicDir: './.tmagic/public',
      build: {
        cssCodeSplit: false,
        sourcemap: true,//是否生成源码映射 调试用的
        minify: false,
        target: 'esnext',
        outDir: isAdmin ? `./dist/entry/${type}` : `../../playground/public/entry/vue3/${type}`,//这里可配置 runtime 编译输出的 entry 和 runtime
 
        lib: {
          entry: `.tmagic/${type}-entry.ts`,//应该就是这个输出的文件
          name: `magicPreset${capitalToken}s`,
          fileName: 'index',
          formats: ['umd'],
        },
      },
    };
  }

还一个编译配置: runtime\vue3\tmagic.config.ts 指定了路径

import path from 'path';
 
import { defineConfig } from '@tmagic/cli';
 
export default defineConfig({
  packages: [path.join(__dirname, '../../packages/ui')],//指定了ui项目路径
  componentFileAffix: '.vue',
  dynamicImport: true,
});

packages 导出组件

最终在 packages\ui\src\index.ts 导出即可

import Button from './button';
import Container from './container';
import Img from './img';
import Overlay from './overlay';
import Page from './page';
import Qrcode from './qrcode';
import Text from './text';
import testComp from './testComp';
const ui: Record<string, any> = {
  page: Page,
  container: Container,
  button: Button,
  text: Text,
  img: Img,
  qrcode: Qrcode,
  overlay: Overlay,
 
  testComp:testComp, //导出新增的组件
};
export default ui;

其次还需要注意 runtime 编译出来的 npm run build:config && npm run build:value && npm run build:event 替换 playground 的 entryPath 指向的文件

 
// npm run build:config && npm run build:value && npm run build:event //命令, 可只更新配置文件, 不用每次都重新打包
 
npm run build //完整打包 ui

安装第三方库

> cd packages\ui
pnpm install element-plus --save
pnpm install --save lodash
pnpm install --save pinia

错误? This error shows up when you run npm install in a directory where you previously ran pnpm install. The solution is to remove your node_modules directory and run npm install again. 使用 pnpm pnpm install element-plus --save

ui 添不添加 element-plus 都无所谓, 只是为了ide引用不报错; 因为最终打包发布是 runtime 编译的 page 和 playground

cd runtime\vue3
pnpm install element-plus --save
pnpm install --save lodash
pnpm install --save pinia
 

踩坑指南

playground 几个关键点方便调试

在画布 选择/移除 组件时事件处理 packages\editor\src\utils\stage.ts

packages\editor\src\layouts\workspace\Stage.vue

//在组件拖动到画布时, 由该函数处理
const dropHandler = async (e: DragEvent) => {
  e.preventDefault();
 
  const doc = stage?.renderer.contentWindow?.document;
  const parentEl: HTMLElement | null | undefined = doc?.querySelector(`.${stageOptions?.containerHighlightClassName}`);
 
  let parent: MContainer | undefined | null = page.value;
  if (parentEl) {
    parent = services?.editorService.getNodeById(parentEl.id, false) as MContainer;
  }
  ....
// config 即是 componentGroupList 的组件配置
  services?.editorService.add(config, parent);

编辑服务 packages\editor\src\services\editor.ts

playground 新增组件没有配置属性值?

获取组件默认值在 packages\editor\src\services\props.ts

public async getPropsValue(type: string, { inputEvent, ...defaultValue }: Record<string, any> = {}) {
  ....
 
    return {
      id,
      ...defaultPropsValue,
      ...mergeWith({}, cloneDeep(this.state.propsValueMap[type] || {}), data),// this.state.propsValueMap 配置属性值 全部缓存在这里, 最组件名称 test-comp ; 将驼峰命名 testComp 使用 -  我靠!!
    };
  }
 

所以注册时全部统一用 ’-’ 命名!!

ui/index.ts 导出的组件名是 undefined?

ui 项目 index.ts 导出的组件名是 undefined? 编译出来的 entry 配置名称是 undefined ?

runtime 编译的配置 runtime\vue3\tmagic.config.ts cli 源码 packages\cli\src\utils\resolveAppPackages.ts

const getComponentPackageImports = function ({
  ............
    if (propertyMatch) {
      result.imports.push({
        // type: property.key.name,// ui/index.ts 导出的ui key真按字符串定义, name 没值, 用 value 离谱!!
        type: property.key.name? property.key.name:  property.key.value,
        name: propertyMatch.specifiers[0].local.name,
        indexPath: getIndexPath(path.resolve(path.dirname(indexPath), propertyMatch.source.value)),
      });
    }
  });
 
  return result;
};

改完, 需要重新编译一下 cli

runtime 编译错误 terser not found.?

[vite:terser] terser not found. Since Vite v3, terser has become an optional dependency. You need to install it.

npm install terser -S

pinia 开发环境正常, 打包后响应式失效?

Cannot read properties of undefined (reading ‘_s’) 明显打包编译 pinia 代码有问题 TODO

The inferred type of ‘useClick’ cannot be named without a reference to

error TS2742: The inferred type of ‘useClick’ cannot be named without a reference to ‘.pnpm/@types+lodash@4.17.4/node_modules/@types/lodash’. This is likely not portable. A type annotation is necessary.

项目归档

playground (编辑器)项目

Transclude of playground_20230630.zip

runtime + ui 项目

Transclude of sny-tmagic-components_20230630.zip

tmagic-editor + ui + runtime 更新20240521 改善数据源

Transclude of tmagic-editor_20240521.zip

组件开发

组件是tmagic-editor可配置页面元素的最小单位。我们都会从左面板的组件区中选中组件,加入到工作区的模拟器中,然后在右面板中对组件进行配置。一个组件的基本内容,会包含如下:

  1. 组件样式、逻辑代码(即开发者写的 vue, react 等代码)。

  2. 表单配置描述,tmagic-editor的定义是导出一个表单对象,这份配置仅在编辑器中使用表单配置是编辑器右面板展示的内容,配置项目都是由组件里的表单描述来决定的,用户可以在表单配置区域里通过配置项来改变组件的行为和样式。 注意,由于每个组件都需要有一些共同的表单配置项目,所以tmagic-editor通过在表单渲染器,统一为所有组件加上了通用的表单配置项目。包括基础组件样式配置、钩子事件配置等。

  3. 拓展描述,这部分内容目前还未有严格定义,但是我们保留这个扩展能力。

  4. 组件 type, 是组件的类型,这是用来告诉编辑器,我们要渲染的是什么组件。每个组件在开发时就应该确定这样一个唯一、不和其他组件冲突的组件 type。

https://tencent.github.io/tmagic-editor/docs/guide/component.html

组件规范

组件的基础形式,需要有四个文件 index 入口文件,引入下面几个文件

  1. formConfig 表单配置描述;

关于配置的输入类型定义

  1. initValue 表单初始值

  2. event 定义联动事件 具体可以参考(组件联动)[https://tencent.github.io/tmagic-editor/docs/guide/advanced/coupling.html#%E7%BB%84%E4%BB%B6%E8%81%94%E5%8A%A8]

  3. component.{vue,jsx} 组件样式、逻辑代码

@tmagic/ui 中的 button/text 就是基础的组件示例。我们要求声明 index 入口,因为我们希望在后续的配套打包工具实现上,可以有一个统一规范入口。

为容器增加 flex 支持

复制自带的 container 主要就是修改 formConfig.ts

 const onStyleChange = (formState: any, v: string, ctx: any) => {
  let { model , prop } = ctx;
  if (!model.style) return v;
  model.style[prop] = v;
}
export default [
  {
    name: 'layout',
    text: '容器布局',
    type: 'select',
    defaultValue: 'absolute',
    options: [
      { value: 'absolute', text: '绝对定位' },
      { value: 'relative', text: '流式布局' },
    ],
    onChange: (formState: any, v: string, { model }: any) => {
      if (!model.style) return v;
      if (v === 'relative') {
        model.style.display = 'block';
        model.style.height = 'auto';
      } else {
        const el = formState.stage?.renderer?.contentWindow.document.getElementById(model.id);
        if (el) {
          model.style.display = 'block';
          model.style.height = el.getBoundingClientRect().height;
        }
      }
    },
  },
  {
    name: 'display',
    text: 'display',
    type: 'select',
    defaultValue: 'block',
    options: [
      { value: 'block', text: 'block' },
      { value: 'flex', text: 'flex' },
    ],
    onChange: onStyleChange,
  },
  {
    name: 'flex-direction',
    text: 'direction',
    type: 'select',
    defaultValue: 'row',
    options: [
      { value: 'row', text: 'row' },
      { value: 'row-reverse', text: 'row-reverse' },
      { value: 'column', text: 'column' },
      { value: 'column-reverse', text: 'column-reverse' },
    ],
    onChange: onStyleChange,
  },
  {
    name: 'flex-wrap',
    text: 'wrap',
    type: 'select',
    defaultValue: 'nowrap',
    options: [
      { value: 'nowrap', text: 'nowrap' },
      { value: 'wrap', text: 'wrap' },
    ],
    onChange: onStyleChange,
  },
  {
    name: 'align-content',
    text: 'align-c',
    type: 'select',
    defaultValue: 'stretch',
    options: [
      { value: 'stretch', text: 'stretch' },
      { value: 'flex-start', text: 'flex-start' },
      { value: 'flex-end', text: 'flex-end' },
      { value: 'center', text: 'center' },
      { value: 'space-between', text: 'space-between' },
      { value: 'space-around', text: 'space-around' },
    ],
    onChange: onStyleChange,
  },
  {
    name: 'align-items',
    text: 'align-i',
    type: 'select',
    defaultValue: 'stretch',
    options: [
      { value: 'stretch', text: 'stretch' },
      { value: 'flex-start', text: 'flex-start' },
      { value: 'flex-end', text: 'flex-end' },
      { value: 'center', text: 'center' },
      { value: 'baseline', text: 'baseline' },
    ],
    onChange: onStyleChange,
  },
  {
    name: 'justify-content',
    text: 'justify-c',
    type: 'select',
    defaultValue: 'flex-start',
    options: [
      { value: 'flex-start', text: 'flex-start' },
      { value: 'flex-end', text: 'flex-end' },
      { value: 'center', text: 'center' },
      { value: 'space-between', text: 'space-between' },
      { value: 'space-around', text: 'space-around' },
    ],
    onChange: onStyleChange,
  },
 
];

npm run build:config && npm run build:value && npm run build:event 更新配置

组件自定义事件

组件联动-添加组件自定义事件

可以通过声明组件中的 event 文件,在文件中描述当前组件可以配置的事件名,和可以被触发的动作。 event.js

export default {
  events: [
    {
      label: '完成某事件',
      value: 'yourComponent:finishSomething',
    },
  ],
  methods: [
    {
      label: '弹出 Toast',// 编辑器可以触发的动作 行为
      value: 'toast',
    },
  ],
};

in short events组件主动触发的, methods组件被动接受的

触发事件和提供方法 index.vue

const node = app?.page?.getNode(props.config.id);//触发事件, 第二个参数必须是 node !!
const onClick = () => {
  // app.emit 第一个参数为事件名,其余参数为你要传给接受事件组件的参数
  app?.emit("yourComponent:finishSomething", node, /*可以传参给接收方*/);
};
 
defineExpose({
  // 此处实现事件动作
  // 实际触发时是调用vue实例上的方法,所以需要将改方法暴露到实例上
  toast: (/*触发组件node*/, /*接收触发事件组件传进来的参数*/) => {
    toast('测试 vue3')
  }
});

插件开发

https://tencent.github.io/tmagic-editor/docs/guide/component.html#%E6%8F%92%E4%BB%B6%E5%BC%80%E5%8F%91

插件开发和组件开发形式类似,但是插件开发不需要有组件的规范。在以 vue 为基础的 ui 和 runtime 中,插件其实就是一个 vue 插件。

我们只需要在插件中提供一个入口文件,其中包含 vue 的 install 方法即可。

javascript export default { install() {} } 在插件中开发者可以自由实现需要的业务逻辑。插件和组件一样,只需要在 units.js 中,加入导出的 units 对象里即可。

数据源

const app = new Core({
  ua: window.navigator.userAgent,
});
 
const dataSourceManager = app?.dataSourceManager
ds = dataSourceManager.get("ds_b64c92b5");// 拿到对应的 DataSource
 
//通过 DataSource 的 setData 修改的数据, 会触发 change 事件
// ds.setData(data: Record<string, any>)
ds.setData({text:'测试修改'})
 

git 发布页

https://github.com/Tencent/tmagic-editor/releases