Dva + Immer,輕鬆實現撤銷重作功能

前言

以前在社區裏發表過一篇文章——《Web 應用的撤銷重作實現》,裏面詳細介紹了幾種關於撤銷重作實現的思路。經過評論區得知了 Immer 這個庫,因而就去了解實踐了一下,再結合當前本身的平常開發需求,擼了一個實現撤銷重作的 Dva 插件。git

插件使用介紹

插件地址:github

github.com/frontdog/dv…redux

1. 實例化插件

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,設置撤銷重作棧的數量限制。

2. 撤銷 Undo

dispatch({ type: 'timeline/undo' })
複製代碼

3. 重作 Redo

dispatch({ type: 'timeline/redo' })
複製代碼

4. Reducer 默認內置了 Immer

在插件中,咱們已經內置了 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:_handleActionsonReducerextraReducerspost

extraReducers 初始化默認 state

extraReducerscreateStore 的時候額外的 reducer,後經過 combineReducers 聚合 reducer,這樣在初始化的時候就會自動聚合 statespa

export default (options = {}) => {
    const { namespace } = options;
    const initialState = {
        canUndo: false,
        canRedo: false,
        undoCount: 0,
        redoCount: 0,
    };
    return {
        // ...
        extraReducers: {
            [namespace](state = initialState) {
                return state;
            }
        },
    };
}
複製代碼

_handleActions 內置 Immer 並收集 patches

_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 實現撤銷重作

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;
                    }
                });
            };
        },

    };
}
複製代碼

在這個函數裏,咱們攔截了三個 actionnamespace/undonamespace/redonamespace/clear,而後根據以前收集的 patches,來對狀態進行操做以實現撤銷重作。

這裏,咱們還能夠在執行完正常的 reducer 後對總體的 state 作一些修改,這裏用來改 canRedocanUndo 等狀態。

相關文章
相關標籤/搜索