likes
comments
collection
share

你还在用redux吗?太麻烦了!useModel了解下?umi4.x推出的hook,useModel,但是只能在umi用

作者站长头像
站长
· 阅读数 245

写最仔细的文章,适配尽可能多的读者

umi在4.x的版本里面推出来一个超精简的状态管理工具。使用的复杂度相对于redux来说方便的多。但可惜的是,它并不是第三方依赖包。做不到在哪想用就直接install来使用。

所以,本文就来告诉大家如何在其他项目使用useModel,本文以 Taro(react) 版为例。

useModel介绍

官方给他的介绍是:它是一种基于 hooks 范式的轻量级数据管理方案,可以在 umi 项目中管理全局的共享数据。

底层使用是useContent

个人觉得useModel对于其他公共状态管理的优点是:以hook的形式去进行的状态管理,真的很方便。相对于传统项目的redux和 umi3.x 推出的dva来说。useModel把状态管理简化到了当前技术框架的极致。

umi官方对于useModel的介绍

useModel源码里使用的是useContent这个hook进行的状态管理。这也是为什么他能以hook直接进行状态管理的原因。

你还在用redux吗?太麻烦了!useModel了解下?umi4.x推出的hook,useModel,但是只能在umi用

useModel的使用方法。

  1. 在src文件夹下建立一个 models 文件夹。
  2. 然后以 (ts/js) 文件为单位建立公共存储空间,文件名既是公共存储空间的名称。文件里面是一个函数,函数返回一个对象。对象里面就是需要被分享到其他页面的公共参数。
  3. 在需要使用公共存储空间的地方把useModel引入。然后指定需引入存储空间名称,结构其内容。结束。

文字表述有点抽象,小问题,三个步骤,我们用图再描述一下。

你还在用redux吗?太麻烦了!useModel了解下?umi4.x推出的hook,useModel,但是只能在umi用

你还在用redux吗?太麻烦了!useModel了解下?umi4.x推出的hook,useModel,但是只能在umi用

相对于之前的redux和dva。是不是方便了超多!!!

你还在用redux吗?太麻烦了!useModel了解下?umi4.x推出的hook,useModel,但是只能在umi用

useModel需要注意的是

这里有一个很狗血的问题是,假设存储空间A返回的对象里面包含{a,fun1,b,fun2}四个内容。

然后你在页面A调用fun1,去修改存储空间A的a变量(a变量没有在当前页面使用)。

页面也会重新渲染。

 //这种写法是会重新渲染当前页面的,哪怕被修改的state与当前页面无关
 const { stateName } = useModel("modelName");

例子如下

你还在用redux吗?太麻烦了!useModel了解下?umi4.x推出的hook,useModel,但是只能在umi用

你还在用redux吗?太麻烦了!useModel了解下?umi4.x推出的hook,useModel,但是只能在umi用

你还在用redux吗?太麻烦了!useModel了解下?umi4.x推出的hook,useModel,但是只能在umi用

总结:如果使用useModel中的某一个存储空间,不论使用的当前存储空间中的哪一个数据,当前存储空间的其他数据发生变化时,页面都会重新渲染

你还在用redux吗?太麻烦了!useModel了解下?umi4.x推出的hook,useModel,但是只能在umi用

解决方案

usemodel有第二个参数,是个函数,用于标记当前页面所需要使用的变量

 //这种写法如果被修改state与当前页面无关是不会重新渲染当前页面的
  const { increment } = useModel("initialState", (model: any) => ({
    increment: model.increment,
  }));

你还在用redux吗?太麻烦了!useModel了解下?umi4.x推出的hook,useModel,但是只能在umi用

你还在用redux吗?太麻烦了!useModel了解下?umi4.x推出的hook,useModel,但是只能在umi用

你还在用redux吗?太麻烦了!useModel了解下?umi4.x推出的hook,useModel,但是只能在umi用

umi4.x里如何配置useModel

1.安装依赖

pnpm i @umijs/plugins/dist/initial-state @umijs/plugins/dist/model

2.在.umirc.st(或config.ts)配置
 plugins: [
    '@umijs/plugins/dist/initial-state',
    '@umijs/plugins/dist/model',
  ],
  initialState: {}, 
  model: {},
3.然后按照上方“useModel的使用方法”的流程去做就好啦。

demo(下方)

umi配置useModel的github demo

如何在除umi以外项目使用(以Taro为例)

核心流程

  1. 复制useModel源码到想用useModel的项目中去。修改下源码(后面会描述如何修改,包会的)。
  2. 然后手动引出修改后的useModel源码里的一个叫做dataflowProvider的函数。把项目的入口组件(比如App.js)传进去。
  3. 然后,按上文的 “useModel的使用方法” 去使用就好啦!

