Dojo Store 概念詳解

翻譯自:https://github.com/dojo/framework/blob/master/docs/en/stores/supplemental.md

State 對象

在現代瀏覽器中,state 對象是作爲 CommandRequest 的一部分傳入的。對 state 對象的任何修改都將轉換爲相應的 operation,然後應用到 store 上。

import { createCommandFactory } from '@dojo/framework/stores/process';
import { State } from './interfaces';
import { remove, replace } from '@dojo/framework/stores/state/operations';

const createCommand = createCommandFactory<State>();

const addUser = createCommand<User>(({ payload, state }) => {
    const currentUsers = state.users.list || [];
    state.users.list = [...currentUsers, payload];
});

注意,IE 11 不支持訪問 state,如果嘗試訪問將立即拋出錯誤。

StoreProvider

StoreProvider 接收三個屬性

  • renderer: 一個渲染函數,已將 store 注入其中,能訪問狀態並向子部件傳入 process。
  • stateKey: 註冊狀態時使用的 key 值。
  • paths (可選): 將此 provider 連接到狀態的某一局部上。

失效

StoreProvider 有兩種方法觸發失效並促使重新渲染。

  1. 推薦的方式是,通過向 provider 傳入 paths 屬性來註冊 path,以確保只有相關狀態變化時纔會失效。
  2. 另一種是較籠統的方式,當沒有爲 provider 定義 path 時,store 中的 任何 數據變化都會引起失效。

Process

生命週期

Process 有一個執行生命週期,它定義了所定義行爲的流程。

  1. 如果存在轉換器,則首先執行轉換器來轉換 payload 對象
  2. 按順序同步執行 before 中間件
  3. 按順序執行定義的 command
  4. 在執行完每個 command (如果是多個 command 則是一塊 command)之後,應用命令返回的 operation
  5. 如果在執行命令期間拋出了異常,則不會再執行後續命令,並且也不會應用當前的 operation
  6. 按順序同步執行 after 中間件

Process 中間件

使用可選的 beforeafter 方法在 process 的前後應用中間件。這允許在 process 所定義行爲的前和後加入通用的、可共享的操作。

也可以在列表中定義多箇中間件。會根據中間件在列表中的順序同步調用。

Before

before 中間件塊能獲取傳入的 payloadstore 的引用。

middleware/beforeLogger.ts

const beforeOnly: ProcessCallback = () => ({
    before(payload, store) {
        console.log('before only called');
    }
});

After

after 中間件塊能獲取傳入的 error (如果發生了錯誤的話)和 process 的 result

middleware/afterLogger.ts

const afterOnly: ProcessCallback = () => ({
    after(error, result) {
        console.log('after only called');
    }
});

result 實現了 Proce***esult 接口,以提供有關應用到 store 上的變更信息和提供對 store 的訪問。

  • executor - 允許在 store 上運行其他 process
  • store - store 引用
  • operations - 一組應用的 operation
  • undoOperations - 一組 operation,用來撤銷所應用的 operation
  • apply - store 上的 apply 方法
  • payload - 提供的 payload
  • id - 用於命名 process 的 id

訂閱 store 的變化

Store 有一個 onChange(path, callback) 方法,該方法接收一個或一組 path,並在狀態變更時調用回調函數。

main.ts

const store = new Store<State>();
const { path } = store;

store.onChange(path('auth', 'token'), () => {
    console.log('new login');
});

store.onChange([path('users', 'current'), path('users', 'list')], () => {
    // Make sure the current user is in the user list
});

Store 中還有一個 invalidate 事件,store 變化時就觸發該事件。

main.ts

store.on('invalidate', () => {
    // do something when the store's state has been updated.
});

共享的狀態管理模式

初始狀態

首次創建 store 時,它爲空。然後,可以使用一個 process 爲 store 填充初始的應用程序狀態。

main.ts

const store = new Store<State>();
const { path } = store;

const createCommand = createCommandFactory<State>();

const initialStateCommand = createCommand(({ path }) => {
    return [add(path('auth'), { token: undefined }), add(path('users'), { list: [] })];
});

const initialStateProcess = createProcess('initial', [initialStateCommand]);

initialStateProcess(store)({});

Undo

Dojo store 使用 patch operation 跟蹤底層 store 的變化。這樣,Dojo 就很容易創建一組 operation,然後撤銷這組 operation,以恢復一組 command 所修改的任何數據。undoOperationsProce***esult 的一部分,可在 after 中間件中使用。

