roymondchen d16ab9a805 docs: 重写快速开始与 runtime 指南,与 playground/runtime 源码对齐
快速开始:
- 补充 admin-client / runtime 项目结构说明
- 完善 UI adapter(element-plus / tdesign-vue-next)说明
- 增加 Monaco worker 注入与常见报错处理
- 重写 m-editor 完整示例,对齐 playground 源码

runtime 指南:
- 完善 tmagic.config.ts 与 .tmagic 入口产物说明
- 拆分 playground / page 双入口实现细节
- 新增 vite 多入口构建、跨域方案
- 补充 @tmagic/vue-runtime-help 常用 Hook 表格

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-15 19:40:49 +08:00

15 KiB
Raw Blame History

快速开始

tmagic-editor 的编辑器已经封装成 npm 包,可以直接安装使用。编辑器使用 Vue 3 开发(仅支持 Vue 3),但承载真实业务的 runtime 不限框架,可以使用 Vue 2、Vue 3、React 等开发业务组件。

整个项目结构由两部分组成:

  • admin-client(编辑器 / 管理端):基于 @tmagic/editor,加载 runtime iframe、提供拖拽/属性配置/发布等能力。
  • runtime(运行时):负责解析 DSL 并渲染页面,分为编辑器内嵌的 playground 和线上发布使用的 page 两个产物。

仓库 playground/runtime/vue/ 就是一份完整可运行的最小实践,本节内容均与之对齐,可以对照阅读源码。

使用脚手架创建(推荐)

::: code-group

$ npm create tmagic@latest
$ pnpm create tmagic

:::

按照交互式提示,可以创建以下 6 种项目:

类型 说明
runtime 运行时DSL 渲染)
admin-client 管理端(编辑器)
components 组件库(组件 / 插件 / 数据源)
component 单个组件
data-source 单个数据源
plugin 单个插件

最少需要一个 runtime 加一个 admin-client,就能跑起一个完整的可视化搭建流程。后续可以再陆续创建组件、插件、数据源;新建好后到 runtime/tmagic.config.tspackages 中注册即可,参考组件开发页面发布 § @tmagic/cli

手动安装

::: tip 环境要求

  • Node.js ^20.19.0 || >=22.12.0
  • 推荐使用 Vite;如果使用 Vue CLI 需要在 vue.config.js 中加上 transpileDependencies: [/@tmagic/] :::

1. 安装编辑器依赖

@tmagic/editor 把内部使用到的 UI 组件抽象到了 @tmagic/design,通过 adapter 的形式接入具体的 UI 组件库。我们提供了:

任选其一即可,下面以 Element Plus 为例:

$ npm install @tmagic/editor @tmagic/core @tmagic/element-plus-adapter element-plus -S

@tmagic/editor 内部使用了 monaco-editor 作为代码编辑器,需要额外安装并按照官方配置指引注入 worker

$ npm install monaco-editor -S

2. 引入 @tmagic/editor

参考 playground/src/main.ts,在入口文件中按以下顺序完成 Monaco worker、UI 库样式、editor 样式与 adapter 的注入:

import { createApp } from "vue";
import EditorWorker from "monaco-editor/esm/vs/editor/editor.worker?worker";
import CssWorker from "monaco-editor/esm/vs/language/css/css.worker?worker";
import HtmlWorker from "monaco-editor/esm/vs/language/html/html.worker?worker";
import JsonWorker from "monaco-editor/esm/vs/language/json/json.worker?worker";
import TsWorker from "monaco-editor/esm/vs/language/typescript/ts.worker?worker";

import editorPlugin from "@tmagic/editor";
import MagicElementPlusAdapter from "@tmagic/element-plus-adapter";

import App from "./App.vue";

import "element-plus/dist/index.css";
import "@tmagic/editor/dist/style.css";

// @ts-ignore
globalThis.MonacoEnvironment = {
  getWorker(_: any, label: string) {
    if (label === "json") return new JsonWorker();
    if (["css", "scss", "less"].includes(label)) return new CssWorker();
    if (["html", "handlebars", "razor"].includes(label))
      return new HtmlWorker();
    if (["typescript", "javascript"].includes(label)) return new TsWorker();
    return new EditorWorker();
  },
};

createApp(App).use(editorPlugin, MagicElementPlusAdapter).mount("#app");

::: tip 切换 UI 适配器 playground 通过 sessionStorage 来切换 adapter参考实现

const adapter =
  sessionStorage.getItem("tmagic-playground-ui-adapter") || "element-plus";
const adapterModule =
  adapter === "tdesign-vue-next"
    ? import("@tmagic/tdesign-vue-next-adapter")
    : import("@tmagic/element-plus-adapter");

:::

