系列
说明
相比于 redux 的集中状态管理,mobx 在概念上的确更简洁、灵活一些,我们只需要创建一个"可观察"对象,然后将组件变成了"观察者"就可以监控可观察对象的变化,从而做出响应了。但是为了团队合作更加方便,提升开发效率。适当的做一些封装还是很有必要的
封装原则
- 封装繁琐的重复的部分,让日常使用时有更少的引用,更少的配置
- 抽象通用的部分,提供更方便的接口、方法调用
- 借助 Typescript 提供更全面的提示及纠错能力,尽量借助类型推断,减少使用时的代码量
- 全面考虑使用场景,减少后续改动频次
封装目标
- 通过 Class 来创建 Store 创建的 Store 会有通用的 action, 不需要重复编写
- store 之间有连通,每个子 store 中可以访问其他 store 的数据及方法
- 采用集中管理方案,即所有 store 都会汇总为一个
- 使用 makeAutoObservable 来将数据变成 ”可观察数据“,来减少使用者的操作(makeObservable 需要枚举定义可观察数据及 action 的类型)
- 可通过 Provider 传递 store, 可通过 inject 将 store 注入到组件中,并提供合理的 TS 类型提示
- 被 inject 包裹的组件,其挂载的”类方法“还需要保留
- 可以按照预期正常使用 ref (包括 函数式组件)
1. Mobx 封装实现细节
1. 封装 Store 的创建,抽离通用的 action
预期目标
- 通过 Class 来创建 Store 创建的 Store 会有通用的 action, 不需要重复编写
- store 之间有连通,每个子 store 中可以访问其他 store 的数据及方法
- 采用集中管理方案,即所有 store 都会汇总为一个
- 使用 makeAutoObservable 来将数据变成 ”可观察数据“,来减少使用者的操作(makeObservable 需要枚举定义可观察数据及 action 的类型)
注意:由于 makeAutoObservable 包裹的对象不能够有包含 super 或者 subclass(会报错),因此不能通过类的继承来实现封装。
1. 首先封装通用的 action (store/baseActions.ts)
通用 action 维护在一个对象上,这样方便处理 TS 类型
import { STORE_CONST } from './constants';
type TUpdateFn<T> = (storeInstance: T) => void;
const baseActions = {
// 一个更新 store 的通用方法,使用时会有 TS 提示与检查
updateStore(param: {} | TUpdateFn<{}>): void {
if (typeof param === 'function') {
param(this);
} else {
Object.keys(param).forEach((key: string) => {
this[key] = param[key];
});
}
},
// 一个重置 store 的通用方法
resetStore(): void {
const Constructor = _.get(this, 'constructor');
const initialData = new Constructor();
const ignore = [STORE_CONST.GLOBAL_STORE_NAME.toString()].concat(Object.keys(baseActions));
Object.keys(initialData).forEach((key: string) => {
const value = initialData[key];
if (_.isFunction(value) || ignore.includes(key)) return;
this[key] = value;
});
},
};
type Reset<T, K extends keyof T, O> = {
[P in keyof T]: P extends K ? O : T[P];
};
type TempBaseActions = typeof baseActions;
export type TUpdateStore<T extends {}> = (param: Partial<T> | TUpdateFn<T>) => void;
// 重点是将 BaseAction 的类型与 传入的 T(Store 的类型) 关联上,这样才会有 TS 的检查与提示
export type TBaseActions<T> = Reset<TempBaseActions, 'updateStore', TUpdateStore<T>>;
export default baseActions;
2. 封装将 Store 数据变为 ”可观察“ 的工具方法 (store/enhanceStore.ts)
目标是将 Store 变成 Observable 并且要融合 baseAction 中的 action ,通过可以store 内可以通过 this.global.xxxStore 来访问其他 store
虽然 makeAutoObservable 不能处理通过继承后实例化的对象,但是我们可以给对象插入属性后再交给 makeAutoObservable 处理,前提是要处理好 TS 的类型
import { makeAutoObservable } from 'mobx';
import { STORE_CONST } from './constants';
import baseActions, { TBaseActions } from './baseActions';
const baseActionsKeys = Object.getOwnPropertyNames(baseActions);
function enhanceStore<T, N>(storeInstance: T, globalStore?: N): T & TBaseActions<T> {
baseActionsKeys.forEach((key: string) => {
if (key === 'constructor') return;
const descriptor = Object.getOwnPropertyDescriptor(baseActions, key);
Object.defineProperty(storeInstance, key, descriptor!);
});
// 将 globalStore 作为 store 的一个属性保留,以便可以通过 this.global.xxxStore 来访问其他store
if (globalStore) {
Object.defineProperty(storeInstance, STORE_CONST.GLOBAL_STORE_NAME, {
value: globalStore,
configurable: true,
});
}
/**
为什么不使用 extendObservable 来做属性及方法的扩展,因为它会与类中的类型声明发生冲突:
1. 我们传入 storeInstance 是一个实例,这个实例构造器类上为了在类结构体中使用 baseAction 或者 globalStore
会有相关的类型声明,声明的同时不可避免的会有属性初始化
2. extendObservable 是用来给对象扩展新的属性的,有过初始化的属性再使用 extendObservable 来做扩展目前会有报错不好实现
*/
return makeAutoObservable(storeInstance as T & TBaseActions<T>);
}
export default enhanceStore;
3. 日常开发中如何管理 Store (store/index.ts)
- 创建某个模块的 ModuleStore
- 将这个 ModuleStore 合并到 GlobalStore 上
// 创建一个全局的 Store 并建立 store 间通信的桥梁
import enhanceStore from 'common/store/enhanceStore';
import RemoteStore from '../../modules/remote/RemoteStore';
import UserStore from '../../modules/user/UserStore';
// enhanceStore 封装的Store 会通过类型推断拿到正确的类型提示
// 通过传入第二个参数 this 建立 store 间的链接,通过 this.global.xxxStore 访问其他 Store
class GlobalStore {
public remoteStore = enhanceStore(new RemoteStore(), this);
public userStore = enhanceStore(new UserStore(), this);
/**
定义两个全局方法,用来处理默认的成功失败提示
TODO:是否需要放在 baseAction 中 ?,
*/
public handleError = () => {};
public handleSuccess = () => {};
}
type TGlobalStore = GlobalStore;
export * from 'common/store/baseActions';
export { TGlobalStore };
export default new GlobalStore();
// 某个模块的 Store 案例
import { TGlobalStore, TUpdateStore } from 'portalBase/store';
class UserStore {
/**
Store 外部,即组件中可以直接访问 baseAction 中的方法
但是在 Store 内部如果想访问 baseAction 中的方法或者访问 global Store
则需要显示的声明一下类型
*/
private readonly global: TGlobalStore;
public updateStore: TUpdateStore<UserStore>;
public resetStore: () => void;
public users: { name: string; tel: number }[] = [];
public increaseUser = (user: { name: string; tel: number }) => {
// 1. action 中直接处理 ”可观察数据“
this.users.push(user);
// 2. 也可以借助 baseAction 中的方法,会有 TS 类型检查
this.updateStore({users: [user]});
// 3. store 中可以直接访问其他 store 的内容
console.log(this.global.remoteStore.counter);
};
}
export default UserStore;
2. 封装 Provider inject 给组件提供 store 上下文
虽然
"mobx-react"
原本提供了Provider inject
但是其官网中就已说明:不再建议使用这两个 Api ,反而建议通过 React.createContext 自己建.
而且 这两个 api 在使用时有些别扭:
- Provider 支持传入 store, 但是要一个一个传 然后在使用时会被合并到一个 root 对象上, 这种情况下各个 store 是断联的,不容易互相引用
- inject 并没有提供更好的 TS 类型提示,需要我们 type Props = {userStore?: UserStore } 传入的 userStore 变成了可选值,用起来比较麻烦
综上,我们需要自己建立一套 mobx store 的传递及使用方案
预期使用效果:
- 可通过 Provider 传递 store, 可通过 inject 将 store 插入组件中,并提供合理的 TS 类型提示
- 被 inject 包裹的组件,其挂载”类方法“还需要保留
- 可以按照预期正常使用 ref (包括 函数式组件)
1. 封装创建 context 的方法 createContext (store/createStoreContext.ts)
import { observer } from 'mobx-react';
import React from 'react';
import hoistNonReactStatics from 'hoist-non-react-statics'; // 一个用来将类的静态方法拷贝到其他类上的工具(会避开React 内置的静态方法)
// 一个获得组件 Props 的 Type
type GetProps<C> = C extends React.ComponentType<infer P>
? C extends React.ComponentClass<P>
? React.ClassAttributes<InstanceType<C>> & P
: P
: never;
// 定义 Ref 类型
type InjectorRef<C> = C extends React.ComponentType<infer P>
? C extends React.ComponentClass<P>
? InstanceType<C>
: C extends React.ForwardRefExoticComponent<
React.PropsWithoutRef<P> & React.RefAttributes<infer T>
>
? T
: never
: never;
type IReactComponent<P = any> =
| React.ClassicComponentClass<P>
| React.ComponentClass<P>
| React.FunctionComponent<P>
| React.ForwardRefExoticComponent<P>;
/**
定义一个创建 context 的方法,这个方法会输出 Provider inject 等一系列工具
这些工具都会关联上传入 泛型 T(即 GlobalStore)
*/
function createContext<T extends {}>() {
const StoreContext = React.createContext<T>({} as T);
interface Props {
store: T;
children: React.ReactNode;
}
// 1. 顶层的 store 包装
const Provider: React.FC<Props> = (props: Props) => {
const { store, children } = props;
return <StoreContext.Provider value={store}>{children}</StoreContext.Provider>;
};
// 2. 函数式组件直接获得全量 store
const useStore = (): T => React.useContext(StoreContext);
// 3. 函数式 | 类组件,注入指定 store 并转换为 observer
function inject<K extends {}>(parseFn: (store: T) => K) {
return <C extends IReactComponent<K & {}>>(
Component: C,
options: { isObserver: boolean } = { isObserver: true }, // 可用来关闭 observer
) => {
const component = options.isObserver ? observer(Component) : Component;
const Injector = React.forwardRef(
(props: Omit<GetProps<C>, keyof K | 'ref'>, ref: React.ForwardedRef<InjectorRef<C>>) => {
const store = React.useContext(StoreContext);
const injectedStore = parseFn(store);
const newProps = { ...props, ...injectedStore, ...(ref ? { ref } : {}) };
return React.createElement(component, newProps);
},
);
Injector.defaultProps = { ...component.defaultProps };
Injector.displayName = `inject(${Component.name || Component.displayName})`;
return hoistNonReactStatics(Injector, Component); };
}
// 4. 函数式 | 类组件,装换为 observer (同时保持 static 方法, 直接用 observer 会丢失静态方法和静态属性)
function convert<C extends IReactComponent<{}>>(Component: C) {
return hoistNonReactStatics(observer(Component), Component);
}
return { StoreContext, Provider, useStore, inject, convert };
}
export default createContext;
2. 在 global Store 创建后创建 Provider (store/index.ts)
createContext 的执行依赖传入的 GlobalStore 类型,因此会将 context 的创建与 global store 的创建放在一起
import enhanceStore from 'common/store/enhanceStore';
import createContext from 'common/store/createStoreContext';
import RemoteStore from '../../modules/remote/RemoteStore';
import UserStore from '../../modules/user/UserStore';
class GlobalStore {
public remoteStore = enhanceStore(new RemoteStore(), this);
public userStore = enhanceStore(new UserStore(), this);
public handleError = () => {};
public handleSuccess = () => {};
}
type TGlobalStore = GlobalStore;
// 依据传入的 GlobalStore 创建
const { Provider, StoreContext, useStore, inject, convert } = createContext<TGlobalStore>();
export * from 'common/store/baseActions';
export { TGlobalStore, Provider, StoreContext, useStore, inject, convert };
export default new GlobalStore();
3. 日常开发中如何使用 store
// 1. 项目最外层使用 Provider
import globalStore, { Provider } from 'portalBase/store';
import RootContainer from './modules/root/routes';
ReactDOM.render(
<Provider store={globalStore}>
<RootContainer />
</Provider>,
document.getElementById('root'),
);
// 2. 组件中 注入 store
import { TGlobalStore, inject } from 'portalBase/store';
interface Props {
userStore: TGlobalStore['userStore'];
}
class User extends React.Component<Props> {
render(): React.ReactNode {
const { userStore } = this.props;
return (
<div>
<p>user length: {userStore.users.length}</p>
<UserCreateForm userStore={userStore} />
<UserList userStore={userStore} />
</div>
);
}
}
// 使用 inject 注入 store
export default inject((store) => {
return { userStore: store.userStore };
})(User);
// 3. 组件中调用了 ”可观察数据“ 但不是注入 store
import { TGlobalStore, convert } from 'portalBase/store';
import UserLess from '../style/user.less';
import UserItem from './UserItem';
interface Props {
userStore: TGlobalStore['userStore'];
}
const UserList: React.FC<Props> = (props: Props) => {
const { userStore } = props;
console.log('render User List');
return (
<ul className={UserLess.main}>
<li>
<span>姓名</span>
<span>电话</span>
</li>
{userStore.users.map((user: { name: string; tel: number }) => {
return <UserItem user={user} key={user.name} />;
})}
</ul>
);
};
// 使用 convert 代替 mobx-react 的 observer 来将组件变成观察者
export default convert(UserList);
// 4. 组件中只是引用了 store 但是并没有调用其中的 ”可观察数据“
import { Button, Form, Input, InputNumber } from 'antd';
import { TGlobalStore } from 'portalBase/store';
import UserLess from '../style/user.less';
interface Props {
userStore: TGlobalStore['userStore'];
}
const UserCreateForm: React.FC<Props> = (props: Props) => {
const { originalStore } = props;
const onFinish = (values: { name: string; tel: number }) => {
// 仅调用了 action 而不是调用 ”可观察数据“
originalStore.increaseUser(values);
};
console.log('render user create Form');
return (
<header className={UserLess.header}>
<Form onFinish={onFinish}>
<Form.Item label="姓名" name="name">
<Input />
</Form.Item>
<Form.Item label="电话" name="tel">
<InputNumber />
</Form.Item>
<Form.Item label="module" name="module">
<Button type="primary" htmlType="submit">
create
</Button>
</Form.Item>
</Form>
</header>
);
};
// 为了提高性能或者减少不必要的 render 函数式组件需要通过 React.memo 包裹,类组件则使用 React.PureComponent
export default React.memo(UserCreateForm);