上一篇《??低代码平台的属性面板该如何设计???》讲了低代码平台的属性面板的设计。今天我们就来说说canvas区域的undo和redo设计。撤销和重做其实是我们一直在使用的操作。对应的快捷键一般是?Z/Ctrl+Z,??Z/Ctrl+Shift+Z。这个功能很常见,可以大大提高用户体验,提高编辑效率,但是如何用代码实现呢?具体来说,在我们的低代码平台上,对于canvas区域的元素,应该如何设计一系列的操作呢?我们先来分析一系列的状态变化。默认情况下,用户对画布的一系列操作都会改变整个画布的渲染状态:当执行某个操作时,用户可以返回到之前的状态,即撤销:当然,撤销操作之后,user可以恢复这个操作,对应redo:看之前canvas的数据结构:consteditorModule={state:{components:[],},mutations:{addComponent(state,component){component.id=uuidv4();state.components.push(组件);},updateComponent(state,{id,key,value,isProps}){constupdatedComponent=state.components.find((component)=>component.id===(id||state.currentElement));if(updatedComponent){if(isProps){updatedComponent.props[key]=value;}else{updatedComponent[key]=value;}}},deleteComponent(state,id){state.components=state.components.filter((component)=>component.id!==id);},},}对应操作:添加组件:addComponent更新组件:updateComponent删除组件:deleteComponent结合以上三张图,不难想到我们需要维护一个单独的数据来存储变更记录。在执行撤销和重做操作时,我们会从这条变更记录中取出已有的数据,然后更新原有的组件。在原始状态下添加://Changerecordhistories:[],//游标,用于标记变化的位置historyIndex:-1,在画布区域操作(增、删、更新)时,更新原组件data同时,保持变化的记录。我们需要封装一个更新变更记录的方法updateHistory。一般情况下,只需要在历史中添加记录:constupdateHistory=(state,historyRecord)=>{state.:添加组件添加组件的时候,添加一个changeType为addtohistories的组件数据,但是这里的组件需要深度复制:addComponent(state,component){component.id=uuidv4();state.components.push(组件);updateHistory(state,{id:uuidv4(),componentId:component.id,changeType:"add",data:cloneDeep(component),});}更新组件时,添加一个changeType修改为history更新组件组件数据,添加新/旧值和key:updateComponent(state,{id,key,value,isProps}){constupdatedComponent=state.components.find((component)=>component.id===(id||state.currentElement));if(updatedComponent){if(isProps){constoldValue=updatedComponent.props[key]updatedComponent.props[key]=value;updateHistory(state,{id:uuidv4(),componentId:id||state.currentElement,changeType:"修改",数据:{oldValue,newValue:value,key},});}else{updatedComponent[key]=value;}}},删除组件时,在histories中添加一条changeType为delete的数据,同时还要记录index,因为后面进行undo操作时,会重新插入到原来的位置根据索引:deleteComponent(state,id){constcomponentData=state.components.find((component)=>component.id===id)asComponentData;constcomponentIndex=state.components.findIndex((component)=>component.id===id);state.components=state.components.filter((component)=>component.id!==id);updateHistory(state,{id:uuidv4(),componentId:componentData.id,changeType:"delete",data:componentData,index:componentIndex,});},可以看到在添加history的过程中,有一个附加changeType字段以区分更改的类型:typechangeType='add'|'修改'|'delete'这个也是为后续的undo/redo做铺垫,有历史记录,针对不同的changeTypes执行相应的数据处理首先看undo,即undo。第一步是找到当前游标,也就是撤销操作所在的位置。如果之前从未有过undo操作,即当historyIndex为-1时,则将historyIndex设置为历史中的最后一项。否则,使用historyIndex--:if(state.historyIndex===-1){state.historyIndex=state.histories.length-1;}else{state.historyIndex--;}找到撤销的位置,下一个step是将上一步history中记录的不同changeType用于相应的数据处理:consthistory=state.history[state.historyIndex];switch(history.changeType){case"add":state.components=state.components.filter((component)=>component.id!==history.componentId);休息;案例“删除”:state.components=insert(state.components,history.index,history.data);休息;case"modify":{const{componentId,data}=history;const{key,oldValue}=dataconstupdatedComponent=state.component.find(component=>component.id===componentId)if(updatedComponent){updatedComponent.props[key]=oldValue}中断;}default:break;}如果之前添加过组件,那么对应的移除过程就是删除过程。如果删除过程之前已经完成,那么相应的删除就是将之前删除的组件恢复到原来的位置。如果之前更改了组件的属性,则撤消时将组件的相应属性恢复为原始值。然后对于redo,就是undo的逆向操作,可以理解为普通操作:consthistory=state.history[state.historyIndex];switch(history.changeType){case"add":state.components.push(历史数据);休息;案例“删除”:state.components=state.components.filter((component)=>component.id!==history.componentId);休息;case"modify":{const{componentId,data}=history;const{key,newValue}=dataconstupdatedComponent=state.component.find(component=>component.id===componentId)if(updatedComponent){updatedComponent.props[key]=newValue}break;}default:break;}其实这里已经实现了基本的撤销和重做。但这不符合使用习惯。我们用编辑器的时候,你不可能无限撤销。我们通过设置maxHistoryNumber来控制这个,并调整之前的updateHistory:}else{state.histories.shift();state.histories.push(历史记录);}}当历史记录条目小于设定的最大历史条目前,正常向历史添加记录。如果大于或等于maxHistoryNumber,则删除历史记录中的第一个,并将最新的添加到历史记录的末尾。还有一种场景:在undo/redo的过程中,一般都是在canvas区域进行操作。在这种情况下,通常的做法是直接删除所有大于historyIndex的历史记录,并将historyIndex设置为-1,即初始状态。因为现在进入了一个新的状态分支:if(state.historyIndex!==-1){state.histories=state.histories.slice(0,state.historyIndex);state.historyIndex=-1;}至此,低代码平台undo/redo设计思路的分享就结束了。