useModel的源码没有很复杂,只有寥寥三个文件,一个导入文件,一个过滤文件,进行核心的useContent处理的其实只有一个文件。在umi配置完useModel以后在项目的.umi文件夹下会出现一个plugin-model文件,这里面存放的就是useModel的源码了。如下图。

你还在用redux吗?太麻烦了!useModel了解下?umi4.x推出的hook,useModel,但是只能在umi用

而我们要在其他项目使用useModel,就需要知道如何把这三个文件安全的复制到其他项目里去,修修改改之后进行使用。

如果一步一步告诉大家哪几个地方需要修改,显然那样太麻烦,大家看起来也会很费劲。所以,源码相关的整合,我已经给大家整合到一个文件里处理干净了。(要个点赞收藏关注的不过分吧--狗头.jpg)

// @ts-nocheck
// This file is generated by Umi automatically
// DO NOT CHANGE IT MANUALLY!
// @ts-ignore
import { models as rawModels } from '@/models';
import React, { useContext, useEffect, useMemo, useRef, useState } from "react";

function isEqual(a: any, b: any) {
  if (a === b) return true;

  if (a && b && typeof a === 'object' && typeof b === 'object') {
      if (a.constructor !== b.constructor) return false;

      let length, i;
      if (Array.isArray(a)) {
          length = a.length;
          if (length != b.length) return false;
          for (i = length; i-- !== 0; ) if (!isEqual(a[i], b[i])) return false;
          return true;
      }

      if (a.constructor === RegExp) return a.source === b.source && a.flags === b.flags;
      if (a.valueOf !== Object.prototype.valueOf) return a.valueOf() === b.valueOf();
      if (a.toString !== Object.prototype.toString) return a.toString() === b.toString();

      const keys = Object.keys(a);
      length = keys.length;
      if (length !== Object.keys(b).length) return false;

      for (i = length; i-- !== 0; ) if (!Object.prototype.hasOwnProperty.call(b, keys[i])) return false;

      for (i = length; i-- !== 0; ) {
          const key = keys[i];

          if (!isEqual(a[key], b[key])) return false;
      }

      return true;
  }

  // true if both NaN, false otherwise
  return a !== a && b !== b;
}


type Models = typeof rawModels;

type GetNamespaces<M> = {
  [K in keyof M]: M[K] extends { namespace: string }
    ? M[K]["namespace"]
    : never;
}[keyof M];

type Namespaces = GetNamespaces<Models>;

// @ts-ignore
const Context = React.createContext<{ dispatcher: Dispatcher }>(null);

class Dispatcher {
  callbacks: Record<Namespaces, Set<Function>> = {};
  data: Record<Namespaces, unknown> = {};
  update = (namespace: Namespaces) => {
    if (this.callbacks[namespace]) {
      this.callbacks[namespace].forEach((cb) => {
        try {
          const data = this.data[namespace];
          cb(data);
        } catch (e) {
          cb(undefined);
        }
      });
    }
  };
}

interface ExecutorProps {
  hook: () => any;
  onUpdate: (val: any) => void;
  namespace: string;
}

function Executor(props: ExecutorProps) {
  const { hook, onUpdate, namespace } = props;

  const updateRef = useRef(onUpdate);
  const initialLoad = useRef(false);

  let data: any;
  try {
    data = hook();
  } catch (e) {
    console.error(
      `plugin-model: Invoking '${namespace || "unknown"}' model failed:`,
      e
    );
  }

  // 首次执行时立刻返回初始值
  useMemo(() => {
    updateRef.current(data);
  }, []);

  // React 16.13 后 update 函数用 useEffect 包裹
  useEffect(() => {
    if (initialLoad.current) {
      updateRef.current(data);
    } else {
      initialLoad.current = true;
    }
  });

  return null;
}

const dispatcher = new Dispatcher();

export function Provider(props: {
  models: Record<string, any>;
  children: React.ReactNode;
}) {
  return (
    <Context.Provider value={{ dispatcher }}>
      {Object.keys(props.models).map((namespace) => {
        return (
          <Executor
            key={namespace}
            hook={props.models[namespace]}
            namespace={namespace}
            onUpdate={(val) => {
              dispatcher.data[namespace] = val;
              dispatcher.update(namespace);
            }}
          />
        );
      })}
      {props.children}
    </Context.Provider>
  );
}

type GetModelByNamespace<M, N> = {
  [K in keyof M]: M[K] extends { namespace: string; model: unknown }
    ? M[K]["namespace"] extends N
      ? M[K]["model"] extends (...args: any) => any
        ? ReturnType<M[K]["model"]>
        : never
      : never
    : never;
}[keyof M];

type Model<N> = GetModelByNamespace<Models, N>;
type Selector<N, S> = (model: Model<N>) => S;

type SelectedModel<N, T> = T extends (...args: any) => any
  ? ReturnType<NonNullable<T>>
  : Model<N>;

export function useModel<N extends Namespaces>(namespace: N): Model<N>;

