前段时间撸了个效率工具,基本能够应对业务上相对复杂的场景,目前相关业务代码已上线几个月,运行良好
-
业务比较复杂且上下牵扯较多时,通常接口函数到处调用,一会这需要 loading,那需要 await 的,想着就麻烦
-
而且 mobx 对于异步后(await)的 action 如无法感知,必须手动包上 runInAction,让我吐一口血
-
再者一个 api 通常要用 try catch finally 走一遍,书写麻烦,用不好还有可能引起多次 rerender
-
传统书写方式要管理的东西太多,不能很好的专注于业务,容错代码一大堆
没法办,程序员就是懒……所以有个这款 Loadx
效率工具
-
方便对 api 的组织和管理
-
方便 error 容错、finally 和后续 action 操作等的书写,专注于业务
-
并发和串行请求智能 loading,统一 rerender
-
方便对是否 loading,预 loading 的初始化配置,以及动态配置
-
支持 Class Component 和 hooks
-
方便的 type 提示和各种书写容错
-
其实最核心的是利用了 mobx 提供的 api createAtom
-
看过我之前的 mobx 源码解读系列 文章,应该对这个 api 或者
Atom
类有印象,这就mobx
的订阅-发布的原子,所有的 observableValue 等都是基于这个做的 -
其中两个核心方法:
reportObserved
和reportChanged
-
reportObserved
:访问“自己”时向订阅者报告 -
reportChanged
:改变“自己”时像订阅者报告
- 如果我们自己实现一套订阅-发布中的“发布机制”的话(订阅机制当然是 mobx 劫持 render 触发 forceupdate)啦,这就是控制 loading 与否的关键
-
维护一个 requests 的队列,当有接口来时入栈,请求完出栈,查看 request 的数量就知道当前是否该处于 loading 状态
-
与
atom
结合的话,嘿嘿
-
request length 0 -> 1:开始 loading,调用
atom.reportChanged
-
reqeust length 1 -> 0:结束 loading,也调用
atom.reportChanged
-
注意 length 1 -> n 或 n -> 1 时,都不调用,因为还在 loading 嘛,这就做到了批量处理 loading,一次性 rerender
-
获取 loading 状态时:调用
atom.reportObserved
-
太麻烦了,帮我把 try catch 和 runInAction 封装了吧。好的!
-
但毕竟要在 action 中执行,那就返回个函数吧
- 架子
export interface LoadxConfig {
name?: string;
requests?: Promise<any>[];
}
export class Loadx {
name: string;
private atom: IAtom;
requests: Promise[] = [];
constructor(config?: LoadxConfig) {
const {
name = 'loadx',
requests,
} = config || {};
this.name = name;
this.atom = createAtom(name);
// new 时预填充 request
requests && requests.forEach((p) => this.load(p));
}
get loading() {
// 获取状态时,往上报告
this.atom.reportObserved();
return !!this.requests.length;
}
// 单个 request 入栈
load(request: Promise) {
const { length: preLen } = this.requests;
const thenable = Promise.resolve(request);
this.requests.push(thenable);
// 0 -> 1
if (!preLen) {
this.atom.reportChanged();
}
return thenable
.then(effect => {
let res = effect;
runInAction(() => {
// 处理业务上返回的 action 函数
typeof effect === "function" && (res = effect.apply(this));
// 出栈
this.finish(thenable);
});
return res;
})
.catch(err => {
runInAction(() => {
this.finish(thenable);
});
return Promise.reject(finalErr);
});
}
private finish(promise: Promise) {
this.requests.splice(this.requests.indexOf(promise), 1);
// 1 -> 0
if (!this.requests.length) {
this.atom.reportChanged();
}
}
}
- 使用
class Store {
private loadx = new Loadx();
count = 0;
// 为避免外部直接调用 loadx,以及在 react render 层面注册对该 atom 的依赖
@computed
get loading() {
return this.loadx.loading;
}
_getCount() {
const count = await api();
// 返回 action fn
return () => {
this.count = count;
}
}
getCount() {
// 关联
return this.loadx.load(action(this._getCount));
}
}
const store = new Store();
autorun(r => {
console.log(store.loading, store.count);
});
store.getCount();
凭啥我还要主动去调用 loadx.load 关联下。是的,封装了!像 @action 一样调用不香么
- 我们使用 mobx 的造神装饰器:createDecoratorForEnhancer,不清楚的小伙伴可以看我之前文章,上面还有简化版的小例子
class Loadx {
/**
* 像 mobx.action 一样支持两种装饰写法
* @action(ActionConfig) fn
* @action fn
*/
static action = createPropDecorator(function (target, prop, descriptor, args) {
// @action fn() {}
// 注意这种写法在解构调用时会出现 this bind 问题哦,相信大家遇到过,一起封装了
if (descriptor) {
return {
configurable: true,
enumerable: false,
get() {
const fn = descriptor.value || (descriptor as any).initializer.call(this);
// 即 Object.defineProperty
addHiddenProp(this, prop, createLoadxFn(fn, args[0], this));
return this[prop];
},
set() {}
};
} else {
// @action fn = () => {}
Object.defineProperty(target, prop, {
configurable: true,
enumerable: false,
get() {},
set(fn) {
addHiddenProp(this, prop, createLoadxFn(fn, args[0], this));
}
});
}
});
}
- 然后看下装饰器调用的核心:
createLoadxFn
export interface ActionConfig {
loadx?: string | Loadx; // 关联的 store Loadx
action?: string; // 自定义 return action name
}
export function createLoadxFn(fn, config: ActionConfig = {}, context: any = null) {
const id = getUid();
return function (this: any, ...args: any[]) {
const that = context || this;
const { loadx: lName = "", action } = config;
// 根据 name 从 store 实例中找 loadx 实例
const loadx:Loadx = that[lName];
// 在 action 中执行 store 的方法,返回 Promise
const req = runInAction(action, () => fn.apply(that, args));
return loadx.load(req);
};
}
- 使用
class Store {
private loadx = new Loadx();
@Loadx.action
getUser() {
// 批量处理:会等 Promise.all 以及后续 effect action 后放开 loading
await getPermission();
const [name, age] = await Promise.all([nameApi(), ageApi()]);
// 返回 action fn
return () => {
if (this.perm) {
this.name = name;
this.age = age;
}
}
}
@Loadx.action({
// 不传也行,默认容错
// name: 'loadx'
})
getPermission = () => {
const perm = await permApi();
return () => {
this.perm = perm;
}
}
}
new Store().getUser();
- 美化
bind
当然如果是动态方法啥的,没法用装饰器,只能手动 bind,那么我们美化下吧
class Loadx {
bind<T extends FnType>(fn: T, context?: any);
bind<T extends FnType>(fn: T, config = {}, context: any = null
) {
// 不传 config
if (!isPlainObject(config)) {
context = config;
config = {};
}
return createLoadxFn(fn, { ...config, loadx: this }, context);
}
// 使用
class Store {
getCount = this.loadx.bind(this._getCount, this);
_getCount() {
const count = await api();
return () => {
this.count = count;
}
}
}
const store = new Store();
store.getGender = store.loadx.bind(getGenderFromOtherPlace, store);
try catch finally
-
由于 catch 都被拆带外面去了,所以不能在写在 action fn 中
-
这也是我们想要的嘛,简洁书写,让我们更关注业务本身,而不是必须把健壮性给塞进去。所以,封装了吧!
export interface ActionConfig {
loadx?: string | Loadx; // 关联 Loadx
action?: string; // action name
// 通过配置传进来
onError?: (err: any, ...originalArgs: any[]) => void | Promise<void>; // error 回调
onComplete?: (resOrErr?: any, ...originalArgs: any[]) => void | Promise<void>; // complete 回调
}
export function createLoadxFn(fn config: ActionConfig = {}, context: any = null) {
return function (this: any, ...args: any[]) {
const that = context || this;
const {
loadx: lName = "",
action,
onComplete,
onError
} = config;
// ...
// 从装饰器中获取参数,并挂到 req 上
onError && (req._onError = onError.bind(that));
onComplete && (req._onComplete = onComplete.bind(that));
return loadx.load(req);
};
}
class Loadx {
load<T>(request: T): LoadxLoadType<T> {
// 获取 config
const { _onError, _onComplete } = request;
const { length: preLen } = this.requests;
// ...
return thenable
.then(effect => {
let res = effect;
runInAction(() => {
typeof effect === "function" && (res = effect.apply(this));
_onComplete && _onComplete(res, ..._args);
this.finish(thenable);
});
return res;
})
.catch(err => {
const finalErr = err;
runInAction(() => {
_onError && _onError(finalErr, ..._args);
_onComplete && _onComplete(finalErr, ..._args);
this.finish(thenable);
});
return Promise.reject(finalErr);
});
}
}
// 使用
class Store {
@Loadx.action({
onError(this: Store, err) {
console.log("onError", this, err);
},
onComplete(_resOrErr) {}
})
getUser() {
// ...
}
}
- 控制
loading
-
有时我想提前开启 loading 为 true 的状态,比如进入 page 的初始化 loading
-
或者我在某些情况下,我不想去监听 loading,而且还是动态设置呢
export interface ActionConfig {
loadx?: string | Loadx;
action?: string;
// 继续配置参数
observe?: boolean; // 设置是否监听 loading 变化,默认为 true
preload?: boolean | number;
onError?: (err: any, ...originalArgs: any[]) => void | Promise<void>;
onComplete?: (resOrErr?: any, ...originalArgs: any[]) => void | Promise<void>;
}
class Loadx {
setConfig(config: ActionConfig) {
// 根据传的参来改变
if (has(config, "observe")) {
this.observe = config.observe;
}
if (has(config, "preload")) {
this.preload = config.preload;
if (!this.preload) return;
this.startPreload();
}
}
private startPreload() {
// 使用 setTimeout 模拟预发起了一个请求
const loadReq = new Promise((resolve) => {
setTimeout(resolve, this.preload);
this.preloadFn = () => {
resolve();
this.preload = false;
this.preloadFn = null;
};
});
this.load(loadReq);
}
private finish(promise: Promise) {
this.requests.splice(this.requests.indexOf(promise), 1);
if (!this.requests.length) {
// 监听时才上报
this.observe && this.atom.reportChanged();
}
}
}
// 使用
class Store {
@Loadx.action
getUser() {
this.loadx.setConfig({ observe: false });
const count = await api();
return () => {
this.count = count;
}
}
}
- 智能的 type 提示,写法兼容
-
现在 store 方法都以返回的函数形式,组件里使用起来比较迷惑,会看到:
Promise<() => void>
-
又有些人比较倔强,就不想返回 action fn,非要自己写 runInAction
-
再者,有人又想以 return actionFn 形式,还想返回 await api 之后的数据。安排!
class Store {
@Loadx.action
getUser() {
const perm = await getPermission();
const [name, age] = await Promise.all([nameApi(), ageApi()]);
// 返回 action fn
return () => {
if (this.perm) {
this.name = name;
this.age = age;
}
}
}
@Loadx.action
getPermission = (flag = 1) => {
const perm: boolean = await permApi(flag);
// 1:纯 api
// return perm;
// 2:为啥你如此倔强
// runInAction(() => {
// this.perm = perm;
// })
// 3
return () => {
this.perm = perm;
return perm;
}
}
}
class Loadx {
load<T>(request: T): LoadxLoadType<T> {
// ...
return thenable
.then(effect => {
let res = effect;
runInAction(() => {
// 不是函数直接放回,是函数将函数的执行结果返回
typeof effect === "function" && (res = effect.apply(this));
_onComplete && _onComplete(res, ..._args);
this.finish(thenable);
});
return res;
})
.catch(err => {
// ...
});
}
}
最后再操作一波 types。至于具体的 types,不是本文重点,我会单独抽到 types 技巧汇总里面的文章,敬请期待
export type UnFnReturn<T> = T extends (...args: any[]) => infer R ? R : T;
export type LoadxLoadFnType<T> = T extends (...args: any[]) => Promise<infer R> | Generator<any, infer R>
? (...args: Parameters<T>) => Promise<UnFnReturn<R>>
: T;
// 再针对 store 中返回是 Promise<fn> 的方法,限制一下
export type LoadxStore<T extends Record<string, any>> = {
[K in keyof T]: LoadxLoadFnType<T[K]>;
};
// 使用
@observer
class CC extends Component<{store: LoadxStore<Store>}> {
render() {
const { getPermission } = this.props.store;
// type:
// const getPermission: (flag: string) => Promise<boolean>
return <div></div>
}
}
都完善差不多了,该 hooks 出场了,其实就是 useLocalStore,只是暴露了 api 而已
export function useLoadx(
initializer: (source: P & { loadx: Loadx }) => T,
loadxConfig?: ActionConfig,
current?: P
): LoadxLocaleStore<T> {
// create loadx for store
const loadx = useMemo(() => new Loadx(loadxConfig), []);
current && (current.loadx = loadx);
const store = useLocalStore(initializer, current);
return {
store,
loading: loadx.loading,
bind: (fn: T, config = {}, context: any = null) => {
// 不传 config
if (!isPlainObject(config)) {
context = config;
config = {};
}
return createLoadxFn(
fn,
{ ...config, loadx },
context || store
);
},
setConfig: (config) => loadx.setConfig(config)
};
}
// 使用
const Fc = () => {
const { store, loading, bind } = useLoadx(() => ({
data: { name: "empty" }
}));
const getDataWithLoading = bind(async () => {
const res = await wait(1000, { name: 'lawler', age: 20 });
return () => {
store.data = res;
};
});
const { data } = store;
return useObserver(() => (
<div>
<div>
{`loading: ${loading}, data: ${data.name}`}
</div>
<button onClick={getDataWithLoading}>FC loadx</button>
</div>
));
};
export default observer(Fc);
-
源码获取:Loadx source
-
如果反响好的话可以考虑开源(嗯,装13而已),同时欢迎 folk 完善
-
另外,我是写完这个 util 才发现 github 已经有位大佬完成了类似的工具 mobx-task,看了下,和他源码以及使用姿势完全不同,但同样可作为参考
-
喜欢的小伙伴,记得留下你的小 ❤️ 哦~