dojo dragon main logo

介绍

Dojo **存储** 提供了一个可预测、一致的状态容器,并内置支持常见的状态管理模式。

Dojo 存储包提供了一个集中式存储,旨在成为应用程序的单一事实来源。Dojo 应用程序使用单向数据流运行;因此,所有应用程序数据都遵循相同的生命周期,确保应用程序逻辑可预测且易于理解。

功能 描述
全局数据存储 应用程序状态在单一事实来源中全局存储。
单向数据流 可预测且全局的应用程序状态管理。
类型安全 状态的访问和修改受接口保护。
操作驱动的状态更改 封装的、定义明确的状态修改,可以记录、撤消和重放。
异步支持 开箱即用的异步命令支持。
操作中间件 操作前后、错误处理和数据转换。
简单的小部件集成 用于轻松与 Dojo 小部件集成的工具和模式。

基本用法

Dojo 提供了一个反应式架构,关注不断修改和渲染应用程序的当前状态。在简单的系统中,这可以在本地级别发生,小部件可以维护自己的状态。但是,随着系统变得更加复杂,更好地划分和封装数据以及创建清晰的关注点分离的需求迅速增长。

存储通过单向数据流提供了一个干净的接口,用于从全局对象存储、修改和检索数据。存储包括对常见模式的支持,例如异步数据检索、中间件和撤消。存储及其模式使小部件能够专注于其主要作用,即提供信息的视觉表示并监听用户交互。

存储

存储保存整个应用程序的全局原子状态。存储应在应用程序创建时创建,并在 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 转换,因为这通过使 Dojo 虚拟 DOM 系统更容易确定何时发生数据更改来提高性能。

interfaces.d.ts

interface User {
	id: string;
	name: string;
}

export interface State {
	auth: {
		token: string;
	};
	users: {
		current: User;
		list: User[];
	};
}

以上是一个简单的示例,它定义了在本指南其余示例中使用的存储的结构。

更新存储

在使用 Dojo 存储时,有三个核心概念。

  • **操作** - 用于操作存储持有的状态的指令
  • **命令** - 执行业务逻辑并返回操作的简单函数
  • **流程** - 执行一组命令并表示应用程序行为

命令和操作

要修改存储,在执行流程时,会调用命令函数。命令函数返回要应用于存储的操作列表。每个命令都传递一个 CommandRequest,它提供 pathat 函数以类型安全的方式生成 Path,一个用于访问存储状态的 get 函数,一个用于流程执行器调用的参数的 payload 对象。

命令工厂

存储有一个简单的包装函数,充当创建新命令的类型安全工厂。

要创建存储工厂

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 确保包装的命令具有正确的类型,并且传递的 CommandRequest 函数被类型化为提供给 createCommandFactoryState 接口。虽然可以手动类型化命令,但本指南中的示例使用 createCommand

path

路径是一个 string,它描述了应用操作的位置。path 函数是 CommandRequest 的一部分,可以在 Command 中访问。

在本例中,path 描述了存储中的一个位置。State 与上面在 interface.d.ts 中定义的相同。State 接口被 Store 用于理解状态数据的形状。

要为当前用户名定义一个 path

const store = new Store<State>();
const { path } = store;

path('users', 'current', 'name');

此路径引用位于 /users/current/namestring 值。path 用于以类型安全的方式遍历层次结构,确保仅使用 State 接口中定义的属性名称。

at

at 函数与 path 结合使用以标识数组中的位置。此示例利用了 at 函数。

const store = new Store<State>();
const { at, path } = store;

at(path('users', 'list'), 1);

此路径引用位于 /users/list 偏移量为 1User

add 操作

将值添加到对象或将其插入数组。

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 操作

从对象或数组中删除值。

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 的初始状态,并从列表中删除了第一个 user

replace 操作

替换值。等效于 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)
	];
});

此示例将 list 中的第二个用户替换为 newUser

get

get 函数从存储中返回指定路径的值,如果该位置不存在值,则返回 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 是一个对象字面量,在从流程调用命令时传递给命令。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 的开头。

异步命令

命令可以是同步的或异步的。异步命令应返回一个 Promise 以指示它们何时完成。操作在每个命令成功完成之后被收集并原子地应用。

流程

一个 Process 是一个用于顺序地对 store 执行命令以更改应用程序状态的构造。流程使用 createProcess 工厂函数创建,该函数接受一个命令列表和可选的中介件列表。

创建流程

首先,创建一个负责获取用户令牌并使用该令牌加载 User 的命令。然后创建一个使用这些命令的流程。每个流程都必须由一个唯一的流程 ID 标识。此 ID 在存储中内部使用。

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 类型

流程执行器的 payload 从命令的 payload 类型推断。如果 payload 不同,则需要显式定义 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' });

连接小部件和存储

小部件有两个状态容器可用:StoreContainerStoreProvider。这些容器将应用程序存储与小部件连接起来。当使用函数式小部件时,还可以创建类型化的存储中介件。

请注意,本节中的文档旨在说明小部件和状态(由存储提供)是如何连接的。有关一般小部件状态管理的更多信息,请参阅 创建小部件参考指南

存储中介件

当使用基于函数的小部件时,可以使用 createStoreMiddleware 帮助程序来创建类型化的存储中介件,该中介件为小部件提供对存储的访问。

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 方法,该方法可用于在存储上运行流程。

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 并连接到存储。它总是封装在另一个小部件中,因为它没有定义自己的属性。

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 是一个完全封装另一个小部件的小部件。它使用 getProperties 函数将存储连接到小部件。

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 负责从存储中访问数据并为小部件创建属性。

一个 StoreContainer 的属性与它包装的小部件一一对应。小部件的属性成为 StoreContainer 的属性,但它们都是可选的。

执行流程

流程只是为一组数据定义了一个执行流程。要执行流程,流程需要访问存储以创建执行器。StoreContainerStoreProvider 小部件都提供对存储的访问。

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} />;
			}}
		/>
	);
});