Dojo Store 簡介

翻譯自 https://github.com/dojo/framework/blob/master/docs/en/stores/introduction.md

介紹

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 參數,它提供了 pathat 函數,會以類型安全的方式生成 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/namepath 以類型安全的方式遍歷層次結構,確保只能使用在 State 接口中定義的屬性名。

at

at 函數與 path 函數一起標識數組中的位置。本示例使用了 at 函數。

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

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

這個 path 引用的是位於 /user/list 中偏移量爲 1User

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

替換值。相當於先 removeadd

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

Processstore 上按順序執行 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

有兩個狀態容器可用於部件:StoreContainerStoreProvider。這些容器將應用程序的 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>;
            }}
        />
    );
});

StoreProviderUser 渲染內容的一部分,並且跟其它 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 封裝了顯示當前用戶名的 UsercreateStoreContainer 是一個封裝器,用於確保 getProperties 中的類型正確。getProperties 負責從 store 中訪問數據,併爲部件創建屬性。

StoreContainer 屬性與其封裝部件的屬性是 1:1 映射的。部件的屬性成爲 StoreContainer 的屬性,但這些屬性都是可選的。

執行 process

process 只是爲一組數據定義了一個執行流。要執行 process,就需要讓 process 基於 store 創建一個執行器。StoreContainerStoreProvider 部件都支持訪問 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} />;
            }}
        />
    );
});
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章