Dojo Store 詳解

State 對象

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

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,若是嘗試訪問將當即拋出錯誤。github

StoreProvider

StoreProvider 接收三個屬性編程

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

失效

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

  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 實現了 ProcessResult 接口,以提供有關應用到 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 所修改的任何數據。undoOperationsProcessResult 的一部分,可在 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<S>(path: Path<M, S>): S 接收一個 Path 對象,並返回當前狀態中該 path 指向的值
  • at<S extends Path<M, Array<any>>>(path: S, index: number): Path<M, S['value'][0]> 返回一個 Path 對象,該對象指向 path 定位到的數組中索引爲 index 的值
  • path: StatePaths<M> 以類型安全的方式,爲狀態中給定的 path 生成一個 Path 對象
  • apply(operations: PatchOperation<T>[]): PatchOperation<T>[] 將提供的 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 中最小的、必需的部分。

相關文章
相關標籤/搜索