Fabric.js Vue 3 项目有一个经典 bug:把 Fabric.js 画布放进深层 ref。Vue 会把它包成响应式代理,Fabric.js 就会出现各种怪问题,比如 loadFromJSON 之后不渲染。把画布和引擎放在 shallowRef 或普通变量里,工具栏状态用 createDocumentStateStore。
组件
<script setup lang="ts">
import { Canvas, Rect } from "fabric";
import {
createDocumentEngine,
createDocumentStateStore,
type DocumentEngine,
type DocumentState,
} from "fabricjs-document-engine";
import { onBeforeUnmount, onMounted, shallowRef, useTemplateRef } from "vue";
const element = useTemplateRef<HTMLCanvasElement>("element");
const engine = shallowRef<DocumentEngine>();
const state = shallowRef<DocumentState>();
let cleanup = () => {};
onMounted(() => {
const canvas = new Canvas(element.value!, { width: 800, height: 500 });
const created = createDocumentEngine({ canvas });
const store = createDocumentStateStore(created);
state.value = store.getSnapshot();
const unsubscribe = store.subscribe(() => {
state.value = store.getSnapshot();
});
engine.value = created;
cleanup = () => {
unsubscribe();
store.destroy();
created.destroy();
canvas.dispose();
};
});
onBeforeUnmount(() => cleanup());
function addSquare() {
engine.value?.canvas.add(
new Rect({ width: 80, height: 60, fill: "#2a9d8f" })
);
}
</script>
<template>
<div role="toolbar">
<button @click="addSquare">Square</button>
<button :disabled="!state?.canUndo" @click="engine?.undo()">
Undo {{ state?.undoLabel }}
</button>
<button :disabled="!state?.canRedo" @click="engine?.redo()">Redo</button>
</div>
<canvas ref="element" aria-label="Drawing canvas" />
</template>为什么用 shallowRef
shallowRef 只在你替换 .value 时触发更新,不会包装里面的 Fabric.js 对象,所以 Fabric.js 拿到的是真实对象。state 快照每次变化都是新对象,所以 shallowRef 照样能更新模板。
画布已经是响应式的
如果 Fabric.js 画布已经放进了 reactive() 或深层 ref,用 toRaw 取出原始对象再传入:
import { toRaw } from "vue";
const engine = createDocumentEngine({ canvas: toRaw(canvasRef.value) });Pinia store 也一样:不要把 Fabric.js 对象放进 store 的 state,或者用 markRaw 标记它们。
Fabric.js Vue 3 的其余配置和原生 JavaScript 一样。要保存,请加上存储适配器。