欢迎来到DIVCSS5查找CSS资料与学习DIV CSS布局技术!
浏览器的功能越来越强大,许多原来由其他客户端提供的功能渐渐转移到了前端,前端应用也越来越复杂。许多前端应用,尤其是一些在线编辑软件,运行时需要不断处理用户的交互,提供了撤消重做功能来保证交互的流畅性。不过为一个应用实现撤销重做功能并不是一件容易的事情。Redux官方文档中 介绍了如何在 redux 应用中实现撤销重做功能。基于 redux 的撤销功能是一个自顶向下的方案:引入 redux-undo 之后所有的操作都变为了「可撤销的」,然后我们不断修改其配置使得撤销功能变得越来越好用(这也是 redux-undo 有那么多配置项 的原因)。
 
本文将采用自底向上的思路,以一个简易的在线画图工具为例子,使用 TypeScript、Immutable.js 实现一个实用的「撤消重做」功能。大致效果如下图所示:
 
撤销重做功能预览
 
上图看不清的话,可以看这里。
 
第一步:确定哪些状态需要历史记录,创建自定义的 State 类
 
并非所有的状态都需要历史记录。许多状态是非常琐碎的,尤其是一些与鼠标或者键盘交互相关的状态,例如在画图工具中拖拽一个图形时我们需要设置一个「正在进行拖拽」的标记,页面会根据该标记显示对应的拖拽提示,显然该拖拽标记不应该出现在历史记录中;而另一些状态无法被撤销或是不需要被撤销,例如网页窗口大小,向后台发送过的请求列表等。
 
排除那些不需要历史记录的状态,我们将剩下的状态用 Immutable Record 封装起来,并定义 State 类:
 
// State.ts
 
import { Record, List, Set } from 'immutable'
 
const StateRecord = Record({
 
  items: List<Item>
 
  transform: d3.ZoomTransform
 
  selection: number
 
})
 
// 用类封装,便于书写 TypeScript,注意这里最好使用Immutable 4.0 以上的版本
 
export default class State extends StateRecord {}
 
这里我们的例子是一个简易的在线画图工具,所以上面的 State 类中包含了三个字段,items 用来记录已经绘制的图形,transform 用来记录画板的平移和缩放状态,selection 则表示目前选中的图形的 ID。而画图工具中的其他状态,例如图形绘制预览,自动对齐配置,操作提示文本等,则没有放在 State 类中。
 
第二步:定义 Action 基类,并为每种不同的操作创建对应的 Action 子类
 
与 redux-undo 不同的是,我们仍然采用命令模式:定义基类 Action,所有对 State 的操作都被封装为一个 Action 的实例;定义若干 Action 的子类,对应于不同类型的操作。
 
在 TypeScript 中,Action 基类用 Abstract Class 来定义比较方便。
 
// actions/index.ts
 
export default abstract class Action {
 
  abstract next(state: State): State
 
  abstract prev(state: State): State
 
  prepare(appHistory: AppHistory): AppHistory { return appHistory }
 
  getMessage() { return this.constructor.name }
 
}
 
Action 对象的 next 方法用来计算「下一个状态」,prev 方法用来计算「上一个状态」。getMessage 方法用来获取 Action 对象的简短描述。通过 getMessage 方法,我们可以将用户的操作记录显示在页面上,让用户更方便地了解最近发生了什么。prepare 方法用来在 Action 第一次被应用之前,使其「准备好」,AppHistory 的定义在本文后面会给出。
 
Action 子类举例
 
下面的 AddItemAction 是一个典型的 Action 子类,用于表达「添加一个新的图形」。
 
// actions/AddItemAction.ts
 
export default class AddItemAction extends Action {
 
  newItem: Item
 
  prevSelection: number
 
  constructor(newItem: Item) {
 
    super()
 
    this.newItem = newItem
 
  }
 
  prepare(history: AppHistory) {
 
    // 创建新的图形后会自动选中该图形,为了使得撤销该操作时 state.selection 变为原来的值
 
    // prepare 方法中读取了「添加图形之前 selection 的值」并保存到 this.prevSelection
 
    this.prevSelection = history.state.selection
 
    return history
 
  }
 
  next(state: State) {
 
    return state
 
      .setIn(['items', this.newItem.id], this.newItem)
 
      .set('selection', this.newItemId)
 
  }
 
  prev(state: State) {
 
    return state
 
      .deleteIn(['items', this.newItem.id])
 
      .set('selection', this.prevSelection)
 
  }
 
  getMessage() { return ——Add item ${this.newItem.id}—— }
 
}
 
运行时行为
 
应用运行时,用户交互产生一个 Action 流,每次产生 Action 对象时,我们调用该对象的 next 方法来计算后一个状态,然后将该 action 保存到一个列表中以备后用;用户进行撤销操作时,我们从 action 列表中取出最近一个 Action 并调用其 prev 方法。应用运行时,next/prev 方法被调用的情况大致如下:
 
// initState 是一开始就给定的应用初始状态
 
// 某一时刻,用户交互产生了 action1 ...
 
state1 = action1.next(initState)
 
// 又一个时刻,用户交互产生了 action2 ...
 
state2 = action2.next(state1)
 
// 同样的,action3也出现了 ...
 
state3 = action3.next(state2)
 
// 用户进行撤销,此时我们需要调用最近一个action的prev方法
 
state4 = action3.prev(state3)
 
// 如果再次进行撤销,我们从action列表中取出对应的action,调用其prev方法
 
state5 = action2.prev(state4)
 
// 重做的时候,取出最近一个被撤销的action,调用其next方法
 
state6 = action2.next(state5)
 
Applied-Action
 
为了方便后面的说明,我们对 Applied-Action 进行一个简单的定义:Applied-Action 是指那些操作结果已经反映在当前应用状态中的 action;当 action 的 next 方法执行时,该 action 变为 applied;当 prev 方法被执行时,该 action 变为 unapplied。

如需转载,请注明文章出处和来源网址:http://www.divcss5.com/html/h63619.shtml