tmagic-editor

2,267 阅读13分钟

核心库

  • @tmagic/editor 实现一个可视化编辑器。
  • @tmagic/form 实现组件在编辑器中自定义表单配置。
  • @tmagic/core 实现对组件进行跨框架管理与一些通用复杂逻辑的实现。
  • @tmagic/stage 实现在编辑器中对组件的位置拖动与大小拖拉。
  • @tmagic/ui 提供一些vue3基础组件。
  • @tmagic/ui-vue2 提供一些vue2基础组件。
  • @tmagic/ui-react 提供一些react基础组件。
  • runtime 实现在编辑器中对使用不同框架的组件的渲染。

目录介绍

目录

packages

packages目录中提供的内容,我们都以 npm 包形式输出,开发者可以通过安装对应的包来使用。

runtime

runtime是承载tmagic-editor页面的运行环境 是我们提供的编辑器活动页和编辑器模拟器运行的页面项目示例。可以直接使用,也可以参考并自行实现。

playground

playground是一个简单的编辑器项目示例。即使用了 packages 和 runtime 内容的集成项目。

@tmagic/editor其中使用到的UI组件👇

@tmagic/editor其中使用到的UI组件

componentGroupList

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

componentGroupList

Editor.vue

编译器核心入口

componentGroupList

概念

runtimeUrl

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

🌰:

const runtimeUrl = `${VITE_RUNTIME_PATH}/playground/index.html`;

propsConfigs/propsValues

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

configs 既可以通过 hardcode 方式写上每个组件的表单配置,也可以通过组件打包方式得到对应内容,然后通过异步加载来载入。比如:

setup() {
  asyncLoadJs(`/runtime/vue3/assets/config.js`).then(() => {
    propsConfigs.value = window.magicPresetConfigs;
  });
  asyncLoadJs(`/runtime/vue3/assets/value.js`).then(() => {
    propsValues.value = window.magicPresetValues;
  });
}

如何快速得到一个 configs/values

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

上述代码中传入的产物在这里找到

componentGroupList

PS:组件的type应是唯一的。

其他基础概念参考 tencent.github.io/tmagic-edit… 本文略

页面发布

编辑器产物 DSL

在tmagic-editor编辑器中,所有的操作和配置信息,最终都保存成这一份 DSL。这份配置在tmagic-editor runtime 中被加载和渲染,最终呈现出tmagic-editor项目页。

runtime

runtime 是承载tmagic-editor项目页面的运行环境。

涉及到两个不同的 runtime:

  • 编辑器中的模拟器
  • 终端打开真实页面

runtime 是tmagic-editor页面的渲染环境,提供不同场景下的能力封装。

runtime 只是对tmagic-editor的渲染器做了一层包装,在不同 runtime 中,tmagic-editor的渲染逻辑和组件代码都是相同的。

并且,由于tmagic-editor在编辑器中的模拟器是通过 iframe 渲染的,和tmagic-editor平台本身可以做到框架解耦,所以 runtime 也可以用不同框架开发。目前tmagic-editor提供了 vue2/vue3 和 react 的 runtime 示例。

各个 runtime 的作用除了作为不同场景下的渲染环境,同时也是不同环境的打包构建载体。tmagic-editor示例代码中的打包就是基于 runtime 进行的。

业务相关

由于 runtime 是页面渲染的承载环境,其中会加载 @tmagic/ui 以及各个业务组件,业务发布项目页也是基于 runtime,所以在 runtime 中实现业务方的自定义逻辑是最合适的。runtime 可以提供一些全局 API,供业务组件调用。

模拟器中的页面渲染(Playground)

这一部分,对应的是 runtime 中的 playground。其实仔细查看源码,playground 和 page runtime 的差异,在于 playground 中需要响应编辑器中用户的操作:

  • 组件的增删改
  • 表单配置修改

@tmagic/cli

组件开发中可以知道,一个组件是由组件(component)、表单配置(formConfig)、初始值(initValue)三个部分组成,其中表单配置跟初始值是提供给@tmagic/editor使用的,组件则是提供给runtime使用的。所以提供了@tmagic/cli来生成这几个部分的入口文件,处理以上提到的三个部分,还有组件的事件配置列表(@tmagic/editor中使用),插件列表(runtime中使用),总共5个入口文件。

《补》

componentGroupList

RUNTIME

实现一个 runtime

runtime 和 UI 是配套实现的。每个版本的 runtime 都需要一个对应的 UI 来作为渲染器,实现渲染 DSL 呈现页面的功能。

UI

一个 UI 应该至少包含一个渲染器,来实现页面渲染同时可以提供一些基础组件。

page

runtime 的 page 部分,就是真实项目页面的渲染环境。发布出去的项目页都需要基于该部分来实现渲染功能。而 page 的主要逻辑,就是需要加载 UI,同时实现业务方需要的业务逻辑,比如:

  • 提供页面需要的全局 api
  • 业务需要的特殊实现逻辑
  • 加载第三方全局组件/插件等

playground

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

onRuntimeReady

在 playground 页面渲染后,需要调用接口通知编辑器完成加载。该调用需要传入一个参数 API,即挂载了增删改查功能的对象示例,提供给编辑器。