::: tip 常见报错

  1. Preprocessor dependency "sass" not found. —— 安装 sassnpm i sass -D
  2. Uncaught ReferenceError: global is not defined —— Vite 项目需要在 vite.config.ts 中加上:
// vite 8以下版本
optimizeDeps: {
  esbuildOptions: {
    define: { global: 'globalThis' },
  },
}
// vite 8及以上
define: {
  global: 'globalThis',
},

:::

3. 渲染 m-editor

App.vue 中渲染 <TMagicEditor />(即 m-editor 组件),最少需要传入 v-modelruntime-urlcomponent-group-listprops-configsprops-values 五个核心属性:

<template>
  <div class="editor-app">
    <TMagicEditor
      v-model="value"
      ref="editor"
      :menu="menu"
      :runtime-url="runtimeUrl"
      :props-configs="propsConfigs"
      :props-values="propsValues"
      :event-method-list="eventMethodList"
      :datasource-configs="datasourceConfigs"
      :datasource-values="datasourceValues"
      :datasource-event-method-list="datasourceEventMethodList"
      :component-group-list="componentGroupList"
      :default-selected="defaultSelected"
      :stage-rect="stageRect"
      :auto-scroll-into-view="true"
    />
  </div>
</template>

<script lang="ts" setup>
import { ref, shallowRef } from "vue";
import type { MApp } from "@tmagic/core";
import { TMagicEditor } from "@tmagic/editor";

import componentGroupList from "./configs/componentGroupList";
import dsl from "./configs/dsl";
import { useEditorRes } from "./composables/use-editor-res";

const editor = shallowRef<InstanceType<typeof TMagicEditor>>();
const value = ref<MApp>(dsl);
const defaultSelected = ref(dsl.items[0].id);
const stageRect = ref({ width: 375, height: 817 });

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

const {
  propsValues,
  propsConfigs,
  eventMethodList,
  datasourceConfigs,
  datasourceValues,
  datasourceEventMethodList,
} = useEditorRes();

const menu = {
  left: [{ type: "text", text: "魔方" }],
  center: ["delete", "undo", "redo", "guides", "rule", "zoom"],
  right: [
    {
      type: "button",
      text: "保存",
      handler: () =>
        localStorage.setItem("magicDSL", JSON.stringify(value.value)),
    },
  ],
};
</script>

<style lang="scss">
html,
body,
#app {
  width: 100%;
  height: 100%;
  margin: 0;
  padding: 0;
}
.editor-app {
  width: 100%;
  height: 100%;
}
.editor-app .m-editor {
  flex: 1;
  height: 100%;
}
</style>

完整的菜单/预览/键盘快捷键实现可以参考 playground/src/pages/Editor.vue

更多 prop 详见编辑器 API,下文重点介绍最关键的 4 个:runtimeUrlcomponentGroupListpropsConfigs/propsValues、初始 DSLv-model)。

runtimeUrl

编辑器中央的模拟器画布是一个 iframeruntimeUrl 就是这个 iframe 加载的地址,里面运行着一份 playground runtime,负责响应编辑器中组件的增删改查。

playground 中通过 Vite proxy 把 runtime 服务(默认端口 8078)代理到了同一个域:

server: {
  port: 8098,
  proxy: {
    '^/tmagic-editor/playground/runtime': {
      target: 'http://127.0.0.1:8078',
      changeOrigin: true,
      prependPath: false,
    },
  },
}

实际项目中可以使用 npm create tmagic 快速生成一个 runtime 项目,详见RUNTIME

componentGroupList

componentGroupList 决定左侧组件库展示哪些组件分组。每个 item 通过 type 与 runtime 中注册的组件类型一一对应,添加到画布时编辑器会基于 type 通知 runtime 渲染对应组件。

import {
  Files,
  FolderOpened,
  PictureFilled,
  SwitchButton,
  Tickets,
} from "@element-plus/icons-vue";
import type { ComponentGroup } from "@tmagic/editor";

export default [
  {
    title: "示例容器",
    items: [
      { icon: FolderOpened, text: "组", type: "container" },
      { icon: FolderOpened, text: "蒙层", type: "overlay" },
      { icon: Files, text: "迭代器容器", type: "iterator-container" },
    ],
  },
  {
    title: "示例组件",
    items: [
      { icon: Tickets, text: "文本", type: "text" },
      { icon: SwitchButton, text: "按钮", type: "button" },
      { icon: PictureFilled, text: "图片", type: "img" },
    ],
  },
  // 也可以提供完整 schema 作为「组合」,添加时直接落入完整子树
  {
    title: "组合",
    items: [
      {
        icon: Tickets,
        text: "弹窗",
        data: {
          type: "overlay",
          name: "弹窗",
          style: {
            position: "fixed",
            width: "100%",
            height: "100%",
            top: 0,
            left: 0,
          },
          items: [
            /* ... */
          ],
        },
      },
    ],
  },
] as ComponentGroup[];