當一個 process 包含了多個修改 store 狀態的 command,並且其中一個 command 執行失敗,需要回滾時,撤銷(Undo) operation 非常有用。

undo middleware

const undoOnFailure = () => {
    return {
        after: () => (error, result) {
            if (error) {
                result.store.apply(result.undoOperations);
            }
        }
    };
};

const process = createProcess('do-something', [
    command1, command2, command3
], [ undoOnFailure ])

在執行時,任何 command 出錯,則 undoOnFailure 中間件就負責應用 undoOperations

需要注意的是,undoOperations 僅適用於在 process 中完全執行的 command。在回滾狀態時,它將不包含以下任何 operation,這些狀態的變更可能是異步執行的其他 process 引起的,或者在中間件中執行的狀態變更,或者直接在 store 上操作的。這些用例不在 undo 系統的範圍內。

樂觀更新

樂觀更新可用於構建響應式 UI,儘管交互可能需要一些時間才能響應,例如往遠程保存資源。

例如,假使正在添加一個 todo 項,通過樂觀更新,可以在向服務器發送持久化對象的請求之前,就將 todo 項添加到 store 中,從而避免尷尬的等待期或者加載指示器。當服務器響應後,可以根據服務器操作的結果成功與否,來協調 store 中的 todo 項。

在成功的場景中,使用服務器響應中提供的 id 來更新已添加的 Todo 項,並將 Todo 項的顏色改爲綠色,以指示已保存成功。

在出錯的場景中,可以顯示一個通知,說明請求失敗,並將 Todo 項的顏色改爲紅色,同時顯示一個“重試”按鈕。甚至可以恢復或撤銷添加的 Todo 項,以及在 process 中發生的其他任何操作。

const handleAddTodoErrorProcess = createProcess('error', [ () => [ add(path('failed'), true) ]; ]);

const addTodoErrorMiddleware = () => {
    return {
        after: () => (error, result) {
            if (error) {
                result.store.apply(result.undoOperations);
                result.executor(handleAddTodoErrorProcess);
            }
        }
    };
};

const addTodoProcess = createProcess('add-todo', [
        addTodoCommand,
        calculateCountsCommand,
        postTodoCommand,
        calculateCountsCommand
    ],
    [ addTodoCallback ]);
  • addTodoCommand - 在應用程序狀態中添加一個 todo 項
  • calculateCountsCommand - 重新計算已完成的待辦項個數和活動的待辦項個數
  • postTodoCommand - 將 todo 項提交給遠程服務,並使用 process 的 after 中間件在發生錯誤時執行進一步更改
    • 失敗時 將恢復更改,並將 failed 狀態字段設置爲 true
    • 成功時 使用從遠程服務返回的值更新 todo 項的 id 字段
  • calculateCountsCommand - postTodoCommand 成功後再運行一次

同步更新

在某些情況下,在繼續執行 process 之前,最好等後端調用完成。例如,當 process 從屏幕中刪除一個元素時,或者 outlet 發生變化要顯示不同的視圖,恢復觸發這些操作的狀態可能會讓人感到很詭異(譯註:數據先從界面上刪掉了,因爲後臺刪除失敗,過一會數據又出現在界面上)。

因爲 process 支持異步 command,只需簡單的返回 Promise 以等待結果。

function byId(id: string) {
    return (item: any) => id === item.id;
}

async function deleteTodoCommand({ get, payload: { id } }: CommandRequest) {
    const { todo, index } = find(get('/todos'), byId(id));
    await fetch(`/todo/${todo.id}`, { method: 'DELETE' });
    return [remove(path('todos', index))];
}

const deleteTodoProcess = createProcess('delete', [deleteTodoCommand, calculateCountsCommand]);

併發 command

Process 支持併發執行多個 command,只需將這些 command 放在一個數組中即可。

process.ts

createProcess('my-process', [commandLeft, [concurrentCommandOne, concurrentCommandTwo], commandRight]);

本示例中,commandLeft 先執行,然後併發執行 concurrentCommandOneconcurrentCommandTwo。當所有的併發 command 執行完成後,就按需應用返回的結果。如果任一併發 command 出錯,則不會應用任何操作。最後,執行 commandRight

可替換的狀態實現

當實例化 store 時,會默認使用 MutableState 接口的實現。在大部分情況下,默認的狀態接口都經過了很好的優化,足以適用於常見情況。如果一個特殊的用例需要另一個實現,則可以在初始化時傳入該實現。

const store = new Store({ state: myStateImpl });

MutableState API

