Immutable.js 기반 리 셋 취소 기능 의 인 스 턴 스 코드

브 라 우 저의 기능 이 점점 강해 지고 다른 클 라 이언 트 가 제공 하 던 많은 기능 이 점점 전단 으로 옮 겨 가 며 전단 응용 도 점점 복잡 해 집 니 다.많은 전단 응용,특히 일부 온라인 편집 소프트웨어 는 운영 할 때 사용자 의 상호작용 을 계속 처리 하고 재 구축 기능 을 취소 하여 상호작용 의 유창 성 을 확보 해 야 한다.그러나 하나의 응용 을 위해 리 셋 기능 을 취소 하 는 것 은 쉬 운 일이 아니다.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 와 달리 우 리 는 명령 모드 를 사용 합 니 다.기본 액 션 을 정의 하고 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 로 변 합 니 다.
세 번 째 단계:과거 기록 용기 만 들 기 AppHistory
앞의 State 클래스 는 특정한 시간 에 응 용 된 상 태 를 나타 내 는 데 사 용 됩 니 다.다음은 AppHistory 클래스 가 응 용 된 역사 기록 을 나타 내 는 데 사 용 됩 니 다.마찬가지 로 우 리 는 여전히 Immutable Record 를 사용 하여 역사 기록 을 정의 합 니 다.그 중에서 state 필드 는 현재 응용 상 태 를 표현 하고 list 필드 는 모든 action 을 저장 하 며 index 필드 는 최근 applied-action 의 아래 표 시 를 기록 합 니 다.응 용 된 역사 상 태 는 undo/redo 방법 으로 계산 할 수 있 습 니 다.apply 방법 은 AppHistory 에 구체 적 인 Action 을 추가 하고 실행 하 는 데 사 용 됩 니 다.구체 적 인 코드 는 다음 과 같다.

// AppHistory.ts
const emptyAction = Symbol('empty-action')
export const undo = Symbol('undo')
export type undo = typeof undo // TypeScript2.7   symbol       
export const redo = Symbol('redo')
export type redo = typeof redo
const AppHistoryRecord = Record({
 //       
 state: new State(),
 // action   
 list: List<Action>(),
 // index       applied-action list    。-1       applied-action
 index: -1,
})
export default class AppHistory extends AppHistoryRecord {
 pop() { //           
 return this
  .update('list', list => list.splice(this.index, 1))
  .update('index', x => x - 1)
 }
 getLastAction() { return this.index === -1 ? emptyAction : this.list.get(this.index) }
 getNextAction() { return this.list.get(this.index + 1, emptyAction) }
 apply(action: Action) {
 if (action === emptyAction) return this
 return this.merge({
  list: this.list.setSize(this.index + 1).push(action),
  index: this.index + 1,
  state: action.next(this.state),
 })
 }
 redo() {
 const action = this.getNextAction()
 if (action === emptyAction) return this
 return this.merge({
  list: this.list,
  index: this.index + 1,
  state: action.next(this.state),
 })
 }
 undo() {
 const action = this.getLastAction()
 if (action === emptyAction) return this
 return this.merge({
  list: this.list,
  index: this.index - 1,
  state: action.prev(this.state),
 })
 }
}
STEP 4:'다시 시작 취소'기능 추가
만약 에 응용 중의 다른 코드 가 웹 페이지 의 상호작용 을 일련의 Action 대상 으로 바 꾸 었 다 고 가정 하면 응용 에'다시 시작 취소'기능 을 추가 하 는 대체적인 코드 는 다음 과 같다.

type HybridAction = undo | redo | Action
//    Redux     ,       reudcer     「         」
//     reducer             
function reducer(history: AppHistory, action: HybridAction): AppHistory {
 if (action === undo) {
 return history.undo()
 } else if (action === redo) {
 return history.redo()
 } else { //     Action
 //         prepare  ,   action「   」
 return action.prepare(history).apply(action)
 }
}
//      Stream/Observable     ,          reducer
const action$: Stream<HybridAction> = generatedFromUserInteraction
const appHistory$: Stream<AppHistory> = action$.fold(reducer, new AppHistory())
const state$ = appHistory$.map(h => h.state)
//           ,       reducer
onActionHappen = function (action: HybridAction) {
 const nextHistory = reducer(getLastHistory(), action)
 updateAppHistory(nextHistory)
 updateState(nextHistory.state)
}
다섯 번 째 단계:Action 을 합병 하여 사용자 의 상호작용 체험 을 보완 한다.
위의 네 가지 절 차 를 통 해 그림 그리 기 도 구 는 취소 재 작업 기능 을 가지 지만 이 기능 은 사용자 체험 이 좋 지 않다.그림 그리 기 도구 에서 그림 을 끌 때 MoveItemAction 의 발생 빈 도 는 mousemove 사건 의 발생 빈도 와 같 습 니 다.만약 에 우리 가 이 상황 을 처리 하지 않 으 면 MoveItemAction 은 전체 역사 기록 을 오염 시 킬 것 입 니 다.우 리 는 기 록 된 모든 액 션 이 합 리 적 으로 입 도 를 취소 할 수 있 도록 주파수 가 높 은 액 션 을 통합 해 야 한다.
모든 Action 이 적용 되 기 전에 prepare 방법 이 호출 됩 니 다.prepare 방법 에서 역사 기록 을 수정 할 수 있 습 니 다.예 를 들 어 MoveItemAction 에 대해 저 희 는 이전 action 이 현재 action 과 같은 이동 작업 에 속 하 는 지 판단 한 다음 에 현재 action 을 응용 하기 전에 이전 action 을 제거 할 지 여 부 를 결정 합 니 다.코드 는 다음 과 같 습 니 다:

