Store
store 是 Ydesign 编辑器的响应式数据中心。所有 UI 状态(画布尺寸、缩放、选中、字体、背景等)和画布引用(editor)都收敛在一个 MobX-State-Tree(简称 MST)实例上。
你可以理解为:
React / Vue 组件 ──读──► store ◄──写── 用户交互
│
└──► store.editor.xxxHandler ──► Fabric 画布
组件只读 / 写 store,所有画布操作都通过 store.editor 下面的 Handler 下发;反过来,画布事件会自动同步回 store,视图自动更新。
💡 与 Polotno 的
Store不同,Ydesign 的数据模型里没有Page、没有Element类。对象即 Fabric.js 的原生FabricObject。多页能力后续通过SceneHandler扩展,详见该页。
创建 Store
import { createStore } from '@ydesign/react-editor';
const store = createStore({
key: 'YOUR_API_KEY', // 调用后端能力(字体、模板、AI)的凭据
token: 'user-token', // 可选:业务登录态
workspaceId: 'ws-001', // 可选:多租户隔离
});
返回的是 MST 实例。所有状态字段和动作方法都挂在它上面。
与 <DesignEditorApp /> 的关系
一次性启动完整编辑器时,createDesignEditorApp 会自动创建并返回 store:
import { createDesignEditorApp } from '@ydesign/react-editor';
const { store } = createDesignEditorApp({
container: document.getElementById('root'),
key: 'YOUR_API_KEY',
});
// store 就是同一个实例,后续所有读写都基于它
store.setSize({ width: 1200, height: 800 });
基本状态字段
| 字段 | 类型 | 说明 |
|---|---|---|
width | number | 画布宽度(单位:像素) |
height | number | 画布高度(单位:像素) |
scale | number | 当前缩放比,1 表示 100% |
scaleToFit | number | "适配屏幕"下的基础缩放值 |
unit | 'px' | 'mm' | 'cm' | 'in' | 画布单位 |
dpi | number | 打印分辨率,默认 72 |
bleed | number | 出血位(印刷场景) |
bleedVisible | boolean | 是否显示出血线 |
rulesVisible | boolean | 是否显示标尺 |
backgroundColor | string | object | 背景色,支持纯色或渐变对象 |
selectedElementsIds | string[] | 当前选中的元素 ID 列表 |
fonts | Font[] | 当前设计用到的字体 |
openedSidePanel | string | 当前打开的侧边面板名(templates / text / ...) |
editor | Editor | null | @ydesign/core 的 Editor 实例,持有所有 Handler |
objects | Array<any> | 画布上所有元素的快照(只读镜像) |
editor字段是"面向画布的遥控器":所有画布操作(添加元素、缩放、裁剪、历史等)都通过store.editor.xxxHandler.xxx()下发。详见 元素操作 与 场景操作。
常用派生视图
MST 的 views 让你可以拿到"从源状态计算出"的派生值:
// 当前选中的元素(Fabric 实例)
store.selectedElements;
// => FabricObject[]
// 递归展开选中(组里的子元素一并返回)
store.selectedShapes;
// => FabricObject[](不含 group 自身)
// 按 ID 拿到元素
store.getElementById('abc123');
// 按回调条件查找第一个元素(递归搜索 group)
store.find(e => e.type === 'textbox');
常用 Actions(UI 层)
这些 actions 是用户交互时常用的入口:
画布尺寸 / 单位 / 背景
// 画布尺寸(会同步到画布)
store.setSize({ width: 1200, height: 800 });
// 单位 / DPI
store.setUnit({ unit: 'mm', dpi: 300 });
// 背景色(支持纯色 / 渐变)
store.setBackgroundColor('#f0f0f0');
store.setBackgroundColor({
type: 'linear',
coords: { x1: 0, y1: 0, x2: 1, y2: 1 },
colorStops: [
{ offset: 0, color: '#ff6a00' },
{ offset: 1, color: '#2253eb' },
],
});
// 缩放(仅改 UI 字段;缩放画布请用 store.editor.zoomHandler)
store.setScale(1);
选中 / 批量操作
// 选中一批元素
store.selectElements(['id-1', 'id-2']);
// 添加新元素(来自 JSON)
store.addElement({
type: 'textbox',
text: 'Hello Ydesign',
left: 100,
top: 100,
fontSize: 48,
fill: '#111',
});
// 批量修改属性(作用在当前选中,或指定 element)
store.set({ opacity: 0.8 });
store.set({ fill: '#ff6a00' }, someElement);
// 批量删除
store.deleteElements(['id-1', 'id-2']);
// 克隆当前选中
store.clone();
图层
store.moveElementsUp(['id-1']); // 向上一层
store.moveElementsDown(['id-1']); // 向下一层
store.moveElementsTop(['id-1']); // 置顶
store.moveElementsBottom(['id-1']); // 置底
// 按钮态判断
store.canMoveElementsUp(['id-1']); // => boolean
store.canMoveElementsTop(['id-1']);
详见 元素操作 · 图层。
字体
store.addFont({
fontFamily: 'Noto Sans SC',
url: 'https://cdn.example.com/noto-sans-sc.woff2',
});
store.removeFont('Noto Sans SC');
// 按需预加载某个字体(返回 Promise,等待字体可用)
await store.loadFont('Noto Sans SC');
字体有 用户字体(存在
store.fonts,随设计一起导出)与 全局字体(进程级注册,不进入 JSON)两种。详见 编辑器配置 · 字体管理。
撤销 / 重做
历史栈由 HistoryHandler 管理,暴露在 editor.historyHandler:
store.editor?.historyHandler.undo();
store.editor?.historyHandler.redo();
// 查询是否可以继续撤销 / 重做
const { hasUndo, hasRedo } = store.editor!.historyHandler.getStatus();
导入 / 导出
// 导出 JSON(完整设计稿)
const json = store.toJSON();
// 从 JSON 加载设计稿
await store.loadJSON(json);
// 导出图片(base64 / Blob / 下载到本地)
const dataUrl = await store.toDataURL({ multiplier: 2, format: 'png' });
const blob = await store.toBlob({ multiplier: 2, format: 'jpeg', quality: 0.9 });
await store.saveAsImage({ multiplier: 2, format: 'png', fileName: 'my-design.png' });
完整导出选项见 场景操作 · 导入 / 导出。
响应式:订阅状态变化
方式 A:React observer
@ydesign/react-editor 已经把 observer 用在所有内置组件上。你自己的组件也可以:
import { observer } from 'mobx-react-lite';
const MyStatusBar = observer(({ store }) => {
return (
<div>
画布:{store.width} × {store.height} px|选中 {store.selectedElementsIds.length} 个元素
</div>
);
});
方式 B:MobX reaction / autorun
适合和 React 无关的业务逻辑(自动保存、脏标记、统计等):
import { reaction } from 'mobx';
reaction(
() => store.toJSON(),
json => {
localStorage.setItem('design-draft', JSON.stringify(json));
},
{ delay: 1000 } // 1s 节流
);
方式 C:store.on('change', cb)
当 store.objects(画布对象快照)发生变化时触发,专门用于"设计稿改动检测":
const dispose = store.on('change', objects => {
console.log('画布对象变动:', objects.length, '个元素');
});
// 取消订阅
dispose();
序列化
直接用 MST 的 getSnapshot:
import { getSnapshot } from 'mobx-state-tree';
const snapshot = getSnapshot(store);
// => 纯 JSON:{ width, height, scale, backgroundColor, fonts, ... }
如果你只关心画布内容(元素 + 工作区),用 store.toJSON() 更直接,它只包含 Fabric 可再序列化的对象数据。
完整字段 / 方法参考
FAQ
Q:为什么没有 Page / Element 类?
A:Ydesign 底层直接用 Fabric.js 的 FabricObject,"元素"就是 Fabric 对象。好处是无额外抽象层、生态兼容性好、上手成本低。Polotno 的 Element 类更像一个适配层,Ydesign 认为多此一举。
Q:我的设计有多页,怎么办?
A:当前版本只支持单页。多页能力将通过 SceneHandler 扩展,每一页对应一份独立的画布 JSON,切换时通过 importFromJSON 热加载。详见场景文档。
Q:能直接 mutate store.width = 1000 吗?
A:不能。MST 的顶层字段只接受 action 写入,请用 store.setSize({ width: 1000, height: ... })。
Q:store.editor 为什么可能是 null?
A:Editor 是在挂载 <Workspace /> 时异步创建的。使用前记得判空(或者用 ?. 调用):store.editor?.zoomHandler.zoomToFit()。