天天看點

Dojo Store 概念詳解共享的狀态管理模式進階的 store operation

翻譯自: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 中間件

使用可選的

before

after

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

也可以在清單中定義多個中間件。會根據中間件在清單中的順序同步調用。

Before

before

中間件塊能擷取傳入的

payload

store

的引用。

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 變化時就觸發該事件。

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

共享的狀态管理模式

初始狀态

首次建立 store 時,它為空。然後,可以使用一個 process 為 store 填充初始的應用程式狀态。

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 所修改的任何資料。

undoOperations

ProcessResult

的一部分,可在

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

先執行,然後并發執行

concurrentCommandOne

concurrentCommandTwo

。當所有的并發 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 後都會覆寫資料。此實作不适用于不能序列化的資料(如

Date

ArrayBuffer

)。

進階的 store operation

Dojo Store 使用 operation 來更改應用程式的底層狀态。這樣設計 operation,有助于簡化對 store 的常用互動,例如,operation 将自動建立支援

add

replace

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

操作或者通過擷取底層資料來斷言該資訊,并通過程式設計的方式來驗證。

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))]);           
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)
]);           

繼續閱讀