// actions/MoveItemAction.ts
export default class MoveItemAction extends Action {
 prevItem: Item
 //                       :
 //           (startPos),          (movingPos),         ID
 constructor(readonly startPos: Point, readonly movingPos: Point, readonly itemId: number) {
 //      readonly startPos: Point        :
 // 1.  MoveItemAction   startPos    
 // 2.          this.startPos = startPos
 super()
 }
 prepare(history: AppHistory) {
 const lastAction = history.getLastAction()
 if (lastAction instanceof MoveItemAction && lastAction.startPos == this.startPos) {
  //      action  MoveItemAction,             action  
  //         action         
  this.prevItem = lastAction.prevItem
  return history.pop() //   pop         action
 } else {
  //             ,    
  this.prevItem = history.state.items.get(this.itemId)
  return history
 }
 }
 next(state: State): State {
 const dx = this.movingPos.x - this.startPos.x
 const dy = this.movingPos.y - this.startPos.y
 const moved = this.prevItem.move(dx, dy)
 return state.setIn(['items', this.itemId], moved)
 }
 prev(state: State) {
 //                 prevItem  
 return state.setIn(['items', this.itemId], this.prevItem)
 }
 getMessage() { /* ... */ }
}
위의 코드 에서 볼 수 있 듯 이 prepare 방법 은 action 자체 가 준비 되 는 것 외 에 역사 기록 도 준비 할 수 있 습 니 다.서로 다른 Action 유형 은 서로 다른 합병 규칙 이 있 고 모든 Action 에 합 리 적 인 prepare 함 수 를 실현 한 후에 재 작업 기능 을 취소 하 는 사용자 체험 을 크게 향상 시 킬 수 있 습 니 다.
다른 주의해 야 할 점 들
리 셋 기능 을 취소 하 는 것 은 매우 가 변성 에 의존 하 는 것 입 니 다.하나의 Action 대상 이 Apphistory.list 를 넣 은 후에 인 용 된 대상 은 모두 가 변 적 이지 않 아야 합 니 다.action 에서 인용 한 대상 이 바 뀌 었 다 면 후속 취소 시 오류 가 발생 할 수 있 습 니 다.이 방안 에 서 는 작업 이 발생 했 을 때 필요 한 정 보 를 기록 하 는 데 편리 하도록 Action 대상 의 prepare 방법 에서 제자리 수정 작업 이 허용 되 지만 prepare 방법 은 action 이 역사 기록 에 들 어가 기 전에 한 번 만 호출 됩 니 다.action 이 기록 목록 에 들 어가 면 변 할 수 없습니다.
총결산
이상 은 실 용적 인 취소 기능 을 실현 하 는 모든 절차 이다.서로 다른 전단 프로젝트 는 서로 다른 수요 와 기술 방안 이 있 기 때문에 위의 코드 가 프로젝트 에서 한 줄 도 사용 하지 못 할 수도 있 습 니 다.그러나 재 작업 을 취소 하 는 사고방식 은 같 아야 한다.본 고가 너 에 게 약간의 깨 우 침 을 줄 수 있 기 를 바란다.
위 에서 말 한 것 은 임 뮤 타 블 리 제 이 스 를 기반 으로 재 작업 취소 기능 을 실현 하 는 인 스 턴 스 코드 입 니 다.여러분 에 게 도움 이 되 기 를 바 랍 니 다.궁금 한 점 이 있 으 시 면 메 시 지 를 남 겨 주세요.소 편 은 제때에 답 해 드 리 겠 습 니 다.여기 서도 저희 사이트 에 대한 여러분 의 지지 에 감 사 드 립 니 다!

좋은 웹페이지 즐겨찾기