1. 程式人生 > 實用技巧 >Dojo Store 概念詳解

Dojo Store 概念詳解

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

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

MutableStateAPI

任何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 });

廣州品牌設計公司https://www.houdianzi.com PPT模板下載大全https://redbox.wode007.com

本地儲存

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或replaceoperation 所需的底層結構。

在未初始化的 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)
]);