mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2026-10-28 13:30:46 +00:00
- 新增 packages/editor/src/sdk,通过 sdk.ts 对外导出 - 将 services 下的核心服务迁移至 sdk/services - 工具函数拆分至 sdk/utils,校验逻辑迁移至 sdk/validate,新增 sdk/wiring - services/sdk-instance.ts 负责在编辑器中实例化 SDK - 补充 SDK 相关单元测试,新增 docs/api/editor/sdk.md 文档
155 lines
7.9 KiB
Markdown
155 lines
7.9 KiB
Markdown
# DSL SDK(`@tmagic/editor/sdk`)
|
||
|
||
不依赖 DOM / Vue 界面 / 画布的 DSL 操作 SDK,只负责对 DSL 做增删改查、历史撤销重做、依赖收集与合法性校验,可以直接在 Node 中运行(比如给 AI agent 调用的命令行工具使用)。
|
||
|
||
浏览器端的 `editorService`、`historyService` 等单例就是用同一个工厂创建的默认实例,因此 `import { editorService } from '@tmagic/editor'` 的用法不受影响。
|
||
|
||
```ts
|
||
import { createDslSdk } from '@tmagic/editor/sdk';
|
||
```
|
||
|
||
## 快速开始
|
||
|
||
```ts
|
||
import { createDslSdk } from '@tmagic/editor/sdk';
|
||
|
||
const sdk = createDslSdk({
|
||
// 初始 DSL,会被深拷贝,不会修改传入的对象
|
||
dsl: { type: 'app', id: 'app_1', items: [{ type: 'page', id: 'page_1', name: 'index', items: [] }] },
|
||
// 此前通过 sdk.serializeHistory() 导出的历史快照(可选)
|
||
// history,
|
||
// 组件列表:用来判断新增的组件类型是否合法
|
||
componentGroupList: [{ title: '基础', items: [{ text: '文本', type: 'text' }] }],
|
||
// 组件属性表单配置,key 是组件 type,值是该组件的表单配置(FormConfig,即表单项数组):用来校验属性是否合法
|
||
propsConfigs: {
|
||
text: [{ type: 'text', name: 'text', text: '文本', rules: [{ required: true, message: '文本不能为空' }] }],
|
||
},
|
||
// 组件被添加时的初始值,与 propsConfigs 的 key 一一对应
|
||
propsValues: { text: { text: '文本' } },
|
||
});
|
||
|
||
await sdk.ready; // 登记表(含异步的属性配置)全部就绪
|
||
|
||
// 没有选中节点时,默认添加到第一个页面;id 由 SDK 生成,后续操作使用返回值里的 id
|
||
const node = await sdk.editor.add({ type: 'text', text: 'hello' });
|
||
await sdk.editor.update({ id: node.id, text: 'world' });
|
||
await sdk.editor.undo();
|
||
|
||
// 属性不合法时(strict 模式)会抛出 DslValidationError,且不会修改 DSL
|
||
await sdk.editor.update({ id: node.id, text: '' }).catch((e) => console.log(e.errors)); // [{ ..., message: '文本 -> 文本不能为空' }]
|
||
|
||
const newDsl = sdk.getDsl(); // 深拷贝,已包含依赖收集结果
|
||
const newHistory = sdk.serializeHistory(); // 可 JSON.stringify,下次作为 history 传回
|
||
```
|
||
|
||
::: tip
|
||
`propsConfigs` 的格式与 [Editor 的 propsConfigs](./props.md#propsconfigs) 相同。实际项目中通常直接使用 runtime 构建出来的 `magicPresetConfigs` / `magicPresetValues`(参考[快速开始](../../guide/index.md#propsconfigs-propsvalues)),在 Node 中加载构建产物后传入即可。
|
||
:::
|
||
|
||
## 无状态使用
|
||
|
||
每次调用传入「DSL + 历史快照」,操作完成后导出新的「DSL + 历史快照」:
|
||
|
||
```ts
|
||
const sdk = createDslSdk({ dsl: readJson('dsl.json'), history: readJson('history.json') });
|
||
await sdk.editor.update({ id: 'button_1', text: 'new' });
|
||
writeJson('dsl.json', sdk.getDsl());
|
||
writeJson('history.json', sdk.serializeHistory());
|
||
```
|
||
|
||
::: tip
|
||
- `serializeHistory()` 导出的快照可以直接 `JSON.stringify`;浏览器端 `historyService.serialize()` 默认保留 `Map`,适合写入 IndexedDB。
|
||
- Node 下默认的 `parseDSL` 只解析 JSON,不会执行代码;如果历史里包含函数 / 正则,请自行传入 `parseDSL`。
|
||
:::
|
||
|
||
## createDslSdk(options)
|
||
|
||
每次调用返回一组相互连线、互相独立的 service,多个实例互不影响。
|
||
|
||
### options
|
||
|
||
| 参数 | 说明 | 默认值 |
|
||
| --- | --- | --- |
|
||
| `dsl` | 初始 DSL | - |
|
||
| `history` | 历史快照 | - |
|
||
| `validateMode` | 校验模式,见[校验](#校验) | `'strict'` |
|
||
| `autoWiring` | 是否自动绑定依赖收集连线 | `true` |
|
||
| `reactivity` | 响应式实现:`reactive` / `shallowReactive` / `toRaw` / `nextTick`,默认使用 Vue 的实现,可覆盖 | Vue |
|
||
| `storage` | 存储适配器(`localStorage` 天然满足),用于复制粘贴、代码块草稿 | 内存实现 `MemoryStorage` |
|
||
| `parseDSL` | 把序列化字符串还原成值 | 只解析 JSON |
|
||
| `historyPersistence` | 历史持久化适配器 `{ get, set }`(浏览器端对应 IndexedDB),不提供时 `saveToIndexedDB` / `restoreFromIndexedDB` 会 reject | - |
|
||
| `collectWorker` | 依赖收集 worker 适配器,不提供则在主线程收集 | - |
|
||
| `copyText` / `notify` | 复制到剪贴板、提示信息,不提供则不做任何事 | - |
|
||
| `componentGroupList` / `propsConfigs` / `propsValues` / `eventMethodList` / `datasourceList` / `datasourceConfigs` / `datasourceValues` / `datasourceEventMethodList` / `disabledCodeBlock` / `disabledDataSource` / `disabledPageFragment` / `disabledMultiSelect` / `alwaysMultiSelect` / `canDropIn` | 登记表,与 [Editor props](./props.md) 同名项一一对应 | - |
|
||
|
||
### 返回值
|
||
|
||
| 属性 | 说明 |
|
||
| --- | --- |
|
||
| `editor` / `history` / `props` / `dataSource` / `codeBlock` / `dep` / `storage` / `events` / `componentList` | 各 service 实例,方法与浏览器端同名 service 一致 |
|
||
| `validate` | 校验器,见[校验](#校验) |
|
||
| `ctx` | service 之间共享的上下文 |
|
||
| `ready` | 登记表全部就绪后 resolve |
|
||
| `getDsl()` | 导出当前 DSL(深拷贝) |
|
||
| `setDsl(dsl)` | 整体替换 DSL,会写入历史记录,返回的 Promise 在自动选中节点完成后 resolve |
|
||
| `serializeHistory()` | 导出可 JSON 序列化的历史快照 |
|
||
| `bindWiring()` | 绑定依赖收集连线(`autoWiring: false` 时手动调用),返回解绑函数。绑定时如果 DSL 已经设置,会立即补做一次依赖收集 |
|
||
| `destroy()` | 销毁 |
|
||
|
||
## 校验
|
||
|
||
`validateMode` 决定修改 DSL 时如何处理不合法的内容:
|
||
|
||
- `strict`:修改前校验,不通过抛出 `DslValidationError`(`errors` 里是全部问题),**DSL 不会被修改**,适合给 agent 调用,agent 可以根据 `errors` 修正后重试。Node 下默认。
|
||
- `record`:不抛错,把校验结果写进 `editor.state.invalidNodeIds`。
|
||
- `off`:不做额外校验。浏览器端默认实例使用这个模式(属性面板自己负责校验)。
|
||
|
||
可以通过 `sdk.ctx.validateMode = 'off'` 在运行时切换。
|
||
|
||
校验内容:
|
||
|
||
| 类别 | 内容 |
|
||
| --- | --- |
|
||
| 结构(`kind: 'structure'`) | 组件类型是否在组件列表中(`page` / `page-fragment` / `container` 内置合法,组件列表为空时不校验)、父节点是否是容器、`canDropIn`、是否禁用页面片、id 是否重复、不允许修改节点类型、数据源类型是否可用 |
|
||
| 属性(`kind: 'props'`) | 用 `@tmagic/form/headless` 的 `validateForm` 校验节点 / 数据源的属性表单配置,包含 `typeMatch` |
|
||
|
||
```ts
|
||
import { DslValidationError } from '@tmagic/editor/sdk';
|
||
|
||
try {
|
||
await sdk.editor.add({ type: 'unknown' });
|
||
} catch (e) {
|
||
if (e instanceof DslValidationError) {
|
||
console.log(e.errors); // [{ target, id, path, kind, message, suggestion }]
|
||
}
|
||
}
|
||
|
||
// 主动校验
|
||
await sdk.validate.node('text_1');
|
||
await sdk.validate.dataSource('ds_1');
|
||
await sdk.validate.dsl();
|
||
```
|
||
|
||
::: warning
|
||
- 数据源的 `add` / `update` 是同步方法,只做同步的结构校验(数据源类型是否可用);属性校验请用 `sdk.validate.dataSource(id)`。
|
||
- SDK 内只注册了 `@tmagic/form/headless` 的内置字段,编辑器自定义字段(`data-source-select`、`code-select` 等)的 `typeMatch` 依赖编辑器的单例 service,不在 SDK 校验范围内;需要时可在浏览器环境下通过 `@tmagic/editor/headless` 的 `editorFields` 注册。
|
||
:::
|
||
|
||
## 依赖收集
|
||
|
||
`autoWiring` 为 `true` 时,节点、数据源、代码块发生变化会同步收集依赖,并把结果写回 DSL 的 `dataSourceDeps` / `dataSourceCondDeps` / `dataSourceMethodDeps`,所以操作返回时导出的 DSL 依赖信息已经是最新的。
|
||
|
||
不需要依赖收集(只做 DSL 增删改)时可以传 `autoWiring: false`,之后需要时再调用 `sdk.bindWiring()`;注意关闭期间不会同步数据源 / 代码块与依赖目标,也不会自动选中页面。
|
||
|
||
## 插件
|
||
|
||
所有 service 都支持 `usePlugin`,与浏览器端一致:
|
||
|
||
```ts
|
||
sdk.editor.usePlugin({
|
||
beforeAdd(addNode, parent) {
|
||
return [addNode, parent];
|
||
},
|
||
});
|
||
```
|