LiteGraph.js与Vuex持久化:localStorage集成终极指南
LiteGraph.js是一个强大的JavaScript图形节点引擎和编辑器,类似于Unreal Blueprints或UDK蓝图系统。它允许开发者在HTML5 Canvas2D中创建可视化节点图,并支持将图形导出为JSON格式,可以独立集成到应用程序中。本文将为您提供完整的LiteGraph.js与Vuex状态管理集成的解决方案,重点介绍如何通过localStorage实现持久化存储,确保您的可视化节点数据在页面刷新后不会丢失。
LiteGraph.js核心功能概述
LiteGraph.js作为一个可视化节点编辑器,提供了丰富的功能集,包括:
- Canvas2D渲染:支持缩放、平移和复杂界面渲染
- 节点编程:轻松创建自定义节点类型
- 编辑器功能:搜索框、键盘快捷键、多选、上下文菜单
- 高性能优化:支持数百个节点同时运行
- 主题定制:可自定义颜色、形状和背景
- 子图支持:节点可以包含嵌套的子图
- 实时模式系统:隐藏图形但调用节点渲染任意内容
为什么需要Vuex与localStorage集成?
当您使用LiteGraph.js构建复杂的可视化编辑器时,用户创建的节点图、连接关系和配置信息构成了应用程序的核心状态。Vuex作为Vue.js的官方状态管理库,非常适合管理这种复杂的状态数据。然而,Vuex状态在页面刷新时会重置,这就需要与localStorage集成来实现持久化存储。
基础集成方案
1. 安装与配置LiteGraph.js
首先,在Vue项目中安装LiteGraph.js:
npm install litegraph.js
然后在Vue组件中初始化LiteGraph.js:
import LiteGraph from 'litegraph.js';
import 'litegraph.js/css/litegraph.css';
export default {
mounted() {
this.graph = new LiteGraph.LGraph();
this.canvas = new LiteGraph.LGraphCanvas("#mycanvas", this.graph);
// 初始化示例节点
const nodeConst = LiteGraph.createNode("basic/const");
nodeConst.pos = [200, 200];
this.graph.add(nodeConst);
nodeConst.setValue(4.5);
this.graph.start();
}
}
2. 创建Vuex存储模块
创建一个专门的Vuex模块来管理LiteGraph.js的状态:
// store/modules/litegraph.js
const state = {
graphData: null,
selectedNodes: [],
canvasState: {
zoom: 1,
offset: [0, 0]
}
};
const mutations = {
SET_GRAPH_DATA(state, data) {
state.graphData = data;
},
SET_SELECTED_NODES(state, nodes) {
state.selectedNodes = nodes;
},
SET_CANVAS_STATE(state, canvasState) {
state.canvasState = canvasState;
}
};
const actions = {
saveGraph({ commit, state }) {
if (state.graph) {
const graphData = state.graph.serialize();
commit('SET_GRAPH_DATA', graphData);
return graphData;
}
return null;
},
loadGraph({ commit, state }, graphData) {
if (state.graph && graphData) {
state.graph.configure(graphData);
commit('SET_GRAPH_DATA', graphData);
}
}
};
export default {
namespaced: true,
state,
mutations,
actions
};
localStorage持久化实现
1. Vuex插件方案
创建一个Vuex插件来自动处理localStorage持久化:
// store/plugins/localStorage.js
const localStoragePlugin = store => {
// 从localStorage加载初始状态
const savedState = localStorage.getItem('litegraph-app-state');
if (savedState) {
try {
store.replaceState(JSON.parse(savedState));
} catch (e) {
console.error('Failed to load state from localStorage:', e);
}
}
// 监听mutation,保存到localStorage
store.subscribe((mutation, state) => {
// 只保存litegraph相关的状态变化
if (mutation.type.includes('litegraph/')) {
localStorage.setItem('litegraph-app-state', JSON.stringify(state));
}
});
};
export default localStoragePlugin;
2. 选择性持久化策略
为了优化性能,我们可以实现选择性持久化:
// utils/persistence.js
export const PERSISTENCE_STRATEGIES = {
FULL: 'full', // 保存所有状态
GRAPH_ONLY: 'graph', // 只保存图形数据
SELECTIVE: 'selective' // 选择性保存
};
export class LiteGraphPersistence {
constructor(strategy = PERSISTENCE_STRATEGIES.SELECTIVE) {
this.strategy = strategy;
this.STORAGE_KEY = 'litegraph_vuex_state';
}
saveState(state) {
let dataToSave;
switch(this.strategy) {
case PERSISTENCE_STRATEGIES.FULL:
dataToSave = state;
break;
case PERSISTENCE_STRATEGIES.GRAPH_ONLY:
dataToSave = {
litegraph: state.litegraph
};
break;
case PERSISTENCE_STRATEGIES.SELECTIVE:
default:
dataToSave = {
litegraph: {
graphData: state.litegraph?.graphData,
canvasState: state.litegraph?.canvasState
}
};
break;
}
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(dataToSave));
}
loadState() {
const saved = localStorage.getItem(this.STORAGE_KEY);
if (!saved) return null;
try {
return JSON.parse(saved);
} catch (e) {
console.error('Failed to parse saved state:', e);
return null;
}
}
clearState() {
localStorage.removeItem(this.STORAGE_KEY);
}
}
高级集成技巧
1. 自动保存与防抖处理
为了避免频繁写入localStorage影响性能,实现自动保存功能:
// utils/autoSave.js
import debounce from 'lodash/debounce';
export class AutoSaveManager {
constructor(vuexStore, persistence, options = {}) {
this.store = vuexStore;
this.persistence = persistence;
this.options = {
debounceDelay: 2000, // 2秒防抖
maxSaveFrequency: 10000, // 最多每10秒保存一次
...options
};
this.lastSaveTime = 0;
this.isSaving = false;
this.debouncedSave = debounce(this.save.bind(this), this.options.debounceDelay);
}
start() {
// 监听Vuex mutations
this.store.subscribe((mutation) => {
if (mutation.type.startsWith('litegraph/')) {
this.debouncedSave();
}
});
// 监听窗口关闭事件
window.addEventListener('beforeunload', () => {
this.saveImmediate();
});
}
save() {
const now = Date.now();
if (now - this.lastSaveTime < this.options.maxSaveFrequency && !this.isSaving) {
return;
}
this.isSaving = true;
this.persistence.saveState(this.store.state);
this.lastSaveTime = now;
this.isSaving = false;
}
saveImmediate() {
this.persistence.saveState(this.store.state);
}
}
2. 版本控制与迁移
处理数据结构变更的版本控制:
// utils/versionMigration.js
export class StateMigration {
constructor() {
this.VERSION_KEY = 'litegraph_state_version';
this.CURRENT_VERSION = 2;
}
migrate(state) {
const savedVersion = localStorage.getItem(this.VERSION_KEY) || 1;
const version = parseInt(savedVersion);
if (version < this.CURRENT_VERSION) {
state = this.applyMigrations(state, version);
localStorage.setItem(this.VERSION_KEY, this.CURRENT_VERSION.toString());
}
return state;
}
applyMigrations(state, fromVersion) {
let migratedState = { ...state };
// 版本1到版本2的迁移
if (fromVersion < 2) {
if (migratedState.litegraph && !migratedState.litegraph.version) {
migratedState.litegraph.version = 2;
// 添加新的字段或修改结构
if (migratedState.litegraph.graphData) {
migratedState.litegraph.graphData.metadata = {
migratedAt: new Date().toISOString(),
fromVersion: 1
};
}
}
}
return migratedState;
}
}
实战示例:完整的Vue组件
<template>
<div class="litegraph-editor">
<div class="toolbar">
<button @click="saveGraph">💾 保存</button>
<button @click="loadGraph">📂 加载</button>
<button @click="clearGraph">🗑️ 清空</button>
<span class="auto-save-status">
{{ autoSaveStatus }}
</span>
</div>
<div class="canvas-container">
<canvas id="litegraph-canvas" ref="canvas"></canvas>
</div>
<div class="node-palette">
<div class="palette-header">节点库</div>
<div
v-for="nodeType in nodeTypes"
:key="nodeType"
class="node-item"
draggable="true"
@dragstart="onNodeDragStart(nodeType, $event)"
>
{{ nodeType }}
</div>
</div>
</div>
</template>
<script>
import { mapState, mapActions } from 'vuex';
import LiteGraph from 'litegraph.js';
import 'litegraph.js/css/litegraph.css';
import { AutoSaveManager } from '@/utils/autoSave';
import { LiteGraphPersistence, PERSISTENCE_STRATEGIES } from '@/utils/persistence';
export default {
name: 'LiteGraphEditor',
data() {
return {
graph: null,
canvas: null,
autoSaveManager: null,
autoSaveStatus: '就绪'
};
},
computed: {
...mapState('litegraph', ['graphData']),
nodeTypes() {
return ['basic/const', 'basic/watch', 'math/sum', 'math/multiply', 'interface/slider'];
}
},
mounted() {
this.initLiteGraph();
this.initPersistence();
this.loadSavedGraph();
},
methods: {
...mapActions('litegraph', ['saveGraph', 'loadGraph']),
initLiteGraph() {
this.graph = new LiteGraph.LGraph();
this.canvas = new LiteGraph.LGraphCanvas(
this.$refs.canvas,
this.graph
);
// 配置画布事件
this.canvas.onNodeSelected = this.onNodeSelected;
this.canvas.onNodeDeselected = this.onNodeDeselected;
this.canvas.onGraphChanged = this.onGraphChanged;
this.graph.start();
},
initPersistence() {
const persistence = new LiteGraphPersistence(PERSISTENCE_STRATEGIES.SELECTIVE);
this.autoSaveManager = new AutoSaveManager(this.$store, persistence);
this.autoSaveManager.start();
},
async loadSavedGraph() {
if (this.graphData) {
this.graph.configure(this.graphData);
this.autoSaveStatus = '已加载保存的图形';
}
},
onNodeSelected(node) {
this.$store.commit('litegraph/SET_SELECTED_NODES', [node]);
},
onNodeDeselected(node) {
this.$store.commit('litegraph/SET_SELECTED_NODES', []);
},
onGraphChanged() {
this.autoSaveStatus = '自动保存中...';
this.$store.dispatch('litegraph/saveGraph');
setTimeout(() => {
this.autoSaveStatus = '已保存';
}, 1000);
},
onNodeDragStart(nodeType, event) {
event.dataTransfer.setData('text/plain', nodeType);
},
clearGraph() {
this.graph.clear();
this.$store.commit('litegraph/SET_GRAPH_DATA', null);
this.autoSaveStatus = '图形已清空';
}
},
beforeDestroy() {
if (this.autoSaveManager) {
this.autoSaveManager.saveImmediate();
}
}
};
</script>
<style scoped>
.litegraph-editor {
display: flex;
flex-direction: column;
height: 100vh;
}
.toolbar {
padding: 10px;
background: #2c3e50;
color: white;
display: flex;
gap: 10px;
align-items: center;
}
.canvas-container {
flex: 1;
position: relative;
overflow: hidden;
}
#litegraph-canvas {
width: 100%;
height: 100%;
background: #1a1a1a;
}
.node-palette {
width: 200px;
background: #34495e;
color: white;
padding: 10px;
}
.palette-header {
font-weight: bold;
margin-bottom: 10px;
padding-bottom: 5px;
border-bottom: 1px solid #7f8c8d;
}
.node-item {
padding: 8px;
margin: 5px 0;
background: #2c3e50;
border-radius: 4px;
cursor: move;
transition: background 0.2s;
}
.node-item:hover {
background: #3498db;
}
.auto-save-status {
margin-left: auto;
font-size: 0.9em;
color: #95a5a6;
}
</style>
性能优化与最佳实践
1. 数据压缩策略
对于大型图形,localStorage可能有限制(通常5-10MB),需要数据压缩:
// utils/compression.js
export class GraphCompression {
static compress(graphData) {
// 移除临时数据
const compressed = {
nodes: graphData.nodes.map(node => ({
id: node.id,
type: node.type,
pos: node.pos,
size: node.size,
properties: node.properties,
inputs: node.inputs,
outputs: node.outputs
})),
links: graphData.links,
groups: graphData.groups,
config: graphData.config
};
return JSON.stringify(compressed);
}
static decompress(compressedString) {
const data = JSON.parse(compressedString);
// 恢复完整结构
return {
last_node_id: data.nodes.length > 0 ?
Math.max(...data.nodes.map(n => n.id)) : 0,
last_link_id: data.links.length,
nodes: data.nodes,
links: data.links,
groups: data.groups,
config: data.config
};
}
}
2. 增量保存机制
只保存变化的部分,而不是整个状态:
// utils/incrementalSave.js
export class IncrementalSaveManager {
constructor() {
this.lastFullState = null;
this.changeLog = [];
this.MAX_CHANGES = 100; // 最多保存100个变更
}
recordChange(mutation, stateDiff) {
this.changeLog.push({
timestamp: Date.now(),
mutation,
diff: stateDiff
});
// 限制日志大小
if (this.changeLog.length > this.MAX_CHANGES) {
this.changeLog.shift();
}
}
getIncrementalSave(currentState) {
if (!this.lastFullState) {
this.lastFullState = currentState;
return { type: 'full', data: currentState };
}
const diff = this.calculateDiff(this.lastFullState, currentState);
if (this.shouldDoFullSave(diff)) {
this.lastFullState = currentState;
this.changeLog = [];
return { type: 'full', data: currentState };
}
return { type: 'incremental', data: diff };
}
calculateDiff(oldState, newState) {
// 简化的差异计算
const diff = {};
if (JSON.stringify(oldState.litegraph?.graphData) !==
JSON.stringify(newState.litegraph?.graphData)) {
diff.graphData = newState.litegraph?.graphData;
}
return diff;
}
shouldDoFullSave(diff) {
// 如果差异太大,还是完整保存
const diffSize = JSON.stringify(diff).length;
const fullSize = JSON.stringify(this.lastFullState).length;
return diffSize > fullSize * 0.5; // 差异超过50%时完整保存
}
}
故障排除与调试
常见问题解决
-
localStorage配额超出
try { localStorage.setItem(key, data); } catch (e) { if (e.name === 'QuotaExceededError') { // 清理旧数据或提示用户 this.clearOldData(); } } -
数据损坏恢复
class DataIntegrityChecker { static validateGraphData(data) { if (!data || typeof data !== 'object') return false; if (!Array.isArray(data.nodes)) return false; if (!Array.isArray(data.links)) return false; // 验证节点ID唯一性 const nodeIds = new Set(data.nodes.map(n => n.id)); if (nodeIds.size !== data.nodes.length) return false; return true; } static repairGraphData(data) { // 尝试修复常见问题 const repaired = { ...data }; if (!repaired.nodes) repaired.nodes = []; if (!repaired.links) repaired.links = []; if (!repaired.groups) repaired.groups = []; // 修复重复ID const idMap = new Map(); repaired.nodes.forEach((node, index) => { if (idMap.has(node.id)) { node.id = Math.max(...repaired.nodes.map(n => n.id)) + 1; } idMap.set(node.id, true); }); return repaired; } }
总结
LiteGraph.js与Vuex通过localStorage的集成提供了一个强大而灵活的可视化编辑器解决方案。通过合理的持久化策略、性能优化和错误处理,您可以创建出既功能强大又用户体验良好的应用程序。记住以下关键点:
- 选择合适的持久化策略:根据应用需求选择完整保存、仅图形保存或选择性保存
- 实现自动保存:使用防抖技术避免频繁写入,同时确保数据安全
- 处理数据迁移:为未来的数据结构变更做好准备
- 优化性能:使用压缩和增量保存处理大型图形
- 增强用户体验:提供清晰的保存状态反馈和错误恢复机制
通过本文介绍的技术,您可以构建出专业级的可视化节点编辑器,为用户提供无缝的图形创建和编辑体验。无论是简单的流程图工具还是复杂的可视化编程环境,LiteGraph.js与Vuex的集成都能为您提供坚实的基础架构。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考






