翻譯自:https://github.com/dojo/framework/blob/master/docs/en/stores/supplemental.md
State
對象
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
StoreProvider 接收三個屬性
-
: 一個渲染函數,已将 store 注入其中,能通路狀态并向子部件傳入 process。renderer
-
: 注冊狀态時使用的 key 值。stateKey
-
(可選): 将此 provider 連接配接到狀态的某一局部上。paths
失效
StoreProvider
有兩種方法觸發失效并促使重新渲染。
- 推薦的方式是,通過向 provider 傳入
屬性來注冊paths
,以確定隻有相關狀态變化時才會失效。path
- 另一種是較籠統的方式,當沒有為 provider 定義
時,store 中的 任何 資料變化都會引起失效。path
Process
Process
生命周期
Process
有一個執行生命周期,它定義了所定義行為的流程。
- 如果存在轉換器,則首先執行轉換器來轉換 payload 對象
- 按順序同步執行
中間件before
- 按順序執行定義的 command
- 在執行完每個 command (如果是多個 command 則是一塊 command)之後,應用指令傳回的 operation
- 如果在執行指令期間抛出了異常,則不會再執行後續指令,并且也不會應用目前的 operation
-
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 的通路。
-
- 允許在 store 上運作其他 processexecutor
-
- store 引用store
-
- 一組應用的 operationoperations
-
- 一組 operation,用來撤銷所應用的 operationundoOperations
-
- store 上的 apply 方法apply
-
- 提供的 payloadpayload
-
- 用于命名 process 的 idid
訂閱 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 ]);
-
- 在應用程式狀态中添加一個 todo 項addTodoCommand
-
- 重新計算已完成的待辦項個數和活動的待辦項個數calculateCountsCommand
-
- 将 todo 項送出給遠端服務,并使用 process 的postTodoCommand
中間件在發生錯誤時執行進一步更改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
MutableState
任何
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 生成一個path: StatePaths<M>
Path
-
将提供的 operation 應用到目前狀态上apply(operations: PatchOperation<T>[]): PatchOperation<T>[]
ImmutableState
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)
]);