快速开始: - 补充 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>
15 KiB
快速开始
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.ts 的 packages 中注册即可,参考组件开发 与页面发布 § @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 常见报错
Preprocessor dependency "sass" not found.—— 安装 sass:npm i sass -DUncaught 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-model、runtime-url、component-group-list、props-configs、props-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 个:runtimeUrl、componentGroupList、propsConfigs/propsValues、初始 DSL(v-model)。
runtimeUrl
编辑器中央的模拟器画布是一个 iframe,runtimeUrl 就是这个 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 propsValues 与 componentGroupList 中声明的组件通过 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-model:DSL 初始值
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 提供了多组 service(editorService / 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 文档。
下一步
- 基础概念:编辑器 / 模拟器 / runtime / DSL 的关系
- RUNTIME:实现并打包一个 runtime
- 组件开发:自定义业务组件
- 页面发布:基于
@tmagic/cli的产物结构与发布流程 - Playground 源码:与本节示例完全对应
通过 pnpm bootstrap && pnpm pg 即可在仓库本地启动这份 playground,自由调试。