介紹
Dojo store 提供可預測的、一緻的狀态容器,内置了對共享狀态管理模式的支援。
Dojo store 包提供了一個集中式存儲,為應用程式提供真正的單一資料源。Dojo 應用程式的操作使用單向資料流;是以,所有應用程式資料遵循相同的生命周期,確定應用程式邏輯是可預測的,且易于了解。
功能 | 描述 |
全局資料存儲 | 應用程式狀态全局存儲在真正的單一資料源中。 |
單向資料流 | 可預測的、全局的應用程式狀态管理。 |
類型安全 | 對狀态的通路和修改都受接口的保護。 |
操作驅動的狀态變更 | 封裝的、定義良好的狀态修改,可以記錄、撤消和重放。 |
異步支援 | 開箱即用的異步指令(command)支援。 |
操作中間件 | 在操作前和操作後進行錯誤處理和資料轉換。 |
簡單的部件內建 | 提供與 Dojo 部件輕松內建的工具和模式。 |
基本用法
Dojo 提供了一種響應式架構,能夠持續修改和渲染應用程式的目前狀态。在簡單系統中,這通常發生在部件内部,并且部件可以修改自己的狀态。然而,随着系統變得越來越複雜,就需要更好的劃分和封裝資料,并随着快速增長建立一個清晰的隔離。
Store 提供了一個清晰的接口,通過單向資料流對全局對象進行存儲、修改和檢索。Store 中包含對共享模式的支援,如異步資料擷取、中間件和撤銷。Store 及其模式允許部件聚焦于它們的主要角色,即對資訊的可視化展示和監聽使用者互動。
store 對象
store 對象存儲整個應用程式全局的、原子的狀态。應該在建立應用程式時建立 store 對象,并使用一個注入器将其定義到 Registry 中。
main.ts
import { registerStoreInjector } from '@dojo/framework/stores/StoreInjector';
import Store from '@dojo/framework/stores/Store';
import { State } from './interfaces';
const store = new Store<State>();
const registry = registerStoreInjector(store);
State 使用接口定義全局存儲的結構。State 中的所有内容都應是可序列化的,即能轉換為 jsON 或從 jsON 轉換回來,這樣的話, Dojo 的虛拟 DOM 系統更容易确定何時更改了資料,進而提高性能。
interfaces.d.ts
interface User {
id: string;
name: string;
}
export interface State {
auth: {
token: string;
};
users: {
current: User;
list: User[];
};
}
上面是一個簡單的示例,定義了 store 的結構,會在本指南的其餘示例中使用。
更新 store
使用 Dojo store 時需注意三個核心概念。
- Operation - 操作 store 所持狀态的指令
- Command - 執行業務邏輯并傳回 operation 的簡單函數
- Process - 執行一組 command 和表示應用程式的行為
Command 和 operation
要修改 store 中的值,則在執行 process 時,會調用一個 command 函數。command 函數傳回要應用到 store 上的一系列 operation。每個 command 都要傳入一個 CommandRequest 參數,它提供了 path 和 at 函數,會以類型安全的方式生成 Path,也提供了 get 函數來通路 store 中的狀态,以及提供 payload 對象來為被調用的 process 執行器傳入參數。
Command 工廠
Store 中有一個簡單的封裝函數,用于建立 command,是一個類型安全的工廠函數。
建立 store 工廠:
import { createCommandFactory } from '@dojo/framework/stores/process';
import { State } from './interfaces';
const createCommand = createCommandFactory<State>();
const myCommand = createCommand(({ at, get, path, payload, state }) => {
return [];
});
createCommand 確定封裝的 command 具有正确的類型,而傳入的 CommandRequest 函數能獲得通過 createCommandFactory 提供的 State 接口的類型。雖然可以手動為 command 設定類型,但本指南中的示例使用 createCommand。
path
path 是一個 string,用于描述應用 operation 的位置。path 函數是 CommandRequest 中的一部分,可以在 Command 中通路。
本示例中,path 描述了 store 中的一個位置。State 與上面 interface.d.ts 中定義的相同。Store 通過 State接口獲知狀态資料的形狀。
定義一個擷取目前使用者名的 path:
const store = new Store<State>();
const { path } = store;
path('users', 'current', 'name');
這個 path 引用的 string 值位于 /users/current/name。path 以類型安全的方式周遊層次結構,確定隻能使用在 State 接口中定義的屬性名。
at
at 函數與 path 函數一起辨別數組中的位置。本示例使用了 at 函數。
const store = new Store<State>();
const { at, path } = store;
at(path('users', 'list'), 1);
這個 path 引用的是位于 /user/list 中偏移量為 1 的 User。
add operation
用于向對象中添加值或者向數組中插入值。
import { createCommandFactory } from '@dojo/framework/stores/process';
import { State } from './interfaces';
import { add } from '@dojo/framework/stores/state/operations';
const createCommand = createCommandFactory<State>();
const myCommand = createCommand(({ at, get, path, payload, state }) => {
const user = { id: '0', name: 'Paul' };
return [add(at(path('users', 'list'), 0), user)];
});
會将 user 插入到使用者清單的起始位置。
remove operation
從對象或數組中移除值。
import { createCommandFactory } from '@dojo/framework/stores/process';
import { State } from './interfaces';
import { add, remove } from '@dojo/framework/stores/state/operations';
const createCommand = createCommandFactory<State>();
const myCommand = createCommand(({ at, get, path, payload, state }) => {
const user = { id: '0', name: 'Paul' };
return [
add(path('users'), {
current: user,
list: [user]
}),
remove(at(path('users', 'list'), 0))
];
});
本示例先為 users 添加一個初始狀态,然後移除 list 中的第一個 user。
replace operation
替換值。相當于先 remove 再 add。
import { createCommandFactory } from '@dojo/framework/stores/process';
import { State } from './interfaces';
import { add, replace } from '@dojo/framework/stores/state/operations';
const createCommand = createCommandFactory<State>();
const myCommand = createCommand(({ at, get, path, payload, state }) => {
const users = [{ id: '0', name: 'Paul' }, { id: '1', name: 'Michael' }];
const newUser = { id: '2', name: 'Shannon' };
return [
add(path('users'), {
current: user[0],
list: users
}),
replace(at(path('users', 'list'), 1), newUser)
];
});
本示例使用 newUser 替換掉 list 中的第二個使用者資訊。
get
get 函數會傳回 store 在指定 path 位置的值,如果該位置不存在值,則傳回 undefined。
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 updateCurrentUser = createCommand(async ({ at, get, path }) => {
const token = get(path('auth', 'token'));
if (!token) {
return [remove(path('users', 'current'))];
} else {
const user = await fetchCurrentUser(token);
return [replace(path('users', 'current'), user)];
}
});
本示例檢查是否存在身份認證令牌,然後據此更新目前使用者的資訊。
payload
payload 是一個對象字面量,當 process 調用 command 時,會将其傳給 command。也可以在建構指令時傳入 payload 的類型。
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>(({ at, path, payload }) => {
return [add(at(path('users', 'list'), 0), payload)];
});
本示例将 payload 提供的使用者資訊添加到 /users/list 的起始位置。
異步 command
command 可以同步執行,也可以異步執行。異步 command 應該傳回一個 Promise,以便指出何時完成。每個 command 成功完成後,将自動收集和應用 operation。
Process
Process 在 store 上按順序執行 command,以修改應用程式的狀态。使用 createProcess 工廠函數建立 process,該函數可傳入一系列 command,以及選擇性的傳入一系列中間件。
建立 process
首先,建立兩個 command,負責擷取使用者令牌,并使用該令牌加載 User。然後建立一個 process 來使用這兩個 command。每一個 process 都應該使用 ID 唯一辨別。此 ID 在 store 内部使用。
import { createCommandFactory, createProcess } from "@dojo/framework/stores/process";
import { State } from './interfaces';
import { add, replace } from "@dojo/framework/stores/state/operations";
const createCommand = createCommandFactory<State>();
const fetchUser = createCommand(async ({ at, get, payload: { username, password } }) => {
const token = await fetchToken(username, password);
return [
add(path('auth', 'token'), token);
];
}
const loadUserData = createCommand(async ({ path }) => {
const token = get(path('auth', 'token'));
const user = await fetchCurrentUser(token);
return [
replace(path('users', 'current'), user)
];
});
export const login = createProcess('login', [ fetchUser, loadUserData ]);
payload 類型
process 執行器(process executor)的 payload 是從 command 的 payload 類型推斷出來的。如果指令間的 payload 類型不同,則需要顯式定義 process 執行器的 payload 類型。
const createCommand = createCommandFactory<State>();
const commandOne = createCommand<{ one: string }>(({ payload }) => {
return [];
});
const commandTwo = createCommand<{ two: string }>(({ payload }) => {
return [];
});
const process = createProcess<State, { one: string; two: string }>('example', [commandOne, commandTwo]);
process(store)({ one: 'one', two: 'two' });
關聯部件和 store
有兩個狀态容器可用于部件:StoreContainer 和 StoreProvider。這些容器将應用程式的 store 與部件關聯起來。當使用函數部件時,也可以建立類型化的 store 中間件。
注意,本節旨在介紹部件和狀态(通過 store 提供的)是如何關聯起來的。
Store 中間件
當使用基于函數的部件時,createStoreModdleware 幫助函數用于建立類型化的 store 中間件,讓部件能通路 store。
middleware/store.ts
import createStoreMiddleware from '@dojo/framework/core/middleware/store';
import { State } from '../interfaces';
export default createStoreMiddleware<State>();
widgets/User.tsx
import { create } from '@dojo/framework/core/vdom';
import store from '../middleware/store';
import { State } from '../../interfaces';
const factory = create({ store }).properties();
export const User = factory(function User({ middleware: { store } }) {
const { get, path } = store;
const name = get(path('users', 'current', 'name'));
return <h1>{`Hello, ${name}`}</h1>;
});
此中間件包含一個 executor 方法,用于在 store 上運作 process。
import { create } from '@dojo/framework/core/vdom';
import store from '../middleware/store';
import logout from '../processes/logout';
import { State } from '../../interfaces';
const factory = create({ store }).properties();
export const User = factory(function User({ middleware: { store } }) {
const { get, path } = store;
const name = get(path('users', 'current', 'name'));
const onLogOut = () => {
store.executor(logout)({});
};
return (
<h1>
{`Hello, ${name}`}
<button onclick={onLogOut}>Log Out</button>
</h1>
);
});
StoreProvider
StoreProvider 是一個 Dojo 部件,它擁有 renderer,并與 store 關聯。它總是封裝在另一個部件内,因為它無法定義自己的屬性。
widget/User.ts
import { create } from '@dojo/framework/core/vdom';
import { State } from '../../interfaces';
const factory = create().properties();
export const User = factory(function User() {
return (
<StoreProvider
stateKey="state"
paths={(path) => [path('users', 'current')]}
renderer={(store) => {
const { get, path } = store;
const name = get(path('users', 'current', 'name'));
return <h1>{`Hello, ${name}`}</h1>;
}}
/>
);
});
StoreProvider 是 User 渲染内容的一部分,并且跟其它 Dojo 部件一樣,提供了自己的 renderer。
Container
Container 是一個部件,它完全封裝另一個部件。它使用 getProperties 函數将 store 關聯到部件上。
widget/User.tsx
import { create, tsx } from '@dojo/framework/core/vdom';
interface UserProperties {
name?: string;
}
const factory = create().properties<UserProperties>();
export const User = factory(function User({ properties }) {
const { name = 'Stranger' } = properties();
return <h1>{`Hello, ${name}`}</h1>;
});
widget/User.container.ts
import { createStoreContainer } from '@dojo/framework/stores/StoreContainer';
import { State } from '../interfaces';
import User from './User';
const StoreContainer = createStoreContainer<State>();
const UserContainer = StoreContainer(User, 'state', {
getProperties({ get, path }) {
const name = get(path('user', 'current', 'name'));
return { name };
}
});
本示例中,UserContainer 封裝了顯示目前使用者名的 User。createStoreContainer 是一個封裝器,用于確定 getProperties 中的類型正确。getProperties 負責從 store 中通路資料,并為部件建立屬性。
StoreContainer 屬性與其封裝部件的屬性是 1:1 映射的。部件的屬性成為 StoreContainer 的屬性,但這些屬性都是可選的。
執行 process
process 隻是為一組資料定義了一個執行流。要執行 process,就需要讓 process 基于 store 建立一個執行器。StoreContainer 和 StoreProvider 部件都支援通路 store。
import { logout } from './processes/logout';
import StoreProvider from '@dojo/framework/stores/StoreProvider';
import { State } from '../../interfaces';
import User from './User';
import { create, tsx } from '@dojo/framework/core/vdom';
const factory = create().properties();
export const UserProvider = factory(function UserProvider() {
return (
<StoreProvider
stateKey="state"
paths={(path) => [path('users', 'current')]}
renderer={(store) => {
const { get, path } = store;
const name = get(path('users', 'current', 'name'));
const onLogOut = () => {
logout(store)({});
};
return <User name={name} onLogOut={onLogOut} />;
}}
/>
);
});