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

443 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 快速开始
tmagic-editor 的编辑器已经封装成 npm 包,可以直接安装使用。编辑器使用 Vue 3 开发(**仅支持 Vue 3**),但承载真实业务的 runtime 不限框架,可以使用 Vue 2、Vue 3、React 等开发业务组件。
整个项目结构由两部分组成:
- **admin-client**(编辑器 / 管理端):基于 `@tmagic/editor`,加载 runtime iframe、提供拖拽/属性配置/发布等能力。
- **runtime**(运行时):负责解析 DSL 并渲染页面,分为编辑器内嵌的 `playground` 和线上发布使用的 `page` 两个产物。
> 仓库 [`playground/`](https://github.com/Tencent/tmagic-editor/tree/master/playground) 与 [`runtime/vue/`](https://github.com/Tencent/tmagic-editor/tree/master/runtime/vue) 就是一份完整可运行的最小实践,本节内容均与之对齐,可以对照阅读源码。
## 使用脚手架创建(推荐)
::: code-group
```bash [npm]
$ npm create tmagic@latest
```
```bash [pnpm]
$ pnpm create tmagic
```
:::
按照交互式提示,可以创建以下 `6` 种项目:
| 类型 | 说明 |
| -------------- | ------------------------------ |
| `runtime` | 运行时DSL 渲染) |
| `admin-client` | 管理端(编辑器) |
| `components` | 组件库(组件 / 插件 / 数据源) |
| `component` | 单个组件 |
| `data-source` | 单个数据源 |
| `plugin` | 单个插件 |
最少需要一个 `runtime` 加一个 `admin-client`,就能跑起一个完整的可视化搭建流程。后续可以再陆续创建组件、插件、数据源;新建好后到 `runtime/tmagic.config.ts` 的 `packages` 中注册即可,参考[组件开发](./component.md) 与[页面发布 § @tmagic/cli](./publish.md#tmagic-cli)。
## 手动安装
::: tip 环境要求
- Node.js `^20.19.0 || >=22.12.0`
- 推荐使用 [Vite](https://cn.vitejs.dev/);如果使用 [Vue CLI](https://cli.vuejs.org/zh/) 需要在 `vue.config.js` 中加上 `transpileDependencies: [/@tmagic/]`
:::
### 1. 安装编辑器依赖
`@tmagic/editor` 把内部使用到的 UI 组件抽象到了 `@tmagic/design`,通过 **adapter** 的形式接入具体的 UI 组件库。我们提供了:
- [`@tmagic/element-plus-adapter`](https://github.com/Tencent/tmagic-editor/tree/master/packages/element-plus-adapter):接入 [Element Plus](https://element-plus.org/)
- [`@tmagic/tdesign-vue-next-adapter`](https://github.com/Tencent/tmagic-editor/tree/master/packages/tdesign-vue-next-adapter):接入 [TDesign Vue Next](https://tdesign.tencent.com/vue-next/overview)
任选其一即可,下面以 Element Plus 为例:
```bash
$ npm install @tmagic/editor @tmagic/core @tmagic/element-plus-adapter element-plus -S
```
`@tmagic/editor` 内部使用了 [monaco-editor](https://microsoft.github.io/monaco-editor/) 作为代码编辑器,需要额外安装并按照官方[配置指引](https://github.com/microsoft/monaco-editor/blob/main/docs/integrate-esm.md)注入 worker
```bash
$ npm install monaco-editor -S
```
### 2. 引入 @tmagic/editor
参考 [`playground/src/main.ts`](https://github.com/Tencent/tmagic-editor/blob/master/playground/src/main.ts),在入口文件中按以下顺序完成 Monaco worker、UI 库样式、editor 样式与 adapter 的注入:
```ts
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参考实现
```ts
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.` —— 安装 sass`npm i sass -D`
2. `Uncaught ReferenceError: global is not defined` —— Vite 项目需要在 `vite.config.ts` 中加上:
```ts
// vite 8以下版本
optimizeDeps: {
esbuildOptions: {
define: { global: 'globalThis' },
},
}
```
```ts
// vite 8及以上
define: {
global: 'globalThis',
},
```
:::
### 3. 渲染 m-editor
在 `App.vue` 中渲染 `<TMagicEditor />`(即 `m-editor` 组件),最少需要传入 `v-model`、`runtime-url`、`component-group-list`、`props-configs`、`props-values` 五个核心属性:
```vue
<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`](https://github.com/Tencent/tmagic-editor/blob/master/playground/src/pages/Editor.vue)。
更多 prop 详见[编辑器 API](../api/editor/props.md),下文重点介绍最关键的 4 个:`runtimeUrl`、`componentGroupList`、`propsConfigs/propsValues`、初始 DSL`v-model`)。
## runtimeUrl
编辑器中央的模拟器画布是一个 `iframe``runtimeUrl` 就是这个 iframe 加载的地址,里面运行着一份 **playground runtime**,负责响应编辑器中组件的增删改查。
playground 中通过 Vite proxy 把 runtime 服务(默认端口 `8078`)代理到了同一个域:
```ts
server: {
port: 8098,
proxy: {
'^/tmagic-editor/playground/runtime': {
target: 'http://127.0.0.1:8078',
changeOrigin: true,
prependPath: false,
},
},
}
```
实际项目中可以使用 `npm create tmagic` 快速生成一个 runtime 项目,详见[RUNTIME](./runtime.md)。
## componentGroupList
`componentGroupList` 决定左侧组件库展示哪些组件分组。每个 item 通过 `type` 与 runtime 中注册的组件类型一一对应,添加到画布时编辑器会基于 `type` 通知 runtime 渲染对应组件。
```ts
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`](../api/editor/props.md#componentgrouplist)。
## propsConfigs / propsValues
`propsConfigs` `propsValues` 与 `componentGroupList` 中声明的组件通过 `type` 一一对应:
- `propsConfigs[type]`:组件**右侧表单**的配置描述(在组件中 `formConfig` 字段提供)。
- `propsValues[type]`:组件被添加到画布时的**初始默认值**(在组件中 `initValue` 字段提供)。
这些内容会通过 `@tmagic/cli` 在 runtime 构建时打包出对应的 UMD 文件编辑器异步加载即可。playground 中的真实做法([`use-editor-res.ts`](https://github.com/Tencent/tmagic-editor/blob/master/playground/src/pages/composables/use-editor-res.ts)
```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`](https://github.com/Tencent/tmagic-editor/blob/master/runtime/vue/package.json)),会在 `dist/entry/` 下生成 `config/value/event/ds-config/ds-value` 五个目录的 UMD 文件,全局变量分别为 `magicPresetConfigs` `magicPresetValues` `magicPresetEvents` `magicPresetDsConfigs` `magicPresetDsValues`。
:::
如果是在调试期,也可以直接 hardcode 一份 `propsConfigs` / `propsValues`,比如:
```ts
const propsConfigs = ref({
text: [{ name: "text", text: "文案" }],
button: [{ name: "text", text: "按钮文案" }],
});
const propsValues = ref({
text: { text: "一段文字" },
button: { text: "按钮" },
});
```
## v-modelDSL 初始值
`v-model` 绑定的是整个页面的 [DSL](./advanced/js-schema.md),最简的初始 DSL 长这样:
```ts
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`](https://github.com/Tencent/tmagic-editor/blob/master/playground/src/configs/dsl.ts)。
::: tip 持久化与历史记录
playground 用 `localStorage` + `serialize-javascript` 做了一个本地持久化方案,并在保存后调用 `editor.editorService.resetModifiedNodeId()` 重置修改状态,可以直接复用。
:::
## 进阶:编辑器服务与插件
`@tmagic/editor` 提供了多组 **service**`editorService` / `propsService` / `historyService` / `uiService` …)和 **插件机制**,可以非侵入式扩展行为。例如 playground 中:
```ts
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"],
});
```
更多扩展能力见[编辑器扩展](./editor-expand.md)与各 service 的 [API 文档](../api/editor/props.md)。
## 下一步
- [基础概念](./conception.md):编辑器 / 模拟器 / runtime / DSL 的关系
- [RUNTIME](./runtime.md):实现并打包一个 runtime
- [组件开发](./component.md):自定义业务组件
- [页面发布](./publish.md):基于 `@tmagic/cli` 的产物结构与发布流程
- [Playground 源码](https://github.com/Tencent/tmagic-editor/tree/master/playground):与本节示例完全对应
通过 `pnpm bootstrap && pnpm pg` 即可在仓库本地启动这份 playground自由调试。