export function useModel<N extends Namespaces, S>(
  namespace: N,
  selector: Selector<N, S>
): SelectedModel<N, typeof selector>;

export function useModel<N extends Namespaces, S>(
  namespace: N,
  selector?: Selector<N, S>
): SelectedModel<N, typeof selector> {
  const { dispatcher } = useContext<{ dispatcher: Dispatcher }>(Context);
  const selectorRef = useRef(selector);
  selectorRef.current = selector;
  const [state, setState] = useState(() =>
    selectorRef.current
      ? selectorRef.current(dispatcher.data[namespace])
      : dispatcher.data[namespace]
  );
  const stateRef = useRef<any>(state);
  stateRef.current = state;

  const isMount = useRef(false);
  useEffect(() => {
    isMount.current = true;
    return () => {
      isMount.current = false;
    };
  }, []);

  useEffect(() => {
    const handler = (data: any) => {
      if (!isMount.current) {
        // 如果 handler 执行过程中,组件被卸载了,则强制更新全局 data
        // TODO: 需要加个 example 测试
        setTimeout(() => {
          dispatcher.data[namespace] = data;
          dispatcher.update(namespace);
        });
      } else {
        const currentState = selectorRef.current
          ? selectorRef.current(data)
          : data;
        const previousState = stateRef.current;
        if (!isEqual(currentState, previousState)) {
          // 避免 currentState 拿到的数据是老的,从而导致 isEqual 比对逻辑有问题
          stateRef.current = currentState;
          setState(currentState);
        }
      }
    };

    dispatcher.callbacks[namespace] ||= new Set() as any; // rawModels 是 umi 动态生成的文件,导致前面 callback[namespace] 的类型无法推导出来,所以用 as any 来忽略掉
    dispatcher.callbacks[namespace].add(handler);
    dispatcher.update(namespace);

    return () => {
      dispatcher.callbacks[namespace].delete(handler);
    };
  }, [namespace]);

  return state;
}

function ProviderWrapper(props: any) {
  const models = React.useMemo(() => {
    return Object.keys(rawModels).reduce((memo, key) => {
      memo[rawModels[key].namespace] = rawModels[key].model;
      return memo;
    }, {});
  }, []);
  return (
    <Provider models={models} {...props}>
      {props.children}
    </Provider>
  );
}

export function dataflowProvider(container, opts?:any) {
  return <ProviderWrapper {...opts}>{container}</ProviderWrapper>;
}

你还在用redux吗?太麻烦了!useModel了解下?umi4.x推出的hook,useModel,但是只能在umi用

给Taro嵌入useModel

本次demo版本 taro 3.6.32 node 16+ pnpm 7.27.0

1.在src目录下建立useModel文件,然后把上方我整合过的源码整个复制进去。

你还在用redux吗?太麻烦了!useModel了解下?umi4.x推出的hook,useModel,但是只能在umi用

2.在入口文件引入使用(注意千万不要直接把App这个组件传进去,把入口文件内部那个动态的children传入)

你还在用redux吗?太麻烦了!useModel了解下?umi4.x推出的hook,useModel,但是只能在umi用

3.在src文件夹下建立models文件夹,index文件用于导出全部存储空间。以test存储空间为例

你还在用redux吗?太麻烦了!useModel了解下?umi4.x推出的hook,useModel,但是只能在umi用

你还在用redux吗?太麻烦了!useModel了解下?umi4.x推出的hook,useModel,但是只能在umi用

4.在需要用到当前存储空间的页面使用useModel

你还在用redux吗?太麻烦了!useModel了解下?umi4.x推出的hook,useModel,但是只能在umi用

效果如下

你还在用redux吗?太麻烦了!useModel了解下?umi4.x推出的hook,useModel,但是只能在umi用

demo(下方)

把usemodel嵌入到taro项目中的github demo

你还在用redux吗?太麻烦了!useModel了解下?umi4.x推出的hook,useModel,但是只能在umi用

最后

usemodel可以在vue中用吗?

不能,vue没有useContent

useModel可以在vite中用吗?

可以的。使用方法基本一致

useModel这样嵌入有什么问题吗。

基本不会有,useModel本质上就是使用useContent建立的状态管理,整个源码甚至都没有使用除React以外的其他任何一个第三方依赖,只要是支持useContent的react版本就没什么问题。

如果对你有用的话 你还在用redux吗?太麻烦了!useModel了解下?umi4.x推出的hook,useModel,但是只能在umi用

留个三连最好还能介绍个工作啥的。

个人求职

我需要一份工作,地点杭州,期望16+

有可以内推或者看上我的大佬。请联系我 wx:XXF1096032096

此留言尚在,就一直在求职ing

你还在用redux吗?太麻烦了!useModel了解下?umi4.x推出的hook,useModel,但是只能在umi用

转载自:https://juejin.cn/post/7405147618660843532
评论
请登录