完整字段参考 componentGroupList

propsConfigs / propsValues

propsConfigs propsValuescomponentGroupList 中声明的组件通过 type 一一对应:

  • propsConfigs[type]:组件右侧表单的配置描述(在组件中 formConfig 字段提供)。
  • propsValues[type]:组件被添加到画布时的初始默认值(在组件中 initValue 字段提供)。

这些内容会通过 @tmagic/cli 在 runtime 构建时打包出对应的 UMD 文件编辑器异步加载即可。playground 中的真实做法(use-editor-res.ts

import { ref } from "vue";
import { asyncLoadJs } from "@tmagic/editor";

const { VITE_ENTRY_PATH } = import.meta.env;

export const useEditorRes = () => {
  const propsValues = ref<Record<string, any>>({});
  const propsConfigs = ref<Record<string, any>>({});
  const eventMethodList = ref<Record<string, any>>({});
  const datasourceConfigs = ref<Record<string, any>>({});
  const datasourceValues = ref<Record<string, any>>({});
  const datasourceEventMethodList = ref<Record<string, any>>({
    base: { events: [], methods: [] },
  });

  asyncLoadJs(`${VITE_ENTRY_PATH}/config/index.umd.cjs`).then(() => {
    propsConfigs.value = (globalThis as any).magicPresetConfigs;
  });
  asyncLoadJs(`${VITE_ENTRY_PATH}/value/index.umd.cjs`).then(() => {
    propsValues.value = (globalThis as any).magicPresetValues;
  });
  asyncLoadJs(`${VITE_ENTRY_PATH}/event/index.umd.cjs`).then(() => {
    eventMethodList.value = (globalThis as any).magicPresetEvents;
  });
  asyncLoadJs(`${VITE_ENTRY_PATH}/ds-config/index.umd.cjs`).then(() => {
    datasourceConfigs.value = (globalThis as any).magicPresetDsConfigs;
  });
  asyncLoadJs(`${VITE_ENTRY_PATH}/ds-value/index.umd.cjs`).then(() => {
    datasourceValues.value = (globalThis as any).magicPresetDsValues;
  });

  return {
    propsValues,
    propsConfigs,
    eventMethodList,
    datasourceConfigs,
    datasourceValues,
    datasourceEventMethodList,
  };
};

::: tip 怎样得到这些 UMD 文件? 在 runtime 项目中执行 npm run build:libs(参考 runtime/vue/package.json),会在 dist/entry/ 下生成 config/value/event/ds-config/ds-value 五个目录的 UMD 文件,全局变量分别为 magicPresetConfigs magicPresetValues magicPresetEvents magicPresetDsConfigs magicPresetDsValues。 :::

如果是在调试期,也可以直接 hardcode 一份 propsConfigs / propsValues,比如:

const propsConfigs = ref({
  text: [{ name: "text", text: "文案" }],
  button: [{ name: "text", text: "按钮文案" }],
});

const propsValues = ref({
  text: { text: "一段文字" },
  button: { text: "按钮" },
});

v-modelDSL 初始值

v-model 绑定的是整个页面的 DSL,最简的初始 DSL 长这样:

import { type MApp, NodeType } from "@tmagic/core";

const dsl: MApp = {
  id: "1",
  name: "demo",
  type: NodeType.ROOT,
  items: [
    {
      type: NodeType.PAGE,
      id: "page_1",
      name: "index",
      layout: "absolute",
      style: { position: "relative", width: "100%", height: "100%" },
      items: [],
    },
  ],
};

完整含数据源、代码块、事件联动的 DSL 示例见 playground/src/configs/dsl.ts

::: tip 持久化与历史记录 playground 用 localStorage + serialize-javascript 做了一个本地持久化方案,并在保存后调用 editor.editorService.resetModifiedNodeId() 重置修改状态,可以直接复用。 :::

进阶:编辑器服务与插件

@tmagic/editor 提供了多组 serviceeditorService / propsService / historyService / uiService …)和 插件机制,可以非侵入式扩展行为。例如 playground 中:

import { editorService, propsService } from "@tmagic/editor";

editorService.usePlugin({
  beforeDoAdd: (config, parent) => {
    if (config.type === "overlay") {
      // 蒙层始终插入到当前 page 下,并钉到 (0, 0)
      config.style = { ...config.style, left: 0, top: 0 };
      return [config, editorService.get("page")];
    }
    return [config, parent];
  },
});

propsService.usePlugin({
  beforeFillConfig: (config) => [config, "100px"],
});

更多扩展能力见编辑器扩展与各 service 的 API 文档

下一步

通过 pnpm bootstrap && pnpm pg 即可在仓库本地启动这份 playground自由调试。