任何 State 實現都必須提供四個方法,以在狀態上正確的應用操作。

  • get&lt;S&gt;(path: Path&lt;M, S&gt;): S 接收一個 Path 對象,並返回當前狀態中該 path 指向的值
  • at&lt;S extends Path&lt;M, Array&lt;any&gt;&gt;&gt;(path: S, index: number): Path&lt;M, S['value'][0]&gt; 返回一個 Path 對象,該對象指向 path 定位到的數組中索引爲 index 的值
  • path: StatePaths&lt;M&gt; 以類型安全的方式,爲狀態中給定的 path 生成一個 Path 對象
  • apply(operations: PatchOperation&lt;T&gt;[]): PatchOperation&lt;T&gt;[] 將提供的 operation 應用到當前狀態上

ImmutableState

Dojo Store 通過 Immutable 爲 MutableState 接口提供了一個實現。如果對 store 的狀態做頻繁的、較深層級的更新,則這個實現可能會提高性能。在最終決定使用這個實現之前,應先測試和驗證性能。

Using Immutable

import State from './interfaces';
import Store from '@dojo/framework/stores/Store';
import Registry from '@dojo/framework/widget-core/Registry';
import ImmutableState from '@dojo/framework/stores/state/ImmutableState';

const registry = new Registry();
const customStore = new ImmutableState<State>();
const store = new Store<State>({ store: customStore });

本地存儲

Dojo Store 提供了一組工具來使用本地存儲(local storage)。

本地存儲中間件監視指定路徑上的變化,並使用 collector 中提供的 id 和 path 中定義的結構,將它們存儲在本地磁盤上。

使用本地存儲中間件:

export const myProcess = createProcess(
    'my-process',
    [command],
    collector('my-process', (path) => {
        return [path('state', 'to', 'save'), path('other', 'state', 'to', 'save')];
    })
);

來自 LocalStorage 中的 load 函數用於與 store 結合

與狀態結合:

import { load } from '@dojo/framework/stores/middleware/localStorage';
import { Store } from '@dojo/framework/stores/Store';

const store = new Store();
load('my-process', store);

注意,數據要能夠被序列化以便存儲,並在每次調用 process 後都會覆蓋數據。此實現不適用於不能序列化的數據(如 DateArrayBuffer)。

高級的 store operation

Dojo Store 使用 operation 來更改應用程序的底層狀態。這樣設計 operation,有助於簡化對 store 的常用交互,例如,operation 將自動創建支持 addreplace operation 所需的底層結構。

在未初始化的 store 中執行一個深度 add

import Store from '@dojo/framework/stores/Store';
import { add } from '@dojo/framework/stores/state/operations';

const store = new Store<State>();
const { at, path, apply } = store;
const user = { id: '0', name: 'Paul' };

apply([add(at(path('users', 'list'), 10), user)]);

結果爲:

{
    "users": {
        "list": [
            {
                "id": "0",
                "name": "Paul"
            }
        ]
    }
}

即使狀態尚未初始化,Dojo 也能基於提供的 path 創建出底層的層次結構。這個操作是安全的,因爲 TypeScript 和 Dojo 提供了類型安全。這允許用戶很自然的使用 store 所用的 State 接口,而不需要顯式關注 store 中保存的數據。

當需要顯式使用數據時,可以使用 test 操作或者通過獲取底層數據來斷言該信息,並通過編程的方式來驗證。

本示例使用 test 操作來確保已初始化,確保始終將 user 添加到列表的末尾:

import Store from '@dojo/framework/stores/Store';
import { test } from '@dojo/framework/stores/state/operations';

const store = new Store<State>();
const { at, path, apply } = store;

apply([test(at(path('users', 'list', 'length'), 0))]);

本示例通過編程的方式,確保 user 總是作爲最後一個元素添加到列表的末尾:

import Store from '@dojo/framework/stores/Store';
import { add, test } from '@dojo/framework/stores/state/operations';

const store = new Store<State>();
const { get, at, path, apply } = store;
const user = { id: '0', name: 'Paul' };
const pos = get(path('users', 'list', 'length')) || 0;
apply([
    add(at(path('users', 'list'), pos), user),
    test(at(path('users', 'list'), pos), user),
    test(path('users', 'list', 'length'), pos + 1)
]);

禁止訪問狀態的根節點,如果訪問將會引發錯誤,例如嘗試執行 get(path('/'))。此限制也適用於 operation;不能創建一個更新狀態根節點的 operation。@dojo/framewok/stores 的最佳實踐是鼓勵只訪問 store 中最小的、必需的部分。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章