以前在社區裏發表過一篇文章——《Web 應用的撤銷重作實現》,裏面詳細介紹了幾種關於撤銷重作實現的思路。經過評論區得知了 Immer 這個庫,因而就去了解實踐了一下,再結合當前本身的平常開發需求,擼了一個實現撤銷重作的 Dva 插件。git
插件地址:github
import dva from 'dva';
import createUndoRedo from 'dva-immer-undo-redo';
const app = dva();
// ... 其餘插件先使用,確保該插件在最後
app.use(
createUndoRedo({
include: ['namespace'],
namespace: 'timeline' // 默認是 timeline
})
);
app.router(...);
app.start(...);
複製代碼
插件的 options
有三個可配置項:bash
options.namespace
:選填,默認值:timeline
,存儲撤銷重作狀態的命名空間,默認狀態:// state.timeline
{
canRedo: false,
canUndo: false,
undoCount: 0,
redoCount: 0,
}
複製代碼
options.include
:必填,但願實現撤銷重作的命名空間。options.limit
:選填,默認值:1024
,設置撤銷重作棧的數量限制。dispatch({ type: 'timeline/undo' })
複製代碼
dispatch({ type: 'timeline/redo' })
複製代碼
在插件中,咱們已經內置了 Immer,因此在 Reducer 中,你能夠直接對 state
進行操做,例如:app
// models/counter.js
{
namespace: 'counter',
state: {
count: 0,
},
reducers: {
add(state) {
state.count += 1;
}
}
}
複製代碼
這樣你就不須要本身去構造不可變數據並 return newState
,讓 reducer
代碼變得更加簡潔。函數
export default (options) => {
return {
_handleActions,
onReducer,
extraReducers,
};
}
複製代碼
這裏用到了 Dva 插件的三個 hook:_handleActions
、onReducer
、extraReducers
post
extraReducers
是 createStore
的時候額外的 reducer
,後經過 combineReducers
聚合 reducer
,這樣在初始化的時候就會自動聚合 state
。spa
export default (options = {}) => {
const { namespace } = options;
const initialState = {
canUndo: false,
canRedo: false,
undoCount: 0,
redoCount: 0,
};
return {
// ...
extraReducers: {
[namespace](state = initialState) {
return state;
}
},
};
}
複製代碼
_handleActions
讓你有能力爲全部的 reducer
進行擴展,這裏咱們利用這一點,與 immer
配合使用,這樣就能夠將本來 reducer
中的 state
變成 draft
。同時,能夠在第三個參數中,咱們就能夠收集一次更改的 patches
。插件
import immer, { applyPatches } from 'immer';
export default (options = {}) => {
const { namespace } = options;
const initialState = {...};
let stack = [];
let inverseStack = [];
return {
// ...
_handleActions(handlers, defaultState) {
return (state = defaultState, action) => {
const { type } = action;
const result = immer(state, (draft) => {
const handler = handlers[type];
if (typeof handler === 'function') {
const compatiableRet = handler(draft, action);
if (compatiableRet !== undefined) {
return compatiableRet;
}
}
}, (patches, inversePatches) => {
if (patches.length) {
const namespace = type.split('/')[0];
if (newOptions.include.includes(namespace) && !namespace.includes('@@')) {
inverseStack = [];
if (action.clear === true) {
stack = [];
} else if (action.replace === true) {
const stackItem = stack.pop();
if (stackItem) {
const { patches: itemPatches, inversePatches: itemInversePatches } = stackItem;
patches = [...itemPatches, ...patches];
inversePatches = [...inversePatches, ...itemInversePatches]
}
}
if (action.clear !== true) {
stack.push({ namespace, patches, inversePatches });
}
stack = stack.slice(-newOptions.limit);
}
}
});
return result === undefined ? {} : result;
};
},
};
}
複製代碼
onReducer
這個 hook
能夠實現高階 Reducer
函數,參照直接使用 Redux
,等價於:code
const originReducer = (state, action) => state;
const reduxReducer = onReducer(originReducer);
複製代碼
利用高階函數,咱們就能夠劫持一些 action
,進行特殊處理:
import immer, { applyPatches } from 'immer';
export default (options = {}) => {
const { namespace } = options;
const initialState = {...};
let stack = [];
let inverseStack = [];
return {
// ...
onReducer(reducer) {
return (state, action) => {
let newState = state;
if (action.type === `${namespace}/undo`) {
const stackItem = stack.pop();
if (stackItem) {
inverseStack.push(stackItem);
newState = immer(state, (draft) => {
const { namespace: nsp, inversePatches } = stackItem;
draft[nsp] = applyPatches(draft[nsp], inversePatches);
});
}
} else if (action.type === `${namespace}/redo`) {
const stackItem = inverseStack.pop();
if (stackItem) {
stack.push(stackItem);
newState = immer(state, (draft) => {
const { namespace: nsp, patches } = stackItem;
draft[nsp] = applyPatches(draft[nsp], patches);
});
}
} else if (action.type === `${namespace}/clear`) {
stack = [];
inverseStack = [];
} else {
newState = reducer(state, action);
}
return immer(newState, (draft: any) => {
const canUndo = stack.length > 0;
const canRedo = inverseStack.length > 0;
if (draft[namespace].canUndo !== canUndo) {
draft[namespace].canUndo = canUndo;
}
if (draft[namespace].canRedo !== canRedo) {
draft[namespace].canRedo = canRedo;
}
if (draft[namespace].undoCount !== stack.length) {
draft[namespace].undoCount = stack.length;
}
if (draft[namespace].redoCount !== inverseStack.length) {
draft[namespace].redoCount = inverseStack.length;
}
});
};
},
};
}
複製代碼
在這個函數裏,咱們攔截了三個 action
:namespace/undo
、namespace/redo
、namespace/clear
,而後根據以前收集的 patches
,來對狀態進行操做以實現撤銷重作。
這裏,咱們還能夠在執行完正常的 reducer
後對總體的 state
作一些修改,這裏用來改 canRedo
、canUndo
等狀態。