window.magic?.onRuntimeReady(API)

onPageElUpdate

playground 在每次更新了页面配置后,调用一次 onPageElUpdate 并传入一个 DOM 节点,该方法作用是传入一个页面渲染组件的根节点,用来告知编辑器的模拟器遮罩如何作出反应。

window.magic.onPageElUpdate(document.querySelector('.magic-ui-page'));

提供 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 , distroot }: SortEventData

页面发布

如介绍中提到的,tmagic-editor页面发布方案,是对构建产物 page/index.html 进行项目信息注入。项目信息就是搭建平台存储的页面配置。发布时,将注入项目信息的 page/index.html 发布出去即可。

版本管理

基于上一步提到的打包原理,每次构建后,得到的产物都可以进行归档编号,存为版本。涉及到的组件改动和新增修改,体现在各个版本中。

版本选择

版本管理具体如何实现,这取决于使用tmagic-editor的业务方。版本管理具有如下优点:

  1. 对于已经配置好发布的项目,使用固定版本,不会被新版本的特性影响,保证项目线上稳定运行
  2. 发布的新版本如果出现问题,可以及时回退选择使用旧版本

结合业务定制

tmagic-editor的静态资源构建,项目配置保存,页面发布,在tmagic-editor的提供的示例方案中,流程是:

  1. 触发构建,执行流水线,基于 runtime 执行 build
  2. 将构建产物归档推送至 cdn,存为一个ui版本
  3. 项目配保存后,项目发布时,将项目配置发布至 CDN 存储为 DSL.js,同时根据当前项目使用的ui版本,获取到 page/index.html,将 DSL.js 引用方式以 script 标签形式写入。
  4. 将注入信息的 page/index.html 发布为项目静态资源 act.html
  5. 线上可加载 act.html 访问项目

其中各个步骤的定制,可以交由业务方根据tmagic-editor提供的示例进行自定义修改。

开发组件

组件开发

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

组件注册

在 playground 中,我们可以尝试点击添加一个组件,在模拟器区域里,就会出现这个组件。其中就涉及到组件注册。组件注册其实就是保存好组件 type 的映射关系。type 可以参考组件介绍

vue3 版本的 @tmagic/ui 中,组件渲染逻辑里,type 会作为组件名进入渲染。所以在 vue3 的组件开发中,我们也需要在为 vue 组件声明 name 字段时,和 type 值对应起来,才能正确渲染组件。

组件规范

组件的基础形式,需要有四个文件

image.png
  • index.ts 入口文件,引入下面几个文件
  • formConfig 表单配置描述
  • initValue 表单初始值
  • event 定义联动事件
  • component.{vue,jsx} 组件样式、逻辑代码

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

1. 创建组件

在项目中,如 runtime vue3 目录中,创建一个名为 test-component 的组件目录,其中包含上面四个规范文件。

// index.js
// vue
import Test from './Test.vue';
// react 
import Test from './Test.tsx';

export { default as config } from './formConfig';
export { default as value } from './initValue';

export default Test;
// formConfig.js
export default [
  {
    type: 'select',
    text: '字体颜色',
    name: 'color',
    options: [
      {
        text: '红色字体',
        value: 'red',
      },
      {
        text: '蓝色字体',
        value: 'blue',
      },
    ],
  },
  {
    name: 'text',
    text: '配置文案',
  },
];
// initValue.js
export default {
  color: 'red',
  text: '一段文字',
};
<!-- Test.vue -->
<template>
  <div>
    <span>this is a Test component:</span>
    <span :style="{ color: config.color }">{{ config.text }}</span>
  </div>
</template>

<script>
export default {
  name: 'magic-ui-test',

  props: {
    config: {
      type: Object,
      default: () => ({}),
    },
  },

  setup() {},
};
</script>

2. 使用tmagic-cli

在 runtime vue3 中,我们已经提供好一份示例。在 tmagic.config.ts 文件中。只需要在 packages 加入你创建的组件的路径(如果是个 npm 包,则将路径替换为包名即可),打包工具就会自动识别到你的组件。

image.png

3. 启动 playground

在上面的步骤完成后,在 playground/src/configs/componentGroupList 中。找到组件栏的基础组件列表,在其中加入你的开发组件

{
  title: '基础组件',
  items: [
    {
      text: '文本',
      type: 'text',
    },
    {
      text: '按钮',
      type: 'button',
    },
    // 加入这个测试组件
    {
      text: '测试',
      type: 'test',
    },
  ],
}

然后,在 magic 项目根目录中,运行

npm run playground

至此,我们打开 playground 后,就能添加开发中的组件,并且得到这个开发中的组件在编辑器中的表现了。

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

编辑器扩展

UI扩展

一、顶部菜单栏定制

通常使用 m-editor 组件的 menu prop 来对齐进行设置;

顶部菜单栏分为  三个部分组成,所以 menu prop的数据格式如下:

{ left: [], center: [], right: [] }

数组的内容可以有三种种形式:内部定义好的字符串其他字符串MenuButton 或者 MenuComponent 对象

1. 内部定义好的字符串:

