mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2026-05-20 07:23:38 +00:00
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>
This commit is contained in:
parent
df8790042f
commit
d16ab9a805
@ -1,6 +1,15 @@
|
||||
# 快速开始
|
||||
|
||||
tmagic-editor的编辑器我们已经封装成一个 npm 包,可以直接安装使用。编辑器是使用 vue3 开发的(仅支持vue3),但使用编辑器的业务(runtime)可以不限框架,可以用 vue2、react 等开发业务组件。
|
||||
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
|
||||
|
||||
@ -11,220 +20,423 @@ $ npm create tmagic@latest
|
||||
```bash [pnpm]
|
||||
$ pnpm create tmagic
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
按照提示操作可以创建`6`种项目:
|
||||
按照交互式提示,可以创建以下 `6` 种项目:
|
||||
|
||||
* runtime:运行时(DSL渲染)
|
||||
* admin-client:管理端(编辑器)
|
||||
* components:组件库(组件/插件/数据源)
|
||||
* component:组件
|
||||
* data-source:数据源
|
||||
* plugin:插件
|
||||
| 类型 | 说明 |
|
||||
| -------------- | ------------------------------ |
|
||||
| `runtime` | 运行时(DSL 渲染) |
|
||||
| `admin-client` | 管理端(编辑器) |
|
||||
| `components` | 组件库(组件 / 插件 / 数据源) |
|
||||
| `component` | 单个组件 |
|
||||
| `data-source` | 单个数据源 |
|
||||
| `plugin` | 单个插件 |
|
||||
|
||||
至少需要一个runtime与admin-client后,就可以运行起一个最简单的项目了。
|
||||
|
||||
后续还需要新增组件、插件、数据源等,可以继续添加后面几种类型的项目。
|
||||
|
||||
新增好一个组件/插件/数据源后可以到runtime/tmagic.config.ts中配置到packages中
|
||||
最少需要一个 `runtime` 加一个 `admin-client`,就能跑起一个完整的可视化搭建流程。后续可以再陆续创建组件、插件、数据源;新建好后到 `runtime/tmagic.config.ts` 的 `packages` 中注册即可,参考[组件开发](./component.md) 与[页面发布 § @tmagic/cli](./publish.md#tmagic-cli)。
|
||||
|
||||
## 手动安装
|
||||
|
||||
node.js >= 18
|
||||
::: tip 环境要求
|
||||
|
||||
可以通过[Vite](https://cn.vitejs.dev/) 或 [Vue CLI](https://cli.vuejs.org/zh/)快速创建项目。
|
||||
- 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/]`
|
||||
:::
|
||||
|
||||
> 使用Vue CLI生成的项目需要在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 -S
|
||||
$ npm install @tmagic/editor @tmagic/core @tmagic/element-plus-adapter element-plus -S
|
||||
```
|
||||
|
||||
由于在实际应用中项目常常会用到例如[element-plus](https://element-plus.org/)、[tdesign-vue-next](https://tdesign.tencent.com/vue-next/overview)等UI组件库。为了能让使用者能够选择不同UI库,[@tmagic/editor](https://github.com/Tencent/tmagic-editor/tree/master/packages/editor)将其中使用到的UI组件封装到[@tmagic/design](https://github.com/Tencent/tmagic-editor/tree/master/packages/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/),所以还需要安装相关的依赖。
|
||||
|
||||
```bash
|
||||
$ npm install @tmagic/element-plus-adapter element-plus -S
|
||||
```
|
||||
|
||||
editor 中还包含了[monaco-editor](https://microsoft.github.io/monaco-editor/),所以还需安装monaco-editor,可以参考 monaco-editor 的[配置指引](https://github.com/microsoft/monaco-editor/blob/main/docs/integrate-esm.md)。
|
||||
`@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
|
||||
|
||||
## 引入 @tmagic/editor
|
||||
参考 [`playground/src/main.ts`](https://github.com/Tencent/tmagic-editor/blob/master/playground/src/main.ts),在入口文件中按以下顺序完成 Monaco worker、UI 库样式、editor 样式与 adapter 的注入:
|
||||
|
||||
在 main.js 中写入以下内容:
|
||||
```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";
|
||||
|
||||
```js
|
||||
import { createApp } from 'vue';
|
||||
import ElementPlus from 'element-plus';
|
||||
import zhCn from 'element-plus/es/locale/lang/zh-cn';
|
||||
import editorPlugin from "@tmagic/editor";
|
||||
import MagicElementPlusAdapter from "@tmagic/element-plus-adapter";
|
||||
|
||||
import editorPlugin from '@tmagic/editor';
|
||||
import MagicElementPlusAdapter from '@tmagic/element-plus-adapter';
|
||||
import App from "./App.vue";
|
||||
|
||||
import App from './App.vue';
|
||||
import "element-plus/dist/index.css";
|
||||
import "@tmagic/editor/dist/style.css";
|
||||
|
||||
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();
|
||||
},
|
||||
};
|
||||
|
||||
const app = createApp(App);
|
||||
app.use(ElementPlus, {
|
||||
locale: zhCn,
|
||||
});
|
||||
app.use(editorPlugin, MagicElementPlusAdapter);
|
||||
app.mount('#app');
|
||||
createApp(App).use(editorPlugin, MagicElementPlusAdapter).mount("#app");
|
||||
```
|
||||
|
||||
以上代码便完成了 @tmagic/editor 的引入。需要注意的是,样式文件需要单独引入。
|
||||
::: tip 切换 UI 适配器
|
||||
playground 通过 `sessionStorage` 来切换 adapter,参考实现:
|
||||
|
||||
可以参考我们提供的[Playground](https://github.com/Tencent/tmagic-editor/blob/master/playground/src/main.ts)示例实现代码
|
||||
```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");
|
||||
```
|
||||
|
||||
## 使用 m-editor 组件
|
||||
:::
|
||||
|
||||
在 App.vue 中写入以下内容:
|
||||
::: tip 常见报错
|
||||
|
||||
```html
|
||||
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>
|
||||
<m-editor
|
||||
v-model="dsl"
|
||||
:menu="menu"
|
||||
:runtime-url="runtimeUrl"
|
||||
:props-configs="propsConfigs"
|
||||
:props-values="propsValues"
|
||||
:component-group-list="componentGroupList"
|
||||
>
|
||||
</m-editor>
|
||||
<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>
|
||||
import { defineComponent, ref } from "vue";
|
||||
<script lang="ts" setup>
|
||||
import { ref, shallowRef } from "vue";
|
||||
import type { MApp } from "@tmagic/core";
|
||||
import { TMagicEditor } from "@tmagic/editor";
|
||||
|
||||
export default defineComponent({
|
||||
name: "App",
|
||||
import componentGroupList from "./configs/componentGroupList";
|
||||
import dsl from "./configs/dsl";
|
||||
import { useEditorRes } from "./composables/use-editor-res";
|
||||
|
||||
setup() {
|
||||
return {
|
||||
menu: ref({
|
||||
left: [
|
||||
// 顶部左侧菜单按钮
|
||||
],
|
||||
center: [
|
||||
// 顶部中间菜单按钮
|
||||
],
|
||||
right: [
|
||||
// 顶部右侧菜单按钮
|
||||
],
|
||||
}),
|
||||
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 });
|
||||
|
||||
dsl: ref({
|
||||
// 初始化页面数据
|
||||
}),
|
||||
const { VITE_RUNTIME_PATH } = import.meta.env;
|
||||
const runtimeUrl = `${VITE_RUNTIME_PATH}/playground/index.html`;
|
||||
|
||||
runtimeUrl: "/runtime/vue/playground/index.html",
|
||||
const {
|
||||
propsValues,
|
||||
propsConfigs,
|
||||
eventMethodList,
|
||||
datasourceConfigs,
|
||||
datasourceValues,
|
||||
datasourceEventMethodList,
|
||||
} = useEditorRes();
|
||||
|
||||
propsConfigs: [
|
||||
// 组件属性列表
|
||||
],
|
||||
propsValues: [
|
||||
// 组件默认值
|
||||
],
|
||||
|
||||
componentGroupList: ref([
|
||||
// 组件列表
|
||||
]),
|
||||
};
|
||||
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 {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#app {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.m-editor {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
}
|
||||
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>
|
||||
```
|
||||
|
||||
关于 [@tmagic/editor](https://github.com/Tencent/tmagic-editor/tree/master/packages/editor) 组件,更多的属性配置详情请参考[编辑器 API](../api/editor/props.md)。
|
||||
完整的菜单/预览/键盘快捷键实现可以参考 [`playground/src/pages/Editor.vue`](https://github.com/Tencent/tmagic-editor/blob/master/playground/src/pages/Editor.vue)。
|
||||
|
||||
其中,**有四个需要注意的属性配置项**:`runtimeUrl` `propsValues` `propsConfigs` `componentGroupList`。这是能让我们的编辑器正常运行的关键。
|
||||
更多 prop 详见[编辑器 API](../api/editor/props.md),下文重点介绍最关键的 4 个:`runtimeUrl`、`componentGroupList`、`propsConfigs/propsValues`、初始 DSL(`v-model`)。
|
||||
|
||||
:::tip
|
||||
如果出现```Preprocessor dependency "sass" not found. Did you install it?```,那么需要install sass
|
||||
## runtimeUrl
|
||||
|
||||
```bash
|
||||
npm install sass -D
|
||||
```
|
||||
:::
|
||||
编辑器中央的模拟器画布是一个 `iframe`,`runtimeUrl` 就是这个 iframe 加载的地址,里面运行着一份 **playground runtime**,负责响应编辑器中组件的增删改查。
|
||||
|
||||
:::tip
|
||||
如果是使用vite构建工具,如果出现 ```Uncaught ReferenceError: global is not defined```,那么需要再vite.config.js中添加如下配置:
|
||||
playground 中通过 Vite proxy 把 runtime 服务(默认端口 `8078`)代理到了同一个域:
|
||||
|
||||
```js
|
||||
{
|
||||
optimizeDeps: {
|
||||
esbuildOptions: {
|
||||
define: {
|
||||
global: 'globalThis',
|
||||
},
|
||||
```ts
|
||||
server: {
|
||||
port: 8098,
|
||||
proxy: {
|
||||
'^/tmagic-editor/playground/runtime': {
|
||||
target: 'http://127.0.0.1:8078',
|
||||
changeOrigin: true,
|
||||
prependPath: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
:::
|
||||
|
||||
## runtimeUrl
|
||||
|
||||
该配置涉及到 [runtime 概念](runtime.md),tmagic-editor编辑器中心的模拟器画布,是一个 iframe(这里的 `runtimeUrl` 配置的,就是你提供的 iframe 的 url),其中渲染了一个 runtime,用来响应编辑器中的组件增删改等操作。
|
||||
:::tip
|
||||
可以使用`npm create tmagic` 来快速创建一个runtime项目。
|
||||
:::
|
||||
实际项目中可以使用 `npm create tmagic` 快速生成一个 runtime 项目,详见[RUNTIME](./runtime.md)。
|
||||
|
||||
## componentGroupList
|
||||
|
||||
`componentGroupList` 是指定左侧组件库内容的配置。此处定义了在编辑器组件库中有什么组件。在添加的时候通过组件 `type` 来确定 runtime 中要渲染什么组件。可以参考 [componentGroupList 配置](../api/editor/props.html#componentgrouplist)。
|
||||
`componentGroupList` 决定左侧组件库展示哪些组件分组。每个 item 通过 `type` 与 runtime 中注册的组件类型一一对应,添加到画布时编辑器会基于 `type` 通知 runtime 渲染对应组件。
|
||||
|
||||
## propsConfigs/propsValues
|
||||
```ts
|
||||
import {
|
||||
Files,
|
||||
FolderOpened,
|
||||
PictureFilled,
|
||||
SwitchButton,
|
||||
Tickets,
|
||||
} from "@element-plus/icons-vue";
|
||||
import type { ComponentGroup } from "@tmagic/editor";
|
||||
|
||||
`propsConfigs` `propsValues` 和 `componentGroupList` 中声明的组件是一一对应的,通过 `type` 来识别属于哪个组件,该配置涉及的内容,就是组件的表单配置描述,在[组件开发中](./component.md)会通过 formConfig 配置来声明这份内容。
|
||||
|
||||
`configs` 既可以通过 hardcode 方式写上每个组件的表单配置,也可以通过组件打包方式得到对应内容,然后通过异步加载来载入。比如:
|
||||
|
||||
```javascript
|
||||
setup() {
|
||||
asyncLoadJs(`/runtime/vue/assets/config.js`).then(() => {
|
||||
propsConfigs.value = window.magicPresetConfigs;
|
||||
});
|
||||
asyncLoadJs(`/runtime/vue/assets/value.js`).then(() => {
|
||||
propsValues.value = window.magicPresetValues;
|
||||
});
|
||||
}
|
||||
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[];
|
||||
```
|
||||
|
||||
::: tip 如何快速得到一个 configs/values
|
||||
上述的 runtime 产物中,dist 目录中即包含一个 entry 文件夹,在你的项目组件初始化之后,分别异步加载里面的config/index.umd.js、value/index.umd.js。并如上面代码中,赋值给 configs/values 即可。
|
||||
完整字段参考 [`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: "按钮文案" }],
|
||||
});
|
||||
|
||||
除了上述内容外,文档的其他章节中,也会更深入的描述整个tmagic-editor的设计理念和实现细节。同时你也可以查看我们的[项目源码](https://github.com/Tencent/tmagic-editor),从源码提供的 playground 和 runtime 示例来开发和理解tmagic-editor。
|
||||
const propsValues = ref({
|
||||
text: { text: "一段文字" },
|
||||
button: { text: "按钮" },
|
||||
});
|
||||
```
|
||||
|
||||
## v-model:DSL 初始值
|
||||
|
||||
`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,自由调试。
|
||||
|
||||
@ -1,29 +1,328 @@
|
||||
# RUNTIME
|
||||
本章详细介绍如何深入理解tmagic-editor的打包,以及如何根据需求定制,修改tmagic-editor的页面打包发布方案。页面发布、打包相关的定制化开发,需要使用tmagic-editor的业务方,搭建好基于开源tmagic-editor的管理平台、存储服务等配套设施。
|
||||
|
||||
本章详细介绍 tmagic-editor 中 runtime 的概念、目录结构与实现方式。所有内容均与开源仓库 [`runtime/vue/`](https://github.com/Tencent/tmagic-editor/tree/master/runtime/vue) 一一对应,可以对照阅读。
|
||||
|
||||
## runtime 是什么
|
||||
|
||||
runtime是用来解析DSL的执行环境,用于渲染 DSL 呈现页面。
|
||||
**runtime 是用来解析 DSL 的执行环境**。编辑器只负责生成 DSL,最终把它**渲染成可见页面**的工作交给 runtime。
|
||||
|
||||
编辑器生成出来的DSL需要通过 runtime 来渲染。
|
||||
在一份完整的 tmagic-editor 项目中,runtime 同时承担两个角色:
|
||||
|
||||
## 实现一个 runtime
|
||||
| 角色 | 入口 | 用途 |
|
||||
| --- | --- | --- |
|
||||
| **page** | `runtime/vue/page/` | 线上发布产物,加载 `window.magicDSL` 渲染真实页面 |
|
||||
| **playground** | `runtime/vue/playground/` | 编辑器中央 iframe 加载的画布,响应增删改并渲染所见即所得 |
|
||||
|
||||
:::tip
|
||||
可以使用`npm create tmagic` 来快速创建一个runtime项目。
|
||||
两者共用同一份组件、插件、数据源代码,只在入口(`main.ts` / `App.vue`)上有差异。
|
||||
|
||||
::: tip
|
||||
DSL、playground 与 editor 之间的通信原理可以前往[教程](/guide/tutorial/)继续了解。
|
||||
:::
|
||||
|
||||
创建出来的项目会包含page、playground两个目录。
|
||||
## 创建 runtime 项目
|
||||
|
||||
::: tip
|
||||
推荐用 `npm create tmagic@latest` / `pnpm create tmagic` 快速生成 runtime 模板,按提示选择 `runtime` 即可。
|
||||
:::
|
||||
|
||||
生成的项目结构如下(与 [`runtime/vue/`](https://github.com/Tencent/tmagic-editor/tree/master/runtime/vue) 完全一致):
|
||||
|
||||
```bash
|
||||
.
|
||||
├── page
|
||||
├── playground
|
||||
runtime/vue
|
||||
├── page/ # 线上 page 入口
|
||||
│ ├── App.vue
|
||||
│ ├── index.html
|
||||
│ ├── main.ts
|
||||
│ └── utils/
|
||||
├── playground/ # 编辑器内 iframe 入口
|
||||
│ ├── App.vue
|
||||
│ ├── index.html
|
||||
│ └── main.ts
|
||||
├── public/
|
||||
├── scripts/ # build 脚本(res / page / playground / all)
|
||||
├── tmagic.config.ts # @tmagic/cli 配置:声明组件、插件、数据源
|
||||
├── tmagic.config.local.ts# 本地覆盖配置(可选)
|
||||
└── vite.config.ts # 多入口构建:page + playground
|
||||
```
|
||||
|
||||
page用于生产环境
|
||||
## tmagic.config.ts:声明组件 / 插件 / 数据源
|
||||
|
||||
playground用于编辑器中
|
||||
`tmagic.config.ts` 是 [@tmagic/cli](./publish.md#tmagic-cli) 的入口,它会扫描 `packages` 列表,生成 `.tmagic/comp-entry.ts` 等 5 个入口文件,runtime 只需要从这些入口里 `import` 即可:
|
||||
|
||||
:::tip
|
||||
想要了解DSL的解析以及runtime与编辑器的通信,可以前往[教程](/guide/tutorial/)
|
||||
```ts
|
||||
import { defineConfig } from '@tmagic/cli';
|
||||
|
||||
export default defineConfig({
|
||||
componentFileAffix: '.vue',
|
||||
// 是否使用 vite + 异步组件,详见 page/main.ts 中的 defineAsyncComponent
|
||||
dynamicImport: true,
|
||||
npmConfig: {
|
||||
client: 'pnpm',
|
||||
keepPackageJsonClean: true,
|
||||
},
|
||||
packages: [
|
||||
{
|
||||
// key 为组件 type,需要与编辑器中 componentGroupList 的 type 对应
|
||||
button: '@tmagic/vue-button',
|
||||
container: '@tmagic/vue-container',
|
||||
img: '@tmagic/vue-img',
|
||||
'iterator-container': '@tmagic/vue-iterator-container',
|
||||
overlay: '@tmagic/vue-overlay',
|
||||
page: '@tmagic/vue-page',
|
||||
'page-fragment': '@tmagic/vue-page-fragment',
|
||||
'page-fragment-container': '@tmagic/vue-page-fragment-container',
|
||||
qrcode: '@tmagic/vue-qrcode',
|
||||
text: '@tmagic/vue-text',
|
||||
},
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
`tmagic.config.local.ts` 用于本地覆盖(不会被提交),常见用法是把线上 npm 包临时替换为本地组件目录调试。
|
||||
|
||||
执行 `npm run tmagic`(即 `tmagic entry`)后,runtime 根目录下会生成:
|
||||
|
||||
```bash
|
||||
.tmagic/
|
||||
├── comp-entry.ts # page 同步组件入口
|
||||
├── async-comp-entry.ts # page 异步组件入口(dynamicImport 时使用)
|
||||
├── config-entry.ts # 编辑器右侧表单配置
|
||||
├── value-entry.ts # 组件初始值
|
||||
├── event-entry.ts # 组件事件 / 方法列表
|
||||
├── plugin-entry.ts # 插件入口
|
||||
├── datasource-entry.ts # 同步数据源入口
|
||||
└── async-datasource-entry.ts # 异步数据源入口
|
||||
```
|
||||
|
||||
> 详细产物说明见[页面发布 § @tmagic/cli](./publish.md#tmagic-cli)。
|
||||
|
||||
## playground runtime 实现
|
||||
|
||||
playground 是编辑器中央 iframe 加载的画布,最关键的逻辑就是把编辑器派发的 DSL 变更同步到本地 Vue 状态并触发重新渲染。
|
||||
|
||||
`@tmagic/vue-runtime-help` 提供的 `useEditorDsl` Hook 已经帮我们实现了与编辑器的通信(`onRuntimeReady` / `updateRootConfig` / `updatePageId` / `add` / `update` / `remove` 等);只需要在入口里:
|
||||
|
||||
1. 创建 `TMagicApp` 实例,注册组件、数据源、插件;
|
||||
2. 通过 `provide('app', app)` 把实例注入子组件;
|
||||
3. 在 `App.vue` 里使用 `useEditorDsl()` + `useComponent('page')` 渲染页面。
|
||||
|
||||
完整的 [`runtime/vue/playground/main.ts`](https://github.com/Tencent/tmagic-editor/blob/master/runtime/vue/playground/main.ts):
|
||||
|
||||
```ts
|
||||
import { createApp } from 'vue';
|
||||
import TMagicApp, { DataSourceManager, DeepObservedData } from '@tmagic/core';
|
||||
|
||||
import App from './App.vue';
|
||||
|
||||
import '@tmagic/core/resetcss.css';
|
||||
|
||||
DataSourceManager.registerObservedData(DeepObservedData);
|
||||
|
||||
Promise.all([
|
||||
import('../.tmagic/comp-entry'),
|
||||
import('../.tmagic/plugin-entry'),
|
||||
import('../.tmagic/datasource-entry'),
|
||||
]).then(([components, plugins, dataSources]) => {
|
||||
const vueApp = createApp(App);
|
||||
|
||||
const app = new TMagicApp({
|
||||
ua: window.navigator.userAgent,
|
||||
platform: 'editor',
|
||||
});
|
||||
|
||||
if (app.env.isWeb) {
|
||||
app.setDesignWidth(window.document.documentElement.getBoundingClientRect().width);
|
||||
}
|
||||
|
||||
Object.entries(components.default).forEach(([type, component]: [string, any]) => {
|
||||
app.registerComponent(type, component);
|
||||
});
|
||||
|
||||
Object.entries(dataSources.default).forEach(([type, ds]: [string, any]) => {
|
||||
DataSourceManager.register(type, ds);
|
||||
});
|
||||
|
||||
Object.values(plugins.default).forEach((plugin: any) => {
|
||||
vueApp.use(plugin, { app });
|
||||
});
|
||||
|
||||
window.appInstance = app;
|
||||
vueApp.config.globalProperties.app = app;
|
||||
vueApp.provide('app', app);
|
||||
|
||||
vueApp.mount('#app');
|
||||
});
|
||||
```
|
||||
|
||||
[`playground/App.vue`](https://github.com/Tencent/tmagic-editor/blob/master/runtime/vue/playground/App.vue) 出乎意料地短:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<component v-if="pageConfig" :is="pageComponent" :key="pageConfig.id" :config="pageConfig"></component>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useComponent, useEditorDsl } from '@tmagic/vue-runtime-help';
|
||||
|
||||
const { pageConfig } = useEditorDsl();
|
||||
const pageComponent = useComponent('page');
|
||||
</script>
|
||||
```
|
||||
|
||||
::: tip 关键点
|
||||
- `platform: 'editor'` 告知 `@tmagic/core` 进入编辑模式;
|
||||
- `useEditorDsl()` 内部已经调用 `window.magic?.onRuntimeReady({...})`,把 add/update/remove 等回调挂载到全局,编辑器通过 `iframe.contentWindow.magic` 触发;
|
||||
- 当 DSL 变化时,`pageConfig` 自动更新;当页面 DOM 渲染完成,`useEditorDsl` 会调用 `magic.onPageElUpdate(...)` 把页面元素同步给编辑器,让选中框能够吸附。
|
||||
:::
|
||||
|
||||
## page runtime 实现(线上发布)
|
||||
|
||||
[`runtime/vue/page/main.ts`](https://github.com/Tencent/tmagic-editor/blob/master/runtime/vue/page/main.ts) 与 playground 的差别在于:
|
||||
|
||||
1. 不需要响应编辑器消息,直接读取 `window.magicDSL`(或 `localPreview` 模式下从 `localStorage` 读取);
|
||||
2. 使用 `defineAsyncComponent` + 异步入口,按需加载组件,**减小首屏体积**;
|
||||
3. 数据源走 `registerDataSourceOnDemand`,只注册当前 DSL 用到的;
|
||||
4. 注入 `request` 与 `userRender` 等业务侧 API 给组件复用。
|
||||
|
||||
```ts
|
||||
import { createApp, defineAsyncComponent, resolveDirective, withDirectives } from 'vue';
|
||||
|
||||
import TMagicApp, { DataSourceManager, DeepObservedData, getUrlParam, registerDataSourceOnDemand } from '@tmagic/core';
|
||||
|
||||
import components from '../.tmagic/async-comp-entry';
|
||||
import asyncDataSources from '../.tmagic/async-datasource-entry';
|
||||
import plugins from '../.tmagic/plugin-entry';
|
||||
|
||||
import request, { service } from './utils/request';
|
||||
import AppComponent from './App.vue';
|
||||
import { getLocalConfig } from './utils';
|
||||
|
||||
import '@tmagic/core/resetcss.css';
|
||||
|
||||
DataSourceManager.registerObservedData(DeepObservedData);
|
||||
|
||||
const vueApp = createApp(AppComponent);
|
||||
vueApp.use(request);
|
||||
|
||||
const dsl = ((getUrlParam('localPreview') ? getLocalConfig() : window.magicDSL) || [])[0] || {};
|
||||
|
||||
const app = new TMagicApp({
|
||||
ua: window.navigator.userAgent,
|
||||
config: dsl,
|
||||
request: service,
|
||||
curPage: getUrlParam('page'),
|
||||
useMock: Boolean(getUrlParam('useMock')),
|
||||
});
|
||||
|
||||
app.setDesignWidth(app.env.isWeb ? window.document.documentElement.getBoundingClientRect().width : 375);
|
||||
|
||||
Object.entries(components).forEach(([type, component]: [string, any]) => {
|
||||
app.registerComponent(type, defineAsyncComponent(component));
|
||||
});
|
||||
|
||||
Object.values(plugins).forEach((plugin: any) => {
|
||||
vueApp.use(plugin, { app });
|
||||
});
|
||||
|
||||
registerDataSourceOnDemand(dsl, asyncDataSources).then((dataSources) => {
|
||||
Object.entries(dataSources).forEach(([type, ds]: [string, any]) => {
|
||||
DataSourceManager.register(type, ds);
|
||||
});
|
||||
|
||||
vueApp.config.globalProperties.app = app;
|
||||
vueApp.provide('app', app);
|
||||
vueApp.mount('#app');
|
||||
});
|
||||
```
|
||||
|
||||
[`page/App.vue`](https://github.com/Tencent/tmagic-editor/blob/master/runtime/vue/page/App.vue) 用 `useDsl()`(注意不是 `useEditorDsl`):
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<component :is="pageComponent" :config="pageConfig as MPage"></component>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { MPage } from '@tmagic/core';
|
||||
import { useComponent, useDsl } from '@tmagic/vue-runtime-help';
|
||||
|
||||
const { pageConfig, app } = useDsl();
|
||||
const pageComponent = useComponent('page');
|
||||
</script>
|
||||
```
|
||||
|
||||
## vite 多入口构建
|
||||
|
||||
`runtime/vue` 通过单个 vite 工程构建出两份产物([`vite.config.ts`](https://github.com/Tencent/tmagic-editor/blob/master/runtime/vue/vite.config.ts)):
|
||||
|
||||
```ts
|
||||
build: {
|
||||
rolldownOptions: {
|
||||
input: {
|
||||
page: path.resolve(__dirname, './page/index.html'),
|
||||
playground: path.resolve(__dirname, './playground/index.html'),
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
加上 `package.json` 中提供的 build 脚本:
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"tmagic": "tmagic entry",
|
||||
"dev": "vite --force",
|
||||
"build": "rimraf ./dist && node scripts/build.mjs --type=all",
|
||||
"build:libs": "node scripts/build.mjs --type=res",
|
||||
"build:page": "node scripts/build.mjs --type=page",
|
||||
"build:playground": "node scripts/build.mjs --type=playground"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
最常用的两个:
|
||||
|
||||
- `npm run build:libs`:构建 **编辑器侧**用到的 `config / value / event / ds-config / ds-value` 五份 UMD 资源(输出到 `dist/entry/`),编辑器通过 `asyncLoadJs` 异步加载(参考[快速开始 § propsConfigs / propsValues](./index.md#propsconfigs-propsvalues))。
|
||||
- `npm run build`:同时产出 `playground/index.html`、`page/index.html` 与 `entry/`,可以一份产物覆盖编辑器、预览、线上三种场景。
|
||||
|
||||
## @tmagic/vue-runtime-help 常用 Hook
|
||||
|
||||
| Hook | 作用 |
|
||||
| --- | --- |
|
||||
| `useEditorDsl()` | playground 入口使用,建立与编辑器通信、维护当前页面 `pageConfig` |
|
||||
| `useDsl()` | page 入口使用,从 `window.magicDSL` 中读取并维护 `pageConfig` |
|
||||
| `useComponent(type)` | 通过组件 type 解析出已注册的 Vue 组件(找不到时会回退到 `magic-ui-${type}`) |
|
||||
| `useApp()` | 取出注入的 `TMagicApp` 实例 |
|
||||
| `useComponentStatus()` | 获取组件在编辑器中的展示/禁用状态 |
|
||||
|
||||
::: tip
|
||||
React runtime 的实现思路完全一致,对应包是 [`@tmagic/react-runtime-help`](https://github.com/Tencent/tmagic-editor/tree/master/runtime/react-runtime-help),可以参照本节自行迁移。
|
||||
:::
|
||||
|
||||
## 跨域
|
||||
|
||||
playground 是被编辑器以 iframe 形式加载的,开发期需要保证 runtime 服务允许跨域。仓库里的做法是用 Vite 的 proxy 把 runtime 反代到 playground 同域:
|
||||
|
||||
```ts
|
||||
// playground/vite.config.ts
|
||||
server: {
|
||||
port: 8098,
|
||||
proxy: {
|
||||
'^/tmagic-editor/playground/runtime': {
|
||||
target: 'http://127.0.0.1:8078',
|
||||
changeOrigin: true,
|
||||
prependPath: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
如果编辑器和 runtime 跨域部署,需要在 runtime 服务侧返回 `Access-Control-Allow-Origin`,并保证 iframe 的 `postMessage` 同源策略允许双方通信。
|
||||
|
||||
## 进一步阅读
|
||||
|
||||
- [基础概念](./conception.md):编辑器、模拟器、runtime 的关系
|
||||
- [组件开发](./component.md):组件四件套(component / form-config / init-value / event)
|
||||
- [页面发布](./publish.md):page.html 注入 DSL 的发布流程
|
||||
- [教程](./tutorial/index.md):从零实现一份 runtime,理解 magic API 与 DSL 解析
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user