跳到主要内容

远程存储

每次变更都发送编辑器的全部状态并不高效。
为此,编辑器提供了实时数据复制 API。

安装

npm install @dineug/erd-editor

用于数据复制的 store 位于 @dineug/erd-editor/engine.js 子路径,而不是包的根路径。
该入口不接触 DOM,因此可以在 Web Worker 或主线程之外的任何地方运行。
若改为导入包的根路径,则会注册 <erd-editor> 自定义元素,而它需要 DOM。

使用

type ReplicationStore = {
readonly value: string;
on: (listeners: Partial<{ change: () => void }>) => Unsubscribe;
setInitialValue: (value: string) => void;
dispatch: (actions: Array<AnyAction> | AnyAction) => void;
dispatchSync: (actions: Array<AnyAction> | AnyAction) => void;
destroy: () => void;
};

type InjectEngineContext = {
toWidth: (text: string) => number;
};

type CreateReplicationStore = (
context: InjectEngineContext
) => ReplicationStore;

// example
import { createReplicationStore } from '@dineug/erd-editor/engine.js';

const replicationStore = createReplicationStore({
toWidth: text => text.length * 10,
});

复制实时编辑器

这些操作来自实时编辑器的 shared store。
参见协同编辑

import { createReplicationStore } from '@dineug/erd-editor/engine.js';

const replicationStore = createReplicationStore({ toWidth });
replicationStore.setInitialValue(savedJson);

replicationStore.on({
change: () => save(replicationStore.value),
});

const sharedStore = editor.getSharedStore();
sharedStore.subscribe(actions => {
replicationStore.dispatch(actions);
});

跨越边界的只有变更本身,而不是整个文档。
副本在自己这一侧应用变更并序列化结果,change 则会告知何时值得写入。

InjectEngineContext

toWidth

以像素为单位测量文本的 width。
store 没有 DOM,无法自行测量文本,因此每当名称、数据类型、默认值或注释发生变更时,都会通过该函数重新计算列宽度。

测量方式需与编辑器保持一致:使用编辑器字体栈中的 400 12px,四舍五入后再加上 2px 的内边距。
否则复制出来的列宽度会与屏幕上的宽度产生偏差。

const TEXT_PADDING = 2;
const FONT =
"400 12px -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, " +
"'Helvetica Neue', 'Open Sans', system-ui, sans-serif, " +
"'Apple Color Emoji', 'Segoe UI Emoji'";

let context = null;

function getContext() {
if (context) return context;

try {
context = new OffscreenCanvas(0, 0).getContext('2d');
if (context) context.font = FONT;
} catch {
// no canvas available
}

return context;
}

function toWidth(text) {
const context = getContext();
const width = context ? context.measureText(text).width : text.length * 10;

return Math.round(width) + TEXT_PADDING;
}

const replicationStore = createReplicationStore({ toWidth });

text.length * 10 是没有 canvas 可用时的回退方案。
对于文档只被存储、不被渲染的 store 来说,这已经足够。

ReplicationStore

value getter

序列化当前的编辑器状态。
返回的是可以直接存储的 JSON 字符串,而不是解析后的对象。

const data = replicationStore.value;

序列化时会应用文档自身的 ignoreSaveSettings
设置了滚动标志位时,滚动位置会写为 0,设置了缩放标志位时,缩放级别会写为 1
关于这些标志位,参见 Schema

setInitialValue

加载此前保存的编辑器状态。

replicationStore.setInitialValue('json...');

传入空字符串或任何非字符串的值时不会抛出错误,而是加载一个空文档。

加载时还会对文档执行一次垃圾回收。
不再出现在 doc 中且四天以上未被改动的实体会被清除,因此协作者正在进行的变更不会被回收。
该回收过程以异步方式完成。

加载与回收都不会触发 change
如果希望将回收后的文档写回存储,需要在加载后自行读取 replicationStore.value

on@change

订阅编辑器的状态变更事件。

const unsubscribe = replicationStore.on({
change: () => {
const data = replicationStore.value;
// save...
},
});

change 有 200ms 的防抖,因此短时间内的大量操作只会汇总为一次通知。
它只在会改变文档的操作上触发,因此中继过来的状态信息操作不会将其唤醒。
调用返回的函数即可取消订阅。

dispatch, dispatchSync

将远程编辑器的变更应用到 replicationStore。

replicationStore.dispatch(actions); // async
replicationStore.dispatchSync(actions); // sync

dispatch 会延迟到微任务中执行,dispatchSync 则会立即应用这批变更。

可以不做过滤,直接把 shared store 的操作流交给它们。
两个方法都只保留会改变文档的操作并丢弃其余部分,因此协同会话所携带的状态信息(每位用户的鼠标光标、焦点、选中内容与选择框)传入后不会产生任何效果。

destroy

彻底销毁 replicationStore 实例。

replicationStore.destroy();

销毁后 store 不再触发 change,也会忽略后续的 dispatch。
文档会停留在最后的值上,而不会抛出错误。
若要重新开始,需要创建新的 store。