封装 mobx 6 + typescript 实现React状态管理 (2) -- mobx 封装

系列

说明

相比于 redux 的集中状态管理,mobx 在概念上的确更简洁、灵活一些,我们只需要创建一个"可观察"对象,然后将组件变成了"观察者"就可以监控可观察对象的变化,从而做出响应了。但是为了团队合作更加方便,提升开发效率。适当的做一些封装还是很有必要的

封装原则

  1. 封装繁琐的重复的部分,让日常使用时有更少的引用,更少的配置
  2. 抽象通用的部分,提供更方便的接口、方法调用
  3. 借助 Typescript 提供更全面的提示及纠错能力,尽量借助类型推断,减少使用时的代码量
  4. 全面考虑使用场景,减少后续改动频次

封装目标

  1. 通过 Class 来创建 Store 创建的 Store 会有通用的 action, 不需要重复编写
  2. store 之间有连通,每个子 store 中可以访问其他 store 的数据及方法
  3. 采用集中管理方案,即所有 store 都会汇总为一个
  4. 使用 makeAutoObservable 来将数据变成 ”可观察数据“,来减少使用者的操作(makeObservable 需要枚举定义可观察数据及 action 的类型)
  5. 可通过 Provider 传递 store, 可通过 inject 将 store 注入到组件中,并提供合理的 TS 类型提示
  6. 被 inject 包裹的组件,其挂载的”类方法“还需要保留
  7. 可以按照预期正常使用 ref (包括 函数式组件)

1. Mobx 封装实现细节

1. 封装 Store 的创建,抽离通用的 action

预期目标

  1. 通过 Class 来创建 Store 创建的 Store 会有通用的 action, 不需要重复编写
  2. store 之间有连通,每个子 store 中可以访问其他 store 的数据及方法
  3. 采用集中管理方案,即所有 store 都会汇总为一个
  4. 使用 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)
  1. 创建某个模块的 ModuleStore
  2. 将这个 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 的传递及使用方案

预期使用效果:

  1. 可通过 Provider 传递 store, 可通过 inject 将 store 插入组件中,并提供合理的 TS 类型提示
  2. 被 inject 包裹的组件,其挂载”类方法“还需要保留
  3. 可以按照预期正常使用 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);
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值