你还在用redux吗?太麻烦了!useModel了解下?umi4.x推出的hook,useModel,但是只能在umi用
写最仔细的文章,适配尽可能多的读者
umi在4.x的版本里面推出来一个超精简的状态管理工具。使用的复杂度相对于redux来说方便的多。但可惜的是,它并不是第三方依赖包。做不到在哪想用就直接install来使用。
所以,本文就来告诉大家如何在其他项目使用useModel,本文以 Taro(react) 版为例。
useModel介绍
官方给他的介绍是:它是一种基于 hooks
范式的轻量级数据管理方案,可以在 umi 项目中管理全局的共享数据。
底层使用是useContent
个人觉得useModel对于其他公共状态管理的优点是:以hook的形式去进行的状态管理,真的很方便。相对于传统项目的redux和 umi3.x 推出的dva来说。useModel把状态管理简化到了当前技术框架的极致。
useModel源码里使用的是useContent这个hook进行的状态管理。这也是为什么他能以hook直接进行状态管理的原因。
useModel的使用方法。
- 在src文件夹下建立一个 models 文件夹。
- 然后以 (ts/js) 文件为单位建立公共存储空间,文件名既是公共存储空间的名称。文件里面是一个函数,函数返回一个对象。对象里面就是需要被分享到其他页面的公共参数。
- 在需要使用公共存储空间的地方把useModel引入。然后指定需引入存储空间名称,结构其内容。结束。
文字表述有点抽象,小问题,三个步骤,我们用图再描述一下。
相对于之前的redux和dva。是不是方便了超多!!!
useModel需要注意的是
这里有一个很狗血的问题是,假设存储空间A返回的对象里面包含{a,fun1,b,fun2}四个内容。
然后你在页面A调用fun1,去修改存储空间A的a变量(a变量没有在当前页面使用)。
页面也会重新渲染。
//这种写法是会重新渲染当前页面的,哪怕被修改的state与当前页面无关
const { stateName } = useModel("modelName");
例子如下
总结:如果使用useModel中的某一个存储空间,不论使用的当前存储空间中的哪一个数据,当前存储空间的其他数据发生变化时,页面都会重新渲染
解决方案
usemodel有第二个参数,是个函数,用于标记当前页面所需要使用的变量
//这种写法如果被修改state与当前页面无关是不会重新渲染当前页面的
const { increment } = useModel("initialState", (model: any) => ({
increment: model.increment,
}));
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以外项目使用(以Taro为例)
核心流程
- 复制useModel源码到想用useModel的项目中去。修改下源码(后面会描述如何修改,包会的)。
- 然后手动引出修改后的useModel源码里的一个叫做dataflowProvider的函数。把项目的入口组件(比如App.js)传进去。
- 然后,按上文的 “useModel的使用方法” 去使用就好啦!
useModel的源码没有很复杂,只有寥寥三个文件,一个导入文件,一个过滤文件,进行核心的useContent处理的其实只有一个文件。在umi配置完useModel以后在项目的.umi文件夹下会出现一个plugin-model文件,这里面存放的就是useModel的源码了。如下图。
而我们要在其他项目使用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>;
}
给Taro嵌入useModel
本次demo版本 taro 3.6.32 node 16+ pnpm 7.27.0
1.在src目录下建立useModel文件,然后把上方我整合过的源码整个复制进去。
2.在入口文件引入使用(注意千万不要直接把App这个组件传进去,把入口文件内部那个动态的children传入)
3.在src文件夹下建立models文件夹,index文件用于导出全部存储空间。以test存储空间为例
4.在需要用到当前存储空间的页面使用useModel
效果如下
demo(下方)
把usemodel嵌入到taro项目中的github demo
最后
usemodel可以在vue中用吗?
不能,vue没有useContent
useModel可以在vite中用吗?
可以的。使用方法基本一致
useModel这样嵌入有什么问题吗。
基本不会有,useModel本质上就是使用useContent建立的状态管理,整个源码甚至都没有使用除React以外的其他任何一个第三方依赖,只要是支持useContent的react版本就没什么问题。
如果对你有用的话
留个三连最好还能介绍个工作啥的。
个人求职
我需要一份工作,地点杭州,期望16+
有可以内推或者看上我的大佬。请联系我 wx:XXF1096032096
此留言尚在,就一直在求职ing
转载自:https://juejin.cn/post/7405147618660843532