'/' | 'delete' | 'undo' | 'redo' | 'zoom' | 'zoom-in' | 'zoom-out' | 'guides' | 'rule' | 'scale-to-original' | 'scale-to-fit'

是组件内部定义的可直接使用的内置功能,具体含义可以查看 menu

2. 其他字符串

除去内部定好的字符串的其他字符串,则会被当成普通文本直接显示

3. MenuButton 或者 MenuComponent 对象

MenuButton 的定义

用于自定义一个按钮,例如定义一个返回按钮可以由如下配置实现

{
  type: 'buuton',
  text: '返回',
  handler: () => window.history.back(),
}
image.png

如果需要更复杂的功能则可以使用 MenuComponent, 可以用于实现渲染任意一个Vue组件

MenuComponent 的定义

tips:如果对内置的顶部菜单栏实现不满意还可以使用自定义的实现完全替换掉

<m-editor>
  <template #nav>
    <your-nav></your-nav>
  </template>
</m-editor>

二、左侧菜单栏

三、右侧属性配置栏

默认的属性配置栏会分为属性、样式、事件、高级4个tab分页,其中只有属性是在组件中的formConfig文件中定义,其他三个分页都是自动生成的,所有组件都是一样的。

默认的属性读取流程如下:

组件中定义formConfig -> 通过tamgic-cli构建成 runtime 中 /config/index.umd.cjs -> m-editor中加载然后配置到propsConfig prop中 -> m-editor保存到propsService中 -> 选中组件时editorService会去propsService调用getPropsConfig中读取

propsService.getPropsConfig会调取propsService.fillConfig添加样式、事件、高级3个tab分页

3. 自定义属性配置栏

默认属性配置栏是是使用@tmagic/form来实现的,如果需要使用其他组件来实现可以使用props-panelslot来将其替换掉

<m-editor>
  <template #props-panel>
    <your-props-panel></your-props-panel>
  </template>
</m-editor>

四、中间工作区域

JS Schema

tmagic-editor的业务组件需要有表单配置能力,我们通过一份配置来描述表单,我们采用的描述方案是 JS schema。当我们在编辑器中配置一个页面时,页面的基本信息和页面包含的组件信息,也是采用 JS schema 描述的。JS schema 描述方案,也是我们提供代码块功能的基础。

组件的配置描述,参考示例,是由开发者在开发组件时,通过 @tmagic/form 支持的表单项来提供的。

在编辑器中对页面进行编辑,保存得到的是一份关于页面基本信息、页面所包含组件以及组件配置信息的配置,我们称为 DSL,这份配置是最终页面渲染需要的描述信息。

JS schema 本质即是一个 js 对象,这个形式可以支持我们在组件的表单配置描述中,直接进行函数编写,功能灵活,对于前端开发来说更符合直觉,几乎没有理解成本。

表单配置

组件中的表单配置描述,在经过 @tmagic/form 表单渲染器后,可以生成表单栏的配置项。在表单栏中对表单进行配置,配置数据将动态写入 DSL 中。

DSL

编辑器中生成的 DSL 序列化存储后,在发布时,将其作为 js 文件发布出去,供生成页使用。一个生成页最终保存的 DSL 配置示例如下:

{
  id: "75f0extui9d7yksklx27hff8xg",
  name: "test_page",
  type: "app",
  beginTime: "2021-04-26T16:00:00.000Z",
  endTime: "2021-05-28T16:00:00.000Z",
  items: [
    {
      type: "page",
      name: "index",
      title: "1",
      layout: "absolute",
      style: {
        width: "375",
        height: "1728",
        backgroundColor: "rgba(218, 192, 192, 1)"
      },
      id: "39381280",
      items: [
        {
          type: "container",
          name: "组",
          id: "98549062",
          items: [
            {
              type: "button",
              id: "87016850",
              name: "按钮",
              style: {
                position: "absolute",
                left: 57,
                top: 152,
                right: "",
                bottom: "",
                width: 270,
                height: 38,
                backgroundImage: "",
                backgroundColor: "#fb6f00",
                backgroundRepeat: "no-repeat",
                backgroundSize: "100% 100%",
                transform: "none",
                textAlign: "center",
                border: 0
              },
              events: [
                {
                  name: "magic:common:events:click",
                  to: "button_3877",
                  method: ""
                }
              ],
              created: ()=>{},
              text: "请输入文本内容",
            },
            {
              id: "text_7909",
              style: {
                left: 88,
                top: -73,
                position: "absolute",
                width: 100,
                height: 14,
                transform: "none"
              },
              type: "text",
              name: "文本",
              text: "请输入文本内容",
              multiple: true,
            },
            {
              type: "button",
              id: "button_3877",
              style: {
                position: "absolute",
                left: "57",
                width: "270",
                height: "37.5",
                border: 0,
                backgroundColor: "#fb6f00"
              },
              name: "按钮",
              text: "请输入文本内容",
              multiple: true,
            }
          ],
          style: {
            width: "100%",
            height: "100",
            position: "absolute",
            left: 0,
            top: 204
          }
        }
      ]
    },
  ]
}

更新中⌛️......